From 4dc68e92a79d5cbfc254b8222f8bfda2d2124288 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sat, 29 Aug 2026 17:24:45 +0200 Subject: [PATCH] feat(servers): implement dynamic server & channel sidebar navigation and layout integration --- app/(app)/layout.tsx | 74 +++++------- app/(app)/page.tsx | 30 ++--- .../[serverId]/channels/[channelId]/page.tsx | 47 ++++++++ app/(app)/servers/[serverId]/layout.tsx | 50 ++++++++ app/(app)/servers/[serverId]/page.tsx | 40 +++++++ components/chat/ChatHeader.tsx | 45 ------- components/chat/ChatInput.tsx | 2 +- components/chat/ChatItem.tsx | 64 ++++++++++ components/chat/ChatMessages.tsx | 47 ++++++++ components/layout/AppHeader.tsx | 84 +++++++++++++ components/layout/AppSidebar.tsx | 65 ++++++++++ components/layout/MemberDrawer.tsx | 112 +++++++++++++++++ components/sidebar/ChannelSidebar.tsx | 99 +++++++++++---- components/sidebar/DirectMessageSidebar.tsx | 58 +++++++++ components/sidebar/MemberSidebar.tsx | 66 ++++++++-- components/sidebar/ServerSidebar.tsx | 113 ++++++++++++++++-- .../{UserProfile.tsx => UserPanel.tsx} | 13 +- components/ui/MobileDrawer.tsx | 42 +++---- lib/context/ServerContext.tsx | 67 +++++++++++ lib/providers/ServerStateSync.tsx | 29 +++++ lib/services/channel.service.ts | 20 ++++ lib/services/message.service.ts | 29 +++++ lib/services/server.service.ts | 57 +++++++++ lib/stores/useSidebarStore.ts | 21 ++-- 24 files changed, 1084 insertions(+), 190 deletions(-) create mode 100644 app/(app)/servers/[serverId]/channels/[channelId]/page.tsx create mode 100644 app/(app)/servers/[serverId]/layout.tsx create mode 100644 app/(app)/servers/[serverId]/page.tsx delete mode 100644 components/chat/ChatHeader.tsx create mode 100644 components/chat/ChatItem.tsx create mode 100644 components/chat/ChatMessages.tsx create mode 100644 components/layout/AppHeader.tsx create mode 100644 components/layout/AppSidebar.tsx create mode 100644 components/layout/MemberDrawer.tsx create mode 100644 components/sidebar/DirectMessageSidebar.tsx rename components/sidebar/{UserProfile.tsx => UserPanel.tsx} (81%) create mode 100644 lib/context/ServerContext.tsx create mode 100644 lib/providers/ServerStateSync.tsx create mode 100644 lib/services/channel.service.ts create mode 100644 lib/services/message.service.ts create mode 100644 lib/services/server.service.ts diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index a19ade6..a9b5b1e 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -1,57 +1,43 @@ /** * @file app/(app)/layout.tsx - * @description Main application layout component that manages responsive sidebars and drawer overlays. + * @description Root application layout wrapping sidebars and main workspace content within the ServerProvider context. */ -"use client"; - -import { useSidebarStore } from "@/lib/stores/useSidebarStore"; -import { ServerSidebar } from "@/components/sidebar/ServerSidebar"; -import { ChannelSidebar } from "@/components/sidebar/ChannelSidebar"; -import { MemberSidebar } from "@/components/sidebar/MemberSidebar"; -import { UserProfile } from "@/components/sidebar/UserProfile"; -import { MobileDrawer } from "@/components/ui/MobileDrawer"; +import { auth } from "@/auth"; +import { getUserServers } from "@/lib/services/server.service"; +import { AppSidebar } from "@/components/layout/AppSidebar"; +import { MemberDrawer } from "@/components/layout/MemberDrawer"; +import { ServerProvider } from "@/lib/context/ServerContext"; +import { redirect } from "next/navigation"; /** - * Client component serving as the primary application layout, handling the server, channel, and member sidebars with mobile drawer support. + * Server component layout wrapper for authenticated application views. + * Handles session verification, fetches user servers, and renders global layout components. * * @param {Object} props - The component props. - * @param {React.ReactNode} props.children - The child layout or page content to render in the central main view. - * @returns {JSX.Element} The application layout structure with responsive sidebars. + * @param {React.ReactNode} props.children - The child page content to render inside the main viewport layout. + * @returns {Promise} The rendered application layout hierarchy with provider contexts. */ -export default function AppLayout({ children }: { children: React.ReactNode }) { - const { isNavOpen, isMembersOpen, closeAll } = useSidebarStore(); +export default async function AppLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + + if (!session?.user?.id) { + redirect("/login"); + } + + const userServers = await getUserServers(session.user.id); return ( -
- {/* Left Navigation: Server + Channels + User Profile */} - -
-
- - -
- -
-
- - {/* Center: Main Area */} -
{children}
- - {/* Right: List of Members */} - - - -
+ +
+ +
{children}
+ +
+
); } diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 791343e..a325ec9 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,28 +1,28 @@ /** * @file app/(app)/page.tsx - * @description Main dashboard page displaying the primary chat view with header, message area, and input control. + * @description Main application layout page featuring a dynamic header and default welcome screen. */ -import { ChatHeader } from "@/components/chat/ChatHeader"; -import { ChatInput } from "@/components/chat/ChatInput"; +import { AppHeader } from "@/components/layout/AppHeader"; /** - * Renders the main application dashboard containing the general chat interface. + * Renders the default application page with the header and central welcome message. * - * @returns {Promise} The rendered application dashboard page. + * @returns {JSX.Element} The rendered application page view. */ -export default async function ApplicationDashboardPage() { +export default function AppPage() { return ( -
- +
+ {/* Dynamic Header */} + -
-
- This is the beginning of the #general channel. -
+ {/* Main Content */} +
+

Welcome back!

+

+ Select a server from the left side or start a chat with your friends. +

- - -
+ ); } diff --git a/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx b/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx new file mode 100644 index 0000000..6ba4be9 --- /dev/null +++ b/app/(app)/servers/[serverId]/channels/[channelId]/page.tsx @@ -0,0 +1,47 @@ +/** + * @file app/servers/[serverId]/channels/[channelId]/page.tsx + * @description Page component for viewing a specific channel and its messages. + */ + +import { notFound } from "next/navigation"; +import { AppHeader } from "@/components/layout/AppHeader"; +import { ChatInput } from "@/components/chat/ChatInput"; +import { ChatMessages } from "@/components/chat/ChatMessages"; +import { getChannelById } from "@/lib/services/channel.service"; +import { getChannelMessages } from "@/lib/services/message.service"; + +/** + * Server component that fetches and renders a channel's details and message feed based on route parameters. + * + * @param {Object} props - The component props. + * @param {Promise<{ channelId: string }>} props.params - Async route parameters containing the channel ID. + * @returns {Promise} The rendered channel page view or triggers a 404 notFound error. + */ +export default async function ChannelPage({ + params, +}: { + params: Promise<{ channelId: string }>; +}) { + const { channelId } = await params; + + // Paralleles Laden von Kanal-Daten und Nachrichten + const [channel, channelMessages] = await Promise.all([ + getChannelById(channelId), + getChannelMessages(channelId), + ]); + + if (!channel) return notFound(); + + return ( +
+ {/* Header */} + + + {/* Messages Feed */} + + + {/* Input Field */} + +
+ ); +} diff --git a/app/(app)/servers/[serverId]/layout.tsx b/app/(app)/servers/[serverId]/layout.tsx new file mode 100644 index 0000000..cc7272a --- /dev/null +++ b/app/(app)/servers/[serverId]/layout.tsx @@ -0,0 +1,50 @@ +/** + * @file app/(app)/servers/[serverId]/layout.tsx + * @description Server layout component synchronizing active server state and guarding server route access. + */ + +import { auth } from "@/auth"; +import { getServerWithChannels } from "@/lib/services/server.service"; +import { ServerStateSync } from "@/lib/providers/ServerStateSync"; +import { redirect } from "next/navigation"; + +/** + * Properties for the ServerLayout component. + * + * @interface ServerLayoutProps + * @property {React.ReactNode} children - Child elements to be rendered within the layout context. + * @property {Promise<{ serverId: string }>} params - Asynchronous route parameters containing the active serverId. + */ +interface ServerLayoutProps { + children: React.ReactNode; + params: Promise<{ serverId: string }>; +} + +/** + * Server component that verifies user authentication, retrieves the target server with its channels, synchronizes server state, and wraps child routes. + * + * @param {ServerLayoutProps} props - The component props. + * @returns {Promise} The rendered server layout tree. + */ +export default async function ServerLayout({ + children, + params, +}: ServerLayoutProps) { + const { serverId } = await params; + const session = await auth(); + + if (!session?.user?.id) redirect("/login"); + + const server = await getServerWithChannels(serverId, session.user.id); + + if (!server) redirect("/"); + + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/app/(app)/servers/[serverId]/page.tsx b/app/(app)/servers/[serverId]/page.tsx new file mode 100644 index 0000000..5c9d0a4 --- /dev/null +++ b/app/(app)/servers/[serverId]/page.tsx @@ -0,0 +1,40 @@ +/** + * @file app/servers/[serverId]/page.tsx + * @description Page component redirecting to the first available channel of a server. + */ + +import { auth } from "@/auth"; +import { getServerWithChannels } from "@/lib/services/server.service"; +import { redirect } from "next/navigation"; + +export default async function ServerPage({ + params, +}: { + params: Promise<{ serverId: string }>; +}) { + const { serverId } = await params; + const session = await auth(); + + if (!session?.user?.id) redirect("/login"); + + const server = await getServerWithChannels(serverId, session.user.id); + + if (!server) redirect("/"); + + // Falls doch jemand direkt /servers/[serverId] aufruft + if (server.channels.length > 0) { + redirect(`/servers/${serverId}/channels/${server.channels[0].id}`); + } + + return ( +
+

+ Welcome to {server.name}! +

+

+ No channels have been created on this server yet. Create a channel to + chat. +

+
+ ); +} diff --git a/components/chat/ChatHeader.tsx b/components/chat/ChatHeader.tsx deleted file mode 100644 index 9492958..0000000 --- a/components/chat/ChatHeader.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @file components/chat/ChatHeader.tsx - * @description Client component rendering the top header of the chat interface with sidebar toggles and current channel details. - */ - -"use client"; - -import { Menu, Hash, Users } from "lucide-react"; -import { useSidebarStore } from "@/lib/stores/useSidebarStore"; - -/** - * Renders the top navigation header for the chat view, providing toggle triggers for mobile navigation and member list sidebars. - * - * @returns {JSX.Element} The rendered chat header component. - */ -export function ChatHeader() { - const { toggleNav, toggleMembers } = useSidebarStore(); - - return ( -
-
- - -
- - general -
-
- - -
- ); -} diff --git a/components/chat/ChatInput.tsx b/components/chat/ChatInput.tsx index a782b4c..973c514 100644 --- a/components/chat/ChatInput.tsx +++ b/components/chat/ChatInput.tsx @@ -10,7 +10,7 @@ */ export function ChatInput() { return ( -
+
+ {/* Avatar */} +
+ {initial} +
+ + {/* Message Header & Content */} +
+
+ + {fullName} + + {formattedTime} +
+

+ {message.content} +

+
+
+ ); +} diff --git a/components/chat/ChatMessages.tsx b/components/chat/ChatMessages.tsx new file mode 100644 index 0000000..022e3ba --- /dev/null +++ b/components/chat/ChatMessages.tsx @@ -0,0 +1,47 @@ +/** + * @file components/chat/ChatMessages.tsx + * @description Message history container component displaying initial channel greeting and rendering a list of individual chat messages. + */ + +"use client"; + +import { ChatItem, type MessageWithMember } from "./ChatItem"; + +/** + * Properties for the ChatMessages component. + * + * @interface ChatMessagesProps + * @property {string} channelName - The name of the active chat channel to display in the header greeting. + * @property {MessageWithMember[]} messages - Array of message objects, each containing message details and associated member information. + */ +interface ChatMessagesProps { + channelName: string; + messages: MessageWithMember[]; +} + +/** + * Renders the scrollable message list along with a welcoming channel header. + * + * @param {ChatMessagesProps} props - The component props. + * @returns {JSX.Element} The rendered chat messages container. + */ +export function ChatMessages({ channelName, messages }: ChatMessagesProps) { + return ( +
+
+

+ Welcome to #{channelName}! +

+

+ This is the beginning of the channel #{channelName}. +

+
+ +
+ {messages.map((message) => ( + + ))} +
+
+ ); +} diff --git a/components/layout/AppHeader.tsx b/components/layout/AppHeader.tsx new file mode 100644 index 0000000..3a017c8 --- /dev/null +++ b/components/layout/AppHeader.tsx @@ -0,0 +1,84 @@ +/** + * @file components/layout/AppHeader.tsx + * @description Unified application header supporting general views, chat channels, and DM views. + */ + +"use client"; + +import { useSidebarStore } from "@/lib/stores/useSidebarStore"; +import { PanelLeftOpen, PanelLeftClose, Users, Hash } from "lucide-react"; + +/** + * Properties for the AppHeader component. + * + * @interface AppHeaderProps + * @property {string} [title] - Optional channel or page title to display in the header. + * @property {boolean} [showMembersButton=false] - Flag indicating whether to display the member list toggle button. + */ +interface AppHeaderProps { + title?: string; + showMembersButton?: boolean; +} + +/** + * Renders the application header bar with navigation controls, dynamic page titles, and member list toggle capability. + * + * @param {AppHeaderProps} props - The component props. + * @param {string} [props.title] - Optional channel or page title to display. + * @param {boolean} [props.showMembersButton=false] - Whether to show the button toggling the right sidebar/member panel. + * @returns {JSX.Element} The header component visual structure. + */ +export function AppHeader({ + title, + showMembersButton = false, +}: AppHeaderProps) { + const { isNavOpen, toggleNav, toggleMembers } = useSidebarStore(); + + const hasContent = !isNavOpen || !!title || showMembersButton; + + return ( +
+
+ {/* Toggle button for navigation */} + + + {/* Dynamic Title (Channel/Page Name) */} + {title && ( +
+ +

{title}

+
+ )} +
+ + {/* Button for the member bar */} + {showMembersButton && ( + + )} +
+ ); +} diff --git a/components/layout/AppSidebar.tsx b/components/layout/AppSidebar.tsx new file mode 100644 index 0000000..9e0be55 --- /dev/null +++ b/components/layout/AppSidebar.tsx @@ -0,0 +1,65 @@ +/** + * @file components/layout/AppSidebar.tsx + * @description Responsive sidebar wrapper for server and channel sidebars with collapsible desktop support. + */ + +"use client"; + +import { useSidebarStore } from "@/lib/stores/useSidebarStore"; +import { ServerSidebar } from "@/components/sidebar/ServerSidebar"; +import { ChannelSidebar } from "@/components/sidebar/ChannelSidebar"; +import { UserPanel } from "@/components/sidebar/UserPanel"; +import type { ServerWithChannels } from "@/lib/context/ServerContext"; +import { clsx } from "clsx"; + +/** + * Renders the responsive application sidebar containing server navigation, channel lists, and user profile. + * + * @param {Object} props - The component props. + * @param {ServerWithChannels[]} props.servers - Array of server objects with channels to display in the server navigation bar. + * @returns {JSX.Element} The rendered mobile overlay and responsive sidebar structure. + */ +export function AppSidebar({ servers }: { servers: ServerWithChannels[] }) { + const { isNavOpen, closeAll } = useSidebarStore(); + + return ( + <> + {/* 1. DESKTOP VIEW (md:flex) - Collapses flexibly via transition/width */} + + + {/* 2. MOBILE VIEW (md:hidden) - Functions as a slide-out drawer */} + {isNavOpen && ( +
+ )} + +
+
+ + +
+ +
+ + ); +} diff --git a/components/layout/MemberDrawer.tsx b/components/layout/MemberDrawer.tsx new file mode 100644 index 0000000..1a76c5e --- /dev/null +++ b/components/layout/MemberDrawer.tsx @@ -0,0 +1,112 @@ +/** + * @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(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 }) => ( +
+ + Members + + +
+ ); + + return ( + <> + {/* Desktop View */} + + + {/* Mobile Backdrop & Drawer */} + {isMembersOpen && ( +
+ )} + +
+
+
+ +
+
+ + ); +} diff --git a/components/sidebar/ChannelSidebar.tsx b/components/sidebar/ChannelSidebar.tsx index 83c9e26..2bffab8 100644 --- a/components/sidebar/ChannelSidebar.tsx +++ b/components/sidebar/ChannelSidebar.tsx @@ -1,39 +1,88 @@ /** * @file components/sidebar/ChannelSidebar.tsx - * @description Sidebar component for server navigation, rendering channel lists. + * @description Sidebar component listing channels for the active server, or rendering direct messages when no server is active. */ -import { Hash } from "lucide-react"; +"use client"; + +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useActiveServer } from "@/lib/context/ServerContext"; +import { DirectMessageSidebar } from "@/components/sidebar/DirectMessageSidebar"; +import { useSidebarStore } from "@/lib/stores/useSidebarStore"; +import { PanelLeftClose } from "lucide-react"; /** - * Navigation sidebar displaying server details and text channels. + * Renders the channel sidebar for the active server or defaults to the direct message view. + * Handles responsive sidebar toggling and highlights active channels based on URL parameters. * - * @returns {JSX.Element} The rendered channel sidebar element. + * @returns {JSX.Element} The rendered channel sidebar or direct message sidebar component. */ export function ChannelSidebar() { - return ( - +
); } diff --git a/components/sidebar/DirectMessageSidebar.tsx b/components/sidebar/DirectMessageSidebar.tsx new file mode 100644 index 0000000..83cb0b8 --- /dev/null +++ b/components/sidebar/DirectMessageSidebar.tsx @@ -0,0 +1,58 @@ +/** + * @file components/sidebar/DirectMessageSidebar.tsx + * @description Sidebar listing direct messages and friends list when no server is selected. + */ + +"use client"; + +import { useSidebarStore } from "@/lib/stores/useSidebarStore"; +import { PanelLeftClose } from "lucide-react"; +import Link from "next/link"; + +/** + * DirectMessageSidebar component that renders navigation for direct messages and friends. + * Includes a header with collapsible controls and links for viewing chat channels and active conversations. + * + * @returns {JSX.Element} The direct message sidebar component layout. + */ +export function DirectMessageSidebar() { + const { toggleNav } = useSidebarStore(); + + return ( +
+ {/* Header */} +
+ Direct Messages + +
+ + {/* Navigation & List */} +
+
+ + Friends + +
+ +
+
+ Direct Messages +
+
+ No active chats +
+
+
+
+ ); +} diff --git a/components/sidebar/MemberSidebar.tsx b/components/sidebar/MemberSidebar.tsx index 9107783..27ce6c4 100644 --- a/components/sidebar/MemberSidebar.tsx +++ b/components/sidebar/MemberSidebar.tsx @@ -1,25 +1,67 @@ /** * @file components/sidebar/MemberSidebar.tsx - * @description Sidebar component displaying the list of active server/chat members and their online status. + * @description Sidebar component displaying lists of online and offline channel members with avatar and status indicators. */ /** - * Renders the member sidebar showing online users and their profile avatars. + * Renders the member sidebar showing categorized online and offline user statuses. * - * @returns {JSX.Element} The rendered member sidebar navigation container. + * @returns {JSX.Element} The rendered member sidebar interface. */ export function MemberSidebar() { return ( - + + {/* Offline Section */} +
+

+ Offline — 2 +

+
+
+
+
+ U +
+ +
+ + User 2 + +
+ +
+
+
+ U +
+ +
+ + User 3 + +
+
+
+
); } diff --git a/components/sidebar/ServerSidebar.tsx b/components/sidebar/ServerSidebar.tsx index b73c93a..6fc2ab9 100644 --- a/components/sidebar/ServerSidebar.tsx +++ b/components/sidebar/ServerSidebar.tsx @@ -1,24 +1,119 @@ /** * @file components/sidebar/ServerSidebar.tsx - * @description Sidebar navigation component for switching between servers and direct messages. + * @description Sidebar navigation component for switching between servers and home view. */ +"use client"; + +import Link from "next/link"; +import NextImage from "next/image"; +import { usePathname } from "next/navigation"; +import { + useActiveServer, + type ServerWithChannels, +} from "@/lib/context/ServerContext"; + /** - * Navigation bar for switching between servers and direct messages. + * ServerSidebar component rendering the list of available servers, home navigation, and server creation trigger. * - * @returns {JSX.Element} The rendered server sidebar component. + * @param {Object} props - The component props. + * @param {ServerWithChannels[]} props.servers - Array of server objects containing channel and display metadata. + * @returns {JSX.Element} The rendered server sidebar navigation. */ -export function ServerSidebar() { +export function ServerSidebar({ servers }: { servers: ServerWithChannels[] }) { + const pathname = usePathname(); + const { setActiveServer } = useActiveServer(); + + /** + * Mapping of Tailwind CSS background color classes for server icons. + */ + const COLOR_CLASSES: Record = { + "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", + }; + + /** + * Shared base CSS utility classes for server icon buttons. + */ + const baseIconStyles = + "w-12 h-12 flex items-center justify-center transition-all duration-200 shadow-md shrink-0"; + /** + * CSS utility classes applied to the currently active server icon. + */ + const activeIconStyles = + "rounded-xl ring-2 ring-accent ring-offset-2 ring-offset-surface cursor-default pointer-events-none opacity-100"; + /** + * CSS utility classes applied to inactive server icons. + */ + const inactiveIconStyles = + "rounded-3xl opacity-80 hover:opacity-100 hover:rounded-xl hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-accent/40 cursor-pointer active:scale-95"; + return ( ); diff --git a/components/sidebar/UserProfile.tsx b/components/sidebar/UserPanel.tsx similarity index 81% rename from components/sidebar/UserProfile.tsx rename to components/sidebar/UserPanel.tsx index 0fcb811..2feea3b 100644 --- a/components/sidebar/UserProfile.tsx +++ b/components/sidebar/UserPanel.tsx @@ -1,6 +1,6 @@ /** - * @file components/sidebar/UserProfile.tsx - * @description User profile component displaying current session info, status, settings, and logout action. + * @file components/sidebar/UserPanel.tsx + * @description Client component providing a footer user panel with session information, avatar display, online status, and settings/logout actions. */ "use client"; @@ -9,13 +9,16 @@ import { LogOut, Settings } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; /** - * Renders the user profile card with user information, online status, settings, and logout trigger. + * Renders the user panel footer component showing current session profile details and authentication controls. * - * @returns {JSX.Element} The rendered user profile footer element. + * @returns {JSX.Element} The rendered user panel component. */ -export function UserProfile() { +export function UserPanel() { const { data: session } = useSession(); + /** + * Triggers the NextAuth sign-out procedure and redirects the user to the login page. + */ const handleLogout = () => { signOut({ callbackUrl: "/login" }); }; diff --git a/components/ui/MobileDrawer.tsx b/components/ui/MobileDrawer.tsx index d29adab..0d4f209 100644 --- a/components/ui/MobileDrawer.tsx +++ b/components/ui/MobileDrawer.tsx @@ -1,6 +1,6 @@ /** * @file components/ui/MobileDrawer.tsx - * @description Slide-out drawer component for mobile views with configurable side placement, responsive breakpoint behavior, and backdrop overlay. + * @description Slide-out drawer component for mobile views with optional responsive breakpoint behavior. */ "use client"; @@ -9,58 +9,48 @@ import { clsx } from "clsx"; interface MobileDrawerProps { isOpen: boolean; - onClose: () => void; side: "left" | "right"; - breakpoint: "md" | "xl"; + breakpoint?: "md" | "none"; children: React.ReactNode; } /** - * Renders a responsive drawer container that slides in from either side on mobile viewports and transitions to a static layout at specified breakpoints. + * Slide-out drawer component designed for mobile layouts with configurable side positioning and responsive display logic. * * @param {MobileDrawerProps} props - The component props. - * @param {boolean} props.isOpen - Controls the visibility state of the drawer overlay on mobile viewports. - * @param {() => void} props.onClose - Callback function triggered when clicking the backdrop overlay to close the drawer. - * @param {"left" | "right"} props.side - The screen edge from which the drawer slides out. - * @param {"md" | "xl"} props.breakpoint - Tailwind responsive breakpoint at which the drawer becomes static and hides the mobile backdrop. - * @param {React.ReactNode} props.children - Content rendered within the drawer body. - * @returns {JSX.Element} The drawer element alongside its conditional backdrop overlay. + * @param {boolean} props.isOpen - Indicates whether the drawer is currently expanded or hidden. + * @param {"left" | "right"} props.side - The edge of the viewport from which the drawer slides out. + * @param {"md" | "none"} [props.breakpoint="none"] - Optional breakpoint at which the drawer becomes statically positioned. + * @param {React.ReactNode} props.children - The elements to render inside the drawer container. + * @returns {JSX.Element} The rendered mobile drawer component. */ export function MobileDrawer({ isOpen, - onClose, side, - breakpoint, + breakpoint = "none", children, }: MobileDrawerProps) { const isLeft = side === "left"; const translateHidden = isLeft ? "-translate-x-full" : "translate-x-full"; + const breakpointStatic = - breakpoint === "md" - ? "md:static md:translate-x-0" - : "xl:static xl:translate-x-0"; - const breakpointHidden = breakpoint === "md" ? "md:hidden" : "xl:hidden"; + breakpoint === "md" ? "md:static md:translate-x-0 md:w-auto" : ""; return ( <>
- {children} +
+ {children} +
- - {isOpen && ( -
- )} ); } diff --git a/lib/context/ServerContext.tsx b/lib/context/ServerContext.tsx new file mode 100644 index 0000000..5e8badb --- /dev/null +++ b/lib/context/ServerContext.tsx @@ -0,0 +1,67 @@ +/** + * @file lib/context/ServerContext.tsx + * @description Context for managing active server state across sidebars and mobile drawers. + */ + +"use client"; + +import { createContext, useContext, useState } from "react"; +import type { Server, Channel } from "@/db/schema"; + +/** + * Type definition representing a server entity along with its associated channels array. + */ +export type ServerWithChannels = Server & { channels: Channel[] }; + +/** + * Interface defining the shape of the ServerContext state and update functions. + * + * @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. + */ +interface ServerContextType { + activeServer: ServerWithChannels | null; + setActiveServer: (server: ServerWithChannels | null) => void; +} + +/** + * React Context instance for providing and consuming active server state. + */ +const ServerContext = createContext({ + activeServer: null, + setActiveServer: () => {}, +}); + +/** + * Context provider component that wraps the tree to manage and broadcast active server state. + * + * @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. + */ +export function ServerProvider({ children }: { children: React.ReactNode }) { + const [activeServer, setActiveServer] = useState( + null, + ); + + return ( + + {children} + + ); +} + +/** + * Custom hook to consume the ServerContext values. + * + * @returns {ServerContextType} The active server context state and setter method. + * @throws {Error} Throws an error if used outside of a ServerProvider wrapper. + */ +export function useActiveServer() { + const context = useContext(ServerContext); + if (!context) { + throw new Error("useActiveServer must be used within a ServerProvider"); + } + return context; +} diff --git a/lib/providers/ServerStateSync.tsx b/lib/providers/ServerStateSync.tsx new file mode 100644 index 0000000..a307420 --- /dev/null +++ b/lib/providers/ServerStateSync.tsx @@ -0,0 +1,29 @@ +/** + * @file lib/providers/ServerStateSync.tsx + * @description Syncs active server data into ServerContext on mount and updates on change. + */ + +"use client"; + +import { useEffect } from "react"; +import { + useActiveServer, + type ServerWithChannels, +} from "@/lib/context/ServerContext"; + +/** + * Client component that synchronizes the current server state with the global server context. + * + * @param {Object} props - The component props. + * @param {ServerWithChannels} props.server - The server object containing channels to be set as active. + * @returns {null} Renders no UI elements. + */ +export function ServerStateSync({ server }: { server: ServerWithChannels }) { + const { setActiveServer } = useActiveServer(); + + useEffect(() => { + setActiveServer(server); + }, [server, setActiveServer]); + + return null; +} diff --git a/lib/services/channel.service.ts b/lib/services/channel.service.ts new file mode 100644 index 0000000..9ca7bfd --- /dev/null +++ b/lib/services/channel.service.ts @@ -0,0 +1,20 @@ +/** + * @file lib/services/channel.service.ts + * @description Service module providing database access methods for channel management. + */ + +import { db } from "@/db"; +import { channels } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +/** + * Retrieves a single channel record from the database by its unique identifier. + * + * @param {string} channelId - The unique identifier of the channel to fetch. + * @returns {Promise} The channel object if found, or undefined if no matching channel exists. + */ +export async function getChannelById(channelId: string) { + return await db.query.channels.findFirst({ + where: eq(channels.id, channelId), + }); +} diff --git a/lib/services/message.service.ts b/lib/services/message.service.ts new file mode 100644 index 0000000..0812512 --- /dev/null +++ b/lib/services/message.service.ts @@ -0,0 +1,29 @@ +/** + * @file lib/services/message.service.ts + * @description Data access service for retrieving and managing message entities from the database. + */ + +import { db } from "@/db"; +import { messages } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +/** + * Retrieves all messages belonging to a specific channel, ordered chronologically ascending, + * including associated member and user details. + * + * @param {string} channelId - The unique identifier of the target channel. + * @returns {Promise>} A promise resolving to a list of message objects populated with nested member and user data. + */ +export async function getChannelMessages(channelId: string) { + return await db.query.messages.findMany({ + where: eq(messages.channelId, channelId), + with: { + member: { + with: { + user: true, + }, + }, + }, + orderBy: (messages, { asc }) => [asc(messages.createdAt)], + }); +} diff --git a/lib/services/server.service.ts b/lib/services/server.service.ts new file mode 100644 index 0000000..37c75c5 --- /dev/null +++ b/lib/services/server.service.ts @@ -0,0 +1,57 @@ +/** + * @file lib/services/server.service.ts + * @description Service module providing database queries for managing server data, user memberships, and associated channels. + */ + +import { db } from "@/db"; +import { members, servers } from "@/db/schema"; +import { eq, and } from "drizzle-orm"; + +/** + * Retrieves a server along with its channels sorted chronologically if the specified user is a verified member. + * + * @param {string} serverId - The unique identifier of the server to retrieve. + * @param {string} userId - The unique identifier of the requesting user. + * @returns {Promise} The server record with nested channels array, or null if the user is not a member or the server is not found. + */ +export async function getServerWithChannels(serverId: string, userId: string) { + const isMember = await db.query.members.findFirst({ + where: and(eq(members.serverId, serverId), eq(members.userId, userId)), + }); + + if (!isMember) return null; + + const server = await db.query.servers.findFirst({ + where: eq(servers.id, serverId), + with: { + channels: { + orderBy: (channels, { asc }) => [asc(channels.createdAt)], + }, + }, + }); + + return server; +} + +/** + * Fetches all servers that the specified user belongs to, including each server's sorted channels list. + * + * @param {string} userId - The unique identifier of the user whose servers are to be fetched. + * @returns {Promise>} An array of server objects associated with the user. + */ +export async function getUserServers(userId: string) { + const userMemberships = await db.query.members.findMany({ + where: eq(members.userId, userId), + with: { + server: { + with: { + channels: { + orderBy: (channels, { asc }) => [asc(channels.createdAt)], + }, + }, + }, + }, + }); + + return userMemberships.map((membership) => membership.server).filter(Boolean); +} diff --git a/lib/stores/useSidebarStore.ts b/lib/stores/useSidebarStore.ts index ffb176e..4c41645 100644 --- a/lib/stores/useSidebarStore.ts +++ b/lib/stores/useSidebarStore.ts @@ -1,6 +1,6 @@ /** * @file lib/stores/useSidebarStore.ts - * @description Condition state management store for controlling the visibility and mutual exclusion of navigation and members sidebars. + * @description State management store using Zustand for controlling the visibility of navigation and members sidebars. */ import { create } from "zustand"; @@ -11,8 +11,10 @@ import { create } from "zustand"; * @interface SidebarState * @property {boolean} isNavOpen - Indicates whether the navigation sidebar is open. * @property {boolean} isMembersOpen - Indicates whether the members list sidebar is open. - * @property {() => void} toggleNav - Toggles the navigation sidebar while automatically closing the members sidebar. - * @property {() => void} toggleMembers - Toggles the members sidebar while automatically closing the navigation sidebar. + * @property {() => void} toggleNav - Toggles the visibility of the navigation sidebar. + * @property {() => void} toggleMembers - Toggles the visibility of the members list sidebar. + * @property {() => void} closeNav - Explicitly closes the navigation sidebar. + * @property {() => void} closeMembers - Explicitly closes the members list sidebar. * @property {() => void} closeAll - Closes both the navigation and members sidebars simultaneously. */ interface SidebarState { @@ -20,20 +22,23 @@ interface SidebarState { isMembersOpen: boolean; toggleNav: () => void; toggleMembers: () => void; + closeNav: () => void; + closeMembers: () => void; closeAll: () => void; } /** - * Custom Condition hook for managing global sidebar visibility states. + * Custom Zustand hook for managing global sidebar visibility states. * * @returns {SidebarState} The current sidebar state and action handlers. */ export const useSidebarStore = create((set) => ({ - isNavOpen: false, + isNavOpen: true, isMembersOpen: false, - toggleNav: () => - set((state) => ({ isNavOpen: !state.isNavOpen, isMembersOpen: false })), + toggleNav: () => set((state) => ({ isNavOpen: !state.isNavOpen })), toggleMembers: () => - set((state) => ({ isMembersOpen: !state.isMembersOpen, isNavOpen: false })), + set((state) => ({ isMembersOpen: !state.isMembersOpen })), + closeNav: () => set({ isNavOpen: false }), + closeMembers: () => set({ isMembersOpen: false }), closeAll: () => set({ isNavOpen: false, isMembersOpen: false }), }));