feat(members): implement online status and member sidebar with API integration
This commit is contained in:
parent
cc8677ae37
commit
08513015d3
11 changed files with 463 additions and 196 deletions
|
|
@ -6,7 +6,7 @@
|
|||
import { auth } from "@/auth";
|
||||
import { getUserServers } from "@/lib/services/server.service";
|
||||
import { AppSidebar } from "@/components/layout/AppSidebar";
|
||||
import { MemberDrawer } from "@/components/layout/MemberDrawer";
|
||||
import { MemberSidebar } from "@/components/layout/MemberSidebar";
|
||||
import { ServerProvider } from "@/lib/context/ServerContext";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ export default async function AppLayout({
|
|||
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
|
||||
<AppSidebar servers={userServers} />
|
||||
<div className="flex-1 flex min-w-0">{children}</div>
|
||||
<MemberDrawer />
|
||||
<MemberSidebar />
|
||||
</div>
|
||||
</ServerProvider>
|
||||
);
|
||||
|
|
|
|||
31
app/api/servers/[serverId]/members/route.ts
Normal file
31
app/api/servers/[serverId]/members/route.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @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,112 +0,0 @@
|
|||
/**
|
||||
* @file components/layout/MemberDrawer.tsx
|
||||
* @description Responsive drawer and sidebar component that manages the visibility, mobile overlay, click-outside dismissal, and rendering of the member list.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
||||
import { MemberSidebar } from "@/components/sidebar/MemberSidebar";
|
||||
import { X } from "lucide-react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
/**
|
||||
* Renders the desktop sidebar panel and mobile drawer for displaying channel members, handling state triggers and click-outside closing logic.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered member drawer component for desktop and mobile views.
|
||||
*/
|
||||
export function MemberDrawer() {
|
||||
const { isMembersOpen, closeMembers } = useSidebarStore();
|
||||
const desktopSidebarRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Handles mouse click events outside of the desktop sidebar to close it.
|
||||
*
|
||||
* @param {MouseEvent} event - The native DOM mouse event.
|
||||
*/
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.closest('button[title*="Mitgliederliste"]')) return;
|
||||
|
||||
if (
|
||||
isMembersOpen &&
|
||||
desktopSidebarRef.current &&
|
||||
!desktopSidebarRef.current.contains(target)
|
||||
) {
|
||||
closeMembers();
|
||||
}
|
||||
}
|
||||
|
||||
if (isMembersOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isMembersOpen, closeMembers]);
|
||||
|
||||
/**
|
||||
* Internal header component for the member drawer containing title labeling and close action button.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.title - The tooltip title for the close button.
|
||||
* @returns {JSX.Element} The header element.
|
||||
*/
|
||||
const Header = ({ title }: { title: string }) => (
|
||||
<div className="h-14 border-b border-surface/50 flex items-center justify-between px-4 shrink-0">
|
||||
<span className="font-semibold text-xs text-muted uppercase tracking-wider">
|
||||
Members
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeMembers}
|
||||
className="p-1.5 rounded-md text-muted hover:text-white hover:bg-background transition-colors cursor-pointer"
|
||||
title={title}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop View */}
|
||||
<aside
|
||||
ref={desktopSidebarRef}
|
||||
className={clsx(
|
||||
"hidden md:flex flex-col h-full bg-surface shrink-0 transition-all duration-300 ease-in-out overflow-hidden border-l border-surface/50",
|
||||
isMembersOpen
|
||||
? "w-60 opacity-100"
|
||||
: "w-0 opacity-0 pointer-events-none border-l-0",
|
||||
)}
|
||||
>
|
||||
<div className="w-60 flex flex-col h-full">
|
||||
<Header title="Mitgliederliste einklappen" />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<MemberSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Backdrop & Drawer */}
|
||||
{isMembersOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 z-30 md:hidden"
|
||||
onClick={closeMembers}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed inset-y-0 right-0 z-40 flex flex-col h-full w-screen sm:w-64 bg-surface transition-transform duration-200 ease-in-out md:hidden shadow-2xl border-l border-surface/50",
|
||||
isMembersOpen ? "translate-x-0" : "translate-x-full",
|
||||
)}
|
||||
>
|
||||
<Header title="Schließen" />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<MemberSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
176
components/layout/MemberSidebar.tsx
Normal file
176
components/layout/MemberSidebar.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/**
|
||||
* @file components/layout/MemberSidebar.tsx
|
||||
* @description Layout sidebar container handling member fetching, responsive drawers, and click-outside dismissal.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
||||
import { useActiveServer } from "@/lib/context/ServerContext";
|
||||
import { MemberList } from "@/components/members/MemberList";
|
||||
import type { Member } from "@/components/members/MemberItem";
|
||||
import { X, Loader2 } from "lucide-react";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
/**
|
||||
* Properties for the MemberHeader component.
|
||||
*
|
||||
* @interface MemberHeaderProps
|
||||
* @property {string} title - The tooltip or accessibility title for the close button.
|
||||
* @property {() => void} onClose - Callback function triggered when the close button is clicked.
|
||||
*/
|
||||
interface MemberHeaderProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the header section of the member sidebar with a title and close button.
|
||||
*
|
||||
* @param {MemberHeaderProps} props - Component properties.
|
||||
* @returns {JSX.Element} The rendered member header element.
|
||||
*/
|
||||
function MemberHeader({ title, onClose }: MemberHeaderProps) {
|
||||
return (
|
||||
<div className="h-14 border-b border-surface/50 flex items-center justify-between px-4 shrink-0">
|
||||
<span className="font-semibold text-xs text-muted uppercase tracking-wider">
|
||||
Members
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-md text-muted hover:text-white hover:bg-background transition-colors cursor-pointer"
|
||||
title={title}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the list of members for the active server in a responsive sidebar or drawer layout.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered member sidebar component.
|
||||
*/
|
||||
export function MemberSidebar() {
|
||||
const { isMembersOpen, closeMembers } = useSidebarStore();
|
||||
const { activeServer } = useActiveServer();
|
||||
const desktopSidebarRef = useRef<HTMLElement>(null);
|
||||
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
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: Member[] = 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]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.closest('button[title*="Mitgliederliste"]')) return;
|
||||
|
||||
if (
|
||||
isMembersOpen &&
|
||||
desktopSidebarRef.current &&
|
||||
!desktopSidebarRef.current.contains(target)
|
||||
) {
|
||||
closeMembers();
|
||||
}
|
||||
}
|
||||
|
||||
if (isMembersOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [isMembersOpen, closeMembers]);
|
||||
|
||||
/**
|
||||
* Renders either a loading spinner or the populated member list depending on state.
|
||||
*
|
||||
* @returns {JSX.Element} The active content component.
|
||||
*/
|
||||
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 (
|
||||
<>
|
||||
{/* Desktop View */}
|
||||
<aside
|
||||
ref={desktopSidebarRef}
|
||||
className={clsx(
|
||||
"hidden md:flex flex-col h-full bg-surface shrink-0 transition-all duration-300 ease-in-out overflow-hidden border-l border-surface/50",
|
||||
isMembersOpen
|
||||
? "w-60 opacity-100"
|
||||
: "w-0 opacity-0 pointer-events-none border-l-0",
|
||||
)}
|
||||
>
|
||||
<div className="w-60 flex flex-col h-full">
|
||||
<MemberHeader
|
||||
title="Mitgliederliste einklappen"
|
||||
onClose={closeMembers}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto">{renderContent()}</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Backdrop & Drawer */}
|
||||
{isMembersOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 z-30 md:hidden"
|
||||
onClick={closeMembers}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed inset-y-0 right-0 z-40 flex flex-col h-full w-screen sm:w-64 bg-surface transition-transform duration-200 ease-in-out md:hidden shadow-2xl border-l border-surface/50",
|
||||
isMembersOpen ? "translate-x-0" : "translate-x-full",
|
||||
)}
|
||||
>
|
||||
<MemberHeader title="Schließen" onClose={closeMembers} />
|
||||
<div className="flex-1 overflow-y-auto">{renderContent()}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
79
components/members/MemberItem.tsx
Normal file
79
components/members/MemberItem.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* @file components/members/MemberItem.tsx
|
||||
* @description Single member item displaying avatar, username, and status badge.
|
||||
*/
|
||||
|
||||
import type { UserStatus } from "@/db/schema";
|
||||
import {
|
||||
MEMBER_COLOR_CLASSES,
|
||||
MEMBER_STATUS_COLOR_CLASSES,
|
||||
} from "@/lib/constants/member.styles";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
/**
|
||||
* Interface representing a member user.
|
||||
*
|
||||
* @interface Member
|
||||
* @property {string} id - Unique identifier for the member.
|
||||
* @property {string} username - Display name of the member.
|
||||
* @property {string} [color] - Color identifier for the member avatar.
|
||||
* @property {UserStatus} status - Current online status of the member.
|
||||
*/
|
||||
export interface Member {
|
||||
id: string;
|
||||
username: string;
|
||||
color: string;
|
||||
status: UserStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties for the MemberItem component.
|
||||
*
|
||||
* @interface MemberItemProps
|
||||
* @property {Member} member - The member object containing user details.
|
||||
* @property {boolean} [isOffline=false] - Optional flag indicating whether the member should be displayed as offline.
|
||||
*/
|
||||
interface MemberItemProps {
|
||||
member: Member;
|
||||
isOffline?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* MemberItem component to display a user's avatar, online status indicator, and username.
|
||||
*
|
||||
* @param {MemberItemProps} props - Component properties.
|
||||
* @returns {JSX.Element} The rendered member item.
|
||||
*/
|
||||
export function MemberItem({ member, isOffline = false }: MemberItemProps) {
|
||||
const userBg = member.color && MEMBER_COLOR_CLASSES[member.color];
|
||||
const statusBg = MEMBER_STATUS_COLOR_CLASSES[member.status];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-background/50 transition-colors cursor-pointer group",
|
||||
isOffline && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
<div
|
||||
className={clsx(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs text-white",
|
||||
userBg,
|
||||
)}
|
||||
>
|
||||
{member.username ? member.username.charAt(0).toUpperCase() : "?"}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
"absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full ring-2 ring-surface",
|
||||
statusBg,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
|
||||
{member.username || "Unbekannt"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
components/members/MemberList.tsx
Normal file
56
components/members/MemberList.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @file components/members/MemberList.tsx
|
||||
* @description Renders categorized online and offline member lists.
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { MemberItem, type Member } from "@/components/members/MemberItem";
|
||||
|
||||
/**
|
||||
* Renders categorized lists of online and offline members with user counters.
|
||||
*
|
||||
* @param {Object} props - Component properties.
|
||||
* @param {Member[]} [props.members=[]] - Array of member objects to group and render.
|
||||
* @returns {JSX.Element} The rendered member list container.
|
||||
*/
|
||||
export function MemberList({ members = [] }: { members?: Member[] }) {
|
||||
const { onlineMembers, offlineMembers } = useMemo(() => {
|
||||
return {
|
||||
onlineMembers: members.filter((m) => m.status !== "OFFLINE"),
|
||||
offlineMembers: members.filter((m) => m.status === "OFFLINE"),
|
||||
};
|
||||
}, [members]);
|
||||
|
||||
return (
|
||||
<div className="w-full bg-surface p-3 shrink-0 h-full overflow-y-auto space-y-4">
|
||||
{/* Online Section */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-muted uppercase tracking-wider px-2 mb-1.5">
|
||||
Online — {onlineMembers.length}
|
||||
</h2>
|
||||
<div className="space-y-0.5">
|
||||
{onlineMembers.map((member) => (
|
||||
<MemberItem key={member.id} member={member} />
|
||||
))}
|
||||
{onlineMembers.length === 0 && (
|
||||
<p className="text-xs text-muted/60 px-2 italic">
|
||||
No members online
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offline Section */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-muted uppercase tracking-wider px-2 mb-1.5">
|
||||
Offline — {offlineMembers.length}
|
||||
</h2>
|
||||
<div className="space-y-0.5">
|
||||
{offlineMembers.map((member) => (
|
||||
<MemberItem key={member.id} member={member} isOffline />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/**
|
||||
* @file components/sidebar/MemberSidebar.tsx
|
||||
* @description Sidebar component displaying lists of online and offline channel members with avatar and status indicators.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders the member sidebar showing categorized online and offline user statuses.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered member sidebar interface.
|
||||
*/
|
||||
export function MemberSidebar() {
|
||||
return (
|
||||
<div className="w-full bg-surface p-3 shrink-0 h-full overflow-y-auto space-y-4">
|
||||
{/* Online Section */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-muted uppercase tracking-wider px-2 mb-1.5">
|
||||
Online — 1
|
||||
</h2>
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-background/50 transition-colors cursor-pointer group">
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-500 flex items-center justify-center font-bold text-xs text-white">
|
||||
U
|
||||
</div>
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-emerald-500 ring-2 ring-surface" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
|
||||
User 1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offline Section */}
|
||||
<div>
|
||||
<h2 className="text-xs font-semibold text-muted uppercase tracking-wider px-2 mb-1.5">
|
||||
Offline — 2
|
||||
</h2>
|
||||
<div className="space-y-0.5 opacity-65">
|
||||
<div className="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-background/50 transition-colors cursor-pointer group">
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-600 flex items-center justify-center font-bold text-xs text-white">
|
||||
U
|
||||
</div>
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-slate-500 ring-2 ring-surface" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
|
||||
User 2
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-background/50 transition-colors cursor-pointer group">
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-600 flex items-center justify-center font-bold text-xs text-white">
|
||||
U
|
||||
</div>
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-slate-500 ring-2 ring-surface" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted group-hover:text-white truncate">
|
||||
User 3
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
db/schema.ts
13
db/schema.ts
|
|
@ -22,6 +22,16 @@ import {
|
|||
*/
|
||||
export const roleEnum = pgEnum("role", ["OWNER", "ADMIN", "MEMBER"]);
|
||||
|
||||
/**
|
||||
* Enum representing online status of a user.
|
||||
*/
|
||||
export const userStatusEnum = pgEnum("user_status", [
|
||||
"ONLINE",
|
||||
"OFFLINE",
|
||||
"IDLE",
|
||||
"DND",
|
||||
]);
|
||||
|
||||
// ==========================================
|
||||
// Tables
|
||||
// ==========================================
|
||||
|
|
@ -35,6 +45,8 @@ export const users = pgTable("users", {
|
|||
email: varchar("email", { length: 255 }).notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
color: varchar("color", { length: 50 }).default("bg-indigo-500").notNull(),
|
||||
status: userStatusEnum("status").default("OFFLINE").notNull(),
|
||||
lastSeenAt: timestamp("last_seen_at").defaultNow().notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
|
|
@ -160,3 +172,4 @@ export type Server = typeof servers.$inferSelect;
|
|||
export type Member = typeof members.$inferSelect;
|
||||
export type Channel = typeof channels.$inferSelect;
|
||||
export type Message = typeof messages.$inferSelect;
|
||||
export type UserStatus = (typeof userStatusEnum.enumValues)[number];
|
||||
|
|
|
|||
35
lib/constants/member.styles.ts
Normal file
35
lib/constants/member.styles.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* @file lib/constants/member.styles.ts
|
||||
*/
|
||||
|
||||
import type { UserStatus } from "@/db/schema";
|
||||
|
||||
/**
|
||||
* Shared Tailwind CSS class constants mapping available member background color keys to their respective CSS utility classes.
|
||||
*/
|
||||
export const MEMBER_COLOR_CLASSES: Record<string, string> = {
|
||||
"bg-indigo-500": "bg-indigo-500",
|
||||
"bg-emerald-500": "bg-emerald-500",
|
||||
"bg-rose-500": "bg-rose-500",
|
||||
"bg-amber-500": "bg-amber-500",
|
||||
"bg-sky-500": "bg-sky-500",
|
||||
"bg-violet-500": "bg-violet-500",
|
||||
"bg-fuchsia-500": "bg-fuchsia-500",
|
||||
"bg-cyan-500": "bg-cyan-500",
|
||||
};
|
||||
|
||||
/**
|
||||
* List of available color option class names for member icon selection.
|
||||
* Dynamically generated from MEMBER_COLOR_OPTIONS to avoid duplicate maintenance.
|
||||
*/
|
||||
export const MEMBER_COLOR_OPTIONS = Object.keys(MEMBER_COLOR_CLASSES);
|
||||
|
||||
/**
|
||||
* Mapping of user status keys to their respective Tailwind CSS indicator background colors.
|
||||
*/
|
||||
export const MEMBER_STATUS_COLOR_CLASSES: Record<UserStatus, string> = {
|
||||
ONLINE: "bg-emerald-500",
|
||||
IDLE: "bg-amber-500",
|
||||
DND: "bg-rose-500",
|
||||
OFFLINE: "bg-slate-500",
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @file lib/context/ServerContext.tsx
|
||||
* @description Context for managing active server state across sidebars and mobile drawers.
|
||||
* @description Context for managing active server state and members across sidebars and mobile drawers.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
|
@ -14,49 +14,69 @@ import type { Server, Channel } from "@/db/schema";
|
|||
export type ServerWithChannels = Server & { channels: Channel[] };
|
||||
|
||||
/**
|
||||
* Interface defining the shape of the ServerContext state and update functions.
|
||||
* Represents a member within a server.
|
||||
*
|
||||
* @interface ServerMember
|
||||
* @property {string} id - The unique identifier of the server member.
|
||||
* @property {string} name - The display name of the server member.
|
||||
* @property {boolean} [isOnline] - Optional flag indicating whether the member is currently online.
|
||||
*/
|
||||
export interface ServerMember {
|
||||
id: string;
|
||||
name: string;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface defining the shape of the ServerContext state and update handlers.
|
||||
*
|
||||
* @interface ServerContextType
|
||||
* @property {ServerWithChannels | null} activeServer - The currently selected active server, or null if no server is active.
|
||||
* @property {(server: ServerWithChannels | null) => void} setActiveServer - Callback function to update the active server state.
|
||||
* @property {ServerWithChannels | null} activeServer - The currently active server instance with its associated channels.
|
||||
* @property {(server: ServerWithChannels | null) => void} setActiveServer - State setter function for updating the active server.
|
||||
* @property {ServerMember[]} members - The list of members belonging to the active server.
|
||||
* @property {(members: ServerMember[]) => void} setMembers - State setter function for updating the server members list.
|
||||
*/
|
||||
interface ServerContextType {
|
||||
activeServer: ServerWithChannels | null;
|
||||
setActiveServer: (server: ServerWithChannels | null) => void;
|
||||
members: ServerMember[];
|
||||
setMembers: (members: ServerMember[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Context instance for providing and consuming active server state.
|
||||
*/
|
||||
const ServerContext = createContext<ServerContextType>({
|
||||
activeServer: null,
|
||||
setActiveServer: () => {},
|
||||
members: [],
|
||||
setMembers: () => {},
|
||||
});
|
||||
|
||||
/**
|
||||
* Context provider component that wraps the tree to manage and broadcast active server state.
|
||||
* Provider component that wraps the application layout to provide global access to active server state and member listings.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {React.ReactNode} props.children - The child components to be rendered within the context provider context.
|
||||
* @returns {JSX.Element} The rendered context provider wrapper.
|
||||
* @param {Object} props - React component properties.
|
||||
* @param {React.ReactNode} props.children - The child components wrapped by the provider.
|
||||
* @returns {JSX.Element} The rendered React provider wrapping the child elements.
|
||||
*/
|
||||
export function ServerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [activeServer, setActiveServer] = useState<ServerWithChannels | null>(
|
||||
null,
|
||||
);
|
||||
const [members, setMembers] = useState<ServerMember[]>([]);
|
||||
|
||||
return (
|
||||
<ServerContext.Provider value={{ activeServer, setActiveServer }}>
|
||||
<ServerContext.Provider
|
||||
value={{ activeServer, setActiveServer, members, setMembers }}
|
||||
>
|
||||
{children}
|
||||
</ServerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to consume the ServerContext values.
|
||||
* Custom hook to access the current ServerContext state.
|
||||
*
|
||||
* @returns {ServerContextType} The active server context state and setter method.
|
||||
* @throws {Error} Throws an error if used outside of a ServerProvider wrapper.
|
||||
* @throws {Error} Throws an error if used outside of a `ServerProvider`.
|
||||
* @returns {ServerContextType} The server context value containing active server state and member management functions.
|
||||
*/
|
||||
export function useActiveServer() {
|
||||
const context = useContext(ServerContext);
|
||||
|
|
|
|||
36
lib/services/member.service.ts
Normal file
36
lib/services/member.service.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* @file lib/services/member.service.ts
|
||||
* @description Service module providing data access functions for server members using Drizzle ORM.
|
||||
*/
|
||||
|
||||
import { db } from "@/db";
|
||||
import { members, users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Retrieves all members belonging to a specific server, including user profile details.
|
||||
*
|
||||
* @async
|
||||
* @function getServerMembers
|
||||
* @param {string} serverId - The unique identifier of the server to fetch members for.
|
||||
* @returns {Promise<Array<{ id: string, username: string, color: string | null, status: string | null }>>} Array of member user profiles, or an empty array if an error occurs.
|
||||
*/
|
||||
export async function getServerMembers(serverId: string) {
|
||||
try {
|
||||
const result = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
color: users.color,
|
||||
status: users.status,
|
||||
})
|
||||
.from(members)
|
||||
.innerJoin(users, eq(members.userId, users.id))
|
||||
.where(eq(members.serverId, serverId));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching server members:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue