feat(v4): move dashboard

This commit is contained in:
shadcn
2025-03-17 22:26:43 +04:00
parent 4ebfd6896f
commit 829ef0cbf6
42 changed files with 1063 additions and 1063 deletions

View File

@@ -1,183 +0,0 @@
"use client"
import * as React from "react"
import {
IconCamera,
IconChartBar,
IconDashboard,
IconDatabase,
IconFileAi,
IconFileDescription,
IconFileWord,
IconFolder,
IconHelp,
IconInnerShadowTop,
IconListDetails,
IconReport,
IconSearch,
IconSettings,
IconUsers,
} from "@tabler/icons-react"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
// import { NavClouds } from "@/app/(examples)/dashboard-02/components/nav-clouds"
import { NavDocuments } from "@/app/(examples)/dashboard-02/components/nav-documents"
import { NavMain } from "@/app/(examples)/dashboard-02/components/nav-main"
import { NavSecondary } from "@/app/(examples)/dashboard-02/components/nav-secondary"
import { NavUser } from "@/app/(examples)/dashboard-02/components/nav-user"
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: "#",
icon: IconDashboard,
},
{
title: "Lifecycle",
url: "#",
icon: IconListDetails,
},
{
title: "Analytics",
url: "#",
icon: IconChartBar,
},
{
title: "Projects",
url: "#",
icon: IconFolder,
},
{
title: "Team",
url: "#",
icon: IconUsers,
},
],
navClouds: [
{
title: "Capture",
icon: IconCamera,
isActive: true,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Proposal",
icon: IconFileDescription,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Prompts",
icon: IconFileAi,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Settings",
url: "#",
icon: IconSettings,
},
{
title: "Get Help",
url: "#",
icon: IconHelp,
},
{
title: "Search",
url: "#",
icon: IconSearch,
},
],
documents: [
{
name: "Data Library",
url: "#",
icon: IconDatabase,
},
{
name: "Reports",
url: "#",
icon: IconReport,
},
{
name: "Word Assistant",
url: "#",
icon: IconFileWord,
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="offcanvas" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
>
<a href="#">
<IconInnerShadowTop className="!size-5" />
<span className="text-base font-semibold">Acme Inc.</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
{/* <NavClouds items={data.navClouds} /> */}
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
)
}

View File

@@ -1,56 +0,0 @@
"use client"
import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon?: Icon
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<SidebarMenuButton
tooltip="Quick Create"
className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
>
<IconCirclePlusFilled />
<span>Quick Create</span>
</SidebarMenuButton>
<Button
size="icon"
className="size-8 group-data-[collapsible=icon]:opacity-0"
variant="outline"
>
<IconMail />
<span className="sr-only">Inbox</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuButton key={item.title} tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</SidebarMenuButton>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}

View File

@@ -1,71 +0,0 @@
"use client"
import * as React from "react"
import { IconBrightness, type Icon } from "@tabler/icons-react"
import { useTheme } from "next-themes"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: Icon
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
const { resolvedTheme, setTheme } = useTheme()
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
<SidebarMenu className="group-data-[collapsible=icon]:hidden">
<SidebarMenuButton asChild>
<label>
<IconBrightness />
<span>Dark Mode</span>
{mounted ? (
<Switch
className="ml-auto"
checked={resolvedTheme !== "light"}
onCheckedChange={() =>
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
/>
) : (
<Skeleton className="ml-auto h-4 w-8 rounded-full" />
)}
</label>
</SidebarMenuButton>
</SidebarMenu>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}

View File

@@ -1,110 +0,0 @@
"use client"
import {
IconCreditCard,
IconDotsVertical,
IconLogout,
IconNotification,
IconUserCircle,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/registry/new-york-v4/ui/sidebar"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg grayscale">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
<IconDotsVertical className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconUserCircle />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<IconCreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<IconNotification />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<IconLogout />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -1,34 +0,0 @@
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { SidebarTrigger } from "@/registry/new-york-v4/ui/sidebar"
import { ModeToggle } from "@/app/(examples)/dashboard-02/components/mode-toggle"
import { ThemeSelector } from "@/app/(examples)/dashboard-02/components/theme-selector"
export function SiteHeader() {
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
<h1 className="text-base font-medium">Documents</h1>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard-02"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
GitHub
</a>
</Button>
<ThemeSelector />
<ModeToggle />
</div>
</div>
</header>
)
}

View File

@@ -1,36 +0,0 @@
import { cookies } from "next/headers"
import {
SidebarInset,
SidebarProvider,
} from "@/registry/new-york-v4/ui/sidebar"
import { AppSidebar } from "@/app/(examples)/dashboard-02/components/app-sidebar"
import { SiteHeader } from "@/app/(examples)/dashboard-02/components/site-header"
import "./theme.css"
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<SidebarProvider
defaultOpen={defaultOpen}
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">{children}</div>
</SidebarInset>
</SidebarProvider>
)
}

View File

