HextorUI
block

Storefront — Order history

The customer's full order list, newest first, with reorder.

npx shadcn@latest add @hextor/store-account-orders

Source

import Link from "next/link";
import { ArrowLeft, PackageSearch } from "lucide-react";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/registry/hextor/components/store/storefront/empty-state";
import { OrderHistoryRow } from "@/registry/hextor/components/store/storefront/order-history-row";
import {
  StoreHeader,
  type StoreHeaderProps,
} from "@/registry/hextor/components/store/storefront/store-header";
import {
  StoreFooter,
  type StoreFooterProps,
} from "@/registry/hextor/components/store/storefront/store-footer";
import { cn } from "@/registry/hextor/lib/utils";
import type { Order } from "@/registry/hextor/lib/store";
import {
  resolveLabels,
  type AccountLabelsOverrides,
} from "@/registry/hextor/components/store/storefront/account-labels";

export interface OrderHistoryPageProps {
  orders: Order[];
  orderHref?: (order: Order) => string;
  /** Fired with the order to duplicate into a new cart; wire up the real "add these lines to cart" mutation on your end. */
  onReorder?: (order: Order) => void;
  shopHref?: string;
  accountHref?: string;
  header: StoreHeaderProps;
  footer: StoreFooterProps;
  locale?: string;
  labels?: AccountLabelsOverrides;
  className?: string;
}

/**
 * The full order history: every order this customer has placed, newest
 * first, each row carrying its number, date, payment AND fulfilment state as
 * two independent badges (never collapsed into one "status"), total, and a
 * reorder action. `OrderHistoryRow` is the same component the account
 * overview uses for its "recent orders" preview, so the two never drift.
 */
export default function OrderHistoryView({
  orders,
  orderHref,
  onReorder,
  shopHref = "/",
  accountHref = "/account",
  header,
  footer,
  locale,
  labels,
  className,
}: OrderHistoryPageProps) {
  const copy = resolveLabels(labels);
  const sorted = [...orders].sort(
    (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
  );

  const rowLabels = {
    payment: copy.statuses.payment,
    fulfillment: copy.statuses.fulfillment,
    itemsCount: copy.orders.itemsCount,
    reorder: copy.orders.reorder,
  };

  return (
    <div className={cn("flex min-h-screen flex-col", className)}>
      <StoreHeader {...header} />

      <main className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-6 px-4 py-8 sm:px-6">
        <div className="flex flex-col gap-3">
          <Link
            href={accountHref}
            className="flex w-fit items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-brand"
          >
            <ArrowLeft aria-hidden="true" className="size-4" />
            {copy.nav.overview}
          </Link>
          <div className="flex flex-col gap-2">
            <span aria-hidden="true" className="h-[3px] w-8 rounded-full bg-brand" />
            <h1 className="font-heading text-3xl font-semibold tracking-tight sm:text-4xl">{copy.orders.heading}</h1>
            <p className="text-sm text-muted-foreground">{copy.orders.description}</p>
          </div>
        </div>

        {sorted.length === 0 ? (
          <EmptyState
            icon={PackageSearch}
            title={copy.orders.emptyTitle}
            description={copy.orders.emptyDescription}
            action={<Button size="sm" render={<Link href={shopHref}>{copy.orders.browseCta}</Link>} />}
          />
        ) : (
          <div className="divide-y divide-border rounded-2xl px-4 ring-1 ring-foreground/10 sm:px-6">
            {sorted.map((order) => (
              <OrderHistoryRow
                key={order.id}
                order={order}
                orderHref={orderHref?.(order)}
                onReorder={onReorder}
                locale={locale}
                labels={rowLabels}
              />
            ))}
          </div>
        )}
      </main>

      <StoreFooter {...footer} />
    </div>
  );
}

Docs

Payment and fulfilment stay separate here too — an order can be paid and unfulfilled, or fulfilled and refunded, and a customer asking "where is my order" needs to see which.

Ships with sample data from @hextor/store-fixtures so it renders a real shop the moment it installs — a template you have to wire up before you can look at it is a template nobody evaluates. The route file is the only place that import appears.

Dependencies

@hextor/store-ui@hextor/store-fixtures