shadimage
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 · medium
iconslucide-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 &amp; 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),
    };
  });
}

More from Blocks.so

Browse all components in this library →