Skip to content
6

Dashboard 1

Operations dashboard with a Tabs date-range switcher (1d / 7d / 30d / 90d), a 4-up metric strip, a weekly bar chart and a recent-activity feed. Every metric declares which direction is good news, so falling churn reads green; each chart bucket is a focusable control, so the numbers reach the keyboard as well as the mouse.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/dashboard-01.json

Code

components/blocks/dashboard-01.tsx
'use client';

import * as React from 'react';
import { ArrowDownRight, ArrowUpRight, Download, Filter, Minus, RefreshCw } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Avatar, AvatarFallback } from '@/registry/hirael/bases/radix/ui/avatar';
import { Badge } from '@/registry/hirael/bases/radix/ui/badge';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/registry/hirael/bases/radix/ui/card';
import { Separator } from '@/registry/hirael/bases/radix/ui/separator';
import { Tabs, TabsList, TabsTrigger } from '@/registry/hirael/bases/radix/ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/hirael/bases/radix/ui/tooltip';

interface Metric {
  label: string;
  value: string;
  /** Change against the previous period; the sign carries the direction. */
  delta: number;
  unit: '%' | 'pt' | 's';
  /** Which way this metric has to move to be good news. Churn falls. */
  goodWhen: 'up' | 'down';
}

type Range = '1d' | '7d' | '30d' | '90d';

const RANGES: { value: Range; label: string }[] = [
  { value: '1d', label: 'Today' },
  { value: '7d', label: '7 days' },
  { value: '30d', label: '30 days' },
  { value: '90d', label: '90 days' },
];

const METRICS_BY_RANGE: Record<Range, readonly Metric[]> = {
  '1d': [
    { label: 'MRR', value: '$48,510', delta: 0.5, unit: '%', goodWhen: 'up' },
    {
      label: 'Active orgs',
      value: '1,289',
      delta: 0.4,
      unit: '%',
      goodWhen: 'up',
    },
    { label: 'Churn', value: '1.9%', delta: 0, unit: '%', goodWhen: 'down' },
    {
      label: 'Avg. session',
      value: '4m 18s',
      delta: 6,
      unit: 's',
      goodWhen: 'up',
    },
  ],
  '7d': [
    { label: 'MRR', value: '$48,250', delta: 8.7, unit: '%', goodWhen: 'up' },
    {
      label: 'Active orgs',
      value: '1,284',
      delta: 4.1,
      unit: '%',
      goodWhen: 'up',
    },
    { label: 'Churn', value: '1.8%', delta: -0.4, unit: '%', goodWhen: 'down' },
    {
      label: 'Avg. session',
      value: '4m 12s',
      delta: 0,
      unit: 's',
      goodWhen: 'up',
    },
  ],
  '30d': [
    { label: 'MRR', value: '$46,180', delta: 18.2, unit: '%', goodWhen: 'up' },
    {
      label: 'Active orgs',
      value: '1,231',
      delta: 12.6,
      unit: '%',
      goodWhen: 'up',
    },
    { label: 'Churn', value: '2.1%', delta: -0.6, unit: '%', goodWhen: 'down' },
    {
      label: 'Avg. session',
      value: '4m 04s',
      delta: 14,
      unit: 's',
      goodWhen: 'up',
    },
  ],
  '90d': [
    { label: 'MRR', value: '$41,920', delta: 34.6, unit: '%', goodWhen: 'up' },
    {
      label: 'Active orgs',
      value: '1,096',
      delta: 26.3,
      unit: '%',
      goodWhen: 'up',
    },
    { label: 'Churn', value: '2.4%', delta: -1.1, unit: '%', goodWhen: 'down' },
    {
      label: 'Avg. session',
      value: '3m 51s',
      delta: 27,
      unit: 's',
      goodWhen: 'up',
    },
  ],
};

const CHART_BY_RANGE: Record<Range, readonly { d: string; a: number; b: number }[]> = {
  '1d': [
    { d: '00', a: 6, b: 3 },
    { d: '04', a: 4, b: 2 },
    { d: '08', a: 18, b: 10 },
    { d: '12', a: 32, b: 22 },
    { d: '16', a: 28, b: 18 },
    { d: '20', a: 14, b: 9 },
    { d: '24', a: 8, b: 5 },
  ],
  '7d': [
    { d: 'Mon', a: 38, b: 22 },
    { d: 'Tue', a: 52, b: 35 },
    { d: 'Wed', a: 47, b: 30 },
    { d: 'Thu', a: 64, b: 41 },
    { d: 'Fri', a: 78, b: 55 },
    { d: 'Sat', a: 60, b: 48 },
    { d: 'Sun', a: 72, b: 58 },
  ],
  '30d': [
    { d: 'W1', a: 210, b: 142 },
    { d: 'W2', a: 268, b: 188 },
    { d: 'W3', a: 312, b: 224 },
    { d: 'W4', a: 346, b: 252 },
    { d: 'W5', a: 298, b: 211 },
    { d: 'W6', a: 384, b: 281 },
    { d: 'W7', a: 412, b: 306 },
  ],
  '90d': [
    { d: 'M1', a: 920, b: 612 },
    { d: 'M2', a: 1080, b: 752 },
    { d: 'M3', a: 1240, b: 882 },
    { d: 'M4', a: 1180, b: 856 },
    { d: 'M5', a: 1320, b: 968 },
    { d: 'M6', a: 1480, b: 1102 },
    { d: 'M7', a: 1620, b: 1224 },
  ],
};

