From a37554d2057f044e7af99a60dc3455892700f696 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:52:00 +0100 Subject: [PATCH] refactor: sidebar --- apps/web/src/app/(ui)/layout.tsx | 6 +- .../src/components/app/Sidebar/Sidebar.tsx | 192 ++++++++++-------- apps/web/src/components/ui/sidebar.tsx | 4 +- 3 files changed, 115 insertions(+), 87 deletions(-) diff --git a/apps/web/src/app/(ui)/layout.tsx b/apps/web/src/app/(ui)/layout.tsx index 73f15de..d81b879 100644 --- a/apps/web/src/app/(ui)/layout.tsx +++ b/apps/web/src/app/(ui)/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; +import { cookies } from 'next/headers'; import '../globals.css'; import Navbar from '@/components/app/NavBar/NavBar'; import { SessionProvider } from '@/lib/providers/SessionProvider'; @@ -31,6 +32,9 @@ export default async function RootLayout({ children: React.ReactNode; }>) { const sessionData = await validateRequest(); + const cookieStore = await cookies(); + const defaultOpen = cookieStore.get('sidebar:state')?.value === 'true'; + return ( @@ -47,7 +51,7 @@ export default async function RootLayout({ /> - + {/* this promise is ugly but i'm lazy to fix the type errors */} )} /> diff --git a/apps/web/src/components/app/Sidebar/Sidebar.tsx b/apps/web/src/components/app/Sidebar/Sidebar.tsx index 84ab6a0..5ca3dfc 100644 --- a/apps/web/src/components/app/Sidebar/Sidebar.tsx +++ b/apps/web/src/components/app/Sidebar/Sidebar.tsx @@ -1,8 +1,8 @@ 'use client'; import * as React from 'react'; -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { Avatar } from '@/components/ui/avatar'; +import { ChevronDown, ChevronUp, Radio } from 'lucide-react'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Sidebar as UISidebar, SidebarContent, @@ -13,131 +13,153 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarTrigger, + useSidebar, } from '@/components/ui/sidebar'; import { StreamInfoResponse, useStreams } from '@/lib/providers/StreamInfoProvider'; import { useRouter } from 'next/navigation'; import { Skeleton } from '@/components/ui/skeleton'; import { useAllChannels } from '@/lib/hooks/useUserList'; +import { cn } from '@/lib/utils'; +import { Separator } from '@/components/ui/separator'; export default function Sidebar({ ...props }: React.ComponentProps) { const { channels: stream, isLoading } = useAllChannels(5000); const [followedExpanded, setFollowedExpanded] = React.useState(true); + const { state } = useSidebar(); + const isCollapsed = state === 'collapsed'; - if (isLoading) return ; + if (isLoading) return ; const liveStreamers = stream?.filter((s) => s.isLive) || []; const offlineStreamers = stream?.filter((s) => !s.isLive) || []; return ( - + - - + + + Live Channels + + + {liveStreamers.length} + - {followedExpanded && ( - - - {liveStreamers.map((streamer) => ( - - ))} - - - )} + + + {liveStreamers.length === 0 && !isCollapsed && ( +
+ No channels live +
+ )} + {liveStreamers.map((streamer) => ( + + ))} +
+
- {offlineStreamers.length > 0 && ( - - Offline Channels - - - {offlineStreamers.map((streamer) => ( - - ))} - - - - )} + + + + + + Offline Channels + + + + + {offlineStreamers.map((streamer) => ( + + ))} + + +
); } -function StreamerItem({ streamer }: { streamer: StreamInfoResponse[0] }) { +function StreamerItem({ streamer, isCollapsed }: { streamer: StreamInfoResponse[0], isCollapsed: boolean }) { const router = useRouter(); + return ( - - { - router.push(`/${streamer.username}`); - }}> -
- - {/* eslint-disable-next-line @next/next/no-img-element */} - {streamer.username} - - {streamer.isLive && ( - + + router.push(`/${streamer.username}`)} + > +
-
-

{streamer.username}

-

{streamer.category}

- {streamer.isLive && ( -

- {streamer.viewers} viewer{streamer.viewers === 1 ? '' : 's'} -

- )} -
+
); } function SidebarSkeleton({ ...props }: React.ComponentProps) { + const { state } = useSidebar(); + const isCollapsed = state === 'collapsed'; + return ( - + - - + + - {Array(3).fill(0).map((_, i) => ( - + ))} + + - Offline Channels + + + {Array(5).fill(0).map((_, i) => ( - + ))} @@ -147,16 +169,18 @@ function SidebarSkeleton({ ...props }: React.ComponentProps) { ); } -function StreamerItemSkeleton() { +function StreamerItemSkeleton({ isCollapsed }: { isCollapsed: boolean }) { return ( - -
- -
-
- - + +
+ + {!isCollapsed && ( +
+ + +
+ )}
diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 91e90e4..72d6e4f 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -23,7 +23,7 @@ const SIDEBAR_COOKIE_NAME = "sidebar:state" const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_WIDTH = "16rem" const SIDEBAR_WIDTH_MOBILE = "18rem" -const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_WIDTH_ICON = "4rem" const SIDEBAR_KEYBOARD_SHORTCUT = "b" type SidebarContext = { @@ -512,7 +512,7 @@ const SidebarMenuItem = React.forwardRef< SidebarMenuItem.displayName = "SidebarMenuItem" const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-12 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { variants: { variant: {