149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
/**
|
|
* @file components/sidebar/ChannelSidebar.tsx
|
|
* @description Sidebar component for navigating text channels within an active server, featuring channel creation, editing, and mobile responsiveness.
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { useParams } from "next/navigation";
|
|
import { useActiveServer } from "@/lib/context/ServerContext";
|
|
import { useSidebarStore } from "@/lib/stores/useSidebarStore";
|
|
import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
|
|
import { EditChannelModal } from "@/components/modals/EditChannelModal";
|
|
import { PanelLeftClose, Plus, Settings } from "lucide-react";
|
|
import type { Channel } from "@/db/schema";
|
|
|
|
/**
|
|
* Renders the channel sidebar for the active server with text channel lists, creation triggers, and settings handlers.
|
|
*
|
|
* @returns {JSX.Element | null} The rendered channel sidebar container or null if no active server exists.
|
|
*/
|
|
export function ChannelSidebar() {
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
const [editingChannel, setEditingChannel] = useState<Channel | null>(null);
|
|
|
|
const params = useParams();
|
|
const currentChannelId = params?.channelId as string;
|
|
const { activeServer } = useActiveServer();
|
|
const { closeNav, toggleNav } = useSidebarStore();
|
|
|
|
if (!activeServer) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Handles channel selection clicks, automatically closing the mobile navigation drawer on smaller screens.
|
|
*
|
|
* @function handleChannelClick
|
|
* @returns {void}
|
|
*/
|
|
const handleChannelClick = () => {
|
|
if (!window.matchMedia("(min-width: 768px)").matches) {
|
|
closeNav();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Opens the channel settings modal for the selected channel, preventing event propagation.
|
|
*
|
|
* @function handleOpenSettings
|
|
* @param {React.MouseEvent} e - The mouse click event.
|
|
* @param {Channel} channel - The channel object to edit.
|
|
* @returns {void}
|
|
*/
|
|
const handleOpenSettings = (e: React.MouseEvent, channel: Channel) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setEditingChannel(channel);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex-1 w-full bg-surface/50 border-r border-background flex flex-col h-full min-w-0 overflow-hidden">
|
|
{/* Server Header */}
|
|
<div className="h-14 border-b border-background flex items-center justify-between px-4 font-bold text-white shadow-sm shrink-0">
|
|
<span className="truncate">{activeServer.name}</span>
|
|
<button
|
|
type="button"
|
|
onClick={toggleNav}
|
|
title="Collapse the sidebar"
|
|
className="p-1.5 rounded-md text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer shrink-0"
|
|
>
|
|
<PanelLeftClose className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Channel List */}
|
|
<div className="flex-1 overflow-y-auto p-3 space-y-1 min-w-0">
|
|
<div className="flex items-center justify-between text-xs font-semibold text-muted px-2 py-1 uppercase tracking-wider">
|
|
<span>Text Channels</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsCreateModalOpen(true)}
|
|
className="p-1 rounded text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer"
|
|
aria-label="Create channel"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-0.5 w-full">
|
|
{activeServer.channels.map((channel) => {
|
|
const isActive = currentChannelId === channel.id;
|
|
|
|
return (
|
|
<Link
|
|
key={channel.id}
|
|
href={`/servers/${activeServer.id}/channels/${channel.id}`}
|
|
onClick={handleChannelClick}
|
|
prefetch={false}
|
|
className={`flex items-center justify-between w-full px-2 py-1.5 rounded-md text-sm transition-all group min-w-0 overflow-hidden ${
|
|
isActive
|
|
? "bg-accent/50 text-white font-medium"
|
|
: "text-muted hover:bg-surface hover:text-white"
|
|
}`}
|
|
>
|
|
{/* Text Area */}
|
|
<div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
|
|
<span className="text-muted group-hover:text-white text-base shrink-0">
|
|
#
|
|
</span>
|
|
<span className="truncate">{channel.name}</span>
|
|
</div>
|
|
|
|
{/* Gear Button */}
|
|
{!channel.isDefault && (
|
|
<button
|
|
type="button"
|
|
onClick={(e) => handleOpenSettings(e, channel)}
|
|
className="opacity-0 group-hover:opacity-100 p-1 text-muted hover:text-white focus:outline-none transition-all cursor-pointer shrink-0 ml-2"
|
|
aria-label="Channel Settings"
|
|
title="Channel Settings"
|
|
>
|
|
<Settings className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modals */}
|
|
<CreateChannelModal
|
|
isOpen={isCreateModalOpen}
|
|
onClose={() => setIsCreateModalOpen(false)}
|
|
serverId={activeServer.id}
|
|
/>
|
|
|
|
<EditChannelModal
|
|
isOpen={!!editingChannel}
|
|
onClose={() => setEditingChannel(null)}
|
|
channel={editingChannel}
|
|
/>
|
|
</>
|
|
);
|
|
}
|