Blocks.so
blockdashboard-01source: ok · hit
Dashboard with Revenue and Customers
A dashboard with revenue and customers block.
npx shadcn add "https://blocks.so/r/dashboard-01.json"free
Where it goes
kanbandashboard-shell
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumiconslucide-react
chartsrecharts
Registry dependencies
avatarbuttonchartdialogdropdown-menufieldinputinput-grouppopoverprogressseparatorsidebartabletoggle-group
Files5 · source cached 10/8/2026, 5:59:17 PM
content/components/dashboard/dashboard-01/dashboard-sidebar.tsx
'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="/settings/company"
>
<BriefcaseBusiness />
Company profile
</a>
}
/>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
render={
<a aria-label="Team" href="/settings/team">
<Users />
Team
</a>
}
/>
<DropdownMenuItem
className="min-h-9 gap-2.5 px-2.5"
render={
<a aria-label="Billing" href="/settings/billing">
<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 },
];
content/components/dashboard/dashboard-01/app/page.tsx
'use client';
import { useState } from 'react';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { cn } from '@/lib/utils';
import { CustomersTable } from '@/components/dashboard-01/customers-table';
import { DashboardCharts } from '@/components/dashboard-01/dashboard-charts';
import { DashboardHeader } from '@/components/dashboard-01/dashboard-header';
import { DashboardSidebar } from '@/components/dashboard-01/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',
},
];
content/components/dashboard/dashboard-01/customers-table.tsx
'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',
};
content/components/dashboard/dashboard-01/dashboard-header.tsx
'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 '@/components/dashboard-01/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="/settings/profile">
<UserRound />
Profile
</a>
}
/>
<DropdownMenuItem
className="min-h-8.5 gap-2.5 px-2.5"
render={
<a
aria-label="Notifications"
href="/settings/notifications"
>
<Bell />
Notifications
</a>
}
/>
<DropdownMenuItem
className="min-h-8.5 gap-2.5 px-2.5"
render={
<a aria-label="Security" href="/settings/security">
<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="/login">
<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' },
];
content/components/dashboard/dashboard-01/dashboard-charts.tsx
'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),
};
});
}