"use client";
import * as React from "react";
import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react";
import { Check, ChevronDown, CircleDot, Download, MapPin, Package, PackageCheck, RotateCcw, Search, ShoppingCart, Truck, X, CreditCard, Home, Undo2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { ProductArt, type ArtKind } from "./product-art";
export type OrderStatus = "processing" | "shipped" | "out-for-delivery" | "delivered" | "cancelled" | "return-requested";
export type OrderLine = { id: string; name: string; variant?: string; price: number; qty: number; kind: ArtKind; hex: string; accent?: string; label?: string };
export type TrackingEvent = { label: string; detail?: string; time: string; done: boolean };
export type Order = {
id: string;
number: string;
date: string;
status: OrderStatus;
lines: OrderLine[];
shipping: number;
address: string;
payment: string;
carrier?: string;
trackingNo?: string;
eta?: string;
events: TrackingEvent[];
returnCode?: string;
};
export type ReturnRequest = { orderId: string; lineIds: string[]; reason: string; resolution: "refund" | "exchange" | "credit" };
export interface OrderHistoryProps {
title?: string;
orders?: Order[];
currency?: string;
locale?: string;
storeName?: string;
onReorder?: (order: Order) => void;
onReturnRequest?: (req: ReturnRequest) => string | void;
onTrack?: (order: Order) => void;
className?: string;
}
const L = (id: string, name: string, variant: string, price: number, qty: number, kind: ArtKind, hex: string, accent?: string, label?: string): OrderLine => ({ id, name, variant, price, qty, kind, hex, accent, label });
const ORDERS: Order[] = [
{
id: "o1",
number: "LC-204981",
date: "22 Sep 2026",
status: "out-for-delivery",
lines: [L("a", "Highland Single Malt 12", "Sherry cask · 70 cl", 39.9, 1, "bottle", "#b45309", "#f5efe0", "Lumen"), L("b", "Crystal Rocks Tumbler", "Set of 2", 12, 2, "tumbler", "#d97706")],
shipping: 0,
address: "12 Harbour Street, D02 X285 Dublin",
payment: "Card ending 4242",
carrier: "Northwind Express",
trackingNo: "NW 8841 2290 17",
eta: "Today, 14:00–16:00",
events: [
{ label: "Order placed", time: "22 Sep, 09:12", done: true },
{ label: "Packed at Lumen warehouse", detail: "Dublin fulfilment centre", time: "22 Sep, 15:40", done: true },
{ label: "Shipped", detail: "Handed to Northwind Express", time: "23 Sep, 07:05", done: true },
{ label: "Out for delivery", detail: "Driver is 6 stops away", time: "24 Sep, 08:31", done: true },
{ label: "Delivered", time: "Expected today", done: false },
],
},
{
id: "o2",
number: "LC-203377",
date: "9 Sep 2026",
status: "delivered",
lines: [L("c", "Reserva Tinto 2019", "75 cl", 18.9, 3, "wine", "#7f1d1d", "#d4af37", "Orbit"), L("d", "Hazy IPA 4-pack", "4 × 440 ml", 11.9, 1, "can", "#ca8a04", "#1e293b", "Brewlab"), L("e", "Soy Wax Candle", "Oak & amber", 16, 1, "candle", "#e7d5c0", "#1c1917", "Lumen")],
shipping: 4.99,
address: "12 Harbour Street, D02 X285 Dublin",
payment: "Lumen Wallet",
carrier: "Northwind Express",
trackingNo: "NW 8812 0021 44",
events: [
{ label: "Order placed", time: "9 Sep, 18:20", done: true },
{ label: "Packed", time: "10 Sep, 10:02", done: true },
{ label: "Shipped", time: "10 Sep, 16:45", done: true },
{ label: "Out for delivery", time: "11 Sep, 08:10", done: true },
{ label: "Delivered", detail: "Left with neighbour at no. 14", time: "11 Sep, 12:47", done: true },
],
},
{
id: "o3",
number: "LC-201560",
date: "28 Aug 2026",
status: "processing",
lines: [L("f", "Tasting Gift Set", "4 × 5 cl", 59, 1, "box", "#6d28d9", "#fbbf24", "Orbit")],
shipping: 0,
address: "7 Linden Allee, 10117 Berlin",
payment: "Card ending 1881",
eta: "Ships in 1–2 days",
events: [
{ label: "Order placed", time: "28 Aug, 21:03", done: true },
{ label: "Packing", detail: "Engraving your gift box", time: "In progress", done: false },
{ label: "Shipped", time: "—", done: false },
{ label: "Delivered", time: "—", done: false },
],
},
{
id: "o4",
number: "LC-198004",
date: "2 Aug 2026",
status: "delivered",
lines: [L("g", "Coastal Dry Gin", "70 cl", 29.5, 2, "bottle", "#0ea5e9", "#f0f9ff", "Northwind"), L("h", "Morning Ceramic Mug", "Tangerine", 24, 1, "mug", "#f97316", "#fff7ed", "Lumen")],
shipping: 4.99,
address: "12 Harbour Street, D02 X285 Dublin",
payment: "Card ending 4242",
carrier: "Northwind Express",
trackingNo: "NW 8790 5512 03",
events: [
{ label: "Order placed", time: "2 Aug, 11:30", done: true },
{ label: "Shipped", time: "3 Aug, 09:14", done: true },
{ label: "Delivered", time: "4 Aug, 13:02", done: true },
],
},
{
id: "o5",
number: "LC-195212",
date: "14 Jul 2026",
status: "cancelled",
lines: [L("i", "Añejo Tequila", "70 cl", 52, 1, "bottle", "#ca8a04", "#1c1917", "Northwind")],
shipping: 0,
address: "12 Harbour Street, D02 X285 Dublin",
payment: "Refunded to card ending 4242",
events: [
{ label: "Order placed", time: "14 Jul, 20:44", done: true },
{ label: "Cancelled", detail: "Cancelled by you · refund issued", time: "14 Jul, 21:02", done: true },
],
},
];
const STATUS: Record<OrderStatus, { label: string; cls: string; dot: string }> = {
processing: { label: "Processing", cls: "bg-amber-500/12 text-amber-700 dark:text-amber-300", dot: "bg-amber-500" },
shipped: { label: "Shipped", cls: "bg-sky-500/12 text-sky-700 dark:text-sky-300", dot: "bg-sky-500" },
"out-for-delivery": { label: "Out for delivery", cls: "bg-indigo-500/12 text-indigo-700 dark:text-indigo-300", dot: "bg-indigo-500" },
delivered: { label: "Delivered", cls: "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300", dot: "bg-emerald-500" },
cancelled: { label: "Cancelled", cls: "bg-muted text-muted-foreground", dot: "bg-muted-foreground" },
"return-requested": { label: "Return requested", cls: "bg-rose-500/12 text-rose-700 dark:text-rose-300", dot: "bg-rose-500" },
};
const TABS: { id: string; label: string; match: (s: OrderStatus) => boolean }[] = [
{ id: "all", label: "All orders", match: () => true },
{ id: "open", label: "In progress", match: (s) => s === "processing" || s === "shipped" || s === "out-for-delivery" },
{ id: "done", label: "Delivered", match: (s) => s === "delivered" },
{ id: "returns", label: "Returns & cancelled", match: (s) => s === "return-requested" || s === "cancelled" },
];
const REASONS = ["Changed my mind", "Arrived damaged", "Wrong item sent", "Not as described", "Better price elsewhere"];
const ring = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
export function OrderHistory({
title = "Your orders",
orders: initialOrders = ORDERS,
currency = "EUR",
locale = "en-IE",
storeName = "Lumen Cellars",
onReorder,
onReturnRequest,
onTrack,
className,
}: OrderHistoryProps) {
const reduce = useReducedMotion();
const fmt = React.useMemo(() => new Intl.NumberFormat(locale, { style: "currency", currency }), [locale, currency]);
const [orders, setOrders] = React.useState(initialOrders);
const [tab, setTab] = React.useState("all");
const [q, setQ] = React.useState("");
const [open, setOpen] = React.useState<string | null>(initialOrders[0]?.id ?? null);
const [toast, setToast] = React.useState<string | null>(null);
const [returning, setReturning] = React.useState<Order | null>(null);
React.useEffect(() => {
if (!toast) return;
const t = window.setTimeout(() => setToast(null), 2800);
return () => window.clearTimeout(t);
}, [toast]);
const tabDef = TABS.find((t) => t.id === tab) ?? TABS[0];
const list = orders.filter((o) => tabDef.match(o.status) && (!q.trim() || `${o.number} ${o.lines.map((l) => l.name).join(" ")}`.toLowerCase().includes(q.trim().toLowerCase())));
const totalOf = (o: Order) => o.lines.reduce((s, l) => s + l.price * l.qty, 0) + o.shipping;
const invoice = (o: Order) => {
const pad = (s: string, n: number) => (s.length > n ? s.slice(0, n - 1) + "…" : s.padEnd(n));
const rows = o.lines.map((l) => `${pad(`${l.name} (${l.variant ?? ""})`, 44)} ${String(l.qty).padStart(3)} ${fmt.format(l.price * l.qty).padStart(12)}`);
const txt = [
`${storeName.toUpperCase()} — INVOICE`,
"=".repeat(64),
`Order: ${o.number}`,
`Date: ${o.date}`,
`Ship to: ${o.address}`,
`Payment: ${o.payment}`,
"",
`${pad("Item", 44)} Qty ${"Amount".padStart(12)}`,
"-".repeat(64),
...rows,
"-".repeat(64),
`${pad("Shipping", 49)}${fmt.format(o.shipping).padStart(12)}`,
`${pad("TOTAL (incl. VAT)", 49)}${fmt.format(totalOf(o)).padStart(12)}`,
"",
"Thank you for shopping with us.",
].join("\n");
const url = URL.createObjectURL(new Blob([txt], { type: "text/plain" }));
const a = document.createElement("a");
a.href = url;
a.download = `invoice-${o.number}.txt`;
document.body.appendChild(a);
a.click();
a.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
setToast(`Invoice ${o.number} downloaded`);
};
return (
<section className={cn("relative w-full bg-background py-8 text-foreground sm:py-12", className)}>
<div className="mx-auto max-w-5xl px-4 sm:px-6">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">Track, return or buy things again.</p>
</div>
<label className="relative w-full sm:w-72">
<span className="sr-only">Search orders</span>
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden />
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search by order # or product" className={cn("h-10 w-full rounded-full border bg-background pl-9 pr-4 text-sm", ring)} />
</label>
</div>
<div role="tablist" aria-label="Order filters" className="-mx-4 mt-6 flex gap-1 overflow-x-auto border-b px-4 [scrollbar-width:none] sm:mx-0 sm:px-0">
{TABS.map((t) => {
const n = orders.filter((o) => t.match(o.status)).length;
const on = tab === t.id;
return (
<button key={t.id} role="tab" aria-selected={on} onClick={() => setTab(t.id)} className={cn("relative flex shrink-0 items-center gap-2 whitespace-nowrap px-3 pb-3 pt-1 text-sm font-medium transition", on ? "text-foreground" : "text-muted-foreground hover:text-foreground", ring)}>
{t.label}
<span className="rounded-full bg-muted px-1.5 text-[11px] tabular-nums">{n}</span>
{on && <motion.span layoutId="oh-tab" className="absolute inset-x-0 -bottom-px h-0.5 rounded-full bg-foreground" />}
</button>
);
})}
</div>
<LayoutGroup>
<ul className="mt-6 space-y-4">
<AnimatePresence initial={false} mode="popLayout">
{list.map((o) => {
const isOpen = open === o.id;
const total = totalOf(o);
return (
<motion.li key={o.id} layout={!reduce} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, scale: 0.98 }} className="overflow-hidden rounded-2xl border bg-card">
<button type="button" aria-expanded={isOpen} aria-controls={`oh-${o.id}`} onClick={() => setOpen(isOpen ? null : o.id)} className={cn("flex w-full items-center gap-3 p-4 text-left sm:gap-6 sm:p-5", ring, "focus-visible:ring-offset-0 focus-visible:ring-inset")}>
<div className="flex shrink-0 -space-x-3">
{o.lines.slice(0, 3).map((l) => (
<span key={l.id} className="size-10 rounded-xl border-2 border-card bg-muted p-0.5 sm:size-12">
<ProductArt kind={l.kind} color={l.hex} accent={l.accent} />
</span>
))}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold">{o.number}</p>
<p className="text-xs text-muted-foreground">
{o.date} · {o.lines.reduce((s, l) => s + l.qty, 0)} {o.lines.reduce((s, l) => s + l.qty, 0) === 1 ? "item" : "items"}
</p>
<StatusBadge status={o.status} className="mt-1.5 sm:hidden" />
</div>
<StatusBadge status={o.status} className="hidden sm:inline-flex" />
<span className="shrink-0 text-right text-sm font-semibold tabular-nums sm:w-20">{fmt.format(total)}</span>
<motion.span animate={{ rotate: isOpen ? 180 : 0 }} className="shrink-0">
<ChevronDown className="size-4 text-muted-foreground" aria-hidden />
</motion.span>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div id={`oh-${o.id}`} initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: reduce ? 0 : 0.3, ease: [0.4, 0, 0.2, 1] }} className="overflow-hidden">
<div className="grid gap-6 border-t p-4 sm:p-5 md:grid-cols-[1fr_280px]">
<div className="min-w-0">
{o.eta && o.status !== "delivered" && o.status !== "cancelled" && (
<div className="mb-5 flex items-center gap-3 rounded-xl bg-primary/8 p-3 text-sm">
<span className="grid size-9 place-items-center rounded-lg bg-primary text-primary-foreground">
<Truck className="size-4" aria-hidden />
</span>
<span>
<span className="block text-xs text-muted-foreground">{o.status === "processing" ? "Estimated dispatch" : "Arriving"}</span>
<span className="font-semibold">{o.eta}</span>
</span>
</div>
)}
<ul className="divide-y">
{o.lines.map((l) => (
<li key={l.id} className="flex items-center gap-3 py-3 first:pt-0">
<span className="size-14 shrink-0 rounded-lg bg-muted p-1">
<ProductArt kind={l.kind} color={l.hex} accent={l.accent} label={l.label} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{l.name}</span>
<span className="block text-xs text-muted-foreground">
{l.variant} · Qty {l.qty}
</span>
</span>
<span className="text-sm tabular-nums">{fmt.format(l.price * l.qty)}</span>
</li>
))}
</ul>
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-3 gap-y-2 border-t pt-4 text-xs">
<dt className="flex items-center gap-1.5 text-muted-foreground">
<Home className="size-3.5" aria-hidden /> Ship to
</dt>
<dd>{o.address}</dd>
<dt className="flex items-center gap-1.5 text-muted-foreground">
<CreditCard className="size-3.5" aria-hidden /> Payment
</dt>
<dd>{o.payment}</dd>
{o.trackingNo && (
<>
<dt className="flex items-center gap-1.5 text-muted-foreground">
<Package className="size-3.5" aria-hidden /> Tracking
</dt>
<dd className="font-mono">
{o.carrier} · {o.trackingNo}
</dd>
</>
)}
{o.returnCode && (
<>
<dt className="flex items-center gap-1.5 text-muted-foreground">
<Undo2 className="size-3.5" aria-hidden /> Return
</dt>
<dd className="font-mono font-semibold">{o.returnCode}</dd>
</>
)}
</dl>
<div className="mt-5 flex flex-wrap gap-2">
{o.status !== "cancelled" && (
<ActionButton
primary
onClick={() => {
onReorder?.(o);
setToast(`${o.lines.reduce((s, l) => s + l.qty, 0)} items from ${o.number} added to your cart`);
}}
>
<ShoppingCart className="size-4" aria-hidden /> Buy again
</ActionButton>
)}
<ActionButton onClick={() => invoice(o)}>
<Download className="size-4" aria-hidden /> Invoice
</ActionButton>
{o.trackingNo && o.status !== "delivered" && (
<ActionButton onClick={() => onTrack?.(o)}>
<MapPin className="size-4" aria-hidden /> Live tracking
</ActionButton>
)}
{o.status === "delivered" && (
<ActionButton onClick={() => setReturning(o)}>
<RotateCcw className="size-4" aria-hidden /> Return items
</ActionButton>
)}
</div>
</div>
<Timeline events={o.events} cancelled={o.status === "cancelled"} />
</div>
</motion.div>
)}
</AnimatePresence>
</motion.li>
);
})}
</AnimatePresence>
</ul>
</LayoutGroup>
{list.length === 0 && (
<div className="mt-6 flex flex-col items-center rounded-2xl border border-dashed py-14 text-center">
<PackageCheck className="size-8 text-muted-foreground" aria-hidden />
<p className="mt-3 font-medium">No orders here</p>
<p className="text-sm text-muted-foreground">Try a different tab or search.</p>
</div>
)}
</div>
<AnimatePresence>
{returning && (
<ReturnDialog
order={returning}
fmt={fmt}
onClose={() => setReturning(null)}
onSubmit={(req) => {
const code = onReturnRequest?.(req) || `RMA-${returning.number.slice(-4)}${req.lineIds.length}`;
setOrders((os) =>
os.map((o) =>
o.id === req.orderId
? {
...o,
status: "return-requested",
returnCode: code,
events: [...o.events, { label: "Return requested", detail: `${req.reason} · ${req.resolution}`, time: "Just now", done: true }],
}
: o,
),
);
return code;
}}
/>
)}
</AnimatePresence>
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-[60] flex justify-center px-4" aria-live="polite">
<AnimatePresence>
{toast && (
<motion.div initial={{ y: 30, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: 20, opacity: 0 }} className="flex items-center gap-2.5 rounded-2xl bg-foreground px-4 py-3 text-sm text-background shadow-2xl">
<span className="grid size-5 place-items-center rounded-full bg-emerald-500 text-white">
<Check className="size-3" aria-hidden />
</span>
{toast}
</motion.div>
)}
</AnimatePresence>
</div>
</section>
);
}
function StatusBadge({ status, className }: { status: OrderStatus; className?: string }) {
const reduce = useReducedMotion();
const st = STATUS[status];
return (
<span className={cn("inline-flex w-fit shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 py-1 text-xs font-semibold", st.cls, className)}>
<span className="relative flex size-1.5">
{(status === "out-for-delivery" || status === "processing") && !reduce && <span className={cn("absolute inset-0 animate-ping rounded-full opacity-70", st.dot)} />}
<span className={cn("relative size-1.5 rounded-full", st.dot)} />
</span>
{st.label}
</span>
);
}
function ActionButton({ primary, onClick, children }: { primary?: boolean; onClick: () => void; children: React.ReactNode }) {
return (
<button
type="button"
onClick={onClick}
className={cn("flex h-9 items-center gap-2 rounded-lg px-3.5 text-sm font-medium transition", primary ? "bg-foreground text-background hover:bg-foreground/85" : "border hover:bg-muted", ring)}
>
{children}
</button>
);
}
function Timeline({ events, cancelled }: { events: TrackingEvent[]; cancelled: boolean }) {
const reduce = useReducedMotion();
const last = events.map((e) => e.done).lastIndexOf(true);
return (
<div>
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">Tracking</p>
<ol className="relative">
{events.map((e, i) => {
const current = i === last && i < events.length - 1 && !cancelled;
return (
<motion.li
key={`${e.label}-${i}`}
initial={reduce ? false : { opacity: 0, x: -6 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 + i * 0.08 }}
className="relative flex gap-3 pb-5 last:pb-0"
>
{i < events.length - 1 && (
<span className="absolute bottom-0 left-[11px] top-6 w-0.5 bg-muted" aria-hidden>
{events[i + 1].done && (
<motion.span
className={cn("absolute inset-0 origin-top", cancelled ? "bg-muted-foreground" : "bg-emerald-500")}
initial={{ scaleY: reduce ? 1 : 0 }}
animate={{ scaleY: 1 }}
transition={{ duration: 0.3, delay: 0.2 + i * 0.12 }}
/>
)}
</span>
)}
<span className={cn("relative z-10 grid size-6 shrink-0 place-items-center rounded-full border-2 bg-card", e.done ? (cancelled && i === events.length - 1 ? "border-muted-foreground bg-muted-foreground text-card" : "border-emerald-500 bg-emerald-500 text-white") : "border-muted")}>
{e.done ? cancelled && i === events.length - 1 ? <X className="size-3" strokeWidth={3} aria-hidden /> : <Check className="size-3" strokeWidth={3} aria-hidden /> : <CircleDot className="size-3 text-muted-foreground/50" aria-hidden />}
{current && !reduce && <span className="absolute -inset-1 animate-ping rounded-full border-2 border-emerald-500/60" aria-hidden />}
</span>
<span className="min-w-0 pt-0.5">
<span className={cn("block text-sm font-medium", !e.done && "text-muted-foreground")}>{e.label}</span>
{e.detail && <span className="block text-xs text-muted-foreground">{e.detail}</span>}
<span className="block text-[11px] text-muted-foreground/80">{e.time}</span>
</span>
</motion.li>
);
})}
</ol>
</div>
);
}
function ReturnDialog({ order, fmt, onClose, onSubmit }: { order: Order; fmt: Intl.NumberFormat; onClose: () => void; onSubmit: (r: ReturnRequest) => string }) {
const ref = React.useRef<HTMLDivElement>(null);
const [step, setStep] = React.useState<1 | 2 | 3>(1);
const [sel, setSel] = React.useState<string[]>([]);
const [reason, setReason] = React.useState("");
const [resolution, setResolution] = React.useState<ReturnRequest["resolution"]>("refund");
const [code, setCode] = React.useState("");
const [err, setErr] = React.useState("");
const refund = order.lines.filter((l) => sel.includes(l.id)).reduce((s, l) => s + l.price * l.qty, 0);
React.useEffect(() => {
const prev = document.activeElement as HTMLElement | null;
ref.current?.querySelector<HTMLElement>("input, button")?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
if (e.key === "Tab" && ref.current) {
const f = [...ref.current.querySelectorAll<HTMLElement>("button:not([disabled]), input, select")];
if (e.shiftKey && document.activeElement === f[0]) {
e.preventDefault();
f[f.length - 1]?.focus();
} else if (!e.shiftKey && document.activeElement === f[f.length - 1]) {
e.preventDefault();
f[0]?.focus();
}
}
};
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
prev?.focus();
};
}, [onClose]);
const next = () => {
if (step === 1) {
if (!sel.length) return setErr("Select at least one item to return.");
setErr("");
setStep(2);
} else if (step === 2) {
if (!reason) return setErr("Choose a reason.");
setErr("");
setCode(onSubmit({ orderId: order.id, lineIds: sel, reason, resolution }));
setStep(3);
}
};
return (
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center sm:p-6">
<motion.div className="absolute inset-0 bg-black/55 backdrop-blur-sm" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} aria-hidden />
<motion.div
ref={ref}
role="dialog"
aria-modal="true"
aria-labelledby="oh-return-title"
initial={{ y: 40, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 40, opacity: 0 }}
transition={{ type: "spring", stiffness: 360, damping: 34 }}
className="relative w-full max-w-lg rounded-t-3xl border bg-background p-5 shadow-2xl sm:rounded-3xl sm:p-6"
>
<div className="flex items-start justify-between gap-4">
<div>
<h3 id="oh-return-title" className="text-lg font-semibold">
{step === 3 ? "Return requested" : `Return items · ${order.number}`}
</h3>
{step < 3 && <p className="text-xs text-muted-foreground">Step {step} of 2 · Free returns within 30 days</p>}
</div>
<button type="button" onClick={onClose} aria-label="Close" className={cn("grid size-8 place-items-center rounded-full hover:bg-muted", ring)}>
<X className="size-4" />
</button>
</div>
{step < 3 && (
<div className="mt-3 h-1 overflow-hidden rounded-full bg-muted">
<motion.div className="h-full rounded-full bg-primary" animate={{ width: step === 1 ? "50%" : "100%" }} />
</div>
)}
<AnimatePresence mode="wait" initial={false}>
<motion.div key={step} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.2 }} className="mt-5">
{step === 1 && (
<fieldset>
<legend className="text-sm font-medium">Which items?</legend>
<ul className="mt-3 space-y-2">
{order.lines.map((l) => {
const on = sel.includes(l.id);
return (
<li key={l.id}>
<label className={cn("flex cursor-pointer items-center gap-3 rounded-xl border p-3 transition", on ? "border-primary bg-primary/5" : "hover:bg-muted/50")}>
<input type="checkbox" checked={on} onChange={() => setSel((s) => (on ? s.filter((x) => x !== l.id) : [...s, l.id]))} className="size-4 accent-[var(--primary)]" />
<span className="size-11 shrink-0 rounded-lg bg-muted p-0.5">
<ProductArt kind={l.kind} color={l.hex} accent={l.accent} />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{l.name}</span>
<span className="text-xs text-muted-foreground">Qty {l.qty}</span>
</span>
<span className="text-sm tabular-nums">{fmt.format(l.price * l.qty)}</span>
</label>
</li>
);
})}
</ul>
</fieldset>
)}
{step === 2 && (
<div className="space-y-5">
<div>
<label htmlFor="oh-reason" className="text-sm font-medium">
Reason
</label>
<div className="relative mt-1.5">
<select id="oh-reason" value={reason} onChange={(e) => setReason(e.target.value)} className={cn("h-11 w-full appearance-none rounded-lg border bg-background px-3 pr-9 text-sm", ring)}>
<option value="">Select a reason…</option>
{REASONS.map((r) => (
<option key={r}>{r}</option>
))}
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden />
</div>
</div>
<fieldset>
<legend className="text-sm font-medium">What would you like?</legend>
<div className="mt-2 grid grid-cols-3 gap-2">
{(["refund", "exchange", "credit"] as const).map((r) => (
<label key={r} className="cursor-pointer">
<input type="radio" name="oh-res" className="peer sr-only" checked={resolution === r} onChange={() => setResolution(r)} />
<span className="flex h-11 items-center justify-center rounded-xl border text-sm font-medium capitalize transition peer-checked:border-foreground peer-checked:bg-foreground peer-checked:text-background peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background">
{r === "credit" ? "Store credit" : r}
</span>
</label>
))}
</div>
</fieldset>
<p className="rounded-xl bg-muted/60 p-3 text-sm">
Estimated {resolution === "credit" ? "credit (+10% bonus)" : resolution}: <span className="font-semibold">{fmt.format(resolution === "credit" ? refund * 1.1 : refund)}</span>
</p>
</div>
)}
{step === 3 && (
<div className="flex flex-col items-center py-4 text-center">
<motion.span initial={{ scale: 0, rotate: -45 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: "spring", stiffness: 400, damping: 15 }} className="grid size-14 place-items-center rounded-full bg-emerald-500 text-white">
<Check className="size-7" strokeWidth={3} aria-hidden />
</motion.span>
<p className="mt-4 text-sm text-muted-foreground">Your return code</p>
<p className="mt-1 rounded-lg border border-dashed px-4 py-2 font-mono text-lg font-semibold tracking-wider">{code}</p>
<p className="mt-3 max-w-xs text-sm text-muted-foreground">Drop the parcel at any Northwind point within 14 days. We've emailed a prepaid label.</p>
</div>
)}
</motion.div>
</AnimatePresence>
{err && (
<p role="alert" className="mt-3 text-xs text-destructive">
{err}
</p>
)}
<div className="mt-6 flex justify-end gap-2">
{step === 2 && (
<button type="button" onClick={() => setStep(1)} className={cn("h-10 rounded-lg px-4 text-sm font-medium hover:bg-muted", ring)}>
Back
</button>
)}
{step < 3 ? (
<button type="button" onClick={next} className={cn("h-10 rounded-lg bg-primary px-5 text-sm font-semibold text-primary-foreground hover:bg-primary/90", ring)}>
{step === 1 ? `Continue${sel.length ? ` (${sel.length})` : ""}` : "Submit return"}
</button>
) : (
<button type="button" onClick={onClose} className={cn("h-10 rounded-lg bg-foreground px-5 text-sm font-semibold text-background", ring)}>
Done
</button>
)}
</div>
</motion.div>
</div>
);
}