- Add defensive checks in cart store to handle missing/NaN unitPrice values - Display '—' for items with invalid prices instead of NaN - Fix design review page grid layout to align buttons at top of page - Ensure buttons don't get pushed below images on review page
211 lines
7.8 KiB
TypeScript
211 lines
7.8 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import Link from 'next/link';
|
||
import { useCart } from '@/lib/cartStore';
|
||
import { getColorName } from '@/lib/colorNames';
|
||
import Price from '@/components/Price';
|
||
|
||
export default function CartPage() {
|
||
const [mounted, setMounted] = useState(false);
|
||
const items = useCart((s) => s.items);
|
||
const removeItem = useCart((s) => s.removeItem);
|
||
const setQuantity = useCart((s) => s.setQuantity);
|
||
const total = useCart((s) => s.total());
|
||
const hasCustomDesign = items.some((item) => {
|
||
try {
|
||
const design = typeof item.designJson === 'string' ? JSON.parse(item.designJson) : item.designJson;
|
||
return (design.front?.length ?? 0) > 0 || (design.back?.length ?? 0) > 0;
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
|
||
useEffect(() => setMounted(true), []);
|
||
|
||
const handleCheckout = () => {
|
||
// Set flag so LogoutOnUnload doesn't clear cart during navigation to checkout
|
||
if (typeof window !== 'undefined') {
|
||
sessionStorage.setItem('isCheckingOut', 'true');
|
||
console.log('💳 Cart checkout button clicked - set isCheckingOut flag');
|
||
}
|
||
window.location.href = '/checkout';
|
||
};
|
||
|
||
if (!mounted) return null;
|
||
|
||
if (items.length === 0) {
|
||
return (
|
||
<div className="mx-auto max-w-2xl px-6 py-20 text-center">
|
||
<h1 className="font-display text-4xl">Your bag</h1>
|
||
<p className="mt-4 text-muted">Nothing in here yet.</p>
|
||
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||
<Link
|
||
href="/made-to-order"
|
||
className="bg-clay px-6 py-3 text-sm font-medium text-paper hover:bg-clay-dark"
|
||
>
|
||
Start designing
|
||
</Link>
|
||
<Link
|
||
href="/products"
|
||
className="border-2 border-ink px-6 py-3 text-sm font-medium text-ink hover:border-clay hover:text-clay"
|
||
>
|
||
Shop ready-made
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="mx-auto max-w-4xl px-6 py-12">
|
||
<h1 className="font-display text-4xl">Your bag</h1>
|
||
|
||
<div className="mt-8 divide-y divide-line border-t border-line">
|
||
{items.map((item) => {
|
||
const design = typeof item.designJson === 'string' ? JSON.parse(item.designJson) : item.designJson;
|
||
const hasFront = (design.front?.length ?? 0) > 0;
|
||
const hasBack = (design.back?.length ?? 0) > 0;
|
||
|
||
return (
|
||
<div key={item.cartItemId} className="flex flex-wrap items-center gap-4 py-6">
|
||
<div className="h-24 w-24 shrink-0 border border-line bg-surface p-1">
|
||
{item.placementPreviewUrl || item.designPreviewUrl ? (
|
||
<img
|
||
src={item.placementPreviewUrl ?? item.designPreviewUrl}
|
||
alt={item.name}
|
||
className="h-full w-full object-contain"
|
||
/>
|
||
) : (
|
||
<div className="flex h-full w-full items-center justify-center text-xs text-muted">No preview</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="min-w-[160px] flex-1">
|
||
<p className="font-display text-lg">{item.name}</p>
|
||
<div className="mt-1 flex items-center gap-3 text-sm text-muted">
|
||
<span className="flex items-center gap-1.5">
|
||
<span
|
||
className="h-3.5 w-3.5 rounded-full border border-line"
|
||
style={{ backgroundColor: item.color }}
|
||
title={item.color}
|
||
/>
|
||
<span>{getColorName(item.color)}</span>
|
||
</span>
|
||
{item.size && <span>Size {item.size}</span>}
|
||
</div>
|
||
{(hasFront || hasBack) && (
|
||
<div className="mt-1">
|
||
<p className="text-xs text-muted">
|
||
Custom design
|
||
{hasFront && hasBack
|
||
? ' — front & back'
|
||
: hasBack
|
||
? ' — back'
|
||
: ' — front'}
|
||
</p>
|
||
<div className="mt-1 flex gap-3">
|
||
{hasFront && item.designPreviewUrl && (
|
||
<a
|
||
href={item.designPreviewUrl}
|
||
download={`${item.slug}-front-print.png`}
|
||
className="text-xs text-clay hover:text-clay-dark"
|
||
>
|
||
Download front PNG
|
||
</a>
|
||
)}
|
||
{hasBack && item.designPreviewUrlBack && (
|
||
<a
|
||
href={item.designPreviewUrlBack}
|
||
download={`${item.slug}-back-print.png`}
|
||
className="text-xs text-clay hover:text-clay-dark"
|
||
>
|
||
Download back PNG
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center border border-ink">
|
||
<button
|
||
onClick={() => setQuantity(item.cartItemId, item.quantity - 1)}
|
||
className="px-3 py-2 text-sm"
|
||
aria-label="Decrease quantity"
|
||
>
|
||
−
|
||
</button>
|
||
<span className="w-10 text-center font-mono text-sm">{item.quantity}</span>
|
||
<button
|
||
onClick={() => setQuantity(item.cartItemId, item.quantity + 1)}
|
||
className="px-3 py-2 text-sm"
|
||
aria-label="Increase quantity"
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
|
||
<div className="w-24 text-right font-mono text-sm">
|
||
{Number.isFinite(item.unitPrice) ? (
|
||
<Price cents={item.unitPrice * item.quantity} />
|
||
) : (
|
||
<span className="text-muted">—</span>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => removeItem(item.cartItemId)}
|
||
className="tag-label text-muted hover:text-splash-pink"
|
||
>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{hasCustomDesign && (
|
||
<div className="mt-8 border border-splash-orange bg-splash-orange/5 px-4 py-3 text-sm">
|
||
<p className="font-medium text-ink">Please check your design before checking out</p>
|
||
<p className="mt-1 text-muted">
|
||
We print exactly what's shown in your preview above — spelling, wording, and
|
||
approximate placement included. Once your order's placed, that design goes straight to print,
|
||
so take a moment to make sure everything looks right.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-8 flex flex-col items-end gap-4">
|
||
<div className="flex items-baseline gap-3">
|
||
<span className="tag-label">Subtotal</span>
|
||
<span className="font-mono text-xl">
|
||
<Price cents={total} />
|
||
</span>
|
||
</div>
|
||
|
||
<div className="w-full max-w-xs">
|
||
<button
|
||
onClick={handleCheckout}
|
||
className="w-full bg-clay px-6 py-3 text-sm font-medium text-paper transition-colors hover:bg-clay-dark"
|
||
>
|
||
Checkout
|
||
</button>
|
||
<p className="mt-2 text-center text-xs text-muted">You'll enter payment details on the next step.</p>
|
||
<p className="mt-1 text-center text-xs text-muted">
|
||
By placing an order you agree to our{' '}
|
||
<Link href="/terms" className="text-clay hover:text-clay-dark">
|
||
Terms
|
||
</Link>{' '}
|
||
and{' '}
|
||
<Link href="/returns" className="text-clay hover:text-clay-dark">
|
||
Returns policy
|
||
</Link>
|
||
.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|