From c6af4e1565ca06ea4d468d0d72ccea8b83d652d9 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Thu, 17 Sep 2026 04:26:21 +0200 Subject: [PATCH] feat(servers): include members in activeServer payload and remove extra fetch --- app/(app)/servers/[serverId]/layout.tsx | 4 +- app/(app)/servers/[serverId]/page.tsx | 4 +- app/api/servers/[serverId]/members/route.ts | 31 --------- components/layout/MemberSidebar.tsx | 69 +++------------------ lib/context/ServerContext.tsx | 3 +- lib/services/server.service.ts | 37 ++++++++--- 6 files changed, 45 insertions(+), 103 deletions(-) delete mode 100644 app/api/servers/[serverId]/members/route.ts diff --git a/app/(app)/servers/[serverId]/layout.tsx b/app/(app)/servers/[serverId]/layout.tsx index cc7272a..f087302 100644 --- a/app/(app)/servers/[serverId]/layout.tsx +++ b/app/(app)/servers/[serverId]/layout.tsx @@ -4,7 +4,7 @@ */ import { auth } from "@/auth"; -import { getServerWithChannels } from "@/lib/services/server.service"; +import { getUserServer } from "@/lib/services/server.service"; import { ServerStateSync } from "@/lib/providers/ServerStateSync"; import { redirect } from "next/navigation"; @@ -35,7 +35,7 @@ export default async function ServerLayout({ if (!session?.user?.id) redirect("/login"); - const server = await getServerWithChannels(serverId, session.user.id); + const server = await getUserServer(serverId, session.user.id); if (!server) redirect("/"); diff --git a/app/(app)/servers/[serverId]/page.tsx b/app/(app)/servers/[serverId]/page.tsx index 5c9d0a4..b223bcf 100644 --- a/app/(app)/servers/[serverId]/page.tsx +++ b/app/(app)/servers/[serverId]/page.tsx @@ -4,7 +4,7 @@ */ import { auth } from "@/auth"; -import { getServerWithChannels } from "@/lib/services/server.service"; +import { getUserServer } from "@/lib/services/server.service"; import { redirect } from "next/navigation"; export default async function ServerPage({ @@ -17,7 +17,7 @@ export default async function ServerPage({ if (!session?.user?.id) redirect("/login"); - const server = await getServerWithChannels(serverId, session.user.id); + const server = await getUserServer(serverId, session.user.id); if (!server) redirect("/"); diff --git a/app/api/servers/[serverId]/members/route.ts b/app/api/servers/[serverId]/members/route.ts deleted file mode 100644 index b780577..0000000 --- a/app/api/servers/[serverId]/members/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @file app/api/servers/[serverId]/members/route.ts - * @description API route handler for retrieving all members belonging to a specific server. - */ - -import { NextResponse } from "next/server"; -import { getServerMembers } from "@/lib/services/member.service"; - -/** - * Handles HTTP GET requests to fetch members of a server. - * - * @param {Request} req - The incoming HTTP request object. - * @param {Object} context - Route context parameters. - * @param {Promise<{ serverId: string }>} context.params - Asynchronous route parameters containing the serverId. - * @returns {Promise} JSON response containing the list of server members or an error message. - */ -export async function GET( - req: Request, - { params }: { params: Promise<{ serverId: string }> }, -) { - try { - const { serverId } = await params; - const members = await getServerMembers(serverId); - return NextResponse.json(members); - } catch (error) { - return NextResponse.json( - { error: "Failed to fetch server members" }, - { status: 500 }, - ); - } -} diff --git a/components/layout/MemberSidebar.tsx b/components/layout/MemberSidebar.tsx index 65fc26c..98d3042 100644 --- a/components/layout/MemberSidebar.tsx +++ b/components/layout/MemberSidebar.tsx @@ -1,17 +1,16 @@ /** * @file components/layout/MemberSidebar.tsx - * @description Sidebar component fetching and displaying the server member list with responsive overlay support. + * @description Sidebar component displaying the server member list directly from activeServer context. */ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { useSidebarStore } from "@/lib/stores/useSidebarStore"; import { useActiveServer } from "@/lib/context/ServerContext"; import { MemberList } from "@/components/members/MemberList"; -import { X, Loader2 } from "lucide-react"; +import { X } from "lucide-react"; import { clsx } from "clsx"; -import type { User } from "@/db/schema"; /** Props for the MemberHeader component. */ interface MemberHeaderProps { @@ -19,7 +18,7 @@ interface MemberHeaderProps { onClose: () => void; } -/** Header element for the member sidebar with title and close button. */ +/** Header component for the member sidebar with close button. */ function MemberHeader({ title, onClose }: MemberHeaderProps) { return (
@@ -38,52 +37,14 @@ function MemberHeader({ title, onClose }: MemberHeaderProps) { ); } -/** Collapsible sidebar component displaying members of the currently active server. */ +/** Sidebar component displaying active server members with responsive drawer behavior. */ export function MemberSidebar() { const { isMembersOpen, closeMembers } = useSidebarStore(); const { activeServer } = useActiveServer(); const desktopSidebarRef = useRef(null); + const members = activeServer?.members ?? []; - const [members, setMembers] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - /** Fetches members for the active server and manages loading states. */ - useEffect(() => { - if (!activeServer?.id) { - setMembers([]); - return; - } - - const controller = new AbortController(); - - async function fetchMembers() { - try { - setIsLoading(true); - const res = await fetch(`/api/servers/${activeServer?.id}/members`, { - signal: controller.signal, - }); - - if (res.ok) { - const data: User[] = await res.json(); - setMembers(data); - } - } catch (err: unknown) { - if (err instanceof Error && err.name !== "AbortError") { - console.error("Error loading members:", err); - } - } finally { - setIsLoading(false); - } - } - - fetchMembers(); - - return () => { - controller.abort(); - }; - }, [activeServer?.id]); - - /** Closes the members sidebar when clicking outside of it */ + /** Closes the members sidebar when clicking outside of it. */ useEffect(() => { function handleClickOutside(event: MouseEvent) { const target = event.target as HTMLElement; @@ -107,18 +68,6 @@ export function MemberSidebar() { return () => document.removeEventListener("mousedown", handleClickOutside); }, [isMembersOpen, closeMembers]); - /** Renders loading indicator or the member list depending on state. */ - const renderContent = () => { - if (isLoading && members.length === 0) { - return ( -
- -
- ); - } - return ; - }; - return ( <> {isMembersOpen && ( @@ -142,7 +91,9 @@ export function MemberSidebar() { title="Mitgliederliste einklappen" onClose={closeMembers} /> -
{renderContent()}
+
+ +
diff --git a/lib/context/ServerContext.tsx b/lib/context/ServerContext.tsx index 51cff10..ace1f3f 100644 --- a/lib/context/ServerContext.tsx +++ b/lib/context/ServerContext.tsx @@ -6,12 +6,13 @@ "use client"; import { createContext, useContext, useState } from "react"; -import type { Server, Channel, Category } from "@/db/schema"; +import type { Server, Channel, Category, User, Member } from "@/db/schema"; /** Extended server type containing associated channels and categories. */ export type ServerWithChannels = Server & { channels: Channel[]; categories: Category[]; + members: User[]; }; /** Member representation within a server context. */ diff --git a/lib/services/server.service.ts b/lib/services/server.service.ts index 7217cdf..9948134 100644 --- a/lib/services/server.service.ts +++ b/lib/services/server.service.ts @@ -10,8 +10,8 @@ import { z } from "zod"; const uuidSchema = z.uuid(); -/** Retrieves a server along with its channels and categories sorted chronologically if the specified user is a verified member. */ -export async function getServerWithChannels(serverId: string, userId: string) { +/** Retrieves a server along with its channels, categories and members sorted chronologically if the specified user is a verified member. */ +export async function getUserServer(serverId: string, userId: string) { if ( !uuidSchema.safeParse(serverId).success || !uuidSchema.safeParse(userId).success @@ -32,21 +32,30 @@ export async function getServerWithChannels(serverId: string, userId: string) { channels: { orderBy: (channels, { asc }) => [asc(channels.createdAt)], }, - // NEU: Categories mitladen categories: { orderBy: (categories, { asc }) => [asc(categories.createdAt)], }, + members: { + with: { + user: true, + }, + }, }, }); - return server ?? null; + if (!server) return null; + + return { + ...server, + members: server.members.map((m) => m.user).filter(Boolean), + }; } catch (error) { console.error(`Error fetching server ${serverId}:`, error); return null; } } -/** Fetches all servers that the specified user belongs to, including each server's sorted channels and categories list. */ +/** Fetches all servers that the specified user belongs to, including each server's sorted channels, categories, and members. */ export async function getUserServers(userId: string) { if (!uuidSchema.safeParse(userId).success) { return []; @@ -61,18 +70,30 @@ export async function getUserServers(userId: string) { channels: { orderBy: (channels, { asc }) => [asc(channels.createdAt)], }, - // NEU: Categories mitladen categories: { orderBy: (categories, { asc }) => [asc(categories.createdAt)], }, + members: { + with: { + user: true, + }, + }, }, }, }, }); return userMemberships - .map((membership) => membership.server) - .filter(Boolean); + .map((membership) => { + if (!membership.server) return null; + return { + ...membership.server, + members: membership.server.members.map((m) => m.user).filter(Boolean), + }; + }) + .filter((server): server is NonNullable => + Boolean(server), + ); } catch (error) { console.error(`Error fetching servers for user ${userId}:`, error); return [];