Initial commit: Craft2Prints with Stages 1-3
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ProductMockup from './ProductMockup';
|
||||
import Price from './Price';
|
||||
import { useCart } from '@/lib/cartStore';
|
||||
import type { ProductDTO } from '@/lib/types';
|
||||
|
||||
export default function StandardProductView({ product }: { product: ProductDTO }) {
|
||||
const [color, setColor] = useState(product.colors[0]);
|
||||
const [size, setSize] = useState<string | null>(product.sizes[0] ?? null);
|
||||
const [view, setView] = useState<'front' | 'back'>('front');
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [justAdded, setJustAdded] = useState(false);
|
||||
const addItem = useCart((s) => s.addItem);
|
||||
|
||||
const hasBack = Boolean(product.imageUrlBack);
|
||||
|
||||
function renderPhoto(v: 'front' | 'back') {
|
||||
const colorPhoto = v === 'front' ? product.colorPhotos[color] : product.colorPhotosBack[color];
|
||||
if (colorPhoto) {
|
||||
return <img src={colorPhoto} alt={`${product.name} in ${color}`} className="h-full w-full object-cover" />;
|
||||
}
|
||||
|
||||
const activePhoto = v === 'front' ? product.imageUrl : product.imageUrlBack;
|
||||
if (!activePhoto) {
|
||||
return <ProductMockup mockup={product.mockup} color={color} />;
|
||||
}
|
||||
if (product.photoRecolorable) {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<img
|
||||
src={activePhoto}
|
||||
alt={product.name}
|
||||
className="h-full w-full object-cover"
|
||||
style={{ filter: 'grayscale(1) brightness(2.1) contrast(1.15)' }}
|
||||
/>
|
||||
<div className="absolute inset-0" style={{ backgroundColor: color, mixBlendMode: 'multiply' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <img src={activePhoto} alt={product.name} className="h-full w-full object-cover" />;
|
||||
}
|
||||
|
||||
const handleAddToBag = () => {
|
||||
addItem({
|
||||
cartItemId: uuid(),
|
||||
productId: product.id,
|
||||
slug: product.slug,
|
||||
name: product.name,
|
||||
mockup: product.mockup,
|
||||
color,
|
||||
size,
|
||||
quantity,
|
||||
unitPrice: product.effectivePrice,
|
||||
designJson: { front: [], back: [] },
|
||||
designPreviewUrl: product.colorPhotos[color] ?? product.imageUrl ?? '',
|
||||
designPreviewUrlBack: product.imageUrlBack ?? null,
|
||||
placementPreviewUrl: product.colorPhotos[color] ?? product.imageUrl ?? null,
|
||||
placementPreviewUrlBack: product.imageUrlBack ?? null,
|
||||
});
|
||||
setJustAdded(true);
|
||||
setTimeout(() => setJustAdded(false), 2200);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-10 lg:grid-cols-[560px_1fr]">
|
||||
{/* Photo */}
|
||||
<div className="w-full min-w-0 max-w-[560px]">
|
||||
{hasBack && (
|
||||
<div className="mb-3 inline-flex border border-line">
|
||||
<button
|
||||
onClick={() => setView('front')}
|
||||
className={`px-4 py-1.5 text-sm ${view === 'front' ? 'bg-ink text-paper' : 'hover:text-clay'}`}
|
||||
>
|
||||
Front
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('back')}
|
||||
className={`px-4 py-1.5 text-sm ${view === 'back' ? 'bg-ink text-paper' : 'hover:text-clay'}`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative aspect-square w-full border border-line bg-surface">
|
||||
{renderPhoto(view)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex flex-col gap-8">
|
||||
<div>
|
||||
<h1 className="font-display text-4xl">{product.name}</h1>
|
||||
<p className="mt-2 text-muted">{product.description}</p>
|
||||
<p className="mt-4 flex items-baseline gap-2 font-mono text-lg">
|
||||
{product.onSale && product.originalPrice != null && (
|
||||
<Price cents={product.originalPrice} className="text-base text-muted line-through" />
|
||||
)}
|
||||
<Price cents={product.effectivePrice} className={product.onSale ? 'text-splash-pink' : undefined} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="tag-label mb-3">Color</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{product.colors.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setColor(c)}
|
||||
aria-label={`Choose color ${c}`}
|
||||
className={`h-9 w-9 rounded-full border transition-transform ${
|
||||
color === c ? 'scale-110 border-ink ring-2 ring-ink ring-offset-2 ring-offset-paper' : 'border-line'
|
||||
}`}
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{product.imageUrl && !product.photoRecolorable && Object.keys(product.colorPhotos).length === 0 && (
|
||||
<p className="mt-2 text-xs text-muted">
|
||||
This product uses a real photo, so the picture won't change color — your pick is
|
||||
still saved with the order.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product.sizes.length > 0 && (
|
||||
<div>
|
||||
<p className="tag-label mb-3">Size</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{product.sizes.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setSize(s)}
|
||||
className={`min-w-[44px] border px-3 py-2 text-sm transition-colors ${
|
||||
size === s
|
||||
? 'border-clay bg-clay text-paper'
|
||||
: 'border-line text-ink hover:border-clay hover:text-clay'
|
||||
}`}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center border border-ink">
|
||||
<button
|
||||
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
|
||||
className="px-3 py-2 text-sm"
|
||||
aria-label="Decrease quantity"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span className="w-10 text-center font-mono text-sm">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity((q) => q + 1)}
|
||||
className="px-3 py-2 text-sm"
|
||||
aria-label="Increase quantity"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddToBag}
|
||||
className="flex-1 bg-clay px-6 py-3 text-sm font-medium text-paper transition-colors hover:bg-clay-dark"
|
||||
>
|
||||
Add to bag — <Price cents={product.effectivePrice * quantity} />
|
||||
</button>
|
||||
</div>
|
||||
{justAdded && <p className="text-sm text-pine">Added to your bag.</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user