All templates
Overview
Dashboard - Overview
Business overview with revenue and appointment charts, key stats, reviews, and booking source cards, inside the sidebar layout. Dashboard, home, overview, analytics, insights, metrics, KPIs, stats, or reports.
Preview
Open in new tabCode
'use client';
import {
Avatar,
CalDynamicIcon,
Field,
Heading,
IconButton,
type IconProps,
MenuItem,
SelectMenu,
SelectMenuItem,
SelectMenuListbox,
SelectMenuPopover,
SelectMenuTrigger,
Text,
TextLink,
Toggle,
Tooltip,
} from '@adaptavant/eds-core';
import {
ChevronLeftIcon,
ConnectIcon,
CustomerIcon,
GrowYourBrandIcon,
HomeIcon,
IntegrationsIcon,
MenuIcon,
MoneyIcon,
ReviewStarFilledIcon,
ServicesIcon,
SetmoreIcon,
SettingsIcon,
UpdatesIcon,
} from '@adaptavant/eds-icons/icons';
import * as React from 'react';
import {
CartesianGrid,
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
Tooltip as ChartTooltip,
XAxis,
YAxis,
} from 'recharts';
const NAV_ITEMS = [
{ label: 'Dashboard', icon: HomeIcon },
{
label: 'Calendar',
icon: ({ size }: IconProps) => (
<CalDynamicIcon size={size}>{new Date().getDate()}</CalDynamicIcon>
),
},
{ label: 'Services', icon: ServicesIcon },
{ label: 'Connect', icon: ConnectIcon },
{ label: 'Customers', icon: CustomerIcon },
{ label: 'Payments', icon: MoneyIcon },
{ label: 'Integrations', icon: IntegrationsIcon },
{ label: 'Updates', icon: UpdatesIcon },
{ label: 'Settings', icon: SettingsIcon },
];
/**
* The app frame: a collapsible navigation sidebar beside a main area that
* scrolls on its own. Pass the current page as `children`.
*/
export function AppShell({ children }: { children: React.ReactNode }) {
const [activeNav, setActiveNav] = React.useState('Dashboard');
const [isNavCollapsed, setIsNavCollapsed] = React.useState(false);
return (
<div className="flex h-screen overflow-hidden bg-canvas text-primary">
<NavSidebar
activeNav={activeNav}
isCollapsed={isNavCollapsed}
onNavigate={setActiveNav}
onToggleCollapsed={() => setIsNavCollapsed((collapsed) => !collapsed)}
/>
{/* The sidebar is fixed, so the main area is offset by its width. */}
<main
className={`min-w-0 flex-1 overflow-y-auto transition-[margin] duration-200 ease-in-out ${
isNavCollapsed ? 'ms-[57px]' : 'ms-[216px]'
}`}
>
{children}
</main>
</div>
);
}
const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
type DateRange = {
label: string;
dates: string;
/** Revenue per weekday, Monday first. */
revenue: number[];
/** Appointments per weekday, Monday first. */
appointments: number[];
};
const THIS_WEEK: DateRange = {
label: 'This week',
dates: '28 Sep – 4 Oct',
revenue: [0, 0, 0, 0, 0, 0, 0],
appointments: [0, 0, 0, 0, 0, 0, 0],
};
const LAST_WEEK: DateRange = {
label: 'Last week',
dates: '21 – 27 Sep',
revenue: [120, 80, 200, 150, 260, 310, 90],
appointments: [3, 2, 5, 4, 6, 8, 2],
};
const DATE_RANGES: DateRange[] = [THIS_WEEK, LAST_WEEK];
const sum = (values: number[]) => values.reduce((total, n) => total + n, 0);
/** Preview: the business dashboard in the shell's slot. */
export default function OverviewTemplate() {
const [range, setRange] = React.useState(LAST_WEEK);
const [isDetailed, setIsDetailed] = React.useState(false);
return (
<AppShell>
<div className="flex flex-col gap-6 p-8">
<Heading as="h1" className="sr-only">
Dashboard
</Heading>
<div className="flex items-center gap-6">
<Field label="Date range" labelVisibility="hidden">
<SelectMenu popoverPlacement="bottom-start">
<SelectMenuTrigger>
{range.label}{' '}
<span className="text-secondary">({range.dates})</span>
</SelectMenuTrigger>
<SelectMenuPopover>
<SelectMenuListbox options={DATE_RANGES}>
{(option) => (
<SelectMenuItem
id={option.label}
isSelected={option.label === range.label}
onClick={() => setRange(option)}
>
{option.label}
</SelectMenuItem>
)}
</SelectMenuListbox>
</SelectMenuPopover>
</SelectMenu>
</Field>
<Toggle
checked={isDetailed}
label="Detailed view"
onChange={(event) => setIsDetailed(event.target.checked)}
/>
</div>
<div className="grid grid-cols-[2fr_1fr] items-start gap-4">
<div className="flex flex-col gap-4">
<StatCard title="Revenue" value={`$${sum(range.revenue)}`}>
<WeekChart
data={range.revenue}
formatTick={(tick) => `$${tick}`}
/>
</StatCard>
<StatCard title="Appointments" value={sum(range.appointments)}>
<WeekChart data={range.appointments} />
</StatCard>
{isDetailed ? (
<div className="grid grid-cols-2 gap-4">
<StatCard title="Returning customers" value={0} />
<StatCard title="Occupancy rate" value="0%" />
</div>
) : null}
</div>
<div className="flex flex-col gap-4">
<Card title="Booking source">
<Text className="text-body-14 grid h-[120px] place-items-center">
No bookings
</Text>
</Card>
<StatCard title="Reviews" value={0}>
<div className="text-body-14 mt-12 flex items-center justify-between">
<Text>Overall rating</Text>
<Text className="flex items-center gap-2">
<ReviewStarFilledIcon size="16" />0{' '}
<span className="text-secondary">(0 Reviews)</span>
</Text>
</div>
</StatCard>
<Card title="Advanced reports">
<Text className="text-body-14 text-secondary">
Track performance and explore additional insights to inform your
strategy.
</Text>
<TextLink className="text-body-14 mt-10 self-start" href="#">
See insights
</TextLink>
</Card>
</div>
</div>
</div>
</AppShell>
);
}
function Card({
title,
children,
}: {
title: string;
children?: React.ReactNode;
}) {
return (
<section
aria-label={title}
className="flex flex-col gap-2 rounded-12px border border-solid border-tertiary p-6"
>
<Heading as="h2" className="text-heading-14">
{title}
</Heading>
{children}
</section>
);
}
function StatCard({
title,
value,
children,
}: {
title: string;
value: React.ReactNode;
children?: React.ReactNode;
}) {
return (
<Card title={title}>
<Text className="text-heading-32">{value}</Text>
{children}
</Card>
);
}
/**
* A weekday line chart: dashed gridlines at each tick, one point per day.
* The scale never drops below 3, so an empty week still shows its axis.
*/
function WeekChart({
data,
formatTick = String,
}: {
data: number[];
formatTick?: (tick: number) => string;
}) {
const tickCount = 3;
// Round the step up to 1, 2 or 5 times a power of ten, for readable ticks.
const rawStep = Math.max(1, Math.max(...data) / tickCount);
const magnitude = 10 ** Math.floor(Math.log10(rawStep));
const multiple = [1, 2, 5].find((n) => n * magnitude >= rawStep) ?? 10;
const step = multiple * magnitude;
const ticks = Array.from({ length: tickCount + 1 }, (_, i) => i * step);
const points = data.map((value, i) => ({ day: WEEKDAYS[i], value }));
const tick = { fill: 'var(--text-secondary)', fontSize: 12 };
return (
<div className="mt-6 h-[240px]">
<ResponsiveContainer>
<LineChart
data={points}
margin={{ top: 8, right: 0, bottom: 0, left: 0 }}
>
<CartesianGrid
stroke="var(--border-tertiary)"
strokeDasharray="4 4"
vertical={false}
/>
<XAxis
axisLine={false}
dataKey="day"
padding={{ left: 40, right: 40 }}
tick={tick}
tickLine={false}
tickMargin={12}
/>
<YAxis
axisLine={false}
domain={[0, step * tickCount]}
tick={tick}
tickFormatter={formatTick}
tickLine={false}
ticks={ticks}
width={48}
/>
{/* The baseline is solid, and drawn before the line so it sits under it. */}
<ReferenceLine stroke="var(--border-tertiary)" y={0} />
<ChartTooltip
contentStyle={{
background: 'var(--background-canvas)',
border: '1px solid var(--border-tertiary)',
borderRadius: 8,
}}
cursor={{ stroke: 'var(--border-tertiary)' }}
formatter={(value) => [formatTick(Number(value))]}
itemStyle={{ color: 'var(--text-primary)', fontSize: 12 }}
labelStyle={{ color: 'var(--text-secondary)', fontSize: 12 }}
/>
<Line
activeDot={{ r: 4 }}
dataKey="value"
dot={false}
isAnimationActive={false}
stroke="var(--text-primary)"
strokeWidth={2}
type="linear"
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}
function NavSidebar({
activeNav,
isCollapsed,
onNavigate,
onToggleCollapsed,
}: {
activeNav: string;
isCollapsed: boolean;
onNavigate: (label: string) => void;
onToggleCollapsed: () => void;
}) {
const navItem = (
label: string,
Icon: (typeof NAV_ITEMS)[number]['icon'],
isActive = false
) => {
const item = (triggerProps = {}) => (
<MenuItem
{...triggerProps}
aria-current={isActive ? 'page' : undefined}
aria-label={isCollapsed ? label : undefined}
as="a"
className={
isCollapsed
? 'text-body-12 origin-left justify-center overflow-hidden whitespace-nowrap rounded-8px px-2'
: 'text-body-12 origin-left overflow-hidden whitespace-nowrap rounded-8px'
}
classNames={isCollapsed ? { center: 'hidden' } : undefined}
href={`#${label.toLowerCase()}`}
isHighlighted={isActive}
key={label}
onClick={() => onNavigate(label)}
railStart={<Icon size="16" />}
size="large"
>
{label}
</MenuItem>
);
// Collapsed, only the icon shows: the tooltip names it.
return isCollapsed ? (
<Tooltip content={label} key={label} placement="right">
{({ triggerProps }) => item(triggerProps)}
</Tooltip>
) : (
item()
);
};
// Collapsed, the rail's inner width is 32px: padding sizes each row to it.
return (
<aside
className={`fixed left-0 top-0 z-[2] flex h-screen origin-left flex-col items-start gap-6 border-r border-solid border-tertiary bg-neutral pb-4 pe-3 ps-3 pt-4 transition-[width] duration-200 ease-in-out ${
isCollapsed ? 'w-[57px]' : 'w-[216px]'
}`}
>
{/* Collapsed, the header is only the menu button that expands it. */}
<div
className={`flex w-full items-center gap-2 overflow-hidden whitespace-nowrap ${
isCollapsed ? 'justify-center' : 'px-2'
}`}
>
{isCollapsed ? (
<IconButton
aria-label="Expand sidebar"
icon={MenuIcon}
onClick={onToggleCollapsed}
variant="neutralTertiary"
/>
) : (
<>
<SetmoreIcon className="shrink-0" size="24" />
<Text className="text-body-12 line-clamp-2 w-[110px] flex-shrink-0 !leading-4 text-primary font-stronger where:block">
AnywhereWorks
</Text>
<IconButton
aria-label="Collapse sidebar"
icon={ChevronLeftIcon}
onClick={onToggleCollapsed}
variant="neutralTertiary"
/>
</>
)}
</div>
<nav aria-label="Primary" className="flex w-full flex-col gap-1">
{navItem('Grow your brand', GrowYourBrandIcon)}
<div className="my-2" />
{NAV_ITEMS.map((item) =>
navItem(item.label, item.icon, item.label === activeNav)
)}
</nav>
<div className="mt-auto flex w-full flex-col gap-1">
<MenuItem
aria-label={isCollapsed ? 'Alex Morgan' : undefined}
className={
isCollapsed
? 'text-body-12 justify-start overflow-hidden whitespace-nowrap rounded-8px px-1'
: ' text-body-12 justify-start overflow-hidden whitespace-nowrap rounded-8px'
}
classNames={{ center: isCollapsed ? 'w-0' : 'w-full ' }}
railStart={<Avatar name="Alex Morgan" size="24" status="available" />}
size="large"
>
{isCollapsed ? null : 'Alex Morgan'}
</MenuItem>
</div>
</aside>
);
}