Replace entire repo content with correct code from /root/shahikitchen-website/
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user