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

This commit is contained in:
root
2026-06-29 16:22:09 +00:00
parent 57cc67d2d3
commit 50e3a34895
723 changed files with 20055 additions and 26101 deletions
+107
View File
@@ -0,0 +1,107 @@
'use client';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import PageHeader from '@/components/PageHeader';
import CateringPackageCard from '@/components/CateringPackageCard';
import {
cateringPackages,
CATERING_DISH_CATALOG,
CATERING_CATEGORY_ORDER,
} from '@/lib/catering-data';
import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations';
import { container } from '@/infrastructure/di/container';
import { Sparkles } from 'lucide-react';
export default function CateringPage() {
const { language } = useLanguage();
const t = getTranslation(language);
const handleSendInquiry = () => {
container.messagingGateway.openWhatsApp(t.catering.inquiryMessage);
};
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main>
<PageHeader
eyebrow={t.catering.badge}
title={t.catering.title}
subtitle={t.catering.subtitle}
/>
{/* Packages grid */}
<section className="max-w-7xl mx-auto px-6 pb-16">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{cateringPackages.map((pkg, index) => (
<CateringPackageCard key={pkg.id} pkg={pkg} index={index} />
))}
</div>
</section>
{/* Full dish catalog reference */}
<section className="border-y border-[#EDE6D9] bg-[#fffdf8] px-6 py-12">
<div className="mx-auto max-w-7xl">
<div className="mb-8">
<div className="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-[#c99a2e]">
<Sparkles className="h-3.5 w-3.5" />
{t.catering.catalogBadge}
</div>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
<div className="min-w-0 flex-1">
<h2 className="text-xl sm:text-[1.35rem] tracking-[-0.4px] text-[#101724]">
{t.catering.catalogTitle}
</h2>
<p className="mt-1.5 max-w-xl text-sm leading-relaxed text-[#6B665F]">
{t.catering.catalogSubtitle}
</p>
</div>
<button
type="button"
onClick={handleSendInquiry}
className="btn-primary inline-flex shrink-0 items-center justify-center self-start rounded-full px-7 py-3 text-sm font-bold tracking-wide sm:self-center"
>
{t.catering.sendInquiry}
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{CATERING_CATEGORY_ORDER.map((category) => {
const dishes = CATERING_DISH_CATALOG.filter((d) => d.category === category);
const categoryLabel =
(t.catering.categories as Record<string, string>)?.[category] ?? category;
return (
<div
key={category}
className="rounded-2xl border border-[#EDE6D9] bg-white p-5 shadow-sm"
>
<h3 className="mb-3 text-xs font-bold uppercase tracking-[0.2em] text-[#8a6a25]">
{categoryLabel}
</h3>
<ul className="space-y-2">
{dishes.map((dish) => (
<li
key={dish.id}
className="text-sm font-medium text-[#101724]"
>
{dish.name}
</li>
))}
</ul>
</div>
);
})}
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

+482
View File
@@ -0,0 +1,482 @@
/**
* =============================================================================
* GLOBAL DESIGN SYSTEM — Shahi Kitchen
* =============================================================================
*
* Shahi cream + gold visual identity for Shahi Kitchen.
* Bright, warm, appetizing, and luxurious without being dark.
*/
@import "tailwindcss";
:root {
/* === Refined Shahi Light Palette - Warm Cream + Gold === */
/* Backgrounds */
--bg: #F8F5F0;
--bg-elevated: #F5F1E9;
--surface: #FFFFFF;
--surface-subtle: #F2EDE4;
/* Gold System */
--gold: #B38B4D;
--gold-dark: #8C6B3A;
--gold-light: #C9A46B;
--gold-muted: #d4a73d;
/* Emerald for contrast */
--emerald: #0f5a4a;
--emerald-deep: #0a4a3d;
/* Text */
--text: #2C2520;
--text-muted: #6B665F;
--text-light: #8A8478;
/* Borders */
--border: #EDE6D9;
/* Typography */
--font-display: var(--font-playfair);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-arabic: var(--font-arabic);
/* Motion */
--ease: cubic-bezier(0.25, 1, 0.5, 1);
/* Layout — language banner (48px) + navbar (68px) */
--header-height: 116px;
--page-top-gap: 1rem;
}
/* Anchor links (/#menu, /#experience) land below the fixed header */
.scroll-mt-header {
scroll-margin-top: var(--header-height);
}
/* Base */
body,
.shahi-body {
font-family: var(--font-sans);
background-color: var(--bg);
color: var(--text);
background-image:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(201, 154, 46, 0.08), transparent),
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(15, 90, 74, 0.04), transparent);
background-attachment: fixed;
}
/* RTL typography */
html[dir="rtl"] body {
font-family: var(--font-arabic), var(--font-sans);
}
html[dir="rtl"] h1,
html[dir="rtl"] h2,
html[dir="rtl"] h3,
html[dir="rtl"] h4 {
font-family: var(--font-arabic), var(--font-display);
letter-spacing: 0;
}
h1, h2, h3, h4 {
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -0.025em;
color: var(--text);
}
/* Premium Buttons */
.btn-primary {
background: linear-gradient(135deg, var(--gold), var(--gold-muted));
color: #241806;
font-weight: 700;
transition: all 0.2s var(--ease);
}
.btn-primary:hover {
transform: translateY(-1px);
}
.btn-outline {
border: 1px solid var(--gold);
color: var(--gold-dark);
background: transparent;
font-weight: 600;
transition: all 0.2s var(--ease);
}
.btn-outline:hover {
background: var(--gold);
color: #241806;
}
/* Glass effect helper */
.glass {
background: rgba(255, 253, 248, 0.72);
backdrop-filter: blur(20px);
}
/* Luxury card style */
.luxury-card {
border-radius: 2rem;
transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
}
.luxury-card:hover {
transform: translateY(-4px);
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.12);
}
/* Signature dish cards */
.signature-card {
box-shadow: 0 4px 24px -4px rgb(16 23 36 / 0.06);
transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), border-color 0.35s var(--ease);
}
.signature-card:hover {
box-shadow: 0 20px 40px -12px rgb(179 139 77 / 0.18);
border-color: rgba(201, 154, 46, 0.45);
}
/* Section ornament */
.section-ornament {
position: relative;
}
.section-ornament::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 3px;
background: linear-gradient(90deg, transparent, var(--gold-muted), transparent);
border-radius: 2px;
}
/* Hero overlay text */
.hero-glass {
background: linear-gradient(
135deg,
rgba(255, 253, 248, 0.92) 0%,
rgba(255, 250, 240, 0.88) 50%,
rgba(248, 245, 240, 0.85) 100%
);
backdrop-filter: blur(20px);
border: 1px solid rgba(201, 154, 46, 0.2);
box-shadow:
0 25px 50px -12px rgba(16, 23, 36, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
/* =============================================================================
* FOOTER — Matches header cream + gold palette
* ============================================================================= */
.shahi-footer {
background: linear-gradient(
180deg,
#fffcf7 0%,
#fbf7ef 55%,
#f5f0e6 100%
);
color: #101724;
border-top: 1px solid rgba(201, 154, 46, 0.25);
box-shadow: 0 -4px 24px -4px rgba(16, 23, 36, 0.06);
}
.footer-top-rail {
height: 4px;
background: linear-gradient(
90deg,
transparent 0%,
#8a6a25 15%,
#c99a2e 35%,
#f4d47f 50%,
#c99a2e 65%,
#8a6a25 85%,
transparent 100%
);
}
.footer-main {
position: relative;
}
.footer-glow {
position: absolute;
border-radius: 50%;
pointer-events: none;
filter: blur(80px);
}
.footer-glow--gold {
top: -40px;
right: 10%;
width: 280px;
height: 280px;
background: rgba(201, 154, 46, 0.08);
}
.footer-glow--emerald {
bottom: 0;
left: -5%;
width: 320px;
height: 320px;
background: rgba(15, 90, 74, 0.05);
}
.footer-heading-line {
display: inline-block;
width: 20px;
height: 2px;
background: linear-gradient(90deg, #c99a2e, #f4d47f);
border-radius: 1px;
}
.footer-info-card {
border-radius: 1rem;
border: 1px solid rgba(201, 154, 46, 0.22);
background: rgba(255, 255, 255, 0.72);
padding: 1rem 1.125rem;
backdrop-filter: blur(8px);
transition: border-color 0.25s var(--ease), background 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.footer-info-card:hover {
border-color: rgba(201, 154, 46, 0.45);
background: #ffffff;
box-shadow: 0 4px 16px -6px rgba(16, 23, 36, 0.08);
}
.footer-link {
color: #3d4654;
transition: color 0.2s var(--ease), transform 0.2s var(--ease);
}
.footer-link:hover {
color: #0f5a4a;
}
html[dir="rtl"] .footer-link:hover {
transform: translateX(-2px);
}
html[dir="ltr"] .footer-link:hover {
transform: translateX(2px);
}
.footer-social-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.875rem;
border: 1px solid rgba(201, 154, 46, 0.35);
background: rgba(255, 255, 255, 0.85);
color: #8a6a25;
transition: all 0.25s var(--ease);
}
.footer-social-btn:hover {
background: linear-gradient(135deg, #c99a2e, #e8c56a);
color: #241806;
border-color: #c99a2e;
transform: translateY(-2px);
box-shadow: 0 8px 24px -4px rgba(201, 154, 46, 0.35);
}
.footer-logo-ring {
border-color: rgba(201, 154, 46, 0.35);
background: #ffffff;
box-shadow: 0 4px 20px -6px rgba(16, 23, 36, 0.1);
}
.footer-bottom {
background: rgba(255, 255, 255, 0.55);
border-top: 1px solid rgba(201, 154, 46, 0.2);
}
.footer-cta {
backdrop-filter: blur(12px);
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.92) 0%,
rgba(255, 252, 247, 0.88) 100%
);
border-color: rgba(201, 154, 46, 0.35);
}
/* Language banner subtle shine */
.lang-banner {
position: relative;
overflow: hidden;
}
.lang-banner::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.06) 50%,
transparent 100%
);
pointer-events: none;
}
/* Mesmerizing shimmer animation for premium buttons (used in Navbar) */
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
/* Continuous menu reel marquee for 43" displays - items move LEFT (enter from right, exit left), seamless loop with 2x duplication */
@keyframes menu-reel {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.menu-reel-track {
will-change: transform;
animation: menu-reel linear infinite;
/* Force GPU layer for stable poster/video during transform */
transform: translateZ(0);
backface-visibility: hidden;
}
/* Homepage signature menu — dishes scroll left to right */
@keyframes signature-menu-reel-ltr {
0% { transform: translateX(-50%); }
100% { transform: translateX(0); }
}
.signature-menu-reel-track {
will-change: transform;
animation: signature-menu-reel-ltr linear infinite;
transform: translateZ(0);
backface-visibility: hidden;
}
.signature-menu-reel:hover .signature-menu-reel-track {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.signature-menu-reel-track {
animation: none;
flex-wrap: wrap;
width: 100% !important;
justify-content: center;
}
}
/* =============================================================================
* KIOSK / FULLSCREEN DISPLAY PAGES (screen1 & screen2)
* Professional clean TV/window look: no scrollbars ever, even in browser
* fullscreen (F11) or kiosk mode. User zooms browser (in/out) to adjust
* visible reel density instead of scrolling. Content sized to fit 100vh.
* ============================================================================= */
html.screen-kiosk,
body.screen-kiosk {
overflow: hidden !important;
overscroll-behavior: none;
/* Hide scrollbars cross-browser */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
html.screen-kiosk::-webkit-scrollbar,
body.screen-kiosk::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
.screen-root {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
/* Extra insurance: hide any descendant scrollbars */
scrollbar-width: none;
-ms-overflow-style: none;
}
.screen-root::-webkit-scrollbar,
.screen-root *::-webkit-scrollbar {
display: none;
}
/* Subtle polish for pure display (no accidental text selection or pointer) */
.screen-root {
cursor: default;
}
/* =============================================================================
* VERTICAL REEL for screen3 order columns
* Orders move UP (from below to above) in a loop, like screen2 horizontal but vertical.
* Cards have fixed px height; viewport h-full of available column space (after header) =>
* fills entire screen height with no white space (max top to bottom).
* Zoom in browser: fewer items visible per column (larger cards relative).
* Zoom out: more items visible per column (smaller cards relative).
* When > visible count, the track loops seamlessly (duplicated list + 50% translateY).
* ============================================================================= */
@keyframes order-reel-up {
0% { transform: translateY(0); }
100% { transform: translateY(-50%); }
}
.order-reel-track {
display: flex;
flex-direction: column;
will-change: transform;
animation: order-reel-up linear infinite;
/* Force GPU layer for stable animation */
transform: translateZ(0);
backface-visibility: hidden;
}
.reel-viewport {
overflow: hidden;
/* Height 100% of available (flex h-full after section header) to fill screen.
Fixed card heights + zoom changes visible count dynamically. */
}
/* Menu page — smooth category filter scrolling */
.menu-category-scroll {
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
}
.menu-category-scroll::-webkit-scrollbar {
display: none;
}
@media (min-width: 1024px) {
.menu-category-scroll {
scrollbar-width: thin;
scrollbar-color: #d4c9b8 transparent;
}
.menu-category-scroll::-webkit-scrollbar {
display: block;
width: 5px;
}
.menu-category-scroll::-webkit-scrollbar-track {
background: transparent;
}
.menu-category-scroll::-webkit-scrollbar-thumb {
background: #d4c9b8;
border-radius: 999px;
}
.menu-category-scroll::-webkit-scrollbar-thumb:hover {
background: #c99a2e;
}
}
+123
View File
@@ -0,0 +1,123 @@
/**
* ROOT LAYOUT — Shahi Kitchen (shahikitchen.se)
*
* This is the single root layout for the entire Next.js App Router application.
*
* RESPONSIBILITIES:
* - Set up global metadata (title, description, favicon) for SEO and social sharing
* - Load the premium typography system:
* • Playfair Display (display/serif) → used for headings via --font-playfair
* • Geist Sans (system UI) → body text
* • Geist Mono → code / tabular data
* - Initialize the GLOBAL CART SYSTEM:
* • CartProvider wraps the whole tree so any page/component can use `useCart()`
* • CartDrawer is rendered once at the root (slide-in basket panel)
* • Sonner <Toaster> provides beautiful toast notifications used by the cart
*
* ARCHITECTURAL NOTES:
* - We deliberately render <CartDrawer /> and <Toaster /> at the root level instead of
* inside individual pages. This guarantees only ONE instance exists and prevents
* duplicate drawers/toasts when navigating.
* - The cream/gold Shahi theme tokens live in globals.css and are referenced via
* Tailwind arbitrary values (e.g. bg-[#F8F5F0]).
*
* FUTURE DEVELOPERS:
* - To add a new global provider (theme, analytics, etc.), wrap it here.
* - WhatsApp number for orders is currently hardcoded in CartDrawer.tsx (46739381089).
* - If you ever split the cart into a separate modal library, keep the provider here.
*/
import type { Metadata } from "next";
import { Playfair_Display, Geist, Geist_Mono, Noto_Sans_Arabic } from "next/font/google";
import "./globals.css";
import { Toaster } from "sonner";
import CartDrawer from "@/components/CartDrawer";
import WishlistDrawer from "@/components/WishlistDrawer";
import { CartProvider } from "@/presentation/providers/cart-provider";
import { WishlistProvider } from "@/presentation/providers/wishlist-provider";
import { LanguageProvider } from "@/presentation/providers/language-provider";
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
style: ["normal", "italic"],
});
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
const notoArabic = Noto_Sans_Arabic({
variable: "--font-arabic",
subsets: ["arabic"],
weight: ["400", "500", "600", "700"],
});
/**
* Next.js Metadata API (static)
* These values power:
* - Browser tab title
* - Search engine results
* - Social cards (Open Graph / Twitter) when shared
*
* For dynamic per-page metadata, use `generateMetadata()` in page files.
*/
export const metadata: Metadata = {
title: "Shahi Kitchen | Authentic Indian & Pakistani Restaurant in Gothenburg",
description: "Experience Shahi flavors at Shahi Kitchen in Askim, Gothenburg. Authentic Indian & Pakistani cuisine, famous lunch buffet, and traditional sweets since 2016.",
icons: {
icon: "/favicon.ico",
},
};
/**
* RootLayout
*
* The ONLY place in the app where we initialize:
* 1. CartProvider → gives every descendant access to `useCart()` hook
* 2. CartDrawer → the actual slide-in basket UI (controlled via context)
* 3. Toaster → global toast surface used by cart (Sonner library)
*
* IMPORTANT:
* - Never wrap the entire app in another CartProvider — it will break the singleton.
* - The body uses the Shahi cream background (#F8F5F0) as the base canvas.
* - `antialiased` + font variables are applied once at the root for consistency.
*/
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="sv"
suppressHydrationWarning
className={`${playfair.variable} ${geistSans.variable} ${geistMono.variable} ${notoArabic.variable} h-full antialiased`}
>
<body
suppressHydrationWarning
className="min-h-full flex flex-col shahi-body text-[#2C2A26]"
>
<LanguageProvider>
<CartProvider>
<WishlistProvider>
{children}
<CartDrawer />
<WishlistDrawer />
<Toaster position="top-center" richColors closeButton />
</WishlistProvider>
</CartProvider>
</LanguageProvider>
</body>
</html>
);
}
+144
View File
@@ -0,0 +1,144 @@
/**
* LOCATIONS PAGE
*
* Dedicated page for the restaurant's two physical branches.
*
* ARCHITECTURE:
* - Two branches are modeled as distinct concepts:
* 1. Shahi Kitchen Askim (Sisjön) → Full restaurant + famous lunch buffet
* 2. Shahi Sweets Backaplan → Sweets, snacks, café, halwa puri etc.
* - Both are operated by the same team (explicitly stated at the bottom)
* - Google Maps embeds are intentionally simple (lazy loaded)
*
* FUTURE:
* - When real online ordering launches, this page could show "Order from Askim"
* vs "Order from Backaplan" with different delivery radii.
* - Both branches now have the same opening hours.
*/
'use client';
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import PageHeader from "@/components/PageHeader";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
export default function LocationsPage() {
const { language } = useLanguage();
const t = getTranslation(language);
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<PageHeader
className="max-w-5xl pb-4"
centered
eyebrow="WHERE TO FIND US"
title={language === 'sv' ? 'Våra Platser' : 'Our Locations'}
subtitle={
language === 'sv'
? 'Två restauranger i Göteborg — båda serverar autentiska smaker med samma shahi gästfrihet.'
: 'Two branches in Gothenburg — both serving authentic flavors with the same Shahi hospitality.'
}
/>
<div className="max-w-5xl mx-auto px-6 pb-16">
<div className="flex justify-center mb-8">
<a href="/reserve" className="btn-primary px-8 py-3 rounded-full text-sm tracking-[0.5px]">
{t.reserve}
</a>
</div>
<div className="grid md:grid-cols-2 gap-8">
{/* Branch 1: Askim */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">RESTAURANT &amp; BUFFET</div>
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Kitchen Askim (Sisjön)</h2>
<div className="space-y-5 text-[15px]">
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
<div>Datavägen 10A<br />436 32 Askim, Gothenburg</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
<a href="tel:031288910" className="block hover:text-[#B38B4D]">031-28 89 10</a>
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
<div>Monday Sunday 11:00 21:00</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
<div className="text-[#6B665F]">Full restaurant experience with our popular lunch buffet, à la carte, and traditional Pakistani &amp; Indian dishes.</div>
</div>
</div>
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130.5!2d11.95!3d57.65!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sDatav%C3%A4gen%2010A%2C%20436%2032%20Askim!5e0!3m2!1sen!2sse!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
/>
</div>
</div>
{/* Branch 2: Backaplan */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">SWEETS, SNACKS &amp; CAFÉ</div>
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Sweets Backaplan</h2>
<div className="space-y-5 text-[15px]">
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
<div>Krokegårdsgatan 5<br />417 30 Göteborg (Backaplan)</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
<div>Monday Sunday 11:00 21:00</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
<div className="text-[#6B665F]">Fresh mithai (sweets), savory snacks, halwa puri, biryani, nihari, and café-style seating.</div>
</div>
</div>
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130!2d11.92!3d57.72!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sKrokeg%C3%A5rdsgatan%205%2C%20417%2030%20G%C3%B6teborg!5e0!3m2!1sen!2sse!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
/>
</div>
</div>
</div>
<div className="mt-12 text-center text-sm text-[#6B665F]">
Both branches are operated by the same team and share the same passion for authentic flavors.
</div>
</div>
<Footer />
</div>
);
}
+281
View File
@@ -0,0 +1,281 @@
'use client';
import { logoUrl } from "@/lib/assets";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
import { useState } from 'react';
import { motion, AnimatePresence } from "framer-motion";
import { User, Lock, MapPin, LogIn, Phone, MessageCircle } from "lucide-react";
/**
* =============================================================================
* STAFF LOGIN / AUTHENTICATION PAGE
* =============================================================================
*
* Beautiful, theme-matched placeholder login experience.
* - Fully language compatible (sv default + en/hi/ur)
* - No real backend yet — always shows friendly "under construction" message
* - Mobile-first: large tap targets, excellent spacing, easy form on phones
* - Desktop: elegant split layout with dark luxury graphic panel + form
* - Uses existing design tokens (gold accents, cream bg, rounded cards, serif titles)
*
* Future: when real auth is added, this will become the entry to a protected staff area
* (per-site menus, orders, inventory etc. for Askim vs Backaplan).
*/
export default function LoginPage() {
const { language } = useLanguage();
const t = getTranslation(language);
const [formData, setFormData] = useState({
username: '',
password: '',
site: '',
});
const [isSubmitted, setIsSubmitted] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Placeholder behaviour: always show the construction notice.
// No validation beyond native required fields — real auth coming later.
setIsSubmitted(true);
};
const resetForm = () => {
setFormData({ username: '', password: '', site: '' });
setIsSubmitted(false);
};
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main className="pb-20">
{/* Elegant header */}
<div className="max-w-5xl mx-auto px-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
STAFF PORTAL
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
{t.auth.title}
</h1>
<p className="mx-auto max-w-md text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
{t.auth.subtitle}
</p>
</div>
{/* Content area — beautiful split layout on desktop, clean single column on mobile */}
<div className="mx-auto max-w-5xl px-6 mt-10">
<div className="grid items-start gap-8 md:grid-cols-5">
{/* LEFT: Premium dark graphic panel (desktop only). Matches Shahi theme with gold details. */}
<div className="hidden md:col-span-2 md:block">
<div className="sticky top-[116px] flex h-[520px] flex-col justify-between overflow-hidden rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-10 text-white shadow-2xl">
{/* Subtle gold frame lines */}
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e] to-transparent" />
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e]/50 to-transparent" />
<div>
<div className="mb-8 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur">
<img
src={logoUrl()}
alt="Shahi Kitchen"
className="h-full w-full object-contain"
/>
</div>
<div className="font-serif text-[42px] leading-[0.95] tracking-[-1.8px]">
Shahi<br />Kitchen
</div>
<div className="mt-1 text-sm font-medium tracking-[2.5px] text-[#c99a2e]">
STAFF ACCESS
</div>
</div>
<div>
<div className="my-6 h-px w-10 bg-[#c99a2e]/40" />
<p className="max-w-[220px] text-sm leading-relaxed text-white/70">
Two locations serving Shahi Indian &amp; Pakistani hospitality in Gothenburg since 2016.
</p>
<div className="mt-5 text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">
ASKIM BACKAPLAN
</div>
</div>
</div>
</div>
{/* RIGHT / MOBILE: The actual elegant login card */}
<div className="md:col-span-3">
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-8 shadow-xl md:p-10">
{/* Mobile-only logo accent for visual continuity */}
<div className="mb-6 flex justify-center md:hidden">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-[#F8F5F0] p-2.5">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div>
</div>
<AnimatePresence mode="wait">
{!isSubmitted ? (
<motion.div
key="login-form"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Username */}
<div>
<label htmlFor="username" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
{t.auth.usernameLabel}
</label>
<div className="relative">
<User className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
placeholder={t.auth.usernamePlaceholder}
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"
/>
</div>
</div>
{/* Password */}
<div>
<label htmlFor="password" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
{t.auth.passwordLabel}
</label>
<div className="relative">
<Lock className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder={t.auth.passwordPlaceholder}
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"
/>
</div>
</div>
{/* Site / Location dropdown — Askim or Backaplan */}
<div>
<label htmlFor="site" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
{t.auth.siteLabel}
</label>
<div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select
id="site"
name="site"
value={formData.site}
onChange={handleChange}
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"
>
<option value="">{t.auth.sitePlaceholder}</option>
<option value="askim">{t.auth.askim}</option>
<option value="backaplan">{t.auth.backaplan}</option>
</select>
{/* Custom dropdown arrow for polish */}
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
<p className="mt-1.5 text-[11px] text-[#8A8478]">Select the location you are working at today.</p>
</div>
{/* Primary gold action button — large & mobile friendly */}
<button
type="submit"
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
>
<LogIn className="h-5 w-5" />
{t.auth.submit}
</button>
</form>
<p className="mt-8 text-center text-[11px] tracking-wide text-[#8A8478]">
{t.auth.footerNote}
</p>
</motion.div>
) : (
/* Beautiful result state — always shows the construction notice */
<motion.div
key="construction-result"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-[#B38B4D]/10">
<LogIn className="h-8 w-8 text-[#B38B4D]" />
</div>
<h2 className="mb-3 font-serif text-3xl tracking-[-1px]">
{t.auth.construction.title}
</h2>
<p className="mx-auto max-w-md text-[15px] leading-relaxed text-[#6B665F]">
{t.auth.construction.message}
</p>
{/* Helpful contact actions (real numbers from the site) */}
<div className="mt-8 space-y-3">
<a
href="tel:031288910"
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-[#EDE6D9] py-3.5 text-sm font-medium transition active:bg-[#F8F5F0] hover:bg-[#F8F5F0]"
>
<Phone className="h-4 w-4" />
{t.auth.construction.contact} 031-28 89 10
</a>
<a
href="https://wa.me/46739381089"
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-[#EDE6D9] py-3.5 text-sm font-medium transition active:bg-[#F8F5F0] hover:bg-[#F8F5F0]"
>
<MessageCircle className="h-4 w-4" />
WhatsApp +46 73 938 10 89
</a>
<button
onClick={resetForm}
className="mt-2 w-full rounded-2xl bg-[#101724] py-3.5 text-sm font-semibold text-white active:bg-black transition"
>
{t.auth.construction.tryAgain}
</button>
</div>
<p className="mt-6 text-[10px] text-[#8A8478]">
We appreciate your patience while we build a seamless experience.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Small trust line */}
<p className="mt-5 text-center text-[10px] text-[#8A8478] tracking-[1px]">
SHAHI KITCHEN GOTHENBURG EST 2016
</p>
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
+418
View File
@@ -0,0 +1,418 @@
"use client";
/**
* MENU PAGE — Premium Sidebar Navigation
*/
import { useEffect, useRef, useState, useMemo } from "react";
import { menuCategories } from "@/lib/menu-data";
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
import type { MenuItem } from "@/domain/menu/entities";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { motion } from "framer-motion";
import Navbar from "@/components/Navbar";
import PageHeader from "@/components/PageHeader";
import Footer from "@/components/Footer";
import { useCart } from "@/components/CartContext";
import WishlistButton from "@/components/WishlistButton";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
import {
dishImageUrl,
getMenuPosterSrc,
getMenuPosterCandidates,
applyNextImageFallback,
} from "@/lib/assets";
import { playHoverSound } from "@/lib/sounds";
import DishDetailsModal from "@/components/DishDetailsModal";
import { hasDishDetailModal } from "@/domain/menu/dish-details";
gsap.registerPlugin(ScrollTrigger);
export default function MenuPage() {
const [activeCategory, setActiveCategory] = useState("All");
const [searchQuery, setSearchQuery] = useState("");
const [showVegetarianOnly, setShowVegetarianOnly] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [detailItem, setDetailItem] = useState<MenuItem | null>(null);
const categoryListRef = useRef<HTMLDivElement>(null);
const { addToCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
// Sidebar categories
const sidebarCategories = [
{ id: "All", name: t.menu.allDishes || "All Dishes" },
...menuCategories.map((cat) => ({ id: cat.id, name: cat.name })),
];
// Filtering logic
const filteredCategories = useMemo(() => {
return menuCategories
.map((category) => {
let items = category.items;
// Sidebar filter
if (activeCategory !== "All" && category.id !== activeCategory) {
items = [];
}
// Search
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase().trim();
items = items.filter(
(item) =>
item.name.toLowerCase().includes(q) ||
(item.description && item.description.toLowerCase().includes(q))
);
}
// Vegetarian
if (showVegetarianOnly) {
items = items.filter((item) => item.isVegetarian);
}
return { ...category, items };
})
.filter((category) => category.items.length > 0);
}, [searchQuery, showVegetarianOnly, activeCategory]);
const handleCategorySelect = (id: string) => {
setActiveCategory(id);
window.scrollTo({ top: 220, behavior: "smooth" });
};
const getItemDescription = (item: MenuItem) =>
(t as { menuDescriptions?: Record<string, string> }).menuDescriptions?.[item.id] ||
item.description ||
"";
const openDishDetails = (item: MenuItem) => {
setDetailItem(item);
};
/** Detail-modal dishes use their photo on the card; video plays only in the modal. */
const getCardImageSrc = (item: MenuItem, categoryId: string) =>
hasDishDetailModal(item.id, categoryId) && item.image
? dishImageUrl(item.image)
: getMenuPosterSrc(item);
const getCardImageCandidates = (item: MenuItem, categoryId: string) =>
hasDishDetailModal(item.id, categoryId) && item.image
? [dishImageUrl(item.image), ...getMenuPosterCandidates(item)]
: getMenuPosterCandidates(item);
const renderMenuPrice = (item: MenuItem) => {
if (item.pricing === "weight") {
return (
<div className="shrink-0 text-right">
<div className="text-[#B38B4D] text-xl font-medium tracking-[-0.5px] tabular-nums leading-tight">
{item.pricePerHalfKg ?? item.price}
</div>
<div className="text-[10px] text-[#8A8478] tracking-wide">{t.menu.perHalfKg}</div>
<div className="text-[#B38B4D] text-sm font-medium tabular-nums mt-0.5">
{item.pricePerKg} {t.menu.perKg}
</div>
</div>
);
}
return (
<div className="shrink-0 text-right">
<div className="text-[#B38B4D] text-2xl font-medium tracking-[-0.5px] tabular-nums">
{item.price}
</div>
<div className="text-[10px] text-[#8A8478] tracking-widest -mt-1">KR</div>
</div>
);
};
// isMobile detection - used to enable advanced desktop hover effects + sounds only on desktop
useEffect(() => {
const updateMobile = () => setIsMobile(window.innerWidth < 768);
updateMobile();
window.addEventListener("resize", updateMobile);
return () => window.removeEventListener("resize", updateMobile);
}, []);
useEffect(() => {
const container = categoryListRef.current;
if (!container) return;
const activeButton = container.querySelector<HTMLElement>(
`[data-category-id="${activeCategory}"]`
);
if (!activeButton) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
activeButton.scrollIntoView({
behavior: prefersReducedMotion ? "auto" : "smooth",
block: "nearest",
inline: "center",
});
}, [activeCategory]);
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<PageHeader
eyebrow="AUTHENTIC • GENEROUS • ROYAL"
title={t.menu.title}
subtitle={t.menu.subtitle}
/>
{/* Main Layout */}
<div className="max-w-7xl mx-auto px-6 pb-20">
<div className="flex flex-col lg:flex-row gap-10">
{/* BEAUTIFUL SIDEBAR */}
<div className="lg:w-72 flex-shrink-0">
<div className="sticky top-[116px]">
<div className="mb-6">
<div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div>
<h3 className="font-serif text-3xl tracking-tight">Signature Categories</h3>
</div>
{/* Smooth scroll: horizontal on mobile, vertical on desktop when list exceeds viewport */}
<div className="relative">
<div
ref={categoryListRef}
className="menu-category-scroll flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth overscroll-x-contain overscroll-y-contain pb-3 pr-1 snap-x snap-mandatory scroll-px-1 [-webkit-overflow-scrolling:touch] lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:max-h-[calc(100dvh-13.5rem)] lg:snap-none lg:scroll-px-0 lg:pb-2 lg:pr-2"
>
{sidebarCategories.map((cat) => {
const isActive = activeCategory === cat.id;
const itemCount = cat.id === "All"
? menuCategories.reduce((sum, c) => sum + c.items.length, 0)
: menuCategories.find(c => c.id === cat.id)?.items.length || 0;
return (
<button
key={cat.id}
data-category-id={cat.id}
onClick={() => handleCategorySelect(cat.id)}
className={`flex-shrink-0 snap-start min-w-[140px] lg:min-w-0 lg:w-full flex items-center justify-between px-5 py-3 lg:py-3.5 rounded-2xl text-left transition-all active:scale-[0.985] group ${
isActive
? "bg-[#101724] text-white shadow-lg"
: "hover:bg-white hover:shadow-sm active:bg-[#EDE6D9] text-[#101724] border border-transparent hover:border-[#e5e1d7]"
}`}
>
<span className={`font-medium tracking-[-0.1px] text-sm lg:text-base ${isActive ? "" : "group-hover:text-[#0f5a4a]"}`}>
{cat.name}
</span>
<span className={`text-[10px] lg:text-xs px-2 py-0.5 rounded-full font-mono tabular-nums ${
isActive ? "bg-white/20" : "bg-[#e5e1d7] text-[#68717f]"
}`}>
{itemCount}
</span>
</button>
);
})}
</div>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-[#F8F5F0] to-transparent lg:hidden"
/>
</div>
</div>
</div>
{/* MAIN CONTENT */}
<div className="flex-1 min-w-0">
{/* Search + Vegetarian Filter */}
<div className="mb-8 flex flex-col md:flex-row gap-4 items-center">
<input
type="text"
placeholder={t.menu.searchPlaceholder}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full md:w-80 rounded-2xl border border-[#e5e1d7] bg-white px-5 py-3 text-sm placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20 transition-all"
/>
<button
onClick={() => setShowVegetarianOnly(!showVegetarianOnly)}
className={`px-6 py-3 rounded-2xl text-sm font-medium border transition-all active:scale-[0.985] ${
showVegetarianOnly
? "bg-[#0f5a4a] text-white border-[#0f5a4a]"
: "border-[#e5e1d7] hover:border-[#c99a2e] text-[#101724] bg-white"
}`}
>
{showVegetarianOnly ? t.menu.vegetarianOnly : t.menu.showVegetarian}
</button>
{(searchQuery || showVegetarianOnly || activeCategory !== "All") && (
<button
onClick={() => {
setSearchQuery("");
setShowVegetarianOnly(false);
setActiveCategory("All");
}}
className="text-sm font-medium text-[#c99a2e] hover:text-[#8f6b22] active:underline"
>
{t.menu.clearFilters}
</button>
)}
</div>
{/* Menu Items */}
{filteredCategories.length === 0 ? (
<div className="text-center py-16 text-[#6B665F]">
{t.menu.noResults}
</div>
) : (
filteredCategories.map((category) => (
<div key={category.id} className="mb-16">
<div className="flex items-center gap-4 mb-7">
<div className="text-3xl tracking-[-1px] text-[#101724]">{category.name}</div>
<div className="flex-1 h-px bg-gradient-to-r from-[#e5e1d7] to-transparent" />
<div className="text-sm font-medium text-[#68717f] tracking-widest">
{category.items.length} {t.menu.dishes}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{category.items.map((item) => {
const usesDetailModal = hasDishDetailModal(item.id, category.id);
return (
<motion.div
key={item.id}
data-id={item.id}
className={`menu-card group bg-white border border-[#EDE6D9] rounded-2xl overflow-hidden flex flex-col hover:border-[#c99a2e]/40 active:border-[#c99a2e] active:scale-[0.985] transition-all duration-150 touch-manipulation ${
usesDetailModal ? "" : "cursor-pointer"
}`}
onClick={
usesDetailModal
? undefined
: () => addToCart(buildCartLineFromMenuItem(item))
}
whileHover={!isMobile ? {
y: -4,
boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
transition: { type: "spring", stiffness: 300, damping: 20 }
} : {}}
whileTap={{ scale: 0.985 }}
>
{/* Media - Always poster for performance. Advanced desktop hover effect (scale + lift + glow) + subtle sound. Mobile uses simple scale. */}
<div
className={`relative h-52 overflow-hidden bg-[#F2EDE4] ${
usesDetailModal ? "cursor-pointer" : ""
}`}
onClick={
usesDetailModal
? (e) => {
e.stopPropagation();
openDishDetails(item);
}
: undefined
}
onMouseEnter={() => {
if (!isMobile) playHoverSound();
}}
>
<motion.img
src={getCardImageSrc(item, category.id)}
alt={item.name}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
whileHover={!isMobile ? {
scale: 1.08,
y: -3,
transition: { type: "spring", stiffness: 260, damping: 20 }
} : {}}
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getCardImageCandidates(item, category.id),
(e.target as HTMLImageElement).src
);
}}
/>
{/* Subtle gradient + hover shine overlay for advanced desktop feel */}
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-black/10 to-black/25" />
{!isMobile && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
)}
<div className="absolute top-3 right-3 z-10">
<WishlistButton
item={{ id: item.id, name: item.name, price: item.price, image: item.image }}
size="sm"
/>
</div>
</div>
<div className="p-6 flex flex-col flex-1">
<div className="flex justify-between items-start gap-4 mb-4">
<h3 className="text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26]">
{item.name}
</h3>
{renderMenuPrice(item)}
</div>
{usesDetailModal ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
openDishDetails(item);
}}
className="text-left text-[10px] text-[#8A8478] tracking-wide hover:text-[#B38B4D] hover:underline underline-offset-2 transition-colors mb-4"
>
{t.menu.seeDetails}
</button>
) : (
item.description && (
<p className="text-[#6B665F] text-[15px] leading-relaxed mb-4">
{getItemDescription(item)}
</p>
)
)}
<div className="mt-auto space-y-3">
{item.isVegetarian && (
<span className="inline-block text-xs px-3 py-1 rounded-full bg-[#3F5C4A]/10 text-[#3F5C4A] tracking-wider">
VEGETARIAN
</span>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
addToCart(buildCartLineFromMenuItem(item));
}}
className="w-full py-3.5 text-sm tracking-[0.6px] border border-[#B38B4D] text-[#B38B4D] rounded-full hover:bg-[#B38B4D] hover:text-white active:bg-[#8C6B3A] active:text-white active:scale-[0.985] font-medium transition-all touch-manipulation"
>
{t.wishlistDrawer.addToCart}
</button>
</div>
</div>
</motion.div>
);
})}
</div>
</div>
))
)}
</div>
</div>
</div>
<Footer />
{detailItem && (
<DishDetailsModal
item={detailItem}
description={getItemDescription(detailItem)}
isOpen={!!detailItem}
onClose={() => setDetailItem(null)}
labels={{
ingredients: t.menu.ingredients,
close: t.menu.closeDetails,
}}
/>
)}
</div>
);
}
+454
View File
@@ -0,0 +1,454 @@
'use client';
/**
* /orderfromtable — Single reusable table ordering page (QR driven)
*
* This page is the ONLY customer-facing order page for physical tables.
* All 80 table QR codes point here with ?table=backaplan-001 etc.
*
* - Strict validation of table param (branch + 001-040)
* - Local item selection (independent of global cart)
* - On "Send order" we only show confirmation (no backend call yet)
* - Payload is prepared exactly in the shape the future POST /api/table-orders will expect
*/
import React, { useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { filterMenuItems } from '@/application/menu/filter-menu';
import { getMenuPosterSrc, logoUrl, SITE_ASSETS } from '@/lib/assets';
import { buildTableOrderPayload } from '@/application/table-order/table-order-use-cases';
import { parseTableId } from '@/domain/table/table-id';
import {
addOrderLine,
calculateLineTotal,
calculateOrderTotal,
formatLineQuantityLabel,
removeOrderLine,
updateOrderLineQuantity,
type OrderLine,
} from '@/domain/shared/order-line';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
import { container } from '@/infrastructure/di/container';
import type { MenuItem } from '@/domain/menu/entities';
import { playAddSound, playSuccessSound } from '@/infrastructure/audio/web-audio-sound-adapter';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import { Search, Plus, Minus, X, CheckCircle, UtensilsCrossed } from 'lucide-react';
export default function OrderFromTablePage() {
const searchParams = useSearchParams();
const tableParam = searchParams.get('table');
const { language } = useLanguage();
const t = getTranslation(language);
const to = t.tableOrder;
const parsed = useMemo(() => parseTableId(tableParam), [tableParam]);
const allMenuItems = container.menuRepository.getAllItems();
// Local order state (NOT the global cart)
const [selected, setSelected] = useState<OrderLine[]>([]);
const [search, setSearch] = useState('');
const [vegOnly, setVegOnly] = useState(false);
const [notes, setNotes] = useState('');
const [isSending, setIsSending] = useState(false);
const [sent, setSent] = useState(false);
const isValid = parsed.valid;
const branchLabel = isValid
? (parsed.branch === 'askim' ? to.branchAskim : to.branchBackaplan)
: '';
const tableDisplay = isValid ? parsed.tableNumber : '';
// Filtered dishes for the selector grid (same data source as everywhere else)
const filteredDishes = useMemo(
() =>
filterMenuItems(allMenuItems, {
searchQuery: search,
vegetarianOnly: vegOnly,
}),
[allMenuItems, search, vegOnly]
);
const selectedTotal = calculateOrderTotal(selected);
function addDish(dish: MenuItem) {
playAddSound();
setSelected((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
}
function updateQty(id: string, newQty: number) {
setSelected((prev) => updateOrderLineQuantity(prev, id, newQty));
}
function removeItem(id: string) {
setSelected((prev) => removeOrderLine(prev, id));
}
function clearSelection() {
setSelected([]);
setNotes('');
}
async function handleSendOrder() {
if (!isValid || selected.length === 0) return;
setIsSending(true);
const orderPayload = buildTableOrderPayload(
parsed.branch,
parsed.tableNumber,
parsed.tableIdentifier,
selected,
notes
);
// For now we do NOT call any backend.
// This log + comment makes future integration trivial.
// TODO: replace the block below with real fetch when backend is ready.
console.log('[TABLE ORDER] Prepared payload (will be POSTed to /api/table-orders):', orderPayload);
// Simulate a tiny delay for nice UX (feels like it is being sent)
await new Promise((r) => setTimeout(r, 420));
playSuccessSound();
setIsSending(false);
setSent(true);
}
function resetForAnotherOrder() {
setSent(false);
setSelected([]);
setNotes('');
setSearch('');
setVegOnly(false);
}
// ====================== INVALID QR STATE ======================
if (!isValid) {
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="flex min-h-[70vh] items-center justify-center px-6">
<div className="max-w-md text-center">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-red-100/70">
<X className="h-10 w-10 text-red-600" />
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-3">{to.invalidTitle}</h1>
<p className="text-sm md:text-[15px] text-[#6B665F] mb-8">{to.invalidMessage}</p>
<a
href="/"
className="inline-flex items-center gap-2 rounded-full bg-[#101724] px-8 py-3 text-sm font-semibold text-white active:bg-black"
>
Go to homepage
</a>
</div>
</div>
<Footer />
</div>
);
}
// ====================== CONFIRMATION STATE ======================
if (sent) {
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="flex min-h-[70vh] items-center justify-center px-6">
<motion.div
initial={{ opacity: 0, scale: 0.96, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="max-w-lg text-center"
>
<div className="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-[#B38B4D]/10">
<CheckCircle className="h-14 w-14 text-[#B38B4D]" />
</div>
<div className="mb-2 text-sm tracking-[3px] text-[#B38B4D] font-medium">
{branchLabel.toUpperCase()} {to.tableLabel.toUpperCase()} {tableDisplay}
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-4">{to.confirmationTitle}</h1>
<p className="text-sm md:text-[15px] text-[#6B665F] leading-relaxed mb-10">
{to.confirmationMessage}
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button
onClick={resetForAnotherOrder}
className="btn-primary px-10 py-4 rounded-2xl text-base font-semibold tracking-[0.5px] active:scale-[0.985]"
>
{to.orderAnother}
</button>
<a
href="/menu"
className="btn-outline px-10 py-4 rounded-2xl text-base font-semibold tracking-[0.5px]"
>
Browse full menu
</a>
</div>
<p className="mt-8 text-xs text-[#8A8478]">
{branchLabel} {to.tableLabel} {tableDisplay}
</p>
</motion.div>
</div>
<Footer />
</div>
);
}
// ====================== MAIN ORDERING UI ======================
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
{/* Table header bar */}
<div className="sticky top-[116px] z-40 border-b border-[#EDE6D9] bg-[#F8F5F0]/95 backdrop-blur-xl">
<div className="max-w-6xl mx-auto px-6 py-4 flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3">
<div className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#c99a2e]/30 bg-white p-1.5">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div>
<div>
<div className="font-serif text-2xl tracking-[-1px] leading-none">{branchLabel}</div>
<div className="text-[#B38B4D] text-sm font-medium tracking-[1px] mt-0.5">
{to.tableLabel} {tableDisplay}
</div>
</div>
</div>
<div className="sm:ml-auto flex items-center gap-2 text-xs uppercase tracking-[2px] text-[#8A8478]">
<UtensilsCrossed className="h-3.5 w-3.5" />
ORDER FROM TABLE
</div>
</div>
</div>
<div className="max-w-6xl mx-auto px-6 pt-8 pb-24">
{/* Intro */}
<div className="mb-8">
<div className="text-[#B38B4D] text-xs tracking-[3.5px] mb-2 font-medium">{to.tableLabel.toUpperCase()} {tableDisplay}</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">{to.selectTitle}</h1>
<p className="text-sm md:text-[15px] text-[#6B665F] max-w-xl leading-relaxed">{to.selectSubtitle}</p>
</div>
<div className="grid lg:grid-cols-12 gap-8">
{/* DISH SELECTOR */}
<div className="lg:col-span-7">
{/* Filters */}
<div className="mb-5 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="relative flex-1">
<Search className="pointer-events-none absolute left-4 top-3.5 h-4 w-4 text-[#B38B4D]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
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"
/>
</div>
<button
onClick={() => setVegOnly(!vegOnly)}
className={`px-5 py-3 rounded-2xl text-sm font-medium border transition active:scale-[0.985] whitespace-nowrap ${
vegOnly
? 'bg-[#0f5a4a] text-white border-[#0f5a4a]'
: 'border-[#EDE6D9] bg-white text-[#101724] hover:border-[#c99a2e]'
}`}
>
{vegOnly ? '✓ ' : ''}{to.showVegetarian}
</button>
</div>
{/* Dish grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<AnimatePresence>
{filteredDishes.length > 0 ? (
filteredDishes.map((dish) => {
const inOrder = selected.some((s) => s.id === dish.id);
const poster = getMenuPosterSrc(dish, 'optimized');
return (
<motion.button
key={dish.id}
whileTap={{ scale: 0.985 }}
onClick={() => addDish(dish)}
className="group text-left flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-3 active:border-[#B38B4D] hover:border-[#c99a2e]/50 transition overflow-hidden"
>
<div className="relative h-20 w-20 flex-shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]">
<img
src={poster}
alt={dish.name}
className="absolute inset-0 h-full w-full object-cover group-active:scale-105 transition"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement;
target.src = SITE_ASSETS.dishes.defaultPoster;
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 to-black/20" />
</div>
<div className="flex min-w-0 flex-1 flex-col py-0.5">
<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">
{dish.name}
</div>
<div className="shrink-0 text-right font-semibold text-[#B38B4D] tabular-nums text-sm leading-tight">
{dish.pricing === 'weight' ? (
<>
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
<span className="block text-[10px] font-normal text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
</>
) : (
<>
{dish.price}
<span className="text-[10px] font-normal ml-px">kr</span>
</>
)}
</div>
</div>
{dish.description && (
<div className="mt-1 text-xs text-[#6B665F] line-clamp-2">
{dish.description}
</div>
)}
<div className="mt-auto pt-2">
<span className="inline-flex items-center gap-1 text-xs font-medium text-[#B38B4D] group-active:text-[#8C6B3A]">
<Plus className="h-3.5 w-3.5" />
{inOrder ? 'Add another' : 'Add to order'}
</span>
</div>
</div>
</motion.button>
);
})
) : (
<div className="col-span-full py-12 text-center text-[#6B665F]">
No dishes match your search.
</div>
)}
</AnimatePresence>
</div>
</div>
{/* ORDER SUMMARY (sticky on desktop) */}
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-24">
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="font-semibold tracking-tight text-lg">{to.yourOrder}</div>
{selected.length > 0 && (
<button
onClick={clearSelection}
className="text-xs text-[#B38B4D] hover:underline active:text-[#8C6B3A]"
>
Clear all
</button>
)}
</div>
{selected.length === 0 ? (
<div className="py-8 text-center">
<div className="text-5xl mb-3 opacity-50">🍽</div>
<p className="text-[#6B665F] text-sm">{to.emptyOrder}</p>
</div>
) : (
<div className="space-y-3 mb-5 max-h-[320px] overflow-auto pr-1">
{selected.map((item) => (
<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="font-medium text-[15px] tracking-[-0.2px]">{item.name}</div>
<div className="text-xs text-[#6B665F] tabular-nums">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
: `${item.price} kr × ${item.quantity}`}
{' '}= {calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-1.5">
<button
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"
aria-label="Decrease"
>
</button>
<span className="min-w-12 text-center font-medium tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button
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"
aria-label="Increase"
>
+
</button>
<button
onClick={() => removeItem(item.id)}
className="ml-1.5 text-[#B38B4D] p-1 active:text-red-600"
aria-label="Remove"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
)}
{/* Notes */}
<div className="mt-2">
<label className="block text-xs font-semibold tracking-widest text-[#6B665F] mb-1.5">
{to.notesLabel}
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder={to.notesPlaceholder}
rows={2}
className="w-full resize-y rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] p-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 outline-none"
/>
</div>
{/* Total + Send */}
<div className="mt-5 border-t border-[#EDE6D9] pt-5">
<div className="flex items-baseline justify-between text-lg mb-4">
<span className="text-[#6B665F]">{to.total}</span>
<span className="font-semibold tabular-nums">{selectedTotal} kr</span>
</div>
<button
onClick={handleSendOrder}
disabled={selected.length === 0 || isSending}
className="w-full rounded-2xl bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] py-4 text-base font-semibold tracking-[0.6px] text-white shadow-lg active:brightness-95 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isSending ? 'Sending...' : to.sendOrder}
</button>
<p className="mt-3 text-center text-[11px] text-[#8A8478]">
Staff will come to your table to confirm.
</p>
</div>
</div>
{/* Small context line */}
<div className="mt-3 text-center text-xs text-[#8A8478] tracking-wider">
{branchLabel} {to.tableLabel} {tableDisplay}
</div>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { Suspense } from 'react';
import Navbar from '@/components/Navbar';
import OrderFromTableClient from './OrderFromTableClient';
/**
* Server wrapper for /orderfromtable
*
* We must wrap any component that uses useSearchParams() in a Suspense boundary
* (Next.js 16 requirement for pages that read search params).
*
* This keeps the actual ordering UI fully client-side while satisfying the framework.
*/
export default function OrderFromTablePage() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="flex min-h-[50vh] items-center justify-center px-6">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-pulse rounded-full bg-[#B38B4D]/30" />
<p className="text-sm tracking-widest text-[#8A8478]">Loading table...</p>
</div>
</div>
</div>
}
>
<OrderFromTableClient />
</Suspense>
);
}
+191
View File
@@ -0,0 +1,191 @@
"use client";
/**
* SHAHI KITCHEN HOMEPAGE
* Version with advanced Signature Menu + elegant hero (pre full Sultan redesign)
*/
import React, { useEffect, useMemo, useState } from "react";
import Lenis from "lenis";
import { ArrowRight, UtensilsCrossed } from "lucide-react";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { useCart } from "@/components/CartContext";
import { useLanguage } from "@/lib/language-context";
import { heroBannerSources } from "@/lib/assets";
import { menuCategories } from "@/lib/menu-data";
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
import type { MenuItem } from "@/domain/menu/entities";
import { getTranslation } from "@/lib/translations";
import SignatureMenuMarquee, { type SignatureDish } from "@/components/SignatureMenuMarquee";
type SignatureFilter = "All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweets";
type HomepageDish = SignatureDish & { filterTag: SignatureFilter | null };
export default function ShahiKitchenHomepage() {
const { addToCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
const [menuFilter, setMenuFilter] = useState<SignatureFilter>("All");
// Lenis smooth scroll
useEffect(() => {
const lenis = new Lenis({
duration: 1.1,
easing: (t: number) => Math.min(1, 1.001 * (-Math.pow(2, -10 * t) + 1)),
smoothWheel: true,
});
const raf = (time: number) => { lenis.raf(time); requestAnimationFrame(raf); };
requestAnimationFrame(raf);
return () => lenis.destroy();
}, []);
const getDishFilterTag = (item: MenuItem, categoryId: string): SignatureFilter | null => {
if (categoryId === "sweets") return "Sweets";
if (categoryId === "street-food") return "Street";
if (categoryId === "naan-roll") return "Roll";
if (categoryId === "meat") return "Meat";
if (item.id.includes("biryani")) return "Rice";
if (categoryId === "vegetarian" || categoryId === "chicken") return "Curry";
return null;
};
const allDishes = useMemo<HomepageDish[]>(
() =>
menuCategories.flatMap((category) =>
category.items.map((item) => ({
...item,
desc: item.description ?? "",
filterTag: getDishFilterTag(item, category.id),
}))
),
[]
);
const filtered = useMemo(() => {
if (menuFilter === "All") return allDishes;
return allDishes.filter((dish) => dish.filterTag === menuFilter);
}, [allDishes, menuFilter]);
const banner = heroBannerSources();
const addDish = (d: SignatureDish) => {
addToCart(buildCartLineFromMenuItem(d));
};
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
{/* HERO - Full Banner Video (no text overlay) */}
<section
className="relative min-h-[100dvh] overflow-hidden bg-[#fbf7ef]"
aria-label="Hero"
>
{/* Banner Video - Responsive sources for proper mobile/iPad visualization (portrait asset) + desktop (landscape).
Mobile/iPad (<=1024px) uses the compressed banner_mobile-optimized versions (WebM primary for size/speed, MP4 fallback).
These were generated from the provided banner_mobile.mp4 (original kept as source asset). */}
<video
autoPlay
muted
playsInline
preload="auto"
className="absolute inset-0 z-0 w-full h-full object-cover
object-[50%_10%] sm:object-[50%_12%] md:object-[50%_16%]
lg:object-[50%_20%] xl:object-[50%_24%]"
onError={(e) => console.error('Hero banner video failed to load', e)}
>
{/* Mobile + iPad (portrait-friendly framing for better visualization on small screens) */}
<source
src={banner.mobileWebm}
type="video/webm"
media="(max-width: 1024px)"
/>
<source
src={banner.mobileMp4}
type="video/mp4"
media="(max-width: 1024px)"
/>
{/* Desktop / large screens */}
<source src={banner.desktopMp4} type="video/mp4" />
</video>
{/* Cinematic overlays */}
<div className="absolute inset-0 z-[1] bg-gradient-to-b from-[#0f5a4a]/30 via-transparent to-[#101724]/50" />
<div className="absolute inset-0 z-[2] bg-[radial-gradient(ellipse_at_center,transparent_0%,rgba(16,23,36,0.25)_100%)]" />
</section>
{/* ADVANCED SIGNATURE MENU */}
<section id="menu" className="scroll-mt-header section-ornament bg-[#fffdf8] px-6 py-20 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10">
<div>
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-[#fff6dc] px-4 py-1.5 text-sm font-semibold text-[#60420d]">
<UtensilsCrossed className="h-4 w-4" /> {t.signatureMenu.title}
</div>
<h2 className="text-2xl sm:text-[1.75rem] md:text-3xl leading-[1.15] tracking-[-0.8px] break-words text-[#101724]">{t.signatureMenu.title}</h2>
</div>
<p className="max-w-md text-sm md:text-[15px] font-medium text-[#4b5563]">{t.signatureMenu.subtitle}</p>
</div>
{/* Filters - modern horizontal scroller on mobile for easy thumb use */}
<div className="mb-8 -mx-1 px-1 flex gap-2 overflow-x-auto snap-x snap-mandatory pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{(["All", "Rice", "Curry", "Meat", "Street", "Roll", "Sweets"] as const).map((cat) => (
<button
key={cat}
onClick={() => setMenuFilter(cat)}
className={`flex-shrink-0 snap-start rounded-full px-5 py-2.5 text-sm font-bold transition-all active:scale-[0.97] touch-manipulation ${menuFilter === cat ? "bg-[#101724] text-white" : "border border-[#e5e1d7] bg-white text-[#182235] hover:border-[#c99a2e] hover:bg-[#fff6dc] active:bg-[#fff6dc]"}`}
>
{(t.signatureMenu as any)[`filter${cat}`] || cat}
</button>
))}
</div>
<SignatureMenuMarquee
dishes={filtered}
addToCartLabel={t.wishlistDrawer.addToCart}
perHalfKgLabel={t.menu.perHalfKg}
perKgLabel={t.menu.perKg}
onAdd={addDish}
/>
<div className="mt-10 text-center">
<a href="/menu" className="inline-flex items-center gap-2 text-sm font-bold tracking-wider text-[#B38B4D] hover:text-[#8C6B3A]">
{t.signatureMenu.viewFullMenu} <ArrowRight className="h-4 w-4" />
</a>
</div>
</div>
</section>
{/* THE SHAHI EXPERIENCE */}
<section id="experience" className="scroll-mt-header section bg-white border-y border-[#EDE6D9]">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-14">
<div className="text-[#B38B4D] text-xs tracking-[3px] mb-3">{language === 'sv' ? 'SHAHI-SÄTTET' : 'THE SHAHI WAY'}</div>
<h3 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] whitespace-pre-line">{language === 'sv' ? 'Mer än en måltid.\nEtt ögonblick av Shahi.' : 'More than a meal.\nA moment of Shahi.'}</h3>
</div>
<div className="grid md:grid-cols-3 gap-6">
{(language === 'sv' ? [
{ title: "Den Legendariska Buffén", desc: "Vår berömda lunchbuffé har över 20 roterande rätter — curry, biryani, färsk naan och sötsaker." },
{ title: "Shahi Sötsaker", desc: "Hemgjord mithai dagligen. Från färsk jalebi till rasmalai — det perfekta söta avslutet." },
{ title: "Varm Gästfrihet", desc: "Oavsett om du är här för en snabb lunch eller familjefest, behandlas du alltid som shahi." },
] : [
{ title: "The Legendary Buffet", desc: "Our famous lunch buffet features over 20 rotating dishes — curries, biryanis, fresh naan, and sweets." },
{ title: "Shahi Sweets", desc: "Homemade mithai made daily. From fresh Jalebi to Rasmalai — the perfect sweet ending." },
{ title: "Warm Hospitality", desc: "Whether you're here for a quick lunch or a family celebration, you will always be treated like Shahi." },
]).map((item, index) => (
<div key={index} className="experience-card group relative border border-[#EDE6D9] p-9 rounded-2xl bg-[#F8F5F0] overflow-hidden">
<div className="text-[#B38B4D] text-6xl font-light mb-9 tracking-[-2px]">0{index + 1}</div>
<h4 className="text-[29px] tracking-[-0.8px] mb-5 text-[#2C2A26] leading-tight">{item.title}</h4>
<p className="text-[#6B665F] text-[15.5px] leading-relaxed">{item.desc}</p>
</div>
))}
</div>
</div>
</section>
<Footer />
</div>
);
}
+544
View File
@@ -0,0 +1,544 @@
'use client';
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { buildBookingWhatsAppMessage } from '@/application/messaging/whatsapp-message-builder';
import { isBookingComplete } from '@/domain/booking/validation';
import type { BookingDetails, PreOrderLine } from '@/domain/booking/entities';
import {
addOrderLine,
calculateLineTotal,
calculateOrderTotal,
formatLineQuantityLabel,
removeOrderLine,
updateOrderLineQuantity,
} from '@/domain/shared/order-line';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
import type { MenuItem } from '@/domain/menu/entities';
import {
getMenuPosterSrc,
getMenuPosterCandidates,
logoUrl,
applyNextImageFallback,
SITE_ASSETS,
} from '@/lib/assets';
import { container } from '@/infrastructure/di/container';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import { useState } from 'react';
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 { menuCategories } from "@/lib/menu-data";
import { playAddSound, playHoverSound, playSuccessSound } from "@/lib/sounds";
/**
* =============================================================================
* TABLE RESERVATION / BOOKING PAGE
* =============================================================================
*
* Beautiful, theme-matched table booking experience with optional pre-order.
* - Swap implemented: Reserve Table now in central nav pills, Login is the gold CTA.
* - Form first (location, date, time, guests, name, phone, email, notes).
* - After form: full menu selector for pre-ordering (reuse menu-data).
* - End: builds rich WhatsApp inquiry (booking + pre-order items) using same number as cart.
* - Fully translated (sv default + en/hi/ur).
* - Mobile-first, large targets, elegant cards matching /login and site theme.
* - Uses framer-motion for reveals, lucide icons.
* - No backend: client-side only, like staff login and cart WhatsApp.
*
* Linked from navbar (after swap) and locations.
*/
export default function ReservePage() {
const { language } = useLanguage();
const t = getTranslation(language);
const [booking, setBooking] = useState<BookingDetails>({
location: '',
date: '',
time: '',
guests: '',
name: '',
phone: '',
email: '',
notes: '',
});
const [preOrder, setPreOrder] = useState<PreOrderLine[]>([]);
const [showMenu, setShowMenu] = useState(false);
const [search, setSearch] = useState('');
const [sent, setSent] = useState(false);
const handleBookingChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setBooking((prev) => ({ ...prev, [name]: value }));
};
const validateForm = (): boolean => isBookingComplete(booking);
const handleContinue = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
setShowMenu(true);
// Smooth scroll to menu on mobile
setTimeout(() => {
const menuEl = document.getElementById('preorder-menu');
if (menuEl) menuEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
} else {
alert('Please fill in all required fields (location, date, time, guests, name, phone).');
}
};
// Flatten menu for selector (with search)
const allDishes = menuCategories.flatMap((cat) => cat.items);
const filteredDishes = allDishes.filter((dish) =>
dish.name.toLowerCase().includes(search.toLowerCase()) ||
(dish.description && dish.description.toLowerCase().includes(search.toLowerCase()))
);
const addToPreOrder = (dish: MenuItem) => {
setPreOrder((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
};
const updatePreOrderQty = (id: string, newQty: number) => {
setPreOrder((prev) => updateOrderLineQuantity(prev, id, newQty));
};
const removeFromPreOrder = (id: string) => {
setPreOrder((prev) => removeOrderLine(prev, id));
};
const getPreOrderTotal = () => calculateOrderTotal(preOrder);
const handleSendInquiry = () => {
if (!validateForm()) {
alert('Please complete the booking form first.');
return;
}
playSuccessSound();
const message = buildBookingWhatsAppMessage(booking, preOrder, {
askim: t.booking.askim,
backaplan: t.booking.backaplan,
});
if (!message) return;
container.messagingGateway.openWhatsApp(message);
setSent(true);
};
const resetAll = () => {
setBooking({ location: '', date: '', time: '', guests: '', name: '', phone: '', email: '', notes: '' });
setPreOrder([]);
setShowMenu(false);
setSearch('');
setSent(false);
};
const editBooking = () => {
setShowMenu(false);
setSent(false);
};
// Location options from translations
const locationOptions = [
{ value: 'askim', label: t.booking.askim },
{ value: 'backaplan', label: t.booking.backaplan },
];
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main className="pb-20">
{/* Elegant Header */}
<div className="max-w-5xl mx-auto px-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
EXCLUSIVE EXPERIENCE
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
{t.booking.title}
</h1>
<p className="mx-auto max-w-xl text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
{t.booking.subtitle}
</p>
</div>
<div className="max-w-5xl mx-auto px-6 mt-10">
{/* Decorative header info - now ABOVE the form (not on side) so form can be wider */}
<div className="mb-10 flex justify-center">
<div className="w-full max-w-2xl rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-6 sm:p-8 text-white shadow-2xl text-center">
<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" />
</div>
<div className="font-serif 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>
<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.
</p>
<div className="text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">ASKIM BACKAPLAN</div>
</div>
</div>
<AnimatePresence mode="wait">
{!sent ? (
<div className="max-w-4xl mx-auto space-y-8">
{/* 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">
<h2 className="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>
<form onSubmit={handleContinue} className="space-y-6">
{/* Location */}
<div>
<label htmlFor="location" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.locationLabel}</label>
<div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select
id="location"
name="location"
value={booking.location}
onChange={handleBookingChange}
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"
>
<option value="">{t.booking.locationPlaceholder}</option>
{locationOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Date */}
<div>
<label htmlFor="date" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.dateLabel}</label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="date"
name="date"
type="date"
value={booking.date}
onChange={handleBookingChange}
min={new Date().toISOString().split('T')[0]}
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"
/>
</div>
</div>
{/* Time */}
<div>
<label htmlFor="time" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.timeLabel}</label>
<div className="relative">
<Clock className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="time"
name="time"
type="time"
value={booking.time}
onChange={handleBookingChange}
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"
/>
</div>
</div>
</div>
{/* Guests */}
<div>
<label htmlFor="guests" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.guestsLabel}</label>
<div className="relative">
<Users className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<select
id="guests"
name="guests"
value={booking.guests}
onChange={handleBookingChange}
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"
>
<option value="">Select</option>
{[1,2,3,4,5,6,7,8,9,10].map(n => <option key={n} value={n}>{n}</option>)}
</select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
</div>
{/* Name */}
<div>
<label htmlFor="name" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.nameLabel}</label>
<div className="relative">
<User className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="name"
name="name"
type="text"
value={booking.name}
onChange={handleBookingChange}
placeholder={t.booking.namePlaceholder}
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"
/>
</div>
</div>
{/* Phone */}
<div>
<label htmlFor="phone" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.phoneLabel}</label>
<div className="relative">
<Phone className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="phone"
name="phone"
type="tel"
value={booking.phone}
onChange={handleBookingChange}
placeholder={t.booking.phonePlaceholder}
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"
/>
</div>
</div>
{/* Email */}
<div>
<label htmlFor="email" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.emailLabel}</label>
<div className="relative">
<Mail className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="email"
name="email"
type="email"
value={booking.email}
onChange={handleBookingChange}
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"
/>
</div>
</div>
{/* Notes */}
<div>
<label htmlFor="notes" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.notesLabel}</label>
<div className="relative">
<MessageCircle className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<textarea
id="notes"
name="notes"
value={booking.notes}
onChange={handleBookingChange}
placeholder={t.booking.notesPlaceholder}
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"
/>
</div>
</div>
<button
type="submit"
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
>
{t.booking.continueBtn}
<ArrowRight className="h-5 w-5" />
</button>
</form>
</div>
{/* Pre-Order Menu Section - revealed after form */}
<AnimatePresence>
{showMenu && (
<motion.div
id="preorder-menu"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
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>
<p className="text-[#6B665F] mb-6">{t.booking.menuSubtitle}</p>
{/* Search */}
<div className="relative mb-6">
<Search className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
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"
/>
</div>
{/* Dishes Grid - improved visualization: Name, Poster below, Desc under poster, price, nice Add button */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[320px] sm:max-h-[420px] overflow-auto pr-2">
{filteredDishes.length > 0 ? (
filteredDishes.map((dish) => {
const posterSrc = getMenuPosterSrc(dish);
const shortDesc = dish.description ? (dish.description.length > 80 ? dish.description.slice(0, 80) + '...' : dish.description) : '';
const isAdded = preOrder.some((p) => p.id === dish.id);
return (
<motion.div
key={dish.id}
whileHover={{ y: -2, boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" }}
className="flex flex-col border border-[#EDE6D9] rounded-2xl p-3 bg-white hover:border-[#B38B4D]/50 transition-all overflow-hidden"
onMouseEnter={() => playHoverSound()}
>
{/* Item Name */}
<div className="font-semibold tracking-[-0.3px] text-base mb-1 text-[#2C2A26] line-clamp-1">{dish.name}</div>
{/* 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]">
<img
src={posterSrc}
alt={dish.name}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getMenuPosterCandidates(dish),
(e.target as HTMLImageElement).src
);
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/10" />
</div>
{/* Description under the poster */}
<div className="text-xs text-[#6B665F] leading-snug mb-2 flex-1 line-clamp-2">{shortDesc}</div>
<div className="flex items-center justify-between mt-auto pt-1">
<span className="text-[#B38B4D] font-semibold text-sm tabular-nums leading-tight text-right">
{dish.pricing === 'weight' ? (
<>
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
<span className="block text-xs text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
</>
) : (
<span>{dish.price} kr</span>
)}
</span>
{/* Improved graphics for add button */}
<button
onClick={() => {
playAddSound();
addToPreOrder(dish);
}}
className={`flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold shadow-sm active:scale-[0.96] transition-all ${
isAdded
? "bg-[#3F5C4A] text-white hover:bg-[#2a4033]"
: "bg-[#B38B4D] text-white hover:bg-[#8f6b22] hover:shadow-md"
}`}
>
<Plus className="h-3.5 w-3.5" />
{isAdded ? "Add More" : t.booking.addToPreOrder}
</button>
</div>
</motion.div>
);
})
) : (
<p className="text-[#6B665F] col-span-full text-center py-8">No dishes match your search.</p>
)}
</div>
{/* Pre-Order Summary */}
<div className="mt-8 border-t border-[#EDE6D9] pt-6">
<h3 className="font-semibold tracking-wide mb-4 flex items-center gap-2">{t.booking.yourPreOrder} <span className="text-xs text-[#B38B4D] font-normal">({preOrder.length})</span></h3>
{preOrder.length === 0 ? (
<p className="text-sm text-[#6B665F] italic">{t.booking.emptyPreOrder}</p>
) : (
<div className="space-y-3 mb-4">
{preOrder.map((item) => {
const dishInfo = allDishes.find((d) => d.id === item.id);
const thumb = dishInfo ? getMenuPosterSrc(dishInfo) : SITE_ASSETS.fallbacks.logo;
return (
<div key={item.id} className="flex items-center gap-3 bg-[#F8F5F0] rounded-2xl px-3 py-2">
{/* Small poster thumbnail in summary for better visualization */}
<img
src={thumb}
alt={item.name}
className="w-10 h-10 object-cover rounded-lg border border-[#EDE6D9] flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{item.name}</div>
<div className="text-[10px] text-[#6B665F]">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
: `${item.price} kr × ${item.quantity}`}
{' '}= {calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<button onClick={() => updatePreOrderQty(item.id, item.quantity - 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">-</button>
<span className="min-w-10 text-center font-medium text-sm tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button onClick={() => updatePreOrderQty(item.id, item.quantity + 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">+</button>
<button onClick={() => removeFromPreOrder(item.id)} className="ml-1 text-[#B38B4D] hover:text-red-600 p-0.5 active:scale-95"><X className="h-4 w-4" /></button>
</div>
</div>
);
})}
</div>
)}
<div className="flex justify-between text-lg font-medium border-t border-[#EDE6D9] pt-4">
<span>{t.booking.total}</span>
<span>{getPreOrderTotal()} kr</span>
</div>
<button
onClick={handleSendInquiry}
disabled={!validateForm()}
className="mt-6 w-full py-4 rounded-2xl text-base font-semibold tracking-[0.5px] flex items-center justify-center gap-3 bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] text-white shadow-lg hover:brightness-105 active:brightness-95 active:scale-[0.985] transition-all disabled:opacity-60 disabled:cursor-not-allowed disabled:brightness-100"
>
{t.booking.sendInquiry}
<MessageCircle className="h-5 w-5" />
</button>
<p className="mt-3 text-center text-[11px] text-[#8A8478]">{t.booking.inquiryNote}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) : (
/* Success State */
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-md mx-auto text-center py-12"
>
<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]" />
</div>
<h2 className="text-4xl tracking-tight font-serif mb-4">{t.booking.successTitle}</h2>
<p className="text-[#6B665F] mb-8 leading-relaxed">{t.booking.successMessage}</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button onClick={editBooking} className="btn-outline px-8 py-3 rounded-full text-sm font-medium">
{t.booking.editBooking}
</button>
<button onClick={resetAll} className="btn-primary px-8 py-3 rounded-full text-sm font-medium">
{t.booking.backHome}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
</main>
<Footer />
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import ScreenDisplay from '@/components/ScreenDisplay';
export const metadata: Metadata = {
title: 'Shahi Kitchen | Display Screen 1',
description: 'In-restaurant menu visualization — Screen 1',
robots: {
index: false,
follow: false,
},
};
export default function Screen1Page() {
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
// Makes screen1 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
// Pairs with the .screen-root + useEffect in ScreenDisplay for robustness.
return (
<>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
}}
/>
<ScreenDisplay screen={1} />
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import ScreenDisplay from '@/components/ScreenDisplay';
export const metadata: Metadata = {
title: 'Shahi Kitchen | Display Screen 2',
description: 'In-restaurant menu visualization — Screen 2',
robots: {
index: false,
follow: false,
},
};
export default function Screen2Page() {
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
// Makes screen2 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
// Pairs with the .screen-root + useEffect in ScreenDisplay for robustness.
return (
<>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
}}
/>
<ScreenDisplay screen={2} />
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import OrderDisplay from '@/components/OrderDisplay';
export const metadata: Metadata = {
title: 'Shahi Kitchen | Live Order Display',
description: 'Live kitchen order board — in-house tables & online delivery (Foodora etc.) for customers and riders. Real-time status.',
robots: {
index: false,
follow: false,
},
};
export default function Screen3Page() {
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
// Makes screen3 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
// Pairs with .screen-root for the order board.
return (
<>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
}}
/>
<OrderDisplay />
</>
);
}