@@ -1,125 +0,0 @@
import { Metadata } from "next"
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import { ChartAreaInteractive } from "@/app/(examples)/dashboard-02/components/chart-area-interactive"
import { DataTable } from "@/app/(examples)/dashboard-02/components/data-table"
import data from "./data.json"
export const metadata: Metadata = {
title: "Dashboard",
description: "A dashboard with tabs, table and sidebar.",
}
export default async function Dashboard02() {
return (
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
<Card className="@container/card">
<CardHeader>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
$1,250.00
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-2.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Trending up this month <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Visitors for the last 6 months
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>New Customers</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
1,234
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingDown />
-20%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Down 20% this period <IconTrendingDown className="size-4" />
</div>
<div className="text-muted-foreground">
Acquisition needs attention
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
45,678
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Strong user retention <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Engagement exceed targets
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
4.5%
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+4.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Steady performance increase{" "}
<IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Meets growth projections
</div>
</CardFooter>
</Card>
</div>
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div>
)
}

View File

@@ -0,0 +1,84 @@
"use client"
import * as React from "react"
import {
ChartLineIcon,
FileIcon,
HomeIcon,
LifeBuoy,
Send,
Settings2Icon,
ShoppingBagIcon,
ShoppingCartIcon,
UserIcon,
} from "lucide-react"
import { Sidebar, SidebarContent } from "@/registry/new-york-v4/ui/sidebar"
import { NavMain } from "@/app/(examples)/dashboard-03/components/nav-main"
import { NavSecondary } from "@/app/(examples)/dashboard-03/components/nav-secondary"
const data = {
navMain: [
{
title: "Dashboard",
url: "/dashboard",
icon: HomeIcon,
},
{
title: "Analytics",
url: "/dashboard/analytics",
icon: ChartLineIcon,
},
{
title: "Orders",
url: "/dashboard/orders",
icon: ShoppingBagIcon,
},
{
title: "Products",
url: "/dashboard/products",
icon: ShoppingCartIcon,
},
{
title: "Invoices",
url: "/dashboard/invoices",
icon: FileIcon,
},
{
title: "Customers",
url: "/dashboard/customers",
icon: UserIcon,
},
{
title: "Settings",
url: "/dashboard/settings",
icon: Settings2Icon,
},
],
navSecondary: [
{
title: "Support",
url: "#",
icon: LifeBuoy,
},
{
title: "Feedback",
url: "#",
icon: Send,
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar
className="top-(--header-height) h-[calc(100svh-var(--header-height))]!"
{...props}
>
<SidebarContent>
<NavMain items={data.navMain} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
</Sidebar>
)
}

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { IconBrightness } from "@tabler/icons-react" import { MoonIcon, SunIcon } from "lucide-react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
@@ -20,7 +20,8 @@ export function ModeToggle() {
className="group/toggle size-8" className="group/toggle size-8"
onClick={toggleTheme} onClick={toggleTheme}
> >
<IconBrightness /> <SunIcon className="hidden [html.dark_&]:block" />
<MoonIcon className="hidden [html.light_&]:block" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
) )

View File

