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 { 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("/");

View file

@ -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("/");

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
* @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 (
<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() {
const { isMembersOpen, closeMembers } = useSidebarStore();
const { activeServer } = useActiveServer();
const desktopSidebarRef = useRef<HTMLElement>(null);
const members = activeServer?.members ?? [];
const [members, setMembers] = useState<User[]>([]);
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 (
<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 (
<>
{isMembersOpen && (
@ -142,7 +91,9 @@ export function MemberSidebar() {
title="Mitgliederliste einklappen"
onClose={closeMembers}
/>
<div className="flex-1 overflow-y-auto">{renderContent()}</div>
<div className="flex-1 overflow-y-auto">
<MemberList members={members} />
</div>
</div>
</aside>
</>

View file

@ -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. */

View file

@ -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<typeof server> =>
Boolean(server),
);
} catch (error) {
console.error(`Error fetching servers for user ${userId}:`, error);
return [];