const SIGNUPS_BY_RANGE: Record<Range, { count: string; conversion: string }> = {
  '1d': { count: '82', conversion: '3.91%' },
  '7d': { count: '486', conversion: '3.42%' },
  '30d': { count: '2,154', conversion: '3.18%' },
  '90d': { count: '6,820', conversion: '2.94%' },
};

interface Activity {
  initials: string;
  name: string;
  action: string;
  time: string;
}

const ACTIVITY: readonly Activity[] = [
  {
    initials: 'MR',
    name: 'Maya Renner',
    action: 'upgraded to Pro',
    time: '2m ago',
  },
  {
    initials: 'JT',
    name: 'Jules Tanaka',
    action: 'invited 3 teammates',
    time: '14m ago',
  },
  {
    initials: 'AO',
    name: 'Adaeze Okafor',
    action: 'exported 412 rows',
    time: '1h ago',
  },
  {
    initials: 'SK',
    name: 'Soren Kim',
    action: 'rotated API keys',
    time: '3h ago',
  },
  {
    initials: 'LB',
    name: 'Lena Brandt',
    action: 'connected Slack',
    time: '5h ago',
  },
];

/** Tone follows intent, not sign: falling churn is good news, so it is green. */
const deltaTone = ({ delta, goodWhen }: Metric) => {
  if (delta === 0) return 'bg-accent text-muted-foreground';
  const improving = delta > 0 === (goodWhen === 'up');
  return improving ? 'bg-success/10 text-success' : 'bg-destructive/10 text-destructive';
};

const DeltaChip = ({ metric }: { metric: Metric }) => {
  const { delta, unit, label } = metric;
  const Icon = delta > 0 ? ArrowUpRight : delta < 0 ? ArrowDownRight : Minus;
  const direction = delta > 0 ? 'up' : delta < 0 ? 'down' : 'unchanged';
  const measure = unit === '%' ? 'percent' : unit === 'pt' ? 'points' : 'seconds';
  const sign = delta > 0 ? '+' : delta < 0 ? '−' : '';

  return (
    <Badge
      dir="ltr"
      aria-label={`${label} ${direction} ${Math.abs(delta)} ${measure} against the previous period`}
      className={cn('rounded-sm px-1.5 py-0.5 font-mono text-[11px] leading-none tabular-nums', deltaTone(metric))}
    >
      <Icon className="size-3" aria-hidden />
      {sign}
      {Math.abs(delta)}
      {unit}
    </Badge>
  );
};