@@ -0,0 +1,91 @@
"use client"
import { usePathname } from "next/navigation"
import { ChevronRight, type LucideIcon } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/new-york-v4/ui/collapsible"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/registry/new-york-v4/ui/sidebar"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
items?: {
title: string
url: string
}[]
disabled?: boolean
}[]
}) {
const pathname = usePathname()
return (
<SidebarGroup>
<SidebarGroupLabel>Dashboard</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible key={item.title} asChild defaultOpen={item.isActive}>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={pathname === item.url}
disabled={item.disabled}
>
<a
href={item.disabled ? "#" : item.url}
data-disabled={item.disabled}
className="data-[disabled=true]:opacity-50"
>
<item.icon className="text-muted-foreground" />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.items?.length ? (
<>
<CollapsibleTrigger asChild>
<SidebarMenuAction className="data-[state=open]:rotate-90">
<ChevronRight />
<span className="sr-only">Toggle</span>
</SidebarMenuAction>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -0,0 +1,40 @@
import * as React from "react"
import { type LucideIcon } from "lucide-react"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: LucideIcon
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm">
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}

View File

@@ -0,0 +1,90 @@
"use client"
import { BadgeCheck, Bell, CreditCard, LogOut, Sparkles } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Avatar className="size-8 rounded-md">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side="bottom"
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,103 @@
"use client"
import { Fragment, useMemo } from "react"
import { usePathname } from "next/navigation"
import { SidebarIcon } from "lucide-react"
import { ThemeSelector } from "@/components/theme-selector"
import { SearchForm } from "@/registry/new-york-v4/blocks/sidebar-16/components/search-form"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/registry/new-york-v4/ui/breadcrumb"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { useSidebar } from "@/registry/new-york-v4/ui/sidebar"
import { ModeToggle } from "@/app/(examples)/dashboard-03/components/mode-toggle"
import { NavUser } from "@/app/(examples)/dashboard-03/components/nav-user"
export function SiteHeader() {
const { toggleSidebar } = useSidebar()
const pathname = usePathname()
// Faux breadcrumbs for demo.
const breadcrumbs = useMemo(() => {
return pathname
.split("/")
.filter((path) => path !== "")
.map((path, index, array) => ({
label: path,
href: `/${array.slice(0, index + 1).join("/")}`,
}))
}, [pathname])
return (
<header
data-slot="site-header"
className="bg-background sticky top-0 z-50 flex w-full items-center border-b"
>
<div className="flex h-(--header-height) w-full items-center gap-2 px-2 pr-4">
<Button
variant="ghost"
size="sm"
onClick={toggleSidebar}
className="gap-2.5 has-[>svg]:px-2"
>
<SidebarIcon />
<span className="truncate font-medium">Acme Inc</span>
</Button>
<Separator
orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4"
/>
<Breadcrumb className="hidden sm:block">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" className="capitalize">
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
{breadcrumbs.map((breadcrumb, index) =>
index === breadcrumbs.length - 1 ? (
<BreadcrumbItem key={index}>
<BreadcrumbPage className="capitalize">
{breadcrumb.label}
</BreadcrumbPage>
</BreadcrumbItem>
) : (
<Fragment key={index}>
<BreadcrumbItem>
<BreadcrumbLink
href={breadcrumb.href}
className="capitalize"
>
{breadcrumb.label}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</Fragment>
)
)}
</BreadcrumbList>
</Breadcrumb>
<div className="ml-auto flex items-center gap-2">
<SearchForm className="w-fullsm:w-auto" />
<ThemeSelector />
<ModeToggle />
<NavUser
user={{
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
}}
/>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,31 @@
import { cookies } from "next/headers"
import {
SidebarInset,
SidebarProvider,
} from "@/registry/new-york-v4/ui/sidebar"
import { AppSidebar } from "@/app/(examples)/dashboard-03/components/app-sidebar"
import { SiteHeader } from "@/app/(examples)/dashboard-03/components/site-header"
import "../../themes.css"
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<main className="[--header-height:calc(theme(spacing.14))]">
<SidebarProvider defaultOpen={defaultOpen} className="flex flex-col">
<SiteHeader />
<div className="flex flex-1">
<AppSidebar />
<SidebarInset>{children}</SidebarInset>
</div>
</SidebarProvider>
</main>
)
}

View File

@@ -0,0 +1,206 @@
import { Metadata } from "next"
import {
DownloadIcon,
FilterIcon,
TrendingDownIcon,
TrendingUpIcon,
} from "lucide-react"
import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/registry/new-york-v4/ui/card"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import { AnalyticsDatePicker } from "@/app/(examples)/dashboard-03/components/analytics-date-picker"
import { ChartRevenue } from "@/app/(examples)/dashboard-03/components/chart-revenue"
import { ChartVisitors } from "@/app/(examples)/dashboard-03/components/chart-visitors"
import { ProductsTable } from "@/app/(examples)/dashboard-03/components/products-table"
export const metadata: Metadata = {
title: "Dashboard",
description: "An example dashboard to test the new components.",
}
// Load from database.
const products = [
{
id: "1",
name: "BJÖRKSNÄS Dining Table",
price: 599.99,
stock: 12,
dateAdded: "2023-06-15",
status: "In Stock",
},
{
id: "2",
name: "POÄNG Armchair",
price: 249.99,
stock: 28,
dateAdded: "2023-07-22",
status: "In Stock",
},
{
id: "3",
name: "MALM Bed Frame",
price: 399.99,
stock: 15,
dateAdded: "2023-08-05",
status: "In Stock",
},
{
id: "4",
name: "KALLAX Shelf Unit",
price: 179.99,
stock: 32,
dateAdded: "2023-09-12",
status: "In Stock",
},
{
id: "5",
name: "STOCKHOLM Rug",
price: 299.99,
stock: 8,
dateAdded: "2023-10-18",
status: "Low Stock",
},
{
id: "6",
name: "KIVIK Sofa",
price: 899.99,
stock: 6,
dateAdded: "2023-11-02",
status: "Low Stock",
},
{
id: "7",
name: "LISABO Coffee Table",
price: 149.99,
stock: 22,
dateAdded: "2023-11-29",
status: "In Stock",
},
{
id: "8",
name: "HEMNES Bookcase",
price: 249.99,
stock: 17,
dateAdded: "2023-12-10",
status: "In Stock",
},
{
id: "9",
name: "EKEDALEN Dining Chairs (Set of 2)",
price: 199.99,
stock: 14,
dateAdded: "2024-01-05",
status: "In Stock",
},
{
id: "10",
name: "FRIHETEN Sleeper Sofa",
price: 799.99,
stock: 9,
dateAdded: "2024-01-18",
status: "Low Stock",
},
]
export default function DashboardPage() {
return (
<div className="@container/page flex flex-1 flex-col gap-8 p-6">
<Tabs defaultValue="overview" className="gap-6">
<div
data-slot="dashboard-header"
className="flex items-center justify-between"
>
<TabsList className="w-full @3xl/page:w-fit">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
<TabsTrigger value="exports" disabled>
Exports
</TabsTrigger>
</TabsList>
<div className="hidden items-center gap-2 @3xl/page:flex">
<AnalyticsDatePicker />
<Button variant="outline">
<FilterIcon />
Filter
</Button>
<Button variant="outline">
<DownloadIcon />
Export
</Button>
</div>
</div>
<TabsContent value="overview" className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<CardTitle>Total Revenue</CardTitle>
<CardDescription>$1,250.00 in the last 30 days</CardDescription>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<TrendingUpIcon />
+12.5%
</Badge>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>New Customers</CardTitle>
<CardDescription>-12 customers from last month</CardDescription>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<TrendingDownIcon />
-20%
</Badge>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Active Accounts</CardTitle>
<CardDescription>+2,345 users from last month</CardDescription>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<TrendingUpIcon />
+12.5%
</Badge>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Growth Rate</CardTitle>
<CardDescription>+12.5% increase per month</CardDescription>
</CardHeader>
<CardFooter>
<Badge variant="outline">
<TrendingUpIcon />
+4.5%
</Badge>
</CardFooter>
</Card>
</div>
<div className="grid grid-cols-1 gap-4 @4xl/page:grid-cols-[2fr_1fr]">
<ChartRevenue />
<ChartVisitors />
</div>
<ProductsTable products={products} />
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -2,83 +2,182 @@
import * as React from "react" import * as React from "react"
import { import {
ChartLineIcon, IconCamera,
FileIcon, IconChartBar,
HomeIcon, IconDashboard,
LifeBuoy, IconDatabase,
Send, IconFileAi,
Settings2Icon, IconFileDescription,
ShoppingBagIcon, IconFileWord,
ShoppingCartIcon, IconFolder,
UserIcon, IconHelp,
} from "lucide-react" IconInnerShadowTop,
IconListDetails,
IconReport,
IconSearch,
IconSettings,
IconUsers,
} from "@tabler/icons-react"
import { Sidebar, SidebarContent } from "@/registry/new-york-v4/ui/sidebar" import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar"
// import { NavClouds } from "@/app/(examples)/dashboard-02/components/nav-clouds"
import { NavDocuments } from "@/app/(examples)/dashboard/components/nav-documents"
import { NavMain } from "@/app/(examples)/dashboard/components/nav-main" import { NavMain } from "@/app/(examples)/dashboard/components/nav-main"
import { NavSecondary } from "@/app/(examples)/dashboard/components/nav-secondary" import { NavSecondary } from "@/app/(examples)/dashboard/components/nav-secondary"
import { NavUser } from "@/app/(examples)/dashboard/components/nav-user"
const data = { const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [ navMain: [
{ {
title: "Dashboard", title: "Dashboard",
url: "/dashboard", url: "#",
icon: HomeIcon, icon: IconDashboard,
},
{
title: "Lifecycle",
url: "#",
icon: IconListDetails,
}, },
{ {
title: "Analytics", title: "Analytics",
url: "/dashboard/analytics", url: "#",
icon: ChartLineIcon, icon: IconChartBar,
}, },
{ {
title: "Orders", title: "Projects",
url: "/dashboard/orders", url: "#",
icon: ShoppingBagIcon, icon: IconFolder,
}, },
{ {
title: "Products", title: "Team",
url: "/dashboard/products", url: "#",
icon: ShoppingCartIcon, icon: IconUsers,
},
],
navClouds: [
{
title: "Capture",
icon: IconCamera,
isActive: true,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
}, },
{ {
title: "Invoices", title: "Proposal",
url: "/dashboard/invoices", icon: IconFileDescription,
icon: FileIcon, url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
}, },
{ {
title: "Customers", title: "Prompts",
url: "/dashboard/customers", icon: IconFileAi,
icon: UserIcon, url: "#",
}, items: [
{ {
title: "Settings", title: "Active Proposals",
url: "/dashboard/settings", url: "#",
icon: Settings2Icon, },
{
title: "Archived",
url: "#",
},
],
}, },
], ],
navSecondary: [ navSecondary: [
{ {
title: "Support", title: "Settings",
url: "#", url: "#",
icon: LifeBuoy, icon: IconSettings,
}, },
{ {
title: "Feedback", title: "Get Help",
url: "#", url: "#",
icon: Send, icon: IconHelp,
},
{
title: "Search",
url: "#",
icon: IconSearch,
},
],
documents: [
{
name: "Data Library",
url: "#",
icon: IconDatabase,
},
{
name: "Reports",
url: "#",
icon: IconReport,
},
{
name: "Word Assistant",
url: "#",
icon: IconFileWord,
}, },
], ],
} }
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return ( return (
<Sidebar <Sidebar collapsible="offcanvas" {...props}>
className="top-(--header-height) h-[calc(100svh-var(--header-height))]!" <SidebarHeader>
{...props} <SidebarMenu>
> <SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:!p-1.5"
>
<a href="#">
<IconInnerShadowTop className="!size-5" />
<span className="text-base font-semibold">Acme Inc.</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent> <SidebarContent>
<NavMain items={data.navMain} /> <NavMain items={data.navMain} />
{/* <NavClouds items={data.navClouds} /> */}
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className="mt-auto" /> <NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent> </SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar> </Sidebar>
) )
} }

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { MoonIcon, SunIcon } from "lucide-react" import { IconBrightness } from "@tabler/icons-react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
@@ -20,8 +20,7 @@ export function ModeToggle() {
className="group/toggle size-8" className="group/toggle size-8"
onClick={toggleTheme} onClick={toggleTheme}
> >
<SunIcon className="hidden [html.dark_&]:block" /> <IconBrightness />
<MoonIcon className="hidden [html.light_&]:block" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
) )

