Files
shahikitchen-prod/app/reserve/page.tsx
T

545 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { buildBookingWhatsAppMessage } from '@/application/messaging/whatsapp-message-builder';
import { isBookingComplete } from '@/domain/booking/validation';
import type { BookingDetails, PreOrderLine } from '@/domain/booking/entities';
import {
addOrderLine,
calculateLineTotal,
calculateOrderTotal,
formatLineQuantityLabel,
removeOrderLine,
updateOrderLineQuantity,
} from '@/domain/shared/order-line';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
import type { MenuItem } from '@/domain/menu/entities';
import {
getMenuPosterSrc,
getMenuPosterCandidates,
logoUrl,
applyNextImageFallback,
SITE_ASSETS,
} from '@/lib/assets';
import { container } from '@/infrastructure/di/container';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import { useState } from 'react';
import { motion, AnimatePresence } from "framer-motion";
import { MapPin, Calendar, Clock, Users, User, Phone, Mail, MessageCircle, Plus, Minus, X, ArrowRight, Search, CheckCircle } from "lucide-react";
import { menuCategories } from "@/lib/menu-data";
import { playAddSound, playHoverSound, playSuccessSound } from "@/lib/sounds";
/**
* =============================================================================
* TABLE RESERVATION / BOOKING PAGE
* =============================================================================
*
* Beautiful, theme-matched table booking experience with optional pre-order.
* - Swap implemented: Reserve Table now in central nav pills, Login is the gold CTA.
* - Form first (location, date, time, guests, name, phone, email, notes).
* - After form: full menu selector for pre-ordering (reuse menu-data).
* - End: builds rich WhatsApp inquiry (booking + pre-order items) using same number as cart.
* - Fully translated (sv default + en/hi/ur).
* - Mobile-first, large targets, elegant cards matching /login and site theme.
* - Uses framer-motion for reveals, lucide icons.
* - No backend: client-side only, like staff login and cart WhatsApp.
*
* Linked from navbar (after swap) and locations.
*/
export default function ReservePage() {
const { language } = useLanguage();
const t = getTranslation(language);
const [booking, setBooking] = useState<BookingDetails>({
location: '',
date: '',
time: '',
guests: '',
name: '',
phone: '',
email: '',
notes: '',
});
const [preOrder, setPreOrder] = useState<PreOrderLine[]>([]);
const [showMenu, setShowMenu] = useState(false);
const [search, setSearch] = useState('');
const [sent, setSent] = useState(false);
const handleBookingChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setBooking((prev) => ({ ...prev, [name]: value }));
};
const validateForm = (): boolean => isBookingComplete(booking);
const handleContinue = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
setShowMenu(true);
// Smooth scroll to menu on mobile
setTimeout(() => {
const menuEl = document.getElementById('preorder-menu');
if (menuEl) menuEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
} else {
alert('Please fill in all required fields (location, date, time, guests, name, phone).');
}
};
// Flatten menu for selector (with search)
const allDishes = menuCategories.flatMap((cat) => cat.items);
const filteredDishes = allDishes.filter((dish) =>
dish.name.toLowerCase().includes(search.toLowerCase()) ||
(dish.description && dish.description.toLowerCase().includes(search.toLowerCase()))
);
const addToPreOrder = (dish: MenuItem) => {
setPreOrder((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
};
const updatePreOrderQty = (id: string, newQty: number) => {
setPreOrder((prev) => updateOrderLineQuantity(prev, id, newQty));
};
const removeFromPreOrder = (id: string) => {
setPreOrder((prev) => removeOrderLine(prev, id));
};
const getPreOrderTotal = () => calculateOrderTotal(preOrder);
const handleSendInquiry = () => {
if (!validateForm()) {
alert('Please complete the booking form first.');
return;
}
playSuccessSound();
const message = buildBookingWhatsAppMessage(booking, preOrder, {
askim: t.booking.askim,
backaplan: t.booking.backaplan,
});
if (!message) return;
container.messagingGateway.openWhatsApp(message);
setSent(true);
};
const resetAll = () => {
setBooking({ location: '', date: '', time: '', guests: '', name: '', phone: '', email: '', notes: '' });
setPreOrder([]);
setShowMenu(false);
setSearch('');
setSent(false);
};
const editBooking = () => {
setShowMenu(false);
setSent(false);
};
// Location options from translations
const locationOptions = [
{ value: 'askim', label: t.booking.askim },
{ value: 'backaplan', label: t.booking.backaplan },
];
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main className="pb-20">
{/* Elegant Header */}
<div className="max-w-5xl mx-auto px-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
EXCLUSIVE EXPERIENCE
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
{t.booking.title}
</h1>
<p className="mx-auto max-w-xl text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
{t.booking.subtitle}
</p>
</div>
<div className="max-w-5xl mx-auto px-6 mt-10">
{/* Decorative header info - now ABOVE the form (not on side) so form can be wider */}
<div className="mb-10 flex justify-center">
<div className="w-full max-w-2xl rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-6 sm:p-8 text-white shadow-2xl text-center">
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur mx-auto">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div>
<div className="font-serif text-4xl tracking-[-1.5px] mb-1">Shahi Kitchen</div>
<div className="text-sm font-medium tracking-[2.5px] text-[#c99a2e] mb-4">TABLE RESERVATIONS</div>
<p className="text-sm leading-relaxed text-white/70 max-w-xs mx-auto mb-4">
Two locations. Unforgettable evenings. Pre-order your favorites for a perfect arrival.
</p>
<div className="text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">ASKIM BACKAPLAN</div>
</div>
</div>
<AnimatePresence mode="wait">
{!sent ? (
<div className="max-w-4xl mx-auto space-y-8">
{/* Booking Form Card - now wider (full in max-w-4xl) */}
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10">
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.formTitle}</h2>
<p className="text-[#6B665F] mb-8">Fill in your details to secure your table.</p>
<form onSubmit={handleContinue} className="space-y-6">
{/* Location */}
<div>
<label htmlFor="location" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.locationLabel}</label>
<div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select
id="location"
name="location"
value={booking.location}
onChange={handleBookingChange}
required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
>
<option value="">{t.booking.locationPlaceholder}</option>
{locationOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Date */}
<div>
<label htmlFor="date" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.dateLabel}</label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="date"
name="date"
type="date"
value={booking.date}
onChange={handleBookingChange}
min={new Date().toISOString().split('T')[0]}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Time */}
<div>
<label htmlFor="time" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.timeLabel}</label>
<div className="relative">
<Clock className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="time"
name="time"
type="time"
value={booking.time}
onChange={handleBookingChange}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
</div>
{/* Guests */}
<div>
<label htmlFor="guests" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.guestsLabel}</label>
<div className="relative">
<Users className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<select
id="guests"
name="guests"
value={booking.guests}
onChange={handleBookingChange}
required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
>
<option value="">Select</option>
{[1,2,3,4,5,6,7,8,9,10].map(n => <option key={n} value={n}>{n}</option>)}
</select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
</div>
{/* Name */}
<div>
<label htmlFor="name" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.nameLabel}</label>
<div className="relative">
<User className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="name"
name="name"
type="text"
value={booking.name}
onChange={handleBookingChange}
placeholder={t.booking.namePlaceholder}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Phone */}
<div>
<label htmlFor="phone" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.phoneLabel}</label>
<div className="relative">
<Phone className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="phone"
name="phone"
type="tel"
value={booking.phone}
onChange={handleBookingChange}
placeholder={t.booking.phonePlaceholder}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Email */}
<div>
<label htmlFor="email" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.emailLabel}</label>
<div className="relative">
<Mail className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="email"
name="email"
type="email"
value={booking.email}
onChange={handleBookingChange}
placeholder={t.booking.emailPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Notes */}
<div>
<label htmlFor="notes" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.notesLabel}</label>
<div className="relative">
<MessageCircle className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<textarea
id="notes"
name="notes"
value={booking.notes}
onChange={handleBookingChange}
placeholder={t.booking.notesPlaceholder}
rows={3}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 resize-y"
/>
</div>
</div>
<button
type="submit"
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
>
{t.booking.continueBtn}
<ArrowRight className="h-5 w-5" />
</button>
</form>
</div>
{/* Pre-Order Menu Section - revealed after form */}
<AnimatePresence>
{showMenu && (
<motion.div
id="preorder-menu"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10"
>
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.menuTitle}</h2>
<p className="text-[#6B665F] mb-6">{t.booking.menuSubtitle}</p>
{/* Search */}
<div className="relative mb-6">
<Search className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t.booking.searchPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
{/* Dishes Grid - improved visualization: Name, Poster below, Desc under poster, price, nice Add button */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[320px] sm:max-h-[420px] overflow-auto pr-2">
{filteredDishes.length > 0 ? (
filteredDishes.map((dish) => {
const posterSrc = getMenuPosterSrc(dish);
const shortDesc = dish.description ? (dish.description.length > 80 ? dish.description.slice(0, 80) + '...' : dish.description) : '';
const isAdded = preOrder.some((p) => p.id === dish.id);
return (
<motion.div
key={dish.id}
whileHover={{ y: -2, boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" }}
className="flex flex-col border border-[#EDE6D9] rounded-2xl p-3 bg-white hover:border-[#B38B4D]/50 transition-all overflow-hidden"
onMouseEnter={() => playHoverSound()}
>
{/* Item Name */}
<div className="font-semibold tracking-[-0.3px] text-base mb-1 text-[#2C2A26] line-clamp-1">{dish.name}</div>
{/* Poster under name */}
<div className="relative w-full h-24 sm:h-28 md:h-32 rounded-xl overflow-hidden border border-[#EDE6D9] mb-2 bg-[#F8F5F0]">
<img
src={posterSrc}
alt={dish.name}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getMenuPosterCandidates(dish),
(e.target as HTMLImageElement).src
);
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/10" />
</div>
{/* Description under the poster */}
<div className="text-xs text-[#6B665F] leading-snug mb-2 flex-1 line-clamp-2">{shortDesc}</div>
<div className="flex items-center justify-between mt-auto pt-1">
<span className="text-[#B38B4D] font-semibold text-sm tabular-nums leading-tight text-right">
{dish.pricing === 'weight' ? (
<>
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
<span className="block text-xs text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
</>
) : (
<span>{dish.price} kr</span>
)}
</span>
{/* Improved graphics for add button */}
<button
onClick={() => {
playAddSound();
addToPreOrder(dish);
}}
className={`flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold shadow-sm active:scale-[0.96] transition-all ${
isAdded
? "bg-[#3F5C4A] text-white hover:bg-[#2a4033]"
: "bg-[#B38B4D] text-white hover:bg-[#8f6b22] hover:shadow-md"
}`}
>
<Plus className="h-3.5 w-3.5" />
{isAdded ? "Add More" : t.booking.addToPreOrder}
</button>
</div>
</motion.div>
);
})
) : (
<p className="text-[#6B665F] col-span-full text-center py-8">No dishes match your search.</p>
)}
</div>
{/* Pre-Order Summary */}
<div className="mt-8 border-t border-[#EDE6D9] pt-6">
<h3 className="font-semibold tracking-wide mb-4 flex items-center gap-2">{t.booking.yourPreOrder} <span className="text-xs text-[#B38B4D] font-normal">({preOrder.length})</span></h3>
{preOrder.length === 0 ? (
<p className="text-sm text-[#6B665F] italic">{t.booking.emptyPreOrder}</p>
) : (
<div className="space-y-3 mb-4">
{preOrder.map((item) => {
const dishInfo = allDishes.find((d) => d.id === item.id);
const thumb = dishInfo ? getMenuPosterSrc(dishInfo) : SITE_ASSETS.fallbacks.logo;
return (
<div key={item.id} className="flex items-center gap-3 bg-[#F8F5F0] rounded-2xl px-3 py-2">
{/* Small poster thumbnail in summary for better visualization */}
<img
src={thumb}
alt={item.name}
className="w-10 h-10 object-cover rounded-lg border border-[#EDE6D9] flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{item.name}</div>
<div className="text-[10px] text-[#6B665F]">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
: `${item.price} kr × ${item.quantity}`}
{' '}= {calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<button onClick={() => updatePreOrderQty(item.id, item.quantity - 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">-</button>
<span className="min-w-10 text-center font-medium text-sm tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button onClick={() => updatePreOrderQty(item.id, item.quantity + 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">+</button>
<button onClick={() => removeFromPreOrder(item.id)} className="ml-1 text-[#B38B4D] hover:text-red-600 p-0.5 active:scale-95"><X className="h-4 w-4" /></button>
</div>
</div>
);
})}
</div>
)}
<div className="flex justify-between text-lg font-medium border-t border-[#EDE6D9] pt-4">
<span>{t.booking.total}</span>
<span>{getPreOrderTotal()} kr</span>
</div>
<button
onClick={handleSendInquiry}
disabled={!validateForm()}
className="mt-6 w-full py-4 rounded-2xl text-base font-semibold tracking-[0.5px] flex items-center justify-center gap-3 bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] text-white shadow-lg hover:brightness-105 active:brightness-95 active:scale-[0.985] transition-all disabled:opacity-60 disabled:cursor-not-allowed disabled:brightness-100"
>
{t.booking.sendInquiry}
<MessageCircle className="h-5 w-5" />
</button>
<p className="mt-3 text-center text-[11px] text-[#8A8478]">{t.booking.inquiryNote}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) : (
/* Success State */
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-md mx-auto text-center py-12"
>
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[#B38B4D]/10">
<CheckCircle className="h-10 w-10 text-[#B38B4D]" />
</div>
<h2 className="text-4xl tracking-tight font-serif mb-4">{t.booking.successTitle}</h2>
<p className="text-[#6B665F] mb-8 leading-relaxed">{t.booking.successMessage}</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button onClick={editBooking} className="btn-outline px-8 py-3 rounded-full text-sm font-medium">
{t.booking.editBooking}
</button>
<button onClick={resetAll} className="btn-primary px-8 py-3 rounded-full text-sm font-medium">
{t.booking.backHome}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
</main>
<Footer />
</div>
);
}