From ff54346a3523b8e674d5893c89278a94eaf9302d Mon Sep 17 00:00:00 2001 From: Chneemann Date: Mon, 14 Sep 2026 19:24:51 +0200 Subject: [PATCH] refactor(ui): split ChannelSidebar into smaller modular components --- components/sidebar/CategorySection.tsx | 113 +++++++++++++ components/sidebar/ChannelItem.tsx | 59 +++++++ components/sidebar/ChannelSidebar.tsx | 223 +++++-------------------- 3 files changed, 212 insertions(+), 183 deletions(-) create mode 100644 components/sidebar/CategorySection.tsx create mode 100644 components/sidebar/ChannelItem.tsx 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 */}
-
-
- + openCreateChannel(null)} + onEditChannel={setEditingChannel} + /> - -
- - {!collapsedCategories["uncategorized"] && ( -
- {uncategorizedChannels.map(renderChannelItem)} -
- )} -
- - {/* Custom Categories */} - {categoriesList.map((category) => { - const isCollapsed = collapsedCategories[category.id]; - - return ( -
-
- - -
- - -
-
- - {!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)}