View File

@@ -1,23 +1,14 @@
"use client" "use client"
import { usePathname } from "next/navigation" import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
import { ChevronRight, type LucideIcon } from "lucide-react"
import { import { Button } from "@/registry/new-york-v4/ui/button"
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/new-york-v4/ui/collapsible"
import { import {
SidebarGroup, SidebarGroup,
SidebarGroupLabel, SidebarGroupContent,
SidebarMenu, SidebarMenu,
SidebarMenuAction,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/registry/new-york-v4/ui/sidebar" } from "@/registry/new-york-v4/ui/sidebar"
export function NavMain({ export function NavMain({
@@ -26,66 +17,40 @@ export function NavMain({
items: { items: {
title: string title: string
url: string url: string
icon: LucideIcon icon?: Icon
isActive?: boolean
items?: {
title: string
url: string
}[]
disabled?: boolean
}[] }[]
}) { }) {
const pathname = usePathname()
return ( return (
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Dashboard</SidebarGroupLabel> <SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( <SidebarMenuItem className="flex items-center gap-2">
<Collapsible key={item.title} asChild defaultOpen={item.isActive}> <SidebarMenuButton
<SidebarMenuItem> tooltip="Quick Create"
<SidebarMenuButton className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
asChild >
tooltip={item.title} <IconCirclePlusFilled />
isActive={pathname === item.url} <span>Quick Create</span>
disabled={item.disabled} </SidebarMenuButton>
> <Button
<a size="icon"
href={item.disabled ? "#" : item.url} className="size-8 group-data-[collapsible=icon]:opacity-0"
data-disabled={item.disabled} variant="outline"
className="data-[disabled=true]:opacity-50" >
> <IconMail />
<item.icon className="text-muted-foreground" /> <span className="sr-only">Inbox</span>
<span>{item.title}</span> </Button>
</a> </SidebarMenuItem>
</SidebarMenuButton> </SidebarMenu>
{item.items?.length ? ( <SidebarMenu>
<> {items.map((item) => (
<CollapsibleTrigger asChild> <SidebarMenuButton key={item.title} tooltip={item.title}>
<SidebarMenuAction className="data-[state=open]:rotate-90"> {item.icon && <item.icon />}
<ChevronRight /> <span>{item.title}</span>
<span className="sr-only">Toggle</span> </SidebarMenuButton>
</SidebarMenuAction> ))}
</CollapsibleTrigger> </SidebarMenu>
<CollapsibleContent> </SidebarGroupContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
) : null}
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup> </SidebarGroup>
) )
} }

View File

@@ -1,5 +1,8 @@
"use client"
import * as React from "react" import * as React from "react"
import { type LucideIcon } from "lucide-react" import { IconBrightness, type Icon } from "@tabler/icons-react"
import { useTheme } from "next-themes"
import { import {
SidebarGroup, SidebarGroup,
@@ -8,6 +11,8 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
} from "@/registry/new-york-v4/ui/sidebar" } from "@/registry/new-york-v4/ui/sidebar"
import { Skeleton } from "@/registry/new-york-v4/ui/skeleton"
import { Switch } from "@/registry/new-york-v4/ui/switch"
export function NavSecondary({ export function NavSecondary({
items, items,
@@ -16,16 +21,23 @@ export function NavSecondary({
items: { items: {
title: string title: string
url: string url: string
icon: LucideIcon icon: Icon
}[] }[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) { } & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
const { resolvedTheme, setTheme } = useTheme()
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
return ( return (
<SidebarGroup {...props}> <SidebarGroup {...props}>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild size="sm"> <SidebarMenuButton asChild>
<a href={item.url}> <a href={item.url}>
<item.icon /> <item.icon />
<span>{item.title}</span> <span>{item.title}</span>
@@ -33,6 +45,25 @@ export function NavSecondary({
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
))} ))}
<SidebarMenu className="group-data-[collapsible=icon]:hidden">
<SidebarMenuButton asChild>
<label>
<IconBrightness />
<span>Dark Mode</span>
{mounted ? (
<Switch
className="ml-auto"
checked={resolvedTheme !== "light"}
onCheckedChange={() =>
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
/>
) : (
<Skeleton className="ml-auto h-4 w-8 rounded-full" />
)}
</label>
</SidebarMenuButton>
</SidebarMenu>
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>

View File

@@ -1,13 +1,18 @@
"use client" "use client"
import { BadgeCheck, Bell, CreditCard, LogOut, Sparkles } from "lucide-react" import {
IconCreditCard,
IconDotsVertical,
IconLogout,
IconNotification,
IconUserCircle,
} from "@tabler/icons-react"
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
AvatarImage, AvatarImage,
} from "@/registry/new-york-v4/ui/avatar" } from "@/registry/new-york-v4/ui/avatar"
import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -17,6 +22,12 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu" } from "@/registry/new-york-v4/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/registry/new-york-v4/ui/sidebar"
export function NavUser({ export function NavUser({
user, user,
@@ -27,64 +38,73 @@ export function NavUser({
avatar: string avatar: string
} }
}) { }) {
const { isMobile } = useSidebar()
return ( return (
<DropdownMenu> <SidebarMenu>
<DropdownMenuTrigger asChild> <SidebarMenuItem>
<Button variant="ghost" size="icon"> <DropdownMenu>
<Avatar className="size-8 rounded-md"> <DropdownMenuTrigger asChild>
<AvatarImage src={user.avatar} alt={user.name} /> <SidebarMenuButton
<AvatarFallback className="rounded-lg">CN</AvatarFallback> size="lg"
</Avatar> className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
</Button> >
</DropdownMenuTrigger> <Avatar className="h-8 w-8 rounded-lg grayscale">
<DropdownMenuContent <AvatarImage src={user.avatar} alt={user.name} />
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg" <AvatarFallback className="rounded-lg">CN</AvatarFallback>
side="bottom" </Avatar>
align="end" <div className="grid flex-1 text-left text-sm leading-tight">
sideOffset={4} <span className="truncate font-medium">{user.name}</span>
> <span className="text-muted-foreground truncate text-xs">
<DropdownMenuLabel className="p-0 font-normal"> {user.email}
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> </span>
<Avatar className="h-8 w-8 rounded-lg"> </div>
<AvatarImage src={user.avatar} alt={user.name} /> <IconDotsVertical className="ml-auto size-4" />
<AvatarFallback className="rounded-lg">CN</AvatarFallback> </SidebarMenuButton>
</Avatar> </DropdownMenuTrigger>
<div className="grid flex-1 text-left text-sm leading-tight"> <DropdownMenuContent
<span className="truncate font-medium">{user.name}</span> className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
<span className="text-muted-foreground truncate text-xs"> side={isMobile ? "bottom" : "right"}
{user.email} align="end"
</span> sideOffset={4}
</div> >
</div> <DropdownMenuLabel className="p-0 font-normal">
</DropdownMenuLabel> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<DropdownMenuSeparator /> <Avatar className="h-8 w-8 rounded-lg">
<DropdownMenuGroup> <AvatarImage src={user.avatar} alt={user.name} />
<DropdownMenuItem> <AvatarFallback className="rounded-lg">CN</AvatarFallback>
<Sparkles /> </Avatar>
Upgrade to Pro <div className="grid flex-1 text-left text-sm leading-tight">
</DropdownMenuItem> <span className="truncate font-medium">{user.name}</span>
</DropdownMenuGroup> <span className="text-muted-foreground truncate text-xs">
<DropdownMenuSeparator /> {user.email}
<DropdownMenuGroup> </span>
<DropdownMenuItem> </div>
<BadgeCheck /> </div>
Account </DropdownMenuLabel>
</DropdownMenuItem> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuGroup>
<CreditCard /> <DropdownMenuItem>
Billing <IconUserCircle />
</DropdownMenuItem> Account
<DropdownMenuItem> </DropdownMenuItem>
<Bell /> <DropdownMenuItem>
Notifications <IconCreditCard />
</DropdownMenuItem> Billing
</DropdownMenuGroup> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuItem>
<DropdownMenuItem> <IconNotification />
<LogOut /> Notifications
Log out </DropdownMenuItem>
</DropdownMenuItem> </DropdownMenuGroup>
</DropdownMenuContent> <DropdownMenuSeparator />
</DropdownMenu> <DropdownMenuItem>
<IconLogout />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
) )
} }

View File

@@ -1,101 +1,32 @@
"use client"
import { Fragment, useMemo } from "react"
import { usePathname } from "next/navigation"
import { SidebarIcon } from "lucide-react"
import { ThemeSelector } from "@/components/theme-selector"
import { SearchForm } from "@/registry/new-york-v4/blocks/sidebar-16/components/search-form"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/registry/new-york-v4/ui/breadcrumb"
import { Button } from "@/registry/new-york-v4/ui/button" import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator" import { Separator } from "@/registry/new-york-v4/ui/separator"
import { useSidebar } from "@/registry/new-york-v4/ui/sidebar" import { SidebarTrigger } from "@/registry/new-york-v4/ui/sidebar"
import { ModeToggle } from "@/app/(examples)/dashboard/components/mode-toggle" import { ModeToggle } from "@/app/(examples)/dashboard/components/mode-toggle"
import { NavUser } from "@/app/(examples)/dashboard/components/nav-user" import { ThemeSelector } from "@/app/(examples)/dashboard/components/theme-selector"
export function SiteHeader() { export function SiteHeader() {
const { toggleSidebar } = useSidebar()
const pathname = usePathname()
// Faux breadcrumbs for demo.
const breadcrumbs = useMemo(() => {
return pathname
.split("/")
.filter((path) => path !== "")
.map((path, index, array) => ({
label: path,
href: `/${array.slice(0, index + 1).join("/")}`,
}))
}, [pathname])
return ( return (
<header <header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
data-slot="site-header" <div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
className="bg-background sticky top-0 z-50 flex w-full items-center border-b" <SidebarTrigger className="-ml-1" />
>
<div className="flex h-(--header-height) w-full items-center gap-2 px-2 pr-4">
<Button
variant="ghost"
size="sm"
onClick={toggleSidebar}
className="gap-2.5 has-[>svg]:px-2"
>
<SidebarIcon />
<span className="truncate font-medium">Acme Inc</span>
</Button>
<Separator <Separator
orientation="vertical" orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4" className="mx-2 data-[orientation=vertical]:h-4"
/> />
<Breadcrumb className="hidden sm:block"> <h1 className="text-base font-medium">Documents</h1>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/" className="capitalize">
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
{breadcrumbs.map((breadcrumb, index) =>
index === breadcrumbs.length - 1 ? (
<BreadcrumbItem key={index}>
<BreadcrumbPage className="capitalize">
{breadcrumb.label}
</BreadcrumbPage>
</BreadcrumbItem>
) : (
<Fragment key={index}>
<BreadcrumbItem>
<BreadcrumbLink
href={breadcrumb.href}
className="capitalize"
>
{breadcrumb.label}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</Fragment>
)
)}
</BreadcrumbList>
</Breadcrumb>
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<SearchForm className="w-fullsm:w-auto" /> <Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
GitHub
</a>
</Button>
<ThemeSelector /> <ThemeSelector />
<ModeToggle /> <ModeToggle />
<NavUser
user={{
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
}}
/>
</div> </div>
</div> </div>
</header> </header>

View File

@@ -7,7 +7,7 @@ import {
import { AppSidebar } from "@/app/(examples)/dashboard/components/app-sidebar" import { AppSidebar } from "@/app/(examples)/dashboard/components/app-sidebar"
import { SiteHeader } from "@/app/(examples)/dashboard/components/site-header" import { SiteHeader } from "@/app/(examples)/dashboard/components/site-header"
import "../../themes.css" import "./theme.css"
export default async function DashboardLayout({ export default async function DashboardLayout({
children, children,
@@ -18,14 +18,19 @@ export default async function DashboardLayout({
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true" const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return ( return (
<main className="[--header-height:calc(theme(spacing.14))]"> <SidebarProvider
<SidebarProvider defaultOpen={defaultOpen} className="flex flex-col"> defaultOpen={defaultOpen}
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader /> <SiteHeader />
<div className="flex flex-1"> <div className="flex flex-1 flex-col">{children}</div>
<AppSidebar /> </SidebarInset>
<SidebarInset>{children}</SidebarInset> </SidebarProvider>
</div>
</SidebarProvider>
</main>
) )
} }

View File

@@ -1,206 +1,125 @@
import { Metadata } from "next" import { Metadata } from "next"
import { import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
DownloadIcon,
FilterIcon,
TrendingDownIcon,
TrendingUpIcon,
} from "lucide-react"
import { Badge } from "@/registry/new-york-v4/ui/badge" import { Badge } from "@/registry/new-york-v4/ui/badge"
import { Button } from "@/registry/new-york-v4/ui/button"
import { import {
Card, Card,
CardAction,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/registry/new-york-v4/ui/card" } from "@/registry/new-york-v4/ui/card"
import { import { ChartAreaInteractive } from "@/app/(examples)/dashboard/components/chart-area-interactive"
Tabs, import { DataTable } from "@/app/(examples)/dashboard/components/data-table"
TabsContent,
TabsList, import data from "./data.json"
TabsTrigger,
} from "@/registry/new-york-v4/ui/tabs"
import { AnalyticsDatePicker } from "@/app/(examples)/dashboard/components/analytics-date-picker"
import { ChartRevenue } from "@/app/(examples)/dashboard/components/chart-revenue"
import { ChartVisitors } from "@/app/(examples)/dashboard/components/chart-visitors"
import { ProductsTable } from "@/app/(examples)/dashboard/components/products-table"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Dashboard", title: "Dashboard",
description: "An example dashboard to test the new components.", description: "A dashboard with tabs, table and sidebar.",
} }
// Load from database. export default async function Dashboard02() {
const products = [
{
id: "1",
name: "BJÖRKSNÄS Dining Table",
price: 599.99,
stock: 12,
dateAdded: "2023-06-15",
status: "In Stock",
},
{
id: "2",
name: "POÄNG Armchair",
price: 249.99,
stock: 28,
dateAdded: "2023-07-22",
status: "In Stock",
},
{
id: "3",
name: "MALM Bed Frame",
price: 399.99,
stock: 15,
dateAdded: "2023-08-05",
status: "In Stock",
},
{
id: "4",
name: "KALLAX Shelf Unit",
price: 179.99,
stock: 32,
dateAdded: "2023-09-12",
status: "In Stock",
},
{
id: "5",
name: "STOCKHOLM Rug",
price: 299.99,
stock: 8,
dateAdded: "2023-10-18",
status: "Low Stock",
},
{
id: "6",
name: "KIVIK Sofa",
price: 899.99,
stock: 6,
dateAdded: "2023-11-02",
status: "Low Stock",
},
{
id: "7",
name: "LISABO Coffee Table",
price: 149.99,
stock: 22,
dateAdded: "2023-11-29",
status: "In Stock",
},
{
id: "8",
name: "HEMNES Bookcase",
price: 249.99,
stock: 17,
dateAdded: "2023-12-10",
status: "In Stock",
},
{
id: "9",
name: "EKEDALEN Dining Chairs (Set of 2)",
price: 199.99,
stock: 14,
dateAdded: "2024-01-05",
status: "In Stock",
},
{
id: "10",
name: "FRIHETEN Sleeper Sofa",
price: 799.99,
stock: 9,
dateAdded: "2024-01-18",
status: "Low Stock",
},
]
export default function DashboardPage() {
return ( return (
<div className="@container/page flex flex-1 flex-col gap-8 p-6"> <div className="@container/main flex flex-1 flex-col gap-2">
<Tabs defaultValue="overview" className="gap-6"> <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<div <div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
data-slot="dashboard-header" <Card className="@container/card">
className="flex items-center justify-between" <CardHeader>
> <CardDescription>Total Revenue</CardDescription>
<TabsList className="w-full @3xl/page:w-fit"> <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
<TabsTrigger value="overview">Overview</TabsTrigger> $1,250.00
<TabsTrigger value="analytics">Analytics</TabsTrigger> </CardTitle>
<TabsTrigger value="reports">Reports</TabsTrigger> <CardAction>
<TabsTrigger value="exports" disabled>
Exports
</TabsTrigger>
</TabsList>
<div className="hidden items-center gap-2 @3xl/page:flex">
<AnalyticsDatePicker />
<Button variant="outline">
<FilterIcon />
Filter
</Button>
<Button variant="outline">
<DownloadIcon />
Export
</Button>
</div>
</div>
<TabsContent value="overview" className="flex flex-col gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<CardTitle>Total Revenue</CardTitle>
<CardDescription>$1,250.00 in the last 30 days</CardDescription>
</CardHeader>
<CardFooter>
<Badge variant="outline"> <Badge variant="outline">
<TrendingUpIcon /> <IconTrendingUp />
+12.5% +12.5%
</Badge> </Badge>
</CardFooter> </CardAction>
</Card> </CardHeader>
<Card> <CardFooter className="flex-col items-start gap-2.5 text-sm">
<CardHeader> <div className="line-clamp-1 flex gap-2 font-medium">
<CardTitle>New Customers</CardTitle> Trending up this month <IconTrendingUp className="size-4" />
<CardDescription>-12 customers from last month</CardDescription> </div>
</CardHeader> <div className="text-muted-foreground">
<CardFooter> Visitors for the last 6 months
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>New Customers</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
1,234
</CardTitle>
<CardAction>
<Badge variant="outline"> <Badge variant="outline">
<TrendingDownIcon /> <IconTrendingDown />
-20% -20%
</Badge> </Badge>
</CardFooter> </CardAction>
</Card> </CardHeader>
<Card> <CardFooter className="flex-col items-start gap-1.5 text-sm">
<CardHeader> <div className="line-clamp-1 flex gap-2 font-medium">
<CardTitle>Active Accounts</CardTitle> Down 20% this period <IconTrendingDown className="size-4" />
<CardDescription>+2,345 users from last month</CardDescription> </div>
</CardHeader> <div className="text-muted-foreground">
<CardFooter> Acquisition needs attention
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
45,678
</CardTitle>
<CardAction>
<Badge variant="outline"> <Badge variant="outline">
<TrendingUpIcon /> <IconTrendingUp />
+12.5% +12.5%
</Badge> </Badge>
</CardFooter> </CardAction>
</Card> </CardHeader>
<Card> <CardFooter className="flex-col items-start gap-1.5 text-sm">
<CardHeader> <div className="line-clamp-1 flex gap-2 font-medium">
<CardTitle>Growth Rate</CardTitle> Strong user retention <IconTrendingUp className="size-4" />
<CardDescription>+12.5% increase per month</CardDescription> </div>
</CardHeader> <div className="text-muted-foreground">
<CardFooter> Engagement exceed targets
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
4.5%
</CardTitle>
<CardAction>
<Badge variant="outline"> <Badge variant="outline">
<TrendingUpIcon /> <IconTrendingUp />
+4.5% +4.5%
</Badge> </Badge>
</CardFooter> </CardAction>
</Card> </CardHeader>
</div> <CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="grid grid-cols-1 gap-4 @4xl/page:grid-cols-[2fr_1fr]"> <div className="line-clamp-1 flex gap-2 font-medium">
<ChartRevenue /> Steady performance increase{" "}
<ChartVisitors /> <IconTrendingUp className="size-4" />
</div> </div>
<ProductsTable products={products} /> <div className="text-muted-foreground">
</TabsContent> Meets growth projections
</Tabs> </div>
</CardFooter>
</Card>
</div>
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div> </div>
) )
} }

View File

@@ -46,7 +46,7 @@ export const metadata: Metadata = {
siteName: siteConfig.name, siteName: siteConfig.name,
images: [ images: [
{ {
url: siteConfig.ogImage, url: "https://v4.shadcn.com/opengraph-image.png",
width: 1200, width: 1200,
height: 630, height: 630,
alt: siteConfig.name, alt: siteConfig.name,
@@ -57,7 +57,7 @@ export const metadata: Metadata = {
card: "summary_large_image", card: "summary_large_image",
title: siteConfig.name, title: siteConfig.name,
description: siteConfig.description, description: siteConfig.description,
images: [siteConfig.ogImage], images: ["https://v4.shadcn.com/opengraph-image.png"],
creator: "@shadcn", creator: "@shadcn",
}, },
icons: { icons: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB