Skip to content

Commit 980cf72

Browse files
committed
♻️ Refactor Sidebar component
1 parent e158499 commit 980cf72

2 files changed

Lines changed: 63 additions & 33 deletions

File tree

frontend/src/components/ui/sidebar.tsx

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
"use client"
2-
3-
import * as React from "react"
41
import { Slot } from "@radix-ui/react-slot"
52
import { cva, type VariantProps } from "class-variance-authority"
63
import { PanelLeftIcon } from "lucide-react"
4+
import * as React from "react"
75

8-
import { useIsMobile } from "@/hooks/use-mobile"
9-
import { cn } from "@/lib/utils"
106
import { Button } from "@/components/ui/button"
117
import { Input } from "@/components/ui/input"
128
import { Separator } from "@/components/ui/separator"
@@ -24,6 +20,8 @@ import {
2420
TooltipProvider,
2521
TooltipTrigger,
2622
} from "@/components/ui/tooltip"
23+
import { cn } from "@/lib/utils"
24+
import { useIsMobile } from "@/hooks/useMobile"
2725

2826
const SIDEBAR_COOKIE_NAME = "sidebar_state"
2927
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@@ -69,9 +67,21 @@ function SidebarProvider({
6967
const isMobile = useIsMobile()
7068
const [openMobile, setOpenMobile] = React.useState(false)
7169

70+
const getInitialOpen = () => {
71+
if (typeof document === "undefined") return defaultOpen
72+
73+
const cookie = document.cookie
74+
.split("; ")
75+
.find((c) => c.startsWith(`${SIDEBAR_COOKIE_NAME}=`))
76+
77+
if (!cookie) return defaultOpen
78+
79+
return cookie.split("=")[1] === "true"
80+
}
81+
7282
// This is the internal state of the sidebar.
7383
// We use openProp and setOpenProp for control from outside the component.
74-
const [_open, _setOpen] = React.useState(defaultOpen)
84+
const [_open, _setOpen] = React.useState(getInitialOpen)
7585
const open = openProp ?? _open
7686
const setOpen = React.useCallback(
7787
(value: boolean | ((value: boolean) => boolean)) => {
@@ -85,13 +95,13 @@ function SidebarProvider({
8595
// This sets the cookie to keep the sidebar state.
8696
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
8797
},
88-
[setOpenProp, open]
98+
[setOpenProp, open],
8999
)
90100

91101
// Helper to toggle the sidebar.
92102
const toggleSidebar = React.useCallback(() => {
93103
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
94-
}, [isMobile, setOpen, setOpenMobile])
104+
}, [isMobile, setOpen])
95105

96106
// Adds a keyboard shortcut to toggle the sidebar.
97107
React.useEffect(() => {
@@ -123,7 +133,7 @@ function SidebarProvider({
123133
setOpenMobile,
124134
toggleSidebar,
125135
}),
126-
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
136+
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
127137
)
128138

129139
return (
@@ -140,7 +150,7 @@ function SidebarProvider({
140150
}
141151
className={cn(
142152
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
143-
className
153+
className,
144154
)}
145155
{...props}
146156
>
@@ -171,7 +181,7 @@ function Sidebar({
171181
data-slot="sidebar"
172182
className={cn(
173183
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
174-
className
184+
className,
175185
)}
176186
{...props}
177187
>
@@ -223,7 +233,7 @@ function Sidebar({
223233
"group-data-[side=right]:rotate-180",
224234
variant === "floating" || variant === "inset"
225235
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
226-
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
236+
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
227237
)}
228238
/>
229239
<div
@@ -237,7 +247,7 @@ function Sidebar({
237247
variant === "floating" || variant === "inset"
238248
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
239249
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
240-
className
250+
className,
241251
)}
242252
{...props}
243253
>
@@ -258,7 +268,8 @@ function SidebarTrigger({
258268
onClick,
259269
...props
260270
}: React.ComponentProps<typeof Button>) {
261-
const { toggleSidebar } = useSidebar()
271+
const { toggleSidebar, open } = useSidebar()
272+
const sidebarCopy = open ? "Collapse Sidebar" : "Open Sidebar"
262273

263274
return (
264275
<Button
@@ -274,7 +285,7 @@ function SidebarTrigger({
274285
{...props}
275286
>
276287
<PanelLeftIcon />
277-
<span className="sr-only">Toggle Sidebar</span>
288+
<span className="sr-only">{sidebarCopy}</span>
278289
</Button>
279290
)
280291
}
@@ -297,7 +308,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
297308
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
298309
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
299310
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
300-
className
311+
className,
301312
)}
302313
{...props}
303314
/>
@@ -309,9 +320,9 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
309320
<main
310321
data-slot="sidebar-inset"
311322
className={cn(
312-
"bg-background relative flex w-full flex-1 flex-col",
323+
"bg-transparent relative flex w-full flex-1 flex-col",
313324
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
314-
className
325+
className,
315326
)}
316327
{...props}
317328
/>
@@ -337,7 +348,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
337348
<div
338349
data-slot="sidebar-header"
339350
data-sidebar="header"
340-
className={cn("flex flex-col gap-2 p-2", className)}
351+
className={cn("flex flex-col gap-2 p-2 pb-3", className)}
341352
{...props}
342353
/>
343354
)
@@ -348,7 +359,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
348359
<div
349360
data-slot="sidebar-footer"
350361
data-sidebar="footer"
351-
className={cn("flex flex-col gap-2 p-2", className)}
362+
className={cn("flex flex-col gap-3 p-2", className)}
352363
{...props}
353364
/>
354365
)
@@ -375,7 +386,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
375386
data-sidebar="content"
376387
className={cn(
377388
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
378-
className
389+
className,
379390
)}
380391
{...props}
381392
/>
@@ -387,7 +398,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
387398
<div
388399
data-slot="sidebar-group"
389400
data-sidebar="group"
390-
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
401+
className={cn("relative flex w-full min-w-0 flex-col px-2", className)}
391402
{...props}
392403
/>
393404
)
@@ -407,7 +418,7 @@ function SidebarGroupLabel({
407418
className={cn(
408419
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
409420
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
410-
className
421+
className,
411422
)}
412423
{...props}
413424
/>
@@ -430,7 +441,7 @@ function SidebarGroupAction({
430441
// Increases the hit area of the button on mobile.
431442
"after:absolute after:-inset-2 md:after:hidden",
432443
"group-data-[collapsible=icon]:hidden",
433-
className
444+
className,
434445
)}
435446
{...props}
436447
/>
@@ -474,7 +485,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
474485
}
475486

476487
const sidebarMenuButtonVariants = cva(
477-
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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",
488+
"peer/menu-button flex w-full items-center gap-2 overflow-hidden p-2 text-left text-sm outline-hidden 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 data-[state=open]:bg-sidebar-accent",
478489
{
479490
variants: {
480491
variant: {
@@ -483,16 +494,16 @@ const sidebarMenuButtonVariants = cva(
483494
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
484495
},
485496
size: {
486-
default: "h-8 text-sm",
497+
default: "h-8 text-sm rounded-lg",
487498
sm: "h-7 text-xs",
488-
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
499+
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0! rounded-xl data-[state=closed]:rounded-lg",
489500
},
490501
},
491502
defaultVariants: {
492503
variant: "default",
493504
size: "default",
494505
},
495-
}
506+
},
496507
)
497508

498509
function SidebarMenuButton({
@@ -569,8 +580,8 @@ function SidebarMenuAction({
569580
"peer-data-[size=lg]/menu-button:top-2.5",
570581
"group-data-[collapsible=icon]:hidden",
571582
showOnHover &&
572-
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
573-
className
583+
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
584+
className,
574585
)}
575586
{...props}
576587
/>
@@ -592,7 +603,7 @@ function SidebarMenuBadge({
592603
"peer-data-[size=default]/menu-button:top-1.5",
593604
"peer-data-[size=lg]/menu-button:top-2.5",
594605
"group-data-[collapsible=icon]:hidden",
595-
className
606+
className,
596607
)}
597608
{...props}
598609
/>
@@ -645,7 +656,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
645656
className={cn(
646657
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
647658
"group-data-[collapsible=icon]:hidden",
648-
className
659+
className,
649660
)}
650661
{...props}
651662
/>
@@ -691,7 +702,7 @@ function SidebarMenuSubButton({
691702
size === "sm" && "text-xs",
692703
size === "md" && "text-sm",
693704
"group-data-[collapsible=icon]:hidden",
694-
className
705+
className,
695706
)}
696707
{...props}
697708
/>

frontend/src/hooks/useMobile.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from "react"
2+
3+
const MOBILE_BREAKPOINT = 768
4+
5+
export function useIsMobile() {
6+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7+
8+
React.useEffect(() => {
9+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10+
const onChange = () => {
11+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12+
}
13+
mql.addEventListener("change", onChange)
14+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15+
return () => mql.removeEventListener("change", onChange)
16+
}, [])
17+
18+
return !!isMobile
19+
}

0 commit comments

Comments
 (0)