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
+84 -22
View File
@@ -1,36 +1,98 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Shahikitchen — Animated Restaurant Website
## Getting Started
This directory contains the official website for **Shahi Kitchen**, a popular Indian & Pakistani restaurant in Gothenburg (Göteborg), Sweden.
First, run the development server:
**Domain**: [shahikitchen.se](https://shahikitchen.se)
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
---
## About Shahi Kitchen
Shahi Kitchen is an authentic Indian and Pakistani restaurant specializing in flavorful curries, tandoori dishes, biryanis, haleem, fresh naan, and traditional sweets. The restaurant is particularly well-known for its generous **lunch buffet**, which offers excellent value and is popular among locals in the Gothenburg area.
### Key Information
| Detail | Information |
|---------------------|--------------------------------------------------|
| **Full Name** | Shahi Kitchen (Shahikitchen / Shahi Kitchen Sisjön) |
| **Cuisine** | Indian & Pakistani (Halal options available) |
| **Address** | Datavägen 10A, 436 32 Askim, Sweden |
| **Area** | Sisjön / Askim (suburb of Gothenburg) |
| **Phone** | 031-28 89 10 / 0739-381089 (Imran) |
| **Email** | hello@shahikitchen.se |
| **Opened** | Around 2016 |
| **Specialties** | Lunch buffet, Biryani, Lamb Karahi, Curries, Sweets (Shahi Sweets) |
### Current Online Presence
- **Instagram**: [@shahikitchen](https://www.instagram.com/Shahikitchen/)
- **Facebook**: [facebook.com/shahikitchengbg](https://www.facebook.com/shahikitchengbg/)
- **Reviews**: ~4.1/5 on Google (600+ reviews)
- **Note**: As of 2026, the domain `shahikitchen.se` is parked. This project will give the restaurant a proper modern website.
---
## Tech Stack
- **Next.js 15** (App Router) + TypeScript
- **Tailwind CSS**
- **GSAP** — for advanced scroll and UI animations
- **React Three Fiber** — for 3D experiences (dishes)
- **Spline** (optional) — for high-quality hosted 3D scenes
- **Framer Motion** (optional alternative to GSAP for simpler animations)
---
## Project Structure (Planned)
```
shahikitchen/
├── app/
│ ├── layout.tsx
│ ├── page.tsx # Homepage (Hero + Menu + About + Contact)
│ └── globals.css
├── components/
│ ├── MenuSection.tsx
│ ├── Dish3D.tsx # React Three Fiber or Spline component
│ ├── ReservationForm.tsx
│ └── ...
├── lib/
│ └── menu-data.ts # Menu items
├── public/
│ └── images/
└── ...
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
---
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Development
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
### Getting Started
## Learn More
```bash
# Install dependencies
npm install
To learn more about Next.js, take a look at the following resources:
# Run development server
npm run dev
```
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
Open [http://localhost:3000](http://localhost:3000) in your browser.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
### Planned Features (in order)
## Deploy on Vercel
1. ✅ Create Next.js project
2. ⏳ Build normal restaurant homepage (Hero, Navigation, About)
3. ⏳ Add menu items (name, description, price)
4. ⏳ Style with Tailwind CSS
5. ⏳ Add animations (GSAP / Framer Motion)
6. ⏳ Add 3D dishes (React Three Fiber + Spline)
7. ⏳ Reservation / Contact form
8. ⏳ Mobile responsive
9. ⏳ Deploy to Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
---
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
## License
This project is being built for Shahi Kitchen. All rights reserved.
+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>
);
}
+320
View File
@@ -0,0 +1,320 @@
'use client';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Environment } from '@react-three/drei';
import { Suspense, useMemo, useRef } from 'react';
import * as THREE from 'three';
// ================== SMOKE PARTICLES ==================
function SmokeParticles({ count = 120 }: { count?: number }) {
const pointsRef = useRef<THREE.Points>(null!);
const particles = useMemo(() => {
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
const ages = new Float32Array(count);
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Start around the top of the gravy
positions[i3 + 0] = (Math.random() - 0.5) * 2.2;
positions[i3 + 1] = 0.6 + Math.random() * 0.3;
positions[i3 + 2] = (Math.random() - 0.5) * 2.0;
velocities[i3 + 0] = (Math.random() - 0.5) * 0.008;
velocities[i3 + 1] = 0.012 + Math.random() * 0.018;
velocities[i3 + 2] = (Math.random() - 0.5) * 0.008;
ages[i] = Math.random() * 3.5;
sizes[i] = 0.12 + Math.random() * 0.18;
}
return { positions, velocities, ages, sizes };
}, [count]);
useFrame((state, delta) => {
const points = pointsRef.current;
if (!points) return;
const pos = points.geometry.attributes.position as THREE.BufferAttribute;
const posArray = pos.array as Float32Array;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Age and reset
particles.ages[i] += delta * 0.9;
if (particles.ages[i] > 3.8) {
// Respawn at the top of the gravy
particles.ages[i] = 0;
posArray[i3 + 0] = (Math.random() - 0.5) * 2.1;
posArray[i3 + 1] = 0.55 + Math.random() * 0.15;
posArray[i3 + 2] = (Math.random() - 0.5) * 1.9;
particles.velocities[i3 + 0] = (Math.random() - 0.5) * 0.009;
particles.velocities[i3 + 1] = 0.014 + Math.random() * 0.02;
} else {
// Rise with some turbulence
posArray[i3 + 0] += particles.velocities[i3 + 0] + Math.sin(state.clock.elapsedTime * 1.5 + i) * 0.002;
posArray[i3 + 1] += particles.velocities[i3 + 1];
posArray[i3 + 2] += particles.velocities[i3 + 2] + Math.cos(state.clock.elapsedTime * 1.2 + i) * 0.002;
// Slow down as it rises
particles.velocities[i3 + 1] *= 0.992;
}
}
pos.needsUpdate = true;
});
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(particles.positions, 3));
return geo;
}, [particles.positions]);
return (
<points ref={pointsRef} geometry={geometry}>
<pointsMaterial
size={0.22}
color="#f4e9d8"
transparent
opacity={0.32}
depthWrite={false}
sizeAttenuation={true}
/>
</points>
);
}
// ================== BOWL ==================
function Bowl() {
return (
<group>
{/* Outer bowl */}
<mesh position={[0, -0.25, 0]} castShadow receiveShadow>
<cylinderGeometry args={[2.35, 1.95, 1.95, 72, 1, true]} />
<meshPhongMaterial
color="#2f2723"
shininess={45}
specular="#3a2f2a"
side={THREE.DoubleSide}
/>
</mesh>
{/* Inner bowl wall */}
<mesh position={[0, -0.25, 0]} castShadow receiveShadow>
<cylinderGeometry args={[2.1, 1.72, 1.85, 72, 1, true]} />
<meshPhongMaterial
color="#1f1a17"
shininess={25}
side={THREE.DoubleSide}
/>
</mesh>
{/* Bowl bottom inside */}
<mesh position={[0, -1.15, 0]} receiveShadow>
<cylinderGeometry args={[1.72, 1.72, 0.12, 72]} />
<meshPhongMaterial color="#1f1a17" />
</mesh>
</group>
);
}
// ================== GRAVY ==================
function Gravy() {
return (
<group>
{/* Main thick gravy */}
<mesh position={[0, 0.05, 0]} receiveShadow>
<cylinderGeometry args={[2.0, 1.65, 1.35, 72]} />
<meshPhongMaterial
color="#d46f2e"
shininess={95}
specular="#ffe8c4"
/>
</mesh>
{/* Creamy top layer */}
<mesh position={[0, 0.55, 0]}>
<cylinderGeometry args={[1.92, 1.62, 0.42, 72]} />
<meshPhongMaterial
color="#f0a45f"
shininess={120}
specular="#fff4d9"
/>
</mesh>
{/* Glossy highlights layer */}
<mesh position={[0, 0.68, 0]}>
<cylinderGeometry args={[1.78, 1.55, 0.18, 72]} />
<meshPhongMaterial
color="#f8c48a"
shininess={140}
specular="#ffffff"
transparent
opacity={0.65}
/>
</mesh>
</group>
);
}
// ================== CHICKEN PIECES ==================
function ChickenPieces() {
const pieces = [
{ pos: [0.6, 0.45, 0.1], rot: [0.6, 1.2, 0.3], scale: 1.0 },
{ pos: [-0.75, 0.38, 0.55], rot: [-0.4, -0.9, 0.5], scale: 0.95 },
{ pos: [0.15, 0.52, -0.85], rot: [0.9, 0.4, -0.6], scale: 1.05 },
{ pos: [-0.55, 0.42, -0.4], rot: [-0.7, 1.6, 0.2], scale: 0.9 },
{ pos: [0.9, 0.35, -0.55], rot: [0.3, -1.1, -0.4], scale: 0.98 },
{ pos: [-0.2, 0.48, 0.75], rot: [-0.5, 0.7, 0.8], scale: 0.92 },
];
return (
<>
{pieces.map((p, i) => (
<group key={i} position={p.pos as [number, number, number]} rotation={p.rot as [number, number, number]} scale={p.scale}>
{/* Main chicken body */}
<mesh castShadow>
<capsuleGeometry args={[0.42, 0.65, 8]} />
<meshPhongMaterial
color="#b84f25"
shininess={55}
specular="#3a1f14"
/>
</mesh>
{/* Extra volume */}
<mesh position={[0.12, 0.08, -0.1]} castShadow>
<sphereGeometry args={[0.38]} />
<meshPhongMaterial color="#a64520" shininess={40} />
</mesh>
</group>
))}
</>
);
}
// ================== HERBS & SPICES ==================
function Toppings() {
return (
<>
{/* Green herbs */}
{Array.from({ length: 18 }).map((_, i) => {
const angle = i * 0.7 + (i % 3) * 0.3;
const radius = 0.55 + (i % 4) * 0.22;
return (
<mesh
key={`herb-${i}`}
position={[
Math.cos(angle) * radius,
0.82,
Math.sin(angle) * radius * 0.9
]}
rotation={[Math.random() - 0.5, i, Math.random() - 0.5]}
>
<planeGeometry args={[0.22, 0.09]} />
<meshPhongMaterial
color="#2d5c3f"
side={THREE.DoubleSide}
transparent
opacity={0.85}
/>
</mesh>
);
})}
{/* Small spice bits */}
{Array.from({ length: 26 }).map((_, i) => (
<mesh
key={`spice-${i}`}
position={[
(Math.random() - 0.5) * 2.6,
0.78 + Math.random() * 0.12,
(Math.random() - 0.5) * 2.3
]}
>
<sphereGeometry args={[0.035 + Math.random() * 0.025]} />
<meshPhongMaterial color={i % 4 === 0 ? "#8c3a1f" : "#3a2a1f"} />
</mesh>
))}
</>
);
}
// ================== MAIN MODEL ==================
function ButterChickenModel() {
return (
<group>
<Bowl />
<Gravy />
<ChickenPieces />
<Toppings />
{/* Rising Smoke */}
<SmokeParticles count={95} />
</group>
);
}
// ================== MAIN EXPORT ==================
export default function ButterChicken3D() {
return (
<div className="w-full h-full min-h-[420px] rounded-2xl overflow-hidden bg-[#F2EDE4] relative">
<Canvas
camera={{ position: [0, 3.8, 8.5], fov: 40 }}
style={{ background: 'transparent' }}
gl={{
antialias: true,
alpha: true,
preserveDrawingBuffer: true,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1.05,
}}
>
<Suspense fallback={null}>
{/* Warm restaurant lighting */}
<ambientLight intensity={0.28} color="#fff4e6" />
<directionalLight
position={[7, 13, 5]}
intensity={1.85}
color="#fff0d0"
castShadow
/>
<directionalLight
position={[-7, 5, -8]}
intensity={0.95}
color="#ffe8c4"
/>
<pointLight position={[-3, 4, 7]} intensity={0.7} color="#fff8e7" />
<ButterChickenModel />
<Environment preset="apartment" />
</Suspense>
<OrbitControls
enablePan={false}
enableZoom={true}
minDistance={4.5}
maxDistance={13}
minPolarAngle={Math.PI * 0.18}
maxPolarAngle={Math.PI * 0.82}
enableDamping
dampingFactor={0.12}
autoRotate={false}
/>
</Canvas>
<div className="absolute bottom-3 right-3 text-[10px] text-[#8A8478] tracking-widest pointer-events-none">
DRAG TO ROTATE SCROLL TO ZOOM
</div>
</div>
);
}
+310
View File
@@ -0,0 +1,310 @@
'use client';
/**
* =============================================================================
* GLOBAL CART SYSTEM — Shahi Kitchen
* =============================================================================
*
* This file implements a production-grade, persistent shopping cart using:
* • React Context + useReducer (predictable state machine)
* • localStorage persistence (survives page refresh / browser restart)
* • Sonner toasts with "View Cart" action
* • Derived totals (totalItems, totalPrice) computed on every render
*
* WHY THIS ARCHITECTURE?
* - useReducer gives us a single source of truth and easy debugging (time-travel capable)
* - Context lets ANY component (menu cards, navbar icon, drawer, future checkout)
* access cart state without prop drilling
* - localStorage key is namespaced ("shahi-kitchen-cart") so multiple sites on same
* domain won't collide
*
* IMPORTANT INTEGRATION POINTS:
* - CartProvider is mounted ONCE in app/layout.tsx (root of the tree)
* - useCart() hook is used in:
* • app/menu/page.tsx → addToCart() on every dish
* • components/Navbar.tsx → shows live count + opens drawer
* • components/CartDrawer.tsx → full management UI + WhatsApp ordering
*
* WHATSAPP ORDER FLOW (the "checkout" for this restaurant):
* The cart never talks to a real payment gateway. Instead, the drawer builds a
* beautifully formatted message and opens https://wa.me/46709864995 with it pre-filled.
* This matches exactly how the reservation form on the homepage works.
*
* FUTURE IMPROVEMENTS (documented for next developer):
* - Add "special instructions" per item or for the whole order
* - Persist cart across different devices via Supabase / Firebase (when real ordering launches)
* - Add estimated pickup time or "order for later" picker
* - Minimum order value enforcement
*
* GOTCHAS:
* - The localStorage restore logic runs only once on mount. It dispatches multiple
* actions on purpose so the reducer stays pure.
* - Toasts are intentionally fired ONLY from addToCart(), never from restore.
*/
import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react';
import { toast } from 'sonner';
/**
* TYPE DEFINITIONS
*
* CartItem — the shape stored in state and localStorage.
* image is optional because some drinks don't have photos yet.
*
* CartState — minimal internal state. We derive totalItems/totalPrice in the provider
* so consumers never have to recalculate.
*
* CartAction — discriminated union. This is what makes the reducer easy to reason about.
*/
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
image?: string;
}
interface CartState {
items: CartItem[];
isOpen: boolean;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'TOGGLE_CART' }
| { type: 'OPEN_CART' }
| { type: 'CLOSE_CART' };
interface CartContextType {
items: CartItem[];
isOpen: boolean;
totalItems: number;
totalPrice: number;
addToCart: (item: Omit<CartItem, 'quantity'>) => void;
removeFromCart: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
toggleCart: () => void;
openCart: () => void;
closeCart: () => void;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
/**
* THE REDUCER — Single Source of Truth for All Cart Mutations
*
* This is a classic Redux-style reducer: pure, predictable, easy to test.
*
* DESIGN DECISIONS:
* - ADD_ITEM automatically increments quantity if the dish already exists
* (very common restaurant UX — user taps the same samosa twice)
* - UPDATE_QUANTITY with quantity <= 0 removes the item (no negative quantities)
* - Only the drawer can toggle isOpen. Menu cards only ever call addToCart().
* - CLEAR_CART is exposed for the "Clear basket" buttons in the drawer header/footer.
*/
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
// Increment existing line item (most common case after first add)
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
} else {
// Brand new line item
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
};
case 'UPDATE_QUANTITY':
if (action.payload.quantity <= 0) {
// Treat zero or negative as "remove this line"
return {
...state,
items: state.items.filter(item => item.id !== action.payload.id),
};
}
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
};
case 'CLEAR_CART':
return { ...state, items: [] };
case 'TOGGLE_CART':
return { ...state, isOpen: !state.isOpen };
case 'OPEN_CART':
return { ...state, isOpen: true };
case 'CLOSE_CART':
return { ...state, isOpen: false };
default:
return state;
}
}
/**
* PERSISTENCE LAYER
*
* We use a single well-namespaced localStorage key so the cart survives:
* - Page refresh
* - Browser restart
* - User navigating away and coming back days later
*
* The restore logic is intentionally verbose (multiple dispatches) because we want
* the reducer to remain the only place that understands "how to add an item".
* This keeps the restore path debuggable and future-proof.
*/
const CART_STORAGE_KEY = 'shahi-kitchen-cart';
export function CartProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
isOpen: false,
});
// ONE-TIME HYDRATION FROM LOCALSTORAGE
// Runs only on initial mount. Restores previous session's cart silently (no toasts).
useEffect(() => {
const savedCart = localStorage.getItem(CART_STORAGE_KEY);
if (savedCart) {
try {
const parsedItems = JSON.parse(savedCart);
// We replay through the reducer so all business rules stay in one place
parsedItems.forEach((item: CartItem) => {
dispatch({ type: 'ADD_ITEM', payload: item });
// If the saved quantity was > 1 we need a second dispatch to correct it
if (item.quantity > 1) {
dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: item.id, quantity: item.quantity }
});
}
});
} catch (error) {
console.error('Failed to load cart from localStorage');
}
}
}, []);
// AUTO-PERSIST ON EVERY CHANGE
// Extremely simple and reliable. In a future real ordering system you would
// debounce this and also sync to a backend.
useEffect(() => {
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(state.items));
}, [state.items]);
/**
* DERIVED STATE (recomputed on every render — cheap and always fresh)
*/
const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
/**
* PUBLIC API — these functions are what the rest of the app calls.
*
* addToCart is special: it also triggers the beautiful Sonner toast with
* an action button that opens the drawer. This is the primary feedback mechanism.
*/
const addToCart = (item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item });
toast.success(`Added ${item.name} to cart`, {
description: `${item.price} kr`,
action: {
label: "View Cart",
onClick: () => dispatch({ type: 'OPEN_CART' }),
},
});
};
const removeFromCart = (id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id: string, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const toggleCart = () => {
dispatch({ type: 'TOGGLE_CART' });
};
const openCart = () => {
dispatch({ type: 'OPEN_CART' });
};
const closeCart = () => {
dispatch({ type: 'CLOSE_CART' });
};
/**
* CONTEXT PROVIDER VALUE
* Everything a consumer might need is exposed here.
* We intentionally do NOT expose dispatch directly — all mutations go through
* the named methods above (better encapsulation + future-proofing).
*/
return (
<CartContext.Provider
value={{
items: state.items,
isOpen: state.isOpen,
totalItems,
totalPrice,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
toggleCart,
openCart,
closeCart,
}}
>
{children}
</CartContext.Provider>
);
}
/**
* useCart — The only way the rest of the app should consume cart state/actions.
*
* Throws a clear error if someone tries to use it outside the provider tree
* (catches 99% of integration mistakes during development).
*/
export function useCart() {
const context = useContext(CartContext);
if (context === undefined) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
+225
View File
@@ -0,0 +1,225 @@
'use client';
/**
* =============================================================================
* CART DRAWER (SLIDE-IN BASKET PANEL)
* =============================================================================
*
* This is the visual "basket" that slides in from the right when the user
* clicks the cart icon in the navbar or the "View Cart" toast action.
*
* KEY DESIGN CHOICES:
* - Fixed position, full-height, max-w-md (beautiful on both mobile and desktop)
* - Backdrop click closes it (standard mobile pattern)
* - z-[70] sits above almost everything (including the sticky category nav)
* - All cart mutations go through the context — this component is "dumb" UI only
*
* THE ORDERING FLOW (very important for restaurant context):
* Instead of a traditional Stripe checkout, we generate a pre-filled WhatsApp
* message containing every line item + quantities + grand total.
* This matches the restaurant's current real-world ordering process.
* The number 46709864995 is the same one used in the homepage reservation form.
*
* FUTURE ENHANCEMENTS (documented here so nothing is forgotten):
* - Add "Special requests" textarea per order
* - Show estimated preparation time
* - Allow "Order for later" date/time picker
* - Split "Pickup" vs "Delivery" with different messaging
* - After WhatsApp opens, optionally clear the cart (or keep it — current choice)
*/
import { useCart } from './CartContext';
import Link from 'next/link';
export default function CartDrawer() {
const {
items,
isOpen,
closeCart,
totalPrice,
removeFromCart,
updateQuantity,
clearCart
} = useCart();
/**
* WHATSAPP DEEP LINK ORDERING
*
* Builds a human-readable, copy-paste friendly message that the restaurant staff
* can immediately understand and action.
*
* The format deliberately mirrors how the restaurant currently receives orders
* over the phone or via Instagram DMs.
*/
const orderViaWhatsApp = () => {
if (items.length === 0) return;
const lines = items
.map((item) => `${item.quantity} × ${item.name}${item.price * item.quantity} kr`)
.join('\n');
const message =
`Hello Shahi Kitchen 👋
I would like to place an order:
${lines}
Total: ${totalPrice} kr
Thank you!`;
const encoded = encodeURIComponent(message);
// This is the same WhatsApp business number used for table reservations on the homepage
window.open(`https://wa.me/46709864995?text=${encoded}`, '_blank');
};
// Guard clause — drawer only renders when explicitly opened via context
if (!isOpen) return null;
return (
<>
{/* SEMI-TRANSPARENT BACKDROP */}
{/* Clicking anywhere outside the drawer closes it (standard mobile pattern) */}
<div
className="fixed inset-0 bg-black/40 z-[60]"
onClick={closeCart}
/>
{/* THE ACTUAL DRAWER — slides in from right */}
{/* z-[70] ensures it sits above sticky nav, category pills, and most other UI */}
<div className="fixed top-0 right-0 h-full w-full max-w-md bg-[#F8F5F0] z-[70] shadow-2xl flex flex-col">
{/* HEADER — Title + item count + quick clear + close button */}
<div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]">
<div className="flex items-center gap-3">
<h2 className="text-2xl tracking-[-0.5px]">Your Basket</h2>
{items.length > 0 && (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-[#EDE6D9] text-[#6B665F]">
{items.length} {items.length === 1 ? 'item' : 'items'}
</span>
)}
</div>
<div className="flex items-center gap-3">
{items.length > 0 && (
<button
onClick={clearCart}
className="text-xs text-[#B38B4D] hover:underline"
>
Clear
</button>
)}
<button
onClick={closeCart}
className="text-[#6B665F] hover:text-[#2C2A26] text-2xl leading-none pl-1"
>
×
</button>
</div>
</div>
{/* SCROLLABLE CONTENT AREA */}
{/* Empty state is friendly and routes the user back to the menu */}
<div className="flex-1 overflow-y-auto p-6">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="text-6xl mb-4">🛒</div>
<p className="text-lg text-[#6B665F]">Your basket is empty</p>
<Link
href="/menu"
onClick={closeCart}
className="mt-6 text-[#B38B4D] hover:underline"
>
Browse the menu
</Link>
</div>
) : (
<div className="space-y-6">
{items.map((item) => (
<div key={item.id} className="flex gap-4 border-b border-[#EDE6D9] pb-6">
<div className="flex-1">
<div className="flex justify-between">
<div>
<h4 className="font-medium tracking-[-0.3px]">{item.name}</h4>
<p className="text-sm text-[#6B665F]">{item.price} kr × {item.quantity}</p>
</div>
<div className="text-right font-medium">
{(item.price * item.quantity).toFixed(0)} kr
</div>
</div>
{/* Quantity controls */}
<div className="flex items-center gap-3 mt-3">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-8 h-8 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition"
>
</button>
<span className="w-6 text-center font-medium">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-8 h-8 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition"
>
+
</button>
<button
onClick={() => removeFromCart(item.id)}
className="ml-auto text-xs text-[#B38B4D] hover:underline"
>
Remove
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* STICKY FOOTER — always visible when cart has items */}
{/* Contains the two primary actions: WhatsApp (primary) + Phone (secondary) */}
{items.length > 0 && (
<div className="p-6 border-t border-[#EDE6D9] bg-white">
<button
onClick={clearCart}
className="text-xs text-[#8A8478] hover:text-[#B38B4D] mb-3 underline"
>
Clear basket
</button>
<div className="flex justify-between text-lg font-medium mb-4">
<span>Total</span>
<span>{totalPrice.toFixed(0)} kr</span>
</div>
<button
onClick={orderViaWhatsApp}
className="btn-primary w-full py-4 rounded-full text-base tracking-[0.5px] font-medium mb-2 flex items-center justify-center gap-2"
>
Send order via WhatsApp
</button>
<button
onClick={() => window.open('tel:031288910', '_self')}
className="btn-outline w-full py-3 rounded-full text-sm tracking-[0.5px] font-medium mb-3"
>
Call 031-28 89 10
</button>
<button
onClick={closeCart}
className="w-full text-sm text-[#6B665F] hover:text-[#2C2A26] pt-1"
>
Continue browsing
</button>
<p className="text-[10px] text-center text-[#8A8478] mt-4">
WhatsApp opens with your order pre-filled. We will confirm availability.
</p>
</div>
)}
</div>
</>
);
}
+112
View File
@@ -0,0 +1,112 @@
/**
* GLOBAL FOOTER
*
* Appears at the bottom of every page.
*
* Contains:
* - Brand + short tagline
* - Both physical locations with full addresses + phone/email
* - Opening hours (different per branch — Backaplan is more variable)
* - Quick links + social profiles
*
* Note: The phone numbers and addresses here are the canonical source.
* If the restaurant ever changes them, update this file + the contact section
* on the homepage + the locations page.
*/
import Link from "next/link";
export default function Footer() {
return (
<footer className="bg-[#F5F1E9] border-t border-[#EDE6D9] pt-14 pb-10 text-[#6B665F]">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-y-12 gap-x-8">
{/* Brand */}
<div>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl overflow-hidden border border-[#c99a2e]/30 bg-white p-0.5 shadow-sm">
<img
src="/images/logo-shahi-chef-icon.jpg"
alt="Shahi Kitchen Chef Logo"
className="w-full h-full object-contain"
/>
</div>
<span className="text-[#2C2A26] font-medium tracking-[-0.3px]">Shahi Kitchen</span>
</div>
<p className="text-sm leading-relaxed">
Authentic Indian &amp; Pakistani cuisine in Gothenburg since 2016.
</p>
</div>
{/* Contact - Both Locations */}
<div>
<div className="text-[#2C2A26] text-sm tracking-[1.5px] mb-4">OUR LOCATIONS</div>
<div className="space-y-4 text-sm">
<div>
<div className="font-medium text-[#2C2A26]">Shahi Kitchen (Askim / Sisjön)</div>
<div>Datavägen 10A, 436 32 Askim</div>
</div>
<div>
<div className="font-medium text-[#2C2A26]">Shahi Sweets (Backaplan)</div>
<div>Krokegårdsgatan 5, 417 30 Göteborg</div>
</div>
<div>
<a href="tel:0739381089" className="block hover:text-[#B38B4D] transition-colors">
0739-381089
</a>
<a href="mailto:hello@shahikitchen.se" className="block hover:text-[#B38B4D] transition-colors">
hello@shahikitchen.se
</a>
</div>
</div>
</div>
{/* Hours */}
<div>
<div className="text-[#2C2A26] text-sm tracking-[1.5px] mb-4">OPENING HOURS</div>
<div className="text-sm space-y-1">
<div><span className="font-medium">Askim:</span> MonSun 11:0021:00</div>
<div><span className="font-medium">Backaplan:</span> Check Instagram for current hours</div>
</div>
</div>
{/* Quick Links + Social */}
<div>
<div className="text-[#2C2A26] text-sm tracking-[1.5px] mb-4">EXPLORE</div>
<div className="flex flex-col gap-1.5 text-sm mb-8">
<Link href="/" className="hover:text-[#B38B4D] transition-colors">Home</Link>
<Link href="/menu" className="hover:text-[#B38B4D] transition-colors">Menu</Link>
<Link href="/#experience" className="hover:text-[#B38B4D] transition-colors">Our Experience</Link>
<Link href="/#contact" className="hover:text-[#B38B4D] transition-colors">Contact &amp; Reserve</Link>
</div>
<div className="text-[#2C2A26] text-sm tracking-[1.5px] mb-3">FOLLOW US</div>
<div className="flex gap-5 text-sm">
<a
href="https://www.instagram.com/Shahikitchen/"
target="_blank"
rel="noopener noreferrer"
className="hover:text-[#B38B4D] transition-colors"
>
Instagram
</a>
<a
href="https://www.facebook.com/shahikitchengbg/"
target="_blank"
rel="noopener noreferrer"
className="hover:text-[#B38B4D] transition-colors"
>
Facebook
</a>
</div>
</div>
</div>
<div className="mt-14 pt-8 border-t border-[#EDE6D9] text-xs tracking-widest flex flex-col md:flex-row md:items-center justify-between gap-y-2 text-[#8A8478]">
<div>© {new Date().getFullYear()} SHAHI KITCHEN GOTHENBURG. ALL RIGHTS RESERVED.</div>
<div>Made with tradition and heart.</div>
</div>
</div>
</footer>
);
}
+62
View File
@@ -0,0 +1,62 @@
'use client';
import { useState } from 'react';
import { useLanguage } from '@/lib/language-context';
import { languages, Language } from '@/lib/translations';
import { ChevronDown } from 'lucide-react';
export default function LanguageSwitcher() {
const { language, setLanguage } = useLanguage();
const [isOpen, setIsOpen] = useState(false);
const currentLang = languages.find(l => l.code === language)!;
const handleSelect = (lang: Language) => {
setLanguage(lang);
setIsOpen(false);
};
return (
<div className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1.5 rounded-full border border-[#c99a2e]/30 bg-white/70 px-3 py-1.5 text-sm font-medium text-[#101724] backdrop-blur-xl transition hover:border-[#c99a2e] hover:bg-white"
>
<span className="text-base">{currentLang.flag}</span>
<span className="hidden sm:inline text-xs font-semibold tracking-wider">{currentLang.native}</span>
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
{/* Dropdown */}
<div className="absolute right-0 top-full mt-2 z-50 w-44 rounded-2xl border border-[#c99a2e]/20 bg-[#fbf7ef] shadow-2xl overflow-hidden">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => handleSelect(lang.code)}
className={`w-full flex items-center gap-3 px-4 py-3 text-left text-sm transition hover:bg-[#fff6dc] ${
language === lang.code
? 'bg-[#fff6dc] text-[#0f5a4a] font-semibold'
: 'text-[#101724]'
}`}
>
<span className="text-xl">{lang.flag}</span>
<div className="flex flex-col">
<span>{lang.native}</span>
<span className="text-[10px] text-[#8a6a25] -mt-0.5">{lang.name}</span>
</div>
</button>
))}
</div>
</>
)}
</div>
);
}
+235
View File
@@ -0,0 +1,235 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useCart } from "./CartContext";
import { ShoppingBag, ArrowRight } from "lucide-react";
import LanguageSwitcher from "./LanguageSwitcher";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
import { motion } from "framer-motion";
import { usePathname } from "next/navigation";
/**
* =============================================================================
* GLOBAL NAVIGATION BAR — Shahi Kitchen (Luxury Edition)
* =============================================================================
*/
interface NavbarProps {
variant?: "default" | "menu";
}
export default function Navbar({ variant = "default" }: NavbarProps) {
const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { totalItems, openCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
const pathname = usePathname();
// Scroll effect - only for subtle visual polish, NOT height (height must stay consistent)
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navLinks = [
{ href: "/", label: t.nav.home },
{ href: "/menu", label: t.nav.menu },
{ href: "/locations", label: t.nav.locations },
{ href: "/#experience", label: t.nav.experience },
{ href: "/#contact", label: t.nav.contact },
];
// Determine active link (supports hash links)
const isActive = (href: string) => {
if (href === "/") return pathname === "/";
if (href.includes("#")) return false; // hash links handled separately
return pathname === href;
};
const closeMenu = () => setIsOpen(false);
return (
<nav
className="fixed top-0 left-0 right-0 z-50 h-16 border-b border-[#c99a2e]/10 bg-[#fbf7ef]/80 backdrop-blur-3xl"
>
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between h-full">
{/* Subtle gold accent line at the very bottom */}
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e]/30 to-transparent" />
{/* Premium Animated Logo */}
<Link href="/" className="group flex items-center gap-3">
<div className="relative">
<motion.span
whileHover={{ scale: 1.08, rotate: 2 }}
transition={{ type: "spring", stiffness: 300, damping: 15 }}
className="grid h-12 w-12 place-items-center overflow-hidden rounded-2xl border border-[#c99a2e]/30 bg-white shadow-xl shadow-[#0f5a4a]/10 p-1 transition-all duration-300 group-hover:border-[#c99a2e]/70 group-hover:shadow-[#c99a2e]/25"
>
<img
src="/images/logo-shahi-chef-icon.jpg"
alt="Shahi Kitchen - Cute Chef with Large Mustaches"
className="h-10 w-10 object-contain transition-all duration-500"
/>
</motion.span>
<div className="absolute inset-0 rounded-2xl bg-[#c99a2e]/0 group-hover:bg-[#c99a2e]/15 blur-2xl transition-all duration-500 pointer-events-none" />
</div>
<span className="hidden leading-none sm:block">
<span className="block font-serif text-[21px] tracking-[-0.5px] text-[#101724] transition-colors group-hover:text-[#0f5a4a]">Shahi Kitchen</span>
<span className="text-[10px] font-semibold uppercase tracking-[0.32em] text-[#8a6a25]">Royal Taste Gothenburg</span>
</span>
</Link>
{/* Desktop Navigation - Premium Mesmerizing Design */}
<div className="hidden md:block">
<div className="flex items-center rounded-full border border-[#c99a2e]/20 bg-white/60 px-2 py-1.5 backdrop-blur-3xl shadow-sm">
<div className="relative flex items-center gap-1 text-sm font-medium text-[#101724]">
{navLinks.map((link, index) => {
const active = isActive(link.href);
return (
<Link
key={index}
href={link.href}
className="relative px-5 py-2 rounded-full transition-colors hover:text-[#0f5a4a] z-10"
>
<span className="relative z-10">{link.label}</span>
{/* Sliding Active Indicator - Very Premium */}
{active && (
<motion.div
layoutId="activeNavPill"
className="absolute inset-0 rounded-full bg-gradient-to-r from-[#c99a2e] to-[#d4a73d] shadow-md"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
{/* Mesmerizing Gold Underline on Hover */}
<motion.span
className="absolute bottom-1 left-1/2 h-[1.5px] w-0 bg-gradient-to-r from-[#c99a2e] to-[#f4d47f] rounded-full"
whileHover={{ width: "60%", x: "-30%" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
</Link>
);
})}
</div>
</div>
</div>
{/* Desktop Actions — Stunning Mesmerizing Buttons */}
<div className="hidden md:flex items-center gap-3">
<LanguageSwitcher />
{/* Cart Button - Elegant dark with gold accent */}
<button
onClick={openCart}
className="group flex items-center gap-2.5 rounded-full border border-[#c99a2e]/30 bg-white/70 px-5 py-2.5 text-sm font-semibold text-[#101724] backdrop-blur-xl transition-all hover:border-[#c99a2e] hover:bg-white hover:shadow-lg active:scale-[0.985]"
>
<ShoppingBag className="h-4 w-4 transition-transform group-hover:scale-110" />
{t.cart}
{totalItems > 0 && (
<span className="ml-0.5 rounded-full bg-[#c99a2e] px-2 py-px text-[10px] font-black text-white">{totalItems}</span>
)}
</button>
{/* Reserve Table - The star of the nav, with mesmerizing gold gradient + shine */}
<Link
href="/#contact"
className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#c99a2e] via-[#d4a73d] to-[#c99a2e] px-7 py-2.5 text-sm font-bold text-[#241806] shadow-lg shadow-[#c99a2e]/25 transition-all hover:scale-[1.02] active:scale-[0.985] bg-[length:200%_100%] hover:bg-right"
>
<span className="relative z-10 flex items-center gap-2 tracking-[0.3px]">
{t.reserve}
<ArrowRight className="h-4 w-4" />
</span>
{/* Subtle shine sweep on hover */}
<span className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent opacity-0 group-hover:animate-[shimmer_1.2s_ease] group-hover:opacity-100" />
</Link>
</div>
{/* Mobile Hamburger */}
<div className="md:hidden flex items-center gap-3">
{/* MOBILE CART ICON (always visible even when hamburger is closed) */}
<button
onClick={openCart}
className="relative flex items-center justify-center w-9 h-9 rounded-full hover:bg-[#F5F1E9] transition-colors"
aria-label="Open cart"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
{totalItems > 0 && (
<span className="absolute -top-1 -right-1 bg-[#B38B4D] text-white text-[10px] font-medium min-w-[16px] h-[16px] rounded-full flex items-center justify-center px-1">
{totalItems}
</span>
)}
</button>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-[#B38B4D] p-2 -mr-2"
aria-label="Toggle menu"
>
<div className="space-y-1.5">
<span className={`block h-px w-6 bg-current transition-all ${isOpen ? "rotate-45 translate-y-1.5" : ""}`} />
<span className={`block h-px w-6 bg-current transition-all ${isOpen ? "opacity-0" : ""}`} />
<span className={`block h-px w-6 bg-current transition-all ${isOpen ? "-rotate-45 -translate-y-1.5" : ""}`} />
</div>
</button>
</div>
</div>
{/* Mobile Menu - Stunning Elegant Drawer */}
{isOpen && (
<div className="md:hidden fixed inset-0 z-[60] bg-[#101724]/60 backdrop-blur-md">
<div className="ml-auto h-full w-[82%] max-w-[320px] bg-[#fbf7ef] p-8 shadow-2xl border-l border-[#c99a2e]/10">
<div className="flex items-center justify-between mb-10">
<div className="flex items-center gap-3">
<img
src="/images/logo-shahi-chef-icon.jpg"
alt="Shahi Kitchen"
className="h-12 w-12 rounded-xl object-contain"
/>
<span className="font-serif text-xl text-[#101724]">Shahi Kitchen</span>
</div>
<button onClick={closeMenu} className="grid h-10 w-10 place-items-center rounded-full bg-[#f3f5f7] text-[#101724]">
<span className="text-2xl leading-none">×</span>
</button>
</div>
<div className="flex flex-col gap-5 text-xl font-medium text-[#101724]">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={closeMenu}
className="py-1 border-b border-[#e5e1d7] pb-4 hover:text-[#0f5a4a] transition-colors"
>
{link.label}
</Link>
))}
</div>
<div className="mt-10 space-y-3">
<LanguageSwitcher />
<Link
href="/#contact"
onClick={closeMenu}
className="block w-full rounded-full bg-gradient-to-r from-[#c99a2e] to-[#d4a73d] py-4 text-center text-base font-bold text-[#241806] shadow-lg"
>
{t.reserve}
</Link>
<button
onClick={() => { openCart(); closeMenu(); }}
className="block w-full rounded-full border border-[#c99a2e]/40 bg-white py-4 text-base font-semibold text-[#101724]"
>
{t.cart} {totalItems > 0 && `(${totalItems})`}
</button>
</div>
</div>
</div>
)}
</nav>
);
}
+97
View File
@@ -0,0 +1,97 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import React, { useState, useEffect } from "react";
/**
* Shahi Kitchen Hero - Focused on the Chef Logo
* - Large central logo with life (wink + smile cycle)
* - Cream background circle so the logo blends/absorbs into the site color
* - No more small floating dishes
*/
export default function PlayfulHeroScene() {
const [expression, setExpression] = useState<'normal' | 'wink' | 'smile'>('normal');
// Cycle expressions for life (wink and smile)
useEffect(() => {
const interval = setInterval(() => {
setExpression(prev => {
const rand = Math.random();
if (rand < 0.45) return 'wink';
if (rand < 0.9) return 'smile';
return 'normal';
});
}, 2400);
return () => clearInterval(interval);
}, []);
const getLogoSrc = () => {
if (expression === 'wink') return '/images/animation/chef-wink.jpg';
if (expression === 'smile') return '/images/animation/chef-smile.jpg';
return '/images/animation/chef-normal.jpg';
};
return (
<div className="relative w-full h-full min-h-[520px] md:min-h-[620px] flex items-center justify-center overflow-hidden">
{/* Large cream background circle - matches website exactly so logo absorbs */}
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2
w-[440px] h-[440px] md:w-[540px] md:h-[540px] lg:w-[620px] lg:h-[620px]
bg-[#fbf7ef] rounded-full blur-[130px] opacity-95" />
{/* Central Chef Logo with life */}
<div className="relative z-30">
<AnimatePresence mode="wait">
<motion.div
key={expression}
initial={{ opacity: 0.7, scale: 0.97 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -24, 0],
rotate: [-3.5, 3.5, -3.5]
}}
exit={{ opacity: 0.7, scale: 0.97 }}
transition={{
y: { duration: 6.6, repeat: Infinity, ease: "easeInOut" },
rotate: { duration: 6.6, repeat: Infinity, ease: "easeInOut" },
opacity: { duration: 0.4 },
scale: { duration: 0.4 }
}}
className="w-[280px] h-[280px] md:w-[360px] md:h-[360px] lg:w-[420px] lg:h-[420px]"
>
<img
src={getLogoSrc()}
alt="Shahi Kitchen Chef"
className="w-full h-full object-contain drop-shadow-[0_25px_55px_rgba(0,0,0,0.35)]"
/>
</motion.div>
</AnimatePresence>
{/* Enhanced royal glows */}
<div className="absolute -top-14 left-1/2 -translate-x-1/2 w-44 h-44 bg-[#f4d47f] rounded-full blur-3xl opacity-48" />
<div className="absolute -top-7 left-1/2 -translate-x-1/2 w-24 h-24 bg-[#c99a2e] rounded-full blur-2xl opacity-32" />
<div className="absolute -bottom-9 left-1/2 -translate-x-1/2 w-32 h-16 bg-[#c99a2e] rounded-full blur-3xl opacity-22" />
</div>
{/* Subtle steam for life */}
<motion.div
className="absolute left-[41%] top-[36%] w-4 h-11 opacity-32"
animate={{ y: [0, -36, 0], opacity: [0.28, 0.52, 0.28] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
>
<div className="w-full h-full bg-gradient-to-t from-[#f4d47f] to-transparent rounded-full blur-md" />
</motion.div>
<motion.div
className="absolute right-[40%] top-[41%] w-3 h-9 opacity-27"
animate={{ y: [0, -30, 0], opacity: [0.22, 0.48, 0.22] }}
transition={{ duration: 4.2, repeat: Infinity, ease: "easeInOut", delay: 1.5 }}
>
<div className="w-full h-full bg-gradient-to-t from-[#c99a2e] to-transparent rounded-full blur-md" />
</motion.div>
</div>
);
}
+79
View File
@@ -0,0 +1,79 @@
# Shahi Kitchen - High Quality Video Prompts (Realistic + Web Optimized)
Use these prompts in Grok's video generation (or similar tools).
**Recommended settings for all videos:**
- Duration: 5 seconds
- Resolution: 480p or 540p
- Style: Photorealistic, professional food photography
- Motion: Natural, subtle, loop-friendly
- Target file size after optimization: 500900 KB
---
## Butter Chicken
Photorealistic close-up of authentic Indian Butter Chicken in a traditional dark bowl. Rich, glossy, creamy orange-red tomato gravy with tender chicken pieces. Delicate natural steam gently rising from the hot dish. Warm, soft restaurant lighting with beautiful golden highlights on the sauce. Shot from a 3/4 angle, highly appetizing and luxurious. Shallow depth of field. Smooth 5-second loop, web optimized.
## Samosa Chat
Photorealistic close-up of Indian Samosa Chaat. Crispy broken samosas topped with chickpeas, yogurt, colorful chutneys, sev, onions and pomegranate. Fresh, vibrant street food presentation. Natural daylight or warm restaurant lighting. Shallow depth of field, highly detailed and appetizing. Smooth 5-second loop, web optimized.
## Palak Paneer
Photorealistic close-up of Palak Paneer. Fresh vibrant green spinach gravy with soft white paneer cubes, garnished with cream and ginger. Subtle natural steam rising. Warm elegant restaurant lighting. Professional food photography, shallow depth of field. Smooth 5-second loop, web optimized.
## Malai Kofta
Photorealistic close-up of Malai Kofta in rich creamy cashew gravy. Soft golden vegetable dumplings, garnished with cream and nuts. Gentle steam rising. Luxurious warm lighting. High-end Indian restaurant food photography style. Shallow depth of field. Smooth 5-second loop, web optimized.
## Daal Makhani
Photorealistic close-up of Daal Makhani. Thick, creamy, buttery black lentils with rich tomato gravy. Glossy surface with subtle steam. Warm cozy restaurant lighting. Professional appetizing food photography. Shallow depth of field. Smooth 5-second loop, web optimized.
## Lahore Chana
Photorealistic close-up of Lahore Chana (spiced chickpeas). Tangy onion-tomato gravy with chickpeas, garnished with green chilies, ginger and cilantro. Natural steam rising. Warm vibrant lighting. Traditional Punjabi restaurant style. Shallow depth of field. Smooth 5-second loop, web optimized.
## Lamm Palak
Photorealistic close-up of Lamm Palak. Tender lamb pieces in creamy spinach gravy, garnished with cream and ginger. Subtle steam. Warm rich lighting. Professional Indian restaurant food photography. Shallow depth of field. Smooth 5-second loop, web optimized.
## Bong Nihari
Photorealistic close-up of traditional Bong Nihari (slow-cooked shank). Rich aromatic gravy with tender meat, garnished with fresh ginger, green chilies and cilantro. Natural steam. Warm, moody, authentic Pakistani restaurant lighting. Highly detailed and appetizing. Smooth 5-second loop, web optimized.
## Shami Sandwich
Photorealistic close-up of a Shami Sandwich cut in half. Spiced shami kebab in soft bread with onions, chutney and fresh herbs. Appetizing cross-section view. Warm natural lighting. Professional street food / cafe photography style. Short 5-second loop, web optimized.
## Kebab Roll
Photorealistic close-up of a Kebab Roll cut open. Juicy seekh kebab inside soft naan with onions, chutney and salad. Slight steam and fresh ingredients visible. Warm appetizing lighting. Professional fast-casual food photography. Smooth 5-second loop, web optimized.
## Falafel Roll
Photorealistic close-up of a Falafel Roll cut in half. Crispy falafel with fresh vegetables, tahini and chutney inside soft flatbread. Vibrant and fresh presentation. Warm natural lighting. Professional street food photography style. Short 5-second loop, web optimized.
## Paneer Roll
Photorealistic close-up of a Paneer Roll cut open. Grilled paneer tikka with vegetables and sauces inside soft naan. Appetizing cross-section. Warm restaurant lighting. Professional food photography. Smooth 5-second loop, web optimized.
## Namakpare
Photorealistic close-up of traditional Namakpare (savory fried snacks). Golden-brown crunchy diamond-shaped pieces. Fresh, crispy texture. Warm traditional Indian lighting. Professional sweet shop photography style. Short 5-second loop, web optimized.
## Rasmalai
Photorealistic close-up of Rasmalai. Soft spongy cottage cheese dumplings floating in rich saffron and cardamom sweet milk. Garnished with pistachios and saffron strands. Creamy luxurious texture. Elegant warm lighting. High-end Indian dessert photography. Smooth 5-second loop, web optimized.
---
## Notes for Generation
- Always mention "photorealistic, professional food photography, shallow depth of field".
- For dishes with steam/sizzle/pouring, explicitly mention "natural delicate steam rising" or "subtle motion".
- Keep prompts focused on the dish itself (minimal background).
- After generation, optimize heavily using the optimization script or ffmpeg (see `optimize-videos.sh`).
Use these prompts when regenerating videos for better realism.
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# ======================================================
# Extract First Frame as Poster for All Videos
# ======================================================
# This script goes through all .mp4 and .webm files in the current
# folder and extracts the very first frame as a high-quality poster image.
#
# Usage:
# cd /home/khan/code/shahikitchen/public/videos
# bash /home/khan/code/shahikitchen/extract-video-posters.sh
#
# Requirements:
# - ffmpeg installed
#
# Output:
# For butter-chicken-steam.mp4 → butter-chicken-steam-poster.jpg
# (You can then update your menu data or code to use these posters)
# ======================================================
set -e
echo "Extracting first frame posters from videos..."
echo ""
for file in *.mp4 *.webm; do
if [ ! -f "$file" ]; then
continue
fi
base="${file%.*}"
output="${base}-poster.jpg"
# Skip if poster already exists
if [ -f "$output" ]; then
echo "→ Skipping $file (poster already exists)"
continue
fi
echo "→ Extracting poster from: $file"
ffmpeg -y -i "$file" -ss 0.1 -vframes 1 -q:v 2 "$output" 2>/dev/null
if [ -f "$output" ]; then
echo " Created: $output"
else
echo " Failed to create poster for $file"
fi
done
echo ""
echo "✅ Done extracting posters!"
echo ""
echo "You can now update your menu cards to use *-poster.jpg instead of the original dish photos"
echo "for better visual consistency between static state and video."
+50
View File
@@ -0,0 +1,50 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Language } from './translations';
interface LanguageContextType {
language: Language;
setLanguage: (lang: Language) => void;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
const LANGUAGE_STORAGE_KEY = 'shahi-kitchen-language';
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLanguageState] = useState<Language>('en');
// Load language from localStorage on mount
useEffect(() => {
const savedLang = localStorage.getItem(LANGUAGE_STORAGE_KEY) as Language | null;
if (savedLang && ['en', 'sv', 'hi', 'ur'].includes(savedLang)) {
setLanguageState(savedLang);
} else {
// Optional: Try to detect browser language
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith('sv')) setLanguageState('sv');
else if (browserLang.startsWith('hi')) setLanguageState('hi');
else if (browserLang.startsWith('ur')) setLanguageState('ur');
}
}, []);
const setLanguage = (lang: Language) => {
setLanguageState(lang);
localStorage.setItem(LANGUAGE_STORAGE_KEY, lang);
};
return (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
}
+205
View File
@@ -0,0 +1,205 @@
/**
* =============================================================================
* CENTRAL MENU DATA — Shahi Kitchen
* =============================================================================
*
* This is the SINGLE SOURCE OF TRUTH for every dish shown on the website.
*
* WHY THIS FILE EXISTS:
* - Keeps menu content decoupled from UI components (easy for restaurant staff
* or future devs to update prices/descriptions without touching React code)
* - Powers BOTH the public menu page AND the cart system
* - Enables future features: search, filters, online ordering, admin CMS, etc.
*
* DATA MODEL:
* - MenuCategory: A logical section (Street Food, Vegetarian, Chicken, Sweets...)
* - MenuItem: One dish
* id → stable unique key (used in cart, URLs, analytics). NEVER change.
* name → displayed title
* price → integer in Swedish Krona (kr). No decimals in current design.
* description → optional rich text shown under the name
* image → filename inside /public/images/dishes/ (fallback when no video)
* video → filename inside /public/videos/ (MP4 or WebM)
* The menu page automatically looks for a matching
* `-poster.jpg` first frame in /public/images/dishes/
* isVegetarian → boolean flag. Powers the green "VEGETARIAN" pill + filter toggle
*
* VIDEO + POSTER CONTRACT (critical):
* When `video: "butter-chicken-steam.mp4"` exists:
* 1. The card shows the static poster image first (performance + first-frame accuracy)
* 2. On hover the actual video plays (only while hovering)
* 3. On mouse leave → video pauses + resets to time 0
* Poster lookup order (see menu/page.tsx for the exact fallback logic):
* butter-chicken-steam-poster.jpg
* butter-chicken-steam-optimized-poster.jpg
* (and a few legacy variants for safety)
*
* HOW TO ADD A NEW DISH (future-proof instructions):
* 1. Add a high-quality image to /public/images/dishes/your-dish.jpg
* 2. (Optional but recommended) Generate a short 48s video → optimize with ffmpeg
* and place in /public/videos/your-dish.webm + .mp4
* 3. Extract first frame as your-dish-poster.jpg (see extract-video-posters.sh)
* 4. Add the item object below in the correct category
* 5. If vegetarian → set isVegetarian: true
* 6. Update price in both places if the restaurant changes pricing
*
* IMPORTANT GOTCHAS:
* - The `id` must be kebab-case and globally unique across all categories.
* - Price is stored as number (not string) so cart math works.
* - Do NOT delete items that have already been ordered by real customers
* (the cart uses the id as primary key).
*
* RELATED FILES:
* - app/menu/page.tsx → consumes this data + adds cart buttons + video hover
* - components/CartContext.tsx → stores references by id + name + price
*/
export type MenuItem = {
id: string;
name: string;
description?: string;
price: number;
image?: string; // filename from /public/images/dishes/
video?: string; // filename from /public/videos/ (for animated dishes)
isVegetarian?: boolean;
};
export type MenuCategory = {
id: string;
name: string;
items: MenuItem[];
};
/**
* THE ACTUAL MENU DATA
*
* Categories are rendered in the exact order they appear here on /menu.
* Each category has a stable `id` used for:
* - URL hash navigation (#street-food)
* - IntersectionObserver active state
* - Category filter pills (the beautiful sliding gold indicator)
*
* Keep descriptions concise (12 lines max) — the design is generous but not verbose.
*/
export const menuCategories: MenuCategory[] = [
{
id: "street-food",
name: "Street Food & Starters",
items: [
{ id: "samosa-aloo", name: "Samosa Aloo Veg", price: 34, image: "aloo-samosa.jpg", video: "samosa-aloo.mp4" },
{ id: "samosa-keema", name: "Samosa Keema", price: 39, image: "keema-samosa.jpg", video: "samosa-keema.mp4" },
{ id: "samosa-chat", name: "Samosa Chat", price: 89, image: "samosa-chaat.jpg", video: "samosa-chaat.mp4" },
{ id: "chana-chat", name: "Chana Chat", price: 69, image: "chana-chaat.jpg", video: "chana-chaat.mp4" },
{ id: "panipuri", name: "Panipuri / Golgappe", price: 69, image: "panipuri.jpg", video: "panipuri.mp4" },
{ id: "keema-naan-starter", name: "Keema Naan", price: 75, image: "keema-naan.jpg", video: "keema-naan.mp4" },
],
},
{
id: "vegetarian",
name: "Vegetarian",
items: [
{ id: "palak-paneer", name: "Palak Paneer", description: "Cottage cheese cooked in a creamy spinach gravy with mild spices and aromatic herbs.", price: 139, image: "palak-paneer.jpg", video: "palak-paneer.mp4", isVegetarian: true },
{ id: "shahi-paneer", name: "Shahi Paneer", description: "Soft cottage cheese in a rich, creamy cashew and tomato gravy with Indian spices.", price: 139, image: "shahi-paneer.jpg", video: "shahi-paneer.mp4", isVegetarian: true },
{ id: "malai-kofta", name: "Malai Kofta", description: "Soft vegetable koftas simmered in a rich and creamy onion-tomato gravy with mild spices.", price: 139, image: "malai-kofta.jpg", video: "malai-kofta.mp4", isVegetarian: true },
{ id: "daal-makhani", name: "Daal Makhani", description: "Slow-cooked black lentils in a buttery, creamy tomato gravy with aromatic spices.", price: 139, image: "daal-makhani.jpg", video: "daal-makhani.mp4", isVegetarian: true },
{ id: "lahore-chana", name: "Lahore Chana", description: "Spiced chickpeas cooked in a tangy onion-tomato gravy with traditional Punjabi spices.", price: 139, image: "lahore-chana.jpg", isVegetarian: true },
],
},
{
id: "meat",
name: "Meat",
items: [
{ id: "lamm-palak", name: "Lamm Palak", price: 179, image: "lamm-palak.jpg", video: "lamm-palak.mp4" },
{ id: "lamm-vindaloo", name: "Lamm Vindaloo", price: 179, image: "lamm-vindaloo.jpg", video: "lamm-vindaloo.mp4" },
{ id: "lamm-rogan-josh", name: "Lamm Rogan Josh", price: 199, image: "lamm-rogan-josh.jpg", video: "lamm-rogan-josh.mp4" },
{ id: "lamm-karahi", name: "Lamm Karahi", price: 179, image: "lamm-karahi.jpg", video: "lamm-karahi.mp4" },
{ id: "bong-nihari", name: "Bong Nihari", price: 199, image: "bong-nihari.jpg", video: "bong-nihari.mp4" },
{ id: "paye", name: "Paye", price: 149, image: "paye.jpg", video: "paye.mp4" },
],
},
{
id: "burger-sandwich",
name: "Burger & Sandwich",
items: [
{ id: "shahi-burger", name: "Shahi Burger", price: 119, image: "shahi-burger.jpg", video: "shahi-burger.mp4" },
{ id: "shami-sandwich", name: "Shami Sandwich Menu", price: 99, image: "shami-sandwich.jpg", video: "shami-sandwich.mp4" },
],
},
{
id: "chicken",
name: "Chicken",
items: [
{ id: "chicken-biryani", name: "Chicken Biryani", price: 149, image: "chicken-biryani.jpg", video: "chicken-biryani.mp4" },
{ id: "chicken-tikka", name: "Chicken Tikka", price: 149, image: "chicken-tikka.jpg", video: "chicken-tikka.mp4" },
{ id: "tikka-boti", name: "Tikka Boti", price: 149, image: "chicken-tikka.jpg", video: "tikka-boti.mp4" },
{ id: "chicken-karahi", name: "Chicken Karahi", price: 149, image: "chicken-karahi.jpg", video: "chicken-karahi.mp4" },
{ id: "lahore-sizzler", name: "Lahore Sizzler", price: 169, image: "lahore-sizzler.jpg", video: "lahore-sizzler.mp4" },
{ id: "butter-chicken", name: "Butter Chicken", price: 149, image: "butter-chicken.jpg" },
{ id: "chicken-haleem", name: "Chicken Haleem", price: 149, image: "chicken-haleem.jpg", video: "chicken-haleem.mp4" },
],
},
{
id: "pizza",
name: "Pizza",
items: [
{ id: "lahore-pizza", name: "Lahore Pizza", price: 119, image: "lahore-pizza.jpg", video: "lahore-pizza.mp4" },
{ id: "kebab-pizza", name: "Kebab Pizza", price: 119, image: "kebab-pizza.jpg", video: "kebab-pizza.mp4" },
{ id: "tikka-boti-pizza", name: "Tikka Boti Pizza", price: 119, image: "tikka-boti-pizza.jpg", video: "tikka-boti-pizza.mp4" },
{ id: "peshawari-pizza", name: "Peshawari Pizza", price: 119, image: "peshawari-pizza.jpg", video: "peshawari-pizza.mp4" },
{ id: "veg-pizza", name: "Veg Pizza", price: 109, image: "veg-pizza.jpg", isVegetarian: true },
],
},
{
id: "naan-roll",
name: "Naan Roll",
items: [
{ id: "tikka-boti-roll", name: "Tikka Boti Roll", price: 99, image: "tikka-boti-roll.jpg", video: "tikka-boti-roll.mp4" },
{ id: "kebab-roll", name: "Kebab Roll", price: 99, image: "kebab-roll.jpg", video: "kebab-roll.mp4" },
{ id: "falafel-roll", name: "Falafel Roll", price: 99, image: "falafel-roll.jpg", video: "falafel-roll.mp4", isVegetarian: true },
{ id: "paneer-roll", name: "Paneer Roll", price: 99, image: "paneer-roll.jpg", video: "paneer-roll.mp4", isVegetarian: true },
],
},
{
id: "sweets",
name: "Sweets / Mithai",
items: [
{ id: "namakpare", name: "Namakpare", price: 135, image: "namakpare.jpg", video: "namakpare.mp4" },
{ id: "jalebi", name: "Jalebi", price: 119, image: "jalebi.jpg", video: "jalebi.mp4" },
{ id: "gajar-halwa", name: "Gajar Halwa", price: 149, image: "gajar-halwa.jpg", video: "gajar-halwa.mp4" },
{ id: "rasmalai", name: "Rasmalai", price: 45, image: "rasmalai.jpg", video: "rasmalai.mp4" },
{ id: "kulfi", name: "Kulfi", price: 39, image: "kulfi.jpg", video: "kulfi.mp4" },
],
},
{
id: "drinks",
name: "Drinks",
items: [
{ id: "masala-chai", name: "Masala Chai", price: 39, image: "masala-chai.jpg" },
{ id: "mango-lassi", name: "Mango Lassi", price: 45, image: "mango-lassi.jpg", video: "mango-lassi.mp4" },
{ id: "coca-cola", name: "Coca-Cola", price: 29 },
{ id: "pepsi-fanta", name: "Pepsi / Fanta", price: 29 },
{ id: "sprite-ramlosa", name: "Sprite / Ramlösa", price: 29 },
{ id: "energy-drink", name: "Energy Drink", price: 39 },
{ id: "juice", name: "Juice", price: 20, image: "mango-juice.jpg" },
{ id: "coffee", name: "Coffee", price: 39, image: "black-coffee.jpg" },
{ id: "latte", name: "Latte", price: 49, image: "latte.jpg", video: "latte.mp4" },
{ id: "cappuccino", name: "Cappuccino", price: 49, image: "cappuccino.jpg" },
{ id: "tea", name: "Tea", price: 30 },
],
},
];
/**
* UTILITY EXPORT
*
* Flattened list of every single menu item across all categories.
*
* Current use cases:
* - Future global search / command palette
* - Admin tools that need to iterate over everything
* - Analytics or sitemap generation
*
* Example:
* const butterChicken = allMenuItems.find(i => i.id === "butter-chicken");
*/
export const allMenuItems = menuCategories.flatMap((category) => category.items);
+281
View File
@@ -0,0 +1,281 @@
export type Language = 'en' | 'sv' | 'hi' | 'ur';
export const languages: { code: Language; name: string; native: string; flag: string }[] = [
{ code: 'en', name: 'English', native: 'English', flag: '🇬🇧' },
{ code: 'sv', name: 'Swedish', native: 'Svenska', flag: '🇸🇪' },
{ code: 'hi', name: 'Hindi', native: 'हिंदी', flag: '🇮🇳' },
{ code: 'ur', name: 'Urdu', native: 'اردو', flag: '🇵🇰' },
];
export const translations = {
en: {
// Navbar
nav: {
home: 'Home',
menu: 'Menu',
locations: 'Locations',
experience: 'Experience',
contact: 'Contact',
},
reserve: 'Reserve Table',
cart: 'Cart',
// Hero
hero: {
badge: 'Royal Indian & Pakistani Since 2016',
welcome: 'Welcome to',
title: 'ShahiKitchen Online',
subtitle: 'Experience the warmth of royal hospitality and the richness of authentic Indian and Pakistani flavors — now brought to you with elegance, from the heart of Gothenburg.',
exploreMenu: 'Explore the Menu',
viewExperience: 'The Shahi Experience',
},
// Signature Menu
signatureMenu: {
title: 'Signature Menu',
subtitle: 'A curated selection of our most beloved dishes.',
filterAll: 'All',
filterCurry: 'Curry',
filterRice: 'Rice',
filterGrill: 'Grill',
filterSweet: 'Sweet',
addToTable: 'Add to Table',
viewFullMenu: 'VIEW THE COMPLETE MENU — 40+ DISHES',
},
// Experience
experience: {
badge: 'THE SHAHI WAY',
title: 'Warmth like a feast.\nCalm like a palace.',
heritage: {
title: 'Royal Heritage',
text: 'Recipes passed through generations. Every dish tells a story of Punjab and the royal kitchens of the subcontinent.',
},
generous: {
title: 'Generous & Honest',
text: 'Large portions. No shortcuts. We treat every guest like family — the way hospitality was meant to be.',
},
grace: {
title: 'Modern Grace',
text: 'Beautiful spaces in Askim & Backaplan. Fast, thoughtful service. Packaging worthy of a gift.',
},
},
// Locations
locations: {
badge: 'TWO HOMES IN GOTHENBURG',
title: 'Come as guests.\nLeave as family.',
},
// Footer
footer: {
tagline: 'Authentic Indian & Pakistani cuisine in Gothenburg since 2016.',
locations: 'OUR LOCATIONS',
explore: 'EXPLORE',
follow: 'FOLLOW US',
},
// Common
add: 'Add',
viewCart: 'View Cart',
},
sv: {
nav: {
home: 'Hem',
menu: 'Meny',
locations: 'Platser',
experience: 'Upplevelse',
contact: 'Kontakt',
},
reserve: 'Boka Bord',
cart: 'Varukorg',
hero: {
badge: 'Kunglig Indisk & Pakistansk Sedan 2016',
welcome: 'Välkommen till',
title: 'ShahiKitchen Online',
subtitle: 'Upplev värmen från kunglig gästfrihet och rikedomen av autentiska indiska och pakistanska smaker — nu med elegans, från Göteborgs hjärta.',
exploreMenu: 'Utforska Menyn',
viewExperience: 'Shahi-upplevelsen',
},
signatureMenu: {
title: 'Signaturmeny',
subtitle: 'Ett noga utvalt urval av våra mest älskade rätter.',
filterAll: 'Alla',
filterCurry: 'Curry',
filterRice: 'Ris',
filterGrill: 'Grill',
filterSweet: 'Sött',
addToTable: 'Lägg till',
viewFullMenu: 'SE HELA MENYN — 40+ RÄTTER',
},
experience: {
badge: 'SHAHI-SÄTTET',
title: 'Värme som en fest.\nLugn som ett palats.',
heritage: {
title: 'Kungligt Arv',
text: 'Recept som gått i arv i generationer. Varje rätt berättar en historia från Punjab och de kungliga köken på subkontinenten.',
},
generous: {
title: 'Generöst & Ärligt',
text: 'Stora portioner. Inga genvägar. Vi behandlar varje gäst som familj — så som gästfrihet ska vara.',
},
grace: {
title: 'Modern Elegans',
text: 'Vackra lokaler i Askim & Backaplan. Snabb och omtänksam service. Förpackning värdig en gåva.',
},
},
locations: {
badge: 'TVÅ HEM I GÖTEBORG',
title: 'Kom som gäst.\nLämna som familj.',
},
footer: {
tagline: 'Autentisk indisk och pakistansk mat i Göteborg sedan 2016.',
locations: 'VÅRA PLATSER',
explore: 'UPPTÄCK',
follow: 'FÖLJ OSS',
},
add: 'Lägg till',
viewCart: 'Visa Varukorg',
},
hi: {
nav: {
home: 'होम',
menu: 'मेन्यू',
locations: 'स्थान',
experience: 'अनुभव',
contact: 'संपर्क',
},
reserve: 'टेबल बुक करें',
cart: 'कार्ट',
hero: {
badge: '2016 से शाही भारतीय और पाकिस्तानी',
welcome: 'स्वागत है',
title: 'ShahiKitchen Online',
subtitle: 'शाही आतिथ्य की गर्माहट और असली भारतीय-पाकिस्तानी स्वादों की समृद्धि का अनुभव करें — अब गॉथेनबर्ग के दिल से, शान के साथ।',
exploreMenu: 'मेन्यू देखें',
viewExperience: 'शाही अनुभव',
},
signatureMenu: {
title: 'सिग्नेचर मेन्यू',
subtitle: 'हमारी सबसे पसंदीदा व्यंजनों का विशेष चयन।',
filterAll: 'सभी',
filterCurry: 'करी',
filterRice: 'चावल',
filterGrill: 'ग्रिल',
filterSweet: 'मिठाई',
addToTable: 'ऐड करें',
viewFullMenu: 'पूरी मेन्यू देखें — 40+ व्यंजन',
},
experience: {
badge: 'शाही तरीका',
title: 'दावत जैसी गर्माहट।\nमहल जैसा सुकून।',
heritage: {
title: 'शाही विरासत',
text: 'पीढ़ियों से चले आ रहे रेसिपी। हर व्यंजन पंजाब और उपमहाद्वीप के शाही रसोईघरों की कहानी कहता है।',
},
generous: {
title: 'उदार और ईमानदार',
text: 'बड़ी सर्विंग्स। कोई शॉर्टकट नहीं। हम हर मेहमान को परिवार की तरह मानते हैं — जैसी आतिथ्य होनी चाहिए।',
},
grace: {
title: 'आधुनिक शान',
text: 'अस्किम और बैकाप्लान में खूबसूरत जगहें। तेज़ और सोच-समझकर की गई सेवा। तोहफे जैसी पैकेजिंग।',
},
},
locations: {
badge: 'गॉथेनबर्ग में दो घर',
title: 'मेहमान बनकर आएं।\nपरिवार बनकर जाएं।',
},
footer: {
tagline: '2016 से गॉथेनबर्ग में असली भारतीय और पाकिस्तानी खाना।',
locations: 'हमारे स्थान',
explore: 'एक्सप्लोर करें',
follow: 'हमें फॉलो करें',
},
add: 'ऐड करें',
viewCart: 'कार्ट देखें',
},
ur: {
nav: {
home: 'ہوم',
menu: 'مینو',
locations: 'مقامات',
experience: 'تجربہ',
contact: 'رابطہ',
},
reserve: 'ٹیبل بک کریں',
cart: 'کارٹ',
hero: {
badge: '2016 سے شاہی انڈین اور پاکستانی',
welcome: 'خوش آمدید',
title: 'ShahiKitchen Online',
subtitle: 'شاہی مہمان نوازی کی گرمی اور اصلی انڈین پاکستانی ذائقوں کی دولت کا تجربہ کریں — اب گوٹنبرگ کے دل سے، شان و شوکت کے ساتھ۔',
exploreMenu: 'مینو دیکھیں',
viewExperience: 'شاہی تجربہ',
},
signatureMenu: {
title: 'سگنیچر مینو',
subtitle: 'ہمارے سب سے پسندیدہ پکوانوں کا منتخب انتخاب۔',
filterAll: 'سب',
filterCurry: 'کری',
filterRice: 'چاول',
filterGrill: 'گرل',
filterSweet: 'میٹھی',
addToTable: 'شامل کریں',
viewFullMenu: 'مکمل مینو دیکھیں — 40+ پکوان',
},
experience: {
badge: 'شاہی طریقہ',
title: 'دعوت جیسی گرمی۔\nمحل جیسا سکون۔',
heritage: {
title: 'شاہی وراثت',
text: 'نسلوں سے چلے آ رہے نسخے۔ ہر پکوان پنجاب اور برصغیر کی شاہی رasoئیوں کی کہانی سناتا ہے۔',
},
generous: {
title: 'سخی اور ایماندار',
text: 'بڑی سرونگ۔ کوئی شارٹ کٹ نہیں۔ ہم ہر مہمان کو خاندان کی طرح مانتے ہیں — جیسے مہمان نوازی ہونی چاہیے۔',
},
grace: {
title: 'جدید شان',
text: 'اسکیم اور بیکاپلان میں خوبصورت جگہیں۔ تیز اور سوچ سمجھ کر کی گئی سروس۔ تحفے جیسی پیکنگ۔',
},
},
locations: {
badge: 'گوٹنبرگ میں دو گھر',
title: 'مہمان بن کر آئیں۔\nخاندان بن کر جائیں۔',
},
footer: {
tagline: '2016 سے گوٹنبرگ میں اصلی انڈین اور پاکستانی کھانا۔',
locations: 'ہمارے مقامات',
explore: 'دریافت کریں',
follow: 'ہمیں فالو کریں',
},
add: 'شامل کریں',
viewCart: 'کارٹ دیکھیں',
},
} as const;
export function getTranslation(lang: Language) {
return translations[lang];
}
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# ============================================================
# Advanced Video Optimization Script for Shahi Kitchen
# Optimized for realistic food videos (short loops)
# ============================================================
#
# Target per video:
# - Duration: 4-5 seconds
# - Resolution: 480p (854x480)
# - WebM (VP9): 500-800 KB
# - MP4 fallback: 700-1100 KB
#
# Usage:
# cd /home/khan/code/shahikitchen/public/videos
# bash /home/khan/code/shahikitchen/optimize-food-videos.sh
#
# Requirements: ffmpeg with libvpx-vp9 and libx264
# ============================================================
set -e
echo "=== Shahi Kitchen Food Video Optimizer ==="
echo "Creating heavily optimized WebM + MP4 versions..."
echo "Original .mp4 files will NOT be deleted."
echo ""
for file in *.mp4; do
if [ ! -f "$file" ]; then
continue
fi
base="${file%.mp4}"
# Skip if already optimized
if [ -f "${base}-optimized.webm" ]; then
echo "→ Skipping $file (already optimized)"
continue
fi
echo "→ Optimizing: $file"
# === WebM (VP9) - Primary (Best size/quality) ===
# === WebM (VP9) - Primary (Best size/quality for realistic food videos) ===
# 2-pass for best quality at low bitrate
ffmpeg -y -i "$file" \
-c:v libvpx-vp9 -b:v 550k -crf 35 -deadline good -cpu-used 4 \
-vf "scale=854:480,unsharp=5:5:0.5:5:5:0.0" \
-an \
-r 24 \
-pass 1 -f null /dev/null 2>/dev/null
ffmpeg -y -i "$file" \
-c:v libvpx-vp9 -b:v 550k -crf 35 -deadline good -cpu-used 4 \
-vf "scale=854:480,unsharp=5:5:0.5:5:5:0.0" \
-an \
-r 24 \
-pass 2 \
"${base}-optimized.webm" 2>/dev/null
# === MP4 Fallback (H.264) - Good compatibility ===
ffmpeg -y -i "$file" \
-c:v libx264 -b:v 750k -crf 28 -preset slow \
-vf "scale=854:480" \
-c:a aac -b:a 48k \
-r 24 \
-movflags +faststart \
"${base}-optimized.mp4" 2>/dev/null
echo " Created: ${base}-optimized.webm + ${base}-optimized.mp4"
done
echo ""
echo "✅ All videos optimized!"
echo ""
echo "Recommended next steps:"
echo "1. Test a few -optimized.webm files in browser"
echo "2. Update menu data to point to the new optimized files (optional)"
echo "3. When happy, you can replace the old files or keep both versions"
echo ""
echo "Tip: WebM files are usually 30-50% smaller than MP4 at similar quality."
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# ======================================================
# Video Optimization Script for Shahi Kitchen Website
# ======================================================
#
# This script creates optimized .webm versions of your MP4 videos.
#
# IMPORTANT:
# - It will NOT delete or modify any of your original .mp4 files.
# - It only creates new .webm files next to them.
# - Keep both versions for now. We can decide later.
#
# Target: 480p, ~550-700 kbps, VP9
#
# Requirements:
# Ubuntu/Debian: sudo apt update && sudo apt install ffmpeg
# macOS: brew install ffmpeg
#
# Usage:
# cd /home/khan/code/shahikitchen/public/videos
# bash /home/khan/code/shahikitchen/optimize-videos.sh
# ======================================================
set -e
echo "Starting video optimization (WebM only)..."
echo "Your original .mp4 files will NOT be touched or deleted."
echo ""
for file in *.mp4; do
if [ ! -f "$file" ]; then
continue
fi
base="${file%.mp4}"
# Skip if .webm already exists
if [ -f "${base}.webm" ]; then
echo "→ Skipping ${file} (webm already exists)"
continue
fi
echo "→ Processing: $file${base}.webm"
ffmpeg -y -i "$file" \
-c:v libvpx-vp9 -b:v 600k -crf 34 -deadline good -cpu-used 3 \
-vf "scale=854:480" \
-an \
-r 24 \
"${base}.webm"
echo " Created: ${base}.webm"
done
echo ""
echo "✅ Optimization complete!"
echo ""
echo "New .webm files have been created next to your original .mp4 files."
echo "Your website code is already set up to prefer .webm when available."
echo ""
echo "Next steps when you're ready:"
echo "1. Test the site locally (WebM should load much faster and use less data)."
echo "2. Let me know when you want to decide what to do with the old MP4s."
+814 -9
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -9,9 +9,19 @@
"lint": "eslint"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@splinetool/react-spline": "^4.1.0",
"@types/three": "^0.184.1",
"framer-motion": "^12.40.0",
"gsap": "^3.15.0",
"lenis": "^1.3.23",
"lucide-react": "^1.17.0",
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"sonner": "^2.0.7",
"three": "^0.184.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Some files were not shown because too many files have changed in this diff Show More