feat(servers): include members in activeServer payload and remove extra fetch
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 6m1s
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 6m1s
This commit is contained in:
parent
11722ac3e0
commit
c6af4e1565
6 changed files with 45 additions and 103 deletions
|
|
@ -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("/");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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("/");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
|
|
@ -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 [];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue