Skip to content

Commit b76c796

Browse files
feat: Improve the dashboard Sidebar Nav structure
1 parent 678acc4 commit b76c796

File tree

6 files changed

+199
-181
lines changed

6 files changed

+199
-181
lines changed
Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,32 @@
1-
import { AppSidebar } from "@/components/app-sidebar"
2-
import {
3-
Breadcrumb,
4-
BreadcrumbItem,
5-
BreadcrumbLink,
6-
BreadcrumbList,
7-
BreadcrumbPage,
8-
BreadcrumbSeparator,
9-
} from "@/components/ui/breadcrumb"
10-
import { Separator } from "@/components/ui/separator"
11-
import {
12-
SidebarInset,
13-
SidebarProvider,
14-
SidebarTrigger,
15-
} from "@/components/ui/sidebar"
1+
import { PageHeader } from "@/components/page-header"
162

173
export default function Page() {
184
return (
19-
// TODO The sidebar needs to be moved to the layout
20-
<SidebarProvider>
21-
<AppSidebar />
22-
<SidebarInset>
23-
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
24-
<div className="flex items-center gap-2 px-4">
25-
<SidebarTrigger className="-ml-1" />
26-
<Separator orientation="vertical" className="mr-2 h-4" />
27-
<Breadcrumb>
28-
<BreadcrumbList>
29-
<BreadcrumbItem className="hidden md:block">
30-
<BreadcrumbLink href="#">
31-
Building Your Application
32-
</BreadcrumbLink>
33-
</BreadcrumbItem>
34-
<BreadcrumbSeparator className="hidden md:block" />
35-
<BreadcrumbItem>
36-
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
37-
</BreadcrumbItem>
38-
</BreadcrumbList>
39-
</Breadcrumb>
5+
<>
6+
<PageHeader
7+
items={[
8+
{
9+
href: "/dashboard",
10+
label: "Dashboard"
11+
}
12+
]}
13+
/>
14+
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
15+
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
16+
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
17+
Example
4018
</div>
41-
</header>
42-
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
43-
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
44-
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
45-
Example
46-
</div>
47-
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
48-
Example
49-
</div>
50-
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
51-
Example
52-
</div>
19+
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
20+
Example
5321
</div>
54-
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min flex items-center justify-center">
22+
<div className="aspect-video rounded-xl bg-muted/50 flex items-center justify-center">
5523
Example
5624
</div>
5725
</div>
58-
</SidebarInset>
59-
</SidebarProvider>
26+
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min flex items-center justify-center">
27+
Example
28+
</div>
29+
</div>
30+
</>
6031
)
6132
}

src/app/(dashboard)/layout.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
import { getSessionFromCookie } from "@/utils/auth";
2-
import { redirect } from "next/navigation";
1+
import { AppSidebar } from "@/components/app-sidebar"
2+
import { getSessionFromCookie } from "@/utils/auth"
3+
import {
4+
SidebarInset,
5+
SidebarProvider,
6+
} from "@/components/ui/sidebar"
7+
import { redirect } from "next/navigation"
38

49
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
5-
610
const session = await getSessionFromCookie()
711

812
if (!session) {
913
redirect('/')
1014
}
1115

1216
return (
13-
<>{children}</>
14-
);
17+
<SidebarProvider>
18+
<AppSidebar />
19+
<SidebarInset>
20+
{children}
21+
</SidebarInset>
22+
</SidebarProvider>
23+
)
1524
}

src/components/app-sidebar.tsx

Lines changed: 48 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"use client"
22

3-
import * as React from "react"
3+
import { type ComponentType } from "react"
4+
import type { Route } from 'next'
5+
46
import {
57
AudioWaveform,
6-
BookOpen,
7-
Bot,
88
Command,
99
Frame,
1010
GalleryVerticalEnd,
@@ -27,8 +27,33 @@ import {
2727
} from "@/components/ui/sidebar"
2828
import { useSessionStore } from "@/state/session"
2929

30+
export type NavItem = {
31+
title: string
32+
url: Route
33+
icon?: ComponentType
34+
}
35+
36+
export type NavMainItem = NavItem & {
37+
isActive?: boolean
38+
items?: NavItem[]
39+
}
40+
41+
type Data = {
42+
user: {
43+
name: string
44+
email: string
45+
}
46+
teams: {
47+
name: string
48+
logo: ComponentType
49+
plan: string
50+
}[]
51+
navMain: NavMainItem[]
52+
projects: NavItem[]
53+
}
54+
3055
// This is sample data.
31-
const data = {
56+
const data: Data = {
3257
user: {
3358
name: "shadcn",
3459
email: "m@example.com",
@@ -52,66 +77,24 @@ const data = {
5277
],
5378
navMain: [
5479
{
55-
title: "Playground",
56-
url: "#",
80+
title: "Dashboard",
81+
url: "/dashboard",
5782
icon: SquareTerminal,
5883
isActive: true,
59-
items: [
60-
{
61-
title: "History",
62-
url: "#",
63-
},
64-
{
65-
title: "Starred",
66-
url: "#",
67-
},
68-
{
69-
title: "Settings",
70-
url: "#",
71-
},
72-
],
73-
},
74-
{
75-
title: "Models",
76-
url: "#",
77-
icon: Bot,
78-
items: [
79-
{
80-
title: "Genesis",
81-
url: "#",
82-
},
83-
{
84-
title: "Explorer",
85-
url: "#",
86-
},
87-
{
88-
title: "Quantum",
89-
url: "#",
90-
},
91-
],
92-
},
93-
{
94-
title: "Documentation",
95-
url: "#",
96-
icon: BookOpen,
97-
items: [
98-
{
99-
title: "Introduction",
100-
url: "#",
101-
},
102-
{
103-
title: "Get Started",
104-
url: "#",
105-
},
106-
{
107-
title: "Tutorials",
108-
url: "#",
109-
},
110-
{
111-
title: "Changelog",
112-
url: "#",
113-
},
114-
],
84+
// items: [
85+
// {
86+
// title: "History",
87+
// url: "#",
88+
// },
89+
// {
90+
// title: "Starred",
91+
// url: "#",
92+
// },
93+
// {
94+
// title: "Settings",
95+
// url: "#",
96+
// },
97+
// ],
11598
},
11699
{
117100
title: "Settings",
@@ -139,17 +122,17 @@ const data = {
139122
],
140123
projects: [
141124
{
142-
name: "Design Engineering",
125+
title: "Design Engineering",
143126
url: "#",
144127
icon: Frame,
145128
},
146129
{
147-
name: "Sales & Marketing",
130+
title: "Sales & Marketing",
148131
url: "#",
149132
icon: PieChart,
150133
},
151134
{
152-
name: "Travel",
135+
title: "Travel",
153136
url: "#",
154137
icon: Map,
155138
},

src/components/nav-main.tsx

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { ChevronRight, type LucideIcon } from "lucide-react"
3+
import { ChevronRight } from "lucide-react"
44

55
import {
66
Collapsible,
@@ -19,62 +19,73 @@ import {
1919
} from "@/components/ui/sidebar"
2020
import Link from "next/link"
2121
import type { Route } from "next"
22+
import type { NavMainItem } from "./app-sidebar"
23+
24+
type Props = {
25+
items: NavMainItem[]
26+
}
2227

2328
export function NavMain({
2429
items,
25-
}: {
26-
items: {
27-
title: string
28-
url: string
29-
icon?: LucideIcon
30-
isActive?: boolean
31-
items?: {
32-
title: string
33-
url: string
34-
}[]
35-
}[]
36-
}) {
30+
}: Props) {
3731
return (
3832
<SidebarGroup>
3933
<SidebarGroupLabel>Platform</SidebarGroupLabel>
4034
<SidebarMenu>
41-
{items.map((item) => (
42-
<Collapsible
43-
key={item.title}
44-
asChild
45-
defaultOpen={item.isActive}
46-
className="group/collapsible"
47-
>
48-
<SidebarMenuItem>
49-
<CollapsibleTrigger asChild>
50-
<SidebarMenuButton tooltip={item.title}>
51-
{item.icon && <item.icon />}
52-
<span>{item.title}</span>
53-
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
35+
{items.map((item) => {
36+
// If there are no child items, render a direct link
37+
if (!item.items?.length) {
38+
return (
39+
<SidebarMenuItem key={item.title}>
40+
<SidebarMenuButton asChild tooltip={item.title}>
41+
<Link href={item.url as Route}>
42+
{item.icon && <item.icon />}
43+
<span>{item.title}</span>
44+
</Link>
5445
</SidebarMenuButton>
55-
</CollapsibleTrigger>
56-
<CollapsibleContent>
57-
<SidebarMenuSub>
58-
{item.items?.map((subItem) => (
59-
<SidebarMenuSubItem key={subItem.title}>
60-
<SidebarMenuSubButton asChild>
61-
{subItem.url.startsWith('/') ? (
62-
<Link href={subItem.url as Route}>
63-
<span>{subItem.title}</span>
64-
</Link>
65-
) : (
66-
<a href={subItem.url}>
67-
<span>{subItem.title}</span>
68-
</a>
69-
)}
70-
</SidebarMenuSubButton>
71-
</SidebarMenuSubItem>
72-
))}
73-
</SidebarMenuSub>
74-
</CollapsibleContent>
75-
</SidebarMenuItem>
76-
</Collapsible>
77-
))}
46+
</SidebarMenuItem>
47+
)
48+
}
49+
50+
// Otherwise render the collapsible menu
51+
return (
52+
<Collapsible
53+
key={item.title}
54+
asChild
55+
defaultOpen={item.isActive}
56+
className="group/collapsible"
57+
>
58+
<SidebarMenuItem>
59+
<CollapsibleTrigger asChild>
60+
<SidebarMenuButton tooltip={item.title}>
61+
{item.icon && <item.icon />}
62+
<span>{item.title}</span>
63+
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
64+
</SidebarMenuButton>
65+
</CollapsibleTrigger>
66+
<CollapsibleContent>
67+
<SidebarMenuSub>
68+
{item.items?.map((subItem) => (
69+
<SidebarMenuSubItem key={subItem.title}>
70+
<SidebarMenuSubButton asChild>
71+
{subItem.url.startsWith('/') ? (
72+
<Link href={subItem.url as Route}>
73+
<span>{subItem.title}</span>
74+
</Link>
75+
) : (
76+
<a href={subItem.url}>
77+
<span>{subItem.title}</span>
78+
</a>
79+
)}
80+
</SidebarMenuSubButton>
81+
</SidebarMenuSubItem>
82+
))}
83+
</SidebarMenuSub>
84+
</CollapsibleContent>
85+
</SidebarMenuItem>
86+
</Collapsible>
87+
)
88+
})}
7889
</SidebarMenu>
7990
</SidebarGroup>
8091
)

0 commit comments

Comments
 (0)