Initial commit: Shahi Kitchen premium website

- 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
This commit is contained in:
Zeeshan Khan
2026-06-01 15:14:19 +02:00
parent edd906d893
commit 56fe68eb48
314 changed files with 4129 additions and 111 deletions
+97 -16
View File
@@ -1,26 +1,107 @@
/**
* =============================================================================
* GLOBAL DESIGN SYSTEM — Shahi Kitchen
* =============================================================================
*
* Royal cream + gold visual identity for Shahi Kitchen.
* Bright, warm, appetizing, and luxurious without being dark.
*/
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
/* === Refined Royal Light Palette - Warm Cream + Gold === */
/* Backgrounds */
--bg: #F8F5F0;
--bg-elevated: #F5F1E9;
--surface: #FFFFFF;
--surface-subtle: #F2EDE4;
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
/* Gold System */
--gold: #B38B4D;
--gold-dark: #8C6B3A;
--gold-light: #C9A46B;
--gold-muted: #d4a73d;
/* Emerald for contrast (kept from recent improvements) */
--emerald: #0f5a4a;
/* Text */
--text: #2C2520;
--text-muted: #6B665F;
--text-light: #8A8478;
/* Borders */
--border: #EDE6D9;
/* Typography */
--font-display: var(--font-playfair);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/* Motion */
--ease: cubic-bezier(0.25, 1, 0.5, 1);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
/* Base */
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-sans);
background-color: var(--bg);
color: var(--text);
}
h1, h2, h3, h4 {
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -0.025em;
color: var(--text);
}
/* Premium Buttons */
.btn-primary {
background: linear-gradient(135deg, var(--gold), var(--gold-muted));
color: #241806;
font-weight: 700;
transition: all 0.2s var(--ease);
}
.btn-primary:hover {
transform: translateY(-1px);
}
.btn-outline {
border: 1px solid var(--gold);
color: var(--gold-dark);
background: transparent;
font-weight: 600;
transition: all 0.2s var(--ease);
}
.btn-outline:hover {
background: var(--gold);
color: #241806;
}
/* Glass effect helper */
.glass {
background: rgba(255, 253, 248, 0.72);
backdrop-filter: blur(20px);
}
/* Luxury card style */
.luxury-card {
border-radius: 2rem;
transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
}
.luxury-card:hover {
transform: translateY(-4px);
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.12);
}
/* Mesmerizing shimmer animation for premium buttons (used in Navbar) */
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
+80 -5
View File
@@ -1,22 +1,89 @@
/**
* ROOT LAYOUT — Shahi Kitchen (shahikitchen.se)
*
* This is the single root layout for the entire Next.js App Router application.
*
* RESPONSIBILITIES:
* - Set up global metadata (title, description, favicon) for SEO and social sharing
* - Load the premium typography system:
* • Playfair Display (display/serif) → used for headings via --font-playfair
* • Geist Sans (system UI) → body text
* • Geist Mono → code / tabular data
* - Initialize the GLOBAL CART SYSTEM:
* • CartProvider wraps the whole tree so any page/component can use `useCart()`
* • CartDrawer is rendered once at the root (slide-in basket panel)
* • Sonner <Toaster> provides beautiful toast notifications used by the cart
*
* ARCHITECTURAL NOTES:
* - We deliberately render <CartDrawer /> and <Toaster /> at the root level instead of
* inside individual pages. This guarantees only ONE instance exists and prevents
* duplicate drawers/toasts when navigating.
* - The cream/gold royal theme tokens live in globals.css and are referenced via
* Tailwind arbitrary values (e.g. bg-[#F8F5F0]).
*
* FUTURE DEVELOPERS:
* - To add a new global provider (theme, analytics, etc.), wrap it here.
* - WhatsApp number for orders is currently hardcoded in CartDrawer.tsx (46709864995).
* - If you ever split the cart into a separate modal library, keep the provider here.
*/
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Playfair_Display, Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from "sonner";
import { CartProvider } from "@/components/CartContext";
import CartDrawer from "@/components/CartDrawer";
import { LanguageProvider } from "@/lib/language-context";
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
style: ["normal", "italic"],
});
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
/**
* Next.js Metadata API (static)
* These values power:
* - Browser tab title
* - Search engine results
* - Social cards (Open Graph / Twitter) when shared
*
* For dynamic per-page metadata, use `generateMetadata()` in page files.
*/
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Shahi Kitchen | Authentic Indian & Pakistani Restaurant in Gothenburg",
description: "Experience royal flavors at Shahi Kitchen in Askim, Gothenburg. Authentic Indian & Pakistani cuisine, famous lunch buffet, and traditional sweets since 2016.",
icons: {
icon: "/favicon.ico",
},
};
/**
* RootLayout
*
* The ONLY place in the app where we initialize:
* 1. CartProvider → gives every descendant access to `useCart()` hook
* 2. CartDrawer → the actual slide-in basket UI (controlled via context)
* 3. Toaster → global toast surface used by cart (Sonner library)
*
* IMPORTANT:
* - Never wrap the entire app in another CartProvider — it will break the singleton.
* - The body uses the royal cream background (#F8F5F0) as the base canvas.
* - `antialiased` + font variables are applied once at the root for consistency.
*/
export default function RootLayout({
children,
}: Readonly<{
@@ -25,9 +92,17 @@ export default function RootLayout({
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${playfair.variable} ${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="min-h-full flex flex-col bg-[#F8F5F0] text-[#2C2A26]">
<LanguageProvider>
<CartProvider>
{children}
<CartDrawer />
<Toaster position="top-center" richColors closeButton />
</CartProvider>
</LanguageProvider>
</body>
</html>
);
}
+132
View File
@@ -0,0 +1,132 @@
/**
* LOCATIONS PAGE
*
* Dedicated page for the restaurant's two physical branches.
*
* ARCHITECTURE:
* - Two branches are modeled as distinct concepts:
* 1. Shahi Kitchen Askim (Sisjön) → Full restaurant + famous lunch buffet
* 2. Shahi Sweets Backaplan → Sweets, snacks, café, halwa puri etc.
* - Both are operated by the same team (explicitly stated at the bottom)
* - Google Maps embeds are intentionally simple (lazy loaded)
*
* FUTURE:
* - When real online ordering launches, this page could show "Order from Askim"
* vs "Order from Backaplan" with different delivery radii.
* - Instagram handles for each location are mentioned where relevant.
*/
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
export default function LocationsPage() {
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="max-w-5xl mx-auto px-6 pt-20 pb-16">
<div className="text-center mb-12">
<div className="text-[#B38B4D] text-xs tracking-[3px] mb-3">WHERE TO FIND US</div>
<h1 className="text-6xl md:text-7xl tracking-[-2.5px] leading-none mb-4">Our Locations</h1>
<p className="text-xl text-[#6B665F] max-w-md mx-auto">
Two branches in Gothenburg both serving authentic flavors with the same royal hospitality.
</p>
</div>
<div className="flex justify-center mb-8">
<a href="/#contact" className="btn-primary px-8 py-3 rounded-full text-sm tracking-[0.5px]">
Make a Reservation
</a>
</div>
<div className="grid md:grid-cols-2 gap-8">
{/* Branch 1: Askim */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">RESTAURANT &amp; BUFFET</div>
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Kitchen Askim (Sisjön)</h2>
<div className="space-y-5 text-[15px]">
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
<div>Datavägen 10A<br />436 32 Askim, Gothenburg</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
<a href="tel:031288910" className="block hover:text-[#B38B4D]">031-28 89 10</a>
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
<div>Monday Sunday 11:00 21:00</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
<div className="text-[#6B665F]">Full restaurant experience with our popular lunch buffet, à la carte, and traditional Pakistani &amp; Indian dishes.</div>
</div>
</div>
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130.5!2d11.95!3d57.65!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sDatav%C3%A4gen%2010A%2C%20436%2032%20Askim!5e0!3m2!1sen!2sse!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
/>
</div>
</div>
{/* Branch 2: Backaplan */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">SWEETS, SNACKS &amp; CAFÉ</div>
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Sweets Backaplan</h2>
<div className="space-y-5 text-[15px]">
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
<div>Krokegårdsgatan 5<br />417 30 Göteborg (Backaplan)</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
<div>Varies please check our Instagram <span className="text-[#B38B4D]">@shahisweets_bp</span> for current hours.</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
<div className="text-[#6B665F]">Fresh mithai (sweets), savory snacks, halwa puri, biryani, nihari, and café-style seating.</div>
</div>
</div>
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130!2d11.92!3d57.72!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sKrokeg%C3%A5rdsgatan%205%2C%20417%2030%20G%C3%B6teborg!5e0!3m2!1sen!2sse!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
/>
</div>
</div>
</div>
<div className="mt-12 text-center text-sm text-[#6B665F]">
Both branches are operated by the same team and share the same passion for authentic flavors.
</div>
</div>
<Footer />
</div>
);
}
+343
View File
@@ -0,0 +1,343 @@
"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>
);
}
+304 -58
View File
@@ -1,65 +1,311 @@
import Image from "next/image";
"use client";
/**
* SHAHI KITCHEN HOMEPAGE
* Version with advanced Signature Menu + elegant hero (pre full Sultan redesign)
*/
import React, { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Lenis from "lenis";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { Crown, Clock, ArrowRight, UtensilsCrossed } from "lucide-react";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { useCart } from "@/components/CartContext";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
gsap.registerPlugin(ScrollTrigger);
export default function ShahiKitchenHomepage() {
const { addToCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
const [menuFilter, setMenuFilter] = useState<"All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweet">("All");
// Lenis smooth scroll
useEffect(() => {
const lenis = new Lenis({
duration: 1.1,
easing: (t: number) => Math.min(1, 1.001 * (-Math.pow(2, -10 * t) + 1)),
smoothWheel: true,
});
const raf = (time: number) => { lenis.raf(time); requestAnimationFrame(raf); };
requestAnimationFrame(raf);
return () => lenis.destroy();
}, []);
const signatureDishes = [
{
id: "chicken-biryani",
name: "Chicken Biryani",
category: "Rice",
price: 149,
time: "32 min",
desc: "Fragrant aged basmati layered with spiced chicken, saffron & caramelized onions",
image: "chicken-biryani-poster.jpg"
},
{
id: "bong-nihari",
name: "Bong Nihari",
category: "Curry",
price: 199,
time: "45 min",
desc: "Slow-cooked beef shank in rich aromatic gravy with ginger, lemon & fresh naan",
image: "bong-nihari-poster.jpg"
},
{
id: "panipuri",
name: "Golgappy / Panipuri",
category: "Street",
price: 69,
time: "15 min",
desc: "Crispy hollow puris filled with spiced chickpeas, potatoes & tangy tamarind water",
image: "panipuri-poster.jpg"
},
{
id: "lamm-palak",
name: "Lamm Palak",
category: "Meat",
price: 179,
time: "30 min",
desc: "Tender lamb cooked with fresh spinach in a flavorful mild gravy",
image: "lamm-palak-poster.jpg"
},
{
id: "chicken-karahi",
name: "Chicken Karahi",
category: "Curry",
price: 149,
time: "28 min",
desc: "Wok-tossed chicken in a robust tomato, chili & ginger gravy",
image: "chicken-karahi-poster.jpg"
},
{
id: "chicken-haleem",
name: "Chicken Haleem",
category: "Curry",
price: 149,
time: "35 min",
desc: "Slow-cooked shredded chicken with lentils, wheat & aromatic spices",
image: "chicken-haleem-poster.jpg"
},
{
id: "tikka-boti-roll",
name: "Tikka Boti Roll",
category: "Roll",
price: 99,
time: "20 min",
desc: "Juicy chicken tikka wrapped in soft naan with mint chutney & onions",
image: "tikka-boti-roll-poster.jpg"
},
{
id: "jalebi",
name: "Jalebi",
category: "Sweet",
price: 119,
time: "12 min",
desc: "Crispy golden saffron spirals soaked in fragrant sugar syrup",
image: "jalebi-poster.jpg"
},
{
id: "kulfi",
name: "Kulfi",
category: "Sweet",
price: 39,
time: "10 min",
desc: "Traditional frozen milk dessert with cardamom, pistachio & saffron",
image: "kulfi-poster.jpg"
},
];
const filtered = menuFilter === "All" ? signatureDishes : signatureDishes.filter(d => d.category === menuFilter);
const addDish = (d: any) => {
addToCart({ id: d.id, name: d.name, price: d.price, image: d.image });
};
// Advanced GSAP effects for signature cards
useEffect(() => {
const cards = document.querySelectorAll<HTMLElement>(".signature-card");
gsap.fromTo(cards,
{ opacity: 0, y: 45 },
{
opacity: 1, y: 0, duration: 0.9, ease: "power3.out", stagger: 0.06,
scrollTrigger: { trigger: ".signature-menu-grid", start: "top 82%", once: true }
}
);
cards.forEach((card) => {
const image = card.querySelector(".signature-image") as HTMLElement | null;
let bounds: DOMRect;
const onMouseMove = (e: MouseEvent) => {
if (!bounds) bounds = card.getBoundingClientRect();
const x = ((e.clientX - bounds.left) / bounds.width - 0.5) * 2;
const y = ((e.clientY - bounds.top) / bounds.height - 0.5) * 2;
gsap.to(card, {
rotationY: x * 11, rotationX: -y * 8, transformPerspective: 1400, duration: 0.35, ease: "power2.out"
});
if (image) gsap.to(image, { x: x * 8, y: y * 6, duration: 0.45, ease: "power2.out" });
};
const onMouseLeave = () => {
gsap.to(card, { rotationY: 0, rotationX: 0, duration: 1.1, ease: "elastic.out(1, 0.45)" });
if (image) gsap.to(image, { x: 0, y: 0, scale: 1, duration: 0.9, ease: "power2.out" });
};
const onMouseEnter = () => {
if (image) gsap.to(image, { scale: 1.12, duration: 1.1, ease: "power2.out" });
};
card.addEventListener("mousemove", onMouseMove);
card.addEventListener("mouseleave", onMouseLeave);
card.addEventListener("mouseenter", onMouseEnter);
(card as any)._cleanup = () => {
card.removeEventListener("mousemove", onMouseMove);
card.removeEventListener("mouseleave", onMouseLeave);
card.removeEventListener("mouseenter", onMouseEnter);
};
});
return () => {
cards.forEach(card => (card as any)._cleanup?.());
};
}, [filtered]);
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
{/* HERO - Full Banner Video with responsive framing */}
<section className="relative min-h-[70dvh] sm:min-h-[78dvh] md:min-h-[85dvh] lg:min-h-[92dvh] xl:min-h-[100dvh]
flex items-center justify-center pt-20 lg:pt-[88px] overflow-hidden bg-[#fbf7ef]">
{/* Banner Video - Optimized positioning per device */}
<video
autoPlay
muted
loop
playsInline
className="absolute inset-0 z-10 w-full h-full object-cover
object-[50%_22%] sm:object-[50%_26%] md:object-[50%_30%]
lg:object-[50%_35%] xl:object-[50%_38%]"
>
<source src="/videos/banner.webm" type="video/webm" />
<source src="/videos/banner.mp4" type="video/mp4" />
</video>
{/* Subtle gradient to improve visibility of baked-in text */}
<div className="absolute inset-0 z-20 bg-gradient-to-b from-black/5 via-transparent to-black/30" />
</section>
{/* ADVANCED SIGNATURE MENU */}
<section id="menu" className="bg-[#fffdf8] px-6 py-20 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10">
<div>
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-[#fff6dc] px-4 py-1.5 text-sm font-semibold text-[#60420d]">
<UtensilsCrossed className="h-4 w-4" /> {t.signatureMenu.title}
</div>
<h2 className="font-serif text-6xl leading-none tracking-[-0.055em] md:text-7xl">{t.signatureMenu.title}</h2>
</div>
<p className="max-w-md text-lg font-medium text-[#4b5563]">{t.signatureMenu.subtitle}</p>
</div>
{/* Filters */}
<div className="mb-8 flex flex-wrap gap-2">
{["All", "Rice", "Curry", "Meat", "Street", "Roll", "Sweet"].map((cat) => (
<button
key={cat}
onClick={() => setMenuFilter(cat as any)}
className={`rounded-full px-6 py-3 text-sm font-bold transition-all ${menuFilter === cat ? "bg-[#101724] text-white" : "border border-[#e5e1d7] bg-white text-[#182235] hover:border-[#c99a2e] hover:bg-[#fff6dc]"}`}
>
{cat}
</button>
))}
</div>
{/* Menu Grid with Advanced Effects */}
<div className="signature-menu-grid grid gap-5 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
<AnimatePresence>
{filtered.map((dish, index) => (
<motion.div
key={dish.id}
layout
onClick={() => addDish(dish)}
className="signature-card group flex cursor-pointer flex-col overflow-hidden rounded-[2rem] border border-[#EDE6D9] bg-white"
>
<div className="relative h-48 overflow-hidden bg-[#F2EDE4]">
<img
src={`/images/dishes/${dish.image}`}
alt={dish.name}
className="signature-image absolute inset-0 w-full h-full object-cover transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/25" />
<div className="absolute top-4 right-4 px-4 py-1 rounded-full bg-white/95 text-[#B38B4D] text-sm font-medium tracking-tight shadow-sm border border-[#EDE6D9]">
{dish.price} kr
</div>
</div>
<div className="p-5 flex-1 flex flex-col">
<h4 className="text-[21px] leading-tight tracking-[-0.5px] mb-2 group-hover:text-[#B38B4D] transition-colors">
{dish.name}
</h4>
<p className="text-[#6B665F] text-[13.5px] leading-snug flex-1">{dish.desc}</p>
<button
onClick={(e) => { e.stopPropagation(); addDish(dish); }}
className="mt-5 w-full py-3 text-sm tracking-[0.6px] border border-[#B38B4D] text-[#B38B4D] rounded-full hover:bg-[#B38B4D] hover:text-white font-medium transition-all"
>
{t.signatureMenu.addToTable}
</button>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
<div className="mt-10 text-center">
<a href="/menu" className="inline-flex items-center gap-2 text-sm font-bold tracking-wider text-[#B38B4D] hover:text-[#8C6B3A]">
{t.signatureMenu.viewFullMenu} <ArrowRight className="h-4 w-4" />
</a>
</div>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</section>
{/* THE SHAHI EXPERIENCE */}
<section id="experience" className="section bg-white border-y border-[#EDE6D9]">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-14">
<div className="text-[#B38B4D] text-xs tracking-[3px] mb-3">THE SHAHI WAY</div>
<h3 className="text-5xl md:text-6xl tracking-[-2px]">More than a meal.<br />A moment of royalty.</h3>
</div>
<div className="grid md:grid-cols-3 gap-6">
{[
{ title: "The Legendary Buffet", desc: "Our famous lunch buffet features over 20 rotating dishes — curries, biryanis, fresh naan, and sweets." },
{ title: "Shahi Sweets", desc: "Homemade mithai made daily. From fresh Jalebi to Rasmalai — the perfect sweet ending." },
{ title: "Warm Hospitality", desc: "Whether you're here for a quick lunch or a family celebration, you will always be treated like royalty." },
].map((item, index) => (
<div key={index} className="experience-card group relative border border-[#EDE6D9] p-9 rounded-2xl bg-[#F8F5F0] overflow-hidden">
<div className="text-[#B38B4D] text-6xl font-light mb-9 tracking-[-2px]">0{index + 1}</div>
<h4 className="text-[29px] tracking-[-0.8px] mb-5 text-[#2C2A26] leading-tight">{item.title}</h4>
<p className="text-[#6B665F] text-[15.5px] leading-relaxed">{item.desc}</p>
</div>
))}
</div>
</div>
</main>
</section>
<Footer />
</div>
);
}