Skip to content

Commit f3557ea

Browse files
committed
✨ Add Sidebar components
1 parent 64153d7 commit f3557ea

5 files changed

Lines changed: 233 additions & 12 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Briefcase, Home, Users } from "lucide-react"
2+
3+
import { SidebarAppearance } from "@/components/Common/Appearance"
4+
import { Logo } from "@/components/Common/Logo"
5+
import {
6+
Sidebar,
7+
SidebarContent,
8+
SidebarFooter,
9+
SidebarHeader,
10+
} from "@/components/ui/sidebar"
11+
import useAuth from "@/hooks/useAuth"
12+
import { type Item, Main } from "./Main"
13+
import { User } from "./User"
14+
15+
const baseItems: Item[] = [
16+
{ icon: Home, title: "Dashboard", path: "/" },
17+
{ icon: Briefcase, title: "Items", path: "/items" },
18+
]
19+
20+
export function AppSidebar() {
21+
const { user: currentUser } = useAuth()
22+
23+
const items = currentUser?.is_superuser
24+
? [...baseItems, { icon: Users, title: "Admin", path: "/admin" }]
25+
: baseItems
26+
27+
return (
28+
<Sidebar collapsible="icon">
29+
<SidebarHeader className="px-4 py-6 group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:items-center">
30+
<Logo variant="responsive" />
31+
</SidebarHeader>
32+
<SidebarContent>
33+
<Main items={items} />
34+
</SidebarContent>
35+
<SidebarFooter>
36+
<SidebarAppearance />
37+
<User user={currentUser} />
38+
</SidebarFooter>
39+
</Sidebar>
40+
)
41+
}
42+
43+
export default AppSidebar
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Link as RouterLink, useRouterState } from "@tanstack/react-router"
2+
import type { LucideIcon } from "lucide-react"
3+
4+
import {
5+
SidebarGroup,
6+
SidebarGroupContent,
7+
SidebarMenu,
8+
SidebarMenuButton,
9+
SidebarMenuItem,
10+
useSidebar,
11+
} from "@/components/ui/sidebar"
12+
13+
export type Item = {
14+
icon: LucideIcon
15+
title: string
16+
path: string
17+
}
18+
19+
interface MainProps {
20+
items: Item[]
21+
}
22+
23+
export function Main({ items }: MainProps) {
24+
const { isMobile, setOpenMobile } = useSidebar()
25+
const router = useRouterState()
26+
const currentPath = router.location.pathname
27+
28+
const handleMenuClick = () => {
29+
if (isMobile) {
30+
setOpenMobile(false)
31+
}
32+
}
33+
34+
return (
35+
<SidebarGroup>
36+
<SidebarGroupContent>
37+
<SidebarMenu>
38+
{items.map((item) => {
39+
const isActive = currentPath === item.path
40+
41+
return (
42+
<SidebarMenuItem key={item.title}>
43+
<SidebarMenuButton
44+
tooltip={item.title}
45+
isActive={isActive}
46+
asChild
47+
>
48+
<RouterLink to={item.path} onClick={handleMenuClick}>
49+
<item.icon />
50+
<span>{item.title}</span>
51+
</RouterLink>
52+
</SidebarMenuButton>
53+
</SidebarMenuItem>
54+
)
55+
})}
56+
</SidebarMenu>
57+
</SidebarGroupContent>
58+
</SidebarGroup>
59+
)
60+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Link as RouterLink } from "@tanstack/react-router"
2+
import { ChevronsUpDown, LogOut, Settings } from "lucide-react"
3+
4+
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
5+
import {
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuLabel,
10+
DropdownMenuSeparator,
11+
DropdownMenuTrigger,
12+
} from "@/components/ui/dropdown-menu"
13+
import {
14+
SidebarMenu,
15+
SidebarMenuButton,
16+
SidebarMenuItem,
17+
useSidebar,
18+
} from "@/components/ui/sidebar"
19+
import useAuth from "@/hooks/useAuth"
20+
import { getInitials } from "@/utils"
21+
22+
interface UserInfoProps {
23+
fullName?: string
24+
email?: string
25+
}
26+
27+
function UserInfo({ fullName, email }: UserInfoProps) {
28+
return (
29+
<div className="flex items-center gap-2.5 w-full min-w-0">
30+
<Avatar className="size-8">
31+
<AvatarFallback className="bg-zinc-600 text-white">
32+
{getInitials(fullName || "User")}
33+
</AvatarFallback>
34+
</Avatar>
35+
<div className="flex flex-col items-start min-w-0">
36+
<p className="text-sm font-medium truncate w-full">{fullName}</p>
37+
<p className="text-xs text-muted-foreground truncate w-full">{email}</p>
38+
</div>
39+
</div>
40+
)
41+
}
42+
43+
export function User({ user }: { user: any }) {
44+
const { logout } = useAuth()
45+
const { isMobile, setOpenMobile } = useSidebar()
46+
47+
if (!user) return null
48+
49+
const handleMenuClick = () => {
50+
if (isMobile) {
51+
setOpenMobile(false)
52+
}
53+
}
54+
const handleLogout = async () => {
55+
logout()
56+
}
57+
58+
return (
59+
<SidebarMenu>
60+
<SidebarMenuItem>
61+
<DropdownMenu>
62+
<DropdownMenuTrigger asChild>
63+
<SidebarMenuButton
64+
size="lg"
65+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
66+
data-testid="user-menu"
67+
>
68+
<UserInfo fullName={user?.full_name} email={user?.email} />
69+
<ChevronsUpDown className="ml-auto size-4 text-muted-foreground" />
70+
</SidebarMenuButton>
71+
</DropdownMenuTrigger>
72+
<DropdownMenuContent
73+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
74+
side={isMobile ? "bottom" : "right"}
75+
align="end"
76+
sideOffset={4}
77+
>
78+
<DropdownMenuLabel className="p-0 font-normal">
79+
<UserInfo fullName={user?.full_name} email={user?.email} />
80+
</DropdownMenuLabel>
81+
<DropdownMenuSeparator />
82+
<RouterLink to="/settings" onClick={handleMenuClick}>
83+
<DropdownMenuItem>
84+
<Settings />
85+
User Settings
86+
</DropdownMenuItem>
87+
</RouterLink>
88+
<DropdownMenuItem
89+
onClick={handleLogout}
90+
>
91+
<LogOut />
92+
Log Out
93+
</DropdownMenuItem>
94+
</DropdownMenuContent>
95+
</DropdownMenu>
96+
</SidebarMenuItem>
97+
</SidebarMenu>
98+
)
99+
}

