trptk/components/account/OrdersTab.tsx
2026-02-24 17:14:07 +01:00

274 lines
9.1 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import {
HiOutlineChevronDoubleLeft,
HiOutlineChevronLeft,
HiOutlineChevronRight,
HiOutlineChevronDoubleRight,
} from "react-icons/hi2";
import { IconButton } from "@/components/IconButton";
import { formatDisplayName } from "@/lib/variants";
const PAGE_SIZE = 5;
type OrderItem = {
id: string;
title: string;
product_title: string;
variant_title: string;
variant_sku?: string;
variant?: { sku?: string };
quantity: number;
unit_price: number;
total: number;
subtotal?: number;
tax_total?: number;
thumbnail: string | null;
};
type Order = {
id: string;
display_id: number;
created_at: string;
total: number;
subtotal?: number;
shipping_total?: number;
tax_total?: number;
currency_code: string;
items: OrderItem[];
};
function formatPrice(amount: number, currencyCode: string) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currencyCode,
minimumFractionDigits: 2,
}).format(amount);
}
function variantLabel(item: OrderItem): string {
const sku = item.variant_sku ?? item.variant?.sku;
if (sku) {
const friendly = formatDisplayName(sku);
if (friendly) return friendly;
}
return item.variant_title || item.title;
}
export function OrdersTab() {
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
useEffect(() => {
async function fetchOrders() {
try {
const res = await fetch("/api/account/orders");
if (res.ok) {
const data = await res.json();
// Sort newest first (fallback in case API doesn't sort)
const sorted = [...(data.orders ?? [])].sort(
(a: Order, b: Order) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
);
setOrders(sorted);
}
} catch {
// Silently fail — empty state will show
} finally {
setLoading(false);
}
}
fetchOrders();
}, []);
if (loading) {
return (
<p className="my-10 text-center text-lightsec md:my-12 lg:my-20 dark:text-darksec">
Loading orders
</p>
);
}
if (orders.length === 0) {
return (
<p className="py-12 text-center text-lightsec dark:text-darksec">
You haven&apos;t placed any orders yet.
</p>
);
}
const totalPages = Math.max(1, Math.ceil(orders.length / PAGE_SIZE));
const safePage = Math.min(page, totalPages);
const start = (safePage - 1) * PAGE_SIZE;
const paginatedOrders = orders.slice(start, start + PAGE_SIZE);
return (
<div>
<div className="mb-4">
<p className="text-sm text-lightsec dark:text-darksec">
{orders.length} order{orders.length !== 1 ? "s" : ""}
</p>
</div>
<div className="divide-y divide-lighttext/10 dark:divide-darktext/10">
{paginatedOrders.map((order) => {
const currency = order.currency_code ?? "eur";
const subtotal =
order.subtotal ?? order.items?.reduce((sum, i) => sum + (i.subtotal ?? 0), 0) ?? 0;
const taxTotal =
order.tax_total ?? order.items?.reduce((sum, i) => sum + (i.tax_total ?? 0), 0) ?? 0;
const shippingTotal =
order.shipping_total ?? Math.max(0, order.total - subtotal - taxTotal);
return (
<div key={order.id} className="py-12 first:pt-0 last:pb-0">
{/* Order header */}
<div className="flex items-center justify-between">
<div>
<span className="font-silkasb">Order #{order.display_id}</span>
<span className="ml-3 text-sm text-lightsec dark:text-darksec">
{new Date(order.created_at).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
})}
</span>
</div>
</div>
{/* Item cards */}
{order.items?.length > 0 && (
<div className="mt-6">
<div className="grid gap-4 md:grid-cols-2">
{order.items.map((item) => (
<div
key={item.id}
className="relative z-10 flex items-center gap-3 overflow-hidden rounded-xl bg-lightbg shadow-lg ring-1 ring-lightline dark:bg-darkbg dark:ring-darkline"
>
{/* Thumbnail */}
<div className="relative aspect-square w-21 flex-shrink-0 overflow-hidden rounded-xl bg-lightline dark:bg-darkline">
{item.thumbnail ? (
<Image
src={item.thumbnail}
alt={item.product_title ?? item.title}
fill
sizes="64px"
className="object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-xs text-lightsec dark:text-darksec">
No img
</div>
)}
</div>
{/* Info */}
<div className="min-w-0 flex-1 py-3 pr-3">
<p className="truncate font-silkasb text-sm">
{item.product_title ?? item.title}
</p>
<p className="truncate text-xs text-lightsec dark:text-darksec">
{variantLabel(item)}
{item.quantity >= 2 && (
<span className="ml-1 font-silka text-lightsec dark:text-darksec">
x {item.quantity}
</span>
)}
</p>
</div>
</div>
))}
</div>
{/* Price breakdown */}
<div className="mt-7 space-y-1">
<div className="flex justify-between text-sm">
<span className="text-lightsec dark:text-darksec">Subtotal</span>
<span>{formatPrice(subtotal, currency)}</span>
</div>
{shippingTotal > 0 && (
<div className="flex justify-between text-sm">
<span className="text-lightsec dark:text-darksec">Shipping</span>
<span>{formatPrice(shippingTotal, currency)}</span>
</div>
)}
{taxTotal > 0 && (
<div className="flex justify-between text-sm">
<span className="text-lightsec dark:text-darksec">Tax</span>
<span>{formatPrice(taxTotal, currency)}</span>
</div>
)}
{taxTotal > 0 && (
<div className="flex justify-between font-silkasb text-sm">
<span className="">Total</span>
<span className="">{formatPrice(order.total, currency)}</span>
</div>
)}
</div>
</div>
)}
</div>
);
})}
</div>
{totalPages > 1 && (
<Pagination page={safePage} totalPages={totalPages} onPageChange={setPage} />
)}
</div>
);
}
// ── Pagination ──────────────────────────────────────────────────────
function Pagination({
page,
totalPages,
onPageChange,
}: {
page: number;
totalPages: number;
onPageChange: (p: number) => void;
}) {
const hasPrev = page > 1;
const hasNext = page < totalPages;
return (
<nav aria-label="Pagination" className="mt-12 flex items-center justify-between">
<div className="text-sm text-lightsec dark:text-darksec">
Page {page} of {totalPages}
</div>
<div className="flex gap-3 text-lg">
<IconButton disabled={!hasPrev} onClick={() => onPageChange(1)} aria-label="First page">
<HiOutlineChevronDoubleLeft />
</IconButton>
<IconButton
disabled={!hasPrev}
onClick={() => onPageChange(page - 1)}
aria-label="Previous page"
>
<HiOutlineChevronLeft />
</IconButton>
<IconButton
disabled={!hasNext}
onClick={() => onPageChange(page + 1)}
aria-label="Next page"
>
<HiOutlineChevronRight />
</IconButton>
<IconButton
disabled={!hasNext}
onClick={() => onPageChange(totalPages)}
aria-label="Last page"
>
<HiOutlineChevronDoubleRight />
</IconButton>
</div>
</nav>
);
}