feat(servers): include members in activeServer payload and remove extra fetch
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 6m1s

This commit is contained in:
Chneemann 2026-09-17 04:26:21 +02:00
parent 11722ac3e0
commit c6af4e1565
No known key found for this signature in database
6 changed files with 45 additions and 103 deletions

View file

@ -4,7 +4,7 @@
*/ */
import { auth } from "@/auth"; 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 { ServerStateSync } from "@/lib/providers/ServerStateSync";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@ -35,7 +35,7 @@ export default async function ServerLayout({
if (!session?.user?.id) redirect("/login"); 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("/"); if (!server) redirect("/");

View file

@ -4,7 +4,7 @@
*/ */
import { auth } from "@/auth"; import { auth } from "@/auth";
import { getServerWithChannels } from "@/lib/services/server.service"; import { getUserServer } from "@/lib/services/server.service";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function ServerPage({ export default async function ServerPage({
@ -17,7 +17,7 @@ export default async function ServerPage({
if (!session?.user?.id) redirect("/login"); 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("/"); if (!server) redirect("/");

View file

@ -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<NextResponse>} 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 },
);
}
}

View file

@ -1,17 +1,16 @@
/** /**
* @file components/layout/MemberSidebar.tsx * @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"; "use client";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef } from "react";
import { useSidebarStore } from "@/lib/stores/useSidebarStore"; import { useSidebarStore } from "@/lib/stores/useSidebarStore";
import { useActiveServer } from "@/lib/context/ServerContext"; import { useActiveServer } from "@/lib/context/ServerContext";
import { MemberList } from "@/components/members/MemberList"; import { MemberList } from "@/components/members/MemberList";
import { X, Loader2 } from "lucide-react"; import { X } from "lucide-react";
import { clsx } from "clsx"; import { clsx } from "clsx";
import type { User } from "@/db/schema";
/** Props for the MemberHeader component. */ /** Props for the MemberHeader component. */
interface MemberHeaderProps { interface MemberHeaderProps {
@ -19,7 +18,7 @@ interface MemberHeaderProps {
onClose: () => void; 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) { function MemberHeader({ title, onClose }: MemberHeaderProps) {
return ( return (
<div className="h-14 border-b border-surface/50 flex items-center justify-between px-4 shrink-0"> <div className="h-14 border-b border-surface/50 flex items-center justify-between px-4 shrink-0">
@ -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() { export function MemberSidebar() {
const { isMembersOpen, closeMembers } = useSidebarStore(); const { isMembersOpen, closeMembers } = useSidebarStore();
const { activeServer } = useActiveServer(); const { activeServer } = useActiveServer();
const desktopSidebarRef = useRef<HTMLElement>(null); const desktopSidebarRef = useRef<HTMLElement>(null);
const members = activeServer?.members ?? [];
const [members, setMembers] = useState<User[]>([]); /** Closes the members sidebar when clicking outside of it. */
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 */
useEffect(() => { useEffect(() => {
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
@ -107,18 +68,6 @@ export function MemberSidebar() {
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isMembersOpen, closeMembers]); }, [isMembersOpen, closeMembers]);
/** Renders loading indicator or the member list depending on state. */
const renderContent = () => {
if (isLoading && members.length === 0) {
return (
<div className="flex items-center justify-center p-8 text-muted">
<Loader2 className="w-5 h-5 animate-spin" />
</div>
);
}
return <MemberList members={members} />;
};
return ( return (
<> <>
{isMembersOpen && ( {isMembersOpen && (
@ -142,7 +91,9 @@ export function MemberSidebar() {
title="Mitgliederliste einklappen" title="Mitgliederliste einklappen"
onClose={closeMembers} onClose={closeMembers}
/> />
<div className="flex-1 overflow-y-auto">{renderContent()}</div> <div className="flex-1 overflow-y-auto">
<MemberList members={members} />
</div>
</div> </div>
</aside> </aside>
</> </>

View file

@ -6,12 +6,13 @@
"use client"; "use client";
import { createContext, useContext, useState } from "react"; 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. */ /** Extended server type containing associated channels and categories. */
export type ServerWithChannels = Server & { export type ServerWithChannels = Server & {
channels: Channel[]; channels: Channel[];
categories: Category[]; categories: Category[];
members: User[];
}; };
/** Member representation within a server context. */ /** Member representation within a server context. */

View file

@ -10,8 +10,8 @@ import { z } from "zod";
const uuidSchema = z.uuid(); const uuidSchema = z.uuid();
/** Retrieves a server along with its channels and categories sorted chronologically if the specified user is a verified member. */ /** Retrieves a server along with its channels, categories and members sorted chronologically if the specified user is a verified member. */
export async function getServerWithChannels(serverId: string, userId: string) { export async function getUserServer(serverId: string, userId: string) {
if ( if (
!uuidSchema.safeParse(serverId).success || !uuidSchema.safeParse(serverId).success ||
!uuidSchema.safeParse(userId).success !uuidSchema.safeParse(userId).success
@ -32,21 +32,30 @@ export async function getServerWithChannels(serverId: string, userId: string) {
channels: { channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)], orderBy: (channels, { asc }) => [asc(channels.createdAt)],
}, },
// NEU: Categories mitladen
categories: { categories: {
orderBy: (categories, { asc }) => [asc(categories.createdAt)], 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) { } catch (error) {
console.error(`Error fetching server ${serverId}:`, error); console.error(`Error fetching server ${serverId}:`, error);
return null; 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) { export async function getUserServers(userId: string) {
if (!uuidSchema.safeParse(userId).success) { if (!uuidSchema.safeParse(userId).success) {
return []; return [];
@ -61,18 +70,30 @@ export async function getUserServers(userId: string) {
channels: { channels: {
orderBy: (channels, { asc }) => [asc(channels.createdAt)], orderBy: (channels, { asc }) => [asc(channels.createdAt)],
}, },
// NEU: Categories mitladen
categories: { categories: {
orderBy: (categories, { asc }) => [asc(categories.createdAt)], orderBy: (categories, { asc }) => [asc(categories.createdAt)],
}, },
members: {
with: {
user: true,
},
},
}, },
}, },
}, },
}); });
return userMemberships return userMemberships
.map((membership) => membership.server) .map((membership) => {
.filter(Boolean); if (!membership.server) return null;
return {
...membership.server,
members: membership.server.members.map((m) => m.user).filter(Boolean),
};
})
.filter((server): server is NonNullable<typeof server> =>
Boolean(server),
);
} catch (error) { } catch (error) {
console.error(`Error fetching servers for user ${userId}:`, error); console.error(`Error fetching servers for user ${userId}:`, error);
return []; return [];