frontend/src/routes/_layout.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { Flex } from "@chakra-ui/react"
21
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
32

4-
import Navbar from "@/components/Common/Navbar"
5-
import Sidebar from "@/components/Common/Sidebar"
3+
import { Footer } from "@/components/Common/Footer"
4+
import AppSidebar from "@/components/Sidebar/AppSidebar"
5+
import {
6+
SidebarInset,
7+
SidebarProvider,
8+
SidebarTrigger,
9+
} from "@/components/ui/sidebar"
610
import { isLoggedIn } from "@/hooks/useAuth"
711

812
export const Route = createFileRoute("/_layout")({
@@ -18,15 +22,18 @@ export const Route = createFileRoute("/_layout")({
1822

1923
function Layout() {
2024
return (
21-
<Flex direction="column" h="100vh">
22-
<Navbar />
23-
<Flex flex="1" overflow="hidden">
24-
<Sidebar />
25-
<Flex flex="1" direction="column" p={4} overflowY="auto">
25+
<SidebarProvider>
26+
<AppSidebar />
27+
<SidebarInset>
28+
<header className="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b px-4">
29+
<SidebarTrigger className="-ml-1 text-muted-foreground" />
30+
</header>
31+
<main className="flex-1 p-4">
2632
<Outlet />
27-
</Flex>
28-
</Flex>
29-
</Flex>
33+
</main>
34+
<Footer />
35+
</SidebarInset>
36+
</SidebarProvider>
3037
)
3138
}
3239

frontend/src/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,19 @@ function extractErrorMessage(err: ApiError): string {
1313
return errDetail || "Something went wrong."
1414
}
1515

16-
export const handleError = function (this: (msg: string) => void, err: ApiError) {
16+
export const handleError = function (
17+
this: (msg: string) => void,
18+
err: ApiError,
19+
) {
1720
const errorMessage = extractErrorMessage(err)
1821
this(errorMessage)
1922
}
23+
24+
export const getInitials = (name: string): string => {
25+
return name
26+
.split(" ")
27+
.slice(0, 2)
28+
.map((word) => word[0])
29+
.join("")
30+
.toUpperCase()
31+
}

0 commit comments

Comments
 (0)