Dashboard
Full dashboards with a sidebar, stat cards, charts, and data tables.
Dashboard
import { AppSidebar } from "./components/app-sidebar"
import { ChartAreaInteractive } from "./components/chart-area-interactive"
import { DataTable } from "./components/data-table"
import { SectionCards } from "./components/section-cards"
import { SiteHeader } from "./components/site-header"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import data from "./data.json"
export default function Page() {
return (
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
"--header-height": "calc(var(--spacing) * 12)",
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<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">
<SectionCards />
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}"use client"
import * as React from "react"
import { NavDocuments } from "./nav-documents"
import { NavMain } from "./nav-main"
import { NavSecondary } from "./nav-secondary"
import { NavUser } from "./nav-user"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { LayoutDashboardIcon, ListIcon, ChartBarIcon, FolderIcon, UsersIcon, CameraIcon, FileTextIcon, Settings2Icon, CircleHelpIcon, SearchIcon, DatabaseIcon, FileChartColumnIcon, FileIcon, CommandIcon } from "lucide-react"
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: "#",
icon: (
<LayoutDashboardIcon
/>
),
},
{
title: "Lifecycle",
url: "#",
icon: (
<ListIcon
/>
),
},
{
title: "Analytics",
url: "#",
icon: (
<ChartBarIcon
/>
),
},
{
title: "Projects",
url: "#",
icon: (
<FolderIcon
/>
),
},
{
title: "Team",
url: "#",
icon: (
<UsersIcon
/>
),
},
],
navClouds: [
{
title: "Capture",
icon: (
<CameraIcon
/>
),
isActive: true,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Proposal",
icon: (
<FileTextIcon
/>
),
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Prompts",
icon: (
<FileTextIcon
/>
),
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Settings",
url: "#",
icon: (
<Settings2Icon
/>
),
},
{
title: "Get Help",
url: "#",
icon: (
<CircleHelpIcon
/>
),
},
{
title: "Search",
url: "#",
icon: (
<SearchIcon
/>
),
},
],
documents: [
{
name: "Data Library",
url: "#",
icon: (
<DatabaseIcon
/>
),
},
{
name: "Reports",
url: "#",
icon: (
<FileChartColumnIcon
/>
),
},
{
name: "Word Assistant",
url: "#",
icon: (
<FileIcon
/>
),
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="offcanvas" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
className="data-[slot=sidebar-menu-button]:p-1.5!"
render={<a href="#" />}
>
<CommandIcon className="size-5!" />
<span className="text-base font-semibold">Acme Inc.</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
)
}"use client"
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { useIsMobile } from "@/hooks/use-mobile"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group"
export const description = "An interactive area chart"
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
const chartConfig = {
visitors: {
label: "Visitors",
},
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const referenceDate = new Date("2024-06-30")
let daysToSubtract = 90
if (timeRange === "30d") {
daysToSubtract = 30
} else if (timeRange === "7d") {
daysToSubtract = 7
}
const startDate = new Date(referenceDate)
startDate.setDate(startDate.getDate() - daysToSubtract)
return date >= startDate
})
return (
<Card className="@container/card">
<CardHeader>
<CardTitle>Total Visitors</CardTitle>
<CardDescription>
<span className="hidden @[540px]/card:block">
Total for the last 3 months
</span>
<span className="@[540px]/card:hidden">Last 3 months</span>
</CardDescription>
<CardAction>
<ToggleGroup
multiple={false}
value={timeRange ? [timeRange] : []}
onValueChange={(value) => {
setTimeRange(value[0] ?? "90d")
}}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
>
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
</ToggleGroup>
<Select
items={[
{ label: "Last 3 months", value: "90d" },
{ label: "Last 30 days", value: "30d" },
{ label: "Last 7 days", value: "7d" },
]}
value={timeRange}
onValueChange={(value) => {
if (value !== null) {
setTimeRange(value)
}
}}
>
<SelectTrigger
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
size="sm"
aria-label="Select a value"
>
<SelectValue placeholder="Last 3 months" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="90d" className="rounded-lg">
Last 3 months
</SelectItem>
<SelectItem value="30d" className="rounded-lg">
Last 30 days
</SelectItem>
<SelectItem value="7d" className="rounded-lg">
Last 7 days
</SelectItem>
</SelectContent>
</Select>
</CardAction>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={filteredData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={1.0}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
indicator="dot"
/>
}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import {
columnFilteringFeature,
columnVisibilityFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
FlexRender,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
useTable,
type ColumnFiltersState,
type ColumnVisibilityState,
type Row,
type SortingState,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"
import { z } from "zod"
import { useIsMobile } from "@/hooks/use-mobile"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import { Checkbox } from "@/components/ui/checkbox"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { GripVerticalIcon, CircleCheckIcon, LoaderIcon, EllipsisVerticalIcon, Columns3Icon, ChevronDownIcon, PlusIcon, ChevronsLeftIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsRightIcon, TrendingUpIcon } from "lucide-react"
// New in v9: declare the features this table uses — anything you don't
// register is tree-shaken out of the bundle.
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
})
const columnHelper = createColumnHelper<
typeof features,
z.infer<typeof schema>
>()
export const schema = z.object({
id: z.number(),
header: z.string(),
type: z.string(),
status: z.string(),
target: z.string(),
limit: z.string(),
reviewer: z.string(),
})
// Create a separate component for the drag handle
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({
id,
})
return (
<Button
{...attributes}
{...listeners}
variant="ghost"
size="icon"
className="size-7 text-muted-foreground hover:bg-transparent"
>
<GripVerticalIcon className="size-3 text-muted-foreground" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
}
const columns = columnHelper.columns([
columnHelper.display({
id: "drag",
header: () => null,
cell: ({ row }) => <DragHandle id={row.original.id} />,
}),
columnHelper.display({
id: "select",
header: ({ table }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={
table.getIsSomePageRowsSelected() &&
!table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
</div>
),
cell: ({ row }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
enableSorting: false,
enableHiding: false,
}),
columnHelper.accessor("header", {
header: "Header",
cell: ({ row }) => {
return <TableCellViewer item={row.original} />
},
enableHiding: false,
}),
columnHelper.accessor("type", {
header: "Section Type",
cell: ({ row }) => (
<div className="w-32">
<Badge variant="outline" className="px-1.5 text-muted-foreground">
{row.original.type}
</Badge>
</div>
),
}),
columnHelper.accessor("status", {
header: "Status",
cell: ({ row }) => (
<Badge variant="outline" className="px-1.5 text-muted-foreground">
{row.original.status === "Done" ? (
<CircleCheckIcon className="fill-green-500 dark:fill-green-400" />
) : (
<LoaderIcon
/>
)}
{row.original.status}
</Badge>
),
}),
columnHelper.accessor("target", {
header: () => <div className="w-full text-right">Target</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault()
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
})
}}
>
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
Target
</Label>
<Input
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
defaultValue={row.original.target}
id={`${row.original.id}-target`}
/>
</form>
),
}),
columnHelper.accessor("limit", {
header: () => <div className="w-full text-right">Limit</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault()
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
})
}}
>
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
Limit
</Label>
<Input
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
defaultValue={row.original.limit}
id={`${row.original.id}-limit`}
/>
</form>
),
}),
columnHelper.accessor("reviewer", {
header: "Reviewer",
cell: ({ row }) => {
const isAssigned = row.original.reviewer !== "Assign reviewer"
if (isAssigned) {
return row.original.reviewer
}
return (
<>
<Label htmlFor={`${row.original.id}-reviewer`} className="sr-only">
Reviewer
</Label>
<Select
items={[
{ label: "Eddie Lake", value: "Eddie Lake" },
{ label: "Jamik Tashpulatov", value: "Jamik Tashpulatov" },
]}
>
<SelectTrigger
className="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
size="sm"
id={`${row.original.id}-reviewer`}
>
<SelectValue placeholder="Assign reviewer" />
</SelectTrigger>
<SelectContent align="end">
<SelectGroup>
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">
Jamik Tashpulatov
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</>
)
},
}),
columnHelper.display({
id: "actions",
cell: () => (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
className="flex size-8 text-muted-foreground data-open:bg-muted"
size="icon"
/>
}
>
<EllipsisVerticalIcon
/>
<span className="sr-only">Open menu</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
}),
])
function DraggableRow({
row,
}: {
row: Row<typeof features, z.infer<typeof schema>>
}) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.id,
})
return (
<TableRow
data-state={row.getIsSelected() && "selected"}
data-dragging={isDragging}
ref={setNodeRef}
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
style={{
transform: CSS.Transform.toString(transform),
transition: transition,
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<FlexRender cell={cell} />
</TableCell>
))}
</TableRow>
)
}
export function DataTable({
data: initialData,
}: {
data: z.infer<typeof schema>[]
}) {
const [data, setData] = React.useState(() => initialData)
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] =
React.useState<ColumnVisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [sorting, setSorting] = React.useState<SortingState>([])
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
})
const sortableId = React.useId()
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)
const dataIds = React.useMemo<UniqueIdentifier[]>(
() => data?.map(({ id }) => id) || [],
[data]
)
const table = useTable({
features,
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination,
},
getRowId: (row) => row.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
})
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (active && over && active.id !== over.id) {
setData((data) => {
const oldIndex = dataIds.indexOf(active.id)
const newIndex = dataIds.indexOf(over.id)
return arrayMove(data, oldIndex, newIndex)
})
}
}
return (
<Tabs
defaultValue="outline"
className="w-full flex-col justify-start gap-6"
>
<div className="flex items-center justify-between px-4 lg:px-6">
<Label htmlFor="view-selector" className="sr-only">
View
</Label>
<Select
defaultValue="outline"
items={[
{ label: "Outline", value: "outline" },
{ label: "Past Performance", value: "past-performance" },
{ label: "Key Personnel", value: "key-personnel" },
{ label: "Focus Documents", value: "focus-documents" },
]}
>
<SelectTrigger
className="flex w-fit @4xl/main:hidden"
size="sm"
id="view-selector"
>
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="past-performance">Past Performance</SelectItem>
<SelectItem value="key-personnel">Key Personnel</SelectItem>
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
<TabsTrigger value="outline">Outline</TabsTrigger>
<TabsTrigger value="past-performance">
Past Performance <Badge variant="secondary">3</Badge>
</TabsTrigger>
<TabsTrigger value="key-personnel">
Key Personnel <Badge variant="secondary">2</Badge>
</TabsTrigger>
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="sm" />}
>
<Columns3Icon data-icon="inline-start" />
Columns
<ChevronDownIcon data-icon="inline-end" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" &&
column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm">
<PlusIcon
/>
<span className="hidden lg:inline">Add Section</span>
</Button>
</div>
</div>
<TabsContent
value="outline"
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
>
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
id={sortableId}
>
<Table>
<TableHeader className="sticky top-0 z-10 bg-muted">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<FlexRender header={header} />
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (
<SortableContext
items={dataIds}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">
Rows per page
</Label>
<Select
value={`${table.state.pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
items={[10, 20, 30, 40, 50].map((pageSize) => ({
label: `${pageSize}`,
value: `${pageSize}`,
}))}
>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
<SelectValue placeholder={table.state.pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
<SelectGroup>
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.state.pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeftIcon
/>
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon
/>
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon
/>
</Button>
<Button
variant="outline"
className="hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRightIcon
/>
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent
value="past-performance"
className="flex flex-col px-4 lg:px-6"
>
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent
value="focus-documents"
className="flex flex-col px-4 lg:px-6"
>
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
</Tabs>
)
}
const chartData = [
{
month: "January",
desktop: 186,
mobile: 80,
},
{
month: "February",
desktop: 305,
mobile: 200,
},
{
month: "March",
desktop: 237,
mobile: 120,
},
{
month: "April",
desktop: 73,
mobile: 190,
},
{
month: "May",
desktop: 209,
mobile: 130,
},
{
month: "June",
desktop: 214,
mobile: 140,
},
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
const isMobile = useIsMobile()
return (
<Drawer swipeDirection={isMobile ? "down" : "right"}>
<DrawerTrigger
render={
<Button
variant="link"
className="w-fit px-0 text-left text-foreground"
/>
}
>
{item.header}
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="gap-1">
<DrawerTitle>{item.header}</DrawerTitle>
<DrawerDescription>
Showing total visitors for the last 6 months
</DrawerDescription>
</DrawerHeader>
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
{!isMobile && (
<>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 10,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
hide
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.6}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
<Separator />
<div className="grid gap-2">
<div className="flex gap-2 leading-none font-medium">
Trending up by 5.2% this month{" "}
<TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">
Showing total visitors for the last 6 months. This is just
some random text to test the layout. It spans multiple lines
and should wrap around.
</div>
</div>
<Separator />
</>
)}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="header">Header</Label>
<Input id="header" defaultValue={item.header} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="type">Type</Label>
<Select
defaultValue={item.type}
items={[
{ label: "Table of Contents", value: "Table of Contents" },
{ label: "Executive Summary", value: "Executive Summary" },
{
label: "Technical Approach",
value: "Technical Approach",
},
{ label: "Design", value: "Design" },
{ label: "Capabilities", value: "Capabilities" },
{ label: "Focus Documents", value: "Focus Documents" },
{ label: "Narrative", value: "Narrative" },
{ label: "Cover Page", value: "Cover Page" },
]}
>
<SelectTrigger id="type" className="w-full">
<SelectValue placeholder="Select a type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Table of Contents">
Table of Contents
</SelectItem>
<SelectItem value="Executive Summary">
Executive Summary
</SelectItem>
<SelectItem value="Technical Approach">
Technical Approach
</SelectItem>
<SelectItem value="Design">Design</SelectItem>
<SelectItem value="Capabilities">Capabilities</SelectItem>
<SelectItem value="Focus Documents">
Focus Documents
</SelectItem>
<SelectItem value="Narrative">Narrative</SelectItem>
<SelectItem value="Cover Page">Cover Page</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="status">Status</Label>
<Select
defaultValue={item.status}
items={[
{ label: "Done", value: "Done" },
{ label: "In Progress", value: "In Progress" },
{ label: "Not Started", value: "Not Started" },
]}
>
<SelectTrigger id="status" className="w-full">
<SelectValue placeholder="Select a status" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Done">Done</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Not Started">Not Started</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="target">Target</Label>
<Input id="target" defaultValue={item.target} />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="limit">Limit</Label>
<Input id="limit" defaultValue={item.limit} />
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="reviewer">Reviewer</Label>
<Select
defaultValue={item.reviewer}
items={[
{ label: "Eddie Lake", value: "Eddie Lake" },
{ label: "Jamik Tashpulatov", value: "Jamik Tashpulatov" },
{ label: "Emily Whalen", value: "Emily Whalen" },
]}
>
<SelectTrigger id="reviewer" className="w-full">
<SelectValue placeholder="Select a reviewer" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">
Jamik Tashpulatov
</SelectItem>
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</form>
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose render={<Button variant="outline" />}>Done</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { MoreHorizontalIcon, FolderIcon, ShareIcon, Trash2Icon } from "lucide-react"
export function NavDocuments({
items,
}: {
items: {
name: string
url: string
icon: React.ReactNode
}[]
}) {
const { isMobile } = useSidebar()
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Documents</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton render={<a href={item.url} />}>
{item.icon}
<span>{item.name}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuAction
showOnHover
className="aria-expanded:bg-muted"
/>
}
>
<MoreHorizontalIcon
/>
<span className="sr-only">More</span>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-24"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<FolderIcon
/>
<span>Open</span>
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon
/>
<span>Share</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<Trash2Icon
/>
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}"use client"
import { Button } from "@/components/ui/button"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import { CirclePlusIcon, MailIcon } from "lucide-react"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon?: React.ReactNode
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<SidebarMenuButton
tooltip="Quick Create"
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
>
<CirclePlusIcon
/>
<span>Quick Create</span>
</SidebarMenuButton>
<Button
size="icon"
className="size-8 group-data-[collapsible=icon]:opacity-0"
variant="outline"
>
<MailIcon
/>
<span className="sr-only">Inbox</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton tooltip={item.title}>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}"use client"
import * as React from "react"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: React.ReactNode
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton render={<a href={item.url} />}>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}"use client"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { EllipsisVerticalIcon, CircleUserRoundIcon, CreditCardIcon, BellIcon, LogOutIcon } from "lucide-react"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton size="lg" className="aria-expanded:bg-muted" />
}
>
<Avatar className="size-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="truncate text-xs text-foreground/70">
{user.email}
</span>
</div>
<EllipsisVerticalIcon className="ml-auto size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
className="min-w-56"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="size-8">
<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="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<CircleUserRoundIcon
/>
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCardIcon
/>
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<BellIcon
/>
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOutIcon
/>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}"use client"
import { Badge } from "@/components/ui/badge"
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { TrendingUpIcon, TrendingDownIcon } from "lucide-react"
export function SectionCards() {
return (
<div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-linear-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card">
<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">
<TrendingUpIcon
/>
+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">
Trending up this month{" "}
<TrendingUpIcon 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">
<TrendingDownIcon
/>
-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{" "}
<TrendingDownIcon 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">
<TrendingUpIcon
/>
+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{" "}
<TrendingUpIcon 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">
<TrendingUpIcon
/>
+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{" "}
<TrendingUpIcon className="size-4" />
</div>
<div className="text-muted-foreground">Meets growth projections</div>
</CardFooter>
</Card>
</div>
)
}import { Separator } from "@/components/ui/separator"
import { SidebarTrigger } from "@/components/ui/sidebar"
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 h-4 data-vertical:self-auto"
/>
<h1 className="text-base font-medium">Documents</h1>
</div>
</header>
)
}[
{
"id": 1,
"header": "Cover page",
"type": "Cover page",
"status": "In Process",
"target": "18",
"limit": "5",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "Table of contents",
"type": "Table of contents",
"status": "Done",
"target": "29",
"limit": "24",
"reviewer": "Eddie Lake"
},
{
"id": 3,
"header": "Executive summary",
"type": "Narrative",
"status": "Done",
"target": "10",
"limit": "13",
"reviewer": "Eddie Lake"
},
{
"id": 4,
"header": "Technical approach",
"type": "Narrative",
"status": "Done",
"target": "27",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 5,
"header": "Design",
"type": "Narrative",
"status": "In Process",
"target": "2",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 6,
"header": "Capabilities",
"type": "Narrative",
"status": "In Process",
"target": "20",
"limit": "8",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 7,
"header": "Integration with existing systems",
"type": "Narrative",
"status": "In Process",
"target": "19",
"limit": "21",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 8,
"header": "Innovation and Advantages",
"type": "Narrative",
"status": "Done",
"target": "25",
"limit": "26",
"reviewer": "Assign reviewer"
},
{
"id": 9,
"header": "Overview of EMR's Innovative Solutions",
"type": "Technical content",
"status": "Done",
"target": "7",
"limit": "23",
"reviewer": "Assign reviewer"
},
{
"id": 10,
"header": "Advanced Algorithms and Machine Learning",
"type": "Narrative",
"status": "Done",
"target": "30",
"limit": "28",
"reviewer": "Assign reviewer"
},
{
"id": 11,
"header": "Adaptive Communication Protocols",
"type": "Narrative",
"status": "Done",
"target": "9",
"limit": "31",
"reviewer": "Assign reviewer"
},
{
"id": 12,
"header": "Advantages Over Current Technologies",
"type": "Narrative",
"status": "Done",
"target": "12",
"limit": "0",
"reviewer": "Assign reviewer"
},
{
"id": 13,
"header": "Past Performance",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "33",
"reviewer": "Assign reviewer"
},
{
"id": 14,
"header": "Customer Feedback and Satisfaction Levels",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "34",
"reviewer": "Assign reviewer"
},
{
"id": 15,
"header": "Implementation Challenges and Solutions",
"type": "Narrative",
"status": "Done",
"target": "3",
"limit": "35",
"reviewer": "Assign reviewer"
},
{
"id": 16,
"header": "Security Measures and Data Protection Policies",
"type": "Narrative",
"status": "In Process",
"target": "6",
"limit": "36",
"reviewer": "Assign reviewer"
},
{
"id": 17,
"header": "Scalability and Future Proofing",
"type": "Narrative",
"status": "Done",
"target": "4",
"limit": "37",
"reviewer": "Assign reviewer"
},
{
"id": 18,
"header": "Cost-Benefit Analysis",
"type": "Plain language",
"status": "Done",
"target": "14",
"limit": "38",
"reviewer": "Assign reviewer"
},
{
"id": 19,
"header": "User Training and Onboarding Experience",
"type": "Narrative",
"status": "Done",
"target": "17",
"limit": "39",
"reviewer": "Assign reviewer"
},
{
"id": 20,
"header": "Future Development Roadmap",
"type": "Narrative",
"status": "Done",
"target": "11",
"limit": "40",
"reviewer": "Assign reviewer"
},
{
"id": 21,
"header": "System Architecture Overview",
"type": "Technical content",
"status": "In Process",
"target": "24",
"limit": "18",
"reviewer": "Maya Johnson"
},
{
"id": 22,
"header": "Risk Management Plan",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "22",
"reviewer": "Carlos Rodriguez"
},
{
"id": 23,
"header": "Compliance Documentation",
"type": "Legal",
"status": "In Process",
"target": "31",
"limit": "27",
"reviewer": "Sarah Chen"
},
{
"id": 24,
"header": "API Documentation",
"type": "Technical content",
"status": "Done",
"target": "8",
"limit": "12",
"reviewer": "Raj Patel"
},
{
"id": 25,
"header": "User Interface Mockups",
"type": "Visual",
"status": "In Process",
"target": "19",
"limit": "25",
"reviewer": "Leila Ahmadi"
},
{
"id": 26,
"header": "Database Schema",
"type": "Technical content",
"status": "Done",
"target": "22",
"limit": "20",
"reviewer": "Thomas Wilson"
},
{
"id": 27,
"header": "Testing Methodology",
"type": "Technical content",
"status": "In Process",
"target": "17",
"limit": "14",
"reviewer": "Assign reviewer"
},
{
"id": 28,
"header": "Deployment Strategy",
"type": "Narrative",
"status": "Done",
"target": "26",
"limit": "30",
"reviewer": "Eddie Lake"
},
{
"id": 29,
"header": "Budget Breakdown",
"type": "Financial",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 30,
"header": "Market Analysis",
"type": "Research",
"status": "Done",
"target": "29",
"limit": "32",
"reviewer": "Sophia Martinez"
},
{
"id": 31,
"header": "Competitor Comparison",
"type": "Research",
"status": "In Process",
"target": "21",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 32,
"header": "Maintenance Plan",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "23",
"reviewer": "Alex Thompson"
},
{
"id": 33,
"header": "User Personas",
"type": "Research",
"status": "In Process",
"target": "27",
"limit": "24",
"reviewer": "Nina Patel"
},
{
"id": 34,
"header": "Accessibility Compliance",
"type": "Legal",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 35,
"header": "Performance Metrics",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "David Kim"
},
{
"id": 36,
"header": "Disaster Recovery Plan",
"type": "Technical content",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 37,
"header": "Third-party Integrations",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Eddie Lake"
},
{
"id": 38,
"header": "User Feedback Summary",
"type": "Research",
"status": "Done",
"target": "20",
"limit": "15",
"reviewer": "Assign reviewer"
},
{
"id": 39,
"header": "Localization Strategy",
"type": "Narrative",
"status": "In Process",
"target": "12",
"limit": "19",
"reviewer": "Maria Garcia"
},
{
"id": 40,
"header": "Mobile Compatibility",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "James Wilson"
},
{
"id": 41,
"header": "Data Migration Plan",
"type": "Technical content",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Assign reviewer"
},
{
"id": 42,
"header": "Quality Assurance Protocols",
"type": "Technical content",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Priya Singh"
},
{
"id": 43,
"header": "Stakeholder Analysis",
"type": "Research",
"status": "In Process",
"target": "11",
"limit": "14",
"reviewer": "Eddie Lake"
},
{
"id": 44,
"header": "Environmental Impact Assessment",
"type": "Research",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Assign reviewer"
},
{
"id": 45,
"header": "Intellectual Property Rights",
"type": "Legal",
"status": "In Process",
"target": "17",
"limit": "20",
"reviewer": "Sarah Johnson"
},
{
"id": 46,
"header": "Customer Support Framework",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 47,
"header": "Version Control Strategy",
"type": "Technical content",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 48,
"header": "Continuous Integration Pipeline",
"type": "Technical content",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Michael Chen"
},
{
"id": 49,
"header": "Regulatory Compliance",
"type": "Legal",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Assign reviewer"
},
{
"id": 50,
"header": "User Authentication System",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "Eddie Lake"
},
{
"id": 51,
"header": "Data Analytics Framework",
"type": "Technical content",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 52,
"header": "Cloud Infrastructure",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 53,
"header": "Network Security Measures",
"type": "Technical content",
"status": "In Process",
"target": "29",
"limit": "32",
"reviewer": "Lisa Wong"
},
{
"id": 54,
"header": "Project Timeline",
"type": "Planning",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Eddie Lake"
},
{
"id": 55,
"header": "Resource Allocation",
"type": "Planning",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Assign reviewer"
},
{
"id": 56,
"header": "Team Structure and Roles",
"type": "Planning",
"status": "Done",
"target": "20",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 57,
"header": "Communication Protocols",
"type": "Planning",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 58,
"header": "Success Metrics",
"type": "Planning",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Eddie Lake"
},
{
"id": 59,
"header": "Internationalization Support",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 60,
"header": "Backup and Recovery Procedures",
"type": "Technical content",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 61,
"header": "Monitoring and Alerting System",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Daniel Park"
},
{
"id": 62,
"header": "Code Review Guidelines",
"type": "Technical content",
"status": "Done",
"target": "12",
"limit": "15",
"reviewer": "Eddie Lake"
},
{
"id": 63,
"header": "Documentation Standards",
"type": "Technical content",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 64,
"header": "Release Management Process",
"type": "Planning",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Assign reviewer"
},
{
"id": 65,
"header": "Feature Prioritization Matrix",
"type": "Planning",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Emma Davis"
},
{
"id": 66,
"header": "Technical Debt Assessment",
"type": "Technical content",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Eddie Lake"
},
{
"id": 67,
"header": "Capacity Planning",
"type": "Planning",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 68,
"header": "Service Level Agreements",
"type": "Legal",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Assign reviewer"
}
]npx shadcn@latest add dashboard-01Dashboard with Revenue and Customers
"use client"
import { useState } from "react"
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
import { CustomersTable } from "./components/customers-table"
import { DashboardCharts } from "./components/dashboard-charts"
import { DashboardHeader } from "./components/dashboard-header"
import { DashboardSidebar } from "./components/dashboard-sidebar"
export default function Page() {
const [period, setPeriod] = useState<"day" | "week" | "month" | "year">(
"week"
)
const [range, setRange] = useState({ from: "2025-11-10", to: "2025-11-16" })
const bindTheme = (node: HTMLElement | null) => {
if (!node) {
return
}
const onKey = (event: KeyboardEvent) => {
const target = event.target as HTMLElement
if (
event.key === "D" &&
event.shiftKey &&
!target.isContentEditable &&
!["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)
) {
document.documentElement.classList.toggle("dark")
}
}
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}
return (
<SidebarProvider
className="isolate min-h-dvh bg-background font-sans text-foreground antialiased [--chart-1:#3b82f6] [--chart-2:#d4d4d8] [--font-sans:system-ui,sans-serif] [--sidebar-width-icon:3.5rem]! [--sidebar-width:15.5rem]! **:data-[slot=sidebar-group-label]:transition-none **:data-[slot=sidebar-menu-button]:transition-none **:data-[slot=sidebar-container]:ease-[cubic-bezier(0.23,1,0.32,1)] **:data-[slot=sidebar-gap]:ease-[cubic-bezier(0.23,1,0.32,1)] dark:[--chart-2:#3f3f46] dark:[--sidebar-accent:#1a1a1a] dark:[--sidebar:#0d0d0d]"
defaultOpen={false}
>
<a
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:rounded-lg focus:bg-background focus:p-3"
href="#overview"
>
Skip to dashboard
</a>
<DashboardSidebar />
<SidebarInset className="min-w-0" id="overview" ref={bindTheme}>
<DashboardHeader
onPeriodChange={setPeriod}
onRangeChange={setRange}
period={period}
range={range}
/>
<div className="@container/metrics border-b">
<dl className="grid @3xl/metrics:grid-cols-4 grid-cols-2">
{metrics.map((metric) => (
<div
className="flex min-w-0 flex-col justify-center gap-2 border-b @3xl/metrics:border-b-0 @3xl/metrics:not-first:border-l @3xl/metrics:px-7 px-4 py-6 even:border-l [&:nth-last-child(-n+2)]:border-b-0"
key={metric.label}
>
<dt className="truncate text-muted-foreground text-sm sm:text-[0.9375rem]">
{metric.label}
</dt>
<dd className="flex flex-wrap items-baseline gap-2.5">
<span className="font-semibold text-[1.75rem] tabular-nums tracking-tight">
{metric.value}
</span>
<span
className={cn(
"font-medium text-[0.8125rem] tabular-nums",
metric.changeClass
)}
>
{metric.change}
</span>
</dd>
</div>
))}
</dl>
</div>
<DashboardCharts from={range.from} period={period} to={range.to} />
<CustomersTable />
</SidebarInset>
</SidebarProvider>
)
}
const metrics = [
{
label: "New customers",
value: "1,284",
change: "+8.2%",
changeClass: "text-green-600 dark:text-green-400",
},
{
label: "Success rate",
value: "97.4%",
change: "+0.6%",
changeClass: "text-green-600 dark:text-green-400",
},
{
label: "Failure rate",
value: "2.6%",
change: "-0.6%",
changeClass: "text-red-600 dark:text-red-400",
},
{
label: "Refunds",
value: "38",
change: "0.0%",
changeClass: "text-muted-foreground",
},
]"use client"
import {
DownloadIcon,
EllipsisIcon,
EyeIcon,
FunnelIcon,
MailIcon,
SearchIcon,
XIcon,
} from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { cn } from "@/lib/utils"
const easeOut = "ease-[cubic-bezier(0.23,1,0.32,1)]"
const press = `${easeOut} transition-[scale,background-color] duration-150 active:scale-[0.96] motion-reduce:active:scale-100`
const swap = `${easeOut} fade-in zoom-in-95 motion-reduce:zoom-in-100 animate-in duration-150`
export function CustomersTable() {
const [query, setQuery] = useState("")
const [selected, setSelected] = useState<Record<Facet, readonly string[]>>({
Plan: [],
Status: [],
})
const [page, setPage] = useState(1)
const [customer, setCustomer] = useState<Customer | null>(null)
const bindSearch = (node: HTMLInputElement | null) => {
if (!node) {
return
}
const focusSearch = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault()
node.focus()
}
}
window.addEventListener("keydown", focusSearch)
return () => window.removeEventListener("keydown", focusSearch)
}
const term = query.trim().toLocaleLowerCase()
const filtered = customers.filter(
(item) =>
(!term ||
item.name.toLocaleLowerCase().includes(term) ||
item.email.toLocaleLowerCase().includes(term)) &&
(!selected.Plan.length || selected.Plan.includes(item.plan)) &&
(!selected.Status.length || selected.Status.includes(item.status))
)
const pageCount = Math.max(1, Math.ceil(filtered.length / 9))
const visibleCustomers = filtered.slice((page - 1) * 9, page * 9)
const filterCount = selected.Plan.length + selected.Status.length
const toggle = (facet: Facet, option: string) => {
setSelected((current) => ({
...current,
[facet]: current[facet].includes(option)
? current[facet].filter((item) => item !== option)
: [...current[facet], option],
}))
setPage(1)
}
const clear = (facet?: Facet) => {
setSelected((current) =>
facet ? { ...current, [facet]: [] } : { Plan: [], Status: [] }
)
setPage(1)
}
const exportCustomers = () => {
const csv = [
["Customer", "Email", "Signed up", "Plan", "Status"],
...filtered.map((item) => [
item.name,
item.email,
item.signedUp,
item.plan,
item.status,
]),
]
.map((row) =>
row.map((value) => `"${value.replaceAll('"', '""')}"`).join(",")
)
.join("\n")
const url = URL.createObjectURL(
new Blob([csv], { type: "text/csv;charset=utf-8" })
)
const link = document.createElement("a")
link.href = url
link.download = "customers.csv"
link.click()
URL.revokeObjectURL(url)
}
return (
<section className="@container/customers w-full" id="customers">
<div className="flex @3xl/customers:flex-row flex-col @3xl/customers:items-center @3xl/customers:justify-between gap-4 p-4 sm:p-7 sm:py-6">
<div>
<h2 className="font-semibold text-[18px] leading-6">New customers</h2>
<p className="mt-1 text-[13px] text-muted-foreground">
Recent signups across all plans
</p>
</div>
<div className="flex min-w-0 flex-1 flex-wrap items-center @3xl/customers:justify-end gap-2">
{filterCount > 0 && (
<fieldset
aria-label="Active customer filters"
className="flex flex-wrap items-center gap-2"
>
{facets.map(
(facet) =>
selected[facet].length > 0 && (
<div
className={cn(
swap,
"flex h-7 items-center rounded-md bg-muted pl-2 text-[0.8125rem]"
)}
key={facet}
>
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
className="flex h-full max-w-56 items-center gap-1 rounded-l-md outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
type="button"
/>
}
>
<span className="text-muted-foreground">
{facet}:
</span>
<span className="truncate font-medium">
{selected[facet].join(", ")}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-44 rounded-[10px] p-1.5">
<DropdownMenuGroup>
{options[facet].map((option) => (
<DropdownMenuCheckboxItem
checked={selected[facet].includes(option)}
className="min-h-9 px-2.5"
closeOnClick={false}
key={option}
onCheckedChange={() => toggle(facet, option)}
>
{option}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<button
aria-label={`Remove ${facet.toLowerCase()} filter`}
className="flex h-full w-6 items-center justify-center rounded-r-md text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
onClick={() => clear(facet)}
type="button"
>
<XIcon aria-hidden="true" className="size-3.5" />
</button>
</div>
)
)}
<Button
onClick={() => clear()}
size="sm"
type="button"
variant="ghost"
>
Clear all
</Button>
</fieldset>
)}
<label className="sr-only" htmlFor="customer-search">
Search customers
</label>
<InputGroup className="h-9 @3xl/customers:w-60 w-full border-border bg-background dark:border-input">
<InputGroupAddon>
<InputGroupText>
<SearchIcon aria-hidden="true" />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput
className="text-[16px] sm:text-sm"
id="customer-search"
name="customer-search"
onChange={(event) => {
setQuery(event.target.value)
setPage(1)
}}
placeholder="Search customers"
ref={bindSearch}
type="search"
value={query}
/>
<InputGroupAddon align="inline-end">
<kbd className="hidden rounded border bg-muted px-1.5 py-0.5 font-medium text-[10px] sm:block">
⌘K
</kbd>
</InputGroupAddon>
</InputGroup>
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
className={cn(press, "h-9 flex-1 sm:flex-none")}
size="lg"
variant="outline"
/>
}
>
<FunnelIcon aria-hidden="true" />
Filter{filterCount ? ` (${filterCount})` : ""}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{facets.map((facet, index) => (
<DropdownMenuGroup key={facet}>
{index > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel>{facet}</DropdownMenuLabel>
{options[facet].map((option) => (
<DropdownMenuCheckboxItem
checked={selected[facet].includes(option)}
key={option}
onCheckedChange={() => toggle(facet, option)}
>
{option}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
))}
{filterCount > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => clear()}>
Clear filters
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<Button
className={cn(press, "h-9 flex-1 sm:flex-none")}
onClick={exportCustomers}
size="lg"
variant="outline"
>
<DownloadIcon aria-hidden="true" />
Export
</Button>
</div>
</div>
</div>
<div className="border-t">
<Table className="min-w-245 table-fixed [&_td:first-child]:pl-4 sm:[&_td:first-child]:pl-7 [&_td:last-child]:pr-4 sm:[&_td:last-child]:pr-7 [&_th:first-child]:pl-4 sm:[&_th:first-child]:pl-7 [&_th:last-child]:pr-4 sm:[&_th:last-child]:pr-7">
<colgroup>
<col className="w-[27%]" />
<col className="w-[23%]" />
<col className="w-[18%]" />
<col className="w-[12%]" />
<col className="w-[calc(20%-68px)]" />
<col className="w-17" />
</colgroup>
<TableHeader>
<TableRow className="h-11 hover:bg-transparent">
<TableHead className="px-2 text-muted-foreground text-xs">
Customer
</TableHead>
<TableHead className="px-2 text-muted-foreground text-xs">
Email
</TableHead>
<TableHead className="px-2 text-muted-foreground text-xs">
Signed up
</TableHead>
<TableHead className="px-2 text-muted-foreground text-xs">
Plan
</TableHead>
<TableHead className="px-2 text-muted-foreground text-xs">
Status
</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visibleCustomers.map((item) => (
<TableRow className="h-14" key={item.email}>
<TableCell className="px-2 py-0 font-medium text-sm">
{item.name}
</TableCell>
<TableCell className="px-2 py-0 text-muted-foreground text-sm">
{item.email}
</TableCell>
<TableCell className="px-2 py-0 text-muted-foreground text-sm">
{item.signedUp}
</TableCell>
<TableCell className="px-2 py-0 text-sm">{item.plan}</TableCell>
<TableCell className="px-2 py-0">
<StatusDot status={item.status} />
</TableCell>
<TableCell className="px-0 py-0 text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label={`Actions for ${item.name}`}
size="icon-sm"
variant="ghost"
/>
}
>
<EllipsisIcon aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-60 rounded-[10px] p-1.5"
sideOffset={6}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="flex flex-col gap-1 px-2.5 py-2">
<div className="truncate font-medium text-foreground text-sm">
{item.name}
</div>
<div className="truncate font-normal text-xs">
{item.email}
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
onClick={() => setCustomer(item)}
>
<EyeIcon
aria-hidden="true"
className="text-muted-foreground"
/>
View customer
</DropdownMenuItem>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
render={
<a
aria-label={`Send email to ${item.name}`}
href={`mailto:${item.email}`}
>
<MailIcon
aria-hidden="true"
className="text-muted-foreground"
/>
Send email
</a>
}
/>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
{!visibleCustomers.length && (
<TableRow className="h-28 hover:bg-transparent">
<TableCell
className="text-center text-muted-foreground text-sm"
colSpan={6}
>
No customers match your search and filters.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex flex-col gap-3 border-t p-4 sm:flex-row sm:items-center sm:justify-between sm:px-7">
<p aria-live="polite" className="text-[13px] text-muted-foreground">
{filtered.length
? `${(page - 1) * 9 + 1}–${Math.min(page * 9, filtered.length)} of ${filtered.length} customers`
: "0 customers"}
</p>
<nav aria-label="Customer pages" className="flex items-center gap-1">
<Button
disabled={page === 1}
onClick={() => setPage((current) => Math.max(1, current - 1))}
size="sm"
variant="outline"
>
Previous
</Button>
{Array.from({ length: pageCount }, (_, index) => index + 1).map(
(number) => (
<Button
aria-current={page === number ? "page" : undefined}
key={number}
onClick={() => setPage(number)}
size="icon-sm"
variant={page === number ? "secondary" : "ghost"}
>
{number}
</Button>
)
)}
<Button
disabled={page === pageCount}
onClick={() =>
setPage((current) => Math.min(pageCount, current + 1))
}
size="sm"
variant="outline"
>
Next
</Button>
</nav>
</div>
<Dialog
onOpenChange={(open) => {
if (!open) {
setCustomer(null)
}
}}
open={customer !== null}
>
<DialogContent className="sm:max-w-md">
{customer && (
<>
<DialogHeader>
<DialogTitle>{customer.name}</DialogTitle>
<DialogDescription>
Customer details and signup information.
</DialogDescription>
</DialogHeader>
<dl className="grid grid-cols-[88px_1fr] gap-x-4 gap-y-3 text-sm">
<dt className="text-muted-foreground">Email</dt>
<dd className="min-w-0 break-all">{customer.email}</dd>
<dt className="text-muted-foreground">Signed up</dt>
<dd>{customer.signedUp}</dd>
<dt className="text-muted-foreground">Plan</dt>
<dd>{customer.plan}</dd>
<dt className="text-muted-foreground">Status</dt>
<dd>
<StatusDot status={customer.status} />
</dd>
</dl>
<DialogFooter>
<Button
render={
<a
aria-label={`Contact ${customer.name}`}
href={`mailto:${customer.email}`}
>
<MailIcon aria-hidden="true" />
Contact customer
</a>
}
/>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
</section>
)
}
function StatusDot({ status }: { readonly status: Status }) {
return (
<span className="inline-flex items-center gap-2 text-sm">
<span
aria-hidden="true"
className={cn("size-1.5 rounded-full", statusDots[status])}
/>
{status}
</span>
)
}
type Plan = "Starter" | "Pro" | "Team"
type Status = "Active" | "Pending" | "Under review" | "Rejected" | "Inactive"
type Customer = {
readonly name: string
readonly email: string
readonly signedUp: string
readonly plan: Plan
readonly status: Status
}
const customers: readonly Customer[] = [
{
name: "Priya Raman",
email: "priya.raman@lumenlabs.io",
signedUp: "Today, 9:12 AM",
plan: "Pro",
status: "Active",
},
{
name: "Daniel Okafor",
email: "d.okafor@fastmail.com",
signedUp: "Today, 8:40 AM",
plan: "Starter",
status: "Pending",
},
{
name: "Mei Lin Chen",
email: "meilin@outlook.com",
signedUp: "Yesterday, 6:05 PM",
plan: "Team",
status: "Active",
},
{
name: "Tomás Herrera",
email: "tomas.h@gmail.com",
signedUp: "Yesterday, 2:31 PM",
plan: "Pro",
status: "Under review",
},
{
name: "Aisha Bello",
email: "aisha.bello@proton.me",
signedUp: "11 Nov, 4:18 PM",
plan: "Starter",
status: "Rejected",
},
{
name: "Lucas Moreau",
email: "l.moreau@yahoo.fr",
signedUp: "11 Nov, 11:02 AM",
plan: "Team",
status: "Active",
},
{
name: "Hannah Weiss",
email: "hannah.w@gmail.com",
signedUp: "10 Nov, 7:44 PM",
plan: "Pro",
status: "Pending",
},
{
name: "Kenji Sato",
email: "kenji.sato@icloud.com",
signedUp: "10 Nov, 3:15 PM",
plan: "Starter",
status: "Inactive",
},
{
name: "Olivia Brooks",
email: "olivia@brooks.studio",
signedUp: "9 Nov, 10:27 AM",
plan: "Team",
status: "Active",
},
{
name: "Sofia Alvarez",
email: "sofia@northstar.co",
signedUp: "9 Nov, 8:16 AM",
plan: "Pro",
status: "Active",
},
{
name: "Marcus Johnson",
email: "marcus.j@hey.com",
signedUp: "8 Nov, 5:42 PM",
plan: "Team",
status: "Under review",
},
{
name: "Elena Petrova",
email: "elena@kinetic.design",
signedUp: "8 Nov, 1:20 PM",
plan: "Starter",
status: "Pending",
},
{
name: "Noah Williams",
email: "noah@fieldnotes.app",
signedUp: "7 Nov, 4:55 PM",
plan: "Pro",
status: "Active",
},
{
name: "Fatima Zahra",
email: "fatima.z@proton.me",
signedUp: "7 Nov, 11:09 AM",
plan: "Team",
status: "Active",
},
{
name: "Ethan Park",
email: "ethan.park@icloud.com",
signedUp: "6 Nov, 6:33 PM",
plan: "Starter",
status: "Inactive",
},
{
name: "Amara Nwosu",
email: "amara@woven.studio",
signedUp: "6 Nov, 2:08 PM",
plan: "Pro",
status: "Rejected",
},
{
name: "Gabriel Silva",
email: "gabriel@orbit.dev",
signedUp: "5 Nov, 9:47 AM",
plan: "Team",
status: "Active",
},
{
name: "Leila Haddad",
email: "leila.h@fastmail.com",
signedUp: "4 Nov, 3:26 PM",
plan: "Starter",
status: "Pending",
},
]
type Facet = "Plan" | "Status"
const facets: readonly Facet[] = ["Plan", "Status"]
const options: Record<Facet, readonly string[]> = {
Plan: ["Starter", "Pro", "Team"],
Status: ["Active", "Pending", "Under review", "Rejected", "Inactive"],
}
const statusDots: Record<Status, string> = {
Active: "bg-green-500",
Pending: "bg-amber-500",
"Under review": "bg-blue-500",
Rejected: "bg-red-500",
Inactive: "bg-muted-foreground/40",
}"use client"
import { format, parseISO } from "date-fns"
import {
Area,
CartesianGrid,
ComposedChart,
Line,
XAxis,
YAxis,
} from "recharts"
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
import { cn } from "@/lib/utils"
const swap =
"ease-[cubic-bezier(0.23,1,0.32,1)] fade-in zoom-in-95 motion-reduce:zoom-in-100 animate-in duration-150"
export function DashboardCharts({
period,
from,
to,
}: {
period: "day" | "week" | "month" | "year"
from: string
to: string
}) {
const hourly = from === to
const start = days.findIndex((day) => day.date >= from)
const current = days.filter((day) => day.date >= from && day.date <= to)
const prior = days.slice(start - current.length, start)
const data = hourly
? hours(current[0], prior[0])
: current.map((day, index) => ({
date: day.date,
revenue: day.revenue,
priorRevenue: prior[index]?.revenue ?? 0,
charges: day.charges,
priorCharges: prior[index]?.charges ?? 0,
}))
const tickStep = Math.max(1, Math.ceil(data.length / 7))
const ticks = data
.filter((_, index) => index % tickStep === 0)
.map((row) => row.date)
const tickFormat = hourly ? "ha" : data.length > 90 ? "MMM" : "d MMM"
const labelFormat = hourly ? "EEE, d MMM, ha" : "EEE, d MMM yyyy"
return (
<div className="@container/charts border-b">
<div className="grid @4xl/charts:grid-cols-2">
{panels.map((panel) => {
const total = data.reduce((sum, row) => sum + row[panel.current], 0)
const previous = data.reduce(
(sum, row) => sum + row[panel.previous],
0
)
const change = previous ? ((total - previous) / previous) * 100 : 0
const config = {
[panel.current]: {
label: `This ${period}`,
color: "var(--chart-1)",
},
[panel.previous]: {
label: `Last ${period}`,
color: "var(--chart-2)",
},
} satisfies ChartConfig
return (
<section
aria-labelledby={`${panel.id}-title`}
className="@container/panel flex min-h-100 min-w-0 flex-col gap-7 border-t @4xl/charts:border-t-0 @4xl/charts:not-first:border-l p-4 py-7 first:border-t-0 sm:px-7"
id={panel.id}
key={panel.id}
>
<div className="relative flex flex-col gap-2">
<h2
className="text-[0.9375rem] text-muted-foreground"
id={`${panel.id}-title`}
>
{panel.title}
</h2>
<div className="flex flex-wrap items-baseline gap-2.5">
<p className="font-semibold text-[1.75rem] tabular-nums tracking-tight">
{panel.currency ? "$" : ""}
{total.toLocaleString("en-US")}
</p>
<p
className={cn(
"font-medium text-[0.8125rem] tabular-nums",
change < 0
? "text-red-600 dark:text-red-400"
: "text-green-600 dark:text-green-400"
)}
>
{change > 0 ? "+" : ""}
{change.toFixed(1)}%
</p>
<p className="text-[0.8125rem] text-muted-foreground">
vs. last {period}
</p>
</div>
<div className="@lg/panel:absolute @lg/panel:top-1 @lg/panel:right-0 flex items-center gap-4 text-[0.8125rem]">
<div className="flex items-center gap-2">
<span
aria-hidden="true"
className="h-0.75 w-2.5 rounded-full bg-chart-1"
/>
This {period}
</div>
<div className="flex items-center gap-2">
<span
aria-hidden="true"
className="h-0.75 w-2.5 rounded-full bg-chart-2"
/>
Last {period}
</div>
</div>
</div>
{data.length ? (
<ChartContainer
className={cn(swap, "aspect-auto h-63 w-full")}
config={config}
key={`${from}-${to}`}
>
<ComposedChart
accessibilityLayer
data={data}
margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
>
<CartesianGrid
stroke="var(--border)"
strokeDasharray="3 4"
vertical={false}
/>
<XAxis
axisLine={false}
dataKey="date"
height={40}
minTickGap={24}
tickFormatter={(date: string) =>
format(parseISO(date), tickFormat)
}
tickLine={false}
tickMargin={16}
ticks={ticks}
/>
<YAxis
axisLine={false}
domain={[0, "auto"]}
tickCount={4}
tickFormatter={(value: number) =>
`${panel.currency ? "$" : ""}${value >= 1000 ? `${value / 1000}k` : value}`
}
tickLine={false}
tickMargin={8}
width={48}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-47.5 rounded-[10px] bg-popover px-3 py-2.5"
formatter={(value, name) => (
<div className="flex w-full items-center gap-2">
<span
aria-hidden="true"
className={cn(
"size-2 shrink-0 rounded-xs",
name === panel.current
? "bg-chart-1"
: "bg-chart-2"
)}
/>
<span className="flex-1 text-muted-foreground">
{name === panel.current
? `This ${period}`
: `Last ${period}`}
</span>
<span className="font-medium text-foreground tabular-nums">
{panel.currency ? "$" : ""}
{Number(value).toLocaleString("en-US")}
</span>
</div>
)}
labelClassName="font-normal text-muted-foreground"
labelFormatter={(label) =>
typeof label === "string"
? format(parseISO(label), labelFormat)
: label
}
/>
}
cursor={{
stroke: "var(--chart-1)",
strokeDasharray: "3 3",
}}
/>
<Area
activeDot={false}
dataKey={panel.current}
fill="var(--chart-1)"
fillOpacity={0.1}
isAnimationActive={false}
stroke="none"
tooltipType="none"
type="linear"
/>
<Line
activeDot={{
r: 5,
fill: "var(--chart-1)",
stroke: "var(--background)",
strokeWidth: 2,
}}
dataKey={panel.current}
dot={data.length === 1}
isAnimationActive={false}
stroke="var(--chart-1)"
strokeWidth={2.5}
type="linear"
/>
<Line
activeDot={{
r: 4,
fill: "var(--chart-2)",
stroke: "var(--background)",
strokeWidth: 2,
}}
dataKey={panel.previous}
dot={data.length === 1}
isAnimationActive={false}
stroke="var(--chart-2)"
strokeWidth={2}
type="linear"
/>
</ComposedChart>
</ChartContainer>
) : (
<p className="flex flex-1 items-center justify-center text-muted-foreground text-sm">
No data for this date range.
</p>
)}
</section>
)
})}
</div>
</div>
)
}
const panels = [
{
id: "revenue",
title: "Revenue",
current: "revenue",
previous: "priorRevenue",
currency: true,
},
{
id: "charges",
title: "Charges",
current: "charges",
previous: "priorCharges",
currency: false,
},
] as const
type Day = { date: string; revenue: number; charges: number }
// Seeded LCG so every render draws the same two years of demo data.
let seed = 20_251_116
const random = () => {
seed = (Math.imul(seed, 1_664_525) + 1_013_904_223) >>> 0
return seed / 2 ** 32
}
const days: Day[] = Array.from({ length: 731 }, (_, index) => {
const date = new Date(Date.UTC(2023, 10, 17 + index))
const growth = 1 + index / 731
const weekend = date.getUTCDay() % 6 === 0 ? 0.62 : 1
const season =
1 + 0.12 * Math.sin(((date.getUTCMonth() - 3) / 12) * Math.PI * 2)
const spike = random() < 0.04 ? 1.6 : 1
const revenue = Math.round(
3400 * growth * weekend * season * spike * (0.82 + random() * 0.36)
)
return {
date: date.toISOString().slice(0, 10),
revenue,
charges: Math.round((revenue / 13.5) * (0.9 + random() * 0.2)),
}
})
const activity = [
0.54, 0.46, 0.38, 0.35, 0.41, 0.58, 0.74, 0.97, 1.12, 1.38, 1.21, 1.53, 1.29,
1.41, 1.67, 1.48, 1.31, 1.58, 1.36, 1.19, 1.04, 0.86, 0.72, 0.63,
]
// Split a day's totals across 24 hours. Noise is keyed on the date so the
// same day always draws the same curve, and the hourly shares sum to the day.
function hours(day: Day | undefined, previous: Day | undefined) {
if (!(day && previous)) {
return []
}
const weights = [day, previous].map((row) => {
const key = Number(row.date.replaceAll("-", ""))
const noisy = activity.map(
(weight, hour) =>
weight * (0.8 + 0.4 * ((Math.sin(key + hour * 7.31) + 1) / 2))
)
const total = noisy.reduce((sum, weight) => sum + weight, 0)
return noisy.map((weight) => weight / total)
})
let share = 0
let priorShare = 0
return activity.map((_, hour) => {
const from = share
const priorFrom = priorShare
share += weights[0][hour]
priorShare += weights[1][hour]
return {
date: `${day.date}T${String(hour).padStart(2, "0")}:00:00`,
revenue: Math.round(day.revenue * share) - Math.round(day.revenue * from),
priorRevenue:
Math.round(previous.revenue * priorShare) -
Math.round(previous.revenue * priorFrom),
charges: Math.round(day.charges * share) - Math.round(day.charges * from),
priorCharges:
Math.round(previous.charges * priorShare) -
Math.round(previous.charges * priorFrom),
}
})
}"use client"
import { format, parseISO } from "date-fns"
import {
Bell,
CalendarDays,
ChartNoAxesCombined,
ChevronDown,
LockKeyhole,
LogOut,
Moon,
PanelLeftClose,
PanelLeftOpen,
Sun,
UserRound,
} from "lucide-react"
import { type ComponentProps, useState } from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover"
import { Separator } from "@/components/ui/separator"
import { useSidebar } from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { cn } from "@/lib/utils"
import type { DashboardCharts } from "./dashboard-charts"
type ChartOptions = ComponentProps<typeof DashboardCharts>
type DateRange = Pick<ChartOptions, "from" | "to">
const press =
"ease-[cubic-bezier(0.23,1,0.32,1)] transition-[scale,background-color] duration-150 active:scale-[0.96] motion-reduce:active:scale-100"
export function DashboardHeader({
period,
range,
onPeriodChange,
onRangeChange,
}: {
period: ChartOptions["period"]
range: DateRange
onPeriodChange: (period: ChartOptions["period"]) => void
onRangeChange: (range: DateRange) => void
}) {
const { isMobile, openMobile, state, toggleSidebar } = useSidebar()
const sidebarOpen = isMobile ? openMobile : state === "expanded"
const [customRange, setCustomRange] = useState<DateRange | null>(null)
const [unread, setUnread] = useState(true)
const [notificationsOpen, setNotificationsOpen] = useState(false)
const from = parseISO(range.from)
const to = parseISO(range.to)
const rangeLabel =
range.from === range.to
? format(to, "d MMM")
: `${format(from, from.getMonth() === to.getMonth() && from.getFullYear() === to.getFullYear() ? "d" : "d MMM")} – ${format(to, "d MMM")}`
return (
<>
<header className="flex min-h-15 flex-wrap items-center justify-between gap-3 border-b px-4 py-3 lg:px-7 lg:py-2">
<div className="flex items-center gap-3">
<Button
aria-expanded={sidebarOpen}
aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
onClick={toggleSidebar}
size="icon-sm"
type="button"
variant="ghost"
>
{sidebarOpen ? (
<PanelLeftClose aria-hidden="true" />
) : (
<PanelLeftOpen aria-hidden="true" />
)}
</Button>
<h1 className="font-semibold text-lg tracking-tight">Overview</h1>
<p className="text-[0.8125rem] text-muted-foreground max-sm:hidden">
Updated 2 min ago
</p>
</div>
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<ToggleGroup
aria-label="Chart period"
className="rounded-[9px] border bg-muted/50 p-0.75"
onValueChange={(values) => {
const next = periods.find((item) => item.value === values[0])
if (!next) {
return
}
onPeriodChange(next.value)
onRangeChange({ from: next.from, to: "2025-11-16" })
}}
size="sm"
spacing={0.5}
value={[period]}
>
{periods.map((item) => (
<ToggleGroupItem
className="h-7 min-w-0 rounded-md px-3 text-[0.8125rem]"
key={item.value}
value={item.value}
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label={`Date range: ${rangeLabel}`}
className={press}
size="lg"
variant="outline"
/>
}
>
<CalendarDays data-icon="inline-start" />
{rangeLabel}
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-50 rounded-[10px] p-1.5"
>
<DropdownMenuGroup>
<DropdownMenuLabel className="sr-only">
Date range
</DropdownMenuLabel>
<DropdownMenuRadioGroup
onValueChange={(value) => {
const next = ranges.find((item) => item.from === value)
if (next) {
onRangeChange({ from: next.from, to: "2025-11-16" })
}
}}
value={range.to === "2025-11-16" ? range.from : ""}
>
{ranges.map((item) => (
<DropdownMenuRadioItem
className="min-h-8.5 px-2.5"
key={item.label}
value={item.from}
>
{item.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="min-h-8.5 px-2.5"
onClick={() => setCustomRange(range)}
>
Custom range…
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<Separator className="h-5! max-sm:hidden" orientation="vertical" />
<Button
aria-label="Toggle dark mode"
className={cn(press, "relative")}
onClick={() => document.documentElement.classList.toggle("dark")}
size="icon-lg"
title="Toggle dark mode (Shift+D)"
type="button"
variant="ghost"
>
<Sun className="scale-100 opacity-100 transition-[opacity,scale] duration-150 ease-out dark:scale-50 dark:opacity-0" />
<Moon className="absolute scale-50 opacity-0 transition-[opacity,scale] duration-150 ease-out dark:scale-100 dark:opacity-100" />
</Button>
<Popover onOpenChange={setNotificationsOpen} open={notificationsOpen}>
<PopoverTrigger
render={
<Button
aria-label={
unread ? "Notifications, unread updates" : "Notifications"
}
className={cn(press, "relative")}
size="icon-lg"
variant="ghost"
/>
}
>
<Bell />
{unread && (
<span className="absolute top-2 right-2 size-1.5 rounded-full bg-blue-500 ring-2 ring-background" />
)}
</PopoverTrigger>
<PopoverContent
align="end"
className="w-90 max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-xl p-0"
sideOffset={8}
>
<div className="flex h-13 items-center justify-between gap-3 border-b px-4">
<PopoverTitle>Notifications</PopoverTitle>
<div className="text-muted-foreground text-xs tabular-nums">
{unread ? "2 unread" : "All read"}
</div>
<PopoverDescription className="sr-only">
Recent workspace activity.
</PopoverDescription>
</div>
<Button
className="h-auto w-full items-start justify-start gap-3 whitespace-normal rounded-none p-4 text-left font-normal"
onClick={() => setNotificationsOpen(false)}
render={
<a href="#revenue">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-background">
<ChartNoAxesCombined
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium">Weekly report</span>
<time className="shrink-0 text-muted-foreground text-xs">
2m ago
</time>
</div>
<p className="text-[0.8125rem] text-muted-foreground leading-5">
10–16 Nov · $42,910 in revenue
</p>
<p className="text-muted-foreground text-xs">
3,482 charges processed
</p>
</div>
<span
aria-hidden="true"
className={
unread
? "mt-1.5 size-1.5 shrink-0 rounded-full bg-blue-500"
: "mt-1.5 size-1.5 shrink-0"
}
/>
</a>
}
variant="ghost"
/>
<Separator />
<Button
className="h-auto w-full items-start justify-start gap-3 whitespace-normal rounded-none p-4 text-left font-normal"
onClick={() => setNotificationsOpen(false)}
render={
<a href="#customers">
<Avatar className="size-9 rounded-lg after:rounded-lg">
<AvatarImage
alt=""
className="rounded-lg"
src="https://i.pravatar.cc/96?img=47"
/>
<AvatarFallback className="rounded-lg">PR</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium">
Priya Raman signed up
</span>
<time className="shrink-0 text-muted-foreground text-xs">
18m ago
</time>
</div>
<p className="text-[0.8125rem] text-muted-foreground leading-5">
Joined Lumen Labs on the Pro plan.
</p>
</div>
<span
aria-hidden="true"
className={
unread
? "mt-1.5 size-1.5 shrink-0 rounded-full bg-blue-500"
: "mt-1.5 size-1.5 shrink-0"
}
/>
</a>
}
variant="ghost"
/>
<div className="flex items-center justify-end border-t px-3 py-2">
<Button
disabled={!unread}
onClick={() => setUnread(false)}
size="sm"
type="button"
variant="ghost"
>
Mark all as read
</Button>
</div>
</PopoverContent>
</Popover>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
aria-label="Account menu"
className={cn(press, "gap-2 rounded-full pr-2.5 pl-1")}
size="lg"
variant="outline"
/>
}
>
<Avatar className="size-7">
<AvatarFallback className="bg-muted font-semibold text-[0.6875rem] text-foreground">
EB
</AvatarFallback>
</Avatar>
<ChevronDown data-icon="inline-end" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-60 rounded-[10px] p-1.5"
>
<DropdownMenuGroup>
<DropdownMenuLabel className="flex flex-col gap-0.5 px-2.5 py-2">
<div className="font-medium text-foreground text-sm">
Ephraim Brooks
</div>
<div className="font-normal text-[0.8125rem]">
ephraim@lumenlabs.io
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="min-h-8.5 gap-2.5 px-2.5"
render={
<a aria-label="Profile" href="#">
<UserRound />
Profile
</a>
}
/>
<DropdownMenuItem
className="min-h-8.5 gap-2.5 px-2.5"
render={
<a
aria-label="Notifications"
href="#"
>
<Bell />
Notifications
</a>
}
/>
<DropdownMenuItem
className="min-h-8.5 gap-2.5 px-2.5"
render={
<a aria-label="Security" href="#">
<LockKeyhole />
Security
</a>
}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="min-h-8.5 gap-2.5 px-2.5"
render={
<a aria-label="Log out" href="#">
<LogOut />
Log out
</a>
}
variant="destructive"
/>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
<Dialog
onOpenChange={(open) => {
if (!open) {
setCustomRange(null)
}
}}
open={customRange !== null}
>
<DialogContent>
{customRange && (
<form
className="flex flex-col gap-5"
onSubmit={(event) => {
event.preventDefault()
if (
customRange.from &&
customRange.to &&
customRange.from <= customRange.to
) {
onRangeChange(customRange)
setCustomRange(null)
}
}}
>
<DialogHeader>
<DialogTitle>Custom date range</DialogTitle>
<DialogDescription>
Choose the dates to include in your charts.
</DialogDescription>
</DialogHeader>
<FieldGroup>
<Field>
<FieldLabel htmlFor="dashboard-from">Start date</FieldLabel>
<Input
id="dashboard-from"
max={customRange.to}
name="from"
onChange={(event) =>
setCustomRange({
...customRange,
from: event.target.value,
})
}
required
type="date"
value={customRange.from}
/>
</Field>
<Field>
<FieldLabel htmlFor="dashboard-to">End date</FieldLabel>
<Input
id="dashboard-to"
min={customRange.from}
name="to"
onChange={(event) =>
setCustomRange({ ...customRange, to: event.target.value })
}
required
type="date"
value={customRange.to}
/>
</Field>
</FieldGroup>
<DialogFooter>
<Button
onClick={() => setCustomRange(null)}
type="button"
variant="outline"
>
Cancel
</Button>
<Button className={press} type="submit">
Apply range
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
</>
)
}
const periods = [
{ label: "Day", value: "day", from: "2025-11-16" },
{ label: "Week", value: "week", from: "2025-11-10" },
{ label: "Month", value: "month", from: "2025-10-18" },
{ label: "Year", value: "year", from: "2025-01-01" },
] as const
const ranges = [
{ label: "Last 7 days", from: "2025-11-10" },
{ label: "Last 14 days", from: "2025-11-03" },
{ label: "Last 30 days", from: "2025-10-18" },
{ label: "This quarter", from: "2025-10-01" },
{ label: "Year to date", from: "2025-01-01" },
]"use client"
import {
ArrowLeftRight,
BriefcaseBusiness,
ChartNoAxesCombined,
Check,
ChevronDown,
CircleHelp,
CirclePlus,
CreditCard,
LayoutDashboard,
Moon,
Settings,
Users,
} from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Progress } from "@/components/ui/progress"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { cn } from "@/lib/utils"
export function DashboardSidebar() {
const { isMobile, setOpenMobile, state } = useSidebar()
const [workspaces, setWorkspaces] = useState([
"Lumen Labs",
"Halcyon Studio",
"Pinecrest Co.",
])
const [workspace, setWorkspace] = useState("Lumen Labs")
const [creating, setCreating] = useState(false)
const [name, setName] = useState("")
return (
<>
<Sidebar collapsible="icon">
<SidebarHeader className="px-3 pt-3.5 pb-5">
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
aria-label={`${workspace}, switch workspace`}
className="h-9 gap-2 rounded-lg px-2 font-medium group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0!"
/>
}
>
<span className="flex size-5 shrink-0 items-center justify-center rounded-[5px] bg-blue-500 font-semibold text-[0.6875rem] text-white">
{workspace.slice(0, 1)}
</span>
<span className="flex-1 truncate group-data-[collapsible=icon]:hidden">
{workspace}
</span>
<ChevronDown className="text-muted-foreground group-data-[collapsible=icon]:hidden" />
</DropdownMenuTrigger>
<DropdownMenuContent
className={cn(
"rounded-[10px] p-1.5",
state === "collapsed" && !isMobile
? "w-56"
: "w-(--anchor-width)"
)}
side={state === "collapsed" && !isMobile ? "right" : "bottom"}
sideOffset={8}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="px-2.5 py-2">
Workspaces
</DropdownMenuLabel>
{workspaces.map((item, index) => (
<DropdownMenuItem
className={cn(
"min-h-9 gap-2.5 px-2.5",
item === workspace && "bg-accent"
)}
key={item}
onClick={() => setWorkspace(item)}
>
<span
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded-[5px] font-semibold text-[0.6875rem]",
index === 0
? "bg-blue-500 text-white"
: "bg-muted text-muted-foreground"
)}
>
{item.slice(0, 1)}
</span>
<span className="flex-1 truncate">{item}</span>
{item === workspace && <Check aria-label="Selected" />}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
onClick={() => setCreating(true)}
>
<CirclePlus />
Create workspace
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup className="px-3 py-0">
<SidebarGroupLabel className="px-2.5 text-[0.6875rem] tracking-wider">
WORKSPACE
</SidebarGroupLabel>
<SidebarMenu className="gap-0.5">
{navigation.map((item) => (
<SidebarMenuItem
className={
item.label === "Overview"
? "before:-left-3 before:absolute before:top-2.5 before:h-4 before:w-0.75 before:rounded-r-sm before:bg-blue-500"
: undefined
}
key={item.label}
>
<SidebarMenuButton
className="h-9 gap-2.5 rounded-lg px-2.5"
isActive={item.label === "Overview"}
onClick={() => setOpenMobile(false)}
render={
<a aria-label={item.label} href={item.href}>
<item.icon
aria-hidden="true"
className={
item.label === "Overview"
? undefined
: "text-muted-foreground"
}
/>
<span className="flex-1 group-data-[collapsible=icon]:hidden">
{item.label}
</span>
{item.label === "Customers" && (
<div className="rounded-full bg-muted px-1.5 text-[0.6875rem] text-muted-foreground tabular-nums group-data-[collapsible=icon]:hidden">
12
</div>
)}
</a>
}
tooltip={item.label}
/>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="gap-3 px-3 pb-5">
<SidebarMenu className="gap-0.5">
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
aria-label="Settings"
className="h-9 gap-2.5 rounded-lg px-2.5"
tooltip="Settings"
/>
}
>
<Settings className="text-muted-foreground" />
<span className="group-data-[collapsible=icon]:hidden">
Settings
</span>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-64 rounded-[10px] p-1.5"
side="top"
sideOffset={8}
>
<DropdownMenuGroup>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
render={
<a
aria-label="Company profile"
href="#"
>
<BriefcaseBusiness />
Company profile
</a>
}
/>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
render={
<a aria-label="Team" href="#">
<Users />
Team
</a>
}
/>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
render={
<a aria-label="Billing" href="#">
<CreditCard />
Billing
</a>
}
/>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
closeOnClick={false}
onClick={() =>
document.documentElement.classList.toggle("dark")
}
>
<Moon />
<span className="flex-1">Appearance</span>
<div className="text-muted-foreground text-xs">
<span className="dark:hidden">Light</span>
<span className="not-dark:hidden">Dark</span>
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
className="h-9 gap-2.5 rounded-lg px-2.5"
render={
<a
aria-label="Help and support"
href="mailto:support@lumenlabs.io"
>
<CircleHelp className="text-muted-foreground" />
<span className="group-data-[collapsible=icon]:hidden">
Help & support
</span>
</a>
}
tooltip="Help and support"
/>
</SidebarMenuItem>
</SidebarMenu>
<div className="flex flex-col gap-3 rounded-[10px] border border-border bg-background p-3 group-data-[collapsible=icon]:hidden dark:border-input dark:bg-input/30">
<div className="flex items-center justify-between text-[0.8125rem]">
<span className="font-medium">Monthly volume</span>
<div className="text-[0.8125rem] text-muted-foreground tabular-nums">
72%
</div>
</div>
<Progress
aria-label="Monthly volume used"
className="[&_[data-slot=progress-indicator]]:bg-blue-500 [&_[data-slot=progress-track]]:bg-border"
value={72.4}
/>
<p className="text-muted-foreground text-xs tabular-nums">
$72,400 of $100,000 used
</p>
</div>
</SidebarFooter>
</Sidebar>
<Dialog onOpenChange={setCreating} open={creating}>
<DialogContent>
<form
className="flex flex-col gap-5"
onSubmit={(event) => {
event.preventDefault()
const nextName = name.trim()
if (!nextName) {
return
}
if (!workspaces.includes(nextName)) {
setWorkspaces([...workspaces, nextName])
}
setWorkspace(nextName)
setName("")
setCreating(false)
}}
>
<DialogHeader>
<DialogTitle>Create workspace</DialogTitle>
<DialogDescription>
Add a workspace to this dashboard preview.
</DialogDescription>
</DialogHeader>
<FieldGroup>
<Field>
<FieldLabel htmlFor="workspace-name">Workspace name</FieldLabel>
<Input
autoComplete="organization"
id="workspace-name"
maxLength={60}
name="workspace-name"
onChange={(event) => setName(event.target.value)}
pattern=".*\S.*"
required
value={name}
/>
</Field>
</FieldGroup>
<DialogFooter>
<Button
onClick={() => setCreating(false)}
type="button"
variant="outline"
>
Cancel
</Button>
<Button type="submit">Create workspace</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
)
}
const navigation = [
{ label: "Overview", href: "#overview", icon: LayoutDashboard },
{ label: "Customers", href: "#customers", icon: Users },
{ label: "Transactions", href: "/transactions", icon: ArrowLeftRight },
{ label: "Payouts", href: "/payouts", icon: CreditCard },
{ label: "Reports", href: "#revenue", icon: ChartNoAxesCombined },
]npx shadcn@latest add https://blocks.so/r/dashboard-01.json