Initial commit: Craft2Prints with Stages 1-3

This commit is contained in:
Andymick
2026-07-15 18:09:59 +01:00
commit 41937ffc15
158 changed files with 16054 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useCart } from '@/lib/cartStore';
import Price from '@/components/Price';
export default function CartPage() {
const [mounted, setMounted] = useState(false);
const [checkingOut, setCheckingOut] = useState(false);
const [checkoutError, setCheckoutError] = useState<string | null>(null);
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) => item.designJson.front.length > 0 || item.designJson.back.length > 0);
useEffect(() => setMounted(true), []);
const handleCheckout = async () => {
setCheckingOut(true);
setCheckoutError(null);
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
});
const data = await res.json();
if (!res.ok || !data.url) {
throw new Error(data.error ?? 'Something went wrong starting checkout.');
}
window.location.href = data.url;
} catch (err) {
setCheckoutError(err instanceof Error ? err.message : 'Something went wrong.');
setCheckingOut(false);
}
};
if (!mounted) return null; // avoid a hydration mismatch on first paint
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) => (
<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 }}
/>
{item.color}
</span>
{item.size && <span>Size {item.size}</span>}
</div>
{(item.designJson.front.length > 0 || item.designJson.back.length > 0) && (
<div className="mt-1">
<p className="text-xs text-muted">
Custom design
{item.designJson.front.length > 0 && item.designJson.back.length > 0
? ' — front & back'
: item.designJson.back.length > 0
? ' — back'
: ' — front'}
</p>
<div className="mt-1 flex gap-3">
{item.designJson.front.length > 0 && 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>
)}
{item.designJson.back.length > 0 && 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">
<Price cents={item.unitPrice * item.quantity} />
</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
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">
<div className="mb-4 rounded border border-splash-blue/40 bg-splash-blue/5 px-3 py-2 text-xs text-muted">
Your cart will be saved. You&apos;ll need to log in again to complete your order.
</div>
<button
onClick={handleCheckout}
disabled={checkingOut}
className="w-full bg-clay px-6 py-3 text-sm font-medium text-paper transition-colors hover:bg-clay-dark disabled:opacity-60"
>
{checkingOut ? 'Redirecting to checkout…' : 'Checkout'}
</button>
{checkoutError && <p className="mt-2 text-center text-xs text-splash-pink">{checkoutError}</p>}
<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>
);
}