Replace entire repo content with kottgard-production-v1.1.zip (for shahikitchen-prod repo)
This commit is contained in:
@@ -1,107 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
-482
@@ -1,482 +0,0 @@
|
||||
/**
|
||||
* =============================================================================
|
||||
* 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
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* 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 & 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 & 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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
'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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
"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 {
|
||||
getMenuPosterSrc,
|
||||
getMenuPosterCandidates,
|
||||
applyNextImageFallback,
|
||||
} from "@/lib/assets";
|
||||
import { playHoverSound } from "@/lib/sounds";
|
||||
|
||||
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 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 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) => (
|
||||
<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 cursor-pointer touch-manipulation"
|
||||
onClick={() => 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] cursor-pointer"
|
||||
onMouseEnter={() => {
|
||||
if (!isMobile) playHoverSound();
|
||||
}}
|
||||
>
|
||||
<motion.img
|
||||
src={getMenuPosterSrc(item)}
|
||||
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,
|
||||
getMenuPosterCandidates(item),
|
||||
(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>
|
||||
|
||||
{item.description && (
|
||||
<p className="text-[#6B665F] text-[15px] leading-relaxed mb-4">
|
||||
{(t as any).menuDescriptions?.[item.id] || item.description}
|
||||
</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 />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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
@@ -1,191 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,544 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user