From cc8677ae3731232cf8c9b9645b85c1dc905921f8 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sat, 29 Aug 2026 20:10:54 +0200 Subject: [PATCH] feat(servers): add server deletion modal, API endpoint, and delete button to header --- .../[serverId]/channels/[channelId]/page.tsx | 26 ++-- app/api/servers/[serverId]/route.ts | 52 +++++++ components/layout/AppHeader.tsx | 42 ++++-- components/layout/ServerSettingsMenu.tsx | 94 +++++++++++++ components/modals/CreateServerModal.tsx | 10 +- components/modals/DeleteServerModal.tsx | 128 ++++++++++++++++++ lib/services/server.service.ts | 25 +++- 7 files changed, 347 insertions(+), 30 deletions(-) create mode 100644 app/api/servers/[serverId]/route.ts create mode 100644 components/layout/ServerSettingsMenu.tsx create mode 100644 components/modals/DeleteServerModal.tsx diff --git a/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx b/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx index 83c49a4..741f7cf 100644 --- a/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx +++ b/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx @@ -1,6 +1,6 @@ /** * @file app/servers/[serverId]/channels/[channelId]/page.tsx - * @description Page component for viewing a specific channel and its messages. + * @description Dynamic page component for displaying a specific channel within a server, including its messages and chat input. */ import { redirect } from "next/navigation"; @@ -9,33 +9,39 @@ import { ChatInput } from "@/components/chat/ChatInput"; import { ChatMessages } from "@/components/chat/ChatMessages"; import { getChannelById } from "@/lib/services/channel.service"; import { getChannelMessages } from "@/lib/services/message.service"; +import { getServerById } from "@/lib/services/server.service"; /** - * Server component that fetches and renders a channel's details and message feed based on route parameters. + * Renders the channel view by fetching server, channel, and message details in parallel based on route parameters. * * @param {Object} props - The component props. - * @param {Promise<{ channelId: string }>} props.params - Async route parameters containing the channel ID. - * @returns {Promise} The rendered channel page view or triggers a 404 notFound error. + * @param {Promise<{ serverId: string; channelId: string }>} props.params - A promise resolving to the route parameters containing serverId and channelId. + * @returns {Promise} The rendered channel page interface. */ export default async function ChannelPage({ params, }: { - params: Promise<{ channelId: string }>; + params: Promise<{ serverId: string; channelId: string }>; }) { - const { channelId } = await params; + const { serverId, channelId } = await params; - // Parallel loading of channel data and messages - const [channel, channelMessages] = await Promise.all([ + // Parallel loading of server, channel, and messages + const [server, channel, channelMessages] = await Promise.all([ + getServerById(serverId), getChannelById(channelId), getChannelMessages(channelId), ]); - if (!channel) redirect("/"); + if (!channel || !server) redirect("/"); return (
{/* Header */} - + {/* Messages Feed */} diff --git a/app/api/servers/[serverId]/route.ts b/app/api/servers/[serverId]/route.ts new file mode 100644 index 0000000..572bf1d --- /dev/null +++ b/app/api/servers/[serverId]/route.ts @@ -0,0 +1,52 @@ +/** + * @file app/api/servers/[serverId]/route.ts + * @description API route handler for deleting a server. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { servers } from "@/db/schema"; +import { and, eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; + +/** + * Handles DELETE requests to remove a server if the requesting user is the owner. + * + * @param {Request} req - The incoming HTTP request. + * @param {Object} context - Route parameters context. + * @param {Promise<{ serverId: string }>} context.params - Async route parameters containing the `serverId`. + * @returns {Promise} A JSON response with the deleted server data on success, or an appropriate error response. + */ +export async function DELETE( + req: Request, + { params }: { params: Promise<{ serverId: string }> }, +) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + const { serverId } = await params; + + // Delete the server only if the current user is the OWNER + const [deletedServer] = await db + .delete(servers) + .where( + and(eq(servers.id, serverId), eq(servers.ownerId, session.user.id)), + ) + .returning(); + + if (!deletedServer) { + return new NextResponse("Server not found or forbidden", { status: 404 }); + } + + revalidatePath("/", "layout"); + return NextResponse.json(deletedServer); + } catch (error) { + console.error("[SERVER_DELETE]", error); + return new NextResponse("Internal Error", { status: 500 }); + } +} diff --git a/components/layout/AppHeader.tsx b/components/layout/AppHeader.tsx index 3a017c8..045e039 100644 --- a/components/layout/AppHeader.tsx +++ b/components/layout/AppHeader.tsx @@ -7,6 +7,7 @@ import { useSidebarStore } from "@/lib/stores/useSidebarStore"; import { PanelLeftOpen, PanelLeftClose, Users, Hash } from "lucide-react"; +import { ServerSettingsMenu } from "./ServerSettingsMenu"; /** * Properties for the AppHeader component. @@ -14,27 +15,34 @@ import { PanelLeftOpen, PanelLeftClose, Users, Hash } from "lucide-react"; * @interface AppHeaderProps * @property {string} [title] - Optional channel or page title to display in the header. * @property {boolean} [showMembersButton=false] - Flag indicating whether to display the member list toggle button. + * @property {{ id: string; name: string }} [server] - Optional server details to enable server settings & delete functionality. */ interface AppHeaderProps { title?: string; showMembersButton?: boolean; + server?: { + id: string; + name: string; + }; } /** - * Renders the application header bar with navigation controls, dynamic page titles, and member list toggle capability. + * Renders the application header bar with navigation controls, dynamic page titles, server settings, and member list toggle capability. * * @param {AppHeaderProps} props - The component props. * @param {string} [props.title] - Optional channel or page title to display. * @param {boolean} [props.showMembersButton=false] - Whether to show the button toggling the right sidebar/member panel. + * @param {{ id: string; name: string }} [props.server] - Optional server object containing id and name for settings menu. * @returns {JSX.Element} The header component visual structure. */ export function AppHeader({ title, showMembersButton = false, + server, }: AppHeaderProps) { const { isNavOpen, toggleNav, toggleMembers } = useSidebarStore(); - const hasContent = !isNavOpen || !!title || showMembersButton; + const hasContent = !isNavOpen || !!title || showMembersButton || !!server; return (
- {/* Button for the member bar */} - {showMembersButton && ( - - )} + {/* Right Action Buttons */} +
+ {/* Server Settings Menu (contains the Delete Server action) */} + {server && ( + + )} + + {/* Button for the member bar */} + {showMembersButton && ( + + )} +
); } diff --git a/components/layout/ServerSettingsMenu.tsx b/components/layout/ServerSettingsMenu.tsx new file mode 100644 index 0000000..1745a13 --- /dev/null +++ b/components/layout/ServerSettingsMenu.tsx @@ -0,0 +1,94 @@ +/** + * @file components/layout/ServerSettingsMenu.tsx + * @description Header menu button providing server-level action options like deleting a server. + */ + +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { Settings, Trash2 } from "lucide-react"; +import { DeleteServerModal } from "@/components/modals/DeleteServerModal"; + +/** + * Props for the ServerSettingsMenu component. + * + * @interface ServerSettingsMenuProps + * @property {string} serverId - The unique identifier of the server. + * @property {string} serverName - The name of the server used for display and verification during deletion. + */ +interface ServerSettingsMenuProps { + serverId: string; + serverName: string; +} + +/** + * Renders a dropdown menu button for server settings, including an option to open the server deletion modal. + * + * @param {ServerSettingsMenuProps} props - The component props. + * @param {string} props.serverId - The unique identifier of the server. + * @param {string} props.serverName - The display name of the server. + * @returns {JSX.Element} The rendered server settings dropdown menu and modal component. + */ +export function ServerSettingsMenu({ + serverId, + serverName, +}: ServerSettingsMenuProps) { + const [isOpen, setIsOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const menuRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + /** + * Handles mouse click events outside of the menu container to close the dropdown. + * + * @param {MouseEvent} event - The mouse click event object. + */ + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ {/* Icon Button */} + + + {/* Dropdown Menu */} + {isOpen && ( +
+ +
+ )} + + {/* Delete Modal */} + setIsDeleteModalOpen(false)} + /> +
+ ); +} diff --git a/components/modals/CreateServerModal.tsx b/components/modals/CreateServerModal.tsx index 18d9f54..1779701 100644 --- a/components/modals/CreateServerModal.tsx +++ b/components/modals/CreateServerModal.tsx @@ -6,7 +6,6 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { X, Loader2 } from "lucide-react"; import { SERVER_COLOR_CLASSES, @@ -32,7 +31,6 @@ interface CreateServerModalProps { * @returns {JSX.Element | null} The rendered modal component or null when hidden. */ export function CreateServerModal({ isOpen, onClose }: CreateServerModalProps) { - const router = useRouter(); const [name, setName] = useState(""); const [color, setColor] = useState("bg-indigo-500"); const [isLoading, setIsLoading] = useState(false); @@ -83,15 +81,15 @@ export function CreateServerModal({ isOpen, onClose }: CreateServerModalProps) { }; return ( - /* Outer Backdrop: Schließt das Modal bei Klick */ + /* Outer Backdrop */
- {/* Inner Modal Content: Verhindert Event-Bubbling, damit Klicks hier drinnen das Modal NICHT schließen */} + {/* Inner Modal Content */}
e.stopPropagation()} - className="bg-surface border border-surface/50 rounded-2xl w-full max-w-md p-6 shadow-2xl relative animate-in fade-in zoom-in-95 duration-150 cursor-default" + className="bg-surface border border-surface/50 rounded-2xl w-full max-w-md p-6 shadow-2xl relative animate-in fade-in zoom-in-95 duration-150" > + +
+
+ +
+

Delete Server

+
+ +

+ Are you sure you want to delete{" "} + {serverName}? This + action cannot be undone and will permanently remove all channels and + messages. +

+ + {error &&

{error}

} + +
+ + +
+
+
+ ); +} diff --git a/lib/services/server.service.ts b/lib/services/server.service.ts index 16ab768..5280289 100644 --- a/lib/services/server.service.ts +++ b/lib/services/server.service.ts @@ -4,7 +4,7 @@ */ import { db } from "@/db"; -import { members, servers } from "@/db/schema"; +import { members, servers, channels } from "@/db/schema"; import { eq, and } from "drizzle-orm"; import { z } from "zod"; @@ -82,3 +82,26 @@ export async function getUserServers(userId: string) { return []; } } + +/** + * Retrieves a single server by its ID without checking membership. + * + * @param {string} serverId - The unique identifier of the server to retrieve. + * @returns {Promise} The server record or null if not found/invalid ID. + */ +export async function getServerById(serverId: string) { + if (!uuidSchema.safeParse(serverId).success) { + return null; + } + + try { + const server = await db.query.servers.findFirst({ + where: eq(servers.id, serverId), + }); + + return server ?? null; + } catch (error) { + console.error(`Error fetching server by ID ${serverId}:`, error); + return null; + } +}