This commit is contained in:
Vula Builder
2026-10-11 14:20:29 +00:00
parent 936d0b763c
commit 325714a746
+152
View File
@@ -0,0 +1,152 @@
'use client'
import { useState, useEffect } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { Utensils, Minus, Plus, Check } from 'lucide-react'
import { useVulaCart, type VulaCartItem } from '@/hooks/useVulaCart'
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 / 100).toFixed(2)}`
}
interface VulaVariant { id: string; title: string; prices: Array<{ amount: number; currency_code: string }> }
interface VulaDishDetail {
id: string; title: string; description: string | null
thumbnail: string | null; images?: Array<{ url: string }>
variants: VulaVariant[]; collection?: { id: string; title: string }
}
export default function DishDetailClient({ dish: initialDish, dishId }: { dish: VulaDishDetail | null; dishId: string }) {
const [dish, setDish] = useState<VulaDishDetail | null>(initialDish)
const [loading, setLoading] = useState(!initialDish)
const [selectedVariant, setSelectedVariant] = useState<VulaVariant | null>(initialDish?.variants?.[0] ?? null)
const [activeImage, setActiveImage] = useState(0)
const [quantity, setQuantity] = useState(1)
const [added, setAdded] = useState(false)
const { addToCart } = useVulaCart()
useEffect(() => {
if (initialDish) return
const run = async () => {
try {
const res = await fetch(`/api/products?id[]=${dishId}`)
if (!res.ok) { setLoading(false); return }
const data = await res.json()
const d = data.product ?? (data.products?.[0] ?? null)
if (d) { setDish(d); setSelectedVariant(d.variants?.[0] ?? null) }
} catch {
setDish(null)
} finally {
setLoading(false)
}
}
run()
}, [dishId, initialDish])
const handleAddToOrder = async () => {
if (!selectedVariant?.id || !dish) return
const price = selectedVariant.prices?.[0]
const item: VulaCartItem = {
variantId: selectedVariant.id,
productId: dish.id,
title: dish.title,
variant_title: selectedVariant.title ?? '',
unit_price: price?.amount ?? 0,
quantity,
thumbnail: dish.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 (!dish) return (
<div className="text-center py-20">
<Utensils className="h-12 w-12 mx-auto mb-4 text-muted-foreground/40" />
<p className="text-muted-foreground mb-4">Dish not found.</p>
<Link href="/menu" className="text-primary hover:underline">Back to Menu</Link>
</div>
)
const images = [...new Set([dish.thumbnail, ...(dish.images?.map(i => i.url) ?? [])].filter(Boolean))] as string[]
const price = selectedVariant?.prices?.[0]
const mainImage = images[activeImage] ?? images[0]
return (
<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={dish.title} fill sizes="(max-width: 1024px) 100vw, 50vw" className="object-cover" priority />
) : (
<div className="w-full h-full flex items-center justify-center">
<Utensils 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={`${dish.title} view ${i + 1}`} fill sizes="64px" className="object-cover" />
</button>
))}
</div>
)}
</div>
<div className="flex flex-col">
{dish.collection?.title && (
<span className="text-sm text-primary font-medium mb-2">{dish.collection.title}</span>
)}
<h1 className="text-3xl font-bold text-foreground mb-4">{dish.title}</h1>
{price && (
<p className="text-3xl font-bold text-primary mb-6">{formatPrice(price.amount, price.currency_code)}</p>
)}
{dish.description && (
<p className="text-muted-foreground mb-8 leading-relaxed">{dish.description}</p>
)}
{dish.variants.length > 1 && (
<div className="mb-6">
<p className="text-sm font-semibold text-foreground mb-3">Size / Option</p>
<div className="flex flex-wrap gap-2">
{dish.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={handleAddToOrder} 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 to Order!</>) : 'Add to Order'}
</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 Order →
</button>
</div>
</div>
)
}