Replace entire repo content with shahikitchen-v1-prodcution.zip (v1.1 production)

This commit is contained in:
root
2026-06-25 13:44:21 +00:00
parent 284dcfa120
commit 6cd5aaa048
850 changed files with 15616 additions and 26143 deletions
+118
View File
@@ -0,0 +1,118 @@
/**
* =============================================================================
* SITE ASSETS — Single source of truth for all website media
* =============================================================================
*
* Change any image or video path HERE and the whole site updates.
* Physical files live under /public — this file maps logical names → URLs.
*
* Folder layout (public/):
* /images/logo/ — brand logo, hero banner videos
* /images/dishes/ — menu dish photos & video posters
* /images/animation/ — chef expressions, illustrations
* /images/real/ — restaurant interior photos
* /images/booking/ — table QR codes (askim + backaplan)
* /videos/ — dish & promo videos
*/
/** Base folders served from /public */
export const MEDIA_FOLDERS = {
images: '/images',
dishes: '/images/dishes',
logo: '/images/logo',
animation: '/images/animation',
real: '/images/real',
booking: {
askim: '/images/booking/askim',
backaplan: '/images/booking/backaplan',
},
videos: '/videos',
} as const;
/** Join a folder with a filename → public URL */
export function mediaUrl(folder: string, filename: string): string {
return `${folder}/${filename}`;
}
/**
* All named static assets used across the website.
* Prefer these constants over hardcoded strings in components.
*/
export const SITE_ASSETS = {
logo: {
primary: mediaUrl(MEDIA_FOLDERS.logo, 'logo1.png'),
alt: mediaUrl(MEDIA_FOLDERS.logo, 'logo.jpg'),
chef: mediaUrl(MEDIA_FOLDERS.images, 'logo-shahi-chef.jpg'),
chefIcon: mediaUrl(MEDIA_FOLDERS.images, 'logo-shahi-chef-icon.jpg'),
},
hero: {
restaurantInterior: mediaUrl(MEDIA_FOLDERS.images, 'hero-restaurant-interior.jpg'),
restaurantInterior2: mediaUrl(MEDIA_FOLDERS.images, 'hero-restaurant-interior-2.jpg'),
restaurantBg: mediaUrl(MEDIA_FOLDERS.images, 'hero-restaurant-bg.jpg'),
},
banner: {
mobileWebm: mediaUrl(MEDIA_FOLDERS.logo, 'banner_mobile-optimized.webm'),
mobileMp4: mediaUrl(MEDIA_FOLDERS.logo, 'banner_mobile-optimized.mp4'),
desktopMp4: mediaUrl(MEDIA_FOLDERS.logo, 'banner1.mp4'),
originalMp4: mediaUrl(MEDIA_FOLDERS.logo, 'banner.mp4'),
mobileOriginalMp4: mediaUrl(MEDIA_FOLDERS.logo, 'banner_mobile.mp4'),
},
animation: {
chefWink: mediaUrl(MEDIA_FOLDERS.animation, 'chef-wink.jpg'),
chefSmile: mediaUrl(MEDIA_FOLDERS.animation, 'chef-smile.jpg'),
chefNormal: mediaUrl(MEDIA_FOLDERS.animation, 'chef-normal.jpg'),
biryaniIllust: mediaUrl(MEDIA_FOLDERS.animation, 'biryani-illust.jpg'),
butterChickenIllust: mediaUrl(MEDIA_FOLDERS.animation, 'butter-chicken-illust.jpg'),
jalebiIllust: mediaUrl(MEDIA_FOLDERS.animation, 'jalebi-illust.jpg'),
kulfiIllust: mediaUrl(MEDIA_FOLDERS.animation, 'kulfi-illust.jpg'),
naanIllust: mediaUrl(MEDIA_FOLDERS.animation, 'naan-illust.jpg'),
spicesIllust: mediaUrl(MEDIA_FOLDERS.animation, 'spices-illust.jpg'),
chefSequence: mediaUrl(MEDIA_FOLDERS.animation, 'chef_sequence.mp4'),
creamBg: mediaUrl(MEDIA_FOLDERS.animation, 'cream_bg.mp4'),
bannerOriginal: mediaUrl(MEDIA_FOLDERS.animation, 'banner_original.mp4'),
},
dishes: {
/** Default poster when a dish has no image/video */
defaultPoster: mediaUrl(MEDIA_FOLDERS.dishes, 'palak-paneer.jpg'),
},
/** Global fallbacks used in onError handlers */
fallbacks: {
logo: mediaUrl(MEDIA_FOLDERS.logo, 'logo1.png'),
dishPoster: mediaUrl(MEDIA_FOLDERS.dishes, 'palak-paneer.jpg'),
},
} as const;
/** Restaurant interior gallery (real photos) — add/remove filenames here */
export const RESTAURANT_GALLERY = [
'real1.jpg',
'real2.jpg',
'real3.jpg',
'real4.jpg',
'real5.jpg',
'real6.jpg',
'real7.jpg',
'real9.jpg',
'real11.jpg',
'real12.jpg',
'real13.jpg',
'real15.jpg',
'real16.jpg',
'real17.jpg',
'real18.jpg',
'real19.jpg',
] as const;
export function restaurantPhoto(filename: string): string {
return mediaUrl(MEDIA_FOLDERS.real, filename);
}
export function tableQrCode(branch: 'askim' | 'backaplan', tableNumber: string): string {
const folder = MEDIA_FOLDERS.booking[branch];
const prefix = branch === 'askim' ? 'qr-askim' : 'qr-backaplan';
return mediaUrl(folder, `${prefix}-${tableNumber}.svg`);
}
@@ -0,0 +1,102 @@
/** Infrastructure adapter: Web Audio API sound effects. */
let audioContext: AudioContext | null = null;
function getAudioContext(): AudioContext | null {
if (typeof window === 'undefined') return null;
if (!audioContext) {
try {
audioContext = new (window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!)();
} catch {
return null;
}
}
return audioContext;
}
export function playHoverSound() {
const ctx = getAudioContext();
if (!ctx) return;
try {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
osc.type = 'sine';
osc.frequency.value = 1100;
filter.type = 'lowpass';
filter.frequency.value = 1400;
gain.gain.value = 0.04;
osc.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
osc.start();
gain.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.12);
setTimeout(() => osc.stop(), 150);
} catch {
// Silent fail
}
}
export function playAddSound() {
const ctx = getAudioContext();
if (!ctx) return;
try {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
osc.type = 'sine';
osc.frequency.value = 1080;
filter.type = 'lowpass';
filter.frequency.value = 1550;
gain.gain.value = 0.022;
osc.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
osc.start();
gain.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.08);
setTimeout(() => osc.stop(), 120);
} catch {
// Silent fail
}
}
export function playSuccessSound() {
const ctx = getAudioContext();
if (!ctx) return;
try {
const notes = [523, 659, 784, 1046];
notes.forEach((freq, i) => {
setTimeout(() => {
try {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
const filter = ctx.createBiquadFilter();
osc.type = 'sine';
osc.frequency.value = freq;
filter.type = 'lowpass';
filter.frequency.value = 2000;
gain.gain.value = 0.07;
osc.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
osc.start();
gain.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.5);
setTimeout(() => osc.stop(), 600);
} catch {
// Silent fail
}
}, i * 120);
});
} catch {
// Silent fail
}
}
+24
View File
@@ -0,0 +1,24 @@
import type { MenuRepository } from '@/domain/menu/repository';
import type { CartRepository } from '@/domain/cart/repository';
import type { WishlistRepository } from '@/domain/wishlist/repository';
import type { LanguageRepository } from '@/domain/language/repository';
import type { MessagingGateway } from '@/application/messaging/ports';
import { StaticMenuRepository } from '@/infrastructure/menu/static-menu-repository';
import { LocalStorageCartRepository } from '@/infrastructure/persistence/local-storage-cart-repository';
import { LocalStorageWishlistRepository } from '@/infrastructure/persistence/local-storage-wishlist-repository';
import { LocalStorageLanguageRepository } from '@/infrastructure/persistence/local-storage-language-repository';
import { BrowserWhatsAppGateway } from '@/infrastructure/messaging/browser-whatsapp-gateway';
/**
* Composition root — wires domain ports to infrastructure adapters.
* Presentation layer imports from here instead of concrete implementations.
*/
class AppContainer {
readonly menuRepository: MenuRepository = new StaticMenuRepository();
readonly cartRepository: CartRepository = new LocalStorageCartRepository();
readonly wishlistRepository: WishlistRepository = new LocalStorageWishlistRepository();
readonly languageRepository: LanguageRepository = new LocalStorageLanguageRepository();
readonly messagingGateway: MessagingGateway = new BrowserWhatsAppGateway();
}
export const container = new AppContainer();
+233
View File
@@ -0,0 +1,233 @@
/**
* =============================================================================
* 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/ (see infrastructure/assets/site-assets.ts)
* 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
*/
import type { MenuCategory, MenuItem } from '@/domain/menu/entities';
import {
SWEETS_HALF_KG_PRICE,
SWEETS_KG_PRICE,
} from '@/domain/sweets/pricing';
const WEIGHT_SWEET_PRICING = {
pricing: 'weight' as const,
price: SWEETS_HALF_KG_PRICE,
pricePerHalfKg: SWEETS_HALF_KG_PRICE,
pricePerKg: SWEETS_KG_PRICE,
};
export type { MenuItem, MenuCategory };
/**
* 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", description: "Crispy fried triangular pastries filled with spiced potatoes and peas.", price: 34, image: "aloo-samosa.jpg", video: "samosa-aloo.mp4" },
{ id: "samosa-keema", name: "Samosa Keema", description: "Flaky pastries stuffed with spiced minced meat filling.", price: 39, image: "keema-samosa.jpg", video: "samosa-keema.mp4" },
{ id: "samosa-chat", name: "Samosa Chat", description: "Crispy samosas topped with spicy chickpeas, yogurt, chutneys and fresh herbs.", price: 89, image: "samosa-chaat.jpg", video: "samosa-chaat.mp4" },
{ id: "chana-chat", name: "Chana Chat", description: "Tangy spiced chickpeas mixed with potatoes, onions, tomatoes and chutneys.", price: 69, image: "chana-chaat.jpg", video: "chana-chaat.mp4" },
{ id: "panipuri", name: "Panipuri / Golgappe", description: "Crispy hollow puris filled with spiced chickpeas and potatoes, served with tangy tamarind water.", price: 69, image: "panipuri.jpg", video: "panipuri.mp4" },
{ id: "keema-naan-starter", name: "Keema Naan", description: "Soft naan bread stuffed with spiced minced meat, baked until golden.", 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", video: "lahore-chana.mp4", isVegetarian: true },
],
},
{
id: "meat",
name: "Meat",
items: [
{ id: "lamm-palak", name: "Lamm Palak", description: "Tender lamb cooked with fresh spinach in a mild, flavorful gravy.", price: 179, image: "lamm-palak.jpg", video: "lamm-palak.mp4" },
{ id: "lamm-vindaloo", name: "Lamm Vindaloo", description: "Spicy and tangy lamb curry in a vinegar and chili-based sauce.", price: 179, image: "lamm-vindaloo.jpg", video: "lamm-vindaloo.mp4" },
{ id: "lamm-rogan-josh", name: "Lamm Rogan Josh", description: "Aromatic lamb curry simmered in a rich yogurt and Kashmiri spice gravy.", price: 199, image: "lamm-rogan-josh.jpg", video: "lamm-rogan-josh.mp4" },
{ id: "lamm-karahi", name: "Lamm Karahi", description: "Lamb pieces stir-fried in a wok with tomatoes, ginger, garlic and spices.", price: 179, image: "lamm-karahi.jpg", video: "lamm-karahi.mp4" },
{ id: "bong-nihari", name: "Bong Nihari", description: "Slow-cooked beef shank in a rich, aromatic gravy, traditionally served with naan.", price: 199, image: "bong-nihari.jpg", video: "bong-nihari.mp4" },
{ id: "paye", name: "Paye", description: "Slow-simmered lamb trotters in a thick, spicy and flavorful gravy.", price: 149, image: "paye.jpg", video: "paye.mp4" },
],
},
{
id: "burger-sandwich",
name: "Burger & Sandwich",
items: [
{ id: "shahi-burger", name: "Shahi Burger", description: "Juicy spiced meat patty in a soft bun with special sauces, lettuce and tomatoes.", price: 119, image: "shahi-burger.jpg", video: "shahi-burger.mp4" },
{ id: "shami-sandwich", name: "Shami Sandwich Menu", description: "Spiced minced meat shami kebab patties served in bread with chutney and onions.", price: 99, image: "shami-sandwich.jpg", video: "shami-sandwich.mp4" },
],
},
{
id: "chicken",
name: "Chicken",
items: [
{ id: "chicken-biryani", name: "Chicken Biryani", description: "Fragrant aged basmati rice layered with tender spiced chicken, saffron and caramelized onions.", price: 149, image: "chicken-biryani.jpg", video: "chicken-biryani.mp4" },
{ id: "chicken-tikka", name: "Chicken Tikka", description: "Boneless chicken pieces marinated in yogurt and spices, grilled in a tandoor.", price: 149, image: "chicken-tikka.jpg", video: "chicken-tikka.mp4" },
{ id: "tikka-boti", name: "Tikka Boti", description: "Tender chicken chunks marinated in spices and grilled on skewers.", price: 149, image: "chicken-tikka.jpg", video: "tikka-boti.mp4" },
{ id: "chicken-karahi", name: "Chicken Karahi", description: "Wok-tossed chicken in a robust tomato, chili and ginger gravy.", price: 149, image: "chicken-karahi.jpg", video: "chicken-karahi.mp4" },
{ id: "lahore-sizzler", name: "Lahore Sizzler", description: "Sizzling platter of marinated chicken with vegetables and spicy sauces.", price: 169, image: "lahore-sizzler.jpg", video: "lahore-sizzler.mp4" },
{ id: "butter-chicken", name: "Butter Chicken", description: "Tender chicken in a creamy tomato and butter gravy with mild spices.", price: 149, image: "butter-chicken.jpg" },
{ id: "chicken-haleem", name: "Chicken Haleem", description: "Slow-cooked shredded chicken with lentils, wheat and aromatic spices.", price: 149, image: "chicken-haleem.jpg", video: "chicken-haleem.mp4" },
],
},
{
id: "pizza",
name: "Pizza",
items: [
{ id: "lahore-pizza", name: "Lahore Pizza", description: "Pizza topped with spiced chicken, onions and special Lahori sauces on a crispy base.", price: 119, image: "lahore-pizza.jpg", video: "lahore-pizza.mp4" },
{ id: "kebab-pizza", name: "Kebab Pizza", description: "Pizza with minced meat kebab topping, cheese, onions and aromatic spices.", price: 119, image: "kebab-pizza.jpg", video: "kebab-pizza.mp4" },
{ id: "tikka-boti-pizza", name: "Tikka Boti Pizza", description: "Pizza featuring grilled chicken tikka, cheese, tomatoes and fresh herbs.", price: 119, image: "tikka-boti-pizza.jpg", video: "tikka-boti-pizza.mp4" },
{ id: "peshawari-pizza", name: "Peshawari Pizza", description: "Naan-style pizza with tender meat, nuts, raisins and Peshawari spices.", price: 119, image: "peshawari-pizza.jpg", video: "peshawari-pizza.mp4" },
{ id: "veg-pizza", name: "Veg Pizza", description: "Vegetarian pizza loaded with fresh vegetables, cheese and tomato sauce.", price: 109, image: "veg-pizza.jpg", isVegetarian: true },
],
},
{
id: "naan-roll",
name: "Naan Roll",
items: [
{ id: "tikka-boti-roll", name: "Tikka Boti Roll", description: "Grilled chicken tikka wrapped in soft naan with mint chutney and onions.", price: 99, image: "tikka-boti-roll.jpg", video: "tikka-boti-roll.mp4" },
{ id: "kebab-roll", name: "Kebab Roll", description: "Spiced minced meat kebab wrapped in naan with chutney, salad and sauces.", price: 99, image: "kebab-roll.jpg", video: "kebab-roll.mp4" },
{ id: "falafel-roll", name: "Falafel Roll", description: "Crispy falafel wrapped in naan with vegetables, hummus and tangy sauces.", price: 99, image: "falafel-roll.jpg", video: "falafel-roll.mp4", isVegetarian: true },
{ id: "paneer-roll", name: "Paneer Roll", description: "Grilled paneer cubes wrapped in naan with spices, chutney and fresh vegetables.", price: 99, image: "paneer-roll.jpg", video: "paneer-roll.mp4", isVegetarian: true },
],
},
{
id: "sweets",
name: "Sweets / Mithai",
items: [
{ id: "namakpare", name: "Namak Paray", description: "Crispy, savory fried flour snacks seasoned with carom seeds and salt.", image: "namakpare.jpg", video: "namakpare.mp4", ...WEIGHT_SWEET_PRICING },
{ id: "shakar-paray", name: "Shakar Paray", description: "Sweet crispy flour bites coated in sugar syrup — a classic mithai snack.", image: "shakar-paray.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "jalebi", name: "Jalebi", description: "Crispy golden saffron spirals soaked in fragrant sugar syrup.", image: "jalebi.jpg", video: "jalebi.mp4", ...WEIGHT_SWEET_PRICING },
{ id: "gajar-halwa", name: "Gajar Halwa", description: "Sweet carrot pudding cooked slowly with milk, sugar, ghee and nuts.", image: "gajar-halwa.jpg", video: "gajar-halwa.mp4", ...WEIGHT_SWEET_PRICING },
{ id: "gajar-barfi", name: "Gajar Barfi", description: "Rich carrot fudge made with milk, ghee and nuts — dense and aromatic.", image: "gajar-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "habshi-halwa", name: "Habshi Halwa", description: "Dark, caramelised semolina halwa slow-cooked with ghee, milk and nuts.", image: "habshi-halwa.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "gulab-jaman", name: "Gulab Jamun", description: "Soft milk-solid dumplings soaked in rose-cardamom sugar syrup.", image: "gulab-jaman.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "round-gulab-jaman", name: "Round Gulab Jamun", description: "Classic round gulab jamun — warm, syrupy and melt-in-the-mouth.", image: "round-gulab-jaman.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "lambay-gulab-jaman", name: "Lambay Gulab Jamun", description: "Elongated gulab jamun with extra syrup — a Shahi Sweets favourite.", image: "lambay-gulab-jaman.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "cream-gulab-jaman", name: "Cream Gulab Jamun", description: "Gulab jamun filled with creamy centre, finished in fragrant syrup.", image: "cream-gulab-jaman.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "ras-gulay", name: "Ras Gulay", description: "Spongy cottage-cheese balls in light sugar syrup — chilled and refreshing.", image: "ras-gulay.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "rasmalai", name: "Rasmalai", description: "Soft cheese dumplings soaked in chilled sweetened milk with cardamom and saffron.", image: "rasmalai.jpg", video: "rasmalai.mp4", ...WEIGHT_SWEET_PRICING },
{ id: "cham-cham", name: "Cham Cham", description: "Oval Bengali sweet coated in coconut or pistachio — soft and milky.", image: "cham-cham.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "paira", name: "Paira", description: "Traditional milk fudge sweet with a smooth, grainy texture.", image: "paira.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "laddu", name: "Laddu", description: "Round gram-flour and ghee sweet balls — festive and aromatic.", image: "laddu.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "qalakand", name: "Kalakand", description: "Grainy milk cake sweet flavoured with cardamom — fresh and delicate.", image: "qalakand.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "patisa", name: "Patisa", description: "Flaky, layered soan papdi-style sweet that crumbles and melts on the tongue.", image: "patisa.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "baisan-patisa", name: "Besan Patisa", description: "Gram-flour patisa with crisp layers and a nutty, buttery finish.", image: "baisan-patisa.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "plain-barfi", name: "Plain Barfi", description: "Classic milk barfi set with sugar and cardamom — simple and elegant.", image: "plain-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "badam-barfi", name: "Badam Barfi", description: "Rich almond barfi with ground nuts and a smooth, luxurious bite.", image: "badam-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "pistachio-barfi", name: "Pistachio Barfi", description: "Vibrant pistachio barfi — nutty, fragrant and beautifully green.", image: "pistachio-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "pink-barfi", name: "Pink Barfi", description: "Festive pink milk barfi with a soft texture and delicate sweetness.", image: "pink-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "coconut-barfi", name: "Coconut Barfi", description: "Coconut-forward barfi with tropical aroma and a chewy-soft bite.", image: "coconut-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "chocolate-barfi", name: "Chocolate Barfi", description: "Milk barfi blended with cocoa — a modern twist on a classic sweet.", image: "chocolate-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "baisan-barfi", name: "Besan Barfi", description: "Roasted gram-flour barfi with ghee and sugar — warm and nutty.", image: "baisan-barfi.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "milk-cake-plain", name: "Milk Cake", description: "Caramelised milk cake with a dense, fudgy centre and golden crust.", image: "milk-cake-plain.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "milk-cake-khajoor", name: "Milk Cake Khajoor", description: "Milk cake with dates — rich, chewy and naturally sweet.", image: "milk-cake-khajoor.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "milk-cake-akhrot", name: "Milk Cake Akhrot", description: "Milk cake studded with walnuts for extra crunch and depth.", image: "milk-cake-akhrot.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "shahi-tukra", name: "Shahi Tukra", description: "Royal bread pudding with fried bread, rabri, nuts and saffron.", image: "shahi-tukra.jpg", ...WEIGHT_SWEET_PRICING },
{ id: "kulfi", name: "Kulfi", description: "Creamy traditional frozen milk dessert with cardamom, pistachios and saffron.", price: 39, image: "kulfi.jpg", video: "kulfi.mp4" },
],
},
{
id: "drinks",
name: "Drinks",
items: [
{ id: "masala-chai", name: "Masala Chai", description: "Traditional spiced tea brewed with milk, cardamom, ginger and aromatic spices.", price: 39, image: "masala-chai.jpg" },
{ id: "mango-lassi", name: "Mango Lassi", description: "Refreshing sweet yogurt drink blended with ripe mango and cardamom.", price: 45, image: "mango-lassi.jpg", video: "mango-lassi.mp4" },
{ id: "coca-cola", name: "Coca-Cola", description: "Classic chilled cola soft drink.", price: 29, video: "coca-cola.mp4" },
{ id: "pepsi-fanta", name: "Pepsi / Fanta", description: "Refreshing cola or orange flavored carbonated beverage.", price: 29, video: "pepsi-fanta.mp4" },
{ id: "sprite-ramlosa", name: "Sprite / Ramlösa", description: "Crisp lemon-lime soda or sparkling mineral water.", price: 29, video: "sprite-ramlosa.mp4" },
{ id: "energy-drink", name: "Energy Drink", description: "Caffeinated beverage for an instant energy boost.", price: 39, video: "energy-drink.mp4" },
{ id: "juice", name: "Juice", description: "Fresh fruit juice, typically mango or other seasonal flavors.", price: 20, image: "mango-juice.jpg" },
{ id: "coffee", name: "Coffee", description: "Freshly brewed hot coffee.", price: 39, image: "black-coffee.jpg" },
{ id: "latte", name: "Latte", description: "Espresso coffee with steamed milk and a light layer of foam.", price: 49, image: "latte.jpg", video: "latte.mp4" },
{ id: "cappuccino", name: "Cappuccino", description: "Espresso topped with steamed milk and thick foam.", price: 49, image: "cappuccino.jpg" },
{ id: "tea", name: "Tea", description: "Traditional hot black tea, served plain or with milk.", price: 30, video: "tea.mp4" },
],
},
];
/**
* 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);
export function getMenuItemById(id: string): MenuItem | undefined {
return allMenuItems.find((item) => item.id === id);
}
@@ -0,0 +1,16 @@
import type { MenuRepository } from '@/domain/menu/repository';
import { allMenuItems, menuCategories } from './static-menu-data';
export class StaticMenuRepository implements MenuRepository {
getCategories() {
return menuCategories;
}
getAllItems() {
return allMenuItems;
}
findItemById(id: string) {
return allMenuItems.find((item) => item.id === id);
}
}
@@ -0,0 +1,13 @@
import type { MessagingGateway } from '@/application/messaging/ports';
import { RESTAURANT_CONTACT } from '@/domain/shared/constants';
export class BrowserWhatsAppGateway implements MessagingGateway {
openWhatsApp(message: string): void {
if (typeof window === 'undefined') return;
const encoded = encodeURIComponent(message);
window.open(
`https://wa.me/${RESTAURANT_CONTACT.whatsappNumber}?text=${encoded}`,
'_blank'
);
}
}
@@ -0,0 +1,24 @@
import type { CartRepository } from '@/domain/cart/repository';
import type { CartItem } from '@/domain/cart/entities';
import { STORAGE_KEYS } from '@/domain/shared/constants';
export class LocalStorageCartRepository implements CartRepository {
load(): CartItem[] {
if (typeof window === 'undefined') return [];
const raw = localStorage.getItem(STORAGE_KEYS.cart);
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as CartItem[];
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
save(items: CartItem[]): void {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEYS.cart, JSON.stringify(items));
}
}
@@ -0,0 +1,18 @@
import type { LanguageRepository } from '@/domain/language/repository';
import { isLanguage, type Language } from '@/domain/language/entities';
import { STORAGE_KEYS } from '@/domain/shared/constants';
export class LocalStorageLanguageRepository implements LanguageRepository {
load(): Language | null {
if (typeof window === 'undefined') return null;
const saved = localStorage.getItem(STORAGE_KEYS.language);
if (saved && isLanguage(saved)) return saved;
return null;
}
save(language: Language): void {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEYS.language, language);
}
}
@@ -0,0 +1,24 @@
import type { WishlistRepository } from '@/domain/wishlist/repository';
import type { WishlistItem } from '@/domain/wishlist/entities';
import { STORAGE_KEYS } from '@/domain/shared/constants';
export class LocalStorageWishlistRepository implements WishlistRepository {
load(): WishlistItem[] {
if (typeof window === 'undefined') return [];
const raw = localStorage.getItem(STORAGE_KEYS.wishlist);
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as WishlistItem[];
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
save(items: WishlistItem[]): void {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEYS.wishlist, JSON.stringify(items));
}
}