diff --git a/src/app/booking/[productId]/page.tsx b/src/app/booking/[productId]/page.tsx new file mode 100644 index 0000000..2183142 --- /dev/null +++ b/src/app/booking/[productId]/page.tsx @@ -0,0 +1,286 @@ +'use client' +import { useState, useEffect, Suspense } from 'react' +import { useParams, useSearchParams } from 'next/navigation' +import Image from 'next/image' +import Link from 'next/link' +import { ArrowLeft, CalendarCheck, CheckCircle, Loader2 } from 'lucide-react' + +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 Room { + id: string + title: string + description: string + thumbnail: string + images: { url: string }[] + variants: { id: string; prices: { amount: number; currency_code: string }[] }[] +} + +function calcNights(checkIn: string, checkOut: string) { + if (!checkIn || !checkOut) return 0 + return Math.max(0, Math.round((new Date(checkOut).getTime() - new Date(checkIn).getTime()) / 86400000)) +} + +function BookingPageContent() { + const params = useParams() + const searchParams = useSearchParams() + const productId = params.productId as string + const today = new Date().toISOString().split('T')[0] + + const [room, setRoom] = useState(null) + const [loadingRoom, setLoadingRoom] = useState(true) + const [activeImg, setActiveImg] = useState(0) + + const [checkIn, setCheckIn] = useState(searchParams.get('checkIn') || '') + const [checkOut, setCheckOut] = useState(searchParams.get('checkOut') || '') + const [availability, setAvailability] = useState<{ available: boolean } | null>(null) + const [checkingAvail, setCheckingAvail] = useState(false) + + const [form, setForm] = useState({ name: '', email: '', phone: '', notes: '' }) + const [submitting, setSubmitting] = useState(false) + const [success, setSuccess] = useState(false) + const [confirmNo, setConfirmNo] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + if (!productId) return + fetch(`/api/medusa/store/products/${productId}?fields=*variants,*images`) + .then(r => r.json()) + .then(d => { setRoom(d.product ?? null); setLoadingRoom(false) }) + .catch(() => setLoadingRoom(false)) + }, [productId]) + + useEffect(() => { + if (!checkIn || !checkOut || !room) return + if (new Date(checkOut) <= new Date(checkIn)) return + setCheckingAvail(true) + setAvailability(null) + const qs = new URLSearchParams({ room_type: room.id, check_in: checkIn, check_out: checkOut }).toString() + fetch(`/api/medusa/store/hotel/bookings?${qs}`) + .then(r => r.json()) + .then(d => { setAvailability(d.available != null ? { available: d.available } : null); setCheckingAvail(false) }) + .catch(() => setCheckingAvail(false)) + }, [checkIn, checkOut, room]) + + const nights = calcNights(checkIn, checkOut) + const variant = room?.variants?.[0] + const price = variant?.prices?.[0] + const pricePerNight = price?.amount ?? 0 + const currency = price?.currency_code + const totalPrice = pricePerNight * nights + const allImages = room ? [room.thumbnail, ...(room.images?.map(i => i.url) ?? [])].filter(Boolean) : [] + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!room || !availability?.available || nights === 0) return + setSubmitting(true) + setError('') + try { + const res = await fetch('/api/medusa/store/hotel/bookings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + room_type: room.id, + check_in: checkIn, + check_out: checkOut, + customer_name: form.name, + customer_email: form.email || null, + customer_phone: form.phone, + notes: form.notes || null, + }), + signal: AbortSignal.timeout(15000), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error || 'Booking failed') + setConfirmNo(data.confirmation_number ?? '') + setSuccess(true) + } catch (err: any) { + setError(err.message === 'Room is not available for the selected dates' + ? 'Sorry, those dates are no longer available. Please choose different dates.' + : 'Something went wrong. Please try again or call us directly.') + } + setSubmitting(false) + } + + if (loadingRoom) return ( +
+ +
+ ) + + if (!room) return ( +
+

Room not found.

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

Booking Confirmed!

+ {confirmNo &&

Ref: {confirmNo}

} +

{room.title}

+

Check-in: {checkIn}

+

Check-out: {checkOut}

+ {nights > 0 && totalPrice > 0 && ( +

{formatPrice(totalPrice, currency)} total · {nights} night{nights !== 1 ? 's' : ''}

+ )} +

+ {form.email ? `A confirmation will be sent to ${form.email}.` : `We will call ${form.phone} to confirm your reservation.`} +

+ + Browse More Rooms + +
+
+
+ ) + + return ( +
+
+ + Back to Rooms + +
+ + {/* Left — Room details */} +
+ {allImages.length > 0 && ( +
+
+ {room.title} +
+ {allImages.length > 1 && ( +
+ {allImages.map((img, i) => ( + + ))} +
+ )} +
+ )} +

{room.title}

+ {room.description &&

{room.description}

} + {pricePerNight > 0 && ( +

+ {formatPrice(pricePerNight, currency)} + / night +

+ )} +
+ + {/* Right — Booking form */} +
+
+

+ Reserve This Room +

+
+ + {/* Dates */} +
+
+ + { setCheckIn(e.target.value); setAvailability(null) }} + 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" /> +
+
+ + { setCheckOut(e.target.value); setAvailability(null) }} + 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" /> +
+
+ + {/* Availability indicator */} + {checkingAvail &&

Checking availability…

} + {availability && ( +
+ {availability.available ? "✓ Available for selected dates" : "✗ Not available for selected dates — please choose different dates"} +
+ )} + + {/* Price breakdown */} + {nights > 0 && pricePerNight > 0 && ( +
+
+ {formatPrice(pricePerNight, currency)} × {nights} night{nights !== 1 ? 's' : ''} + {formatPrice(totalPrice, currency)} +
+
+ Total + {formatPrice(totalPrice, currency)} +
+
+ )} + + {/* Guest details */} +
+
+ + 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, 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" /> +
+
+ + 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" /> +
+
+ +