This commit is contained in:
Vula Builder
2026-07-29 18:15:24 +00:00
parent af22ccca03
commit dd3ebf35c1
+161
View File
@@ -0,0 +1,161 @@
'use client'
import { useState, useEffect } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Check, Minus, Plus, ShoppingBag } from 'lucide-react'
import { useVulaCart, type VulaCartItem } from '@/hooks/useVulaCart'
import YouMayAlsoLike from '@/components/shop/YouMayAlsoLike'
const CURRENCY_SYMBOLS: Record<string, string> = { zar: 'R', usd: '$', eur: '€', gbp: '£', kes: 'KSh', ngn: '₦', ghs: '₵' }
const formatPrice = (amount: number, code?: string) => {
const sym = CURRENCY_SYMBOLS[(code ?? '').toLowerCase()] ?? (code?.toUpperCase() ?? '')
return `${sym}${amount.toFixed(2)}`
}
interface VulaVariant { id: string; title: string; prices: Array<{ amount: number; currency_code: string }> }
interface VulaProductDetail {
id: string; title: string; description: string | null
thumbnail: string | null; images?: Array<{ url: string }>
variants: VulaVariant[]; collection?: { id: string; title: string }
}
export default function ProductDetailClient({ product: initialProduct, productId }: { product: VulaProductDetail | null; productId: string }) {
const [product, setProduct] = useState<VulaProductDetail | null>(initialProduct)
const [loading, setLoading] = useState(!initialProduct)
const [selectedVariant, setSelectedVariant] = useState<VulaVariant | null>(initialProduct?.variants?.[0] ?? null)
const [activeImage, setActiveImage] = useState(0)
const [quantity, setQuantity] = useState(1)
const [added, setAdded] = useState(false)
const { addToCart } = useVulaCart()
useEffect(() => {
if (initialProduct) return
const run = async () => {
try {
const res = await fetch(`/api/products?id[]=${productId}`)
if (!res.ok) { setLoading(false); return }
const data = await res.json()
const p = data.product ?? (data.products?.[0] ?? null)
if (p) {
setProduct(p)
setSelectedVariant(p.variants?.[0] ?? null)
}
} catch {
setProduct(null)
} finally {
setLoading(false)
}
}
run()
}, [productId, initialProduct])
const handleAddToCart = async () => {
if (!selectedVariant?.id || !product) return
const price = selectedVariant.prices?.[0]
const item: VulaCartItem = {
variantId: selectedVariant.id,
productId: product.id,
title: product.title,
variant_title: selectedVariant.title ?? '',
unit_price: price?.amount ?? 0,
quantity,
thumbnail: product.thumbnail ?? null,
}
for (let i = 0; i < quantity; i++) { addToCart({ ...item, quantity: 1 }) }
setAdded(true)
setTimeout(() => setAdded(false), 2500)
window.dispatchEvent(new CustomEvent('cart-drawer-open'))
}
if (loading) return (
<div className="flex justify-center py-20">
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-primary" />
</div>
)
if (!product) return (
<div className="text-center py-20">
<ShoppingBag className="h-12 w-12 mx-auto mb-4 text-muted-foreground/40" />
<p className="text-muted-foreground mb-4">Product not found.</p>
<Link href="/products" className="text-primary hover:underline">Back to Products</Link>
</div>
)
const images = [product.thumbnail, ...(product.images?.map(i => i.url) ?? [])].filter(Boolean) as string[]
const price = selectedVariant?.prices?.[0]
const mainImage = images[activeImage] ?? images[0]
return (
<div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
<div className="space-y-3">
<div className="relative aspect-square overflow-hidden rounded-2xl bg-muted">
{mainImage ? (
<Image src={mainImage} alt={product.title} fill sizes="(max-width: 1024px) 100vw, 50vw" className="object-cover" priority />
) : (
<div className="w-full h-full flex items-center justify-center">
<ShoppingBag className="h-20 w-20 text-muted-foreground/30" />
</div>
)}
</div>
{images.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-1">
{images.map((url, i) => (
<button key={i} onClick={() => setActiveImage(i)}
className={`relative flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-colors ${activeImage === i ? 'border-primary' : 'border-transparent hover:border-border'}`}>
<Image src={url} alt={`${product.title} view ${i + 1}`} fill sizes="64px" className="object-cover" />
</button>
))}
</div>
)}
</div>
<div className="flex flex-col">
{product.collection?.title && (
<span className="text-sm text-primary font-medium mb-2">{product.collection.title}</span>
)}
<h1 className="text-3xl font-bold text-foreground mb-4">{product.title}</h1>
{price && (
<p className="text-3xl font-bold text-primary mb-6">
{formatPrice(price.amount, price.currency_code)}
</p>
)}
{product.description && (
<p className="text-muted-foreground mb-8 leading-relaxed">{product.description}</p>
)}
{product.variants.length > 1 && (
<div className="mb-6">
<p className="text-sm font-semibold text-foreground mb-3">Option</p>
<div className="flex flex-wrap gap-2">
{product.variants.map(v => (
<button key={v.id} onClick={() => setSelectedVariant(v)}
className={`px-4 py-2 rounded-lg border text-sm font-medium transition-colors ${selectedVariant?.id === v.id ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-foreground hover:border-primary/50'}`}>
{v.title}
</button>
))}
</div>
</div>
)}
<div className="flex gap-3 mb-4 mt-auto">
<div className="flex items-center border border-border rounded-xl overflow-hidden">
<button onClick={() => setQuantity(q => Math.max(1, q - 1))} className="px-3 py-3 hover:bg-muted transition-colors" aria-label="Decrease quantity">
<Minus className="h-4 w-4" />
</button>
<span className="w-10 text-center text-sm font-semibold">{quantity}</span>
<button onClick={() => setQuantity(q => Math.min(10, q + 1))} className="px-3 py-3 hover:bg-muted transition-colors" aria-label="Increase quantity">
<Plus className="h-4 w-4" />
</button>
</div>
<button onClick={handleAddToCart} disabled={!selectedVariant?.id || added}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-semibold text-base transition-all ${added ? 'bg-green-600 text-white' : 'bg-primary text-primary-foreground hover:bg-primary/90'} disabled:opacity-60`}>
{added ? (<><Check className="h-5 w-5" /> Added!</>) : 'Add to Cart'}
</button>
</div>
<button onClick={() => window.dispatchEvent(new CustomEvent('cart-drawer-open'))} className="text-center text-sm text-muted-foreground hover:text-foreground transition-colors">
View Cart
</button>
</div>
</div>
<YouMayAlsoLike currentProductId={product.id} collectionId={product.collection?.id} />
</div>
)
}