HextorUI
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-overview

Source

"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