const Dashboard01 = () => {
  const [range, setRange] = React.useState<Range>('7d');
  const [refreshing, setRefreshing] = React.useState(false);
  const [status, setStatus] = React.useState('');

  const metrics = METRICS_BY_RANGE[range];
  const chart = CHART_BY_RANGE[range];
  const signups = SIGNUPS_BY_RANGE[range];
  const chartMax = Math.max(...chart.flatMap((c) => [c.a, c.b]));

  const onRefresh = async () => {
    setRefreshing(true);
    setStatus('Refreshing data');
    await new Promise((r) => setTimeout(r, 600));
    setRefreshing(false);
    setStatus('Data refreshed');
  };

  return (
    <section className="bg-background py-20 sm:py-28">
      <div className="container w-full">
        <div className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
          <div className="flex max-w-xl flex-col gap-3">
            <Badge variant="outline" className="w-fit">
              overview
            </Badge>
            <h2 className="font-serif text-4xl font-medium tracking-tight sm:text-5xl">
              Operations · {RANGES.find((r) => r.value === range)?.label}.
            </h2>
          </div>
          <div className="flex items-center gap-2">
            <Tabs value={range} onValueChange={(v) => setRange(v as Range)} className="w-fit">
              <TabsList>
                {RANGES.map((r) => (
                  <TabsTrigger key={r.value} value={r.value}>
                    {r.label}
                  </TabsTrigger>
                ))}
              </TabsList>
            </Tabs>
            <Button variant="outline" size="sm" className="hidden sm:inline-flex">
              <Filter className="size-3.5" aria-hidden />
              All teams
            </Button>
            <Button variant="ghost" size="sm" onClick={onRefresh} disabled={refreshing}>
              <RefreshCw aria-hidden className={cn('size-3.5', refreshing && 'motion-safe:animate-spin')} />
              <span className="hidden sm:inline">Refresh</span>
            </Button>
          </div>
        </div>

        <p aria-live="polite" className="sr-only">
          {status}
        </p>

        <div className="mt-10 grid grid-cols-2 gap-px overflow-hidden rounded-md border border-border bg-border lg:grid-cols-4">
          {metrics.map((m) => (
            <div key={m.label} className="flex flex-col gap-2 bg-card p-5">
              <span className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">{m.label}</span>
              <span className="text-3xl font-semibold tracking-[-0.035em] tabular-nums">{m.value}</span>
              <DeltaChip metric={m} />
            </div>
          ))}
        </div>

        <div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3 lg:items-start">
          <Card className="lg:col-span-2">
            <CardHeader>
              <div className="flex items-start justify-between gap-3">
                <div className="flex flex-col gap-1">
                  <CardDescription className="font-mono text-[10px] uppercase tracking-[0.12em]">
                    sign-ups
                  </CardDescription>
                  <CardTitle className="text-lg">{signups.count} new sign-ups</CardTitle>
                </div>
                <div className="flex items-center gap-3">
                  <div className="text-end">
                    <span className="block font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
                      Conversion
                    </span>
                    <span className="font-mono text-lg font-semibold tabular-nums">{signups.conversion}</span>
                  </div>
                  <Button variant="outline" size="sm" aria-label="Export sign-ups">
                    <Download className="size-3.5" aria-hidden />
                    Export
                  </Button>
                </div>
              </div>
            </CardHeader>
            <CardContent>
              <div
                role="group"
                aria-label={`Sign-ups and activations across ${chart.length} buckets`}
                className="grid h-56 items-end gap-2 sm:gap-3"
                style={{
                  gridTemplateColumns: `repeat(${chart.length}, minmax(0, 1fr))`,
                }}
              >
                {chart.map((row) => (
                  <Tooltip key={row.d}>
                    <TooltipTrigger asChild>
                      <button
                        type="button"
                        aria-label={`${row.d}: ${row.a} sign-ups, ${row.b} activated`}
                        className="group/bar flex h-full cursor-default flex-col gap-1.5 rounded-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
                      >
                        <span className="flex h-full items-end gap-1">
                          <span
                            aria-hidden
                            className="flex-1 rounded-t-xs bg-foreground/85 transition-colors group-hover/bar:bg-foreground group-focus-visible/bar:bg-foreground"
                            style={{ height: `${(row.a / chartMax) * 100}%` }}
                          />
                          <span
                            aria-hidden
                            className="flex-1 rounded-t-xs bg-muted-foreground/40 transition-colors group-hover/bar:bg-muted-foreground/60 group-focus-visible/bar:bg-muted-foreground/60"
                            style={{ height: `${(row.b / chartMax) * 100}%` }}
                          />
                        </span>
                        <span className="text-center font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
                          {row.d}
                        </span>
                      </button>
                    </TooltipTrigger>
                    <TooltipContent>
                      <span className="font-mono tabular-nums">
                        {row.d} · {row.a} sign-ups · {row.b} activated
                      </span>
                    </TooltipContent>
                  </Tooltip>
                ))}
              </div>

              <Separator className="my-4" />

              <div className="flex items-center gap-5">
                <span className="inline-flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
                  <span aria-hidden className="size-2 rounded-xs bg-foreground/85" />
                  Sign-ups
                </span>
                <span className="inline-flex items-center gap-1.5 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
                  <span aria-hidden className="size-2 rounded-xs bg-muted-foreground/40" />
                  Activated
                </span>
              </div>
            </CardContent>
          </Card>

          <Card>
            <CardHeader>
              <div className="flex items-center justify-between">
                <CardDescription className="font-mono text-[10px] uppercase tracking-[0.12em]">
                  recent activity
                </CardDescription>
                <Button variant="link" size="sm" className="h-auto p-0" asChild>
                  <a href="#">View all</a>
                </Button>
              </div>
              <CardTitle className="sr-only">Recent activity</CardTitle>
            </CardHeader>
            <CardContent className="px-0">
              <ul className="flex flex-col">
                {ACTIVITY.map((a, i) => (
                  <li
                    key={a.name}
                    className={cn(
                      'flex items-center gap-3 px-6 py-3',
                      i < ACTIVITY.length - 1 && 'border-b border-border',
                    )}
                  >
                    <Avatar aria-hidden>
                      <AvatarFallback className="bg-muted font-mono text-xs font-medium text-foreground">
                        {a.initials}
                      </AvatarFallback>
                    </Avatar>
                    <div className="flex min-w-0 flex-1 flex-col">
                      <p className="truncate text-sm">
                        <span className="font-medium text-foreground">{a.name}</span>{' '}
                        <span className="text-muted-foreground">{a.action}</span>
                      </p>
                      <span className="font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
                        {a.time}
                      </span>
                    </div>
                  </li>
                ))}
              </ul>
            </CardContent>
          </Card>
        </div>
      </div>
    </section>
  );
};

export default Dashboard01;

Dependencies

shadcn registry

avatarbadgebuttoncardseparatortabstooltip

npm

lucide-react