Replace entire repo content with updated code from /root/shahikitchen-website/

This commit is contained in:
root
2026-06-30 12:19:55 +00:00
parent 50e3a34895
commit c8b7dc95e4
26 changed files with 1153 additions and 161 deletions
+5
View File
@@ -55,6 +55,11 @@
scroll-margin-top: var(--header-height); scroll-margin-top: var(--header-height);
} }
/* Prevent horizontal scroll from overflowing children */
html {
overflow-x: clip;
}
/* Base */ /* Base */
body, body,
.shahi-body { .shahi-body {
+1 -1
View File
@@ -113,7 +113,7 @@ export default function RootLayout({
{children} {children}
<CartDrawer /> <CartDrawer />
<WishlistDrawer /> <WishlistDrawer />
<Toaster position="top-center" richColors closeButton /> <Toaster position="top-center" richColors closeButton offset={124} />
</WishlistProvider> </WishlistProvider>
</CartProvider> </CartProvider>
</LanguageProvider> </LanguageProvider>
+2 -2
View File
@@ -55,7 +55,7 @@ export default function LocationsPage() {
{/* Branch 1: Askim */} {/* Branch 1: Askim */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8"> <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> <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> <h2 className="text-2xl sm:text-3xl md:text-4xl tracking-[-1.5px] mb-4 break-words">Shahi Kitchen Askim (Sisjön)</h2>
<div className="space-y-5 text-[15px]"> <div className="space-y-5 text-[15px]">
<div> <div>
@@ -95,7 +95,7 @@ export default function LocationsPage() {
{/* Branch 2: Backaplan */} {/* Branch 2: Backaplan */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8"> <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> <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> <h2 className="text-2xl sm:text-3xl md:text-4xl tracking-[-1.5px] mb-4 break-words">Shahi Sweets Backaplan</h2>
<div className="space-y-5 text-[15px]"> <div className="space-y-5 text-[15px]">
<div> <div>
+7 -7
View File
@@ -137,7 +137,7 @@ export default function LoginPage() {
{t.auth.usernameLabel} {t.auth.usernameLabel}
</label> </label>
<div className="relative"> <div className="relative">
<User className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <User className="absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="username" id="username"
name="username" name="username"
@@ -146,7 +146,7 @@ export default function LoginPage() {
onChange={handleChange} onChange={handleChange}
placeholder={t.auth.usernamePlaceholder} placeholder={t.auth.usernamePlaceholder}
required required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -157,7 +157,7 @@ export default function LoginPage() {
{t.auth.passwordLabel} {t.auth.passwordLabel}
</label> </label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Lock className="absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="password" id="password"
name="password" name="password"
@@ -166,7 +166,7 @@ export default function LoginPage() {
onChange={handleChange} onChange={handleChange}
placeholder={t.auth.passwordPlaceholder} placeholder={t.auth.passwordPlaceholder}
required required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -177,21 +177,21 @@ export default function LoginPage() {
{t.auth.siteLabel} {t.auth.siteLabel}
</label> </label>
<div className="relative"> <div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" /> <MapPin className="pointer-events-none absolute start-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select <select
id="site" id="site"
name="site" name="site"
value={formData.site} value={formData.site}
onChange={handleChange} onChange={handleChange}
required required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
> >
<option value="">{t.auth.sitePlaceholder}</option> <option value="">{t.auth.sitePlaceholder}</option>
<option value="askim">{t.auth.askim}</option> <option value="askim">{t.auth.askim}</option>
<option value="backaplan">{t.auth.backaplan}</option> <option value="backaplan">{t.auth.backaplan}</option>
</select> </select>
{/* Custom dropdown arrow for polish */} {/* Custom dropdown arrow for polish */}
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div> <div className="pointer-events-none absolute end-4 top-4 text-[#B38B4D]"></div>
</div> </div>
<p className="mt-1.5 text-[11px] text-[#8A8478]">Select the location you are working at today.</p> <p className="mt-1.5 text-[11px] text-[#8A8478]">Select the location you are working at today.</p>
</div> </div>
+32 -17
View File
@@ -18,6 +18,11 @@ import { useCart } from "@/components/CartContext";
import WishlistButton from "@/components/WishlistButton"; import WishlistButton from "@/components/WishlistButton";
import { useLanguage } from "@/lib/language-context"; import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations"; import { getTranslation } from "@/lib/translations";
import {
getCategoryName,
getMenuItemDescription,
getMenuItemName,
} from "@/application/i18n/menu-localization";
import { import {
dishImageUrl, dishImageUrl,
getMenuPosterSrc, getMenuPosterSrc,
@@ -45,7 +50,10 @@ export default function MenuPage() {
// Sidebar categories // Sidebar categories
const sidebarCategories = [ const sidebarCategories = [
{ id: "All", name: t.menu.allDishes || "All Dishes" }, { id: "All", name: t.menu.allDishes || "All Dishes" },
...menuCategories.map((cat) => ({ id: cat.id, name: cat.name })), ...menuCategories.map((cat) => ({
id: cat.id,
name: getCategoryName(language, cat.id, cat.name),
})),
]; ];
// Filtering logic // Filtering logic
@@ -62,11 +70,15 @@ export default function MenuPage() {
// Search // Search
if (searchQuery.trim()) { if (searchQuery.trim()) {
const q = searchQuery.toLowerCase().trim(); const q = searchQuery.toLowerCase().trim();
items = items.filter( items = items.filter((item) => {
(item) => const localizedName = getMenuItemName(language, item).toLowerCase();
item.name.toLowerCase().includes(q) || const localizedDescription = getMenuItemDescription(language, t, item).toLowerCase();
(item.description && item.description.toLowerCase().includes(q)) return (
localizedName.includes(q) ||
localizedDescription.includes(q) ||
item.name.toLowerCase().includes(q)
); );
});
} }
// Vegetarian // Vegetarian
@@ -77,7 +89,7 @@ export default function MenuPage() {
return { ...category, items }; return { ...category, items };
}) })
.filter((category) => category.items.length > 0); .filter((category) => category.items.length > 0);
}, [searchQuery, showVegetarianOnly, activeCategory]); }, [searchQuery, showVegetarianOnly, activeCategory, language, t]);
const handleCategorySelect = (id: string) => { const handleCategorySelect = (id: string) => {
setActiveCategory(id); setActiveCategory(id);
@@ -85,9 +97,9 @@ export default function MenuPage() {
}; };
const getItemDescription = (item: MenuItem) => const getItemDescription = (item: MenuItem) =>
(t as { menuDescriptions?: Record<string, string> }).menuDescriptions?.[item.id] || getMenuItemDescription(language, t, item);
item.description ||
""; const getItemName = (item: MenuItem) => getMenuItemName(language, item);
const openDishDetails = (item: MenuItem) => { const openDishDetails = (item: MenuItem) => {
setDetailItem(item); setDetailItem(item);
@@ -173,7 +185,7 @@ export default function MenuPage() {
<div className="sticky top-[116px]"> <div className="sticky top-[116px]">
<div className="mb-6"> <div className="mb-6">
<div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div> <div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div>
<h3 className="font-serif text-3xl tracking-tight">Signature Categories</h3> <h3 className="font-serif text-2xl sm:text-3xl tracking-tight">Signature Categories</h3>
</div> </div>
{/* Smooth scroll: horizontal on mobile, vertical on desktop when list exceeds viewport */} {/* Smooth scroll: horizontal on mobile, vertical on desktop when list exceeds viewport */}
@@ -265,7 +277,9 @@ export default function MenuPage() {
filteredCategories.map((category) => ( filteredCategories.map((category) => (
<div key={category.id} className="mb-16"> <div key={category.id} className="mb-16">
<div className="flex items-center gap-4 mb-7"> <div className="flex items-center gap-4 mb-7">
<div className="text-3xl tracking-[-1px] text-[#101724]">{category.name}</div> <div className="text-xl sm:text-2xl md:text-3xl tracking-[-1px] text-[#101724] min-w-0 break-words">
{getCategoryName(language, category.id, category.name)}
</div>
<div className="flex-1 h-px bg-gradient-to-r from-[#e5e1d7] to-transparent" /> <div className="flex-1 h-px bg-gradient-to-r from-[#e5e1d7] to-transparent" />
<div className="text-sm font-medium text-[#68717f] tracking-widest"> <div className="text-sm font-medium text-[#68717f] tracking-widest">
{category.items.length} {t.menu.dishes} {category.items.length} {t.menu.dishes}
@@ -314,7 +328,7 @@ export default function MenuPage() {
> >
<motion.img <motion.img
src={getCardImageSrc(item, category.id)} src={getCardImageSrc(item, category.id)}
alt={item.name} alt={getItemName(item)}
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
loading="lazy" loading="lazy"
whileHover={!isMobile ? { whileHover={!isMobile ? {
@@ -337,18 +351,18 @@ export default function MenuPage() {
)} )}
<div className="absolute top-3 right-3 z-10"> <div className="absolute top-3 right-3 z-10">
<WishlistButton <WishlistButton
item={{ id: item.id, name: item.name, price: item.price, image: item.image }} item={{ id: item.id, name: getItemName(item), price: item.price, image: item.image }}
size="sm" size="sm"
/> />
</div> </div>
</div> </div>
<div className="p-6 flex flex-col flex-1"> <div className="p-6 flex flex-col flex-1">
<div className="flex justify-between items-start gap-4 mb-4"> <div className="flex justify-between items-start gap-3 mb-4 min-w-0">
<h3 className="text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26]"> <h3 className="min-w-0 flex-1 text-lg sm:text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26] break-words">
{item.name} {getItemName(item)}
</h3> </h3>
{renderMenuPrice(item)} <div className="shrink-0">{renderMenuPrice(item)}</div>
</div> </div>
{usesDetailModal ? ( {usesDetailModal ? (
@@ -404,6 +418,7 @@ export default function MenuPage() {
{detailItem && ( {detailItem && (
<DishDetailsModal <DishDetailsModal
item={detailItem} item={detailItem}
displayName={getItemName(detailItem)}
description={getItemDescription(detailItem)} description={getItemDescription(detailItem)}
isOpen={!!detailItem} isOpen={!!detailItem}
onClose={() => setDetailItem(null)} onClose={() => setDetailItem(null)}
+67 -25
View File
@@ -15,7 +15,7 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { filterMenuItems } from '@/application/menu/filter-menu';
import { getMenuPosterSrc, logoUrl, SITE_ASSETS } from '@/lib/assets'; import { getMenuPosterSrc, logoUrl, SITE_ASSETS } from '@/lib/assets';
import { buildTableOrderPayload } from '@/application/table-order/table-order-use-cases'; import { buildTableOrderPayload } from '@/application/table-order/table-order-use-cases';
import { parseTableId } from '@/domain/table/table-id'; import { parseTableId } from '@/domain/table/table-id';
@@ -34,6 +34,11 @@ import type { MenuItem } from '@/domain/menu/entities';
import { playAddSound, playSuccessSound } from '@/infrastructure/audio/web-audio-sound-adapter'; import { playAddSound, playSuccessSound } from '@/infrastructure/audio/web-audio-sound-adapter';
import { useLanguage } from '@/presentation/providers/language-provider'; import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations'; import { getTranslation } from '@/presentation/i18n/translations';
import {
getMenuItemDescription,
getMenuItemName,
localizeOrderLineName,
} from '@/application/i18n/menu-localization';
import Navbar from '@/components/Navbar'; import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer'; import Footer from '@/components/Footer';
@@ -66,14 +71,20 @@ export default function OrderFromTablePage() {
const tableDisplay = isValid ? parsed.tableNumber : ''; const tableDisplay = isValid ? parsed.tableNumber : '';
// Filtered dishes for the selector grid (same data source as everywhere else) // Filtered dishes for the selector grid (same data source as everywhere else)
const filteredDishes = useMemo( const filteredDishes = useMemo(() => {
() => const query = search.toLowerCase().trim();
filterMenuItems(allMenuItems, { return allMenuItems.filter((item) => {
searchQuery: search, if (vegOnly && !item.isVegetarian) return false;
vegetarianOnly: vegOnly, if (!query) return true;
}), const localizedName = getMenuItemName(language, item).toLowerCase();
[allMenuItems, search, vegOnly] const localizedDescription = getMenuItemDescription(language, t, item).toLowerCase();
return (
localizedName.includes(query) ||
localizedDescription.includes(query) ||
item.name.toLowerCase().includes(query)
); );
});
}, [allMenuItems, search, vegOnly, language, t]);
const selectedTotal = calculateOrderTotal(selected); const selectedTotal = calculateOrderTotal(selected);
@@ -231,7 +242,7 @@ export default function OrderFromTablePage() {
</div> </div>
</div> </div>
<div className="max-w-6xl mx-auto px-6 pt-8 pb-24"> <div className={`max-w-6xl mx-auto px-6 pt-8 ${selected.length > 0 ? 'pb-36 lg:pb-24' : 'pb-24'}`}>
{/* Intro */} {/* Intro */}
<div className="mb-8"> <div className="mb-8">
<div className="text-[#B38B4D] text-xs tracking-[3.5px] mb-2 font-medium">{to.tableLabel.toUpperCase()} {tableDisplay}</div> <div className="text-[#B38B4D] text-xs tracking-[3.5px] mb-2 font-medium">{to.tableLabel.toUpperCase()} {tableDisplay}</div>
@@ -240,18 +251,18 @@ export default function OrderFromTablePage() {
</div> </div>
<div className="grid lg:grid-cols-12 gap-8"> <div className="grid lg:grid-cols-12 gap-8">
{/* DISH SELECTOR */} {/* DISH SELECTOR — below summary on mobile for faster checkout */}
<div className="lg:col-span-7"> <div className="order-2 lg:order-1 lg:col-span-7">
{/* Filters */} {/* Filters */}
<div className="mb-5 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center"> <div className="mb-5 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="pointer-events-none absolute left-4 top-3.5 h-4 w-4 text-[#B38B4D]" /> <Search className="pointer-events-none absolute start-4 top-3.5 h-4 w-4 text-[#B38B4D]" />
<input <input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={to.searchPlaceholder} placeholder={to.searchPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-white py-3 pl-11 pr-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:ring-1 focus:ring-[#B38B4D]/20 outline-none" className="w-full rounded-2xl border border-[#EDE6D9] bg-white py-3 ps-11 pe-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:ring-1 focus:ring-[#B38B4D]/20 outline-none"
/> />
</div> </div>
<button <button
@@ -273,6 +284,8 @@ export default function OrderFromTablePage() {
filteredDishes.map((dish) => { filteredDishes.map((dish) => {
const inOrder = selected.some((s) => s.id === dish.id); const inOrder = selected.some((s) => s.id === dish.id);
const poster = getMenuPosterSrc(dish, 'optimized'); const poster = getMenuPosterSrc(dish, 'optimized');
const dishName = getMenuItemName(language, dish);
const dishDescription = getMenuItemDescription(language, t, dish);
return ( return (
<motion.button <motion.button
key={dish.id} key={dish.id}
@@ -283,7 +296,7 @@ export default function OrderFromTablePage() {
<div className="relative h-20 w-20 flex-shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]"> <div className="relative h-20 w-20 flex-shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]">
<img <img
src={poster} src={poster}
alt={dish.name} alt={dishName}
className="absolute inset-0 h-full w-full object-cover group-active:scale-105 transition" className="absolute inset-0 h-full w-full object-cover group-active:scale-105 transition"
onError={(e) => { onError={(e) => {
const target = e.currentTarget as HTMLImageElement; const target = e.currentTarget as HTMLImageElement;
@@ -296,7 +309,7 @@ export default function OrderFromTablePage() {
<div className="flex min-w-0 flex-1 flex-col py-0.5"> <div className="flex min-w-0 flex-1 flex-col py-0.5">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="font-medium tracking-[-0.2px] text-[15px] leading-tight line-clamp-2 pr-1"> <div className="font-medium tracking-[-0.2px] text-[15px] leading-tight line-clamp-2 pr-1">
{dish.name} {dishName}
</div> </div>
<div className="shrink-0 text-right font-semibold text-[#B38B4D] tabular-nums text-sm leading-tight"> <div className="shrink-0 text-right font-semibold text-[#B38B4D] tabular-nums text-sm leading-tight">
{dish.pricing === 'weight' ? ( {dish.pricing === 'weight' ? (
@@ -313,9 +326,9 @@ export default function OrderFromTablePage() {
</div> </div>
</div> </div>
{dish.description && ( {dishDescription && (
<div className="mt-1 text-xs text-[#6B665F] line-clamp-2"> <div className="mt-1 text-xs text-[#6B665F] line-clamp-2">
{dish.description} {dishDescription}
</div> </div>
)} )}
@@ -338,9 +351,9 @@ export default function OrderFromTablePage() {
</div> </div>
</div> </div>
{/* ORDER SUMMARY (sticky on desktop) */} {/* ORDER SUMMARY (first on mobile, sticky on desktop) */}
<div className="lg:col-span-5"> <div className="order-1 lg:order-2 lg:col-span-5">
<div className="lg:sticky lg:top-24"> <div className="lg:sticky lg:top-[calc(var(--header-height)+5.5rem)]">
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 shadow-sm"> <div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 shadow-sm">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="font-semibold tracking-tight text-lg">{to.yourOrder}</div> <div className="font-semibold tracking-tight text-lg">{to.yourOrder}</div>
@@ -364,7 +377,7 @@ export default function OrderFromTablePage() {
{selected.map((item) => ( {selected.map((item) => (
<div key={item.id} className="flex items-center gap-3 rounded-2xl bg-[#F8F5F0] px-4 py-3"> <div key={item.id} className="flex items-center gap-3 rounded-2xl bg-[#F8F5F0] px-4 py-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="font-medium text-[15px] tracking-[-0.2px]">{item.name}</div> <div className="font-medium text-[15px] tracking-[-0.2px]">{localizeOrderLineName(language, item)}</div>
<div className="text-xs text-[#6B665F] tabular-nums"> <div className="text-xs text-[#6B665F] tabular-nums">
{item.pricingMode === 'weight' {item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}` ? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
@@ -376,7 +389,7 @@ export default function OrderFromTablePage() {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<button <button
onClick={() => updateQty(item.id, item.quantity - 1)} onClick={() => updateQty(item.id, item.quantity - 1)}
className="h-8 w-8 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95" className="h-11 w-11 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95 touch-manipulation"
aria-label="Decrease" aria-label="Decrease"
> >
@@ -386,7 +399,7 @@ export default function OrderFromTablePage() {
</span> </span>
<button <button
onClick={() => updateQty(item.id, item.quantity + 1)} onClick={() => updateQty(item.id, item.quantity + 1)}
className="h-8 w-8 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95" className="h-11 w-11 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95 touch-manipulation"
aria-label="Increase" aria-label="Increase"
> >
+ +
@@ -418,8 +431,8 @@ export default function OrderFromTablePage() {
/> />
</div> </div>
{/* Total + Send */} {/* Total + Send — desktop only; mobile uses sticky bottom bar */}
<div className="mt-5 border-t border-[#EDE6D9] pt-5"> <div className="hidden lg:block mt-5 border-t border-[#EDE6D9] pt-5">
<div className="flex items-baseline justify-between text-lg mb-4"> <div className="flex items-baseline justify-between text-lg mb-4">
<span className="text-[#6B665F]">{to.total}</span> <span className="text-[#6B665F]">{to.total}</span>
<span className="font-semibold tabular-nums">{selectedTotal} kr</span> <span className="font-semibold tabular-nums">{selectedTotal} kr</span>
@@ -437,6 +450,14 @@ export default function OrderFromTablePage() {
Staff will come to your table to confirm. Staff will come to your table to confirm.
</p> </p>
</div> </div>
{/* Mobile: show total inline in summary card */}
<div className="lg:hidden mt-5 border-t border-[#EDE6D9] pt-4">
<div className="flex items-baseline justify-between text-lg">
<span className="text-[#6B665F]">{to.total}</span>
<span className="font-semibold tabular-nums">{selectedTotal} kr</span>
</div>
</div>
</div> </div>
{/* Small context line */} {/* Small context line */}
@@ -448,6 +469,27 @@ export default function OrderFromTablePage() {
</div> </div>
</div> </div>
{/* Mobile sticky order bar */}
{selected.length > 0 && (
<div className="lg:hidden fixed inset-x-0 bottom-0 z-50 border-t border-[#EDE6D9] bg-white/95 backdrop-blur-xl shadow-[0_-4px_24px_-4px_rgba(16,23,36,0.12)] px-4 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
<div className="mx-auto flex max-w-6xl items-center gap-4">
<div className="min-w-0 flex-1">
<div className="text-xs text-[#6B665F]">
{selected.length} {selected.length === 1 ? 'item' : 'items'}
</div>
<div className="font-semibold tabular-nums text-[#101724]">{selectedTotal} kr</div>
</div>
<button
onClick={handleSendOrder}
disabled={isSending}
className="shrink-0 rounded-2xl bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] px-6 py-3.5 text-sm font-semibold tracking-[0.5px] text-white shadow-lg active:brightness-95 disabled:opacity-60 touch-manipulation"
>
{isSending ? 'Sending...' : to.sendOrder}
</button>
</div>
</div>
)}
<Footer /> <Footer />
</div> </div>
); );
+10 -5
View File
@@ -17,6 +17,10 @@ import { menuCategories } from "@/lib/menu-data";
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder"; import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
import type { MenuItem } from "@/domain/menu/entities"; import type { MenuItem } from "@/domain/menu/entities";
import { getTranslation } from "@/lib/translations"; import { getTranslation } from "@/lib/translations";
import {
getMenuItemDescription,
getMenuItemName,
} from "@/application/i18n/menu-localization";
import SignatureMenuMarquee, { type SignatureDish } from "@/components/SignatureMenuMarquee"; import SignatureMenuMarquee, { type SignatureDish } from "@/components/SignatureMenuMarquee";
type SignatureFilter = "All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweets"; type SignatureFilter = "All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweets";
@@ -55,11 +59,12 @@ export default function ShahiKitchenHomepage() {
menuCategories.flatMap((category) => menuCategories.flatMap((category) =>
category.items.map((item) => ({ category.items.map((item) => ({
...item, ...item,
desc: item.description ?? "", name: getMenuItemName(language, item),
desc: getMenuItemDescription(language, t, item),
filterTag: getDishFilterTag(item, category.id), filterTag: getDishFilterTag(item, category.id),
})) }))
), ),
[] [language, t]
); );
const filtered = useMemo(() => { const filtered = useMemo(() => {
@@ -175,9 +180,9 @@ export default function ShahiKitchenHomepage() {
{ title: "Shahi Sweets", desc: "Homemade mithai made daily. From fresh Jalebi to Rasmalai — the perfect sweet ending." }, { 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 Shahi." }, { title: "Warm Hospitality", desc: "Whether you're here for a quick lunch or a family celebration, you will always be treated like Shahi." },
]).map((item, index) => ( ]).map((item, index) => (
<div key={index} className="experience-card group relative border border-[#EDE6D9] p-9 rounded-2xl bg-[#F8F5F0] overflow-hidden"> <div key={index} className="experience-card group relative border border-[#EDE6D9] p-6 sm:p-9 rounded-2xl bg-[#F8F5F0] overflow-hidden">
<div className="text-[#B38B4D] text-6xl font-light mb-9 tracking-[-2px]">0{index + 1}</div> <div className="text-[#B38B4D] text-4xl sm:text-6xl font-light mb-6 sm:mb-9 tracking-[-2px]">0{index + 1}</div>
<h4 className="text-[29px] tracking-[-0.8px] mb-5 text-[#2C2A26] leading-tight">{item.title}</h4> <h4 className="text-xl sm:text-2xl md:text-[29px] tracking-[-0.8px] mb-4 sm:mb-5 text-[#2C2A26] leading-tight break-words">{item.title}</h4>
<p className="text-[#6B665F] text-[15.5px] leading-relaxed">{item.desc}</p> <p className="text-[#6B665F] text-[15.5px] leading-relaxed">{item.desc}</p>
</div> </div>
))} ))}
+47 -32
View File
@@ -26,6 +26,11 @@ import {
import { container } from '@/infrastructure/di/container'; import { container } from '@/infrastructure/di/container';
import { useLanguage } from '@/presentation/providers/language-provider'; import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations'; import { getTranslation } from '@/presentation/i18n/translations';
import {
getMenuItemDescription,
getMenuItemName,
localizeOrderLineName,
} from '@/application/i18n/menu-localization';
import { useState } from 'react'; import { useState } from 'react';
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { MapPin, Calendar, Clock, Users, User, Phone, Mail, MessageCircle, Plus, Minus, X, ArrowRight, Search, CheckCircle } from "lucide-react"; import { MapPin, Calendar, Clock, Users, User, Phone, Mail, MessageCircle, Plus, Minus, X, ArrowRight, Search, CheckCircle } from "lucide-react";
@@ -93,10 +98,16 @@ export default function ReservePage() {
// Flatten menu for selector (with search) // Flatten menu for selector (with search)
const allDishes = menuCategories.flatMap((cat) => cat.items); const allDishes = menuCategories.flatMap((cat) => cat.items);
const filteredDishes = allDishes.filter((dish) => const filteredDishes = allDishes.filter((dish) => {
dish.name.toLowerCase().includes(search.toLowerCase()) || const q = search.toLowerCase();
(dish.description && dish.description.toLowerCase().includes(search.toLowerCase())) const localizedName = getMenuItemName(language, dish).toLowerCase();
const localizedDescription = getMenuItemDescription(language, t, dish).toLowerCase();
return (
localizedName.includes(q) ||
localizedDescription.includes(q) ||
dish.name.toLowerCase().includes(q)
); );
});
const addToPreOrder = (dish: MenuItem) => { const addToPreOrder = (dish: MenuItem) => {
setPreOrder((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish))); setPreOrder((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
@@ -173,7 +184,7 @@ export default function ReservePage() {
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur mx-auto"> <div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur mx-auto">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" /> <img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div> </div>
<div className="font-serif text-4xl tracking-[-1.5px] mb-1">Shahi Kitchen</div> <div className="font-serif text-2xl sm:text-3xl md:text-4xl tracking-[-1.5px] mb-1">Shahi Kitchen</div>
<div className="text-sm font-medium tracking-[2.5px] text-[#c99a2e] mb-4">TABLE RESERVATIONS</div> <div className="text-sm font-medium tracking-[2.5px] text-[#c99a2e] mb-4">TABLE RESERVATIONS</div>
<p className="text-sm leading-relaxed text-white/70 max-w-xs mx-auto mb-4"> <p className="text-sm leading-relaxed text-white/70 max-w-xs mx-auto mb-4">
Two locations. Unforgettable evenings. Pre-order your favorites for a perfect arrival. Two locations. Unforgettable evenings. Pre-order your favorites for a perfect arrival.
@@ -187,7 +198,7 @@ export default function ReservePage() {
<div className="max-w-4xl mx-auto space-y-8"> <div className="max-w-4xl mx-auto space-y-8">
{/* Booking Form Card - now wider (full in max-w-4xl) */} {/* Booking Form Card - now wider (full in max-w-4xl) */}
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10"> <div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10">
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.formTitle}</h2> <h2 className="text-2xl sm:text-3xl tracking-tight font-serif mb-2">{t.booking.formTitle}</h2>
<p className="text-[#6B665F] mb-8">Fill in your details to secure your table.</p> <p className="text-[#6B665F] mb-8">Fill in your details to secure your table.</p>
<form onSubmit={handleContinue} className="space-y-6"> <form onSubmit={handleContinue} className="space-y-6">
@@ -195,21 +206,21 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="location" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.locationLabel}</label> <label htmlFor="location" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.locationLabel}</label>
<div className="relative"> <div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" /> <MapPin className="pointer-events-none absolute start-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select <select
id="location" id="location"
name="location" name="location"
value={booking.location} value={booking.location}
onChange={handleBookingChange} onChange={handleBookingChange}
required required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
> >
<option value="">{t.booking.locationPlaceholder}</option> <option value="">{t.booking.locationPlaceholder}</option>
{locationOptions.map((opt) => ( {locationOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
</select> </select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div> <div className="pointer-events-none absolute end-4 top-4 text-[#B38B4D]"></div>
</div> </div>
</div> </div>
@@ -218,7 +229,7 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="date" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.dateLabel}</label> <label htmlFor="date" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.dateLabel}</label>
<div className="relative"> <div className="relative">
<Calendar className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Calendar className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="date" id="date"
name="date" name="date"
@@ -227,7 +238,7 @@ export default function ReservePage() {
onChange={handleBookingChange} onChange={handleBookingChange}
min={new Date().toISOString().split('T')[0]} min={new Date().toISOString().split('T')[0]}
required required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -236,7 +247,7 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="time" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.timeLabel}</label> <label htmlFor="time" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.timeLabel}</label>
<div className="relative"> <div className="relative">
<Clock className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Clock className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="time" id="time"
name="time" name="time"
@@ -244,7 +255,7 @@ export default function ReservePage() {
value={booking.time} value={booking.time}
onChange={handleBookingChange} onChange={handleBookingChange}
required required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -254,19 +265,19 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="guests" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.guestsLabel}</label> <label htmlFor="guests" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.guestsLabel}</label>
<div className="relative"> <div className="relative">
<Users className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Users className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<select <select
id="guests" id="guests"
name="guests" name="guests"
value={booking.guests} value={booking.guests}
onChange={handleBookingChange} onChange={handleBookingChange}
required required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
> >
<option value="">Select</option> <option value="">Select</option>
{[1,2,3,4,5,6,7,8,9,10].map(n => <option key={n} value={n}>{n}</option>)} {[1,2,3,4,5,6,7,8,9,10].map(n => <option key={n} value={n}>{n}</option>)}
</select> </select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div> <div className="pointer-events-none absolute end-4 top-4 text-[#B38B4D]"></div>
</div> </div>
</div> </div>
@@ -274,7 +285,7 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="name" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.nameLabel}</label> <label htmlFor="name" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.nameLabel}</label>
<div className="relative"> <div className="relative">
<User className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <User className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="name" id="name"
name="name" name="name"
@@ -283,7 +294,7 @@ export default function ReservePage() {
onChange={handleBookingChange} onChange={handleBookingChange}
placeholder={t.booking.namePlaceholder} placeholder={t.booking.namePlaceholder}
required required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -292,7 +303,7 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="phone" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.phoneLabel}</label> <label htmlFor="phone" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.phoneLabel}</label>
<div className="relative"> <div className="relative">
<Phone className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Phone className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="phone" id="phone"
name="phone" name="phone"
@@ -301,7 +312,7 @@ export default function ReservePage() {
onChange={handleBookingChange} onChange={handleBookingChange}
placeholder={t.booking.phonePlaceholder} placeholder={t.booking.phonePlaceholder}
required required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -310,7 +321,7 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="email" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.emailLabel}</label> <label htmlFor="email" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.emailLabel}</label>
<div className="relative"> <div className="relative">
<Mail className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Mail className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
id="email" id="email"
name="email" name="email"
@@ -318,7 +329,7 @@ export default function ReservePage() {
value={booking.email} value={booking.email}
onChange={handleBookingChange} onChange={handleBookingChange}
placeholder={t.booking.emailPlaceholder} placeholder={t.booking.emailPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
</div> </div>
@@ -327,7 +338,7 @@ export default function ReservePage() {
<div> <div>
<label htmlFor="notes" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.notesLabel}</label> <label htmlFor="notes" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.notesLabel}</label>
<div className="relative"> <div className="relative">
<MessageCircle className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <MessageCircle className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<textarea <textarea
id="notes" id="notes"
name="notes" name="notes"
@@ -335,7 +346,7 @@ export default function ReservePage() {
onChange={handleBookingChange} onChange={handleBookingChange}
placeholder={t.booking.notesPlaceholder} placeholder={t.booking.notesPlaceholder}
rows={3} rows={3}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 resize-y" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 resize-y"
/> />
</div> </div>
</div> </div>
@@ -360,18 +371,18 @@ export default function ReservePage() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10" className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10"
> >
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.menuTitle}</h2> <h2 className="text-2xl sm:text-3xl tracking-tight font-serif mb-2">{t.booking.menuTitle}</h2>
<p className="text-[#6B665F] mb-6">{t.booking.menuSubtitle}</p> <p className="text-[#6B665F] mb-6">{t.booking.menuSubtitle}</p>
{/* Search */} {/* Search */}
<div className="relative mb-6"> <div className="relative mb-6">
<Search className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" /> <Search className="pointer-events-none absolute start-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input <input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={t.booking.searchPlaceholder} placeholder={t.booking.searchPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20" className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 ps-12 pe-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/> />
</div> </div>
@@ -380,7 +391,11 @@ export default function ReservePage() {
{filteredDishes.length > 0 ? ( {filteredDishes.length > 0 ? (
filteredDishes.map((dish) => { filteredDishes.map((dish) => {
const posterSrc = getMenuPosterSrc(dish); const posterSrc = getMenuPosterSrc(dish);
const shortDesc = dish.description ? (dish.description.length > 80 ? dish.description.slice(0, 80) + '...' : dish.description) : ''; const dishName = getMenuItemName(language, dish);
const dishDescription = getMenuItemDescription(language, t, dish);
const shortDesc = dishDescription
? (dishDescription.length > 80 ? dishDescription.slice(0, 80) + '...' : dishDescription)
: '';
const isAdded = preOrder.some((p) => p.id === dish.id); const isAdded = preOrder.some((p) => p.id === dish.id);
return ( return (
<motion.div <motion.div
@@ -390,13 +405,13 @@ export default function ReservePage() {
onMouseEnter={() => playHoverSound()} onMouseEnter={() => playHoverSound()}
> >
{/* Item Name */} {/* Item Name */}
<div className="font-semibold tracking-[-0.3px] text-base mb-1 text-[#2C2A26] line-clamp-1">{dish.name}</div> <div className="font-semibold tracking-[-0.3px] text-base mb-1 text-[#2C2A26] line-clamp-1">{dishName}</div>
{/* Poster under name */} {/* Poster under name */}
<div className="relative w-full h-24 sm:h-28 md:h-32 rounded-xl overflow-hidden border border-[#EDE6D9] mb-2 bg-[#F8F5F0]"> <div className="relative w-full h-24 sm:h-28 md:h-32 rounded-xl overflow-hidden border border-[#EDE6D9] mb-2 bg-[#F8F5F0]">
<img <img
src={posterSrc} src={posterSrc}
alt={dish.name} alt={dishName}
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
loading="lazy" loading="lazy"
onError={(e) => { onError={(e) => {
@@ -464,11 +479,11 @@ export default function ReservePage() {
{/* Small poster thumbnail in summary for better visualization */} {/* Small poster thumbnail in summary for better visualization */}
<img <img
src={thumb} src={thumb}
alt={item.name} alt={localizeOrderLineName(language, item)}
className="w-10 h-10 object-cover rounded-lg border border-[#EDE6D9] flex-shrink-0" className="w-10 h-10 object-cover rounded-lg border border-[#EDE6D9] flex-shrink-0"
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{item.name}</div> <div className="font-medium text-sm truncate">{localizeOrderLineName(language, item)}</div>
<div className="text-[10px] text-[#6B665F]"> <div className="text-[10px] text-[#6B665F]">
{item.pricingMode === 'weight' {item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}` ? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
@@ -520,7 +535,7 @@ export default function ReservePage() {
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[#B38B4D]/10"> <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[#B38B4D]/10">
<CheckCircle className="h-10 w-10 text-[#B38B4D]" /> <CheckCircle className="h-10 w-10 text-[#B38B4D]" />
</div> </div>
<h2 className="text-4xl tracking-tight font-serif mb-4">{t.booking.successTitle}</h2> <h2 className="text-2xl sm:text-3xl md:text-4xl tracking-tight font-serif mb-4 break-words">{t.booking.successTitle}</h2>
<p className="text-[#6B665F] mb-8 leading-relaxed">{t.booking.successMessage}</p> <p className="text-[#6B665F] mb-8 leading-relaxed">{t.booking.successMessage}</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
+58
View File
@@ -0,0 +1,58 @@
import type { Language } from '@/domain/language/entities';
import type { MenuItem } from '@/domain/menu/entities';
import type { OrderLine } from '@/domain/shared/order-line';
import { getMenuItemById } from '@/infrastructure/menu/static-menu-data';
import { getCategoryName as resolveCategoryName } from '@/presentation/i18n/menu/category-names';
import { getMenuItemName as resolveItemName } from '@/presentation/i18n/menu/item-names';
import type { TranslationBundle } from '@/presentation/i18n/translations';
type MenuTextBundle = TranslationBundle & {
menuDescriptions?: Record<string, string>;
};
export function getMenuItemName(
lang: Language,
item: Pick<MenuItem, 'id' | 'name'>,
): string {
return resolveItemName(lang, item.id, item.name);
}
export function getMenuItemDescription(
lang: Language,
t: TranslationBundle,
item: Pick<MenuItem, 'id' | 'description'>,
): string {
const bundle = t as MenuTextBundle;
return (
bundle.menuDescriptions?.[item.id] ??
(lang === 'en' ? item.description : undefined) ??
item.description ??
''
);
}
export function getCategoryName(
lang: Language,
categoryId: string,
fallback: string,
): string {
return resolveCategoryName(lang, categoryId, fallback);
}
export function localizeMenuItem(
lang: Language,
t: TranslationBundle,
item: MenuItem,
): MenuItem {
return {
...item,
name: getMenuItemName(lang, item),
description: getMenuItemDescription(lang, t, item),
};
}
export function localizeOrderLineName(lang: Language, line: OrderLine): string {
const menuItem = getMenuItemById(line.id);
if (!menuItem) return line.name;
return getMenuItemName(lang, menuItem);
}
@@ -2,6 +2,7 @@ import type { DeliveryInquiryDetails, PickupInquiryDetails } from '@/domain/cart
import type { BookingDetails } from '@/domain/booking/entities'; import type { BookingDetails } from '@/domain/booking/entities';
import { isBookingComplete } from '@/domain/booking/validation'; import { isBookingComplete } from '@/domain/booking/validation';
import type { Language } from '@/domain/language/entities'; import type { Language } from '@/domain/language/entities';
import { localizeOrderLineName } from '@/application/i18n/menu-localization';
import { import {
calculateOrderTotal, calculateOrderTotal,
formatOrderLinesForInquiry, formatOrderLinesForInquiry,
@@ -46,14 +47,21 @@ export function getSuggestedPickupTime(language: Language, from: Date = new Date
return suggested.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit' }); return suggested.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit' });
} }
function localizeInquiryLines(lang: Language, items: OrderLine[]): string {
return formatOrderLinesForInquiry(
items.map((item) => ({ ...item, name: localizeOrderLineName(lang, item) })),
);
}
export function buildPickupInquiryMessage( export function buildPickupInquiryMessage(
items: OrderLine[], items: OrderLine[],
details: PickupInquiryDetails, details: PickupInquiryDetails,
copy: CartInquiryCopy, copy: CartInquiryCopy,
language: Language = 'en',
): string | null { ): string | null {
if (items.length === 0) return null; if (items.length === 0) return null;
const lines = formatOrderLinesForInquiry(items); const lines = localizeInquiryLines(language, items);
const total = calculateOrderTotal(items); const total = calculateOrderTotal(items);
return `${copy.pickupIntro} return `${copy.pickupIntro}
@@ -67,10 +75,11 @@ export function buildDeliveryInquiryMessage(
items: OrderLine[], items: OrderLine[],
details: DeliveryInquiryDetails, details: DeliveryInquiryDetails,
copy: CartInquiryCopy, copy: CartInquiryCopy,
language: Language = 'en',
): string | null { ): string | null {
if (items.length === 0) return null; if (items.length === 0) return null;
const lines = formatOrderLinesForInquiry(items); const lines = localizeInquiryLines(language, items);
const total = calculateOrderTotal(items); const total = calculateOrderTotal(items);
return `${copy.deliveryIntro} return `${copy.deliveryIntro}
@@ -83,6 +92,15 @@ ${copy.messagePreferredTime}: ${details.preferredTime.trim()}
${copy.deliverySwishNote}`; ${copy.deliverySwishNote}`;
} }
const PACKAGE_NAME_PLACEHOLDER = '{{package}}';
export function buildCateringPackagePricingMessage(
packageName: string,
template: string,
): string {
return template.split(PACKAGE_NAME_PLACEHOLDER).join(packageName);
}
export function buildBookingWhatsAppMessage( export function buildBookingWhatsAppMessage(
booking: BookingDetails, booking: BookingDetails,
preOrder: OrderLine[], preOrder: OrderLine[],
+19 -8
View File
@@ -18,6 +18,8 @@ import { container } from '@/infrastructure/di/container';
import { useCart } from '@/presentation/providers/cart-provider'; import { useCart } from '@/presentation/providers/cart-provider';
import { useLanguage } from '@/presentation/providers/language-provider'; import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations'; import { getTranslation } from '@/presentation/i18n/translations';
import { getMenuItemName } from '@/application/i18n/menu-localization';
import { getMenuItemById } from '@/lib/menu-data';
import CartInquiryModal from '@/components/CartInquiryModal'; import CartInquiryModal from '@/components/CartInquiryModal';
export default function CartDrawer() { export default function CartDrawer() {
@@ -31,7 +33,7 @@ export default function CartDrawer() {
clearCart, clearCart,
} = useCart(); } = useCart();
const { language } = useLanguage(); const { language, isRtl } = useLanguage();
const t = getTranslation(language); const t = getTranslation(language);
const [inquiryMode, setInquiryMode] = useState<FulfillmentMode | null>(null); const [inquiryMode, setInquiryMode] = useState<FulfillmentMode | null>(null);
@@ -53,11 +55,11 @@ export default function CartDrawer() {
}; };
const handlePickupSubmit = (details: PickupInquiryDetails) => { const handlePickupSubmit = (details: PickupInquiryDetails) => {
openWhatsApp(buildPickupInquiryMessage(items, details, inquiryCopy)); openWhatsApp(buildPickupInquiryMessage(items, details, inquiryCopy, language));
}; };
const handleDeliverySubmit = (details: DeliveryInquiryDetails) => { const handleDeliverySubmit = (details: DeliveryInquiryDetails) => {
openWhatsApp(buildDeliveryInquiryMessage(items, details, inquiryCopy)); openWhatsApp(buildDeliveryInquiryMessage(items, details, inquiryCopy, language));
}; };
if (!isOpen) return null; if (!isOpen) return null;
@@ -69,7 +71,11 @@ export default function CartDrawer() {
onClick={closeCart} onClick={closeCart}
/> />
<div className="fixed top-0 right-0 h-full w-full max-w-md bg-[#F8F5F0] z-[990] shadow-2xl flex flex-col"> <div
className={`fixed top-0 h-full w-full max-w-md bg-[#F8F5F0] z-[990] shadow-2xl flex flex-col ${
isRtl ? 'left-0 border-r border-[#EDE6D9]' : 'right-0 border-l border-[#EDE6D9]'
}`}
>
<div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]"> <div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h2 className="text-2xl tracking-[-0.5px]">{t.cartDrawer.title}</h2> <h2 className="text-2xl tracking-[-0.5px]">{t.cartDrawer.title}</h2>
@@ -117,7 +123,12 @@ export default function CartDrawer() {
<div className="flex-1"> <div className="flex-1">
<div className="flex justify-between"> <div className="flex justify-between">
<div> <div>
<h4 className="font-medium tracking-[-0.3px]">{item.name}</h4> <h4 className="font-medium tracking-[-0.3px]">
{getMenuItemName(language, {
id: item.id,
name: getMenuItemById(item.id)?.name ?? item.name,
})}
</h4>
<p className="text-sm text-[#6B665F]"> <p className="text-sm text-[#6B665F]">
{item.pricingMode === 'weight' {item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg` ? `${item.pricePerHalfKg ?? item.price} kr / ½ kg`
@@ -132,7 +143,7 @@ export default function CartDrawer() {
<div className="flex items-center gap-3 mt-3"> <div className="flex items-center gap-3 mt-3">
<button <button
onClick={() => updateQuantity(item.id, item.quantity - 1)} 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" className="w-11 h-11 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition touch-manipulation"
> >
</button> </button>
@@ -141,7 +152,7 @@ export default function CartDrawer() {
</span> </span>
<button <button
onClick={() => updateQuantity(item.id, item.quantity + 1)} 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" className="w-11 h-11 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition touch-manipulation"
> >
+ +
</button> </button>
@@ -161,7 +172,7 @@ export default function CartDrawer() {
</div> </div>
{items.length > 0 && ( {items.length > 0 && (
<div className="p-6 border-t border-[#EDE6D9] bg-white"> <div className="p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] border-t border-[#EDE6D9] bg-white">
<button <button
onClick={clearCart} onClick={clearCart}
className="text-xs text-[#8A8478] hover:text-[#B38B4D] mb-3 underline" className="text-xs text-[#8A8478] hover:text-[#B38B4D] mb-3 underline"
+18 -11
View File
@@ -6,9 +6,11 @@ import {
CATERING_CATEGORY_ORDER, CATERING_CATEGORY_ORDER,
groupDishesByCategory, groupDishesByCategory,
} from '@/lib/catering-data'; } from '@/lib/catering-data';
import { buildCateringPackagePricingMessage } from '@/application/messaging/whatsapp-message-builder';
import { container } from '@/infrastructure/di/container';
import { useLanguage } from '@/lib/language-context'; import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations'; import { getTranslation } from '@/lib/translations';
import { Users, UtensilsCrossed } from 'lucide-react'; import { MessageCircle, Users, UtensilsCrossed } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
interface CateringPackageCardProps { interface CateringPackageCardProps {
@@ -26,6 +28,13 @@ export default function CateringPackageCard({ pkg, index = 0 }: CateringPackageC
const packageDescription = const packageDescription =
(t.catering.packageDescriptions as Record<string, string>)?.[pkg.id] ?? pkg.description; (t.catering.packageDescriptions as Record<string, string>)?.[pkg.id] ?? pkg.description;
const handlePricingInquiry = () => {
const template = (t.catering as { packagePricingInquiry?: string }).packagePricingInquiry;
if (!template) return;
const message = buildCateringPackagePricingMessage(packageName, template);
container.messagingGateway.openWhatsApp(message);
};
return ( return (
<motion.article <motion.article
initial={{ opacity: 0, y: 28 }} initial={{ opacity: 0, y: 28 }}
@@ -67,16 +76,14 @@ export default function CateringPackageCard({ pkg, index = 0 }: CateringPackageC
</div> </div>
</div> </div>
<div className="mb-5 rounded-2xl border border-[#c99a2e]/25 bg-[#fffdf8] px-4 py-3.5"> <button
<div className="flex items-baseline gap-2"> type="button"
<span className="text-2xl font-medium tabular-nums tracking-[-0.5px] text-[#B38B4D]"> onClick={handlePricingInquiry}
{pkg.pricePerHead} className="mb-5 flex w-full items-center justify-center gap-2 rounded-2xl border border-[#c99a2e]/35 bg-[#fffdf8] px-4 py-3.5 text-base font-semibold tracking-[-0.2px] text-[#B38B4D] transition-all hover:border-[#c99a2e] hover:bg-[#fff6dc] hover:text-[#8C6B3A] active:scale-[0.98]"
</span> >
<span className="text-sm font-semibold text-[#8a6a25]"> <MessageCircle className="h-4 w-4 shrink-0" aria-hidden />
{t.catering.perHead} · {pkg.currency} {t.catering.contactForPricing}
</span> </button>
</div>
</div>
<div className="mb-2 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.18em] text-[#8a6a25]"> <div className="mb-2 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.18em] text-[#8a6a25]">
<UtensilsCrossed className="h-3.5 w-3.5 text-[#c99a2e]" /> <UtensilsCrossed className="h-3.5 w-3.5 text-[#c99a2e]" />
+4 -2
View File
@@ -13,6 +13,7 @@ import {
interface DishDetailsModalProps { interface DishDetailsModalProps {
item: MenuItem; item: MenuItem;
displayName: string;
description: string; description: string;
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
@@ -24,6 +25,7 @@ interface DishDetailsModalProps {
export default function DishDetailsModal({ export default function DishDetailsModal({
item, item,
displayName,
description, description,
isOpen, isOpen,
onClose, onClose,
@@ -83,7 +85,7 @@ export default function DishDetailsModal({
) : imageSrc ? ( ) : imageSrc ? (
<img <img
src={imageSrc} src={imageSrc}
alt={item.name} alt={displayName}
className="w-full h-[240px] sm:h-[280px] object-cover bg-[#F2EDE4]" className="w-full h-[240px] sm:h-[280px] object-cover bg-[#F2EDE4]"
onError={(e) => { onError={(e) => {
applyNextImageFallback( applyNextImageFallback(
@@ -114,7 +116,7 @@ export default function DishDetailsModal({
id="dish-detail-title" id="dish-detail-title"
className="font-serif text-2xl sm:text-[26px] tracking-[-0.4px] text-[#2C2A26]" className="font-serif text-2xl sm:text-[26px] tracking-[-0.4px] text-[#2C2A26]"
> >
{item.name} {displayName}
</h2> </h2>
{description && ( {description && (
+16 -12
View File
@@ -28,7 +28,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const { totalItems, openCart } = useCart(); const { totalItems, openCart } = useCart();
const { totalItems: wishlistCount, openWishlist } = useWishlist(); const { totalItems: wishlistCount, openWishlist } = useWishlist();
const { language } = useLanguage(); const { language, isRtl } = useLanguage();
const t = getTranslation(language); const t = getTranslation(language);
const pathname = usePathname(); const pathname = usePathname();
@@ -91,7 +91,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
</Link> </Link>
{/* Desktop Navigation - Premium Mesmerizing Design */} {/* Desktop Navigation - Premium Mesmerizing Design */}
<div className="hidden md:block flex-shrink-0"> <div className="hidden lg:block flex-shrink-0 min-w-0">
<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="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]"> <div className="relative flex items-center gap-1 text-sm font-medium text-[#101724]">
{navLinks.map((link, index) => { {navLinks.map((link, index) => {
@@ -127,7 +127,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
</div> </div>
{/* Desktop Actions — Stunning Mesmerizing Buttons */} {/* Desktop Actions — Stunning Mesmerizing Buttons */}
<div className="hidden md:flex items-center gap-3"> <div className="hidden lg:flex items-center gap-3 shrink-0">
{/* Wishlist Button */} {/* Wishlist Button */}
<button <button
onClick={openWishlist} onClick={openWishlist}
@@ -168,11 +168,11 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
</div> </div>
{/* Mobile Hamburger + Wishlist + Cart (compact, high touch target) */} {/* Mobile Hamburger + Wishlist + Cart (compact, high touch target) */}
<div className="md:hidden flex items-center gap-2"> <div className="lg:hidden flex items-center gap-1.5 shrink-0">
{/* MOBILE WISHLIST ICON */} {/* MOBILE WISHLIST ICON */}
<button <button
onClick={openWishlist} onClick={openWishlist}
className="relative flex items-center justify-center w-10 h-10 rounded-full hover:bg-[#F5F1E9] active:bg-[#EDE6D9] transition-colors" className="relative flex items-center justify-center w-11 h-11 rounded-full hover:bg-[#F5F1E9] active:bg-[#EDE6D9] transition-colors"
aria-label={t.wishlist} aria-label={t.wishlist}
> >
<Heart className={`h-5 w-5 ${wishlistCount > 0 ? 'text-[#c99a2e] fill-[#c99a2e]/30' : 'text-[#101724]'}`} /> <Heart className={`h-5 w-5 ${wishlistCount > 0 ? 'text-[#c99a2e] fill-[#c99a2e]/30' : 'text-[#101724]'}`} />
@@ -186,7 +186,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
{/* MOBILE CART ICON (always visible) */} {/* MOBILE CART ICON (always visible) */}
<button <button
onClick={openCart} onClick={openCart}
className="relative flex items-center justify-center w-10 h-10 rounded-full hover:bg-[#F5F1E9] active:bg-[#EDE6D9] transition-colors" className="relative flex items-center justify-center w-11 h-11 rounded-full hover:bg-[#F5F1E9] active:bg-[#EDE6D9] transition-colors"
aria-label="Open cart" aria-label="Open cart"
> >
<ShoppingBag className="h-5 w-5 text-[#101724]" /> <ShoppingBag className="h-5 w-5 text-[#101724]" />
@@ -199,7 +199,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="text-[#B38B4D] p-2 -mr-1 active:text-[#8C6B3A] transition-colors" className="flex items-center justify-center w-11 h-11 text-[#B38B4D] -me-1 active:text-[#8C6B3A] transition-colors"
aria-label="Toggle menu" aria-label="Toggle menu"
aria-expanded={isOpen} aria-expanded={isOpen}
> >
@@ -219,7 +219,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
{/* Modern Mobile Menu Drawer (slide-in, animated, high-contrast, touch-friendly) - high z to always appear in front of hero banner video etc. */} {/* Modern Mobile Menu Drawer (slide-in, animated, high-contrast, touch-friendly) - high z to always appear in front of hero banner video etc. */}
<AnimatePresence> <AnimatePresence>
{isOpen && ( {isOpen && (
<div className="md:hidden fixed inset-0 z-[9999]"> <div className="lg:hidden fixed inset-0 z-[9999]">
{/* Backdrop */} {/* Backdrop */}
<motion.div <motion.div
className="absolute inset-0 bg-[#101724]/70 backdrop-blur-md" className="absolute inset-0 bg-[#101724]/70 backdrop-blur-md"
@@ -232,10 +232,14 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
{/* Sliding Panel - modern, full-bleed on small phones, elegant on larger */} {/* Sliding Panel - modern, full-bleed on small phones, elegant on larger */}
<motion.div <motion.div
className="absolute right-0 top-0 bottom-0 w-[82%] max-w-[340px] bg-[#fbf7ef] shadow-2xl border-l border-[#c99a2e]/10 flex flex-col overflow-y-auto z-[10000]" className={`absolute top-0 bottom-0 w-[82%] max-w-[340px] bg-[#fbf7ef] shadow-2xl flex flex-col overflow-y-auto z-[10000] ${
initial={{ x: '100%' }} isRtl
? 'left-0 border-r border-[#c99a2e]/10'
: 'right-0 border-l border-[#c99a2e]/10'
}`}
initial={{ x: isRtl ? '-100%' : '100%' }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: '100%' }} exit={{ x: isRtl ? '-100%' : '100%' }}
transition={{ type: 'spring', stiffness: 320, damping: 32 }} transition={{ type: 'spring', stiffness: 320, damping: 32 }}
> >
{/* Header */} {/* Header */}
@@ -294,7 +298,7 @@ export default function Navbar({ variant = "default" }: NavbarProps) {
</div> </div>
{/* Secondary actions */} {/* Secondary actions */}
<div className="mt-auto px-5 pb-8 pt-4 border-t border-[#EDE6D9] bg-white/40 space-y-3"> <div className="mt-auto px-5 pt-4 pb-[max(2rem,env(safe-area-inset-bottom))] border-t border-[#EDE6D9] bg-white/40 space-y-3">
<Link <Link
href="/reserve" href="/reserve"
onClick={closeMenu} onClick={closeMenu}
+2 -2
View File
@@ -151,7 +151,7 @@ export default function OrderDisplay() {
{ id: 'T-01', source: 'table', label: 'Table 1', customer: 'Table 1', items: [{name: 'Shahi Paneer', qty: 1}, {name: 'Naan', qty: 3}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*8), etaMinutes: 7 }, { id: 'T-01', source: 'table', label: 'Table 1', customer: 'Table 1', items: [{name: 'Shahi Paneer', qty: 1}, {name: 'Naan', qty: 3}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*8), etaMinutes: 7 },
{ id: 'T-04', source: 'table', label: 'Table 4', customer: 'Table 4', items: [{name: 'Chicken Tikka', qty: 2}, {name: 'Rice', qty: 1}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*5), etaMinutes: 10 }, { id: 'T-04', source: 'table', label: 'Table 4', customer: 'Table 4', items: [{name: 'Chicken Tikka', qty: 2}, {name: 'Rice', qty: 1}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*5), etaMinutes: 10 },
{ id: 'T-06', source: 'table', label: 'Table 6', customer: 'Table 6', items: [{name: 'Lahore Sizzler', qty: 1}, {name: 'Naan', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*3), etaMinutes: 5 }, { id: 'T-06', source: 'table', label: 'Table 6', customer: 'Table 6', items: [{name: 'Lahore Sizzler', qty: 1}, {name: 'Naan', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*3), etaMinutes: 5 },
{ id: 'T-07', source: 'table', label: 'Table 7', customer: 'Table 7', items: [{name: 'Tikka Boti', qty: 1}, {name: 'Salad', qty: 1}], status: 'received', receivedAt: new Date(now.getTime() - 1000*60*1), etaMinutes: 12 }, { id: 'T-07', source: 'table', label: 'Table 7', customer: 'Table 7', items: [{name: 'Chicken Tikka', qty: 1}, {name: 'Salad', qty: 1}], status: 'received', receivedAt: new Date(now.getTime() - 1000*60*1), etaMinutes: 12 },
// Delivery Partners (online) - 8 orders (mix of platforms) // Delivery Partners (online) - 8 orders (mix of platforms)
{ id: 'FD-3921', source: 'foodora', label: 'Foodora #FD-3921', customer: 'Aisha K.', items: [{name: 'Lahore Pizza', qty: 1}, {name: 'Mango Lassi', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*9), etaMinutes: 3 }, { id: 'FD-3921', source: 'foodora', label: 'Foodora #FD-3921', customer: 'Aisha K.', items: [{name: 'Lahore Pizza', qty: 1}, {name: 'Mango Lassi', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*9), etaMinutes: 3 },
@@ -166,7 +166,7 @@ export default function OrderDisplay() {
// Pickup orders (whatsapp/phone) - 8 orders // Pickup orders (whatsapp/phone) - 8 orders
{ id: 'WA-4521', source: 'pickup', label: 'Pickup WA-4521', customer: 'Ahmed R.', items: [{name: 'Chicken Biryani', qty: 2}, {name: 'Raita', qty: 1}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*13), etaMinutes: 5 }, { id: 'WA-4521', source: 'pickup', label: 'Pickup WA-4521', customer: 'Ahmed R.', items: [{name: 'Chicken Biryani', qty: 2}, {name: 'Raita', qty: 1}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*13), etaMinutes: 5 },
{ id: 'PH-783', source: 'pickup', label: 'Pickup PH-783', customer: 'Sara M.', items: [{name: 'Veg Pizza', qty: 1}, {name: 'Garlic Naan', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*6), etaMinutes: 2 }, { id: 'PH-783', source: 'pickup', label: 'Pickup PH-783', customer: 'Sara M.', items: [{name: 'Veg Pizza', qty: 1}, {name: 'Garlic Naan', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*6), etaMinutes: 2 },
{ id: 'WA-4530', source: 'pickup', label: 'Pickup WA-4530', customer: 'Omar K.', items: [{name: 'Tikka Boti', qty: 1}], status: 'ready', receivedAt: new Date(now.getTime() - 1000*60*20), etaMinutes: 0 }, { id: 'WA-4530', source: 'pickup', label: 'Pickup WA-4530', customer: 'Omar K.', items: [{name: 'Chicken Tikka', qty: 1}], status: 'ready', receivedAt: new Date(now.getTime() - 1000*60*20), etaMinutes: 0 },
{ id: 'WA-4535', source: 'pickup', label: 'Pickup WA-4535', customer: 'Fatima L.', items: [{name: 'Shahi Paneer', qty: 1}, {name: 'Naan', qty: 1}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*8), etaMinutes: 7 }, { id: 'WA-4535', source: 'pickup', label: 'Pickup WA-4535', customer: 'Fatima L.', items: [{name: 'Shahi Paneer', qty: 1}, {name: 'Naan', qty: 1}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*8), etaMinutes: 7 },
{ id: 'PH-791', source: 'pickup', label: 'Pickup PH-791', customer: 'Hassan Q.', items: [{name: 'Daal Makhani', qty: 1}, {name: 'Rice', qty: 2}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*11), etaMinutes: 8 }, { id: 'PH-791', source: 'pickup', label: 'Pickup PH-791', customer: 'Hassan Q.', items: [{name: 'Daal Makhani', qty: 1}, {name: 'Rice', qty: 2}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*11), etaMinutes: 8 },
{ id: 'WA-4542', source: 'pickup', label: 'Pickup WA-4542', customer: 'Ayesha S.', items: [{name: 'Samosa Keema', qty: 3}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*4), etaMinutes: 3 }, { id: 'WA-4542', source: 'pickup', label: 'Pickup WA-4542', customer: 'Ayesha S.', items: [{name: 'Samosa Keema', qty: 3}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*4), etaMinutes: 3 },
+27 -9
View File
@@ -12,6 +12,11 @@ import {
import { menuCategories, allMenuItems, type MenuItem, type MenuCategory } from '@/lib/menu-data'; import { menuCategories, allMenuItems, type MenuItem, type MenuCategory } from '@/lib/menu-data';
import { useLanguage } from '@/lib/language-context'; import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations'; import { getTranslation } from '@/lib/translations';
import {
getMenuItemDescription,
getMenuItemName,
} from '@/application/i18n/menu-localization';
import type { Language } from '@/domain/language/entities';
interface ScreenDisplayProps { interface ScreenDisplayProps {
screen: 1 | 2; screen: 1 | 2;
@@ -102,9 +107,20 @@ function SpiceParticles() {
); );
} }
function MenuCard({ item, isHighlighted, t }: { item: MenuItem; isHighlighted?: boolean; t: any }) { function MenuCard({
item,
isHighlighted,
language,
t,
}: {
item: MenuItem;
isHighlighted?: boolean;
language: Language;
t: ReturnType<typeof getTranslation>;
}) {
const poster = getMenuPosterSrc(item, 'optimized'); const poster = getMenuPosterSrc(item, 'optimized');
const desc = (t as any).menuDescriptions?.[item.id] || item.description || ''; const displayName = getMenuItemName(language, item);
const desc = getMenuItemDescription(language, t, item);
return ( return (
<motion.div <motion.div
@@ -119,7 +135,7 @@ function MenuCard({ item, isHighlighted, t }: { item: MenuItem; isHighlighted?:
<div className="relative h-36 bg-[#F2EDE4] overflow-hidden"> <div className="relative h-36 bg-[#F2EDE4] overflow-hidden">
<motion.img <motion.img
src={poster} src={poster}
alt={item.name} alt={displayName}
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
animate={{ scale: [1, 1.018, 1] }} animate={{ scale: [1, 1.018, 1] }}
transition={{ duration: 8.5, repeat: Infinity, ease: 'easeInOut' }} transition={{ duration: 8.5, repeat: Infinity, ease: 'easeInOut' }}
@@ -147,7 +163,7 @@ function MenuCard({ item, isHighlighted, t }: { item: MenuItem; isHighlighted?:
<div className="p-3.5 flex-1 flex flex-col"> <div className="p-3.5 flex-1 flex flex-col">
<div className="font-medium tracking-[-0.3px] text-[15px] leading-tight text-[#2C2A26] mb-1 line-clamp-1 group-hover:text-[#B38B4D] transition-colors"> <div className="font-medium tracking-[-0.3px] text-[15px] leading-tight text-[#2C2A26] mb-1 line-clamp-1 group-hover:text-[#B38B4D] transition-colors">
{item.name} {displayName}
</div> </div>
<p className="text-[#6B665F] text-[12px] leading-snug line-clamp-2 flex-1"> <p className="text-[#6B665F] text-[12px] leading-snug line-clamp-2 flex-1">
{desc} {desc}
@@ -257,7 +273,8 @@ export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
}; };
}, []); }, []);
const translatedDesc = current ? ((t as any).menuDescriptions?.[current.id] || current.description) : ''; const currentDisplayName = current ? getMenuItemName(language, current) : '';
const translatedDesc = current ? getMenuItemDescription(language, t, current) : '';
// ===================================================== // =====================================================
// CONTINUOUS MENU REEL (one line, loop like a film reel) - items move LEFT // CONTINUOUS MENU REEL (one line, loop like a film reel) - items move LEFT
@@ -282,6 +299,7 @@ export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
// ReelCard: large poster + number+name on left, price on right (opposite side, same line) so price is always clearly visible as cards move left in the reel. Sized big for 43" TV distance viewing. // ReelCard: large poster + number+name on left, price on right (opposite side, same line) so price is always clearly visible as cards move left in the reel. Sized big for 43" TV distance viewing.
function ReelCard({ item, number }: { item: MenuItem; number: number }) { function ReelCard({ item, number }: { item: MenuItem; number: number }) {
const numStr = String(number).padStart(2, '0'); const numStr = String(number).padStart(2, '0');
const reelName = getMenuItemName(language, item);
return ( return (
<div <div
@@ -291,7 +309,7 @@ export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
<div className="relative bg-[#F2EDE4]" style={{ height: '320px' }}> <div className="relative bg-[#F2EDE4]" style={{ height: '320px' }}>
<motion.img <motion.img
src={getMenuPosterSrc(item, 'optimized')} src={getMenuPosterSrc(item, 'optimized')}
alt={item.name} alt={reelName}
className="w-full h-full object-cover" className="w-full h-full object-cover"
animate={{ scale: [1, 1.015, 1] }} animate={{ scale: [1, 1.015, 1] }}
transition={{ duration: 6, repeat: Infinity, ease: 'easeInOut' }} transition={{ duration: 6, repeat: Infinity, ease: 'easeInOut' }}
@@ -304,7 +322,7 @@ export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
</div> </div>
<div className="px-4 py-5 flex items-baseline justify-between gap-4"> <div className="px-4 py-5 flex items-baseline justify-between gap-4">
<div className="font-serif text-3xl tracking-[-0.5px] leading-none text-[#2C2A26]"> <div className="font-serif text-3xl tracking-[-0.5px] leading-none text-[#2C2A26]">
{numStr}. {item.name} {numStr}. {reelName}
</div> </div>
<div className="text-[#B38B4D] text-2xl font-semibold tracking-tight whitespace-nowrap"> <div className="text-[#B38B4D] text-2xl font-semibold tracking-tight whitespace-nowrap">
{item.price} kr {item.price} kr
@@ -381,7 +399,7 @@ export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
<motion.img <motion.img
key={`${current.id}-poster`} key={`${current.id}-poster`}
src={getMenuPosterSrc(current, 'optimized')} src={getMenuPosterSrc(current, 'optimized')}
alt={current.name} alt={currentDisplayName}
className="absolute inset-0 w-full h-full object-cover" className="absolute inset-0 w-full h-full object-cover"
initial={{ opacity: 0, scale: 1 }} initial={{ opacity: 0, scale: 1 }}
animate={{ opacity: 1, scale: [1, 1.06, 1] }} animate={{ opacity: 1, scale: [1, 1.06, 1] }}
@@ -420,7 +438,7 @@ export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
</div> </div>
<h1 className="font-serif text-[56px] md:text-[64px] leading-[0.88] tracking-[-3.2px] mb-2 text-[#B38B4D] drop-shadow-[0_0_10px_rgba(179,139,77,0.85)]"> <h1 className="font-serif text-[56px] md:text-[64px] leading-[0.88] tracking-[-3.2px] mb-2 text-[#B38B4D] drop-shadow-[0_0_10px_rgba(179,139,77,0.85)]">
{current.name} {currentDisplayName}
</h1> </h1>
<div className="flex items-baseline gap-2 mb-4"> <div className="flex items-baseline gap-2 mb-4">
+20 -7
View File
@@ -8,6 +8,7 @@ import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations'; import { getTranslation } from '@/presentation/i18n/translations';
import { getMenuPosterSrc, applyNextImageFallback, getMenuPosterCandidates } from '@/lib/assets'; import { getMenuPosterSrc, applyNextImageFallback, getMenuPosterCandidates } from '@/lib/assets';
import { getMenuItemById } from '@/lib/menu-data'; import { getMenuItemById } from '@/lib/menu-data';
import { getMenuItemName } from '@/application/i18n/menu-localization';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder'; import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
export default function WishlistDrawer() { export default function WishlistDrawer() {
@@ -20,7 +21,7 @@ export default function WishlistDrawer() {
} = useWishlist(); } = useWishlist();
const { addToCart } = useCart(); const { addToCart } = useCart();
const { language } = useLanguage(); const { language, isRtl } = useLanguage();
const t = getTranslation(language); const t = getTranslation(language);
if (!isOpen) return null; if (!isOpen) return null;
@@ -43,7 +44,11 @@ export default function WishlistDrawer() {
onClick={closeWishlist} onClick={closeWishlist}
/> />
<div className="fixed top-0 right-0 h-full w-full max-w-md bg-[#F8F5F0] z-[985] shadow-2xl flex flex-col"> <div
className={`fixed top-0 h-full w-full max-w-md bg-[#F8F5F0] z-[985] shadow-2xl flex flex-col ${
isRtl ? 'left-0 border-r border-[#EDE6D9]' : 'right-0 border-l border-[#EDE6D9]'
}`}
>
<div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]"> <div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Heart className="h-5 w-5 text-[#c99a2e] fill-[#c99a2e]/20" /> <Heart className="h-5 w-5 text-[#c99a2e] fill-[#c99a2e]/20" />
@@ -88,7 +93,14 @@ export default function WishlistDrawer() {
</div> </div>
) : ( ) : (
<div className="space-y-5"> <div className="space-y-5">
{items.map((item) => ( {items.map((item) => {
const menuItem = getMenuItemById(item.id);
const displayName = getMenuItemName(language, {
id: item.id,
name: menuItem?.name ?? item.name,
});
return (
<div <div
key={item.id} key={item.id}
className="flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-4" className="flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-4"
@@ -96,7 +108,7 @@ export default function WishlistDrawer() {
<div className="h-20 w-20 shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]"> <div className="h-20 w-20 shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]">
<img <img
src={getMenuPosterSrc(item)} src={getMenuPosterSrc(item)}
alt={item.name} alt={displayName}
className="h-full w-full object-cover" className="h-full w-full object-cover"
loading="lazy" loading="lazy"
onError={(e) => { onError={(e) => {
@@ -112,7 +124,7 @@ export default function WishlistDrawer() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<h4 className="font-medium tracking-[-0.3px] text-[#101724] truncate"> <h4 className="font-medium tracking-[-0.3px] text-[#101724] truncate">
{item.name} {displayName}
</h4> </h4>
<span className="shrink-0 text-right font-medium text-[#B38B4D] tabular-nums text-sm leading-tight"> <span className="shrink-0 text-right font-medium text-[#B38B4D] tabular-nums text-sm leading-tight">
{(() => { {(() => {
@@ -147,13 +159,14 @@ export default function WishlistDrawer() {
</div> </div>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
)} )}
</div> </div>
{items.length > 0 && ( {items.length > 0 && (
<div className="p-6 border-t border-[#EDE6D9] bg-white"> <div className="p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] border-t border-[#EDE6D9] bg-white">
<p className="text-xs text-center text-[#6B665F] mb-4"> <p className="text-xs text-center text-[#6B665F] mb-4">
{t.wishlistDrawer.hint} {t.wishlistDrawer.hint}
</p> </p>
-1
View File
@@ -130,7 +130,6 @@ export const menuCategories: MenuCategory[] = [
items: [ 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-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: "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: "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: "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: "butter-chicken", name: "Butter Chicken", description: "Tender chicken in a creamy tomato and butter gravy with mild spices.", price: 149, image: "butter-chicken.jpg" },
+4 -2
View File
@@ -69,7 +69,7 @@ export const arTranslations = {
filterGrill: 'مشويات', filterGrill: 'مشويات',
filterSweets: 'حلويات', filterSweets: 'حلويات',
filterMeat: 'لحوم', filterMeat: 'لحوم',
filterStreet: 'شارع', filterStreet: 'طعام الشارع',
filterRoll: 'رول', filterRoll: 'رول',
addToTable: 'أضف إلى الطاولة', addToTable: 'أضف إلى الطاولة',
viewFullMenu: 'اعرض القائمة الكاملة — أكثر من 40 طبقاً', viewFullMenu: 'اعرض القائمة الكاملة — أكثر من 40 طبقاً',
@@ -109,7 +109,6 @@ export const arTranslations = {
'shami-sandwich': 'أقراص شامي كباب من لحم مفروم متبل تُقدَّم في خبز مع صلصة وبصل.', 'shami-sandwich': 'أقراص شامي كباب من لحم مفروم متبل تُقدَّم في خبز مع صلصة وبصل.',
'chicken-biryani': 'أرز بسمتي معتق عطري مع دجاج متبل طري وطبقات من الزعفران والبصل المكرمل.', 'chicken-biryani': 'أرز بسمتي معتق عطري مع دجاج متبل طري وطبقات من الزعفران والبصل المكرمل.',
'chicken-tikka': 'قطع دجاج بدون عظم متبلة بالزبادي والبهارات، مشوية في التندور.', 'chicken-tikka': 'قطع دجاج بدون عظم متبلة بالزبادي والبهارات، مشوية في التندور.',
'tikka-boti': 'قطع دجاج طرية متبلة بالبهارات ومشوية على أسياخ.',
'chicken-karahi': 'دجاج مقلي في الووك بمرق قوي من الطماطم والفلفل والزنجبيل.', 'chicken-karahi': 'دجاج مقلي في الووك بمرق قوي من الطماطم والفلفل والزنجبيل.',
'lahore-sizzler': 'طبق ساخن من دجاج متبل مع خضروات وصلصات حارة.', 'lahore-sizzler': 'طبق ساخن من دجاج متبل مع خضروات وصلصات حارة.',
'butter-chicken': 'دجاج طري في مرق كريمي من الطماطم والزبدة ببهارات خفيفة.', 'butter-chicken': 'دجاج طري في مرق كريمي من الطماطم والزبدة ببهارات خفيفة.',
@@ -179,6 +178,9 @@ export const arTranslations = {
subtitle: subtitle:
'باقات فاخرة لكل ضيف للأعراس والفعاليات الشركات والتجمعات العائلية — نكهات شاهي أصيلة بتقديم راقٍ.', 'باقات فاخرة لكل ضيف للأعراس والفعاليات الشركات والتجمعات العائلية — نكهات شاهي أصيلة بتقديم راقٍ.',
perHead: 'لكل ضيف', perHead: 'لكل ضيف',
contactForPricing: 'تواصل معنا للاستفسار عن الأسعار',
packagePricingInquiry:
'مرحباً Shahi Kitchen 👋\n\nأود معرفة أسعار باقة التموين {{package}}.\n\nيرجى التواصل معي بخصوص التوفر والخيارات.\n\nشكراً لك!',
includedDishes: 'الأطباق المشمولة', includedDishes: 'الأطباق المشمولة',
mostPopular: 'الأكثر شعبية', mostPopular: 'الأكثر شعبية',
guestNote: 'قوائم مخصصة متاحة — تواصل معنا لتخصيص مناسبتك.', guestNote: 'قوائم مخصصة متاحة — تواصل معنا لتخصيص مناسبتك.',
+4 -2
View File
@@ -69,7 +69,7 @@ export const trTranslations = {
filterGrill: 'Izgara', filterGrill: 'Izgara',
filterSweets: 'Tatlılar', filterSweets: 'Tatlılar',
filterMeat: 'Et', filterMeat: 'Et',
filterStreet: 'Sokak', filterStreet: 'Sokak Yemeği',
filterRoll: 'Dürüm', filterRoll: 'Dürüm',
addToTable: 'Masaya Ekle', addToTable: 'Masaya Ekle',
viewFullMenu: 'TAM MENÜYÜ GÖRÜN — 40+ YEMEK', viewFullMenu: 'TAM MENÜYÜ GÖRÜN — 40+ YEMEK',
@@ -109,7 +109,6 @@ export const trTranslations = {
'shami-sandwich': 'Sos ve soğanla ekmek arasında servis edilen baharatlı kıymalı şami kebab köfteleri.', 'shami-sandwich': 'Sos ve soğanla ekmek arasında servis edilen baharatlı kıymalı şami kebab köfteleri.',
'chicken-biryani': 'Yumuşak baharatlı tavuk, safran ve karamelize soğan katmanlarıyla aromatik yaşlandırılmış basmati pilav.', 'chicken-biryani': 'Yumuşak baharatlı tavuk, safran ve karamelize soğan katmanlarıyla aromatik yaşlandırılmış basmati pilav.',
'chicken-tikka': 'Yoğurt ve baharatlarda marine edilmiş kemiksiz tavuk parçaları, tandırda ızgara yapılır.', 'chicken-tikka': 'Yoğurt ve baharatlarda marine edilmiş kemiksiz tavuk parçaları, tandırda ızgara yapılır.',
'tikka-boti': 'Baharatlarda marine edilmiş yumuşak tavuk parçaları, şişte ızgara yapılır.',
'chicken-karahi': 'Wokta kavrulmuş tavuk, domates, biber ve zencefil sosunda.', 'chicken-karahi': 'Wokta kavrulmuş tavuk, domates, biber ve zencefil sosunda.',
'lahore-sizzler': 'Sebzeler ve baharatlı soslarla marine edilmiş tavuk, ıslık çalan tabakta servis edilir.', 'lahore-sizzler': 'Sebzeler ve baharatlı soslarla marine edilmiş tavuk, ıslık çalan tabakta servis edilir.',
'butter-chicken': 'Hafif baharatlı kremalı domates ve tereyağı sosunda yumuşak tavuk.', 'butter-chicken': 'Hafif baharatlı kremalı domates ve tereyağı sosunda yumuşak tavuk.',
@@ -179,6 +178,9 @@ export const trTranslations = {
subtitle: subtitle:
'Düğünler, kurumsal etkinlikler ve aile toplantıları için kişi başı premium paketler — otantik Shahi lezzetleri.', 'Düğünler, kurumsal etkinlikler ve aile toplantıları için kişi başı premium paketler — otantik Shahi lezzetleri.',
perHead: 'kişi başı', perHead: 'kişi başı',
contactForPricing: 'Fiyat için bizimle iletişime geçin',
packagePricingInquiry:
'Merhaba Shahi Kitchen 👋\n\n{{package}} catering paketi için fiyat almak istiyorum.\n\nLütfen müsaitlik ve seçenekler için benimle iletişime geçin.\n\nTeşekkürler!',
includedDishes: 'Dahil yemekler', includedDishes: 'Dahil yemekler',
mostPopular: 'En Popüler', mostPopular: 'En Popüler',
guestNote: 'Özel menüler mevcut — etkinliğiniz için bizimle iletişime geçin.', guestNote: 'Özel menüler mevcut — etkinliğiniz için bizimle iletişime geçin.',
+76
View File
@@ -0,0 +1,76 @@
import type { Language } from '@/domain/language/entities';
const EN: Record<string, string> = {
"street-food": "Street Food & Starters",
"vegetarian": "Vegetarian",
"meat": "Meat",
"burger-sandwich": "Burger & Sandwich",
"chicken": "Chicken",
"pizza": "Pizza",
"naan-roll": "Naan Roll",
"sweets": "Sweets / Mithai",
"drinks": "Drinks"
};
const LOCALIZED: Partial<Record<Language, Record<string, string>>> = {
"sv": {
"street-food": "Gatumat & Förrätter",
"vegetarian": "Vegetariskt",
"meat": "Kött",
"burger-sandwich": "Burgare & Smörgås",
"chicken": "Kyckling",
"pizza": "Pizza",
"naan-roll": "Naanrulle",
"sweets": "Sötsaker / Mithai",
"drinks": "Drycker"
},
"ar": {
"street-food": "طعام الشارع والمقبلات",
"vegetarian": "نباتي",
"meat": "لحوم",
"burger-sandwich": "برغر وساندويتش",
"chicken": "دجاج",
"pizza": "بيتزا",
"naan-roll": "رول نان",
"sweets": "حلويات / ميثاي",
"drinks": "مشروبات"
},
"tr": {
"street-food": "Sokak Yemeği ve Başlangıçlar",
"vegetarian": "Vejetaryen",
"meat": "Et",
"burger-sandwich": "Burger ve Sandviç",
"chicken": "Tavuk",
"pizza": "Pizza",
"naan-roll": "Naan Dürüm",
"sweets": "Tatlılar / Mithai",
"drinks": "İçecekler"
},
"hi": {
"street-food": "स्ट्रीट फूड और स्टार्टर्स",
"vegetarian": "शाकाहारी",
"meat": "मांस",
"burger-sandwich": "बर्गर और सैंडविच",
"chicken": "चिकन",
"pizza": "पिज़्ज़ा",
"naan-roll": "नान रोल",
"sweets": "मिठाइयाँ / मिठाई",
"drinks": "पेय"
},
"ur": {
"street-food": "اسٹریٹ فوڈ اور سٹارٹرز",
"vegetarian": "سبزی خور",
"meat": "گوشت",
"burger-sandwich": "برگر اور سینڈوچ",
"chicken": "چکن",
"pizza": "پیزا",
"naan-roll": "نان رول",
"sweets": "مٹھائیاں / مٹھائی",
"drinks": "مشروبات"
}
};
export function getCategoryName(lang: Language, categoryId: string, fallback: string): string {
if (lang === 'en') return EN[categoryId] ?? fallback;
return LOCALIZED[lang]?.[categoryId] ?? EN[categoryId] ?? fallback;
}
@@ -0,0 +1,207 @@
import type { Language } from '@/domain/language/entities';
export const HINDI_MENU_DESCRIPTIONS: Record<string, string> = {
"samosa-aloo": "मसालेदार आलू और मटर से भरी कुरकुरी तली हुई त्रिकोणीय पेस्ट्री।",
"samosa-keema": "मसालेदार कीमा भरावन वाली परतदार पेस्ट्री।",
"samosa-chat": "मसालेदार चने, दही, चटनी और ताज़ी जड़ी-बूटियों से सजी कुरकुरी समोसा।",
"chana-chat": "आलू, प्याज, टमाटर और चटनियों के साथ मिलाए गए खट्टे मसालेदार चने।",
"panipuri": "मसालेदार चने और आलू से भरे कुरकुरे खोखले पूरी, इमली के खट्टे पानी के साथ।",
"keema-naan-starter": "मसालेदार कीमा से भरा नरम नान, सुनहरा होने तक बेक किया गया।",
"palak-paneer": "हल्के मसालों और सुगंधित जड़ी-बूटियों के साथ क्रीमी पालक ग्रेवी में पका पनीर।",
"shahi-paneer": "भारतीय मसालों के साथ समृद्ध क्रीमी काजू-टमाटर ग्रेवी में नरम पनीर।",
"malai-kofta": "हल्के मसालों वाली समृद्ध क्रीमी प्याज-टमाटर ग्रेवी में नरम सब्ज़ी कोफ्ते।",
"daal-makhani": "सुगंधित मसालों के साथ मक्खनयुक्त क्रीमी टमाटर ग्रेवी में धीमी आंच पर पकी काली दाल।",
"lahore-chana": "पारंपरिक पंजाबी मसालों के साथ तीखी प्याज-टमाटर ग्रेवी में पके मसालेदार चने।",
"lamm-palak": "ताज़ी पालक के साथ हल्की, स्वादिष्ट ग्रेवी में पका कोमल लैम।",
"lamm-vindaloo": "सिरके और मिर्च आधारित सॉस में मसालेदार खट्टी लैम करी।",
"lamm-rogan-josh": "समृद्ध दही और कश्मीरी मसाला ग्रेवी में पकी सुगंधित लैम करी।",
"lamm-karahi": "टमाटर, अदरक, लहसुन और मसालों के साथ वोक में तले लैम के टुकड़े।",
"bong-nihari": "समृद्ध सुगंधित ग्रेवी में धीमी आंच पर पका बीफ शैंक, परंपरागत रूप से नान के साथ।",
"paye": "गाढ़ी, मसालेदार स्वादिष्ट ग्रेवी में धीमी आंच पर पके लैम के पैर।",
"shahi-burger": "विशेष सॉस, लेट्यूस और टमाटर के साथ नरम बन में रसीला मसालेदार मीट पैटी।",
"shami-sandwich": "चटनी और प्याज के साथ ब्रेड में परोसे गए मसालेदार कीमा शामी कबाब पैटी।",
"chicken-biryani": "नरम मसालेदार चिकन, केसर और कैरेमलाइज़्ड प्याज की परतों वाला सुगंधित बासमती चावल।",
"chicken-tikka": "दही और मसालों में मैरीनेट किया हड्डी रहित चिकन, तंदूर में ग्रिल किया।",
"chicken-karahi": "टमाटर, मिर्च और अदरक की मजबूत ग्रेवी में वोक में तला चिकन।",
"lahore-sizzler": "सब्ज़ियों और मसालेदार सॉस के साथ मैरीनेट चिकन की सिज़लिंग प्लेट।",
"butter-chicken": "हल्के मसालों के साथ क्रीमी टमाटर-मक्खन ग्रेवी में नरम चिकन।",
"chicken-haleem": "दाल, गेहूं और सुगंधित मसालों के साथ धीमी आंच पर पका कटा हुआ चिकन।",
"lahore-pizza": "कुरकुरे बेस पर मसालेदार चिकन, प्याज और विशेष लाहौरी सॉस वाली पिज़्ज़ा।",
"kebab-pizza": "कीमा कबाब टॉपिंग, पनीर, प्याज और सुगंधित मसालों वाली पिज़्ज़ा।",
"tikka-boti-pizza": "ग्रिल चिकन टिक्का, पनीर, टमाटर और ताज़ी जड़ी-बूटियों वाली पिज़्ज़ा।",
"peshawari-pizza": "कोमल मांस, मेवे, किशमिश और पेशावरी मसालों वाली नान-स्टाइल पिज़्ज़ा।",
"veg-pizza": "ताज़ी सब्ज़ियों, पनीर और टमाटर सॉस से भरी शाकाहारी पिज़्ज़ा।",
"tikka-boti-roll": "पुदीने की चटनी और प्याज के साथ नरम नान में लिपटा ग्रिल चिकन टिक्का।",
"kebab-roll": "चटनी, सलाद और सॉस के साथ नान में लिपटा मसालेदार कीमा कबाब।",
"falafel-roll": "सब्ज़ियों, हुम्मस और खट्टी सॉस के साथ नान में लिपटा कुरकुरा फलाफेल।",
"paneer-roll": "मसालों, चटनी और ताज़ी सब्ज़ियों के साथ नान में लिपटे ग्रिल पनीर के क्यूब।",
"namakpare": "अजवाइन और नमक से सीज़न किए कुरकुरे, नमकीन तले हुए आटे के स्नैक्स।",
"jalebi": "सुगंधित चीनी की चाशनी में भीगे कुरकुरे सुनहरे केसर के सर्पिल।",
"gajar-halwa": "दूध, चीनी, घी और मेवों के साथ धीमी आंच पर पकाई मीठी गाजर की हलवा।",
"rasmalai": "इलायची और केसर वाले ठंडे मीठे दूध में भीगे नरम पनीर के गोले।",
"kulfi": "इलायची, पिस्ता और केसर वाली क्रीमी पारंपरिक जमी हुई दूध की मिठाई।",
"masala-chai": "दूध, इलायची, अदरक और सुगंधित मसालों से बनी पारंपरिक मसाला चाय।",
"mango-lassi": "पके आम और इलायची के साथ मिलाई ताज़ा मीठी दही की ड्रिंक।",
"coca-cola": "क्लासिक ठंडी कोला सॉफ्ट ड्रिंक।",
"pepsi-fanta": "ताज़ा कोला या संतरे के स्वाद वाला कार्बोनेटेड पेय।",
"sprite-ramlosa": "कुरकुरी नींबू-नीबू सोडा या स्पार्कलिंग मिनरल वॉटर।",
"energy-drink": "तुरंत ऊर्जा के लिए कैफीन युक्त एनर्जी ड्रिंक।",
"juice": "ताज़ा फलों का रस, आमतौर पर आम या अन्य मौसमी स्वाद।",
"coffee": "ताज़ा बनी गर्म कॉफी।",
"latte": "भाप वाले दूध और हल्के फोम के साथ एस्प्रेसो कॉफी।",
"cappuccino": "भाप वाले दूध और गाढ़े फोम से ढका एस्प्रेसो।",
"tea": "पारंपरिक गर्म काली चाय, सादी या दूध के साथ।",
};
export const MENU_DESCRIPTION_SUPPLEMENT: Partial<Record<Language, Record<string, string>>> = {
en: {
"shakar-paray": "Sweet crispy flour bites coated in sugar syrup — a classic mithai snack.",
"gajar-barfi": "Rich carrot fudge made with milk, ghee and nuts — dense and aromatic.",
"habshi-halwa": "Dark, caramelised semolina halwa slow-cooked with ghee, milk and nuts.",
"gulab-jaman": "Soft milk-solid dumplings soaked in rose-cardamom sugar syrup.",
"lambay-gulab-jaman": "Elongated gulab jamun with extra syrup — a Shahi Sweets favourite.",
"cream-gulab-jaman": "Gulab jamun filled with creamy centre, finished in fragrant syrup.",
"ras-gulay": "Spongy cottage-cheese balls in light sugar syrup — chilled and refreshing.",
"cham-cham": "Oval Bengali sweet coated in coconut or pistachio — soft and milky.",
"paira": "Traditional milk fudge sweet with a smooth, grainy texture.",
"laddu": "Round gram-flour and ghee sweet balls — festive and aromatic.",
"qalakand": "Grainy milk cake sweet flavoured with cardamom — fresh and delicate.",
"patisa": "Flaky, layered soan papdi-style sweet that crumbles and melts on the tongue.",
"baisan-patisa": "Gram-flour patisa with crisp layers and a nutty, buttery finish.",
"plain-barfi": "Classic milk barfi set with sugar and cardamom — simple and elegant.",
"badam-barfi": "Rich almond barfi with ground nuts and a smooth, luxurious bite.",
"pistachio-barfi": "Vibrant pistachio barfi — nutty, fragrant and beautifully green.",
"pink-barfi": "Festive pink milk barfi with a soft texture and delicate sweetness.",
"coconut-barfi": "Coconut-forward barfi with tropical aroma and a chewy-soft bite.",
"chocolate-barfi": "Milk barfi blended with cocoa — a modern twist on a classic sweet.",
"baisan-barfi": "Roasted gram-flour barfi with ghee and sugar — warm and nutty.",
"milk-cake-plain": "Caramelised milk cake with a dense, fudgy centre and golden crust.",
"milk-cake-khajoor": "Milk cake with dates — rich, chewy and naturally sweet.",
"milk-cake-akhrot": "Milk cake studded with walnuts for extra crunch and depth."
},
sv: {
"shakar-paray": "Söta knapriga mjölsnacks glaserade med sockersirap — ett klassiskt mithai-snack.",
"gajar-barfi": "Rik morotsfudge gjord med mjölk, ghee och nötter — tät och aromatisk.",
"habshi-halwa": "Mörk karamelliserad halva långsamt kokad med ghee, mjölk och nötter.",
"gulab-jaman": "Mjuka mjölksolidkulor indränkta i ros-kardemummsirap.",
"lambay-gulab-jaman": "Förlängda gulab jamun med extra sirap — en Shahi Sweets-favorit.",
"cream-gulab-jaman": "Gulab jamun fylld med krämig kärna, avslutad i doftande sirap.",
"ras-gulay": "Svampiga kesokulor i lätt sockersirap — kyld och uppfriskande.",
"cham-cham": "Oval bengalisk sötsak täckt med kokos eller pistasch — mjuk och mjölkig.",
"paira": "Traditionell mjölkfudgesöt med slät, kornig textur.",
"laddu": "Runda gramflour- och ghee-sötkulor — festliga och aromatiska.",
"qalakand": "Kornig mjölkkaka söt smaksatt med kardemumma — färsk och delikat.",
"patisa": "Flikigt, lagerformat soan papdi-sött som smular och smälter på tungan.",
"baisan-patisa": "Gramflour-patisa med krispiga lager och nötig, smörig finish.",
"plain-barfi": "Klassisk mjölkbarfi med socker och kardemumma — enkel och elegant.",
"badam-barfi": "Rik mandelbarfi med malda nötter och en slät, lyxig tugga.",
"pistachio-barfi": "Livfull pistaschbarfi — nötig, doftande och vackert grön.",
"pink-barfi": "Festlig rosa mjölkbarfi med mjuk textur och delikat sötma.",
"coconut-barfi": "Kokosbarfi med tropisk arom och en segmjuk tugga.",
"chocolate-barfi": "Mjölkbarfi blandad med kakao — en modern twist på en klassiker.",
"baisan-barfi": "Rostat gramflour-barfi med ghee och socker — varmt och nötigt.",
"milk-cake-plain": "Karamelliserad mjölkkaka med tät, fudgeaktig kärna och gyllene skorpa.",
"milk-cake-khajoor": "Mjölkkaka med dadlar — rik, seg och naturligt söt.",
"milk-cake-akhrot": "Mjölkkaka med valnötter för extra crunch och djup."
},
hi: {
"shakar-paray": "चीनी की चाशनी में लिपटे मीठे कुरकुरे आटे के बाइट्स — एक क्लासिक मिठाई स्नैक।",
"gajar-barfi": "दूध, घी और मेवों से बनी समृद्ध गाजर की बर्फी — घनी और सुगंधित।",
"habshi-halwa": "घी, दूध और मेवों के साथ धीमी आंच पर पकाई गई गहरी कैरेमलाइज़्ड सूजी हलवा।",
"gulab-jaman": "गुलाब-इलायची की चाशनी में भीगे नरम दूध के गोले।",
"lambay-gulab-jaman": "अतिरिक्त चाशनी वाले लंबे गुलाब जामुन — शाही स्वीट्स की पसंदीदा।",
"cream-gulab-jaman": "क्रीमी भरावन वाले गुलाब जामुन, सुगंधित चाशनी में तैयार।",
"ras-gulay": "हल्की चाशनी में स्पंजी पनीर के गोले — ठंडे और ताज़े।",
"cham-cham": "नारियल या पिस्ता से ढकी अंडाकार बंगाली मिठाई — नरम और दूधिया।",
"paira": "पारंपरिक दूध की मिठाई, चिकनी दानेदार बनावट के साथ।",
"laddu": "देसी घी और बेसन के गोल लड्डू — उत्सवी और सुगंधित।",
"qalakand": "इलायची से सुगंधित दानेदार दूध केक मिठाई — ताज़ी और नाज़ुक।",
"patisa": "पतली परतों वाली सोन पापड़ी जैसी मिठाई जो जीभ पर घुल जाती है।",
"baisan-patisa": "कुरकुरी परतों वाली बेसन पटिसा — नट्टी और मक्खन जैसी।",
"plain-barfi": "चीनी और इलायची वाली क्लासिक दूध की बर्फी।",
"badam-barfi": "पिसे बादाम वाली समृद्ध बर्फी — चिकनी और शाही।",
"pistachio-barfi": "खुशबूदार हरी पिस्ता बर्फी।",
"pink-barfi": "त्योहारी गुलाबी दूध बर्फी — नरम और नाज़ुक मिठास।",
"coconut-barfi": "नारियल की सुगंध वाली चबाने वाली बर्फी।",
"chocolate-barfi": "कोकोा मिली दूध बर्फी — क्लासिक का आधुनिक रूप।",
"baisan-barfi": "घी और चीनी वाली भुनी बेसन बर्फी।",
"milk-cake-plain": "सुनहरी परत वाला कैरेमलाइज़्ड दूध केक।",
"milk-cake-khajoor": "खजूर वाला दूध केक — समृद्ध और प्राकृतिक मीठा।",
"milk-cake-akhrot": "अखरोट वाला दूध केक — अतिरिक्त कुरकुरापन।"
},
ar: {
"shakar-paray": "لقمات دقيق مقرمشة حلوة مغطاة بشراب السكر — وجبة ميثاي كلاسيكية.",
"gajar-barfi": "فudge جزر غني مصنوع من الحليب والسمن والمكسرات — كثيف وعطري.",
"habshi-halwa": "حلوى سميد داكنة مطهوة ببطء مع السمن والحليب والمكسرات.",
"gulab-jaman": "كرات حليب طرية منقوعة في شراب الورد والهيل.",
"lambay-gulab-jaman": "جلاب جامون ممدود مع شراب إضافي — مفضل من شاهي سويتس.",
"cream-gulab-jaman": "جلاب جامون محشو بمركز كريمي في شراب عطري.",
"ras-gulay": "كرات جبن إسفنجية في شراب سكر خفيف — باردة ومنعشة.",
"cham-cham": "حلوى بنغالية بيضاوية مغطاة بجوز الهند أو الفستق — طرية وحليبية.",
"paira": "حلوى حليب تقليدية بملمس ناعم وحبيبي.",
"laddu": "كرات حلوة من الدقيق والسمن — احتفالية وعطرية.",
"qalakand": "حلوى كعكة حليب حبيبية بنكهة الهيل — طازجة ورقيقة.",
"patisa": "حلوى سون بابدي متقشرة تتفتت وتذوب على اللسان.",
"baisan-patisa": "باتيسا بيسان بطبقات مقرمشة ولمسة زبدية جوزية.",
"plain-barfi": "بارفي حليب كلاسيكي بالسكر والهيل — بسيط وأنيق.",
"badam-barfi": "بارفي لوز غني بمكسرات مطحونة ولمسة فاخرة.",
"pistachio-barfi": "بارفي فستق نابض بالحياة — جوزي وعطري وأخضر جميل.",
"pink-barfi": "بارفي حليب وردي احتفالي بنعومة وحلاوة رقيقة.",
"coconut-barfi": "بارفي جوز الهند بعبير استوائي وقوام مطاطي ناعم.",
"chocolate-barfi": "بارفي حليب ممزوج بالكاكاو — لمسة عصرية على كلاسيك.",
"baisan-barfi": "بارفي بيسان محمص بالسمن والسكر — دافئ وجوزي.",
"milk-cake-plain": "كعكة حليب كراميلية بمركز فدجي كثيف وقشرة ذهبية.",
"milk-cake-khajoor": "كعكة حليب بالتمر — غنية ومطاطية وحلوة طبيعياً.",
"milk-cake-akhrot": "كعكة حليب بالجوز لمزيد من القرمشة والعمق."
},
tr: {
"shakar-paray": "Şeker şurubuyla kaplı tatlı çıtır un lokmaları — klasik mithai atıştırmalığı.",
"gajar-barfi": "Süt, ghee ve kuruyemişlerle yapılmış zengin havuç fudge — yoğun ve aromatik.",
"habshi-halwa": "Ghee, süt ve kuruyemişlerle yavaş pişirilmiş koyu karamelize irmik helvası.",
"gulab-jaman": "Gül-kakule şurubuna batırılmış yumuşak sütlü toplar.",
"lambay-gulab-jaman": "Ekstra şuruplu uzun gulab jamun — Shahi Sweets favorisi.",
"cream-gulab-jaman": "Kremalı dolgulu gulab jamun, aromatik şurupta.",
"ras-gulay": "Hafif şeker şurubunda süngerimsi peynir topları — soğuk ve ferahlatıcı.",
"cham-cham": "Hindistan cevizi veya antep fıstığı kaplı oval Bengal tatlısı — yumuşak ve sütlü.",
"paira": "Pürüzsüz, taneli dokulu geleneksel sütlü fudge tatlısı.",
"laddu": "Yuvarlak gram unu ve ghee tatlı topları — şenlikli ve aromatik.",
"qalakand": "Kakule aromalı taneli sütlü kek tatlısı — taze ve narin.",
"patisa": "Dilde dağılan gevrek katmanlı soan papdi tarzı tatlı.",
"baisan-patisa": "Çıtır katmanlı besan patisa — fındıksı ve tereyağlı.",
"plain-barfi": "Şeker ve kakule ile klasik sütlü barfi — sade ve zarif.",
"badam-barfi": "Öğütülmüş bademli zengin barfi — pürüzsüz ve lüks.",
"pistachio-barfi": "Canlı antep fıstıklı barfi — fındıksı, aromatik ve yeşil.",
"pink-barfi": "Yumuşak dokulu ve narin tatlı pembe sütlü barfi.",
"coconut-barfi": "Tropikal aroma ve yumuşak çiğnenebilir hindistan cevizli barfi.",
"chocolate-barfi": "Kakao karışımlı sütlü barfi — klasik üzerine modern dokunuş.",
"baisan-barfi": "Ghee ve şekerle kavrulmuş besan barfi — sıcak ve fındıksı.",
"milk-cake-plain": "Yoğun fudji merkezli ve altın kabuklu karamelize sütlü kek.",
"milk-cake-khajoor": "Hurmalı sütlü kek — zengin, yapışkan ve doğal tatlı.",
"milk-cake-akhrot": "Ekstra çıtırlık için cevizli sütlü kek."
},
ur: {
"shakar-paray": "چینی کی چاشنی میں لپٹے میٹھے کرکرے آٹے کے بائٹس — ایک کلاسک مٹھائی سnack۔",
"gajar-barfi": "دودھ، گھی اور خشک میووں سے بنی امیر گاجر کی برفی — گھنی اور خوشبودار۔",
"habshi-halwa": "گھی، دودھ اور خشک میووں کے ساتھ دھیمی آنچ پر پکی گہری کیراملائزڈ سوجی حلوہ۔",
"gulab-jaman": "گلاب-الائچی کی چاشنی میں بھیگے نرم دودھ کے گولے۔",
"lambay-gulab-jaman": "اضافی چاشنی والے لمبے گلاب جامن — شاہی سویٹس کی پسندیدہ۔",
"cream-gulab-jaman": "کریمی بھرائی والے گلاب جامن، خوشبودار چاشنی میں تیار۔",
"ras-gulay": "ہلکی چاشنی میں اسفنجی پنیر کے گولے — ٹھنڈے اور تازہ۔",
"cham-cham": "ناریل یا پستہ سے ڈھکی بیضوی بنگالی مٹھائی — نرم اور دودھ جیسی۔",
"paira": "روایتی دودھ کی مٹھائی، ہموار دانے دار بناوٹ کے ساتھ۔",
"laddu": "دیسی گھی اور بیسن کے گول لڈو — تہواروں والی اور خوشبودار۔",
"qalakand": "الائچی سے خوشبودار دانے دار دودھ کیک مٹھائی — تازہ اور نازک۔",
"patisa": "پتلی پرتوں والی سون پاپڑی جیسی مٹھائی جو زبان پر گھل جاتی ہے۔",
"baisan-patisa": "کرکرے پرتوں والی بیسن پٹیسہ — نٹھی اور مکھن جیسی۔",
"plain-barfi": "چینی اور الائچی والی کلاسک دودھ کی برفی۔",
"badam-barfi": "پیسے بادام والی امیر برفی — ہموار اور شاہی۔",
"pistachio-barfi": "خوشبودار ہری پستہ برفی۔",
"pink-barfi": "تہواروں والی گلابی دودھ برفی — نرم اور نازک مٹھاس۔",
"coconut-barfi": "ناریل کی خوشبو والی چبانے والی برفی۔",
"chocolate-barfi": "کوکو ملی دودھ برفی — کلاسک کا جدید روپ۔",
"baisan-barfi": "گھی اور چینی والی بھنی بیسن برفی۔",
"milk-cake-plain": "سنہری پرت والا کیراملائزڈ دودھ کیک۔",
"milk-cake-khajoor": "کھجور والا دودھ کیک — امیر اور قدرتی میٹھا۔",
"milk-cake-akhrot": "اخروٹ والا دودھ کیک — اضافی کرکراہٹ۔"
},
};
@@ -0,0 +1 @@
export { HINDI_MENU_DESCRIPTIONS } from './description-supplement';
+466
View File
@@ -0,0 +1,466 @@
import type { Language } from '@/domain/language/entities';
const EN: Record<string, string> = {
"samosa-aloo": "Samosa Aloo Veg",
"samosa-keema": "Samosa Keema",
"samosa-chat": "Samosa Chat",
"chana-chat": "Chana Chat",
"panipuri": "Panipuri / Golgappe",
"keema-naan-starter": "Keema Naan",
"palak-paneer": "Palak Paneer",
"shahi-paneer": "Shahi Paneer",
"malai-kofta": "Malai Kofta",
"daal-makhani": "Daal Makhani",
"lahore-chana": "Lahore Chana",
"lamm-palak": "Lamm Palak",
"lamm-vindaloo": "Lamm Vindaloo",
"lamm-rogan-josh": "Lamm Rogan Josh",
"lamm-karahi": "Lamm Karahi",
"bong-nihari": "Bong Nihari",
"paye": "Paye",
"shahi-burger": "Shahi Burger",
"shami-sandwich": "Shami Sandwich Menu",
"chicken-biryani": "Chicken Biryani",
"chicken-tikka": "Chicken Tikka",
"chicken-karahi": "Chicken Karahi",
"lahore-sizzler": "Lahore Sizzler",
"butter-chicken": "Butter Chicken",
"chicken-haleem": "Chicken Haleem",
"lahore-pizza": "Lahore Pizza",
"kebab-pizza": "Kebab Pizza",
"tikka-boti-pizza": "Tikka Boti Pizza",
"peshawari-pizza": "Peshawari Pizza",
"veg-pizza": "Veg Pizza",
"tikka-boti-roll": "Tikka Boti Roll",
"kebab-roll": "Kebab Roll",
"falafel-roll": "Falafel Roll",
"paneer-roll": "Paneer Roll",
"namakpare": "Namak Paray",
"shakar-paray": "Shakar Paray",
"jalebi": "Jalebi",
"gajar-halwa": "Gajar Halwa",
"gajar-barfi": "Gajar Barfi",
"habshi-halwa": "Habshi Halwa",
"gulab-jaman": "Gulab Jamun",
"lambay-gulab-jaman": "Lambay Gulab Jamun",
"cream-gulab-jaman": "Cream Gulab Jamun",
"ras-gulay": "Ras Gulay",
"rasmalai": "Rasmalai",
"cham-cham": "Cham Cham",
"paira": "Paira",
"laddu": "Laddu",
"qalakand": "Kalakand",
"patisa": "Patisa",
"baisan-patisa": "Besan Patisa",
"plain-barfi": "Plain Barfi",
"badam-barfi": "Badam Barfi",
"pistachio-barfi": "Pistachio Barfi",
"pink-barfi": "Pink Barfi",
"coconut-barfi": "Coconut Barfi",
"chocolate-barfi": "Chocolate Barfi",
"baisan-barfi": "Besan Barfi",
"milk-cake-plain": "Milk Cake",
"milk-cake-khajoor": "Milk Cake Khajoor",
"milk-cake-akhrot": "Milk Cake Akhrot",
"kulfi": "Kulfi",
"masala-chai": "Masala Chai",
"mango-lassi": "Mango Lassi",
"coca-cola": "Coca-Cola",
"pepsi-fanta": "Pepsi / Fanta",
"sprite-ramlosa": "Sprite / Ramlösa",
"energy-drink": "Energy Drink",
"juice": "Juice",
"coffee": "Coffee",
"latte": "Latte",
"cappuccino": "Cappuccino",
"tea": "Tea"
};
const LOCALIZED: Partial<Record<Language, Record<string, string>>> = {
sv: {
"samosa-aloo": "Samosa Aloo (veg)",
"samosa-keema": "Samosa Keema",
"samosa-chat": "Samosa Chaat",
"chana-chat": "Chana Chaat",
"panipuri": "Panipuri / Golgappa",
"keema-naan-starter": "Keema Naan",
"palak-paneer": "Palak Paneer",
"shahi-paneer": "Shahi Paneer",
"malai-kofta": "Malai Kofta",
"daal-makhani": "Daal Makhani",
"lahore-chana": "Lahore Chana",
"lamm-palak": "Lamm Palak",
"lamm-vindaloo": "Lamm Vindaloo",
"lamm-rogan-josh": "Lamm Rogan Josh",
"lamm-karahi": "Lamm Karahi",
"bong-nihari": "Bong Nihari",
"paye": "Paye",
"shahi-burger": "Shahi Burgare",
"shami-sandwich": "Shami Sandwich",
"chicken-biryani": "Kyckling Biryani",
"chicken-tikka": "Kyckling Tikka",
"chicken-karahi": "Kyckling Karahi",
"lahore-sizzler": "Lahore Sizzler",
"butter-chicken": "Butter Chicken",
"chicken-haleem": "Kyckling Haleem",
"lahore-pizza": "Lahore Pizza",
"kebab-pizza": "Kebab Pizza",
"tikka-boti-pizza": "Tikka Boti Pizza",
"peshawari-pizza": "Peshawari Pizza",
"veg-pizza": "Veg Pizza",
"tikka-boti-roll": "Tikka Boti Rulle",
"kebab-roll": "Kebab Rulle",
"falafel-roll": "Falafel Rulle",
"paneer-roll": "Paneer Rulle",
"namakpare": "Namak Paray",
"shakar-paray": "Shakar Paray",
"jalebi": "Jalebi",
"gajar-halwa": "Gajar Halwa",
"gajar-barfi": "Gajar Barfi",
"habshi-halwa": "Habshi Halwa",
"gulab-jaman": "Gulab Jamun",
"lambay-gulab-jaman": "Lambay Gulab Jamun",
"cream-gulab-jaman": "Cream Gulab Jamun",
"ras-gulay": "Ras Gulay",
"rasmalai": "Rasmalai",
"cham-cham": "Cham Cham",
"paira": "Paira",
"laddu": "Laddu",
"qalakand": "Kalakand",
"patisa": "Patisa",
"baisan-patisa": "Besan Patisa",
"plain-barfi": "Plain Barfi",
"badam-barfi": "Badam Barfi",
"pistachio-barfi": "Pistachio Barfi",
"pink-barfi": "Pink Barfi",
"coconut-barfi": "Coconut Barfi",
"chocolate-barfi": "Chocolate Barfi",
"baisan-barfi": "Besan Barfi",
"milk-cake-plain": "Mjölkkaka",
"milk-cake-khajoor": "Mjölkkaka med dadlar",
"milk-cake-akhrot": "Mjölkkaka med valnötter",
"kulfi": "Kulfi",
"masala-chai": "Masala Chai",
"mango-lassi": "Mango Lassi",
"coca-cola": "Coca-Cola",
"pepsi-fanta": "Pepsi / Fanta",
"sprite-ramlosa": "Sprite / Ramlösa",
"energy-drink": "Energidryck",
"juice": "Juice",
"coffee": "Kaffe",
"latte": "Latte",
"cappuccino": "Cappuccino",
"tea": "Te"
},
ar: {
"samosa-aloo": "سموسة بطاطا (نباتي)",
"samosa-keema": "سموسة لحم مفروم",
"samosa-chat": "سموسة تشات",
"chana-chat": "تشات حمص",
"panipuri": "بانيبوري / غولغابا",
"keema-naan-starter": "نان لحم مفروم",
"palak-paneer": "بالاك بانير",
"shahi-paneer": "شاهي بانير",
"malai-kofta": "مالاي كوفتا",
"daal-makhani": "دال ماخاني",
"lahore-chana": "لاهوري حمص",
"lamm-palak": "لحم غنم بالسبانخ",
"lamm-vindaloo": "لحم غنم فيندالو",
"lamm-rogan-josh": "لحم غنم روغان جوش",
"lamm-karahi": "لحم غنم كراهي",
"bong-nihari": "نيهاري لحم بقر",
"paye": "أقدام غنم",
"shahi-burger": "برغر شاهي",
"shami-sandwich": "ساندويتش شامي",
"chicken-biryani": "برياني دجاج",
"chicken-tikka": "دجاج تكا",
"chicken-karahi": "دجاج كراهي",
"lahore-sizzler": "سيزلر لاهوري",
"butter-chicken": "دجاج بالزبدة",
"chicken-haleem": "حليم دجاج",
"lahore-pizza": "بيتزا لاهوري",
"kebab-pizza": "بيتزا كباب",
"tikka-boti-pizza": "بيتزا تكا بوتي",
"peshawari-pizza": "بيتزا بشاورية",
"veg-pizza": "بيتزا نباتية",
"tikka-boti-roll": "رول تكا بوتي",
"kebab-roll": "رول كباب",
"falafel-roll": "رول فلافل",
"paneer-roll": "رول بانير",
"namakpare": "ناماك باراي",
"shakar-paray": "شكر باراي",
"jalebi": "جلبي",
"gajar-halwa": "حلوى الجزر",
"gajar-barfi": "بارفي الجزر",
"habshi-halwa": "حلوى حبشي",
"gulab-jaman": "جلاب جامون",
"lambay-gulab-jaman": "جلاب جامون طويل",
"cream-gulab-jaman": "جلاب جامون بالكريمة",
"ras-gulay": "راس غولاي",
"rasmalai": "راس مالاي",
"cham-cham": "تشام تشام",
"paira": "بيرا",
"laddu": "لادو",
"qalakand": "كالاكاند",
"patisa": "باتيسا",
"baisan-patisa": "باتيسا بيسان",
"plain-barfi": "بارفي عادي",
"badam-barfi": "بارفي لوز",
"pistachio-barfi": "بارفي فستق",
"pink-barfi": "بارفي وردي",
"coconut-barfi": "بارفي جوز الهند",
"chocolate-barfi": "بارفي شوكولاتة",
"baisan-barfi": "بارفي بيسان",
"milk-cake-plain": "كعكة الحليب",
"milk-cake-khajoor": "كعكة الحليب بالتمر",
"milk-cake-akhrot": "كعكة الحليب بالجوز",
"kulfi": "كولفي",
"masala-chai": "شاي ماسالا",
"mango-lassi": "لاسي مانجو",
"coca-cola": "كوكا كولا",
"pepsi-fanta": "بيبسي / فانتا",
"sprite-ramlosa": "سبرايت / راملوزا",
"energy-drink": "مشروب طاقة",
"juice": "عصير",
"coffee": "قهوة",
"latte": "لاتيه",
"cappuccino": "كابتشينو",
"tea": "شاي"
},
tr: {
"samosa-aloo": "Patatesli Samosa (vej)",
"samosa-keema": "Kıymalı Samosa",
"samosa-chat": "Samosa Chaat",
"chana-chat": "Chana Chaat",
"panipuri": "Panipuri / Golgappa",
"keema-naan-starter": "Kıymalı Naan",
"palak-paneer": "Palak Paneer",
"shahi-paneer": "Shahi Paneer",
"malai-kofta": "Malai Kofta",
"daal-makhani": "Daal Makhani",
"lahore-chana": "Lahore Nohut",
"lamm-palak": "Kuzu Ispanak",
"lamm-vindaloo": "Kuzu Vindaloo",
"lamm-rogan-josh": "Kuzu Rogan Josh",
"lamm-karahi": "Kuzu Karahi",
"bong-nihari": "Bong Nihari",
"paye": "Paça",
"shahi-burger": "Shahi Burger",
"shami-sandwich": "Shami Sandviç",
"chicken-biryani": "Tavuk Biryani",
"chicken-tikka": "Tavuk Tikka",
"chicken-karahi": "Tavuk Karahi",
"lahore-sizzler": "Lahore Sizzler",
"butter-chicken": "Butter Chicken",
"chicken-haleem": "Tavuk Haleem",
"lahore-pizza": "Lahore Pizza",
"kebab-pizza": "Kebab Pizza",
"tikka-boti-pizza": "Tikka Boti Pizza",
"peshawari-pizza": "Peşaveri Pizza",
"veg-pizza": "Vejetaryen Pizza",
"tikka-boti-roll": "Tikka Boti Dürüm",
"kebab-roll": "Kebab Dürüm",
"falafel-roll": "Falafel Dürüm",
"paneer-roll": "Paneer Dürüm",
"namakpare": "Namak Paray",
"shakar-paray": "Shakar Paray",
"jalebi": "Jalebi",
"gajar-halwa": "Gajar Helva",
"gajar-barfi": "Gajar Barfi",
"habshi-halwa": "Habshi Helva",
"gulab-jaman": "Gulab Jamun",
"lambay-gulab-jaman": "Uzun Gulab Jamun",
"cream-gulab-jaman": "Kremalı Gulab Jamun",
"ras-gulay": "Ras Gulay",
"rasmalai": "Rasmalai",
"cham-cham": "Cham Cham",
"paira": "Paira",
"laddu": "Laddu",
"qalakand": "Kalakand",
"patisa": "Patisa",
"baisan-patisa": "Besan Patisa",
"plain-barfi": "Sade Barfi",
"badam-barfi": "Badem Barfi",
"pistachio-barfi": "Antep Fıstıklı Barfi",
"pink-barfi": "Pembe Barfi",
"coconut-barfi": "Hindistan Cevizli Barfi",
"chocolate-barfi": "Çikolatalı Barfi",
"baisan-barfi": "Besan Barfi",
"milk-cake-plain": "Süt Keki",
"milk-cake-khajoor": "Hurmalı Süt Keki",
"milk-cake-akhrot": "Cevizli Süt Keki",
"kulfi": "Kulfi",
"masala-chai": "Masala Çay",
"mango-lassi": "Mango Lassi",
"coca-cola": "Coca-Cola",
"pepsi-fanta": "Pepsi / Fanta",
"sprite-ramlosa": "Sprite / Ramlösa",
"energy-drink": "Enerji İçeceği",
"juice": "Meyve Suyu",
"coffee": "Kahve",
"latte": "Latte",
"cappuccino": "Kapuçino",
"tea": "Çay"
},
hi: {
"samosa-aloo": "आलू समोसा (वेज)",
"samosa-keema": "कीमा समोसा",
"samosa-chat": "समोसा चाट",
"chana-chat": "चना चाट",
"panipuri": "पानी पूरी / गोलगप्पे",
"keema-naan-starter": "कीमा नान",
"palak-paneer": "पालक पनीर",
"shahi-paneer": "शाही पनीर",
"malai-kofta": "मलाई कोफ्ता",
"daal-makhani": "दाल मखनी",
"lahore-chana": "लाहौर चना",
"lamm-palak": "लैम पालक",
"lamm-vindaloo": "लैम विंदालू",
"lamm-rogan-josh": "लैम रोगन जोश",
"lamm-karahi": "लैम कराही",
"bong-nihari": "बोंग निहारी",
"paye": "पाया",
"shahi-burger": "शाही बर्गर",
"shami-sandwich": "शामी सैंडविच",
"chicken-biryani": "चिकन बिरयानी",
"chicken-tikka": "चिकन टिक्का",
"chicken-karahi": "चिकन कराही",
"lahore-sizzler": "लाहौर सिज़लर",
"butter-chicken": "बटर चिकन",
"chicken-haleem": "चिकन हलीम",
"lahore-pizza": "लाहौर पिज़्ज़ा",
"kebab-pizza": "कबाब पिज़्ज़ा",
"tikka-boti-pizza": "टिक्का बोटी पिज़्ज़ा",
"peshawari-pizza": "पेशावरी पिज़्ज़ा",
"veg-pizza": "वेज पिज़्ज़ा",
"tikka-boti-roll": "टिक्का बोटी रोल",
"kebab-roll": "कबाब रोल",
"falafel-roll": "फलाफेल रोल",
"paneer-roll": "पनीर रोल",
"namakpare": "नमक परे",
"shakar-paray": "शक्कर परे",
"jalebi": "जलेबी",
"gajar-halwa": "गाजर हलवा",
"gajar-barfi": "गाजर बर्फी",
"habshi-halwa": "हबशी हलवा",
"gulab-jaman": "गुलाब जामुन",
"lambay-gulab-jaman": "लंबे गुलाब जामुन",
"cream-gulab-jaman": "क्रीम गुलाब जामुन",
"ras-gulay": "रस गुल्ले",
"rasmalai": "रसमलाई",
"cham-cham": "चम चम",
"paira": "पैरा",
"laddu": "लड्डू",
"qalakand": "कलाकंद",
"patisa": "पटिसा",
"baisan-patisa": "बेसन पटिसा",
"plain-barfi": "सादी बर्फी",
"badam-barfi": "बादाम बर्फी",
"pistachio-barfi": "पिस्ता बर्फी",
"pink-barfi": "गुलाबी बर्फी",
"coconut-barfi": "नारियल बर्फी",
"chocolate-barfi": "चॉकलेट बर्फी",
"baisan-barfi": "बेसन बर्फी",
"milk-cake-plain": "मिल्क केक",
"milk-cake-khajoor": "मिल्क केक खजूर",
"milk-cake-akhrot": "मिल्क केक अखरोट",
"kulfi": "कुल्फी",
"masala-chai": "मसाला चाय",
"mango-lassi": "आम लassi",
"coca-cola": "कोका-कोला",
"pepsi-fanta": "पेप्सी / फैंटा",
"sprite-ramlosa": "स्प्राइट / रामलोसा",
"energy-drink": "एनर्जी ड्रिंक",
"juice": "जूस",
"coffee": "कॉफी",
"latte": "लाटे",
"cappuccino": "कैप्पुचिनो",
"tea": "चाय"
},
ur: {
"samosa-aloo": "آلو سموسہ (ویج)",
"samosa-keema": "کیما سموسہ",
"samosa-chat": "سموسہ چاٹ",
"chana-chat": "چنا چاٹ",
"panipuri": "پانی پوری / گول گپے",
"keema-naan-starter": "کیما نان",
"palak-paneer": "پالک پنیر",
"shahi-paneer": "شاہی پنیر",
"malai-kofta": "ملائی کوفتہ",
"daal-makhani": "دال مکھنی",
"lahore-chana": "لاہور چنا",
"lamm-palak": "لیم پالک",
"lamm-vindaloo": "لیم وندالو",
"lamm-rogan-josh": "لیم روگن جوش",
"lamm-karahi": "لیم کراہی",
"bong-nihari": "بونگ نہاری",
"paye": "پائے",
"shahi-burger": "شاہی برگر",
"shami-sandwich": "شامی سینڈوچ",
"chicken-biryani": "چکن بریانی",
"chicken-tikka": "چکن ٹکا",
"chicken-karahi": "چکن کراہی",
"lahore-sizzler": "لاہور سزлер",
"butter-chicken": "بٹر چکن",
"chicken-haleem": "چکن حلیم",
"lahore-pizza": "لاہور پیزا",
"kebab-pizza": "کباب پیزا",
"tikka-boti-pizza": "ٹکا بوٹی پیزا",
"peshawari-pizza": "پشاوری پیزا",
"veg-pizza": "ویج پیزا",
"tikka-boti-roll": "ٹکا بوٹی رول",
"kebab-roll": "کباب رول",
"falafel-roll": "فلافل رول",
"paneer-roll": "پنیر رول",
"namakpare": "نمک پارے",
"shakar-paray": "شکر پارے",
"jalebi": "جلیبی",
"gajar-halwa": "گاجر حلوہ",
"gajar-barfi": "گاجر برفی",
"habshi-halwa": "حبشی حلوہ",
"gulab-jaman": "گلاب جامن",
"lambay-gulab-jaman": "لمبے گلاب جامن",
"cream-gulab-jaman": "کریم گلاب جامن",
"ras-gulay": "رس گلّے",
"rasmalai": "رسمالی",
"cham-cham": "چم چم",
"paira": "پیرا",
"laddu": "لڈو",
"qalakand": "قلاکند",
"patisa": "پٹیسہ",
"baisan-patisa": "بیسن پٹیسہ",
"plain-barfi": "سادہ برفی",
"badam-barfi": "بادام برفی",
"pistachio-barfi": "پستہ برفی",
"pink-barfi": "گلابی برفی",
"coconut-barfi": "ناریل برفی",
"chocolate-barfi": "چاکلیٹ برفی",
"baisan-barfi": "بیسن برفی",
"milk-cake-plain": "ملک کیک",
"milk-cake-khajoor": "ملک کیک کھجور",
"milk-cake-akhrot": "ملک کیک اخروٹ",
"kulfi": "قلفی",
"masala-chai": "مصالحہ چائے",
"mango-lassi": "آم لسی",
"coca-cola": "کوکا کولا",
"pepsi-fanta": "پیپسی / فانٹا",
"sprite-ramlosa": "سپرائٹ / راملوزا",
"energy-drink": "انرجی ڈرنک",
"juice": "جوس",
"coffee": "کافی",
"latte": "لیٹے",
"cappuccino": "کیپوچینو",
"tea": "چائے"
},
};
export function getMenuItemName(lang: Language, itemId: string, fallback: string): string {
if (lang === 'en') return EN[itemId] ?? fallback;
return LOCALIZED[lang]?.[itemId] ?? EN[itemId] ?? fallback;
}
+36 -10
View File
@@ -2,6 +2,10 @@ import type { Language, LanguageOption } from '@/domain/language/entities';
import { arTranslations } from './languages/ar'; import { arTranslations } from './languages/ar';
import { trTranslations } from './languages/tr'; import { trTranslations } from './languages/tr';
import { mergeTranslations } from './merge'; import { mergeTranslations } from './merge';
import {
HINDI_MENU_DESCRIPTIONS,
MENU_DESCRIPTION_SUPPLEMENT,
} from './menu/description-supplement';
export type { Language }; export type { Language };
@@ -86,7 +90,7 @@ export const translations = {
filterGrill: 'Grill', filterGrill: 'Grill',
filterSweets: 'Sweets', filterSweets: 'Sweets',
filterMeat: 'Meat', filterMeat: 'Meat',
filterStreet: 'Street', filterStreet: 'Street Food',
filterRoll: 'Roll', filterRoll: 'Roll',
addToTable: 'Add to Table', addToTable: 'Add to Table',
viewFullMenu: 'VIEW THE COMPLETE MENU — 40+ DISHES', viewFullMenu: 'VIEW THE COMPLETE MENU — 40+ DISHES',
@@ -126,7 +130,6 @@ export const translations = {
"shami-sandwich": "Spiced minced meat shami kebab patties served in bread with chutney and onions.", "shami-sandwich": "Spiced minced meat shami kebab patties served in bread with chutney and onions.",
"chicken-biryani": "Fragrant aged basmati rice layered with tender spiced chicken, saffron and caramelized onions.", "chicken-biryani": "Fragrant aged basmati rice layered with tender spiced chicken, saffron and caramelized onions.",
"chicken-tikka": "Boneless chicken pieces marinated in yogurt and spices, grilled in a tandoor.", "chicken-tikka": "Boneless chicken pieces marinated in yogurt and spices, grilled in a tandoor.",
"tikka-boti": "Tender chicken chunks marinated in spices and grilled on skewers.",
"chicken-karahi": "Wok-tossed chicken in a robust tomato, chili and ginger gravy.", "chicken-karahi": "Wok-tossed chicken in a robust tomato, chili and ginger gravy.",
"lahore-sizzler": "Sizzling platter of marinated chicken with vegetables and spicy sauces.", "lahore-sizzler": "Sizzling platter of marinated chicken with vegetables and spicy sauces.",
"butter-chicken": "Tender chicken in a creamy tomato and butter gravy with mild spices.", "butter-chicken": "Tender chicken in a creamy tomato and butter gravy with mild spices.",
@@ -196,6 +199,9 @@ export const translations = {
subtitle: subtitle:
'Premium per-head packages for weddings, corporate events, and family gatherings — authentic Shahi flavours, beautifully served.', 'Premium per-head packages for weddings, corporate events, and family gatherings — authentic Shahi flavours, beautifully served.',
perHead: 'per guest', perHead: 'per guest',
contactForPricing: 'Contact us for pricing',
packagePricingInquiry:
'Hello Shahi Kitchen 👋\n\nI would like pricing for the {{package}} catering package.\n\nPlease contact me with availability and options.\n\nThank you!',
includedDishes: 'Included dishes', includedDishes: 'Included dishes',
mostPopular: 'Most Popular', mostPopular: 'Most Popular',
guestNote: 'Custom menus available — contact us to tailor your event.', guestNote: 'Custom menus available — contact us to tailor your event.',
@@ -457,7 +463,7 @@ export const translations = {
filterGrill: 'Grill', filterGrill: 'Grill',
filterSweets: 'Sötsaker', filterSweets: 'Sötsaker',
filterMeat: 'Kött', filterMeat: 'Kött',
filterStreet: 'Gata', filterStreet: 'Gatumat',
filterRoll: 'Rulle', filterRoll: 'Rulle',
addToTable: 'Lägg till', addToTable: 'Lägg till',
viewFullMenu: 'SE HELA MENYN — 40+ RÄTTER', viewFullMenu: 'SE HELA MENYN — 40+ RÄTTER',
@@ -497,7 +503,6 @@ export const translations = {
"shami-sandwich": "Kryddade malet kött shami kebab-patties serverade i bröd med chutney och lök.", "shami-sandwich": "Kryddade malet kött shami kebab-patties serverade i bröd med chutney och lök.",
"chicken-biryani": "Doftande åldrad basmatiris med lager av kryddad kyckling, saffran och karamelliserade lökar.", "chicken-biryani": "Doftande åldrad basmatiris med lager av kryddad kyckling, saffran och karamelliserade lökar.",
"chicken-tikka": "Benfri kyckling marinerad i yoghurt och kryddor, grillad i tandoorugn.", "chicken-tikka": "Benfri kyckling marinerad i yoghurt och kryddor, grillad i tandoorugn.",
"tikka-boti": "Möra kycklingbitar marinerade i kryddor och grillade på spett.",
"chicken-karahi": "Wokad kyckling i en robust tomat-, chili- och ingefärssås.", "chicken-karahi": "Wokad kyckling i en robust tomat-, chili- och ingefärssås.",
"lahore-sizzler": "Surrande fat med marinerad kyckling med grönsaker och kryddiga såser.", "lahore-sizzler": "Surrande fat med marinerad kyckling med grönsaker och kryddiga såser.",
"butter-chicken": "Mör kyckling i en krämig tomat- och smörsås med milda kryddor.", "butter-chicken": "Mör kyckling i en krämig tomat- och smörsås med milda kryddor.",
@@ -564,6 +569,9 @@ export const translations = {
subtitle: subtitle:
'Premium paket per gäst för bröllop, företagsevent och familjefester — autentiska Shahi-smaker, vackert serverade.', 'Premium paket per gäst för bröllop, företagsevent och familjefester — autentiska Shahi-smaker, vackert serverade.',
perHead: 'per gäst', perHead: 'per gäst',
contactForPricing: 'Kontakta oss för pris',
packagePricingInquiry:
'Hej Shahi Kitchen 👋\n\nJag vill ha pris för {{package}} cateringpaketet.\n\nVänligen kontakta mig med tillgänglighet och alternativ.\n\nTack!',
includedDishes: 'Ingående rätter', includedDishes: 'Ingående rätter',
mostPopular: 'Mest populär', mostPopular: 'Mest populär',
guestNote: 'Skräddarsydda menyer finns — kontakta oss för att anpassa ditt event.', guestNote: 'Skräddarsydda menyer finns — kontakta oss för att anpassa ditt event.',
@@ -823,6 +831,7 @@ export const translations = {
filterRice: 'चावल', filterRice: 'चावल',
filterGrill: 'ग्रिल', filterGrill: 'ग्रिल',
filterSweets: 'मिठाइयाँ', filterSweets: 'मिठाइयाँ',
filterStreet: 'स्ट्रीट फूड',
addToTable: 'ऐड करें', addToTable: 'ऐड करें',
viewFullMenu: 'पूरी मेन्यू देखें — 40+ व्यंजन', viewFullMenu: 'पूरी मेन्यू देखें — 40+ व्यंजन',
}, },
@@ -861,7 +870,6 @@ export const translations = {
"shami-sandwich": "Spiced minced meat shami kebab patties served in bread with chutney and onions.", "shami-sandwich": "Spiced minced meat shami kebab patties served in bread with chutney and onions.",
"chicken-biryani": "Fragrant aged basmati rice layered with tender spiced chicken, saffron and caramelized onions.", "chicken-biryani": "Fragrant aged basmati rice layered with tender spiced chicken, saffron and caramelized onions.",
"chicken-tikka": "Boneless chicken pieces marinated in yogurt and spices, grilled in a tandoor.", "chicken-tikka": "Boneless chicken pieces marinated in yogurt and spices, grilled in a tandoor.",
"tikka-boti": "Tender chicken chunks marinated in spices and grilled on skewers.",
"chicken-karahi": "Wok-tossed chicken in a robust tomato, chili and ginger gravy.", "chicken-karahi": "Wok-tossed chicken in a robust tomato, chili and ginger gravy.",
"lahore-sizzler": "Sizzling platter of marinated chicken with vegetables and spicy sauces.", "lahore-sizzler": "Sizzling platter of marinated chicken with vegetables and spicy sauces.",
"butter-chicken": "Tender chicken in a creamy tomato and butter gravy with mild spices.", "butter-chicken": "Tender chicken in a creamy tomato and butter gravy with mild spices.",
@@ -1000,6 +1008,9 @@ export const translations = {
subtitle: subtitle:
'शादियों, कॉर्पोरेट इवेंट और पारिवारिक समारोहों के लिए प्रीमियम प्रति व्यक्ति पैकेज — प्रामाणिक शाही स्वाद, सुंदर परोसा।', 'शादियों, कॉर्पोरेट इवेंट और पारिवारिक समारोहों के लिए प्रीमियम प्रति व्यक्ति पैकेज — प्रामाणिक शाही स्वाद, सुंदर परोसा।',
perHead: 'प्रति अतिथि', perHead: 'प्रति अतिथि',
contactForPricing: 'मूल्य जानने के लिए हमसे संपर्क करें',
packagePricingInquiry:
'नमस्ते Shahi Kitchen 👋\n\nमुझे {{package}} कैटरिंग पैकेज की कीमत जाननी है।\n\nकृपया उपलब्धता और विकल्पों के साथ मुझसे संपर्क करें।\n\nधन्यवाद!',
includedDishes: 'शामिल व्यंजन', includedDishes: 'शामिल व्यंजन',
mostPopular: 'सबसे लोकप्रिय', mostPopular: 'सबसे लोकप्रिय',
guestNote: 'कस्टम मेनू उपलब्ध — अपने इवेंट के लिए हमसे संपर्क करें।', guestNote: 'कस्टम मेनू उपलब्ध — अपने इवेंट के लिए हमसे संपर्क करें।',
@@ -1184,6 +1195,7 @@ export const translations = {
filterRice: 'چاول', filterRice: 'چاول',
filterGrill: 'گرل', filterGrill: 'گرل',
filterSweets: 'مٹھائیاں', filterSweets: 'مٹھائیاں',
filterStreet: 'سٹریٹ فوڈ',
addToTable: 'شامل کریں', addToTable: 'شامل کریں',
viewFullMenu: 'مکمل مینو دیکھیں — 40+ پکوان', viewFullMenu: 'مکمل مینو دیکھیں — 40+ پکوان',
}, },
@@ -1222,7 +1234,6 @@ export const translations = {
"shami-sandwich": "مصالحہ دار کییما کے شامی کباب پیٹیز بریڈ میں چٹنی اور پیاز کے ساتھ۔", "shami-sandwich": "مصالحہ دار کییما کے شامی کباب پیٹیز بریڈ میں چٹنی اور پیاز کے ساتھ۔",
"chicken-biryani": "خوشبودار پرانی باسمتی چاول کی تہوں میں نرم مصالحہ دار چکن، زعفران اور کاراملائزڈ پیاز۔", "chicken-biryani": "خوشبودار پرانی باسمتی چاول کی تہوں میں نرم مصالحہ دار چکن، زعفران اور کاراملائزڈ پیاز۔",
"chicken-tikka": "ہڈیوں والے چکن کے ٹکڑے دہی اور مصالحوں میں مرینیٹ کیے گئے، ٹینڈور میں گرل کیے گئے۔", "chicken-tikka": "ہڈیوں والے چکن کے ٹکڑے دہی اور مصالحوں میں مرینیٹ کیے گئے، ٹینڈور میں گرل کیے گئے۔",
"tikka-boti": "نرم چکن کے ٹکڑے مصالحوں میں مرینیٹ کیے گئے اور سیخوں پر گرل کیے گئے۔",
"chicken-karahi": "ووک میں تلے ہوئے چکن کا مضبوط ٹماٹر، مرچ اور ادرک کی گریوی میں۔", "chicken-karahi": "ووک میں تلے ہوئے چکن کا مضبوط ٹماٹر، مرچ اور ادرک کی گریوی میں۔",
"lahore-sizzler": "مرینیٹ کیے گئے چکن کا سٹیلنگ پلیٹر سبزیوں اور تیز ساس کے ساتھ۔", "lahore-sizzler": "مرینیٹ کیے گئے چکن کا سٹیلنگ پلیٹر سبزیوں اور تیز ساس کے ساتھ۔",
"butter-chicken": "نرم چکن کریمی ٹماٹر اور مکھن کی گریوی میں ہلکے مصالحوں کے ساتھ۔", "butter-chicken": "نرم چکن کریمی ٹماٹر اور مکھن کی گریوی میں ہلکے مصالحوں کے ساتھ۔",
@@ -1361,6 +1372,9 @@ export const translations = {
subtitle: subtitle:
'شادیوں، کارپوریٹ ایونٹس اور خاندانی محفلوں کے لیے پریمیم فی مہمان پیکج — مستند شاہی ذائقے، خوبصورت پیشکش۔', 'شادیوں، کارپوریٹ ایونٹس اور خاندانی محفلوں کے لیے پریمیم فی مہمان پیکج — مستند شاہی ذائقے، خوبصورت پیشکش۔',
perHead: 'فی مہمان', perHead: 'فی مہمان',
contactForPricing: 'قیمت کے لیے ہم سے رابطہ کریں',
packagePricingInquiry:
'ہیلو Shahi Kitchen 👋\n\nمجھے {{package}} کیٹرنگ پیکج کی قیمت جاننی ہے۔\n\nبراہ مہربانی دستیابی اور اختیارات کے ساتھ مجھ سے رابطہ کریں۔\n\nشکریہ!',
includedDishes: 'شامل کھانے', includedDishes: 'شامل کھانے',
mostPopular: 'سب سے مقبول', mostPopular: 'سب سے مقبول',
guestNote: 'حسب ضرورت مینو دستیاب — اپنے ایونٹ کے لیے ہم سے رابطہ کریں۔', guestNote: 'حسب ضرورت مینو دستیاب — اپنے ایونٹ کے لیے ہم سے رابطہ کریں۔',
@@ -1492,8 +1506,20 @@ const trBundle = mergeTranslations(
trTranslations as unknown as Partial<TranslationBundle> trTranslations as unknown as Partial<TranslationBundle>
); );
export function getTranslation(lang: Language): TranslationBundle { function withMenuText(lang: Language, bundle: TranslationBundle): TranslationBundle {
if (lang === 'ar') return arBundle as TranslationBundle; const baseDescriptions = { ...bundle.menuDescriptions };
if (lang === 'tr') return trBundle as TranslationBundle; const hiDescriptions =
return translations[lang] as TranslationBundle; lang === 'hi' ? { ...baseDescriptions, ...HINDI_MENU_DESCRIPTIONS } : baseDescriptions;
const supplement = MENU_DESCRIPTION_SUPPLEMENT[lang];
const menuDescriptions = supplement
? { ...hiDescriptions, ...supplement }
: hiDescriptions;
return { ...bundle, menuDescriptions };
}
export function getTranslation(lang: Language): TranslationBundle {
if (lang === 'ar') return withMenuText(lang, arBundle as TranslationBundle);
if (lang === 'tr') return withMenuText(lang, trBundle as TranslationBundle);
return withMenuText(lang, translations[lang] as TranslationBundle);
} }
+1 -1
View File
@@ -19,7 +19,7 @@ const JPEG_QUALITY = 88;
/** Menu ids that share another dish's ftp photo (main bilder-bp folder) */ /** Menu ids that share another dish's ftp photo (main bilder-bp folder) */
const FTP_ALIASES: Record<string, string> = { const FTP_ALIASES: Record<string, string> = {
'tikka-boti': 'chicken-tikka',
}; };
/** Menu id → folder name under ftp-images/bilder-bp/sweets/ */ /** Menu id → folder name under ftp-images/bilder-bp/sweets/ */