diff --git a/components/sidebar/CategorySection.tsx b/components/sidebar/CategorySection.tsx
new file mode 100644
index 0000000..59579dc
--- /dev/null
+++ b/components/sidebar/CategorySection.tsx
@@ -0,0 +1,113 @@
+/**
+ * @file components/sidebar/CategorySection.tsx
+ * @description Sub-component for rendering a category group and its list of channels.
+ */
+
+"use client";
+
+import { useState } from "react";
+import { ChevronDown, ChevronRight, Plus, Settings } from "lucide-react";
+import { ChannelItem } from "./ChannelItem";
+import type { Channel } from "@/db/schema";
+
+/** Props for the CategorySection component. */
+interface CategorySectionProps {
+ title: string;
+ channels: Channel[];
+ currentChannelId: string;
+ serverId: string;
+ onCloseNav: () => void;
+ onCreateChannel: () => void;
+ onEditChannel: (channel: Channel) => void;
+ onEditCategory?: () => void;
+}
+
+/** Renders a collapsible category section containing channels and contextual actions. */
+export function CategorySection({
+ title,
+ channels,
+ currentChannelId,
+ serverId,
+ onCloseNav,
+ onCreateChannel,
+ onEditChannel,
+ onEditCategory,
+}: CategorySectionProps) {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ /** Handles closing mobile navigation when clicking a channel. */
+ const handleChannelClick = () => {
+ if (!window.matchMedia("(min-width: 768px)").matches) {
+ onCloseNav();
+ }
+ };
+
+ /** Opens settings for a specific channel. */
+ const handleOpenChannelSettings = (e: React.MouseEvent, channel: Channel) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onEditChannel(channel);
+ };
+
+ /** Opens settings for the current category. */
+ const handleOpenCategorySettings = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onEditCategory?.();
+ };
+
+ return (
+
+
+
+
+
+ {onEditCategory && (
+
+ )}
+
+
+
+
+ {!isCollapsed && (
+
+ {channels.map((channel) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/sidebar/ChannelItem.tsx b/components/sidebar/ChannelItem.tsx
new file mode 100644
index 0000000..c76a80a
--- /dev/null
+++ b/components/sidebar/ChannelItem.tsx
@@ -0,0 +1,59 @@
+/**
+ * @file components/sidebar/ChannelItem.tsx
+ * @description Channel item component rendering channel links with active state styling and settings action.
+ */
+
+"use client";
+
+import Link from "next/link";
+import { Settings } from "lucide-react";
+import type { Channel } from "@/db/schema";
+
+/** Props for the ChannelItem component. */
+interface ChannelItemProps {
+ channel: Channel;
+ serverId: string;
+ isActive: boolean;
+ onChannelClick: () => void;
+ onOpenSettings: (e: React.MouseEvent, channel: Channel) => void;
+}
+
+/** Renders an individual channel item link with an optional settings trigger. */
+export function ChannelItem({
+ channel,
+ serverId,
+ isActive,
+ onChannelClick,
+ onOpenSettings,
+}: ChannelItemProps) {
+ return (
+
+
+
+ #
+
+ {channel.name}
+
+
+ {!channel.isDefault && (
+
+ )}
+
+ );
+}
diff --git a/components/sidebar/ChannelSidebar.tsx b/components/sidebar/ChannelSidebar.tsx
index 1b1e57d..a31a50a 100644
--- a/components/sidebar/ChannelSidebar.tsx
+++ b/components/sidebar/ChannelSidebar.tsx
@@ -1,6 +1,6 @@
/**
* @file components/sidebar/ChannelSidebar.tsx
- * @description Channel sidebar component listing categories and channels with actions for creation and editing.
+ * @description Channel sidebar component rendering categories, channels, and modals for creation and editing.
*/
"use client";
@@ -13,32 +13,20 @@ import { CreateChannelModal } from "@/components/modals/CreateChannelModal";
import { CreateCategoryModal } from "@/components/modals/CreateCategoryModal";
import { EditChannelModal } from "@/components/modals/EditChannelModal";
import { EditCategoryModal } from "@/components/modals/EditCategoryModal";
-import {
- ChevronDown,
- ChevronRight,
- FolderPlus,
- PanelLeftClose,
- Plus,
- Settings,
-} from "lucide-react";
+import { CategorySection } from "./CategorySection";
+import { FolderPlus, PanelLeftClose } from "lucide-react";
import type { Category, Channel } from "@/db/schema";
-import Link from "next/link";
-/** Renders the channel navigation sidebar for the active server. */
+/** Renders the channel sidebar navigation for the active server. */
export function ChannelSidebar() {
- const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
- const [isCreateCategoryModalOpen, setIsCreateCategoryModalOpen] =
- useState(false);
+ const [isCreateChannelOpen, setIsCreateChannelOpen] = useState(false);
+ const [isCreateCategoryOpen, setIsCreateCategoryOpen] = useState(false);
const [editingChannel, setEditingChannel] = useState(null);
const [editingCategory, setEditingCategory] = useState(null);
const [selectedCategoryId, setSelectedCategoryId] = useState(
null,
);
- const [collapsedCategories, setCollapsedCategories] = useState<
- Record
- >({});
-
const params = useParams();
const currentChannelId = params?.channelId as string;
const { activeServer } = useActiveServer();
@@ -46,100 +34,30 @@ export function ChannelSidebar() {
if (!activeServer) return null;
- /** Toggles the collapsed state of a channel category. */
- const toggleCategory = (categoryId: string) => {
- setCollapsedCategories((prev) => ({
- ...prev,
- [categoryId]: !prev[categoryId],
- }));
- };
-
- /** Handles mobile navigation closure when clicking a channel link. */
- const handleChannelClick = () => {
- if (!window.matchMedia("(min-width: 768px)").matches) {
- closeNav();
- }
- };
-
- /** Opens the edit channel modal for a specific channel. */
- const handleOpenChannelSettings = (e: React.MouseEvent, channel: Channel) => {
- e.preventDefault();
- e.stopPropagation();
- setEditingChannel(channel);
- };
-
- /** Opens the edit category modal for a specific category. */
- const handleOpenCategorySettings = (
- e: React.MouseEvent,
- category: Category,
- ) => {
- e.preventDefault();
- e.stopPropagation();
- setEditingCategory(category);
- };
-
- /** Opens the create channel modal for a specific category or uncategorized. */
- const handleOpenCreateModal = (categoryId: string | null = null) => {
- setSelectedCategoryId(categoryId);
- setIsCreateModalOpen(true);
+ /** Opens the create channel modal for an optional category. */
+ const openCreateChannel = (catId: string | null = null) => {
+ setSelectedCategoryId(catId);
+ setIsCreateChannelOpen(true);
};
const uncategorizedChannels = activeServer.channels.filter(
(c) => !c.categoryId,
);
-
- const categoriesList = (activeServer.categories || []).map((cat) => ({
+ const categories = (activeServer.categories || []).map((cat) => ({
...cat,
channels: activeServer.channels.filter((c) => c.categoryId === cat.id),
}));
- /** Renders an individual channel item in the list. */
- const renderChannelItem = (channel: Channel) => {
- const isActive = currentChannelId === channel.id;
- return (
-
-
-
- #
-
- {channel.name}
-
-
- {!channel.isDefault && (
-
- )}
-
- );
- };
-
return (
<>
- {/* Server Header */}
+ {/* Header */}
{activeServer.name}
- {/* Channel List */}
+ {/* Content */}
-
-
-
toggleCategory("uncategorized")}
- className="flex items-center gap-1 hover:text-white transition-colors cursor-pointer min-w-0 truncate"
- >
- {collapsedCategories["uncategorized"] ? (
-
- ) : (
-
- )}
- Text Channels
-
+
openCreateChannel(null)}
+ onEditChannel={setEditingChannel}
+ />
- handleOpenCreateModal(null)}
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer"
- aria-label="Create channel"
- >
-
-
-
-
- {!collapsedCategories["uncategorized"] && (
-
- {uncategorizedChannels.map(renderChannelItem)}
-
- )}
-
-
- {/* Custom Categories */}
- {categoriesList.map((category) => {
- const isCollapsed = collapsedCategories[category.id];
-
- return (
-
-
-
toggleCategory(category.id)}
- className="flex items-center gap-1 hover:text-white transition-colors cursor-pointer min-w-0 truncate"
- >
- {isCollapsed ? (
-
- ) : (
-
- )}
- {category.name}
-
-
-
-
handleOpenCategorySettings(e, category)}
- className="p-0.5 rounded text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer"
- aria-label="Edit category"
- >
-
-
-
handleOpenCreateModal(category.id)}
- className="p-0.5 rounded text-muted hover:text-white hover:bg-surface transition-colors cursor-pointer"
- aria-label="Create channel in category"
- >
-
-
-
-
-
- {!isCollapsed && (
-
- {category.channels.map(renderChannelItem)}
-
- )}
-
- );
- })}
+ {categories.map((category) => (
+
openCreateChannel(category.id)}
+ onEditChannel={setEditingChannel}
+ onEditCategory={() => setEditingCategory(category)}
+ />
+ ))}
{/* Modals */}
setIsCreateCategoryModalOpen(false)}
+ isOpen={isCreateCategoryOpen}
+ onClose={() => setIsCreateCategoryOpen(false)}
serverId={activeServer.id}
/>
-
setEditingCategory(null)}
category={editingCategory}
/>
-
setIsCreateModalOpen(false)}
+ isOpen={isCreateChannelOpen}
+ onClose={() => setIsCreateChannelOpen(false)}
serverId={activeServer.id}
defaultCategoryId={selectedCategoryId}
/>
-
setEditingChannel(null)}