Dashboard 1
Operations dashboard with Tabs date-range switcher (1d / 7d / 30d / 90d), 4-up metric strip, weekly bar chart and a recent-activity feed. Data switches live with the range.
Preview
Installation
npx shadcn@latest add https://hirael.com/r/dashboard-01.jsonCode
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/ui/avatar";
import { Badge } from "@/registry/hirael/ui/badge";
import { Button } from "@/registry/hirael/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/hirael/ui/card";
import { Separator } from "@/registry/hirael/ui/separator";
import { Tabs, TabsList, TabsTrigger } from "@/registry/hirael/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/hirael/ui/tooltip";
type Metric = {
label: string;
value: string;
delta: string;
trend: "up" | "down" | "flat";
};
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%", trend: "up" },
{ label: "Active orgs", value: "1,289", delta: "+5", trend: "up" },
{ label: "Churn", value: "1.9%", delta: "0.0%", trend: "flat" },
{ label: "Avg. session", value: "4m 18s", delta: "+6s", trend: "up" },
],
"7d": [
{ label: "MRR", value: "$48,250", delta: "+8.7%", trend: "up" },
{ label: "Active orgs", value: "1,284", delta: "+4.1%", trend: "up" },
{ label: "Churn", value: "1.8%", delta: "-0.4%", trend: "down" },
{ label: "Avg. session", value: "4m 12s", delta: "0.0%", trend: "flat" },
],
"30d": [
{ label: "MRR", value: "$46,180", delta: "+18.2%", trend: "up" },
{ label: "Active orgs", value: "1,231", delta: "+12.6%", trend: "up" },
{ label: "Churn", value: "2.1%", delta: "-0.6%", trend: "down" },
{ label: "Avg. session", value: "4m 04s", delta: "+14s", trend: "up" },
],
"90d": [
{ label: "MRR", value: "$41,920", delta: "+34.6%", trend: "up" },
{ label: "Active orgs", value: "1,096", delta: "+26.3%", trend: "up" },
{ label: "Churn", value: "2.4%", delta: "-1.1%", trend: "down" },
{ label: "Avg. session", value: "3m 51s", delta: "+27s", trend: "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%" },
};
type Activity = {
initials: string;
name: string;
action: string;
time: string;
tone: "primary" | "default" | "muted";
};
const ACTIVITY: readonly Activity[] = [
{
initials: "MR",
name: "Maya Renner",
action: "upgraded to Pro",
time: "2m ago",
tone: "primary",
},
{
initials: "JT",
name: "Jules Tanaka",
action: "invited 3 teammates",
time: "14m ago",
tone: "default",
},
{
initials: "AO",
name: "Adaeze Okafor",
action: "exported 412 rows",
time: "1h ago",
tone: "default",
},
{
initials: "SK",
name: "Soren Kim",
action: "rotated API keys",
time: "3h ago",
tone: "muted",
},
];
function TrendIcon({ trend }: { trend: Metric["trend"] }) {
if (trend === "up") return <ArrowUpRight className="size-3" />;
if (trend === "down") return <ArrowDownRight className="size-3" />;
return <Minus className="size-3" />;
}
function deltaTone(trend: Metric["trend"]) {
if (trend === "up") return "bg-success/10 text-success";
if (trend === "down") return "bg-destructive/10 text-destructive";
return "bg-accent text-muted-foreground";
}
export default function Dashboard01() {
const [range, setRange] = React.useState<Range>("7d");
const [refreshing, setRefreshing] = React.useState(false);
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);
await new Promise((r) => setTimeout(r, 600));
setRefreshing(false);
};
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" />
All teams
</Button>
<Button
variant="ghost"
size="sm"
onClick={onRefresh}
disabled={refreshing}
aria-label="Refresh data"
>
<RefreshCw
className={cn("size-3.5", refreshing && "animate-spin")}
/>
<span className="hidden sm:inline">Refresh</span>
</Button>
</div>
</div>
<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>
<Badge
className={cn(
"rounded-sm px-1.5 py-0.5 font-mono text-[11px] leading-none",
deltaTone(m.trend),
)}
>
<TrendIcon trend={m.trend} />
{m.delta}
</Badge>
</div>
))}
</div>
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
<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" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div
role="img"
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) => (
<div key={row.d} className="flex h-full flex-col gap-1.5">
<div className="flex h-full items-end gap-1">
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex-1 rounded-t-xs bg-foreground/85 transition-all duration-300 ease-out hover:bg-foreground"
style={{ height: `${(row.a / chartMax) * 100}%` }}
/>
</TooltipTrigger>
<TooltipContent>Sign-ups · {row.a}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex-1 rounded-t-xs bg-muted-foreground/40 transition-all duration-300 ease-out hover:bg-muted-foreground/60"
style={{ height: `${(row.b / chartMax) * 100}%` }}
/>
</TooltipTrigger>
<TooltipContent>Activated · {row.b}</TooltipContent>
</Tooltip>
</div>
<span className="text-center font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
{row.d}
</span>
</div>
))}
</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 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 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>
<AvatarFallback
className={cn(
"font-mono text-xs font-medium",
a.tone === "primary"
? "bg-foreground text-background"
: a.tone === "muted"
? "border border-border bg-card text-muted-foreground"
: "bg-muted 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>
);
}
Dependencies
shadcn registry
avatarbadgebuttoncardseparatortabstooltip
npm
lucide-react