56fe68eb48
- Royal cream + gold theme - Playful animated hero with chef mascot - Advanced menu with sidebar + video hover - Multilingual support (EN, SV, HI, UR) - Cart system with WhatsApp ordering - Real restaurant photos integration - Responsive design with proper navbar
344 lines
15 KiB
TypeScript
344 lines
15 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* MENU PAGE — Premium Sidebar + Beautiful Loading Experience
|
|
*/
|
|
|
|
import { useEffect, useState, useMemo } from "react";
|
|
import { menuCategories } from "@/lib/menu-data";
|
|
import { gsap } from "gsap";
|
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
|
import Navbar from "@/components/Navbar";
|
|
import Footer from "@/components/Footer";
|
|
import { useCart } from "@/components/CartContext";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
export default function MenuPage() {
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [activeCategory, setActiveCategory] = useState("All");
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [showVegetarianOnly, setShowVegetarianOnly] = useState(false);
|
|
|
|
const { addToCart } = useCart();
|
|
|
|
// Beautiful loading screen on initial load
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setIsLoading(false);
|
|
}, 1350);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
// Sidebar categories
|
|
const sidebarCategories = [
|
|
{ id: "All", name: "All Dishes" },
|
|
...menuCategories.map((cat) => ({ id: cat.id, name: cat.name })),
|
|
];
|
|
|
|
// Filtering logic
|
|
const filteredCategories = useMemo(() => {
|
|
return menuCategories
|
|
.map((category) => {
|
|
let items = category.items;
|
|
|
|
// Sidebar filter
|
|
if (activeCategory !== "All" && category.id !== activeCategory) {
|
|
items = [];
|
|
}
|
|
|
|
// Search
|
|
if (searchQuery.trim()) {
|
|
const q = searchQuery.toLowerCase().trim();
|
|
items = items.filter(
|
|
(item) =>
|
|
item.name.toLowerCase().includes(q) ||
|
|
(item.description && item.description.toLowerCase().includes(q))
|
|
);
|
|
}
|
|
|
|
// Vegetarian
|
|
if (showVegetarianOnly) {
|
|
items = items.filter((item) => item.isVegetarian);
|
|
}
|
|
|
|
return { ...category, items };
|
|
})
|
|
.filter((category) => category.items.length > 0);
|
|
}, [searchQuery, showVegetarianOnly, activeCategory]);
|
|
|
|
const handleCategorySelect = (id: string) => {
|
|
setActiveCategory(id);
|
|
window.scrollTo({ top: 220, behavior: "smooth" });
|
|
};
|
|
|
|
// === BEAUTIFUL LOADING SCREEN ===
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen bg-[#fbf7ef] flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="relative mx-auto mb-8 w-20 h-20">
|
|
<div className="absolute inset-0 rounded-full border-2 border-[#c99a2e]/30" />
|
|
<motion.div
|
|
className="absolute inset-0 rounded-full border-2 border-transparent border-t-[#c99a2e] border-r-[#d4a73d]"
|
|
animate={{ rotate: 360 }}
|
|
transition={{ duration: 1.3, repeat: Infinity, ease: "linear" }}
|
|
/>
|
|
<div className="absolute inset-[6px] rounded-full border border-[#0f5a4a]/20" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<h2 className="font-serif text-3xl tracking-tight text-[#101724]">
|
|
Loading the Menu
|
|
</h2>
|
|
<p className="text-[#68717f] text-sm tracking-[1.5px]">Preparing the Royal Table...</p>
|
|
</div>
|
|
|
|
<div className="flex justify-center gap-2 mt-6">
|
|
{[0, 1, 2].map((i) => (
|
|
<motion.div
|
|
key={i}
|
|
className="w-1.5 h-1.5 rounded-full bg-[#c99a2e]"
|
|
animate={{ opacity: [0.3, 1, 0.3] }}
|
|
transition={{ duration: 1.1, repeat: Infinity, delay: i * 0.18 }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
|
<Navbar />
|
|
|
|
{/* Page Header */}
|
|
<div className="max-w-7xl mx-auto px-6 pt-14 pb-10">
|
|
<div className="max-w-3xl">
|
|
<div className="text-[#B38B4D] text-xs tracking-[3.5px] mb-4 font-medium">AUTHENTIC • GENEROUS • ROYAL</div>
|
|
<h1 className="text-6xl md:text-7xl tracking-[-2.8px] leading-none mb-5 text-[#101724]">Our Menu</h1>
|
|
<p className="text-2xl text-[#4b5563] max-w-2xl">
|
|
Traditional recipes. Generous portions. Made with heart.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Layout */}
|
|
<div className="max-w-7xl mx-auto px-6 pb-20">
|
|
<div className="flex flex-col lg:flex-row gap-10">
|
|
|
|
{/* BEAUTIFUL SIDEBAR */}
|
|
<div className="lg:w-72 flex-shrink-0">
|
|
<div className="sticky top-24">
|
|
<div className="mb-6">
|
|
<div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div>
|
|
<h3 className="font-serif text-3xl tracking-tight">Signature Categories</h3>
|
|
</div>
|
|
|
|
<div className="space-y-1 pr-4">
|
|
{sidebarCategories.map((cat) => {
|
|
const isActive = activeCategory === cat.id;
|
|
const itemCount = cat.id === "All"
|
|
? menuCategories.reduce((sum, c) => sum + c.items.length, 0)
|
|
: menuCategories.find(c => c.id === cat.id)?.items.length || 0;
|
|
|
|
return (
|
|
<button
|
|
key={cat.id}
|
|
onClick={() => handleCategorySelect(cat.id)}
|
|
className={`w-full flex items-center justify-between px-5 py-3.5 rounded-2xl text-left transition-all group ${
|
|
isActive
|
|
? "bg-[#101724] text-white shadow-lg"
|
|
: "hover:bg-white hover:shadow-sm text-[#101724] border border-transparent hover:border-[#e5e1d7]"
|
|
}`}
|
|
>
|
|
<span className={`font-medium tracking-[-0.1px] ${isActive ? "" : "group-hover:text-[#0f5a4a]"}`}>
|
|
{cat.name}
|
|
</span>
|
|
<span className={`text-xs px-2.5 py-0.5 rounded-full font-mono tabular-nums ${
|
|
isActive ? "bg-white/20" : "bg-[#e5e1d7] text-[#68717f]"
|
|
}`}>
|
|
{itemCount}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* MAIN CONTENT */}
|
|
<div className="flex-1 min-w-0">
|
|
{/* Search + Vegetarian Filter */}
|
|
<div className="mb-8 flex flex-col md:flex-row gap-4 items-center">
|
|
<input
|
|
type="text"
|
|
placeholder="Search dishes..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full md:w-80 rounded-2xl border border-[#e5e1d7] bg-white px-5 py-3 text-sm placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20 transition-all"
|
|
/>
|
|
|
|
<button
|
|
onClick={() => setShowVegetarianOnly(!showVegetarianOnly)}
|
|
className={`px-6 py-3 rounded-2xl text-sm font-medium border transition-all ${
|
|
showVegetarianOnly
|
|
? "bg-[#0f5a4a] text-white border-[#0f5a4a]"
|
|
: "border-[#e5e1d7] hover:border-[#c99a2e] text-[#101724] bg-white"
|
|
}`}
|
|
>
|
|
{showVegetarianOnly ? "Vegetarian Only" : "Show Vegetarian"}
|
|
</button>
|
|
|
|
{(searchQuery || showVegetarianOnly || activeCategory !== "All") && (
|
|
<button
|
|
onClick={() => {
|
|
setSearchQuery("");
|
|
setShowVegetarianOnly(false);
|
|
setActiveCategory("All");
|
|
}}
|
|
className="text-sm font-medium text-[#c99a2e] hover:text-[#8f6b22]"
|
|
>
|
|
Clear filters
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Menu Items */}
|
|
{filteredCategories.length === 0 ? (
|
|
<div className="text-center py-16 text-[#6B665F]">
|
|
No dishes found matching your filters.
|
|
</div>
|
|
) : (
|
|
filteredCategories.map((category) => (
|
|
<div key={category.id} className="mb-16">
|
|
<div className="flex items-center gap-4 mb-7">
|
|
<div className="text-3xl tracking-[-1px] text-[#101724]">{category.name}</div>
|
|
<div className="flex-1 h-px bg-gradient-to-r from-[#e5e1d7] to-transparent" />
|
|
<div className="text-sm font-medium text-[#68717f] tracking-widest">
|
|
{category.items.length} dishes
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{category.items.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="menu-card group bg-white border border-[#EDE6D9] rounded-2xl overflow-hidden flex flex-col hover:border-[#c99a2e]/40 transition-all duration-300 cursor-pointer"
|
|
onClick={() => addToCart({ id: item.id, name: item.name, price: item.price, image: item.image })}
|
|
>
|
|
{/* Media - Restored Premium Video Hover + Poster Logic */}
|
|
{item.video ? (
|
|
<div
|
|
className="relative h-52 overflow-hidden bg-[#F2EDE4] cursor-pointer"
|
|
onMouseEnter={(e) => {
|
|
const video = e.currentTarget.querySelector("video");
|
|
if (video) video.play().catch(() => {});
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
const video = e.currentTarget.querySelector("video");
|
|
if (video) {
|
|
video.pause();
|
|
video.currentTime = 0;
|
|
}
|
|
}}
|
|
onClick={() => addToCart({ id: item.id, name: item.name, price: item.price, image: item.image })}
|
|
>
|
|
{/* Static Poster (first frame) */}
|
|
<img
|
|
src={`/images/dishes/${item.video.replace(".mp4", "-poster.jpg")}`}
|
|
alt={item.name}
|
|
className="absolute inset-0 w-full h-full object-cover group-hover:opacity-0 transition-opacity duration-150"
|
|
loading="lazy"
|
|
onError={(e) => {
|
|
// Fallback poster logic
|
|
const base = (item.video || item.image || "").replace(".mp4", "").replace(/-optimized/g, "");
|
|
const fallbacks = [
|
|
`/images/dishes/${base}-poster.jpg`,
|
|
`/images/dishes/${base}-optimized-poster.jpg`,
|
|
`/images/dishes/${item.image}`,
|
|
].filter(Boolean);
|
|
let i = 0;
|
|
const tryNext = () => {
|
|
if (i < fallbacks.length) {
|
|
(e.target as HTMLImageElement).src = fallbacks[i++];
|
|
}
|
|
};
|
|
tryNext();
|
|
}}
|
|
/>
|
|
|
|
{/* Video on Hover */}
|
|
<video
|
|
muted
|
|
loop
|
|
playsInline
|
|
preload="metadata"
|
|
className="absolute inset-0 w-full h-full object-cover opacity-0 group-hover:opacity-100 transition-opacity duration-150"
|
|
>
|
|
<source src={`/videos/${item.video.replace(".mp4", ".webm")}`} type="video/webm" />
|
|
<source src={`/videos/${item.video}`} type="video/mp4" />
|
|
</video>
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-black/10 to-black/25 group-hover:opacity-0 transition-opacity" />
|
|
</div>
|
|
) : (
|
|
/* Static Image for items without video */
|
|
<div
|
|
className="relative h-52 overflow-hidden bg-[#F2EDE4] cursor-pointer"
|
|
onClick={() => addToCart({ id: item.id, name: item.name, price: item.price, image: item.image })}
|
|
>
|
|
<img
|
|
src={`/images/dishes/${item.image}`}
|
|
alt={item.name}
|
|
className="w-full h-full object-cover group-hover:scale-[1.03] transition-transform duration-700"
|
|
loading="lazy"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-black/10 to-black/25" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-6 flex flex-col flex-1">
|
|
<div className="flex justify-between items-start gap-4 mb-4">
|
|
<h3 className="text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26]">
|
|
{item.name}
|
|
</h3>
|
|
<div className="shrink-0 text-right">
|
|
<div className="text-[#B38B4D] text-2xl font-medium tracking-[-0.5px] tabular-nums">
|
|
{item.price}
|
|
</div>
|
|
<div className="text-[10px] text-[#8A8478] tracking-widest -mt-1">KR</div>
|
|
</div>
|
|
</div>
|
|
|
|
{item.description && (
|
|
<p className="text-[#6B665F] text-[15px] leading-relaxed mb-5">
|
|
{item.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="mt-auto">
|
|
{item.isVegetarian && (
|
|
<span className="text-xs px-3 py-1 rounded-full bg-[#3F5C4A]/10 text-[#3F5C4A] tracking-wider">
|
|
VEGETARIAN
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|