diff --git a/src/app/booking/[productId]/page.tsx b/src/app/booking/[productId]/page.tsx new file mode 100644 index 0000000..21bedd9 --- /dev/null +++ b/src/app/booking/[productId]/page.tsx @@ -0,0 +1,194 @@ +'use client' +import { useState, useEffect } from 'react' +import { useParams } from 'next/navigation' +import Image from 'next/image' +import Link from 'next/link' +import { ArrowLeft, CalendarCheck, CircleCheck, Loader2 } from 'lucide-react' + +const TIME_SLOTS = ['08:00','09:00','10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00'] +const CURRENCY_SYMBOLS: Record = { 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(0)}` +} + +interface Service { + id: string; title: string; description: string; thumbnail: string + variants: { id: string; prices: { amount: number; currency_code: string }[] }[] +} + +export default function BookingPage() { + const params = useParams() + const productId = params.productId as string + + const [service, setService] = useState(null) + const [loadingSvc, setLoadingSvc] = useState(true) + const [form, setForm] = useState({ name: '', email: '', phone: '', date: '', time: '', notes: '' }) + const [bookedSlots, setBookedSlots] = useState([]) + const [loadingSlots, setLoadingSlots] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [confirmationNumber, setConfirmationNumber] = useState('') + const [success, setSuccess] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (!productId) return + fetch(`/api/medusa/store/products/${productId}?fields=*variants`) + .then(r => r.json()) + .then(d => { setService(d.product ?? null); setLoadingSvc(false) }) + .catch(() => setLoadingSvc(false)) + }, [productId]) + + // Fetch booked slots whenever date changes + useEffect(() => { + if (!form.date || !productId) return + setLoadingSlots(true) + setForm(f => ({ ...f, time: '' })) + fetch(`/api/medusa/store/appointments?service_id=${productId}&date=${form.date}`) + .then(r => r.json()) + .then(d => { setBookedSlots(d.booked_slots ?? []); setLoadingSlots(false) }) + .catch(() => setLoadingSlots(false)) + }, [form.date, productId]) + + const price = service?.variants?.[0]?.prices?.[0] + const today = new Date().toISOString().split('T')[0] + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!form.time) { setError('Please select a time slot.'); return } + setSubmitting(true) + setError('') + try { + const res = await fetch('/api/medusa/store/appointments', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + service_id: productId, + appointment_date: form.date, + appointment_time: form.time, + service_name: service?.title || 'Appointment', + customer_name: form.name, + customer_phone: form.phone, + customer_email: form.email || null, + notes: form.notes || null, + }), + signal: AbortSignal.timeout(15000), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || 'Booking failed') + setConfirmationNumber(data.confirmation_number ?? '') + setSuccess(true) + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Something went wrong. Please try again or call us directly.') + } + setSubmitting(false) + } + + if (loadingSvc) return
+ if (!service) return

Service not found.

Back to Services
+ + if (success) return ( +
+
+
+ +

Appointment Booked!

+

{service.title}

+

{form.date} at {form.time}

+ {confirmationNumber &&

{confirmationNumber}

} +

We will confirm your appointment via phone shortly.

+ Browse More Services +
+
+
+ ) + + return ( +
+
+ + Back to Services + +
+
+ {service.thumbnail && ( +
+ {service.title} +
+ )} +

{service.title}

+ {service.description &&

{service.description}

} + {price &&

{formatPrice(price.amount, price.currency_code)}

} +
+
+
+

+ Book an Appointment +

+
+
+ + setForm(f => ({...f, date: e.target.value}))} + className="w-full border border-border rounded-lg px-3 py-2 bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" /> +
+ {form.date && ( +
+
+ + {loadingSlots && } +
+
+ {TIME_SLOTS.map(slot => { + const booked = bookedSlots.includes(slot) + const selected = form.time === slot + return ( + + ) + })} +
+
+ )} +
+
+ + setForm(f => ({...f, name: e.target.value}))} + className="w-full border border-border rounded-lg px-3 py-2 bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" /> +
+
+ + setForm(f => ({...f, phone: e.target.value}))} + placeholder="+27 " className="w-full border border-border rounded-lg px-3 py-2 bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" /> +
+
+ + setForm(f => ({...f, email: e.target.value}))} + className="w-full border border-border rounded-lg px-3 py-2 bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary" /> +
+
+ +