Files
craft2prints/src/app/cart/page.tsx
T
Andymick 62ae2755f0 Fix price display issues and layout in design review page
- 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
2026-07-18 19:46:11 +01:00

211 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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&apos;s shown in your preview above spelling, wording, and
approximate placement included. Once your order&apos;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&apos;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>
);
}