block
Dashboard — Overview
Revenue, orders, AOV and conversion tiles beside a sales chart, recent orders and a low-stock list.
npx shadcn@latest add @hextor/store-dash-overviewSource
"use client";
import type { ReactNode } from "react";
import Link from "next/link";
import { DollarSign, Percent, Receipt, ShoppingBag } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import {
DashboardShell,
DEFAULT_DASHBOARD_NAV,
type DashboardBrand,
type DashboardNavItem,
} from "@/registry/hextor/components/store/dashboard/dashboard-shell";
import { PageHeader } from "@/registry/hextor/components/store/dashboard/page-header";
import { KpiTile, type KpiDeltaSentiment, type KpiTrend } from "@/registry/hextor/components/store/dashboard/kpi-tile";
import { SalesChart, type SalesChartPoint } from "@/registry/hextor/components/store/dashboard/sales-chart";
import { DataTable, type DataTableColumn } from "@/registry/hextor/components/store/dashboard/data-table";
import { OrderStatusBadges } from "@/registry/hextor/components/store/dashboard/order-status-badges";
import { LowStockList, type LowStockItem } from "@/registry/hextor/components/store/dashboard/low-stock-list";
import { EmptyState } from "@/registry/hextor/components/store/dashboard/empty-state";
import { resolveLabels, type DashboardLabelsOverrides } from "@/registry/hextor/components/store/dashboard/labels";
import { formatMoney, type Money, type Order } from "@/registry/hextor/lib/store";
/**
* The dashboard's landing page: four KPI tiles, a revenue trend, a peek at
* the newest orders and whatever inventory is running low. Every number
* arrives as a prop — this file never fetches and never guesses what "low
* stock" means, that threshold is the caller's business rule, baked into
* `lowStockItems` before it gets here.
*/
export interface OverviewMetrics {
revenue: Money;
/** Percent change vs. the prior period, e.g. 12.4 for "+12.4%". Omit to hide the delta row. */
revenueDeltaPct?: number;
ordersCount: number;
ordersDeltaPct?: number;
averageOrderValue: Money;
averageOrderValueDeltaPct?: number;
/** 0-100. */
conversionRatePct: number;
conversionRateDeltaPct?: number;
}
function trendOf(delta: number | undefined): KpiTrend {
if (delta === undefined || delta === 0) return "flat";
return delta > 0 ? "up" : "down";
}
function formatDelta(delta: number | undefined): string | undefined {
if (delta === undefined) return undefined;
const sign = delta > 0 ? "+" : "";
return `${sign}${delta.toFixed(1)}%`;
}
/**
* Whether a delta is good news is a business fact about the metric, never a
* property of the number's sign — a rising refund rate would be "up" and
* bad. Every KPI on this page states its own direction-of-good here rather
* than leaving `KpiTile` to assume "up is good," so adding a metric where
* that isn't true (refund rate, cart abandonment, churn) is one line, not a
* bug waiting to happen.
*/
function sentimentOf(
delta: number | undefined,
direction: "higher-is-better" | "lower-is-better",
): KpiDeltaSentiment {
if (delta === undefined || delta === 0) return "neutral";
const isUp = delta > 0;
const isGood = direction === "higher-is-better" ? isUp : !isUp;
return isGood ? "good" : "bad";
}
export default function OverviewPage({
metrics,
salesData,
recentOrders,
recentOrdersLimit = 5,
lowStockItems,
orderHref,
nav = DEFAULT_DASHBOARD_NAV,
brand,
activeHref = "/dashboard",
actions,
locale = "en-US",
labels,
}: {
metrics: OverviewMetrics;
salesData: SalesChartPoint[];
recentOrders: Order[];
recentOrdersLimit?: number;
lowStockItems: LowStockItem[];
orderHref?: (order: Order) => string;
nav?: DashboardNavItem[];
brand?: DashboardBrand;
activeHref?: string;
actions?: ReactNode;
locale?: string;
labels?: DashboardLabelsOverrides;
}) {
const copy = resolveLabels(labels);
const columns: DataTableColumn<Order>[] = [
{
id: "number",
header: copy.orders.columns.order,
cell: (order) =>
orderHref ? (
<Link href={orderHref(order)} className="font-medium hover:underline">
#{order.number}
</Link>
) : (
<span className="font-medium">#{order.number}</span>
),
},
{
id: "customer",
header: copy.orders.columns.customer,
cell: (order) => <span className="text-muted-foreground">{order.email}</span>,
},
{
id: "status",
header: copy.orders.columns.payment,
cell: (order) => (
<OrderStatusBadges
paymentStatus={order.paymentStatus}
fulfillmentStatus={order.fulfillmentStatus}
labels={copy.statuses}
/>
),
},
{
id: "total",
header: copy.orders.columns.total,
align: "right",
className: "tnum",
cell: (order) => formatMoney(order.totals.total, locale),
},
];
return (
<DashboardShell brand={brand} nav={nav} activeHref={activeHref} actions={actions}>
<PageHeader title={copy.overview.title} description={copy.overview.description} />
<div className="space-y-6 p-4 md:p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<KpiTile
label={copy.overview.kpis.revenue}
value={formatMoney(metrics.revenue, locale)}
icon={DollarSign}
delta={formatDelta(metrics.revenueDeltaPct)}
trend={trendOf(metrics.revenueDeltaPct)}
deltaSentiment={sentimentOf(metrics.revenueDeltaPct, "higher-is-better")}
/>
<KpiTile
label={copy.overview.kpis.orders}
value={metrics.ordersCount.toLocaleString(locale)}
icon={ShoppingBag}
delta={formatDelta(metrics.ordersDeltaPct)}
trend={trendOf(metrics.ordersDeltaPct)}
deltaSentiment={sentimentOf(metrics.ordersDeltaPct, "higher-is-better")}
/>
<KpiTile
label={copy.overview.kpis.averageOrderValue}
value={formatMoney(metrics.averageOrderValue, locale)}
icon={Receipt}
delta={formatDelta(metrics.averageOrderValueDeltaPct)}
trend={trendOf(metrics.averageOrderValueDeltaPct)}
deltaSentiment={sentimentOf(metrics.averageOrderValueDeltaPct, "higher-is-better")}
/>
<KpiTile
label={copy.overview.kpis.conversionRate}
value={`${metrics.conversionRatePct.toFixed(1)}%`}
icon={Percent}
delta={formatDelta(metrics.conversionRateDeltaPct)}
trend={trendOf(metrics.conversionRateDeltaPct)}
deltaSentiment={sentimentOf(metrics.conversionRateDeltaPct, "higher-is-better")}
/>
</div>
<Card>
<CardHeader>
<CardTitle>{copy.overview.salesChart.title}</CardTitle>
<CardDescription>{copy.overview.salesChart.description}</CardDescription>
</CardHeader>
<CardContent>
<SalesChart
data={salesData}
formatValue={(v) => formatMoney({ amount: v, currency: metrics.revenue.currency }, locale)}
tableToggleLabel={copy.overview.salesChart.tableToggle}
periodColumnLabel={copy.overview.salesChart.periodColumn}
valueColumnLabel={copy.overview.salesChart.valueColumn}
/>
</CardContent>
</Card>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-5">
<Card className="lg:col-span-3">
<CardHeader>
<CardTitle>{copy.overview.recentOrders.title}</CardTitle>
</CardHeader>
<CardContent className="px-0">
<DataTable
columns={columns}
rows={recentOrders.slice(0, recentOrdersLimit)}
rowKey={(order) => order.id}
emptyState={<EmptyState title={copy.overview.recentOrders.empty} className="mx-4 border-none" />}
/>
</CardContent>
</Card>
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>{copy.overview.lowStock.title}</CardTitle>
</CardHeader>
<CardContent>
<LowStockList
items={lowStockItems}
unitsLeftLabel={copy.overview.lowStock.unitsLeft}
emptyLabel={copy.overview.lowStock.empty}
/>
</CardContent>
</Card>
</div>
</div>
</DashboardShell>
);
}
Docs
Revenue counts only money that was actually collected — paid and partially-refunded orders. Counting authorised or pending totals as revenue is how a dashboard flatters a bad month.
The tiles are derived from the same orders the table below renders, in the route file, so the headline and the list can never tell two different stories about one week.
Ships with sample data from @hextor/store-fixtures so it renders on install. The route file is the only place that import appears — swap it for your own query and the view is unchanged.
Dependencies
@hextor/store-dash-ui@hextor/store-fixtures