block
Storefront — Order history
The customer's full order list, newest first, with reorder.
npx shadcn@latest add @hextor/store-account-ordersSource
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