Installation
npx shadcn@latest add @boardcn/home-dashboardnpx shadcn@latest add @boardcn/home-dashboardnpm packages
- @remixicon/react
- react-aria-components
- recharts
BoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
What it composes
21 BoardCN components, installed automatically.
Source
The 6 files the CLI copies into your project.
"use client";
import { useEffect, useRef, useState } from "react";
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
import { RiAddLine, RiCloseLine, RiEqualizer2Line, RiMenuLine, RiNotification3Line } from "@remixicon/react";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { StatCards } from "@/components/blocks/dashboard/stat-cards";
import { Avatar } from "@/components/base/avatar/avatar";
import { Badge } from "@/components/base/badges/badge";
import { Breadcrumb, BreadcrumbItem } from "@/components/base/breadcrumb/breadcrumb";
import { Button } from "@/components/base/buttons/button";
import { IconButton } from "@/components/base/buttons/icon-button";
import { Dropdown, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { InputBase } from "@/components/base/input/input";
import { Select, SelectItem } from "@/components/base/select/select";
import type { Stat } from "@/components/blocks/dashboard/stat-cards";
import { ContributionsCard, type ContributionsStat } from "@/components/charts/contributions-card";
import { EarningsChartCard, type EarningsChartPeriods } from "@/components/charts/earnings-chart-card";
import { cx } from "@/utils/cx";
import { HomeCustomersTable } from "./customers-table";
import { RecentHiresCard } from "./recent-hires-card";
import { RevenueChartCard, type RevenuePeriods } from "./revenue-chart-card";
import type {
HomeDashboardCustomer,
HomeDashboardHire,
HomeDashboardNotification,
} from "./types";
function MobileNavigation({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
return (
<ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-50 flex bg-black/40 backdrop-blur-[2px] transition-opacity duration-300 data-[entering]:opacity-0 data-[exiting]:opacity-0 lg:hidden">
<Modal className="h-full w-[min(320px,calc(100vw-48px))] bg-background-full p-3 shadow-dropdown outline-none transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[entering]:-translate-x-full data-[exiting]:-translate-x-full">
<Dialog aria-label="Navigation" className="h-full outline-none">
<DashboardSidebar mobile fluid flat onClose={() => onOpenChange(false)} className="h-full" />
</Dialog>
</Modal>
</ModalOverlay>
);
}
function TicketDialog({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
return (
<ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4 backdrop-blur-[2px] transition-opacity duration-200 data-[entering]:opacity-0 data-[exiting]:opacity-0">
<Modal className="w-full max-w-[430px] rounded-3xl bg-background-full p-5 shadow-dropdown outline-none transition-[opacity,transform] duration-200 data-[entering]:scale-95 data-[entering]:opacity-0 data-[exiting]:scale-95 data-[exiting]:opacity-0">
<Dialog aria-label="Create ticket" className="flex flex-col gap-5 outline-none">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-title-2-medium text-text-primary">Create ticket</h2>
<p className="mt-1 text-body-regular text-text-secondary">Start a new request for the BoardCN team.</p>
</div>
<IconButton icon={RiCloseLine} size="small" aria-label="Close create ticket" onClick={() => onOpenChange(false)} />
</div>
<label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
Ticket title
<InputBase autoFocus aria-label="Ticket title" placeholder="What needs attention?" />
</label>
<label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
Priority
<Select aria-label="Ticket priority" defaultSelectedKey="normal">
<SelectItem id="low" textValue="Low">Low</SelectItem>
<SelectItem id="normal" textValue="Normal">Normal</SelectItem>
<SelectItem id="urgent" textValue="Urgent">Urgent</SelectItem>
</Select>
</label>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button onClick={() => onOpenChange(false)}>Create ticket</Button>
</div>
</Dialog>
</Modal>
</ModalOverlay>
);
}
function NotificationMenu({ notifications }: { notifications: readonly HomeDashboardNotification[] }) {
const [open, setOpen] = useState(false);
const unread = notifications.length;
return (
<Dropdown isOpen={open} onOpenChange={setOpen}>
<DropdownTrigger aria-label="Notifications" className="relative inline-flex size-9 shrink-0 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover">
<RiNotification3Line className="size-5" aria-hidden />
<Badge color="primary" className="absolute -top-2 -right-2 min-w-5 justify-center px-1">{unread}</Badge>
</DropdownTrigger>
<DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(360px,calc(100vw-24px))]">
<div className="flex items-center justify-between px-2 py-1.5">
<span className="text-body-semibold text-text-primary">Notifications</span>
<span className="text-caption-1-medium text-text-tertiary">{unread} unread</span>
</div>
{notifications.map((item) => (
<DropdownItem key={item.title} onSelect={() => setOpen(false)} className="items-start py-2.5">
<span className="mt-1 size-2 shrink-0 rounded-full bg-accent-500" aria-hidden />
<span className="flex min-w-0 flex-1 flex-col items-start">
<span className="text-body-medium text-text-primary">{item.title}</span>
<span className="line-clamp-2 text-left text-body-2-medium text-text-secondary">{item.description}</span>
</span>
<span className="text-caption-1-medium whitespace-nowrap text-text-tertiary">{item.time}</span>
</DropdownItem>
))}
</DropdownPopover>
</Dropdown>
);
}
export type HomeDashboardDateRange = "7-days" | "30-days" | "year";
export type HomeDashboardTeam = "all" | "board" | "engineering";
export interface HomeDashboardFiltersValue {
dateRange: HomeDashboardDateRange;
team: HomeDashboardTeam;
}
export const DEFAULT_HOME_DASHBOARD_FILTERS: HomeDashboardFiltersValue = {
dateRange: "30-days",
team: "all",
};
interface DashboardFiltersProps {
filters?: HomeDashboardFiltersValue;
defaultFilters?: HomeDashboardFiltersValue;
onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
}
export function DashboardFilters({
filters,
defaultFilters = DEFAULT_HOME_DASHBOARD_FILTERS,
onFiltersChange,
}: DashboardFiltersProps) {
const [open, setOpen] = useState(false);
const [uncontrolledFilters, setUncontrolledFilters] = useState(defaultFilters);
const appliedFilters = filters ?? uncontrolledFilters;
const [draftFilters, setDraftFilters] = useState(appliedFilters);
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const close = (restoreFocus = false) => {
setOpen(false);
if (restoreFocus) queueMicrotask(() => triggerRef.current?.focus());
};
useEffect(() => {
if (!open) return;
const closeOnOutsidePress = (event: MouseEvent) => {
if (event.target instanceof Node && !containerRef.current?.contains(event.target)) setOpen(false);
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape" && !event.defaultPrevented) {
event.preventDefault();
close(true);
}
};
document.addEventListener("mousedown", closeOnOutsidePress);
document.addEventListener("keydown", closeOnEscape);
return () => {
document.removeEventListener("mousedown", closeOnOutsidePress);
document.removeEventListener("keydown", closeOnEscape);
};
}, [open]);
return (
<div ref={containerRef} className="relative">
<Button
ref={triggerRef}
variant="secondary"
leadingIcon={RiEqualizer2Line}
aria-expanded={open}
aria-haspopup="dialog"
aria-controls="home-dashboard-filters"
onClick={() => {
if (!open) setDraftFilters(appliedFilters);
setOpen((value) => !value);
}}
>
Filters
</Button>
{open && (
<div id="home-dashboard-filters" role="dialog" aria-label="Dashboard filters" className="absolute top-[calc(100%+6px)] right-0 z-30 flex w-[280px] flex-col gap-3 rounded-2xl border border-border-button-default bg-background-primary-default p-3 shadow-dropdown">
<p className="text-body-semibold text-text-primary">Dashboard filters</p>
<label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Date range
<Select
aria-label="Dashboard date range"
selectedKey={draftFilters.dateRange}
onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, dateRange: String(key) as HomeDashboardDateRange }))}
>
<SelectItem id="7-days" textValue="Last 7 days">Last 7 days</SelectItem>
<SelectItem id="30-days" textValue="Last 30 days">Last 30 days</SelectItem>
<SelectItem id="year" textValue="This year">This year</SelectItem>
</Select>
</label>
<label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Team
<Select
aria-label="Dashboard team"
selectedKey={draftFilters.team}
onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, team: String(key) as HomeDashboardTeam }))}
>
<SelectItem id="all" textValue="All teams">All teams</SelectItem>
<SelectItem id="board" textValue="BoardCN team">BoardCN team</SelectItem>
<SelectItem id="engineering" textValue="Engineering">Engineering</SelectItem>
</Select>
</label>
<div className="flex gap-2 pt-1">
<Button size="small" variant="secondary" className="flex-1" onClick={() => setDraftFilters(DEFAULT_HOME_DASHBOARD_FILTERS)}>Reset</Button>
<Button
size="small"
className="flex-1"
onClick={() => {
setUncontrolledFilters(draftFilters);
onFiltersChange?.(draftFilters);
close(true);
}}
>
Apply
</Button>
</div>
</div>
)}
</div>
);
}
export interface HomeDashboardProps {
className?: string;
/** Applied filter value. When supplied, the dashboard filter panel is controlled. */
filters?: HomeDashboardFiltersValue;
/** Initial applied value for an uncontrolled dashboard. */
defaultFilters?: HomeDashboardFiltersValue;
/** Fires only when the user commits the draft date/team values with Apply. */
onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
stats: Stat[];
hirePages: readonly (readonly HomeDashboardHire[])[];
customers: readonly HomeDashboardCustomer[];
notifications: readonly HomeDashboardNotification[];
earningsPeriods: EarningsChartPeriods;
contributionsStats: readonly ContributionsStat[];
contributionsHeadline: number;
contributionsDelta: string;
revenuePeriods: RevenuePeriods;
}
export function HomeDashboard({
className,
filters,
defaultFilters,
onFiltersChange,
stats,
hirePages,
customers,
notifications,
earningsPeriods,
contributionsStats,
contributionsHeadline,
contributionsDelta,
revenuePeriods,
}: HomeDashboardProps) {
const [navigationOpen, setNavigationOpen] = useState(false);
const [ticketOpen, setTicketOpen] = useState(false);
return (
<div className={cx("relative flex min-h-screen w-full items-start bg-background-full", className)}>
<div className="sticky top-3 ml-3 hidden h-[calc(100dvh-24px)] shrink-0 lg:block">
<DashboardSidebar selected="home" className="h-full" />
</div>
<main className="relative z-20 flex min-w-0 flex-1 justify-center overflow-x-hidden bg-background-full p-3 pt-4 will-change-transform sm:pt-6 lg:z-0 lg:overflow-visible">
<div className="flex w-full max-w-[1300px] flex-col gap-2.5">
<header className="flex w-full flex-col gap-2">
<div className="flex items-center gap-2">
<IconButton icon={RiMenuLine} aria-label="Open navigation" onClick={() => setNavigationOpen(true)} className="lg:hidden" />
<Breadcrumb>
<BreadcrumbItem href="#"><Avatar size="xs" initials="B" />BoardCN team</BreadcrumbItem>
<BreadcrumbItem href="#"><Avatar size="xs" initials="M" />Mertcan</BreadcrumbItem>
<BreadcrumbItem current>Home</BreadcrumbItem>
</Breadcrumb>
</div>
<div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex min-w-0 items-center gap-1.5">
<h1 className="px-1 text-title-2-medium whitespace-nowrap text-text-primary">Welcome Mertcan</h1>
</div>
<div className="flex items-center gap-2">
<NotificationMenu notifications={notifications} />
<DashboardFilters filters={filters} defaultFilters={defaultFilters} onFiltersChange={onFiltersChange} />
<Button leadingIcon={RiAddLine} onClick={() => setTicketOpen(true)}>Create ticket</Button>
</div>
</div>
</header>
<div className="flex w-full flex-col gap-4">
<div className="flex w-full flex-col items-stretch gap-4 xl:flex-row xl:items-start">
<RecentHiresCard pages={hirePages} />
<EarningsChartCard periods={earningsPeriods} />
</div>
<div className="flex w-full flex-col items-stretch gap-4 lg:flex-row lg:items-start">
<RevenueChartCard periods={revenuePeriods} />
<ContributionsCard
accent="violet"
stats={contributionsStats}
headline={contributionsHeadline}
delta={contributionsDelta}
/>
</div>
<StatCards stats={stats} columns={4} />
<HomeCustomersTable customers={customers} />
</div>
</div>
</main>
<MobileNavigation isOpen={navigationOpen} onOpenChange={setNavigationOpen} />
<TicketDialog isOpen={ticketOpen} onOpenChange={setTicketOpen} />
</div>
);
}
export default HomeDashboard;"use client";
import { useEffect, useRef, useState } from "react";
import { Dialog, Modal, ModalOverlay } from "react-aria-components";
import { RiAddLine, RiCloseLine, RiEqualizer2Line, RiMenuLine, RiNotification3Line } from "@remixicon/react";
import { DashboardSidebar } from "@/components/blocks/dashboard/dashboard-sidebar";
import { StatCards } from "@/components/blocks/dashboard/stat-cards";
import { Avatar } from "@/components/base/avatar/avatar";
import { Badge } from "@/components/base/badges/badge";
import { Breadcrumb, BreadcrumbItem } from "@/components/base/breadcrumb/breadcrumb";
import { Button } from "@/components/base/buttons/button";
import { IconButton } from "@/components/base/buttons/icon-button";
import { Dropdown, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { InputBase } from "@/components/base/input/input";
import { Select, SelectItem } from "@/components/base/select/select";
import type { Stat } from "@/components/blocks/dashboard/stat-cards";
import { ContributionsCard, type ContributionsStat } from "@/components/charts/contributions-card";
import { EarningsChartCard, type EarningsChartPeriods } from "@/components/charts/earnings-chart-card";
import { cx } from "@/utils/cx";
import { HomeCustomersTable } from "./customers-table";
import { RecentHiresCard } from "./recent-hires-card";
import { RevenueChartCard, type RevenuePeriods } from "./revenue-chart-card";
import type {
HomeDashboardCustomer,
HomeDashboardHire,
HomeDashboardNotification,
} from "./types";
function MobileNavigation({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
return (
<ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-50 flex bg-black/40 backdrop-blur-[2px] transition-opacity duration-300 data-[entering]:opacity-0 data-[exiting]:opacity-0 lg:hidden">
<Modal className="h-full w-[min(320px,calc(100vw-48px))] bg-background-full p-3 shadow-dropdown outline-none transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[entering]:-translate-x-full data-[exiting]:-translate-x-full">
<Dialog aria-label="Navigation" className="h-full outline-none">
<DashboardSidebar mobile fluid flat onClose={() => onOpenChange(false)} className="h-full" />
</Dialog>
</Modal>
</ModalOverlay>
);
}
function TicketDialog({ isOpen, onOpenChange }: { isOpen: boolean; onOpenChange: (open: boolean) => void }) {
return (
<ModalOverlay isOpen={isOpen} onOpenChange={onOpenChange} isDismissable className="fixed inset-0 z-60 flex items-center justify-center bg-black/50 p-4 backdrop-blur-[2px] transition-opacity duration-200 data-[entering]:opacity-0 data-[exiting]:opacity-0">
<Modal className="w-full max-w-[430px] rounded-3xl bg-background-full p-5 shadow-dropdown outline-none transition-[opacity,transform] duration-200 data-[entering]:scale-95 data-[entering]:opacity-0 data-[exiting]:scale-95 data-[exiting]:opacity-0">
<Dialog aria-label="Create ticket" className="flex flex-col gap-5 outline-none">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-title-2-medium text-text-primary">Create ticket</h2>
<p className="mt-1 text-body-regular text-text-secondary">Start a new request for the BoardCN team.</p>
</div>
<IconButton icon={RiCloseLine} size="small" aria-label="Close create ticket" onClick={() => onOpenChange(false)} />
</div>
<label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
Ticket title
<InputBase autoFocus aria-label="Ticket title" placeholder="What needs attention?" />
</label>
<label className="flex flex-col gap-1.5 text-body-medium text-text-primary">
Priority
<Select aria-label="Ticket priority" defaultSelectedKey="normal">
<SelectItem id="low" textValue="Low">Low</SelectItem>
<SelectItem id="normal" textValue="Normal">Normal</SelectItem>
<SelectItem id="urgent" textValue="Urgent">Urgent</SelectItem>
</Select>
</label>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button onClick={() => onOpenChange(false)}>Create ticket</Button>
</div>
</Dialog>
</Modal>
</ModalOverlay>
);
}
function NotificationMenu({ notifications }: { notifications: readonly HomeDashboardNotification[] }) {
const [open, setOpen] = useState(false);
const unread = notifications.length;
return (
<Dropdown isOpen={open} onOpenChange={setOpen}>
<DropdownTrigger aria-label="Notifications" className="relative inline-flex size-9 shrink-0 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover">
<RiNotification3Line className="size-5" aria-hidden />
<Badge color="primary" className="absolute -top-2 -right-2 min-w-5 justify-center px-1">{unread}</Badge>
</DropdownTrigger>
<DropdownPopover aria-label="Notifications" placement="bottom end" className="w-[min(360px,calc(100vw-24px))]">
<div className="flex items-center justify-between px-2 py-1.5">
<span className="text-body-semibold text-text-primary">Notifications</span>
<span className="text-caption-1-medium text-text-tertiary">{unread} unread</span>
</div>
{notifications.map((item) => (
<DropdownItem key={item.title} onSelect={() => setOpen(false)} className="items-start py-2.5">
<span className="mt-1 size-2 shrink-0 rounded-full bg-accent-500" aria-hidden />
<span className="flex min-w-0 flex-1 flex-col items-start">
<span className="text-body-medium text-text-primary">{item.title}</span>
<span className="line-clamp-2 text-left text-body-2-medium text-text-secondary">{item.description}</span>
</span>
<span className="text-caption-1-medium whitespace-nowrap text-text-tertiary">{item.time}</span>
</DropdownItem>
))}
</DropdownPopover>
</Dropdown>
);
}
export type HomeDashboardDateRange = "7-days" | "30-days" | "year";
export type HomeDashboardTeam = "all" | "board" | "engineering";
export interface HomeDashboardFiltersValue {
dateRange: HomeDashboardDateRange;
team: HomeDashboardTeam;
}
export const DEFAULT_HOME_DASHBOARD_FILTERS: HomeDashboardFiltersValue = {
dateRange: "30-days",
team: "all",
};
interface DashboardFiltersProps {
filters?: HomeDashboardFiltersValue;
defaultFilters?: HomeDashboardFiltersValue;
onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
}
export function DashboardFilters({
filters,
defaultFilters = DEFAULT_HOME_DASHBOARD_FILTERS,
onFiltersChange,
}: DashboardFiltersProps) {
const [open, setOpen] = useState(false);
const [uncontrolledFilters, setUncontrolledFilters] = useState(defaultFilters);
const appliedFilters = filters ?? uncontrolledFilters;
const [draftFilters, setDraftFilters] = useState(appliedFilters);
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const close = (restoreFocus = false) => {
setOpen(false);
if (restoreFocus) queueMicrotask(() => triggerRef.current?.focus());
};
useEffect(() => {
if (!open) return;
const closeOnOutsidePress = (event: MouseEvent) => {
if (event.target instanceof Node && !containerRef.current?.contains(event.target)) setOpen(false);
};
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape" && !event.defaultPrevented) {
event.preventDefault();
close(true);
}
};
document.addEventListener("mousedown", closeOnOutsidePress);
document.addEventListener("keydown", closeOnEscape);
return () => {
document.removeEventListener("mousedown", closeOnOutsidePress);
document.removeEventListener("keydown", closeOnEscape);
};
}, [open]);
return (
<div ref={containerRef} className="relative">
<Button
ref={triggerRef}
variant="secondary"
leadingIcon={RiEqualizer2Line}
aria-expanded={open}
aria-haspopup="dialog"
aria-controls="home-dashboard-filters"
onClick={() => {
if (!open) setDraftFilters(appliedFilters);
setOpen((value) => !value);
}}
>
Filters
</Button>
{open && (
<div id="home-dashboard-filters" role="dialog" aria-label="Dashboard filters" className="absolute top-[calc(100%+6px)] right-0 z-30 flex w-[280px] flex-col gap-3 rounded-2xl border border-border-button-default bg-background-primary-default p-3 shadow-dropdown">
<p className="text-body-semibold text-text-primary">Dashboard filters</p>
<label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Date range
<Select
aria-label="Dashboard date range"
selectedKey={draftFilters.dateRange}
onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, dateRange: String(key) as HomeDashboardDateRange }))}
>
<SelectItem id="7-days" textValue="Last 7 days">Last 7 days</SelectItem>
<SelectItem id="30-days" textValue="Last 30 days">Last 30 days</SelectItem>
<SelectItem id="year" textValue="This year">This year</SelectItem>
</Select>
</label>
<label className="flex flex-col gap-1 text-body-2-medium text-text-secondary">Team
<Select
aria-label="Dashboard team"
selectedKey={draftFilters.team}
onSelectionChange={(key) => setDraftFilters((current) => ({ ...current, team: String(key) as HomeDashboardTeam }))}
>
<SelectItem id="all" textValue="All teams">All teams</SelectItem>
<SelectItem id="board" textValue="BoardCN team">BoardCN team</SelectItem>
<SelectItem id="engineering" textValue="Engineering">Engineering</SelectItem>
</Select>
</label>
<div className="flex gap-2 pt-1">
<Button size="small" variant="secondary" className="flex-1" onClick={() => setDraftFilters(DEFAULT_HOME_DASHBOARD_FILTERS)}>Reset</Button>
<Button
size="small"
className="flex-1"
onClick={() => {
setUncontrolledFilters(draftFilters);
onFiltersChange?.(draftFilters);
close(true);
}}
>
Apply
</Button>
</div>
</div>
)}
</div>
);
}
export interface HomeDashboardProps {
className?: string;
/** Applied filter value. When supplied, the dashboard filter panel is controlled. */
filters?: HomeDashboardFiltersValue;
/** Initial applied value for an uncontrolled dashboard. */
defaultFilters?: HomeDashboardFiltersValue;
/** Fires only when the user commits the draft date/team values with Apply. */
onFiltersChange?: (filters: HomeDashboardFiltersValue) => void;
stats: Stat[];
hirePages: readonly (readonly HomeDashboardHire[])[];
customers: readonly HomeDashboardCustomer[];
notifications: readonly HomeDashboardNotification[];
earningsPeriods: EarningsChartPeriods;
contributionsStats: readonly ContributionsStat[];
contributionsHeadline: number;
contributionsDelta: string;
revenuePeriods: RevenuePeriods;
}
export function HomeDashboard({
className,
filters,
defaultFilters,
onFiltersChange,
stats,
hirePages,
customers,
notifications,
earningsPeriods,
contributionsStats,
contributionsHeadline,
contributionsDelta,
revenuePeriods,
}: HomeDashboardProps) {
const [navigationOpen, setNavigationOpen] = useState(false);
const [ticketOpen, setTicketOpen] = useState(false);
return (
<div className={cx("relative flex min-h-screen w-full items-start bg-background-full", className)}>
<div className="sticky top-3 ml-3 hidden h-[calc(100dvh-24px)] shrink-0 lg:block">
<DashboardSidebar selected="home" className="h-full" />
</div>
<main className="relative z-20 flex min-w-0 flex-1 justify-center overflow-x-hidden bg-background-full p-3 pt-4 will-change-transform sm:pt-6 lg:z-0 lg:overflow-visible">
<div className="flex w-full max-w-[1300px] flex-col gap-2.5">
<header className="flex w-full flex-col gap-2">
<div className="flex items-center gap-2">
<IconButton icon={RiMenuLine} aria-label="Open navigation" onClick={() => setNavigationOpen(true)} className="lg:hidden" />
<Breadcrumb>
<BreadcrumbItem href="#"><Avatar size="xs" initials="B" />BoardCN team</BreadcrumbItem>
<BreadcrumbItem href="#"><Avatar size="xs" initials="M" />Mertcan</BreadcrumbItem>
<BreadcrumbItem current>Home</BreadcrumbItem>
</Breadcrumb>
</div>
<div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex min-w-0 items-center gap-1.5">
<h1 className="px-1 text-title-2-medium whitespace-nowrap text-text-primary">Welcome Mertcan</h1>
</div>
<div className="flex items-center gap-2">
<NotificationMenu notifications={notifications} />
<DashboardFilters filters={filters} defaultFilters={defaultFilters} onFiltersChange={onFiltersChange} />
<Button leadingIcon={RiAddLine} onClick={() => setTicketOpen(true)}>Create ticket</Button>
</div>
</div>
</header>
<div className="flex w-full flex-col gap-4">
<div className="flex w-full flex-col items-stretch gap-4 xl:flex-row xl:items-start">
<RecentHiresCard pages={hirePages} />
<EarningsChartCard periods={earningsPeriods} />
</div>
<div className="flex w-full flex-col items-stretch gap-4 lg:flex-row lg:items-start">
<RevenueChartCard periods={revenuePeriods} />
<ContributionsCard
accent="violet"
stats={contributionsStats}
headline={contributionsHeadline}
delta={contributionsDelta}
/>
</div>
<StatCards stats={stats} columns={4} />
<HomeCustomersTable customers={customers} />
</div>
</div>
</main>
<MobileNavigation isOpen={navigationOpen} onOpenChange={setNavigationOpen} />
<TicketDialog isOpen={ticketOpen} onOpenChange={setTicketOpen} />
</div>
);
}
export default HomeDashboard;"use client";
import { useEffect, useMemo, useState } from "react";
import { RiArchiveLine, RiDeleteBin6Line, RiDownload2Line, RiEditLine, RiFileCopyLine, RiMore2Fill, RiSearchLine, RiUserLine } from "@remixicon/react";
import { Avatar } from "@/components/base/avatar/avatar";
import { Chip } from "@/components/base/badges/chip";
import { StatusDot } from "@/components/base/badges/status-dot";
import { IconButton } from "@/components/base/buttons/icon-button";
import { Checkbox } from "@/components/base/checkbox/checkbox";
import { Dropdown, DropdownGroup, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { InputBase } from "@/components/base/input/input";
import { Pagination } from "@/components/base/pagination/pagination";
import { Select, SelectItem } from "@/components/base/select/select";
import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@/components/base/table/table";
import { ChevronSortDown } from "@/components/foundations/icons/chevrons";
import { cx } from "@/utils/cx";
import type { DeliveryState, HomeDashboardCustomer, PurchaseState } from "./types";
export type { DeliveryState, HomeDashboardCustomer, PurchaseState };
const PAGE_SIZE = 12;
const PRODUCTS = ["Sneakers", "Backpack", "Smart watch", "Headphones", "Sunglasses", "Wallet"] as const;
const REGIONS = ["North America", "Europe", "Asia", "Oceania"] as const;
const PRICE_FILTERS = [
{ id: "all", label: "All prices", test: () => true },
{ id: "under-100", label: "Under $100", test: (price: number) => price < 100 },
{ id: "100-500", label: "$100 – $500", test: (price: number) => price >= 100 && price <= 500 },
{ id: "500-1000", label: "$500 – $1,000", test: (price: number) => price > 500 && price <= 1000 },
{ id: "over-1000", label: "Over $1,000", test: (price: number) => price > 1000 },
] as const;
type SortKey = "name" | "updatedTimestamp" | "price";
type SortDirection = "asc" | "desc";
const deliveryColor: Record<DeliveryState, "lime" | "yellow" | "rose" | "cyan"> = {
"Delivery failed": "rose",
Shipped: "lime",
"Delivery waiting": "yellow",
Confirmed: "cyan",
};
function initials(name: string) {
return name.split(" ").map((part) => part[0]).join("").slice(0, 2).toUpperCase();
}
function formatPrice(value: number) {
return value >= 1000 ? `$${Math.floor(value / 1000)}.${String(value % 1000).padStart(3, "0")}` : `$${value}`;
}
function SortButton({ label, active, direction, onClick }: { label: string; active: boolean; direction: SortDirection; onClick: () => void }) {
return (
<button type="button" aria-label={`Sort by ${label}`} onClick={onClick} className="flex cursor-pointer items-center gap-0.5 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-border-focus-ring">
{label}
<ChevronSortDown className={cx("size-6 shrink-0 transition-[transform,color] duration-150", active && direction === "asc" && "rotate-180", active ? "text-text-secondary" : "text-text-tertiary")} />
</button>
);
}
function PurchaseSelect({ customer, value, onChange }: { customer: string; value: PurchaseState; onChange: (value: PurchaseState) => void }) {
return (
<Select aria-label={`Purchase status for ${customer}`} selectedKey={value} onSelectionChange={(key) => onChange(String(key) as PurchaseState)} className="w-[142px]">
<SelectItem id="completed" textValue="Completed"><StatusDot color="green" />Completed</SelectItem>
<SelectItem id="waiting" textValue="Waiting"><StatusDot color="yellow" />Waiting</SelectItem>
<SelectItem id="processing" textValue="Processing"><StatusDot color="indigo" />Processing</SelectItem>
</Select>
);
}
function MoreMenu({ customer, onAction }: { customer: string; onAction: (message: string) => void }) {
const [open, setOpen] = useState(false);
const actions = [
{ icon: RiUserLine, label: "View profile" },
{ icon: RiFileCopyLine, label: "Duplicate row" },
{ icon: RiDownload2Line, label: "Download invoice" },
{ icon: RiArchiveLine, label: "Archive customer" },
] as const;
return (
<Dropdown isOpen={open} onOpenChange={setOpen}>
<DropdownTrigger aria-label={`More actions for ${customer}`} className="relative inline-flex size-8 shrink-0 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover">
<RiMore2Fill className="size-4" aria-hidden />
</DropdownTrigger>
<DropdownPopover aria-label={`More actions for ${customer}`} placement="bottom end" className="w-[220px] p-2">
<DropdownGroup>
{actions.map(({ icon: Icon, label }) => (
<DropdownItem key={label} onSelect={() => { onAction(`${label}: ${customer}`); setOpen(false); }} className="px-2 py-1.5">
<Icon className="size-[18px] shrink-0 text-foreground-icon-secondary" aria-hidden />
<span className="truncate text-body-medium whitespace-nowrap text-text-primary">{label}</span>
</DropdownItem>
))}
</DropdownGroup>
</DropdownPopover>
</Dropdown>
);
}
export interface HomeCustomersTableProps {
customers: readonly HomeDashboardCustomer[];
className?: string;
}
export function HomeCustomersTable({ customers, className }: HomeCustomersTableProps) {
const [priceFilter, setPriceFilter] = useState("all");
const [productFilter, setProductFilter] = useState("all");
const [regionFilter, setRegionFilter] = useState("all");
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [selected, setSelected] = useState<Set<string>>(() => new Set(["2", "3"]));
const [purchaseChanges, setPurchaseChanges] = useState<Record<string, PurchaseState>>({});
const [sort, setSort] = useState<{ key: SortKey; direction: SortDirection } | null>(null);
const [message, setMessage] = useState("");
const rows = useMemo(() => {
const price = PRICE_FILTERS.find((item) => item.id === priceFilter) ?? PRICE_FILTERS[0];
const normalizedQuery = query.trim().toLowerCase();
const result = customers.filter((customer) =>
price.test(customer.price) &&
(productFilter === "all" || customer.product === productFilter) &&
(regionFilter === "all" || customer.region === regionFilter) &&
(!normalizedQuery || customer.name.toLowerCase().includes(normalizedQuery)),
);
if (!sort) return result;
return [...result].sort((a, b) => {
const left = a[sort.key];
const right = b[sort.key];
const comparison = typeof left === "string" ? left.localeCompare(String(right)) : left - Number(right);
return sort.direction === "asc" ? comparison : -comparison;
});
}, [customers, priceFilter, productFilter, query, regionFilter, sort]);
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
const selectedOnPage = pageRows.filter((row) => selected.has(row.id)).length;
useEffect(() => {
if (page > totalPages) setPage(totalPages);
}, [page, totalPages]);
function changeFilter(action: () => void) {
action();
setPage(1);
}
function changeSort(key: SortKey) {
setSort((current) => current?.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" });
setPage(1);
}
function toggleRow(id: string, checked: boolean) {
setSelected((current) => {
const next = new Set(current);
if (checked) next.add(id); else next.delete(id);
return next;
});
}
function togglePage(checked: boolean) {
setSelected((current) => {
const next = new Set(current);
pageRows.forEach((row) => checked ? next.add(row.id) : next.delete(row.id));
return next;
});
}
return (
<section className={cx("flex w-full flex-col rounded-2xl border border-border-table pt-2 pb-3", className)}>
<div className="flex w-full flex-col items-start gap-3 px-3 py-1 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col justify-center">
<p className="text-body-medium whitespace-nowrap text-text-tertiary">Total Results</p>
<p className="text-body-medium whitespace-nowrap text-text-primary">{rows.length.toLocaleString()} customers</p>
</div>
<div className="-mx-3 flex w-[calc(100%+1.5rem)] items-center gap-2.5 overflow-x-auto px-3 sm:mx-0 sm:w-auto sm:flex-wrap sm:justify-end sm:overflow-visible sm:px-0">
<Select aria-label="Filter by price" selectedKey={priceFilter} onSelectionChange={(key) => changeFilter(() => setPriceFilter(String(key)))} className="shrink-0" popoverClassName="min-w-40">
{PRICE_FILTERS.map((item) => <SelectItem key={item.id} id={item.id} textValue={item.label}>{item.label}</SelectItem>)}
</Select>
<Select aria-label="Filter by product" selectedKey={productFilter} onSelectionChange={(key) => changeFilter(() => setProductFilter(String(key)))} className="shrink-0" popoverClassName="min-w-40">
<SelectItem id="all" textValue="All products">All products</SelectItem>
{PRODUCTS.map((item) => <SelectItem key={item} id={item} textValue={item}>{item}</SelectItem>)}
</Select>
<Select aria-label="Filter by region" selectedKey={regionFilter} onSelectionChange={(key) => changeFilter(() => setRegionFilter(String(key)))} className="shrink-0" popoverClassName="min-w-40">
<SelectItem id="all" textValue="All regions">All regions</SelectItem>
{REGIONS.map((item) => <SelectItem key={item} id={item} textValue={item}>{item}</SelectItem>)}
</Select>
<InputBase aria-label="Search customers" placeholder="Search" leadingIcon={RiSearchLine} value={query} onChange={(event) => changeFilter(() => setQuery(event.target.value))} fieldClassName="min-w-[153px] flex-1 rounded-full bg-background-secondary-default sm:w-[153px] sm:min-w-0 sm:flex-none" className="text-body-medium" />
</div>
</div>
<div className="mt-2 overflow-x-auto overscroll-x-contain">
<Table aria-label="Customers" selectionMode="none" className="min-w-[1000px]">
<TableHeader>
<TableColumn id="name" isRowHeader className="w-[240px]">
<div className="flex items-center gap-2">
<Checkbox slot={null} aria-label="Select all customers on this page" isSelected={pageRows.length > 0 && selectedOnPage === pageRows.length} isIndeterminate={selectedOnPage > 0 && selectedOnPage < pageRows.length} onChange={(checked) => togglePage(Boolean(checked))} />
<SortButton label="Customer name" active={sort?.key === "name"} direction={sort?.direction ?? "asc"} onClick={() => changeSort("name")} />
</div>
</TableColumn>
<TableColumn id="purchase" className="w-[172px]">Purchase</TableColumn>
<TableColumn id="status" className="w-[168px]">Status</TableColumn>
<TableColumn id="updated" className="w-[160px]"><SortButton label="Last updated" active={sort?.key === "updatedTimestamp"} direction={sort?.direction ?? "asc"} onClick={() => changeSort("updatedTimestamp")} /></TableColumn>
<TableColumn id="price" className="w-[140px]"><SortButton label="Price" active={sort?.key === "price"} direction={sort?.direction ?? "asc"} onClick={() => changeSort("price")} /></TableColumn>
<TableColumn id="actions" className="w-[132px]">Actions</TableColumn>
</TableHeader>
<TableBody renderEmptyState={() => <div className="flex h-40 items-center justify-center text-body-medium text-text-tertiary">No customers match your filters.</div>}>
{pageRows.map((customer: HomeDashboardCustomer) => (
<TableRow key={customer.id} id={customer.id} style={selected.has(customer.id) ? { backgroundColor: "var(--color-background-secondary-default)" } : undefined}>
<TableCell className="w-[240px]">
<div className="flex min-w-0 items-center gap-2">
<Checkbox slot={null} aria-label={`Select ${customer.name}`} isSelected={selected.has(customer.id)} onChange={(checked) => toggleRow(customer.id, Boolean(checked))} />
<Avatar size="sm" initials={initials(customer.name)} />
<span className="truncate text-body-medium text-text-primary">{customer.name}</span>
</div>
</TableCell>
<TableCell className="w-[172px]"><PurchaseSelect customer={customer.name} value={purchaseChanges[customer.id] ?? customer.purchase} onChange={(value) => setPurchaseChanges((current) => ({ ...current, [customer.id]: value }))} /></TableCell>
<TableCell className="w-[168px]"><Chip variant="bold" color={deliveryColor[customer.delivery]}>{customer.delivery}</Chip></TableCell>
<TableCell className="w-[160px]"><span className="whitespace-nowrap text-body-medium text-text-primary">{customer.updated}</span></TableCell>
<TableCell className="w-[140px]"><Chip variant="subtle" color="gray">{formatPrice(customer.price)}</Chip></TableCell>
<TableCell className="w-[132px]">
<div className="flex items-center justify-end gap-2.5">
<IconButton icon={RiDeleteBin6Line} size="small" aria-label="Delete" onClick={() => setMessage(`Delete requested for ${customer.name}`)} />
<IconButton icon={RiEditLine} size="small" aria-label="Edit" onClick={() => setMessage(`Editing ${customer.name}`)} />
<MoreMenu customer={customer.name} onAction={setMessage} />
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{totalPages > 1 && <div className="px-3 pt-3"><Pagination page={page} totalPages={totalPages} onChange={setPage} siblingCount={2} /></div>}
<p className="sr-only" role="status" aria-live="polite">{message}</p>
</section>
);
}"use client";
import { useEffect, useMemo, useState } from "react";
import { RiArchiveLine, RiDeleteBin6Line, RiDownload2Line, RiEditLine, RiFileCopyLine, RiMore2Fill, RiSearchLine, RiUserLine } from "@remixicon/react";
import { Avatar } from "@/components/base/avatar/avatar";
import { Chip } from "@/components/base/badges/chip";
import { StatusDot } from "@/components/base/badges/status-dot";
import { IconButton } from "@/components/base/buttons/icon-button";
import { Checkbox } from "@/components/base/checkbox/checkbox";
import { Dropdown, DropdownGroup, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { InputBase } from "@/components/base/input/input";
import { Pagination } from "@/components/base/pagination/pagination";
import { Select, SelectItem } from "@/components/base/select/select";
import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@/components/base/table/table";
import { ChevronSortDown } from "@/components/foundations/icons/chevrons";
import { cx } from "@/utils/cx";
import type { DeliveryState, HomeDashboardCustomer, PurchaseState } from "./types";
export type { DeliveryState, HomeDashboardCustomer, PurchaseState };
const PAGE_SIZE = 12;
const PRODUCTS = ["Sneakers", "Backpack", "Smart watch", "Headphones", "Sunglasses", "Wallet"] as const;
const REGIONS = ["North America", "Europe", "Asia", "Oceania"] as const;
const PRICE_FILTERS = [
{ id: "all", label: "All prices", test: () => true },
{ id: "under-100", label: "Under $100", test: (price: number) => price < 100 },
{ id: "100-500", label: "$100 – $500", test: (price: number) => price >= 100 && price <= 500 },
{ id: "500-1000", label: "$500 – $1,000", test: (price: number) => price > 500 && price <= 1000 },
{ id: "over-1000", label: "Over $1,000", test: (price: number) => price > 1000 },
] as const;
type SortKey = "name" | "updatedTimestamp" | "price";
type SortDirection = "asc" | "desc";
const deliveryColor: Record<DeliveryState, "lime" | "yellow" | "rose" | "cyan"> = {
"Delivery failed": "rose",
Shipped: "lime",
"Delivery waiting": "yellow",
Confirmed: "cyan",
};
function initials(name: string) {
return name.split(" ").map((part) => part[0]).join("").slice(0, 2).toUpperCase();
}
function formatPrice(value: number) {
return value >= 1000 ? `$${Math.floor(value / 1000)}.${String(value % 1000).padStart(3, "0")}` : `$${value}`;
}
function SortButton({ label, active, direction, onClick }: { label: string; active: boolean; direction: SortDirection; onClick: () => void }) {
return (
<button type="button" aria-label={`Sort by ${label}`} onClick={onClick} className="flex cursor-pointer items-center gap-0.5 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-border-focus-ring">
{label}
<ChevronSortDown className={cx("size-6 shrink-0 transition-[transform,color] duration-150", active && direction === "asc" && "rotate-180", active ? "text-text-secondary" : "text-text-tertiary")} />
</button>
);
}
function PurchaseSelect({ customer, value, onChange }: { customer: string; value: PurchaseState; onChange: (value: PurchaseState) => void }) {
return (
<Select aria-label={`Purchase status for ${customer}`} selectedKey={value} onSelectionChange={(key) => onChange(String(key) as PurchaseState)} className="w-[142px]">
<SelectItem id="completed" textValue="Completed"><StatusDot color="green" />Completed</SelectItem>
<SelectItem id="waiting" textValue="Waiting"><StatusDot color="yellow" />Waiting</SelectItem>
<SelectItem id="processing" textValue="Processing"><StatusDot color="indigo" />Processing</SelectItem>
</Select>
);
}
function MoreMenu({ customer, onAction }: { customer: string; onAction: (message: string) => void }) {
const [open, setOpen] = useState(false);
const actions = [
{ icon: RiUserLine, label: "View profile" },
{ icon: RiFileCopyLine, label: "Duplicate row" },
{ icon: RiDownload2Line, label: "Download invoice" },
{ icon: RiArchiveLine, label: "Archive customer" },
] as const;
return (
<Dropdown isOpen={open} onOpenChange={setOpen}>
<DropdownTrigger aria-label={`More actions for ${customer}`} className="relative inline-flex size-8 shrink-0 items-center justify-center rounded-2lg border border-border-button-default bg-background-primary-default text-foreground-icon-primary shadow-xs transition-colors duration-150 hover:bg-background-primary-hover">
<RiMore2Fill className="size-4" aria-hidden />
</DropdownTrigger>
<DropdownPopover aria-label={`More actions for ${customer}`} placement="bottom end" className="w-[220px] p-2">
<DropdownGroup>
{actions.map(({ icon: Icon, label }) => (
<DropdownItem key={label} onSelect={() => { onAction(`${label}: ${customer}`); setOpen(false); }} className="px-2 py-1.5">
<Icon className="size-[18px] shrink-0 text-foreground-icon-secondary" aria-hidden />
<span className="truncate text-body-medium whitespace-nowrap text-text-primary">{label}</span>
</DropdownItem>
))}
</DropdownGroup>
</DropdownPopover>
</Dropdown>
);
}
export interface HomeCustomersTableProps {
customers: readonly HomeDashboardCustomer[];
className?: string;
}
export function HomeCustomersTable({ customers, className }: HomeCustomersTableProps) {
const [priceFilter, setPriceFilter] = useState("all");
const [productFilter, setProductFilter] = useState("all");
const [regionFilter, setRegionFilter] = useState("all");
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [selected, setSelected] = useState<Set<string>>(() => new Set(["2", "3"]));
const [purchaseChanges, setPurchaseChanges] = useState<Record<string, PurchaseState>>({});
const [sort, setSort] = useState<{ key: SortKey; direction: SortDirection } | null>(null);
const [message, setMessage] = useState("");
const rows = useMemo(() => {
const price = PRICE_FILTERS.find((item) => item.id === priceFilter) ?? PRICE_FILTERS[0];
const normalizedQuery = query.trim().toLowerCase();
const result = customers.filter((customer) =>
price.test(customer.price) &&
(productFilter === "all" || customer.product === productFilter) &&
(regionFilter === "all" || customer.region === regionFilter) &&
(!normalizedQuery || customer.name.toLowerCase().includes(normalizedQuery)),
);
if (!sort) return result;
return [...result].sort((a, b) => {
const left = a[sort.key];
const right = b[sort.key];
const comparison = typeof left === "string" ? left.localeCompare(String(right)) : left - Number(right);
return sort.direction === "asc" ? comparison : -comparison;
});
}, [customers, priceFilter, productFilter, query, regionFilter, sort]);
const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
const pageRows = rows.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
const selectedOnPage = pageRows.filter((row) => selected.has(row.id)).length;
useEffect(() => {
if (page > totalPages) setPage(totalPages);
}, [page, totalPages]);
function changeFilter(action: () => void) {
action();
setPage(1);
}
function changeSort(key: SortKey) {
setSort((current) => current?.key === key ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction: "asc" });
setPage(1);
}
function toggleRow(id: string, checked: boolean) {
setSelected((current) => {
const next = new Set(current);
if (checked) next.add(id); else next.delete(id);
return next;
});
}
function togglePage(checked: boolean) {
setSelected((current) => {
const next = new Set(current);
pageRows.forEach((row) => checked ? next.add(row.id) : next.delete(row.id));
return next;
});
}
return (
<section className={cx("flex w-full flex-col rounded-2xl border border-border-table pt-2 pb-3", className)}>
<div className="flex w-full flex-col items-start gap-3 px-3 py-1 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col justify-center">
<p className="text-body-medium whitespace-nowrap text-text-tertiary">Total Results</p>
<p className="text-body-medium whitespace-nowrap text-text-primary">{rows.length.toLocaleString()} customers</p>
</div>
<div className="-mx-3 flex w-[calc(100%+1.5rem)] items-center gap-2.5 overflow-x-auto px-3 sm:mx-0 sm:w-auto sm:flex-wrap sm:justify-end sm:overflow-visible sm:px-0">
<Select aria-label="Filter by price" selectedKey={priceFilter} onSelectionChange={(key) => changeFilter(() => setPriceFilter(String(key)))} className="shrink-0" popoverClassName="min-w-40">
{PRICE_FILTERS.map((item) => <SelectItem key={item.id} id={item.id} textValue={item.label}>{item.label}</SelectItem>)}
</Select>
<Select aria-label="Filter by product" selectedKey={productFilter} onSelectionChange={(key) => changeFilter(() => setProductFilter(String(key)))} className="shrink-0" popoverClassName="min-w-40">
<SelectItem id="all" textValue="All products">All products</SelectItem>
{PRODUCTS.map((item) => <SelectItem key={item} id={item} textValue={item}>{item}</SelectItem>)}
</Select>
<Select aria-label="Filter by region" selectedKey={regionFilter} onSelectionChange={(key) => changeFilter(() => setRegionFilter(String(key)))} className="shrink-0" popoverClassName="min-w-40">
<SelectItem id="all" textValue="All regions">All regions</SelectItem>
{REGIONS.map((item) => <SelectItem key={item} id={item} textValue={item}>{item}</SelectItem>)}
</Select>
<InputBase aria-label="Search customers" placeholder="Search" leadingIcon={RiSearchLine} value={query} onChange={(event) => changeFilter(() => setQuery(event.target.value))} fieldClassName="min-w-[153px] flex-1 rounded-full bg-background-secondary-default sm:w-[153px] sm:min-w-0 sm:flex-none" className="text-body-medium" />
</div>
</div>
<div className="mt-2 overflow-x-auto overscroll-x-contain">
<Table aria-label="Customers" selectionMode="none" className="min-w-[1000px]">
<TableHeader>
<TableColumn id="name" isRowHeader className="w-[240px]">
<div className="flex items-center gap-2">
<Checkbox slot={null} aria-label="Select all customers on this page" isSelected={pageRows.length > 0 && selectedOnPage === pageRows.length} isIndeterminate={selectedOnPage > 0 && selectedOnPage < pageRows.length} onChange={(checked) => togglePage(Boolean(checked))} />
<SortButton label="Customer name" active={sort?.key === "name"} direction={sort?.direction ?? "asc"} onClick={() => changeSort("name")} />
</div>
</TableColumn>
<TableColumn id="purchase" className="w-[172px]">Purchase</TableColumn>
<TableColumn id="status" className="w-[168px]">Status</TableColumn>
<TableColumn id="updated" className="w-[160px]"><SortButton label="Last updated" active={sort?.key === "updatedTimestamp"} direction={sort?.direction ?? "asc"} onClick={() => changeSort("updatedTimestamp")} /></TableColumn>
<TableColumn id="price" className="w-[140px]"><SortButton label="Price" active={sort?.key === "price"} direction={sort?.direction ?? "asc"} onClick={() => changeSort("price")} /></TableColumn>
<TableColumn id="actions" className="w-[132px]">Actions</TableColumn>
</TableHeader>
<TableBody renderEmptyState={() => <div className="flex h-40 items-center justify-center text-body-medium text-text-tertiary">No customers match your filters.</div>}>
{pageRows.map((customer: HomeDashboardCustomer) => (
<TableRow key={customer.id} id={customer.id} style={selected.has(customer.id) ? { backgroundColor: "var(--color-background-secondary-default)" } : undefined}>
<TableCell className="w-[240px]">
<div className="flex min-w-0 items-center gap-2">
<Checkbox slot={null} aria-label={`Select ${customer.name}`} isSelected={selected.has(customer.id)} onChange={(checked) => toggleRow(customer.id, Boolean(checked))} />
<Avatar size="sm" initials={initials(customer.name)} />
<span className="truncate text-body-medium text-text-primary">{customer.name}</span>
</div>
</TableCell>
<TableCell className="w-[172px]"><PurchaseSelect customer={customer.name} value={purchaseChanges[customer.id] ?? customer.purchase} onChange={(value) => setPurchaseChanges((current) => ({ ...current, [customer.id]: value }))} /></TableCell>
<TableCell className="w-[168px]"><Chip variant="bold" color={deliveryColor[customer.delivery]}>{customer.delivery}</Chip></TableCell>
<TableCell className="w-[160px]"><span className="whitespace-nowrap text-body-medium text-text-primary">{customer.updated}</span></TableCell>
<TableCell className="w-[140px]"><Chip variant="subtle" color="gray">{formatPrice(customer.price)}</Chip></TableCell>
<TableCell className="w-[132px]">
<div className="flex items-center justify-end gap-2.5">
<IconButton icon={RiDeleteBin6Line} size="small" aria-label="Delete" onClick={() => setMessage(`Delete requested for ${customer.name}`)} />
<IconButton icon={RiEditLine} size="small" aria-label="Edit" onClick={() => setMessage(`Editing ${customer.name}`)} />
<MoreMenu customer={customer.name} onAction={setMessage} />
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{totalPages > 1 && <div className="px-3 pt-3"><Pagination page={page} totalPages={totalPages} onChange={setPage} siblingCount={2} /></div>}
<p className="sr-only" role="status" aria-live="polite">{message}</p>
</section>
);
}export {
DEFAULT_HOME_DASHBOARD_FILTERS,
HomeDashboard,
type HomeDashboardDateRange,
type HomeDashboardFiltersValue,
type HomeDashboardProps,
type HomeDashboardTeam,
} from "./home-dashboard";
export { HomeCustomersTable, type HomeCustomersTableProps } from "./customers-table";
export { RecentHiresCard, type RecentHiresCardProps, type RecentHiresTeam } from "./recent-hires-card";
export { RevenueChartCard, type RevenueChartCardProps, type RevenuePeriod, type RevenuePeriods } from "./revenue-chart-card";
export type {
DeliveryState,
HomeDashboardCustomer,
HomeDashboardHire,
HomeDashboardNotification,
PurchaseState,
} from "./types";export {
DEFAULT_HOME_DASHBOARD_FILTERS,
HomeDashboard,
type HomeDashboardDateRange,
type HomeDashboardFiltersValue,
type HomeDashboardProps,
type HomeDashboardTeam,
} from "./home-dashboard";
export { HomeCustomersTable, type HomeCustomersTableProps } from "./customers-table";
export { RecentHiresCard, type RecentHiresCardProps, type RecentHiresTeam } from "./recent-hires-card";
export { RevenueChartCard, type RevenueChartCardProps, type RevenuePeriod, type RevenuePeriods } from "./revenue-chart-card";
export type {
DeliveryState,
HomeDashboardCustomer,
HomeDashboardHire,
HomeDashboardNotification,
PurchaseState,
} from "./types";"use client";
import { useState } from "react";
import { RiArrowDownSLine, RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";
import { Avatar } from "@/components/base/avatar/avatar";
import { Button } from "@/components/base/buttons/button";
import { Dropdown, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { cx } from "@/utils/cx";
import type { HomeDashboardHire } from "./types";
export type { HomeDashboardHire };
export interface RecentHiresCardProps {
pages: readonly (readonly HomeDashboardHire[])[];
totalCount?: number;
className?: string;
team?: RecentHiresTeam;
defaultTeam?: RecentHiresTeam;
onTeamChange?: (team: RecentHiresTeam) => void;
}
export type RecentHiresTeam = "BoardCN team" | "Design team" | "Engineering";
const RECENT_HIRES_TEAMS: RecentHiresTeam[] = ["BoardCN team", "Design team", "Engineering"];
function initials(name: string) {
return name.split(" ").map((part) => part[0]).join("").slice(0, 2).toUpperCase();
}
export function RecentHiresCard({
pages,
totalCount = 56,
className,
team: controlledTeam,
defaultTeam = "BoardCN team",
onTeamChange,
}: RecentHiresCardProps) {
const [page, setPage] = useState(0);
const [uncontrolledTeam, setUncontrolledTeam] = useState(defaultTeam);
const team = controlledTeam ?? uncontrolledTeam;
const [teamOpen, setTeamOpen] = useState(false);
const hires = pages[page] ?? pages[0] ?? [];
const pageCount = Math.max(1, pages.length);
return (
<section className={cx("relative flex h-[329px] min-w-0 flex-1 flex-col rounded-2xl bg-background-secondary-default p-2", className)}>
<div className="flex items-start justify-between px-2 pt-2">
<div className="flex min-w-0 flex-col gap-0.5">
<p className="text-body-medium text-text-secondary">Recent hires</p>
<p className="text-title-1-medium whitespace-nowrap text-text-primary">{totalCount}</p>
</div>
<Dropdown isOpen={teamOpen} onOpenChange={setTeamOpen}>
<DropdownTrigger className="flex cursor-pointer items-center gap-1.5 rounded-2lg px-0.5">
<span className="text-body-medium whitespace-nowrap text-text-primary">{team}</span>
<RiArrowDownSLine className={cx("size-4 shrink-0 text-text-secondary transition-transform duration-200", teamOpen && "rotate-180")} aria-hidden />
</DropdownTrigger>
<DropdownPopover aria-label="Choose team" placement="bottom end" className="w-44">
{RECENT_HIRES_TEAMS.map((item) => (
<DropdownItem key={item} selected={item === team} onSelect={() => { setUncontrolledTeam(item); onTeamChange?.(item); setTeamOpen(false); }}>
{item}
</DropdownItem>
))}
</DropdownPopover>
</Dropdown>
</div>
<div key={page} className="mt-[11px] grid flex-1 animate-number-fade grid-cols-2 grid-rows-2 gap-2" aria-live="polite">
{hires.map((hire) => (
<article key={hire.name} className="flex min-w-0 flex-col items-start justify-between rounded-2lg bg-background-inner-default p-2.5 shadow-card">
<div className="flex w-full min-w-0 items-center gap-2">
<Avatar size="lg" src={hire.avatar} alt={hire.avatar ? hire.name : undefined} initials={initials(hire.name)} />
<div className="flex min-w-0 flex-1 flex-col items-start justify-center">
<p className="w-full truncate text-body-medium text-text-primary">{hire.name}</p>
<p className="w-full truncate text-body-2-medium text-text-secondary">{hire.joined}</p>
</div>
</div>
<span className="inline-flex w-full items-center justify-center rounded-md bg-background-recent-hire-role px-1.5 py-1 text-caption-1-medium whitespace-nowrap text-text-secondary">
{hire.role}
</span>
</article>
))}
</div>
<div className="mt-2 flex w-full items-center gap-2">
<Button className="flex-1" variant="secondary" size="small" leadingIcon={RiArrowLeftLine} onClick={() => setPage((current) => (current + pageCount - 1) % pageCount)}>
Previous
</Button>
<Button className="flex-1" variant="secondary" size="small" trailingIcon={RiArrowRightLine} onClick={() => setPage((current) => (current + 1) % pageCount)}>
Next
</Button>
</div>
</section>
);
}"use client";
import { useState } from "react";
import { RiArrowDownSLine, RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";
import { Avatar } from "@/components/base/avatar/avatar";
import { Button } from "@/components/base/buttons/button";
import { Dropdown, DropdownItem, DropdownPopover, DropdownTrigger } from "@/components/base/dropdown/dropdown";
import { cx } from "@/utils/cx";
import type { HomeDashboardHire } from "./types";
export type { HomeDashboardHire };
export interface RecentHiresCardProps {
pages: readonly (readonly HomeDashboardHire[])[];
totalCount?: number;
className?: string;
team?: RecentHiresTeam;
defaultTeam?: RecentHiresTeam;
onTeamChange?: (team: RecentHiresTeam) => void;
}
export type RecentHiresTeam = "BoardCN team" | "Design team" | "Engineering";
const RECENT_HIRES_TEAMS: RecentHiresTeam[] = ["BoardCN team", "Design team", "Engineering"];
function initials(name: string) {
return name.split(" ").map((part) => part[0]).join("").slice(0, 2).toUpperCase();
}
export function RecentHiresCard({
pages,
totalCount = 56,
className,
team: controlledTeam,
defaultTeam = "BoardCN team",
onTeamChange,
}: RecentHiresCardProps) {
const [page, setPage] = useState(0);
const [uncontrolledTeam, setUncontrolledTeam] = useState(defaultTeam);
const team = controlledTeam ?? uncontrolledTeam;
const [teamOpen, setTeamOpen] = useState(false);
const hires = pages[page] ?? pages[0] ?? [];
const pageCount = Math.max(1, pages.length);
return (
<section className={cx("relative flex h-[329px] min-w-0 flex-1 flex-col rounded-2xl bg-background-secondary-default p-2", className)}>
<div className="flex items-start justify-between px-2 pt-2">
<div className="flex min-w-0 flex-col gap-0.5">
<p className="text-body-medium text-text-secondary">Recent hires</p>
<p className="text-title-1-medium whitespace-nowrap text-text-primary">{totalCount}</p>
</div>
<Dropdown isOpen={teamOpen} onOpenChange={setTeamOpen}>
<DropdownTrigger className="flex cursor-pointer items-center gap-1.5 rounded-2lg px-0.5">
<span className="text-body-medium whitespace-nowrap text-text-primary">{team}</span>
<RiArrowDownSLine className={cx("size-4 shrink-0 text-text-secondary transition-transform duration-200", teamOpen && "rotate-180")} aria-hidden />
</DropdownTrigger>
<DropdownPopover aria-label="Choose team" placement="bottom end" className="w-44">
{RECENT_HIRES_TEAMS.map((item) => (
<DropdownItem key={item} selected={item === team} onSelect={() => { setUncontrolledTeam(item); onTeamChange?.(item); setTeamOpen(false); }}>
{item}
</DropdownItem>
))}
</DropdownPopover>
</Dropdown>
</div>
<div key={page} className="mt-[11px] grid flex-1 animate-number-fade grid-cols-2 grid-rows-2 gap-2" aria-live="polite">
{hires.map((hire) => (
<article key={hire.name} className="flex min-w-0 flex-col items-start justify-between rounded-2lg bg-background-inner-default p-2.5 shadow-card">
<div className="flex w-full min-w-0 items-center gap-2">
<Avatar size="lg" src={hire.avatar} alt={hire.avatar ? hire.name : undefined} initials={initials(hire.name)} />
<div className="flex min-w-0 flex-1 flex-col items-start justify-center">
<p className="w-full truncate text-body-medium text-text-primary">{hire.name}</p>
<p className="w-full truncate text-body-2-medium text-text-secondary">{hire.joined}</p>
</div>
</div>
<span className="inline-flex w-full items-center justify-center rounded-md bg-background-recent-hire-role px-1.5 py-1 text-caption-1-medium whitespace-nowrap text-text-secondary">
{hire.role}
</span>
</article>
))}
</div>
<div className="mt-2 flex w-full items-center gap-2">
<Button className="flex-1" variant="secondary" size="small" leadingIcon={RiArrowLeftLine} onClick={() => setPage((current) => (current + pageCount - 1) % pageCount)}>
Previous
</Button>
<Button className="flex-1" variant="secondary" size="small" trailingIcon={RiArrowRightLine} onClick={() => setPage((current) => (current + 1) % pageCount)}>
Next
</Button>
</div>
</section>
);
}"use client";
import { useId, useState } from "react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Chip } from "@/components/base/badges/chip";
import { SegmentedControl, SegmentedControlItem } from "@/components/base/segmented-control/segmented-control";
import { cx } from "@/utils/cx";
const LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
export type RevenuePeriod = "weekly" | "monthly" | "yearly";
export type RevenuePeriods = Record<RevenuePeriod, readonly number[]>;
export interface RevenueChartCardProps {
periods: RevenuePeriods;
defaultTotal?: number;
className?: string;
}
export function RevenueChartCard({ periods, defaultTotal = 18_240, className }: RevenueChartCardProps) {
const [period, setPeriod] = useState<RevenuePeriod>("weekly");
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const gradientId = useId().replace(/:/g, "");
const data = periods[period].map((value, index) => ({ label: LABELS[index], value }));
const active = activeIndex == null ? null : data[activeIndex];
return (
<section className={cx("flex h-[337px] min-w-0 flex-1 flex-col gap-6 rounded-2xl bg-background-secondary-default px-4 pt-4 pb-3", className)}>
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-start sm:gap-0.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p className="w-full text-body-medium text-text-secondary">{active?.label ?? "Revenue"}</p>
<div className="flex w-full items-center gap-2">
<p key={`${period}-${activeIndex}`} className="animate-number-fade text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
${(active?.value ?? defaultTotal).toLocaleString()}
</p>
<Chip variant="bold" color="lime" className={active ? "invisible" : undefined}>+9.4%</Chip>
</div>
</div>
<SegmentedControl selectedKeys={[period]} onSelectionChange={(keys) => { const next = [...keys][0] as RevenuePeriod | undefined; if (next) { setPeriod(next); setActiveIndex(null); } }} aria-label="Revenue period" className="p-0 sm:p-1">
<SegmentedControlItem id="weekly">Weekly</SegmentedControlItem>
<SegmentedControlItem id="monthly">Monthly</SegmentedControlItem>
<SegmentedControlItem id="yearly">Yearly</SegmentedControlItem>
</SegmentedControl>
</div>
<div className="min-h-0 w-full flex-1" role="application" aria-label={`Revenue chart, ${active ? `$${active.value.toLocaleString()} in ${active.label}` : `$${defaultTotal.toLocaleString()} total`}`}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 4, right: 6, bottom: 0, left: 0 }} onMouseMove={(state) => { const index = Number(state?.activeTooltipIndex); setActiveIndex(state?.isTooltipActive && Number.isInteger(index) ? index : null); }} onMouseLeave={() => setActiveIndex(null)}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-chart-6)" stopOpacity="0.42" />
<stop offset="100%" stopColor="var(--color-chart-6)" stopOpacity="0.03" />
</linearGradient>
</defs>
<CartesianGrid vertical={false} stroke="var(--color-chart-track)" strokeDasharray="4 4" />
<YAxis width={44} domain={[0, 6000]} ticks={[0, 2000, 4000, 6000]} tickFormatter={(value) => value === 0 ? "$0" : `$${Number(value) / 1000}K`} tickLine={false} axisLine={false} tick={{ fontSize: 12, fill: "var(--color-text-tertiary)" }} />
<XAxis dataKey="label" tickLine={false} axisLine={false} interval={1} tickMargin={12} tick={{ fontSize: 13, fill: "var(--color-text-tertiary)" }} />
<Tooltip content={() => null} cursor={{ stroke: "var(--color-chart-cursor)", strokeWidth: 1, strokeDasharray: "4 4" }} />
<Area type="linear" dataKey="value" stroke="var(--color-chart-6)" strokeWidth={2} fill={`url(#${gradientId})`} activeDot={{ r: 4, fill: "var(--color-chart-6-active)", stroke: "var(--color-background-secondary-default)", strokeWidth: 2 }} isAnimationActive animationDuration={450} />
</AreaChart>
</ResponsiveContainer>
</div>
</section>
);
}"use client";
import { useId, useState } from "react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { Chip } from "@/components/base/badges/chip";
import { SegmentedControl, SegmentedControlItem } from "@/components/base/segmented-control/segmented-control";
import { cx } from "@/utils/cx";
const LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
export type RevenuePeriod = "weekly" | "monthly" | "yearly";
export type RevenuePeriods = Record<RevenuePeriod, readonly number[]>;
export interface RevenueChartCardProps {
periods: RevenuePeriods;
defaultTotal?: number;
className?: string;
}
export function RevenueChartCard({ periods, defaultTotal = 18_240, className }: RevenueChartCardProps) {
const [period, setPeriod] = useState<RevenuePeriod>("weekly");
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const gradientId = useId().replace(/:/g, "");
const data = periods[period].map((value, index) => ({ label: LABELS[index], value }));
const active = activeIndex == null ? null : data[activeIndex];
return (
<section className={cx("flex h-[337px] min-w-0 flex-1 flex-col gap-6 rounded-2xl bg-background-secondary-default px-4 pt-4 pb-3", className)}>
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-start sm:gap-0.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p className="w-full text-body-medium text-text-secondary">{active?.label ?? "Revenue"}</p>
<div className="flex w-full items-center gap-2">
<p key={`${period}-${activeIndex}`} className="animate-number-fade text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
${(active?.value ?? defaultTotal).toLocaleString()}
</p>
<Chip variant="bold" color="lime" className={active ? "invisible" : undefined}>+9.4%</Chip>
</div>
</div>
<SegmentedControl selectedKeys={[period]} onSelectionChange={(keys) => { const next = [...keys][0] as RevenuePeriod | undefined; if (next) { setPeriod(next); setActiveIndex(null); } }} aria-label="Revenue period" className="p-0 sm:p-1">
<SegmentedControlItem id="weekly">Weekly</SegmentedControlItem>
<SegmentedControlItem id="monthly">Monthly</SegmentedControlItem>
<SegmentedControlItem id="yearly">Yearly</SegmentedControlItem>
</SegmentedControl>
</div>
<div className="min-h-0 w-full flex-1" role="application" aria-label={`Revenue chart, ${active ? `$${active.value.toLocaleString()} in ${active.label}` : `$${defaultTotal.toLocaleString()} total`}`}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 4, right: 6, bottom: 0, left: 0 }} onMouseMove={(state) => { const index = Number(state?.activeTooltipIndex); setActiveIndex(state?.isTooltipActive && Number.isInteger(index) ? index : null); }} onMouseLeave={() => setActiveIndex(null)}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--color-chart-6)" stopOpacity="0.42" />
<stop offset="100%" stopColor="var(--color-chart-6)" stopOpacity="0.03" />
</linearGradient>
</defs>
<CartesianGrid vertical={false} stroke="var(--color-chart-track)" strokeDasharray="4 4" />
<YAxis width={44} domain={[0, 6000]} ticks={[0, 2000, 4000, 6000]} tickFormatter={(value) => value === 0 ? "$0" : `$${Number(value) / 1000}K`} tickLine={false} axisLine={false} tick={{ fontSize: 12, fill: "var(--color-text-tertiary)" }} />
<XAxis dataKey="label" tickLine={false} axisLine={false} interval={1} tickMargin={12} tick={{ fontSize: 13, fill: "var(--color-text-tertiary)" }} />
<Tooltip content={() => null} cursor={{ stroke: "var(--color-chart-cursor)", strokeWidth: 1, strokeDasharray: "4 4" }} />
<Area type="linear" dataKey="value" stroke="var(--color-chart-6)" strokeWidth={2} fill={`url(#${gradientId})`} activeDot={{ r: 4, fill: "var(--color-chart-6-active)", stroke: "var(--color-background-secondary-default)", strokeWidth: 2 }} isAnimationActive animationDuration={450} />
</AreaChart>
</ResponsiveContainer>
</div>
</section>
);
}export type PurchaseState = "completed" | "waiting" | "processing";
export type DeliveryState =
| "Delivery failed"
| "Shipped"
| "Delivery waiting"
| "Confirmed";
export interface HomeDashboardHire {
name: string;
joined: string;
role: string;
avatar?: string;
}
export interface HomeDashboardCustomer {
id: string;
name: string;
purchase: PurchaseState;
delivery: DeliveryState;
updated: string;
updatedTimestamp: number;
price: number;
product: string;
region: string;
avatar?: string;
}
export interface HomeDashboardNotification {
title: string;
description: string;
time: string;
}export type PurchaseState = "completed" | "waiting" | "processing";
export type DeliveryState =
| "Delivery failed"
| "Shipped"
| "Delivery waiting"
| "Confirmed";
export interface HomeDashboardHire {
name: string;
joined: string;
role: string;
avatar?: string;
}
export interface HomeDashboardCustomer {
id: string;
name: string;
purchase: PurchaseState;
delivery: DeliveryState;
updated: string;
updatedTimestamp: number;
price: number;
product: string;
region: string;
avatar?: string;
}
export interface HomeDashboardNotification {
title: string;
description: string;
time: string;
}