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

This commit is contained in:
root
2026-06-29 16:22:09 +00:00
parent 57cc67d2d3
commit 50e3a34895
723 changed files with 20055 additions and 26101 deletions
+26 -5
View File
@@ -1,8 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
@@ -22,13 +28,28 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts
# Developer / deployment instructions
# Contains sensitive production credentials (Gitea admin password, DB password, secrets, SSH keys, etc.)
# This file MUST NEVER be committed or pushed.
developer_instructions.txt
# Video optimization artifacts and temp files
# Generated by optimize-food-videos.sh, optimize-videos.sh, extract-video-posters.sh, ffmpeg passes etc.
# These should not pollute the working tree or be committed.
public/videos/*-optimized.*
public/videos/*-optimized.mp4
public/videos/*-optimized.webm
error*.jpeg
ffmpeg2pass*.log
+5
View File
@@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+82 -51
View File
@@ -1,69 +1,100 @@
# Kött Gård — Premium Halal Meat Delivery
# Shahikitchen — Animated Restaurant Website
A modern, premium e-commerce website for 100% Halal meat delivery. Built with Next.js, TypeScript, and Tailwind CSS.
This directory contains the official website for **Shahi Kitchen**, a popular Indian & Pakistani restaurant in Gothenburg (Göteborg), Sweden.
## Features
**Domain**: [shahikitchen.se](https://shahikitchen.se)
- **Premium Design** — Green, white, and gold color palette with luxurious typography
- **Multi-language** — Swedish (default), English, and Urdu with RTL support
- **Product Customization**
- Chicken: Choose quantity in pieces (4, 8, 10, 12, 14)
- Beef & Lamb: Choose cutting style (Nihari, Karahi, Qeema, Boneless, Steak)
- Fish: Standard professional cut
- **Full E-commerce Flow** — Shop, product details, cart, secure checkout
- **User Accounts** — Registration, login, order history
- **Wishlist** — Save favorite products
- **Responsive** — Mobile-first design
- **SEO Optimized** — Meta tags, semantic HTML, Open Graph
---
## Getting Started
## About Shahi Kitchen
```bash
npm install
npm run dev
```
Shahi Kitchen is an authentic Indian and Pakistani restaurant specializing in flavorful curries, tandoori dishes, biryanis, haleem, fresh naan, and traditional sweets. The restaurant is particularly well-known for its generous **lunch buffet**, which offers excellent value and is popular among locals in the Gothenburg area.
Open [http://localhost:3000](http://localhost:3000).
### Key Information
### Demo Account
| Detail | Information |
|---------------------|--------------------------------------------------|
| **Full Name** | Shahi Kitchen (Shahikitchen / Shahi Kitchen Sisjön) |
| **Cuisine** | Indian & Pakistani (Halal options available) |
| **Address** | Datavägen 10A, 436 32 Askim, Sweden |
| **Area** | Sisjön / Askim (suburb of Gothenburg) |
| **Phone** | 031-28 89 10 / 0739-381089 (Imran) |
| **Email** | hello@shahikitchen.se |
| **Opened** | Around 2016 |
| **Specialties** | Lunch buffet, Biryani, Lamb Karahi, Curries, Sweets (Shahi Sweets) |
- Email: `demo@kottgard.se`
- Password: `demo123`
### Current Online Presence
### Languages
- **Instagram**: [@shahikitchen](https://www.instagram.com/Shahikitchen/)
- **Facebook**: [facebook.com/shahikitchengbg](https://www.facebook.com/shahikitchengbg/)
- **Reviews**: ~4.1/5 on Google (600+ reviews)
- **Note**: As of 2026, the domain `shahikitchen.se` is parked. This project will give the restaurant a proper modern website.
Use the language bar or globe icon to switch between:
- **Svenska** (Swedish) — default
- **English**
- **اردو** (Urdu) — RTL
- **العربية** (Arabic) — RTL
- **فارسی** (Persian) — RTL
- **Türkçe** (Turkish)
---
## Tech Stack
- **Framework:** Next.js 13 (App Router)
- **Language:** TypeScript
- **Styling:** Tailwind CSS
- **State:** Zustand (persisted cart, auth, wishlist, locale)
- **i18n:** Custom translation system with locale store
- **Icons:** Lucide React
- **Next.js 15** (App Router) + TypeScript
- **Tailwind CSS**
- **GSAP** — for advanced scroll and UI animations
- **React Three Fiber** — for 3D experiences (dishes)
- **Spline** (optional) — for high-quality hosted 3D scenes
- **Framer Motion** (optional alternative to GSAP for simpler animations)
## Project Structure (Clean Architecture)
---
## Project Structure (Planned)
```
src/
├── domain/ # Entities + business rules (no React/Next)
├── application/ # Use cases, ports, container.ts
├── infrastructure/ # Zustand, product data, i18n adapters
├── presentation/ # useCart, useAuth, useCatalog hooks
├── app/ # Next.js pages (thin UI)
├── components/ # React UI components
├── i18n/ # Translation dictionaries (en, sv, ur)
├── hooks/ # useTranslation
├── lib/ # Backward-compatible facades
├── store/ # Backward-compatible re-exports
└── types/ # Re-exports domain entities
shahikitchen/
├── app/
│ ├── layout.tsx
│ ├── page.tsx # Homepage (Hero + Menu + About + Contact)
│ └── globals.css
├── components/
│ ├── MenuSection.tsx
│ ├── Dish3D.tsx # React Three Fiber or Spline component
│ ├── ReservationForm.tsx
│ └── ...
├── lib/
│ └── menu-data.ts # Menu items
├── public/
│ └── images/
└── ...
```
See **[docs/CLEAN-ARCHITECTURE.md](docs/CLEAN-ARCHITECTURE.md)** for the full step-by-step guide.
---
## Development
### Getting Started
```bash
# Install dependencies
npm install
# Run development server
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) in your browser.
### Planned Features (in order)
1. ✅ Create Next.js project
2. ⏳ Build normal restaurant homepage (Hero, Navigation, About)
3. ⏳ Add menu items (name, description, price)
4. ⏳ Style with Tailwind CSS
5. ⏳ Add animations (GSAP / Framer Motion)
6. ⏳ Add 3D dishes (React Three Fiber + Spline)
7. ⏳ Reservation / Contact form
8. ⏳ Mobile responsive
9. ⏳ Deploy to Vercel
---
## License
This project is being built for Shahi Kitchen. All rights reserved.
<!-- auto-deploy test Tue Jun 2 10:28:46 UTC 2026 -->
<!-- hook test 2 Tue Jun 2 10:28:59 UTC 2026 -->
+107
View File
@@ -0,0 +1,107 @@
'use client';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import PageHeader from '@/components/PageHeader';
import CateringPackageCard from '@/components/CateringPackageCard';
import {
cateringPackages,
CATERING_DISH_CATALOG,
CATERING_CATEGORY_ORDER,
} from '@/lib/catering-data';
import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations';
import { container } from '@/infrastructure/di/container';
import { Sparkles } from 'lucide-react';
export default function CateringPage() {
const { language } = useLanguage();
const t = getTranslation(language);
const handleSendInquiry = () => {
container.messagingGateway.openWhatsApp(t.catering.inquiryMessage);
};
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main>
<PageHeader
eyebrow={t.catering.badge}
title={t.catering.title}
subtitle={t.catering.subtitle}
/>
{/* Packages grid */}
<section className="max-w-7xl mx-auto px-6 pb-16">
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{cateringPackages.map((pkg, index) => (
<CateringPackageCard key={pkg.id} pkg={pkg} index={index} />
))}
</div>
</section>
{/* Full dish catalog reference */}
<section className="border-y border-[#EDE6D9] bg-[#fffdf8] px-6 py-12">
<div className="mx-auto max-w-7xl">
<div className="mb-8">
<div className="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-[#c99a2e]">
<Sparkles className="h-3.5 w-3.5" />
{t.catering.catalogBadge}
</div>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
<div className="min-w-0 flex-1">
<h2 className="text-xl sm:text-[1.35rem] tracking-[-0.4px] text-[#101724]">
{t.catering.catalogTitle}
</h2>
<p className="mt-1.5 max-w-xl text-sm leading-relaxed text-[#6B665F]">
{t.catering.catalogSubtitle}
</p>
</div>
<button
type="button"
onClick={handleSendInquiry}
className="btn-primary inline-flex shrink-0 items-center justify-center self-start rounded-full px-7 py-3 text-sm font-bold tracking-wide sm:self-center"
>
{t.catering.sendInquiry}
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{CATERING_CATEGORY_ORDER.map((category) => {
const dishes = CATERING_DISH_CATALOG.filter((d) => d.category === category);
const categoryLabel =
(t.catering.categories as Record<string, string>)?.[category] ?? category;
return (
<div
key={category}
className="rounded-2xl border border-[#EDE6D9] bg-white p-5 shadow-sm"
>
<h3 className="mb-3 text-xs font-bold uppercase tracking-[0.2em] text-[#8a6a25]">
{categoryLabel}
</h3>
<ul className="space-y-2">
{dishes.map((dish) => (
<li
key={dish.id}
className="text-sm font-medium text-[#101724]"
>
{dish.name}
</li>
))}
</ul>
</div>
);
})}
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

+482
View File
@@ -0,0 +1,482 @@
/**
* =============================================================================
* GLOBAL DESIGN SYSTEM — Shahi Kitchen
* =============================================================================
*
* Shahi cream + gold visual identity for Shahi Kitchen.
* Bright, warm, appetizing, and luxurious without being dark.
*/
@import "tailwindcss";
:root {
/* === Refined Shahi Light Palette - Warm Cream + Gold === */
/* Backgrounds */
--bg: #F8F5F0;
--bg-elevated: #F5F1E9;
--surface: #FFFFFF;
--surface-subtle: #F2EDE4;
/* Gold System */
--gold: #B38B4D;
--gold-dark: #8C6B3A;
--gold-light: #C9A46B;
--gold-muted: #d4a73d;
/* Emerald for contrast */
--emerald: #0f5a4a;
--emerald-deep: #0a4a3d;
/* Text */
--text: #2C2520;
--text-muted: #6B665F;
--text-light: #8A8478;
/* Borders */
--border: #EDE6D9;
/* Typography */
--font-display: var(--font-playfair);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-arabic: var(--font-arabic);
/* Motion */
--ease: cubic-bezier(0.25, 1, 0.5, 1);
/* Layout — language banner (48px) + navbar (68px) */
--header-height: 116px;
--page-top-gap: 1rem;
}
/* Anchor links (/#menu, /#experience) land below the fixed header */
.scroll-mt-header {
scroll-margin-top: var(--header-height);
}
/* Base */
body,
.shahi-body {
font-family: var(--font-sans);
background-color: var(--bg);
color: var(--text);
background-image:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(201, 154, 46, 0.08), transparent),
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(15, 90, 74, 0.04), transparent);
background-attachment: fixed;
}
/* RTL typography */
html[dir="rtl"] body {
font-family: var(--font-arabic), var(--font-sans);
}
html[dir="rtl"] h1,
html[dir="rtl"] h2,
html[dir="rtl"] h3,
html[dir="rtl"] h4 {
font-family: var(--font-arabic), var(--font-display);
letter-spacing: 0;
}
h1, h2, h3, h4 {
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -0.025em;
color: var(--text);
}
/* Premium Buttons */
.btn-primary {
background: linear-gradient(135deg, var(--gold), var(--gold-muted));
color: #241806;
font-weight: 700;
transition: all 0.2s var(--ease);
}
.btn-primary:hover {
transform: translateY(-1px);
}
.btn-outline {
border: 1px solid var(--gold);
color: var(--gold-dark);
background: transparent;
font-weight: 600;
transition: all 0.2s var(--ease);
}
.btn-outline:hover {
background: var(--gold);
color: #241806;
}
/* Glass effect helper */
.glass {
background: rgba(255, 253, 248, 0.72);
backdrop-filter: blur(20px);
}
/* Luxury card style */
.luxury-card {
border-radius: 2rem;
transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
}
.luxury-card:hover {
transform: translateY(-4px);
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.12);
}
/* Signature dish cards */
.signature-card {
box-shadow: 0 4px 24px -4px rgb(16 23 36 / 0.06);
transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), border-color 0.35s var(--ease);
}
.signature-card:hover {
box-shadow: 0 20px 40px -12px rgb(179 139 77 / 0.18);
border-color: rgba(201, 154, 46, 0.45);
}
/* Section ornament */
.section-ornament {
position: relative;
}
.section-ornament::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 3px;
background: linear-gradient(90deg, transparent, var(--gold-muted), transparent);
border-radius: 2px;
}
/* Hero overlay text */
.hero-glass {
background: linear-gradient(
135deg,
rgba(255, 253, 248, 0.92) 0%,
rgba(255, 250, 240, 0.88) 50%,
rgba(248, 245, 240, 0.85) 100%
);
backdrop-filter: blur(20px);
border: 1px solid rgba(201, 154, 46, 0.2);
box-shadow:
0 25px 50px -12px rgba(16, 23, 36, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
/* =============================================================================
* FOOTER — Matches header cream + gold palette
* ============================================================================= */
.shahi-footer {
background: linear-gradient(
180deg,
#fffcf7 0%,
#fbf7ef 55%,
#f5f0e6 100%
);
color: #101724;
border-top: 1px solid rgba(201, 154, 46, 0.25);
box-shadow: 0 -4px 24px -4px rgba(16, 23, 36, 0.06);
}
.footer-top-rail {
height: 4px;
background: linear-gradient(
90deg,
transparent 0%,
#8a6a25 15%,
#c99a2e 35%,
#f4d47f 50%,
#c99a2e 65%,
#8a6a25 85%,
transparent 100%
);
}
.footer-main {
position: relative;
}
.footer-glow {
position: absolute;
border-radius: 50%;
pointer-events: none;
filter: blur(80px);
}
.footer-glow--gold {
top: -40px;
right: 10%;
width: 280px;
height: 280px;
background: rgba(201, 154, 46, 0.08);
}
.footer-glow--emerald {
bottom: 0;
left: -5%;
width: 320px;
height: 320px;
background: rgba(15, 90, 74, 0.05);
}
.footer-heading-line {
display: inline-block;
width: 20px;
height: 2px;
background: linear-gradient(90deg, #c99a2e, #f4d47f);
border-radius: 1px;
}
.footer-info-card {
border-radius: 1rem;
border: 1px solid rgba(201, 154, 46, 0.22);
background: rgba(255, 255, 255, 0.72);
padding: 1rem 1.125rem;
backdrop-filter: blur(8px);
transition: border-color 0.25s var(--ease), background 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.footer-info-card:hover {
border-color: rgba(201, 154, 46, 0.45);
background: #ffffff;
box-shadow: 0 4px 16px -6px rgba(16, 23, 36, 0.08);
}
.footer-link {
color: #3d4654;
transition: color 0.2s var(--ease), transform 0.2s var(--ease);
}
.footer-link:hover {
color: #0f5a4a;
}
html[dir="rtl"] .footer-link:hover {
transform: translateX(-2px);
}
html[dir="ltr"] .footer-link:hover {
transform: translateX(2px);
}
.footer-social-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.875rem;
border: 1px solid rgba(201, 154, 46, 0.35);
background: rgba(255, 255, 255, 0.85);
color: #8a6a25;
transition: all 0.25s var(--ease);
}
.footer-social-btn:hover {
background: linear-gradient(135deg, #c99a2e, #e8c56a);
color: #241806;
border-color: #c99a2e;
transform: translateY(-2px);
box-shadow: 0 8px 24px -4px rgba(201, 154, 46, 0.35);
}
.footer-logo-ring {
border-color: rgba(201, 154, 46, 0.35);
background: #ffffff;
box-shadow: 0 4px 20px -6px rgba(16, 23, 36, 0.1);
}
.footer-bottom {
background: rgba(255, 255, 255, 0.55);
border-top: 1px solid rgba(201, 154, 46, 0.2);
}
.footer-cta {
backdrop-filter: blur(12px);
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.92) 0%,
rgba(255, 252, 247, 0.88) 100%
);
border-color: rgba(201, 154, 46, 0.35);
}
/* Language banner subtle shine */
.lang-banner {
position: relative;
overflow: hidden;
}
.lang-banner::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.06) 50%,
transparent 100%
);
pointer-events: none;
}
/* Mesmerizing shimmer animation for premium buttons (used in Navbar) */
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
/* Continuous menu reel marquee for 43" displays - items move LEFT (enter from right, exit left), seamless loop with 2x duplication */
@keyframes menu-reel {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.menu-reel-track {
will-change: transform;
animation: menu-reel linear infinite;
/* Force GPU layer for stable poster/video during transform */
transform: translateZ(0);
backface-visibility: hidden;
}
/* Homepage signature menu — dishes scroll left to right */
@keyframes signature-menu-reel-ltr {
0% { transform: translateX(-50%); }
100% { transform: translateX(0); }
}
.signature-menu-reel-track {
will-change: transform;
animation: signature-menu-reel-ltr linear infinite;
transform: translateZ(0);
backface-visibility: hidden;
}
.signature-menu-reel:hover .signature-menu-reel-track {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
.signature-menu-reel-track {
animation: none;
flex-wrap: wrap;
width: 100% !important;
justify-content: center;
}
}
/* =============================================================================
* KIOSK / FULLSCREEN DISPLAY PAGES (screen1 & screen2)
* Professional clean TV/window look: no scrollbars ever, even in browser
* fullscreen (F11) or kiosk mode. User zooms browser (in/out) to adjust
* visible reel density instead of scrolling. Content sized to fit 100vh.
* ============================================================================= */
html.screen-kiosk,
body.screen-kiosk {
overflow: hidden !important;
overscroll-behavior: none;
/* Hide scrollbars cross-browser */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
html.screen-kiosk::-webkit-scrollbar,
body.screen-kiosk::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
.screen-root {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
/* Extra insurance: hide any descendant scrollbars */
scrollbar-width: none;
-ms-overflow-style: none;
}
.screen-root::-webkit-scrollbar,
.screen-root *::-webkit-scrollbar {
display: none;
}
/* Subtle polish for pure display (no accidental text selection or pointer) */
.screen-root {
cursor: default;
}
/* =============================================================================
* VERTICAL REEL for screen3 order columns
* Orders move UP (from below to above) in a loop, like screen2 horizontal but vertical.
* Cards have fixed px height; viewport h-full of available column space (after header) =>
* fills entire screen height with no white space (max top to bottom).
* Zoom in browser: fewer items visible per column (larger cards relative).
* Zoom out: more items visible per column (smaller cards relative).
* When > visible count, the track loops seamlessly (duplicated list + 50% translateY).
* ============================================================================= */
@keyframes order-reel-up {
0% { transform: translateY(0); }
100% { transform: translateY(-50%); }
}
.order-reel-track {
display: flex;
flex-direction: column;
will-change: transform;
animation: order-reel-up linear infinite;
/* Force GPU layer for stable animation */
transform: translateZ(0);
backface-visibility: hidden;
}
.reel-viewport {
overflow: hidden;
/* Height 100% of available (flex h-full after section header) to fill screen.
Fixed card heights + zoom changes visible count dynamically. */
}
/* Menu page — smooth category filter scrolling */
.menu-category-scroll {
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
}
.menu-category-scroll::-webkit-scrollbar {
display: none;
}
@media (min-width: 1024px) {
.menu-category-scroll {
scrollbar-width: thin;
scrollbar-color: #d4c9b8 transparent;
}
.menu-category-scroll::-webkit-scrollbar {
display: block;
width: 5px;
}
.menu-category-scroll::-webkit-scrollbar-track {
background: transparent;
}
.menu-category-scroll::-webkit-scrollbar-thumb {
background: #d4c9b8;
border-radius: 999px;
}
.menu-category-scroll::-webkit-scrollbar-thumb:hover {
background: #c99a2e;
}
}
+123
View File
@@ -0,0 +1,123 @@
/**
* ROOT LAYOUT — Shahi Kitchen (shahikitchen.se)
*
* This is the single root layout for the entire Next.js App Router application.
*
* RESPONSIBILITIES:
* - Set up global metadata (title, description, favicon) for SEO and social sharing
* - Load the premium typography system:
* • Playfair Display (display/serif) → used for headings via --font-playfair
* • Geist Sans (system UI) → body text
* • Geist Mono → code / tabular data
* - Initialize the GLOBAL CART SYSTEM:
* • CartProvider wraps the whole tree so any page/component can use `useCart()`
* • CartDrawer is rendered once at the root (slide-in basket panel)
* • Sonner <Toaster> provides beautiful toast notifications used by the cart
*
* ARCHITECTURAL NOTES:
* - We deliberately render <CartDrawer /> and <Toaster /> at the root level instead of
* inside individual pages. This guarantees only ONE instance exists and prevents
* duplicate drawers/toasts when navigating.
* - The cream/gold Shahi theme tokens live in globals.css and are referenced via
* Tailwind arbitrary values (e.g. bg-[#F8F5F0]).
*
* FUTURE DEVELOPERS:
* - To add a new global provider (theme, analytics, etc.), wrap it here.
* - WhatsApp number for orders is currently hardcoded in CartDrawer.tsx (46739381089).
* - If you ever split the cart into a separate modal library, keep the provider here.
*/
import type { Metadata } from "next";
import { Playfair_Display, Geist, Geist_Mono, Noto_Sans_Arabic } from "next/font/google";
import "./globals.css";
import { Toaster } from "sonner";
import CartDrawer from "@/components/CartDrawer";
import WishlistDrawer from "@/components/WishlistDrawer";
import { CartProvider } from "@/presentation/providers/cart-provider";
import { WishlistProvider } from "@/presentation/providers/wishlist-provider";
import { LanguageProvider } from "@/presentation/providers/language-provider";
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
style: ["normal", "italic"],
});
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
weight: ["400", "500", "600"],
});
const notoArabic = Noto_Sans_Arabic({
variable: "--font-arabic",
subsets: ["arabic"],
weight: ["400", "500", "600", "700"],
});
/**
* Next.js Metadata API (static)
* These values power:
* - Browser tab title
* - Search engine results
* - Social cards (Open Graph / Twitter) when shared
*
* For dynamic per-page metadata, use `generateMetadata()` in page files.
*/
export const metadata: Metadata = {
title: "Shahi Kitchen | Authentic Indian & Pakistani Restaurant in Gothenburg",
description: "Experience Shahi flavors at Shahi Kitchen in Askim, Gothenburg. Authentic Indian & Pakistani cuisine, famous lunch buffet, and traditional sweets since 2016.",
icons: {
icon: "/favicon.ico",
},
};
/**
* RootLayout
*
* The ONLY place in the app where we initialize:
* 1. CartProvider → gives every descendant access to `useCart()` hook
* 2. CartDrawer → the actual slide-in basket UI (controlled via context)
* 3. Toaster → global toast surface used by cart (Sonner library)
*
* IMPORTANT:
* - Never wrap the entire app in another CartProvider — it will break the singleton.
* - The body uses the Shahi cream background (#F8F5F0) as the base canvas.
* - `antialiased` + font variables are applied once at the root for consistency.
*/
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="sv"
suppressHydrationWarning
className={`${playfair.variable} ${geistSans.variable} ${geistMono.variable} ${notoArabic.variable} h-full antialiased`}
>
<body
suppressHydrationWarning
className="min-h-full flex flex-col shahi-body text-[#2C2A26]"
>
<LanguageProvider>
<CartProvider>
<WishlistProvider>
{children}
<CartDrawer />
<WishlistDrawer />
<Toaster position="top-center" richColors closeButton />
</WishlistProvider>
</CartProvider>
</LanguageProvider>
</body>
</html>
);
}
+144
View File
@@ -0,0 +1,144 @@
/**
* LOCATIONS PAGE
*
* Dedicated page for the restaurant's two physical branches.
*
* ARCHITECTURE:
* - Two branches are modeled as distinct concepts:
* 1. Shahi Kitchen Askim (Sisjön) → Full restaurant + famous lunch buffet
* 2. Shahi Sweets Backaplan → Sweets, snacks, café, halwa puri etc.
* - Both are operated by the same team (explicitly stated at the bottom)
* - Google Maps embeds are intentionally simple (lazy loaded)
*
* FUTURE:
* - When real online ordering launches, this page could show "Order from Askim"
* vs "Order from Backaplan" with different delivery radii.
* - Both branches now have the same opening hours.
*/
'use client';
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import PageHeader from "@/components/PageHeader";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
export default function LocationsPage() {
const { language } = useLanguage();
const t = getTranslation(language);
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<PageHeader
className="max-w-5xl pb-4"
centered
eyebrow="WHERE TO FIND US"
title={language === 'sv' ? 'Våra Platser' : 'Our Locations'}
subtitle={
language === 'sv'
? 'Två restauranger i Göteborg — båda serverar autentiska smaker med samma shahi gästfrihet.'
: 'Two branches in Gothenburg — both serving authentic flavors with the same Shahi hospitality.'
}
/>
<div className="max-w-5xl mx-auto px-6 pb-16">
<div className="flex justify-center mb-8">
<a href="/reserve" className="btn-primary px-8 py-3 rounded-full text-sm tracking-[0.5px]">
{t.reserve}
</a>
</div>
<div className="grid md:grid-cols-2 gap-8">
{/* Branch 1: Askim */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">RESTAURANT &amp; BUFFET</div>
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Kitchen Askim (Sisjön)</h2>
<div className="space-y-5 text-[15px]">
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
<div>Datavägen 10A<br />436 32 Askim, Gothenburg</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
<a href="tel:031288910" className="block hover:text-[#B38B4D]">031-28 89 10</a>
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
<div>Monday Sunday 11:00 21:00</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
<div className="text-[#6B665F]">Full restaurant experience with our popular lunch buffet, à la carte, and traditional Pakistani &amp; Indian dishes.</div>
</div>
</div>
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130.5!2d11.95!3d57.65!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sDatav%C3%A4gen%2010A%2C%20436%2032%20Askim!5e0!3m2!1sen!2sse!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
/>
</div>
</div>
{/* Branch 2: Backaplan */}
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">SWEETS, SNACKS &amp; CAFÉ</div>
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Sweets Backaplan</h2>
<div className="space-y-5 text-[15px]">
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
<div>Krokegårdsgatan 5<br />417 30 Göteborg (Backaplan)</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
<div>Monday Sunday 11:00 21:00</div>
</div>
<div>
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
<div className="text-[#6B665F]">Fresh mithai (sweets), savory snacks, halwa puri, biryani, nihari, and café-style seating.</div>
</div>
</div>
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130!2d11.92!3d57.72!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sKrokeg%C3%A5rdsgatan%205%2C%20417%2030%20G%C3%B6teborg!5e0!3m2!1sen!2sse!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
/>
</div>
</div>
</div>
<div className="mt-12 text-center text-sm text-[#6B665F]">
Both branches are operated by the same team and share the same passion for authentic flavors.
</div>
</div>
<Footer />
</div>
);
}
+281
View File
@@ -0,0 +1,281 @@
'use client';
import { logoUrl } from "@/lib/assets";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
import { useState } from 'react';
import { motion, AnimatePresence } from "framer-motion";
import { User, Lock, MapPin, LogIn, Phone, MessageCircle } from "lucide-react";
/**
* =============================================================================
* STAFF LOGIN / AUTHENTICATION PAGE
* =============================================================================
*
* Beautiful, theme-matched placeholder login experience.
* - Fully language compatible (sv default + en/hi/ur)
* - No real backend yet — always shows friendly "under construction" message
* - Mobile-first: large tap targets, excellent spacing, easy form on phones
* - Desktop: elegant split layout with dark luxury graphic panel + form
* - Uses existing design tokens (gold accents, cream bg, rounded cards, serif titles)
*
* Future: when real auth is added, this will become the entry to a protected staff area
* (per-site menus, orders, inventory etc. for Askim vs Backaplan).
*/
export default function LoginPage() {
const { language } = useLanguage();
const t = getTranslation(language);
const [formData, setFormData] = useState({
username: '',
password: '',
site: '',
});
const [isSubmitted, setIsSubmitted] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Placeholder behaviour: always show the construction notice.
// No validation beyond native required fields — real auth coming later.
setIsSubmitted(true);
};
const resetForm = () => {
setFormData({ username: '', password: '', site: '' });
setIsSubmitted(false);
};
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main className="pb-20">
{/* Elegant header */}
<div className="max-w-5xl mx-auto px-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
STAFF PORTAL
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
{t.auth.title}
</h1>
<p className="mx-auto max-w-md text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
{t.auth.subtitle}
</p>
</div>
{/* Content area — beautiful split layout on desktop, clean single column on mobile */}
<div className="mx-auto max-w-5xl px-6 mt-10">
<div className="grid items-start gap-8 md:grid-cols-5">
{/* LEFT: Premium dark graphic panel (desktop only). Matches Shahi theme with gold details. */}
<div className="hidden md:col-span-2 md:block">
<div className="sticky top-[116px] flex h-[520px] flex-col justify-between overflow-hidden rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-10 text-white shadow-2xl">
{/* Subtle gold frame lines */}
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e] to-transparent" />
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e]/50 to-transparent" />
<div>
<div className="mb-8 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur">
<img
src={logoUrl()}
alt="Shahi Kitchen"
className="h-full w-full object-contain"
/>
</div>
<div className="font-serif text-[42px] leading-[0.95] tracking-[-1.8px]">
Shahi<br />Kitchen
</div>
<div className="mt-1 text-sm font-medium tracking-[2.5px] text-[#c99a2e]">
STAFF ACCESS
</div>
</div>
<div>
<div className="my-6 h-px w-10 bg-[#c99a2e]/40" />
<p className="max-w-[220px] text-sm leading-relaxed text-white/70">
Two locations serving Shahi Indian &amp; Pakistani hospitality in Gothenburg since 2016.
</p>
<div className="mt-5 text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">
ASKIM BACKAPLAN
</div>
</div>
</div>
</div>
{/* RIGHT / MOBILE: The actual elegant login card */}
<div className="md:col-span-3">
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-8 shadow-xl md:p-10">
{/* Mobile-only logo accent for visual continuity */}
<div className="mb-6 flex justify-center md:hidden">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-[#F8F5F0] p-2.5">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div>
</div>
<AnimatePresence mode="wait">
{!isSubmitted ? (
<motion.div
key="login-form"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Username */}
<div>
<label htmlFor="username" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
{t.auth.usernameLabel}
</label>
<div className="relative">
<User className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
placeholder={t.auth.usernamePlaceholder}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Password */}
<div>
<label htmlFor="password" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
{t.auth.passwordLabel}
</label>
<div className="relative">
<Lock className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder={t.auth.passwordPlaceholder}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Site / Location dropdown — Askim or Backaplan */}
<div>
<label htmlFor="site" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
{t.auth.siteLabel}
</label>
<div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select
id="site"
name="site"
value={formData.site}
onChange={handleChange}
required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
>
<option value="">{t.auth.sitePlaceholder}</option>
<option value="askim">{t.auth.askim}</option>
<option value="backaplan">{t.auth.backaplan}</option>
</select>
{/* Custom dropdown arrow for polish */}
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
<p className="mt-1.5 text-[11px] text-[#8A8478]">Select the location you are working at today.</p>
</div>
{/* Primary gold action button — large & mobile friendly */}
<button
type="submit"
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
>
<LogIn className="h-5 w-5" />
{t.auth.submit}
</button>
</form>
<p className="mt-8 text-center text-[11px] tracking-wide text-[#8A8478]">
{t.auth.footerNote}
</p>
</motion.div>
) : (
/* Beautiful result state — always shows the construction notice */
<motion.div
key="construction-result"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="text-center"
>
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-[#B38B4D]/10">
<LogIn className="h-8 w-8 text-[#B38B4D]" />
</div>
<h2 className="mb-3 font-serif text-3xl tracking-[-1px]">
{t.auth.construction.title}
</h2>
<p className="mx-auto max-w-md text-[15px] leading-relaxed text-[#6B665F]">
{t.auth.construction.message}
</p>
{/* Helpful contact actions (real numbers from the site) */}
<div className="mt-8 space-y-3">
<a
href="tel:031288910"
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-[#EDE6D9] py-3.5 text-sm font-medium transition active:bg-[#F8F5F0] hover:bg-[#F8F5F0]"
>
<Phone className="h-4 w-4" />
{t.auth.construction.contact} 031-28 89 10
</a>
<a
href="https://wa.me/46739381089"
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-[#EDE6D9] py-3.5 text-sm font-medium transition active:bg-[#F8F5F0] hover:bg-[#F8F5F0]"
>
<MessageCircle className="h-4 w-4" />
WhatsApp +46 73 938 10 89
</a>
<button
onClick={resetForm}
className="mt-2 w-full rounded-2xl bg-[#101724] py-3.5 text-sm font-semibold text-white active:bg-black transition"
>
{t.auth.construction.tryAgain}
</button>
</div>
<p className="mt-6 text-[10px] text-[#8A8478]">
We appreciate your patience while we build a seamless experience.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Small trust line */}
<p className="mt-5 text-center text-[10px] text-[#8A8478] tracking-[1px]">
SHAHI KITCHEN GOTHENBURG EST 2016
</p>
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
+418
View File
@@ -0,0 +1,418 @@
"use client";
/**
* MENU PAGE — Premium Sidebar Navigation
*/
import { useEffect, useRef, useState, useMemo } from "react";
import { menuCategories } from "@/lib/menu-data";
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
import type { MenuItem } from "@/domain/menu/entities";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { motion } from "framer-motion";
import Navbar from "@/components/Navbar";
import PageHeader from "@/components/PageHeader";
import Footer from "@/components/Footer";
import { useCart } from "@/components/CartContext";
import WishlistButton from "@/components/WishlistButton";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
import {
dishImageUrl,
getMenuPosterSrc,
getMenuPosterCandidates,
applyNextImageFallback,
} from "@/lib/assets";
import { playHoverSound } from "@/lib/sounds";
import DishDetailsModal from "@/components/DishDetailsModal";
import { hasDishDetailModal } from "@/domain/menu/dish-details";
gsap.registerPlugin(ScrollTrigger);
export default function MenuPage() {
const [activeCategory, setActiveCategory] = useState("All");
const [searchQuery, setSearchQuery] = useState("");
const [showVegetarianOnly, setShowVegetarianOnly] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [detailItem, setDetailItem] = useState<MenuItem | null>(null);
const categoryListRef = useRef<HTMLDivElement>(null);
const { addToCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
// Sidebar categories
const sidebarCategories = [
{ id: "All", name: t.menu.allDishes || "All Dishes" },
...menuCategories.map((cat) => ({ id: cat.id, name: cat.name })),
];
// Filtering logic
const filteredCategories = useMemo(() => {
return menuCategories
.map((category) => {
let items = category.items;
// Sidebar filter
if (activeCategory !== "All" && category.id !== activeCategory) {
items = [];
}
// Search
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase().trim();
items = items.filter(
(item) =>
item.name.toLowerCase().includes(q) ||
(item.description && item.description.toLowerCase().includes(q))
);
}
// Vegetarian
if (showVegetarianOnly) {
items = items.filter((item) => item.isVegetarian);
}
return { ...category, items };
})
.filter((category) => category.items.length > 0);
}, [searchQuery, showVegetarianOnly, activeCategory]);
const handleCategorySelect = (id: string) => {
setActiveCategory(id);
window.scrollTo({ top: 220, behavior: "smooth" });
};
const getItemDescription = (item: MenuItem) =>
(t as { menuDescriptions?: Record<string, string> }).menuDescriptions?.[item.id] ||
item.description ||
"";
const openDishDetails = (item: MenuItem) => {
setDetailItem(item);
};
/** Detail-modal dishes use their photo on the card; video plays only in the modal. */
const getCardImageSrc = (item: MenuItem, categoryId: string) =>
hasDishDetailModal(item.id, categoryId) && item.image
? dishImageUrl(item.image)
: getMenuPosterSrc(item);
const getCardImageCandidates = (item: MenuItem, categoryId: string) =>
hasDishDetailModal(item.id, categoryId) && item.image
? [dishImageUrl(item.image), ...getMenuPosterCandidates(item)]
: getMenuPosterCandidates(item);
const renderMenuPrice = (item: MenuItem) => {
if (item.pricing === "weight") {
return (
<div className="shrink-0 text-right">
<div className="text-[#B38B4D] text-xl font-medium tracking-[-0.5px] tabular-nums leading-tight">
{item.pricePerHalfKg ?? item.price}
</div>
<div className="text-[10px] text-[#8A8478] tracking-wide">{t.menu.perHalfKg}</div>
<div className="text-[#B38B4D] text-sm font-medium tabular-nums mt-0.5">
{item.pricePerKg} {t.menu.perKg}
</div>
</div>
);
}
return (
<div className="shrink-0 text-right">
<div className="text-[#B38B4D] text-2xl font-medium tracking-[-0.5px] tabular-nums">
{item.price}
</div>
<div className="text-[10px] text-[#8A8478] tracking-widest -mt-1">KR</div>
</div>
);
};
// isMobile detection - used to enable advanced desktop hover effects + sounds only on desktop
useEffect(() => {
const updateMobile = () => setIsMobile(window.innerWidth < 768);
updateMobile();
window.addEventListener("resize", updateMobile);
return () => window.removeEventListener("resize", updateMobile);
}, []);
useEffect(() => {
const container = categoryListRef.current;
if (!container) return;
const activeButton = container.querySelector<HTMLElement>(
`[data-category-id="${activeCategory}"]`
);
if (!activeButton) return;
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
activeButton.scrollIntoView({
behavior: prefersReducedMotion ? "auto" : "smooth",
block: "nearest",
inline: "center",
});
}, [activeCategory]);
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<PageHeader
eyebrow="AUTHENTIC • GENEROUS • ROYAL"
title={t.menu.title}
subtitle={t.menu.subtitle}
/>
{/* Main Layout */}
<div className="max-w-7xl mx-auto px-6 pb-20">
<div className="flex flex-col lg:flex-row gap-10">
{/* BEAUTIFUL SIDEBAR */}
<div className="lg:w-72 flex-shrink-0">
<div className="sticky top-[116px]">
<div className="mb-6">
<div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div>
<h3 className="font-serif text-3xl tracking-tight">Signature Categories</h3>
</div>
{/* Smooth scroll: horizontal on mobile, vertical on desktop when list exceeds viewport */}
<div className="relative">
<div
ref={categoryListRef}
className="menu-category-scroll flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth overscroll-x-contain overscroll-y-contain pb-3 pr-1 snap-x snap-mandatory scroll-px-1 [-webkit-overflow-scrolling:touch] lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:max-h-[calc(100dvh-13.5rem)] lg:snap-none lg:scroll-px-0 lg:pb-2 lg:pr-2"
>
{sidebarCategories.map((cat) => {
const isActive = activeCategory === cat.id;
const itemCount = cat.id === "All"
? menuCategories.reduce((sum, c) => sum + c.items.length, 0)
: menuCategories.find(c => c.id === cat.id)?.items.length || 0;
return (
<button
key={cat.id}
data-category-id={cat.id}
onClick={() => handleCategorySelect(cat.id)}
className={`flex-shrink-0 snap-start min-w-[140px] lg:min-w-0 lg:w-full flex items-center justify-between px-5 py-3 lg:py-3.5 rounded-2xl text-left transition-all active:scale-[0.985] group ${
isActive
? "bg-[#101724] text-white shadow-lg"
: "hover:bg-white hover:shadow-sm active:bg-[#EDE6D9] text-[#101724] border border-transparent hover:border-[#e5e1d7]"
}`}
>
<span className={`font-medium tracking-[-0.1px] text-sm lg:text-base ${isActive ? "" : "group-hover:text-[#0f5a4a]"}`}>
{cat.name}
</span>
<span className={`text-[10px] lg:text-xs px-2 py-0.5 rounded-full font-mono tabular-nums ${
isActive ? "bg-white/20" : "bg-[#e5e1d7] text-[#68717f]"
}`}>
{itemCount}
</span>
</button>
);
})}
</div>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-[#F8F5F0] to-transparent lg:hidden"
/>
</div>
</div>
</div>
{/* MAIN CONTENT */}
<div className="flex-1 min-w-0">
{/* Search + Vegetarian Filter */}
<div className="mb-8 flex flex-col md:flex-row gap-4 items-center">
<input
type="text"
placeholder={t.menu.searchPlaceholder}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full md:w-80 rounded-2xl border border-[#e5e1d7] bg-white px-5 py-3 text-sm placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20 transition-all"
/>
<button
onClick={() => setShowVegetarianOnly(!showVegetarianOnly)}
className={`px-6 py-3 rounded-2xl text-sm font-medium border transition-all active:scale-[0.985] ${
showVegetarianOnly
? "bg-[#0f5a4a] text-white border-[#0f5a4a]"
: "border-[#e5e1d7] hover:border-[#c99a2e] text-[#101724] bg-white"
}`}
>
{showVegetarianOnly ? t.menu.vegetarianOnly : t.menu.showVegetarian}
</button>
{(searchQuery || showVegetarianOnly || activeCategory !== "All") && (
<button
onClick={() => {
setSearchQuery("");
setShowVegetarianOnly(false);
setActiveCategory("All");
}}
className="text-sm font-medium text-[#c99a2e] hover:text-[#8f6b22] active:underline"
>
{t.menu.clearFilters}
</button>
)}
</div>
{/* Menu Items */}
{filteredCategories.length === 0 ? (
<div className="text-center py-16 text-[#6B665F]">
{t.menu.noResults}
</div>
) : (
filteredCategories.map((category) => (
<div key={category.id} className="mb-16">
<div className="flex items-center gap-4 mb-7">
<div className="text-3xl tracking-[-1px] text-[#101724]">{category.name}</div>
<div className="flex-1 h-px bg-gradient-to-r from-[#e5e1d7] to-transparent" />
<div className="text-sm font-medium text-[#68717f] tracking-widest">
{category.items.length} {t.menu.dishes}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{category.items.map((item) => {
const usesDetailModal = hasDishDetailModal(item.id, category.id);
return (
<motion.div
key={item.id}
data-id={item.id}
className={`menu-card group bg-white border border-[#EDE6D9] rounded-2xl overflow-hidden flex flex-col hover:border-[#c99a2e]/40 active:border-[#c99a2e] active:scale-[0.985] transition-all duration-150 touch-manipulation ${
usesDetailModal ? "" : "cursor-pointer"
}`}
onClick={
usesDetailModal
? undefined
: () => addToCart(buildCartLineFromMenuItem(item))
}
whileHover={!isMobile ? {
y: -4,
boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
transition: { type: "spring", stiffness: 300, damping: 20 }
} : {}}
whileTap={{ scale: 0.985 }}
>
{/* Media - Always poster for performance. Advanced desktop hover effect (scale + lift + glow) + subtle sound. Mobile uses simple scale. */}
<div
className={`relative h-52 overflow-hidden bg-[#F2EDE4] ${
usesDetailModal ? "cursor-pointer" : ""
}`}
onClick={
usesDetailModal
? (e) => {
e.stopPropagation();
openDishDetails(item);
}
: undefined
}
onMouseEnter={() => {
if (!isMobile) playHoverSound();
}}
>
<motion.img
src={getCardImageSrc(item, category.id)}
alt={item.name}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
whileHover={!isMobile ? {
scale: 1.08,
y: -3,
transition: { type: "spring", stiffness: 260, damping: 20 }
} : {}}
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getCardImageCandidates(item, category.id),
(e.target as HTMLImageElement).src
);
}}
/>
{/* Subtle gradient + hover shine overlay for advanced desktop feel */}
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-black/10 to-black/25" />
{!isMobile && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
)}
<div className="absolute top-3 right-3 z-10">
<WishlistButton
item={{ id: item.id, name: item.name, price: item.price, image: item.image }}
size="sm"
/>
</div>
</div>
<div className="p-6 flex flex-col flex-1">
<div className="flex justify-between items-start gap-4 mb-4">
<h3 className="text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26]">
{item.name}
</h3>
{renderMenuPrice(item)}
</div>
{usesDetailModal ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
openDishDetails(item);
}}
className="text-left text-[10px] text-[#8A8478] tracking-wide hover:text-[#B38B4D] hover:underline underline-offset-2 transition-colors mb-4"
>
{t.menu.seeDetails}
</button>
) : (
item.description && (
<p className="text-[#6B665F] text-[15px] leading-relaxed mb-4">
{getItemDescription(item)}
</p>
)
)}
<div className="mt-auto space-y-3">
{item.isVegetarian && (
<span className="inline-block text-xs px-3 py-1 rounded-full bg-[#3F5C4A]/10 text-[#3F5C4A] tracking-wider">
VEGETARIAN
</span>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
addToCart(buildCartLineFromMenuItem(item));
}}
className="w-full py-3.5 text-sm tracking-[0.6px] border border-[#B38B4D] text-[#B38B4D] rounded-full hover:bg-[#B38B4D] hover:text-white active:bg-[#8C6B3A] active:text-white active:scale-[0.985] font-medium transition-all touch-manipulation"
>
{t.wishlistDrawer.addToCart}
</button>
</div>
</div>
</motion.div>
);
})}
</div>
</div>
))
)}
</div>
</div>
</div>
<Footer />
{detailItem && (
<DishDetailsModal
item={detailItem}
description={getItemDescription(detailItem)}
isOpen={!!detailItem}
onClose={() => setDetailItem(null)}
labels={{
ingredients: t.menu.ingredients,
close: t.menu.closeDetails,
}}
/>
)}
</div>
);
}
+454
View File
@@ -0,0 +1,454 @@
'use client';
/**
* /orderfromtable — Single reusable table ordering page (QR driven)
*
* This page is the ONLY customer-facing order page for physical tables.
* All 80 table QR codes point here with ?table=backaplan-001 etc.
*
* - Strict validation of table param (branch + 001-040)
* - Local item selection (independent of global cart)
* - On "Send order" we only show confirmation (no backend call yet)
* - Payload is prepared exactly in the shape the future POST /api/table-orders will expect
*/
import React, { useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { filterMenuItems } from '@/application/menu/filter-menu';
import { getMenuPosterSrc, logoUrl, SITE_ASSETS } from '@/lib/assets';
import { buildTableOrderPayload } from '@/application/table-order/table-order-use-cases';
import { parseTableId } from '@/domain/table/table-id';
import {
addOrderLine,
calculateLineTotal,
calculateOrderTotal,
formatLineQuantityLabel,
removeOrderLine,
updateOrderLineQuantity,
type OrderLine,
} from '@/domain/shared/order-line';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
import { container } from '@/infrastructure/di/container';
import type { MenuItem } from '@/domain/menu/entities';
import { playAddSound, playSuccessSound } from '@/infrastructure/audio/web-audio-sound-adapter';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
import { Search, Plus, Minus, X, CheckCircle, UtensilsCrossed } from 'lucide-react';
export default function OrderFromTablePage() {
const searchParams = useSearchParams();
const tableParam = searchParams.get('table');
const { language } = useLanguage();
const t = getTranslation(language);
const to = t.tableOrder;
const parsed = useMemo(() => parseTableId(tableParam), [tableParam]);
const allMenuItems = container.menuRepository.getAllItems();
// Local order state (NOT the global cart)
const [selected, setSelected] = useState<OrderLine[]>([]);
const [search, setSearch] = useState('');
const [vegOnly, setVegOnly] = useState(false);
const [notes, setNotes] = useState('');
const [isSending, setIsSending] = useState(false);
const [sent, setSent] = useState(false);
const isValid = parsed.valid;
const branchLabel = isValid
? (parsed.branch === 'askim' ? to.branchAskim : to.branchBackaplan)
: '';
const tableDisplay = isValid ? parsed.tableNumber : '';
// Filtered dishes for the selector grid (same data source as everywhere else)
const filteredDishes = useMemo(
() =>
filterMenuItems(allMenuItems, {
searchQuery: search,
vegetarianOnly: vegOnly,
}),
[allMenuItems, search, vegOnly]
);
const selectedTotal = calculateOrderTotal(selected);
function addDish(dish: MenuItem) {
playAddSound();
setSelected((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
}
function updateQty(id: string, newQty: number) {
setSelected((prev) => updateOrderLineQuantity(prev, id, newQty));
}
function removeItem(id: string) {
setSelected((prev) => removeOrderLine(prev, id));
}
function clearSelection() {
setSelected([]);
setNotes('');
}
async function handleSendOrder() {
if (!isValid || selected.length === 0) return;
setIsSending(true);
const orderPayload = buildTableOrderPayload(
parsed.branch,
parsed.tableNumber,
parsed.tableIdentifier,
selected,
notes
);
// For now we do NOT call any backend.
// This log + comment makes future integration trivial.
// TODO: replace the block below with real fetch when backend is ready.
console.log('[TABLE ORDER] Prepared payload (will be POSTed to /api/table-orders):', orderPayload);
// Simulate a tiny delay for nice UX (feels like it is being sent)
await new Promise((r) => setTimeout(r, 420));
playSuccessSound();
setIsSending(false);
setSent(true);
}
function resetForAnotherOrder() {
setSent(false);
setSelected([]);
setNotes('');
setSearch('');
setVegOnly(false);
}
// ====================== INVALID QR STATE ======================
if (!isValid) {
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="flex min-h-[70vh] items-center justify-center px-6">
<div className="max-w-md text-center">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-red-100/70">
<X className="h-10 w-10 text-red-600" />
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-3">{to.invalidTitle}</h1>
<p className="text-sm md:text-[15px] text-[#6B665F] mb-8">{to.invalidMessage}</p>
<a
href="/"
className="inline-flex items-center gap-2 rounded-full bg-[#101724] px-8 py-3 text-sm font-semibold text-white active:bg-black"
>
Go to homepage
</a>
</div>
</div>
<Footer />
</div>
);
}
// ====================== CONFIRMATION STATE ======================
if (sent) {
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="flex min-h-[70vh] items-center justify-center px-6">
<motion.div
initial={{ opacity: 0, scale: 0.96, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
className="max-w-lg text-center"
>
<div className="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-[#B38B4D]/10">
<CheckCircle className="h-14 w-14 text-[#B38B4D]" />
</div>
<div className="mb-2 text-sm tracking-[3px] text-[#B38B4D] font-medium">
{branchLabel.toUpperCase()} {to.tableLabel.toUpperCase()} {tableDisplay}
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-4">{to.confirmationTitle}</h1>
<p className="text-sm md:text-[15px] text-[#6B665F] leading-relaxed mb-10">
{to.confirmationMessage}
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button
onClick={resetForAnotherOrder}
className="btn-primary px-10 py-4 rounded-2xl text-base font-semibold tracking-[0.5px] active:scale-[0.985]"
>
{to.orderAnother}
</button>
<a
href="/menu"
className="btn-outline px-10 py-4 rounded-2xl text-base font-semibold tracking-[0.5px]"
>
Browse full menu
</a>
</div>
<p className="mt-8 text-xs text-[#8A8478]">
{branchLabel} {to.tableLabel} {tableDisplay}
</p>
</motion.div>
</div>
<Footer />
</div>
);
}
// ====================== MAIN ORDERING UI ======================
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
{/* Table header bar */}
<div className="sticky top-[116px] z-40 border-b border-[#EDE6D9] bg-[#F8F5F0]/95 backdrop-blur-xl">
<div className="max-w-6xl mx-auto px-6 py-4 flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3">
<div className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#c99a2e]/30 bg-white p-1.5">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div>
<div>
<div className="font-serif text-2xl tracking-[-1px] leading-none">{branchLabel}</div>
<div className="text-[#B38B4D] text-sm font-medium tracking-[1px] mt-0.5">
{to.tableLabel} {tableDisplay}
</div>
</div>
</div>
<div className="sm:ml-auto flex items-center gap-2 text-xs uppercase tracking-[2px] text-[#8A8478]">
<UtensilsCrossed className="h-3.5 w-3.5" />
ORDER FROM TABLE
</div>
</div>
</div>
<div className="max-w-6xl mx-auto px-6 pt-8 pb-24">
{/* Intro */}
<div className="mb-8">
<div className="text-[#B38B4D] text-xs tracking-[3.5px] mb-2 font-medium">{to.tableLabel.toUpperCase()} {tableDisplay}</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">{to.selectTitle}</h1>
<p className="text-sm md:text-[15px] text-[#6B665F] max-w-xl leading-relaxed">{to.selectSubtitle}</p>
</div>
<div className="grid lg:grid-cols-12 gap-8">
{/* DISH SELECTOR */}
<div className="lg:col-span-7">
{/* Filters */}
<div className="mb-5 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="relative flex-1">
<Search className="pointer-events-none absolute left-4 top-3.5 h-4 w-4 text-[#B38B4D]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={to.searchPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-white py-3 pl-11 pr-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:ring-1 focus:ring-[#B38B4D]/20 outline-none"
/>
</div>
<button
onClick={() => setVegOnly(!vegOnly)}
className={`px-5 py-3 rounded-2xl text-sm font-medium border transition active:scale-[0.985] whitespace-nowrap ${
vegOnly
? 'bg-[#0f5a4a] text-white border-[#0f5a4a]'
: 'border-[#EDE6D9] bg-white text-[#101724] hover:border-[#c99a2e]'
}`}
>
{vegOnly ? '✓ ' : ''}{to.showVegetarian}
</button>
</div>
{/* Dish grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<AnimatePresence>
{filteredDishes.length > 0 ? (
filteredDishes.map((dish) => {
const inOrder = selected.some((s) => s.id === dish.id);
const poster = getMenuPosterSrc(dish, 'optimized');
return (
<motion.button
key={dish.id}
whileTap={{ scale: 0.985 }}
onClick={() => addDish(dish)}
className="group text-left flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-3 active:border-[#B38B4D] hover:border-[#c99a2e]/50 transition overflow-hidden"
>
<div className="relative h-20 w-20 flex-shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]">
<img
src={poster}
alt={dish.name}
className="absolute inset-0 h-full w-full object-cover group-active:scale-105 transition"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement;
target.src = SITE_ASSETS.dishes.defaultPoster;
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 to-black/20" />
</div>
<div className="flex min-w-0 flex-1 flex-col py-0.5">
<div className="flex items-start justify-between gap-2">
<div className="font-medium tracking-[-0.2px] text-[15px] leading-tight line-clamp-2 pr-1">
{dish.name}
</div>
<div className="shrink-0 text-right font-semibold text-[#B38B4D] tabular-nums text-sm leading-tight">
{dish.pricing === 'weight' ? (
<>
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
<span className="block text-[10px] font-normal text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
</>
) : (
<>
{dish.price}
<span className="text-[10px] font-normal ml-px">kr</span>
</>
)}
</div>
</div>
{dish.description && (
<div className="mt-1 text-xs text-[#6B665F] line-clamp-2">
{dish.description}
</div>
)}
<div className="mt-auto pt-2">
<span className="inline-flex items-center gap-1 text-xs font-medium text-[#B38B4D] group-active:text-[#8C6B3A]">
<Plus className="h-3.5 w-3.5" />
{inOrder ? 'Add another' : 'Add to order'}
</span>
</div>
</div>
</motion.button>
);
})
) : (
<div className="col-span-full py-12 text-center text-[#6B665F]">
No dishes match your search.
</div>
)}
</AnimatePresence>
</div>
</div>
{/* ORDER SUMMARY (sticky on desktop) */}
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-24">
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<div className="font-semibold tracking-tight text-lg">{to.yourOrder}</div>
{selected.length > 0 && (
<button
onClick={clearSelection}
className="text-xs text-[#B38B4D] hover:underline active:text-[#8C6B3A]"
>
Clear all
</button>
)}
</div>
{selected.length === 0 ? (
<div className="py-8 text-center">
<div className="text-5xl mb-3 opacity-50">🍽</div>
<p className="text-[#6B665F] text-sm">{to.emptyOrder}</p>
</div>
) : (
<div className="space-y-3 mb-5 max-h-[320px] overflow-auto pr-1">
{selected.map((item) => (
<div key={item.id} className="flex items-center gap-3 rounded-2xl bg-[#F8F5F0] px-4 py-3">
<div className="min-w-0 flex-1">
<div className="font-medium text-[15px] tracking-[-0.2px]">{item.name}</div>
<div className="text-xs text-[#6B665F] tabular-nums">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
: `${item.price} kr × ${item.quantity}`}
{' '}= {calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-1.5">
<button
onClick={() => updateQty(item.id, item.quantity - 1)}
className="h-8 w-8 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95"
aria-label="Decrease"
>
</button>
<span className="min-w-12 text-center font-medium tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button
onClick={() => updateQty(item.id, item.quantity + 1)}
className="h-8 w-8 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95"
aria-label="Increase"
>
+
</button>
<button
onClick={() => removeItem(item.id)}
className="ml-1.5 text-[#B38B4D] p-1 active:text-red-600"
aria-label="Remove"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
)}
{/* Notes */}
<div className="mt-2">
<label className="block text-xs font-semibold tracking-widest text-[#6B665F] mb-1.5">
{to.notesLabel}
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder={to.notesPlaceholder}
rows={2}
className="w-full resize-y rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] p-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 outline-none"
/>
</div>
{/* Total + Send */}
<div className="mt-5 border-t border-[#EDE6D9] pt-5">
<div className="flex items-baseline justify-between text-lg mb-4">
<span className="text-[#6B665F]">{to.total}</span>
<span className="font-semibold tabular-nums">{selectedTotal} kr</span>
</div>
<button
onClick={handleSendOrder}
disabled={selected.length === 0 || isSending}
className="w-full rounded-2xl bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] py-4 text-base font-semibold tracking-[0.6px] text-white shadow-lg active:brightness-95 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isSending ? 'Sending...' : to.sendOrder}
</button>
<p className="mt-3 text-center text-[11px] text-[#8A8478]">
Staff will come to your table to confirm.
</p>
</div>
</div>
{/* Small context line */}
<div className="mt-3 text-center text-xs text-[#8A8478] tracking-wider">
{branchLabel} {to.tableLabel} {tableDisplay}
</div>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { Suspense } from 'react';
import Navbar from '@/components/Navbar';
import OrderFromTableClient from './OrderFromTableClient';
/**
* Server wrapper for /orderfromtable
*
* We must wrap any component that uses useSearchParams() in a Suspense boundary
* (Next.js 16 requirement for pages that read search params).
*
* This keeps the actual ordering UI fully client-side while satisfying the framework.
*/
export default function OrderFromTablePage() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<div className="flex min-h-[50vh] items-center justify-center px-6">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-pulse rounded-full bg-[#B38B4D]/30" />
<p className="text-sm tracking-widest text-[#8A8478]">Loading table...</p>
</div>
</div>
</div>
}
>
<OrderFromTableClient />
</Suspense>
);
}
+191
View File
@@ -0,0 +1,191 @@
"use client";
/**
* SHAHI KITCHEN HOMEPAGE
* Version with advanced Signature Menu + elegant hero (pre full Sultan redesign)
*/
import React, { useEffect, useMemo, useState } from "react";
import Lenis from "lenis";
import { ArrowRight, UtensilsCrossed } from "lucide-react";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { useCart } from "@/components/CartContext";
import { useLanguage } from "@/lib/language-context";
import { heroBannerSources } from "@/lib/assets";
import { menuCategories } from "@/lib/menu-data";
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
import type { MenuItem } from "@/domain/menu/entities";
import { getTranslation } from "@/lib/translations";
import SignatureMenuMarquee, { type SignatureDish } from "@/components/SignatureMenuMarquee";
type SignatureFilter = "All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweets";
type HomepageDish = SignatureDish & { filterTag: SignatureFilter | null };
export default function ShahiKitchenHomepage() {
const { addToCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
const [menuFilter, setMenuFilter] = useState<SignatureFilter>("All");
// Lenis smooth scroll
useEffect(() => {
const lenis = new Lenis({
duration: 1.1,
easing: (t: number) => Math.min(1, 1.001 * (-Math.pow(2, -10 * t) + 1)),
smoothWheel: true,
});
const raf = (time: number) => { lenis.raf(time); requestAnimationFrame(raf); };
requestAnimationFrame(raf);
return () => lenis.destroy();
}, []);
const getDishFilterTag = (item: MenuItem, categoryId: string): SignatureFilter | null => {
if (categoryId === "sweets") return "Sweets";
if (categoryId === "street-food") return "Street";
if (categoryId === "naan-roll") return "Roll";
if (categoryId === "meat") return "Meat";
if (item.id.includes("biryani")) return "Rice";
if (categoryId === "vegetarian" || categoryId === "chicken") return "Curry";
return null;
};
const allDishes = useMemo<HomepageDish[]>(
() =>
menuCategories.flatMap((category) =>
category.items.map((item) => ({
...item,
desc: item.description ?? "",
filterTag: getDishFilterTag(item, category.id),
}))
),
[]
);
const filtered = useMemo(() => {
if (menuFilter === "All") return allDishes;
return allDishes.filter((dish) => dish.filterTag === menuFilter);
}, [allDishes, menuFilter]);
const banner = heroBannerSources();
const addDish = (d: SignatureDish) => {
addToCart(buildCartLineFromMenuItem(d));
};
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
{/* HERO - Full Banner Video (no text overlay) */}
<section
className="relative min-h-[100dvh] overflow-hidden bg-[#fbf7ef]"
aria-label="Hero"
>
{/* Banner Video - Responsive sources for proper mobile/iPad visualization (portrait asset) + desktop (landscape).
Mobile/iPad (<=1024px) uses the compressed banner_mobile-optimized versions (WebM primary for size/speed, MP4 fallback).
These were generated from the provided banner_mobile.mp4 (original kept as source asset). */}
<video
autoPlay
muted
playsInline
preload="auto"
className="absolute inset-0 z-0 w-full h-full object-cover
object-[50%_10%] sm:object-[50%_12%] md:object-[50%_16%]
lg:object-[50%_20%] xl:object-[50%_24%]"
onError={(e) => console.error('Hero banner video failed to load', e)}
>
{/* Mobile + iPad (portrait-friendly framing for better visualization on small screens) */}
<source
src={banner.mobileWebm}
type="video/webm"
media="(max-width: 1024px)"
/>
<source
src={banner.mobileMp4}
type="video/mp4"
media="(max-width: 1024px)"
/>
{/* Desktop / large screens */}
<source src={banner.desktopMp4} type="video/mp4" />
</video>
{/* Cinematic overlays */}
<div className="absolute inset-0 z-[1] bg-gradient-to-b from-[#0f5a4a]/30 via-transparent to-[#101724]/50" />
<div className="absolute inset-0 z-[2] bg-[radial-gradient(ellipse_at_center,transparent_0%,rgba(16,23,36,0.25)_100%)]" />
</section>
{/* ADVANCED SIGNATURE MENU */}
<section id="menu" className="scroll-mt-header section-ornament bg-[#fffdf8] px-6 py-20 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10">
<div>
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-[#fff6dc] px-4 py-1.5 text-sm font-semibold text-[#60420d]">
<UtensilsCrossed className="h-4 w-4" /> {t.signatureMenu.title}
</div>
<h2 className="text-2xl sm:text-[1.75rem] md:text-3xl leading-[1.15] tracking-[-0.8px] break-words text-[#101724]">{t.signatureMenu.title}</h2>
</div>
<p className="max-w-md text-sm md:text-[15px] font-medium text-[#4b5563]">{t.signatureMenu.subtitle}</p>
</div>
{/* Filters - modern horizontal scroller on mobile for easy thumb use */}
<div className="mb-8 -mx-1 px-1 flex gap-2 overflow-x-auto snap-x snap-mandatory pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{(["All", "Rice", "Curry", "Meat", "Street", "Roll", "Sweets"] as const).map((cat) => (
<button
key={cat}
onClick={() => setMenuFilter(cat)}
className={`flex-shrink-0 snap-start rounded-full px-5 py-2.5 text-sm font-bold transition-all active:scale-[0.97] touch-manipulation ${menuFilter === cat ? "bg-[#101724] text-white" : "border border-[#e5e1d7] bg-white text-[#182235] hover:border-[#c99a2e] hover:bg-[#fff6dc] active:bg-[#fff6dc]"}`}
>
{(t.signatureMenu as any)[`filter${cat}`] || cat}
</button>
))}
</div>
<SignatureMenuMarquee
dishes={filtered}
addToCartLabel={t.wishlistDrawer.addToCart}
perHalfKgLabel={t.menu.perHalfKg}
perKgLabel={t.menu.perKg}
onAdd={addDish}
/>
<div className="mt-10 text-center">
<a href="/menu" className="inline-flex items-center gap-2 text-sm font-bold tracking-wider text-[#B38B4D] hover:text-[#8C6B3A]">
{t.signatureMenu.viewFullMenu} <ArrowRight className="h-4 w-4" />
</a>
</div>
</div>
</section>
{/* THE SHAHI EXPERIENCE */}
<section id="experience" className="scroll-mt-header section bg-white border-y border-[#EDE6D9]">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-14">
<div className="text-[#B38B4D] text-xs tracking-[3px] mb-3">{language === 'sv' ? 'SHAHI-SÄTTET' : 'THE SHAHI WAY'}</div>
<h3 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] whitespace-pre-line">{language === 'sv' ? 'Mer än en måltid.\nEtt ögonblick av Shahi.' : 'More than a meal.\nA moment of Shahi.'}</h3>
</div>
<div className="grid md:grid-cols-3 gap-6">
{(language === 'sv' ? [
{ title: "Den Legendariska Buffén", desc: "Vår berömda lunchbuffé har över 20 roterande rätter — curry, biryani, färsk naan och sötsaker." },
{ title: "Shahi Sötsaker", desc: "Hemgjord mithai dagligen. Från färsk jalebi till rasmalai — det perfekta söta avslutet." },
{ title: "Varm Gästfrihet", desc: "Oavsett om du är här för en snabb lunch eller familjefest, behandlas du alltid som shahi." },
] : [
{ title: "The Legendary Buffet", desc: "Our famous lunch buffet features over 20 rotating dishes — curries, biryanis, fresh naan, and sweets." },
{ title: "Shahi Sweets", desc: "Homemade mithai made daily. From fresh Jalebi to Rasmalai — the perfect sweet ending." },
{ title: "Warm Hospitality", desc: "Whether you're here for a quick lunch or a family celebration, you will always be treated like Shahi." },
]).map((item, index) => (
<div key={index} className="experience-card group relative border border-[#EDE6D9] p-9 rounded-2xl bg-[#F8F5F0] overflow-hidden">
<div className="text-[#B38B4D] text-6xl font-light mb-9 tracking-[-2px]">0{index + 1}</div>
<h4 className="text-[29px] tracking-[-0.8px] mb-5 text-[#2C2A26] leading-tight">{item.title}</h4>
<p className="text-[#6B665F] text-[15.5px] leading-relaxed">{item.desc}</p>
</div>
))}
</div>
</div>
</section>
<Footer />
</div>
);
}
+544
View File
@@ -0,0 +1,544 @@
'use client';
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { buildBookingWhatsAppMessage } from '@/application/messaging/whatsapp-message-builder';
import { isBookingComplete } from '@/domain/booking/validation';
import type { BookingDetails, PreOrderLine } from '@/domain/booking/entities';
import {
addOrderLine,
calculateLineTotal,
calculateOrderTotal,
formatLineQuantityLabel,
removeOrderLine,
updateOrderLineQuantity,
} from '@/domain/shared/order-line';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
import type { MenuItem } from '@/domain/menu/entities';
import {
getMenuPosterSrc,
getMenuPosterCandidates,
logoUrl,
applyNextImageFallback,
SITE_ASSETS,
} from '@/lib/assets';
import { container } from '@/infrastructure/di/container';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import { useState } from 'react';
import { motion, AnimatePresence } from "framer-motion";
import { MapPin, Calendar, Clock, Users, User, Phone, Mail, MessageCircle, Plus, Minus, X, ArrowRight, Search, CheckCircle } from "lucide-react";
import { menuCategories } from "@/lib/menu-data";
import { playAddSound, playHoverSound, playSuccessSound } from "@/lib/sounds";
/**
* =============================================================================
* TABLE RESERVATION / BOOKING PAGE
* =============================================================================
*
* Beautiful, theme-matched table booking experience with optional pre-order.
* - Swap implemented: Reserve Table now in central nav pills, Login is the gold CTA.
* - Form first (location, date, time, guests, name, phone, email, notes).
* - After form: full menu selector for pre-ordering (reuse menu-data).
* - End: builds rich WhatsApp inquiry (booking + pre-order items) using same number as cart.
* - Fully translated (sv default + en/hi/ur).
* - Mobile-first, large targets, elegant cards matching /login and site theme.
* - Uses framer-motion for reveals, lucide icons.
* - No backend: client-side only, like staff login and cart WhatsApp.
*
* Linked from navbar (after swap) and locations.
*/
export default function ReservePage() {
const { language } = useLanguage();
const t = getTranslation(language);
const [booking, setBooking] = useState<BookingDetails>({
location: '',
date: '',
time: '',
guests: '',
name: '',
phone: '',
email: '',
notes: '',
});
const [preOrder, setPreOrder] = useState<PreOrderLine[]>([]);
const [showMenu, setShowMenu] = useState(false);
const [search, setSearch] = useState('');
const [sent, setSent] = useState(false);
const handleBookingChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setBooking((prev) => ({ ...prev, [name]: value }));
};
const validateForm = (): boolean => isBookingComplete(booking);
const handleContinue = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
setShowMenu(true);
// Smooth scroll to menu on mobile
setTimeout(() => {
const menuEl = document.getElementById('preorder-menu');
if (menuEl) menuEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
} else {
alert('Please fill in all required fields (location, date, time, guests, name, phone).');
}
};
// Flatten menu for selector (with search)
const allDishes = menuCategories.flatMap((cat) => cat.items);
const filteredDishes = allDishes.filter((dish) =>
dish.name.toLowerCase().includes(search.toLowerCase()) ||
(dish.description && dish.description.toLowerCase().includes(search.toLowerCase()))
);
const addToPreOrder = (dish: MenuItem) => {
setPreOrder((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
};
const updatePreOrderQty = (id: string, newQty: number) => {
setPreOrder((prev) => updateOrderLineQuantity(prev, id, newQty));
};
const removeFromPreOrder = (id: string) => {
setPreOrder((prev) => removeOrderLine(prev, id));
};
const getPreOrderTotal = () => calculateOrderTotal(preOrder);
const handleSendInquiry = () => {
if (!validateForm()) {
alert('Please complete the booking form first.');
return;
}
playSuccessSound();
const message = buildBookingWhatsAppMessage(booking, preOrder, {
askim: t.booking.askim,
backaplan: t.booking.backaplan,
});
if (!message) return;
container.messagingGateway.openWhatsApp(message);
setSent(true);
};
const resetAll = () => {
setBooking({ location: '', date: '', time: '', guests: '', name: '', phone: '', email: '', notes: '' });
setPreOrder([]);
setShowMenu(false);
setSearch('');
setSent(false);
};
const editBooking = () => {
setShowMenu(false);
setSent(false);
};
// Location options from translations
const locationOptions = [
{ value: 'askim', label: t.booking.askim },
{ value: 'backaplan', label: t.booking.backaplan },
];
return (
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
<Navbar />
<main className="pb-20">
{/* Elegant Header */}
<div className="max-w-5xl mx-auto px-6 text-center">
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
EXCLUSIVE EXPERIENCE
</div>
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
{t.booking.title}
</h1>
<p className="mx-auto max-w-xl text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
{t.booking.subtitle}
</p>
</div>
<div className="max-w-5xl mx-auto px-6 mt-10">
{/* Decorative header info - now ABOVE the form (not on side) so form can be wider */}
<div className="mb-10 flex justify-center">
<div className="w-full max-w-2xl rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-6 sm:p-8 text-white shadow-2xl text-center">
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur mx-auto">
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
</div>
<div className="font-serif text-4xl tracking-[-1.5px] mb-1">Shahi Kitchen</div>
<div className="text-sm font-medium tracking-[2.5px] text-[#c99a2e] mb-4">TABLE RESERVATIONS</div>
<p className="text-sm leading-relaxed text-white/70 max-w-xs mx-auto mb-4">
Two locations. Unforgettable evenings. Pre-order your favorites for a perfect arrival.
</p>
<div className="text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">ASKIM BACKAPLAN</div>
</div>
</div>
<AnimatePresence mode="wait">
{!sent ? (
<div className="max-w-4xl mx-auto space-y-8">
{/* Booking Form Card - now wider (full in max-w-4xl) */}
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10">
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.formTitle}</h2>
<p className="text-[#6B665F] mb-8">Fill in your details to secure your table.</p>
<form onSubmit={handleContinue} className="space-y-6">
{/* Location */}
<div>
<label htmlFor="location" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.locationLabel}</label>
<div className="relative">
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
<select
id="location"
name="location"
value={booking.location}
onChange={handleBookingChange}
required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
>
<option value="">{t.booking.locationPlaceholder}</option>
{locationOptions.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Date */}
<div>
<label htmlFor="date" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.dateLabel}</label>
<div className="relative">
<Calendar className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="date"
name="date"
type="date"
value={booking.date}
onChange={handleBookingChange}
min={new Date().toISOString().split('T')[0]}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Time */}
<div>
<label htmlFor="time" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.timeLabel}</label>
<div className="relative">
<Clock className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="time"
name="time"
type="time"
value={booking.time}
onChange={handleBookingChange}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
</div>
{/* Guests */}
<div>
<label htmlFor="guests" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.guestsLabel}</label>
<div className="relative">
<Users className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<select
id="guests"
name="guests"
value={booking.guests}
onChange={handleBookingChange}
required
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
>
<option value="">Select</option>
{[1,2,3,4,5,6,7,8,9,10].map(n => <option key={n} value={n}>{n}</option>)}
</select>
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]"></div>
</div>
</div>
{/* Name */}
<div>
<label htmlFor="name" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.nameLabel}</label>
<div className="relative">
<User className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="name"
name="name"
type="text"
value={booking.name}
onChange={handleBookingChange}
placeholder={t.booking.namePlaceholder}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Phone */}
<div>
<label htmlFor="phone" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.phoneLabel}</label>
<div className="relative">
<Phone className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="phone"
name="phone"
type="tel"
value={booking.phone}
onChange={handleBookingChange}
placeholder={t.booking.phonePlaceholder}
required
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Email */}
<div>
<label htmlFor="email" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.emailLabel}</label>
<div className="relative">
<Mail className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
id="email"
name="email"
type="email"
value={booking.email}
onChange={handleBookingChange}
placeholder={t.booking.emailPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
</div>
{/* Notes */}
<div>
<label htmlFor="notes" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.notesLabel}</label>
<div className="relative">
<MessageCircle className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<textarea
id="notes"
name="notes"
value={booking.notes}
onChange={handleBookingChange}
placeholder={t.booking.notesPlaceholder}
rows={3}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 resize-y"
/>
</div>
</div>
<button
type="submit"
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
>
{t.booking.continueBtn}
<ArrowRight className="h-5 w-5" />
</button>
</form>
</div>
{/* Pre-Order Menu Section - revealed after form */}
<AnimatePresence>
{showMenu && (
<motion.div
id="preorder-menu"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10"
>
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.menuTitle}</h2>
<p className="text-[#6B665F] mb-6">{t.booking.menuSubtitle}</p>
{/* Search */}
<div className="relative mb-6">
<Search className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t.booking.searchPlaceholder}
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
/>
</div>
{/* Dishes Grid - improved visualization: Name, Poster below, Desc under poster, price, nice Add button */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[320px] sm:max-h-[420px] overflow-auto pr-2">
{filteredDishes.length > 0 ? (
filteredDishes.map((dish) => {
const posterSrc = getMenuPosterSrc(dish);
const shortDesc = dish.description ? (dish.description.length > 80 ? dish.description.slice(0, 80) + '...' : dish.description) : '';
const isAdded = preOrder.some((p) => p.id === dish.id);
return (
<motion.div
key={dish.id}
whileHover={{ y: -2, boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" }}
className="flex flex-col border border-[#EDE6D9] rounded-2xl p-3 bg-white hover:border-[#B38B4D]/50 transition-all overflow-hidden"
onMouseEnter={() => playHoverSound()}
>
{/* Item Name */}
<div className="font-semibold tracking-[-0.3px] text-base mb-1 text-[#2C2A26] line-clamp-1">{dish.name}</div>
{/* Poster under name */}
<div className="relative w-full h-24 sm:h-28 md:h-32 rounded-xl overflow-hidden border border-[#EDE6D9] mb-2 bg-[#F8F5F0]">
<img
src={posterSrc}
alt={dish.name}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getMenuPosterCandidates(dish),
(e.target as HTMLImageElement).src
);
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/10" />
</div>
{/* Description under the poster */}
<div className="text-xs text-[#6B665F] leading-snug mb-2 flex-1 line-clamp-2">{shortDesc}</div>
<div className="flex items-center justify-between mt-auto pt-1">
<span className="text-[#B38B4D] font-semibold text-sm tabular-nums leading-tight text-right">
{dish.pricing === 'weight' ? (
<>
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
<span className="block text-xs text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
</>
) : (
<span>{dish.price} kr</span>
)}
</span>
{/* Improved graphics for add button */}
<button
onClick={() => {
playAddSound();
addToPreOrder(dish);
}}
className={`flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold shadow-sm active:scale-[0.96] transition-all ${
isAdded
? "bg-[#3F5C4A] text-white hover:bg-[#2a4033]"
: "bg-[#B38B4D] text-white hover:bg-[#8f6b22] hover:shadow-md"
}`}
>
<Plus className="h-3.5 w-3.5" />
{isAdded ? "Add More" : t.booking.addToPreOrder}
</button>
</div>
</motion.div>
);
})
) : (
<p className="text-[#6B665F] col-span-full text-center py-8">No dishes match your search.</p>
)}
</div>
{/* Pre-Order Summary */}
<div className="mt-8 border-t border-[#EDE6D9] pt-6">
<h3 className="font-semibold tracking-wide mb-4 flex items-center gap-2">{t.booking.yourPreOrder} <span className="text-xs text-[#B38B4D] font-normal">({preOrder.length})</span></h3>
{preOrder.length === 0 ? (
<p className="text-sm text-[#6B665F] italic">{t.booking.emptyPreOrder}</p>
) : (
<div className="space-y-3 mb-4">
{preOrder.map((item) => {
const dishInfo = allDishes.find((d) => d.id === item.id);
const thumb = dishInfo ? getMenuPosterSrc(dishInfo) : SITE_ASSETS.fallbacks.logo;
return (
<div key={item.id} className="flex items-center gap-3 bg-[#F8F5F0] rounded-2xl px-3 py-2">
{/* Small poster thumbnail in summary for better visualization */}
<img
src={thumb}
alt={item.name}
className="w-10 h-10 object-cover rounded-lg border border-[#EDE6D9] flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{item.name}</div>
<div className="text-[10px] text-[#6B665F]">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
: `${item.price} kr × ${item.quantity}`}
{' '}= {calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<button onClick={() => updatePreOrderQty(item.id, item.quantity - 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">-</button>
<span className="min-w-10 text-center font-medium text-sm tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button onClick={() => updatePreOrderQty(item.id, item.quantity + 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">+</button>
<button onClick={() => removeFromPreOrder(item.id)} className="ml-1 text-[#B38B4D] hover:text-red-600 p-0.5 active:scale-95"><X className="h-4 w-4" /></button>
</div>
</div>
);
})}
</div>
)}
<div className="flex justify-between text-lg font-medium border-t border-[#EDE6D9] pt-4">
<span>{t.booking.total}</span>
<span>{getPreOrderTotal()} kr</span>
</div>
<button
onClick={handleSendInquiry}
disabled={!validateForm()}
className="mt-6 w-full py-4 rounded-2xl text-base font-semibold tracking-[0.5px] flex items-center justify-center gap-3 bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] text-white shadow-lg hover:brightness-105 active:brightness-95 active:scale-[0.985] transition-all disabled:opacity-60 disabled:cursor-not-allowed disabled:brightness-100"
>
{t.booking.sendInquiry}
<MessageCircle className="h-5 w-5" />
</button>
<p className="mt-3 text-center text-[11px] text-[#8A8478]">{t.booking.inquiryNote}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
) : (
/* Success State */
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-md mx-auto text-center py-12"
>
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[#B38B4D]/10">
<CheckCircle className="h-10 w-10 text-[#B38B4D]" />
</div>
<h2 className="text-4xl tracking-tight font-serif mb-4">{t.booking.successTitle}</h2>
<p className="text-[#6B665F] mb-8 leading-relaxed">{t.booking.successMessage}</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<button onClick={editBooking} className="btn-outline px-8 py-3 rounded-full text-sm font-medium">
{t.booking.editBooking}
</button>
<button onClick={resetAll} className="btn-primary px-8 py-3 rounded-full text-sm font-medium">
{t.booking.backHome}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
</main>
<Footer />
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import ScreenDisplay from '@/components/ScreenDisplay';
export const metadata: Metadata = {
title: 'Shahi Kitchen | Display Screen 1',
description: 'In-restaurant menu visualization — Screen 1',
robots: {
index: false,
follow: false,
},
};
export default function Screen1Page() {
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
// Makes screen1 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
// Pairs with the .screen-root + useEffect in ScreenDisplay for robustness.
return (
<>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
}}
/>
<ScreenDisplay screen={1} />
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import ScreenDisplay from '@/components/ScreenDisplay';
export const metadata: Metadata = {
title: 'Shahi Kitchen | Display Screen 2',
description: 'In-restaurant menu visualization — Screen 2',
robots: {
index: false,
follow: false,
},
};
export default function Screen2Page() {
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
// Makes screen2 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
// Pairs with the .screen-root + useEffect in ScreenDisplay for robustness.
return (
<>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
}}
/>
<ScreenDisplay screen={2} />
</>
);
}
+27
View File
@@ -0,0 +1,27 @@
import type { Metadata } from 'next';
import OrderDisplay from '@/components/OrderDisplay';
export const metadata: Metadata = {
title: 'Shahi Kitchen | Live Order Display',
description: 'Live kitchen order board — in-house tables & online delivery (Foodora etc.) for customers and riders. Real-time status.',
robots: {
index: false,
follow: false,
},
};
export default function Screen3Page() {
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
// Makes screen3 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
// Pairs with .screen-root for the order board.
return (
<>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
}}
/>
<OrderDisplay />
</>
);
}
+28
View File
@@ -0,0 +1,28 @@
import type { MenuItem } from '@/domain/menu/entities';
import type { NewOrderLine } from '@/domain/shared/order-line';
import {
SWEETS_HALF_KG_PRICE,
SWEETS_KG_PRICE,
} from '@/domain/sweets/pricing';
export function buildCartLineFromMenuItem(item: MenuItem): NewOrderLine {
if (item.pricing === 'weight') {
return {
id: item.id,
name: item.name,
price: item.pricePerHalfKg ?? SWEETS_HALF_KG_PRICE,
image: item.image,
pricingMode: 'weight',
pricePerKg: item.pricePerKg ?? SWEETS_KG_PRICE,
pricePerHalfKg: item.pricePerHalfKg ?? SWEETS_HALF_KG_PRICE,
};
}
return {
id: item.id,
name: item.name,
price: item.price,
image: item.image,
pricingMode: 'standard',
};
}
@@ -0,0 +1,29 @@
import {
DEFAULT_LANGUAGE,
isLanguage,
type Language,
} from '@/domain/language/entities';
import type { LanguageRepository } from '@/domain/language/repository';
export function detectBrowserLanguage(): Language {
if (typeof navigator === 'undefined') return DEFAULT_LANGUAGE;
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith('sv')) return 'sv';
if (browserLang.startsWith('ar')) return 'ar';
if (browserLang.startsWith('tr')) return 'tr';
if (browserLang.startsWith('hi')) return 'hi';
if (browserLang.startsWith('ur')) return 'ur';
if (browserLang.startsWith('en')) return 'en';
return DEFAULT_LANGUAGE;
}
export function resolveInitialLanguage(repository: LanguageRepository): Language {
const saved = repository.load();
if (saved && isLanguage(saved)) return saved;
return detectBrowserLanguage();
}
export function persistLanguage(repository: LanguageRepository, language: Language): void {
repository.save(language);
}
+126
View File
@@ -0,0 +1,126 @@
import type { MenuItem } from '@/domain/menu/entities';
import {
MEDIA_FOLDERS,
SITE_ASSETS,
mediaUrl,
} from '@/infrastructure/assets/site-assets';
export type PosterVariant = 'standard' | 'optimized';
/** Strip .mp4 extension from a video filename */
export function videoBaseName(videoFilename: string): string {
return videoFilename.replace(/\.mp4$/i, '');
}
/** Full URL for a dish image filename (e.g. "butter-chicken.jpg") */
export function dishImageUrl(filename: string): string {
return mediaUrl(MEDIA_FOLDERS.dishes, filename);
}
/** Poster URL derived from a video filename */
export function dishPosterFromVideo(
videoFilename: string,
variant: PosterVariant = 'standard'
): string {
const base = videoBaseName(videoFilename);
const suffix = variant === 'optimized' ? '-optimized-poster.jpg' : '-poster.jpg';
return mediaUrl(MEDIA_FOLDERS.dishes, `${base}${suffix}`);
}
/** Primary logo used in navbar, footer, login, etc. */
export function logoUrl(): string {
return SITE_ASSETS.logo.primary;
}
/** Hero banner video sources for homepage */
export function heroBannerSources() {
return {
mobileWebm: SITE_ASSETS.banner.mobileWebm,
mobileMp4: SITE_ASSETS.banner.mobileMp4,
desktopMp4: SITE_ASSETS.banner.desktopMp4,
};
}
/** Chef expression images for PlayfulHeroScene */
export function chefExpressionUrl(expression: 'wink' | 'smile' | 'normal'): string {
const map = {
wink: SITE_ASSETS.animation.chefWink,
smile: SITE_ASSETS.animation.chefSmile,
normal: SITE_ASSETS.animation.chefNormal,
};
return map[expression];
}
/** Resolve primary poster path for a menu item */
export function getMenuPosterSrc(
item: MenuItem,
variant: PosterVariant = 'standard'
): string {
// Prefer static dish photo when available (sweets use synced .jpg, not -poster.jpg)
if (item.image) {
return dishImageUrl(item.image);
}
if (item.video) {
return dishPosterFromVideo(item.video, variant);
}
return variant === 'optimized'
? SITE_ASSETS.dishes.defaultPoster
: SITE_ASSETS.fallbacks.dishPoster;
}
/** Poster candidates for progressive fallback in UI `<img onError>` */
export function getMenuPosterCandidates(item: MenuItem): string[] {
const base = item.video ? videoBaseName(item.video) : '';
const candidates: string[] = [];
if (item.image) candidates.push(dishImageUrl(item.image));
if (item.video) {
candidates.push(
dishPosterFromVideo(item.video, 'standard'),
dishPosterFromVideo(item.video, 'optimized'),
mediaUrl(MEDIA_FOLDERS.dishes, `${base}.jpg`),
);
}
candidates.push(
SITE_ASSETS.dishes.defaultPoster,
SITE_ASSETS.fallbacks.dishPoster,
SITE_ASSETS.fallbacks.logo,
);
return [...new Set(candidates)];
}
/** Full video URL for a menu item */
export function getMenuVideoSrc(item: MenuItem): string | null {
if (!item.video) return null;
return mediaUrl(MEDIA_FOLDERS.videos, item.video);
}
/** Optimized + fallback video sources for kiosk displays */
export function getMenuVideoSources(videoFilename: string) {
if (!videoFilename) {
return { webm: '', mp4: '', fallback: '' };
}
const base = videoBaseName(videoFilename);
return {
webm: mediaUrl(MEDIA_FOLDERS.videos, `${base}-optimized.webm`),
mp4: mediaUrl(MEDIA_FOLDERS.videos, `${base}-optimized.mp4`),
fallback: mediaUrl(MEDIA_FOLDERS.videos, `${base}.mp4`),
};
}
/** Apply next fallback src in an img onError handler */
export function applyNextImageFallback(
target: HTMLImageElement,
candidates: string[],
currentSrc: string
): void {
const currentIndex = candidates.indexOf(currentSrc);
const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
if (nextIndex < candidates.length) {
target.src = candidates[nextIndex];
}
}
// Re-export registry for direct access when needed
export { SITE_ASSETS, MEDIA_FOLDERS, mediaUrl } from '@/infrastructure/assets/site-assets';
+45
View File
@@ -0,0 +1,45 @@
import type { MenuCategory, MenuFilter, MenuItem } from '@/domain/menu/entities';
export function filterMenuCategories(
categories: MenuCategory[],
filter: MenuFilter
): MenuCategory[] {
const query = filter.searchQuery?.toLowerCase().trim() ?? '';
return categories
.map((category) => {
if (filter.categoryId && filter.categoryId !== 'All' && category.id !== filter.categoryId) {
return { ...category, items: [] };
}
let items = category.items;
if (query) {
items = items.filter(
(item) =>
item.name.toLowerCase().includes(query) ||
(item.description?.toLowerCase().includes(query) ?? false)
);
}
if (filter.vegetarianOnly) {
items = items.filter((item) => item.isVegetarian);
}
return { ...category, items };
})
.filter((category) => category.items.length > 0);
}
export function filterMenuItems(items: MenuItem[], filter: MenuFilter): MenuItem[] {
const query = filter.searchQuery?.toLowerCase().trim() ?? '';
return items.filter((item) => {
if (filter.vegetarianOnly && !item.isVegetarian) return false;
if (!query) return true;
return (
item.name.toLowerCase().includes(query) ||
(item.description?.toLowerCase().includes(query) ?? false)
);
});
}
+13
View File
@@ -0,0 +1,13 @@
/**
* @deprecated Import from `@/application/media/asset-resolver` instead.
* Kept for backward compatibility.
*/
export {
getMenuPosterSrc,
getMenuPosterCandidates,
getMenuVideoSrc,
getMenuVideoSources,
dishImageUrl,
dishPosterFromVideo,
type PosterVariant,
} from '@/application/media/asset-resolver';
+4
View File
@@ -0,0 +1,4 @@
/** Port: external messaging channel (WhatsApp, SMS, etc.). */
export interface MessagingGateway {
openWhatsApp(message: string): void;
}
@@ -0,0 +1,122 @@
import type { DeliveryInquiryDetails, PickupInquiryDetails } from '@/domain/cart/inquiry';
import type { BookingDetails } from '@/domain/booking/entities';
import { isBookingComplete } from '@/domain/booking/validation';
import type { Language } from '@/domain/language/entities';
import {
calculateOrderTotal,
formatOrderLinesForInquiry,
formatOrderLinesForMessage,
type OrderLine,
} from '@/domain/shared/order-line';
import { PICKUP_LEAD_TIME_MINUTES } from '@/domain/shared/constants';
export interface CartInquiryCopy {
pickupIntro: string;
deliveryIntro: string;
messageTotal: string;
messageName: string;
messagePhone: string;
messageAddress: string;
messagePreferredTime: string;
deliverySwishNote: string;
}
/** @deprecated Legacy cart copy — kept for any remaining references */
export interface CartDrawerCopy {
messageHello: string;
messageIntro: string;
messageAddress: string;
messageConfirm: string;
messageTotal: string;
messageThanks: string;
}
export interface BookingLocationCopy {
askim: string;
backaplan: string;
}
export function getSuggestedPickupTime(language: Language, from: Date = new Date()): string {
const suggested = new Date(from.getTime() + PICKUP_LEAD_TIME_MINUTES * 60 * 1000);
const timeLocale =
language === 'sv' ? 'sv-SE' :
language === 'ar' ? 'ar-SA' :
language === 'tr' ? 'tr-TR' :
'en-GB';
return suggested.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit' });
}
export function buildPickupInquiryMessage(
items: OrderLine[],
details: PickupInquiryDetails,
copy: CartInquiryCopy,
): string | null {
if (items.length === 0) return null;
const lines = formatOrderLinesForInquiry(items);
const total = calculateOrderTotal(items);
return `${copy.pickupIntro}
${lines}
${copy.messageTotal}: ${total} kr
${copy.messageName}: ${details.name.trim()}
${copy.messagePhone}: ${details.phone.trim()}`;
}
export function buildDeliveryInquiryMessage(
items: OrderLine[],
details: DeliveryInquiryDetails,
copy: CartInquiryCopy,
): string | null {
if (items.length === 0) return null;
const lines = formatOrderLinesForInquiry(items);
const total = calculateOrderTotal(items);
return `${copy.deliveryIntro}
${lines}
${copy.messageTotal}: ${total} kr
${copy.messageName}: ${details.name.trim()}
${copy.messageAddress}: ${details.address.trim()}
${copy.messagePhone}: ${details.phone.trim()}
${copy.messagePreferredTime}: ${details.preferredTime.trim()}
${copy.deliverySwishNote}`;
}
export function buildBookingWhatsAppMessage(
booking: BookingDetails,
preOrder: OrderLine[],
locationCopy: BookingLocationCopy
): string | null {
if (!isBookingComplete(booking)) return null;
const locationLabel =
booking.location === 'askim' ? locationCopy.askim : locationCopy.backaplan;
const itemsText =
preOrder.length > 0
? formatOrderLinesForMessage(preOrder).replace(/ — /g, ' — ')
: 'None';
const totalText =
preOrder.length > 0 ? `\nPre-order total: ${calculateOrderTotal(preOrder)} kr\n` : '';
return `Hello Shahi Kitchen 👋
Table Reservation Inquiry:
Name: ${booking.name}
Phone: ${booking.phone}
Email: ${booking.email || 'N/A'}
Location: ${locationLabel}
Date: ${booking.date}
Time: ${booking.time}
Guests: ${booking.guests}
Special Requests: ${booking.notes || 'None'}
Pre-ordered items:
${itemsText}${totalText}
Please confirm table availability and pre-order.
Thank you!`;
}
@@ -0,0 +1,28 @@
import type { Branch } from '@/domain/table/table-id';
import type { OrderLine } from '@/domain/shared/order-line';
export interface TableOrderPayload {
branch: Branch;
tableNumber: string;
tableIdentifier: string;
items: OrderLine[];
notes?: string;
createdAt: string;
}
export function buildTableOrderPayload(
branch: Branch,
tableNumber: string,
tableIdentifier: string,
items: OrderLine[],
notes?: string
): TableOrderPayload {
return {
branch,
tableNumber,
tableIdentifier,
items,
notes: notes?.trim() || undefined,
createdAt: new Date().toISOString(),
};
}
+320
View File
@@ -0,0 +1,320 @@
'use client';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Environment } from '@react-three/drei';
import { Suspense, useMemo, useRef } from 'react';
import * as THREE from 'three';
// ================== SMOKE PARTICLES ==================
function SmokeParticles({ count = 120 }: { count?: number }) {
const pointsRef = useRef<THREE.Points>(null!);
const particles = useMemo(() => {
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
const ages = new Float32Array(count);
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Start around the top of the gravy
positions[i3 + 0] = (Math.random() - 0.5) * 2.2;
positions[i3 + 1] = 0.6 + Math.random() * 0.3;
positions[i3 + 2] = (Math.random() - 0.5) * 2.0;
velocities[i3 + 0] = (Math.random() - 0.5) * 0.008;
velocities[i3 + 1] = 0.012 + Math.random() * 0.018;
velocities[i3 + 2] = (Math.random() - 0.5) * 0.008;
ages[i] = Math.random() * 3.5;
sizes[i] = 0.12 + Math.random() * 0.18;
}
return { positions, velocities, ages, sizes };
}, [count]);
useFrame((state, delta) => {
const points = pointsRef.current;
if (!points) return;
const pos = points.geometry.attributes.position as THREE.BufferAttribute;
const posArray = pos.array as Float32Array;
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Age and reset
particles.ages[i] += delta * 0.9;
if (particles.ages[i] > 3.8) {
// Respawn at the top of the gravy
particles.ages[i] = 0;
posArray[i3 + 0] = (Math.random() - 0.5) * 2.1;
posArray[i3 + 1] = 0.55 + Math.random() * 0.15;
posArray[i3 + 2] = (Math.random() - 0.5) * 1.9;
particles.velocities[i3 + 0] = (Math.random() - 0.5) * 0.009;
particles.velocities[i3 + 1] = 0.014 + Math.random() * 0.02;
} else {
// Rise with some turbulence
posArray[i3 + 0] += particles.velocities[i3 + 0] + Math.sin(state.clock.elapsedTime * 1.5 + i) * 0.002;
posArray[i3 + 1] += particles.velocities[i3 + 1];
posArray[i3 + 2] += particles.velocities[i3 + 2] + Math.cos(state.clock.elapsedTime * 1.2 + i) * 0.002;
// Slow down as it rises
particles.velocities[i3 + 1] *= 0.992;
}
}
pos.needsUpdate = true;
});
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(particles.positions, 3));
return geo;
}, [particles.positions]);
return (
<points ref={pointsRef} geometry={geometry}>
<pointsMaterial
size={0.22}
color="#f4e9d8"
transparent
opacity={0.32}
depthWrite={false}
sizeAttenuation={true}
/>
</points>
);
}
// ================== BOWL ==================
function Bowl() {
return (
<group>
{/* Outer bowl */}
<mesh position={[0, -0.25, 0]} castShadow receiveShadow>
<cylinderGeometry args={[2.35, 1.95, 1.95, 72, 1, true]} />
<meshPhongMaterial
color="#2f2723"
shininess={45}
specular="#3a2f2a"
side={THREE.DoubleSide}
/>
</mesh>
{/* Inner bowl wall */}
<mesh position={[0, -0.25, 0]} castShadow receiveShadow>
<cylinderGeometry args={[2.1, 1.72, 1.85, 72, 1, true]} />
<meshPhongMaterial
color="#1f1a17"
shininess={25}
side={THREE.DoubleSide}
/>
</mesh>
{/* Bowl bottom inside */}
<mesh position={[0, -1.15, 0]} receiveShadow>
<cylinderGeometry args={[1.72, 1.72, 0.12, 72]} />
<meshPhongMaterial color="#1f1a17" />
</mesh>
</group>
);
}
// ================== GRAVY ==================
function Gravy() {
return (
<group>
{/* Main thick gravy */}
<mesh position={[0, 0.05, 0]} receiveShadow>
<cylinderGeometry args={[2.0, 1.65, 1.35, 72]} />
<meshPhongMaterial
color="#d46f2e"
shininess={95}
specular="#ffe8c4"
/>
</mesh>
{/* Creamy top layer */}
<mesh position={[0, 0.55, 0]}>
<cylinderGeometry args={[1.92, 1.62, 0.42, 72]} />
<meshPhongMaterial
color="#f0a45f"
shininess={120}
specular="#fff4d9"
/>
</mesh>
{/* Glossy highlights layer */}
<mesh position={[0, 0.68, 0]}>
<cylinderGeometry args={[1.78, 1.55, 0.18, 72]} />
<meshPhongMaterial
color="#f8c48a"
shininess={140}
specular="#ffffff"
transparent
opacity={0.65}
/>
</mesh>
</group>
);
}
// ================== CHICKEN PIECES ==================
function ChickenPieces() {
const pieces = [
{ pos: [0.6, 0.45, 0.1], rot: [0.6, 1.2, 0.3], scale: 1.0 },
{ pos: [-0.75, 0.38, 0.55], rot: [-0.4, -0.9, 0.5], scale: 0.95 },
{ pos: [0.15, 0.52, -0.85], rot: [0.9, 0.4, -0.6], scale: 1.05 },
{ pos: [-0.55, 0.42, -0.4], rot: [-0.7, 1.6, 0.2], scale: 0.9 },
{ pos: [0.9, 0.35, -0.55], rot: [0.3, -1.1, -0.4], scale: 0.98 },
{ pos: [-0.2, 0.48, 0.75], rot: [-0.5, 0.7, 0.8], scale: 0.92 },
];
return (
<>
{pieces.map((p, i) => (
<group key={i} position={p.pos as [number, number, number]} rotation={p.rot as [number, number, number]} scale={p.scale}>
{/* Main chicken body */}
<mesh castShadow>
<capsuleGeometry args={[0.42, 0.65, 8]} />
<meshPhongMaterial
color="#b84f25"
shininess={55}
specular="#3a1f14"
/>
</mesh>
{/* Extra volume */}
<mesh position={[0.12, 0.08, -0.1]} castShadow>
<sphereGeometry args={[0.38]} />
<meshPhongMaterial color="#a64520" shininess={40} />
</mesh>
</group>
))}
</>
);
}
// ================== HERBS & SPICES ==================
function Toppings() {
return (
<>
{/* Green herbs */}
{Array.from({ length: 18 }).map((_, i) => {
const angle = i * 0.7 + (i % 3) * 0.3;
const radius = 0.55 + (i % 4) * 0.22;
return (
<mesh
key={`herb-${i}`}
position={[
Math.cos(angle) * radius,
0.82,
Math.sin(angle) * radius * 0.9
]}
rotation={[Math.random() - 0.5, i, Math.random() - 0.5]}
>
<planeGeometry args={[0.22, 0.09]} />
<meshPhongMaterial
color="#2d5c3f"
side={THREE.DoubleSide}
transparent
opacity={0.85}
/>
</mesh>
);
})}
{/* Small spice bits */}
{Array.from({ length: 26 }).map((_, i) => (
<mesh
key={`spice-${i}`}
position={[
(Math.random() - 0.5) * 2.6,
0.78 + Math.random() * 0.12,
(Math.random() - 0.5) * 2.3
]}
>
<sphereGeometry args={[0.035 + Math.random() * 0.025]} />
<meshPhongMaterial color={i % 4 === 0 ? "#8c3a1f" : "#3a2a1f"} />
</mesh>
))}
</>
);
}
// ================== MAIN MODEL ==================
function ButterChickenModel() {
return (
<group>
<Bowl />
<Gravy />
<ChickenPieces />
<Toppings />
{/* Rising Smoke */}
<SmokeParticles count={95} />
</group>
);
}
// ================== MAIN EXPORT ==================
export default function ButterChicken3D() {
return (
<div className="w-full h-full min-h-[420px] rounded-2xl overflow-hidden bg-[#F2EDE4] relative">
<Canvas
camera={{ position: [0, 3.8, 8.5], fov: 40 }}
style={{ background: 'transparent' }}
gl={{
antialias: true,
alpha: true,
preserveDrawingBuffer: true,
toneMapping: THREE.ACESFilmicToneMapping,
toneMappingExposure: 1.05,
}}
>
<Suspense fallback={null}>
{/* Warm restaurant lighting */}
<ambientLight intensity={0.28} color="#fff4e6" />
<directionalLight
position={[7, 13, 5]}
intensity={1.85}
color="#fff0d0"
castShadow
/>
<directionalLight
position={[-7, 5, -8]}
intensity={0.95}
color="#ffe8c4"
/>
<pointLight position={[-3, 4, 7]} intensity={0.7} color="#fff8e7" />
<ButterChickenModel />
<Environment preset="apartment" />
</Suspense>
<OrbitControls
enablePan={false}
enableZoom={true}
minDistance={4.5}
maxDistance={13}
minPolarAngle={Math.PI * 0.18}
maxPolarAngle={Math.PI * 0.82}
enableDamping
dampingFactor={0.12}
autoRotate={false}
/>
</Canvas>
<div className="absolute bottom-3 right-3 text-[10px] text-[#8A8478] tracking-widest pointer-events-none">
DRAG TO ROTATE SCROLL TO ZOOM
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @deprecated Import from `@/presentation/providers/cart-provider`.
* Kept for backward compatibility during Clean Architecture migration.
*/
export {
CartProvider,
useCart,
type CartItem,
} from '@/presentation/providers/cart-provider';
+247
View File
@@ -0,0 +1,247 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { MessageCircle } from 'lucide-react';
import {
buildDeliveryInquiryMessage,
buildPickupInquiryMessage,
} from '@/application/messaging/whatsapp-message-builder';
import type { FulfillmentMode } from '@/domain/cart/inquiry';
import type { DeliveryInquiryDetails, PickupInquiryDetails } from '@/domain/cart/inquiry';
import {
calculateLineTotal,
formatLineQuantityLabel,
} from '@/domain/shared/order-line';
import { RESTAURANT_CONTACT } from '@/domain/shared/constants';
import { container } from '@/infrastructure/di/container';
import { useCart } from '@/presentation/providers/cart-provider';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import CartInquiryModal from '@/components/CartInquiryModal';
export default function CartDrawer() {
const {
items,
isOpen,
closeCart,
totalPrice,
removeFromCart,
updateQuantity,
clearCart,
} = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
const [inquiryMode, setInquiryMode] = useState<FulfillmentMode | null>(null);
const inquiryCopy = {
pickupIntro: t.cartDrawer.inquiryPickupIntro,
deliveryIntro: t.cartDrawer.inquiryDeliveryIntro,
messageTotal: t.cartDrawer.messageTotal,
messageName: t.cartDrawer.messageName,
messagePhone: t.cartDrawer.messagePhone,
messageAddress: t.cartDrawer.messageAddress,
messagePreferredTime: t.cartDrawer.messagePreferredTime,
deliverySwishNote: t.cartDrawer.deliverySwishNote,
};
const openWhatsApp = (message: string | null) => {
if (!message) return;
container.messagingGateway.openWhatsApp(message);
setInquiryMode(null);
};
const handlePickupSubmit = (details: PickupInquiryDetails) => {
openWhatsApp(buildPickupInquiryMessage(items, details, inquiryCopy));
};
const handleDeliverySubmit = (details: DeliveryInquiryDetails) => {
openWhatsApp(buildDeliveryInquiryMessage(items, details, inquiryCopy));
};
if (!isOpen) return null;
return (
<>
<div
className="fixed inset-0 bg-black/40 z-[980]"
onClick={closeCart}
/>
<div className="fixed top-0 right-0 h-full w-full max-w-md bg-[#F8F5F0] z-[990] shadow-2xl flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]">
<div className="flex items-center gap-3">
<h2 className="text-2xl tracking-[-0.5px]">{t.cartDrawer.title}</h2>
{items.length > 0 && (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-[#EDE6D9] text-[#6B665F]">
{items.length} {items.length === 1 ? t.cartDrawer.item : t.cartDrawer.items}
</span>
)}
</div>
<div className="flex items-center gap-3">
{items.length > 0 && (
<button
onClick={clearCart}
className="text-xs text-[#B38B4D] hover:underline"
>
{t.cartDrawer.clear}
</button>
)}
<button
onClick={closeCart}
className="text-[#6B665F] hover:text-[#2C2A26] text-2xl leading-none pl-1"
>
×
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<div className="text-6xl mb-4">🛒</div>
<p className="text-lg text-[#6B665F]">{t.cartDrawer.empty}</p>
<Link
href="/menu"
onClick={closeCart}
className="mt-6 text-[#B38B4D] hover:underline"
>
{t.cartDrawer.browseMenu}
</Link>
</div>
) : (
<div className="space-y-6">
{items.map((item) => (
<div key={item.id} className="flex gap-4 border-b border-[#EDE6D9] pb-6">
<div className="flex-1">
<div className="flex justify-between">
<div>
<h4 className="font-medium tracking-[-0.3px]">{item.name}</h4>
<p className="text-sm text-[#6B665F]">
{item.pricingMode === 'weight'
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg`
: `${item.price} kr × ${item.quantity}`}
</p>
</div>
<div className="text-right font-medium">
{calculateLineTotal(item)} kr
</div>
</div>
<div className="flex items-center gap-3 mt-3">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-8 h-8 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition"
>
</button>
<span className="min-w-12 text-center font-medium tabular-nums">
{formatLineQuantityLabel(item)}
</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-8 h-8 flex items-center justify-center border border-[#EDE6D9] rounded hover:bg-white transition"
>
+
</button>
<button
onClick={() => removeFromCart(item.id)}
className="ml-auto text-xs text-[#B38B4D] hover:underline"
>
{t.cartDrawer.remove}
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{items.length > 0 && (
<div className="p-6 border-t border-[#EDE6D9] bg-white">
<button
onClick={clearCart}
className="text-xs text-[#8A8478] hover:text-[#B38B4D] mb-3 underline"
>
{t.cartDrawer.clearBasket}
</button>
<div className="flex justify-between text-lg font-medium mb-4">
<span>{t.cartDrawer.total}</span>
<span>{totalPrice.toFixed(0)} kr</span>
</div>
<p className="text-xs text-center text-[#6B665F] mb-4">
{t.cartDrawer.inquiryHint}
</p>
<button
type="button"
onClick={() => setInquiryMode('pickup')}
className="btn-primary w-full py-4 rounded-full text-base tracking-[0.5px] font-medium mb-2 flex items-center justify-center gap-2"
>
<MessageCircle className="h-5 w-5" aria-hidden />
{t.cartDrawer.pickupInquiry}
</button>
<button
type="button"
onClick={() => setInquiryMode('delivery')}
className="btn-outline w-full py-4 rounded-full text-base tracking-[0.5px] font-medium mb-2 flex items-center justify-center gap-2 border-[#25D366] text-[#128C7E] hover:bg-[#25D366]/10"
>
<MessageCircle className="h-5 w-5" aria-hidden />
{t.cartDrawer.deliveryInquiry}
</button>
<button
onClick={() => window.open(`tel:${RESTAURANT_CONTACT.phonePrimary}`, '_self')}
className="btn-outline w-full py-3 rounded-full text-sm tracking-[0.5px] font-medium mb-3"
>
{t.cartDrawer.call}
</button>
<button
onClick={closeCart}
className="w-full text-sm text-[#6B665F] hover:text-[#2C2A26] pt-1"
>
{t.cartDrawer.continueBrowsing}
</button>
</div>
)}
</div>
{inquiryMode && (
<CartInquiryModal
mode={inquiryMode}
isOpen={!!inquiryMode}
onClose={() => setInquiryMode(null)}
onSubmitPickup={handlePickupSubmit}
onSubmitDelivery={handleDeliverySubmit}
labels={{
titlePickup: t.cartDrawer.inquiryModalTitlePickup,
titleDelivery: t.cartDrawer.inquiryModalTitleDelivery,
nameLabel: t.cartDrawer.nameLabel,
namePlaceholder: t.cartDrawer.namePlaceholder,
phoneLabel: t.cartDrawer.phoneLabel,
phonePlaceholder: t.cartDrawer.phonePlaceholder,
addressLabel: t.cartDrawer.addressLabel,
addressPlaceholder: t.cartDrawer.addressPlaceholder,
preferredTimeLabel: t.cartDrawer.preferredTimeLabel,
preferredTimePlaceholder: t.cartDrawer.preferredTimePlaceholder,
submitPickup: t.cartDrawer.submitPickup,
submitDelivery: t.cartDrawer.submitDelivery,
cancel: t.cartDrawer.inquiryCancel,
close: t.cartDrawer.inquiryClose,
nameRequired: t.cartDrawer.nameRequired,
phoneRequired: t.cartDrawer.phoneRequired,
addressRequired: t.cartDrawer.addressRequired,
timeRequired: t.cartDrawer.timeRequired,
}}
/>
)}
</>
);
}
+254
View File
@@ -0,0 +1,254 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { MessageCircle, X } from 'lucide-react';
import type { FulfillmentMode } from '@/domain/cart/inquiry';
import type { DeliveryInquiryDetails, PickupInquiryDetails } from '@/domain/cart/inquiry';
export interface CartInquiryModalLabels {
titlePickup: string;
titleDelivery: string;
nameLabel: string;
namePlaceholder: string;
phoneLabel: string;
phonePlaceholder: string;
addressLabel: string;
addressPlaceholder: string;
preferredTimeLabel: string;
preferredTimePlaceholder: string;
submitPickup: string;
submitDelivery: string;
cancel: string;
close: string;
nameRequired: string;
phoneRequired: string;
addressRequired: string;
timeRequired: string;
}
interface CartInquiryModalProps {
mode: FulfillmentMode;
isOpen: boolean;
onClose: () => void;
onSubmitPickup: (details: PickupInquiryDetails) => void;
onSubmitDelivery: (details: DeliveryInquiryDetails) => void;
labels: CartInquiryModalLabels;
}
const emptyPickup = (): PickupInquiryDetails => ({ name: '', phone: '' });
const emptyDelivery = (): DeliveryInquiryDetails => ({
name: '',
phone: '',
address: '',
preferredTime: '',
});
export default function CartInquiryModal({
mode,
isOpen,
onClose,
onSubmitPickup,
onSubmitDelivery,
labels,
}: CartInquiryModalProps) {
const [pickup, setPickup] = useState(emptyPickup);
const [delivery, setDelivery] = useState(emptyDelivery);
const [errors, setErrors] = useState<Record<string, string>>({});
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
},
[onClose],
);
useEffect(() => {
if (!isOpen) return;
setErrors({});
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, handleKeyDown, mode]);
useEffect(() => {
if (!isOpen) {
setPickup(emptyPickup());
setDelivery(emptyDelivery());
setErrors({});
}
}, [isOpen]);
if (!isOpen) return null;
const title = mode === 'pickup' ? labels.titlePickup : labels.titleDelivery;
const submitLabel = mode === 'pickup' ? labels.submitPickup : labels.submitDelivery;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const nextErrors: Record<string, string> = {};
if (mode === 'pickup') {
if (!pickup.name.trim()) nextErrors.name = labels.nameRequired;
if (!pickup.phone.trim()) nextErrors.phone = labels.phoneRequired;
if (Object.keys(nextErrors).length) {
setErrors(nextErrors);
return;
}
onSubmitPickup(pickup);
return;
}
if (!delivery.name.trim()) nextErrors.name = labels.nameRequired;
if (!delivery.phone.trim()) nextErrors.phone = labels.phoneRequired;
if (!delivery.address.trim()) nextErrors.address = labels.addressRequired;
if (!delivery.preferredTime.trim()) nextErrors.preferredTime = labels.timeRequired;
if (Object.keys(nextErrors).length) {
setErrors(nextErrors);
return;
}
onSubmitDelivery(delivery);
};
const inputClass =
'mt-1.5 w-full rounded-xl border border-[#EDE6D9] bg-[#FFFCF7] px-3 py-2.5 text-sm text-[#2C2A26] placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20';
return (
<>
<div
className="fixed inset-0 z-[1000] bg-black/50 backdrop-blur-[2px]"
onClick={onClose}
aria-hidden
/>
<div
className="fixed inset-0 z-[1010] flex items-end sm:items-center justify-center p-0 sm:p-6 pointer-events-none"
role="dialog"
aria-modal="true"
aria-labelledby="cart-inquiry-title"
>
<div
className="pointer-events-auto w-full sm:max-w-md max-h-[90dvh] overflow-y-auto rounded-t-3xl sm:rounded-2xl bg-[#FFFCF7] border border-[#EDE6D9] shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-[#EDE6D9] px-6 py-4">
<h2 id="cart-inquiry-title" className="font-serif text-xl tracking-[-0.3px] text-[#101724]">
{title}
</h2>
<button
type="button"
onClick={onClose}
aria-label={labels.close}
className="flex h-9 w-9 items-center justify-center rounded-full border border-[#EDE6D9] bg-white text-[#6B665F] hover:text-[#101724]"
>
<X className="h-4 w-4" aria-hidden />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4 p-6">
<div>
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
{labels.nameLabel}
</label>
<input
type="text"
value={mode === 'pickup' ? pickup.name : delivery.name}
onChange={(e) => {
const v = e.target.value;
if (mode === 'pickup') setPickup((p) => ({ ...p, name: v }));
else setDelivery((d) => ({ ...d, name: v }));
if (errors.name) setErrors((err) => ({ ...err, name: '' }));
}}
placeholder={labels.namePlaceholder}
className={inputClass}
autoComplete="name"
/>
{errors.name && (
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.name}</p>
)}
</div>
<div>
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
{labels.phoneLabel}
</label>
<input
type="tel"
value={mode === 'pickup' ? pickup.phone : delivery.phone}
onChange={(e) => {
const v = e.target.value;
if (mode === 'pickup') setPickup((p) => ({ ...p, phone: v }));
else setDelivery((d) => ({ ...d, phone: v }));
if (errors.phone) setErrors((err) => ({ ...err, phone: '' }));
}}
placeholder={labels.phonePlaceholder}
className={inputClass}
autoComplete="tel"
/>
{errors.phone && (
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.phone}</p>
)}
</div>
{mode === 'delivery' && (
<>
<div>
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
{labels.addressLabel}
</label>
<textarea
value={delivery.address}
onChange={(e) => {
setDelivery((d) => ({ ...d, address: e.target.value }));
if (errors.address) setErrors((err) => ({ ...err, address: '' }));
}}
placeholder={labels.addressPlaceholder}
rows={3}
className={`${inputClass} resize-none`}
/>
{errors.address && (
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.address}</p>
)}
</div>
<div>
<label className="text-xs font-medium tracking-wide text-[#2C2A26]">
{labels.preferredTimeLabel}
</label>
<input
type="text"
value={delivery.preferredTime}
onChange={(e) => {
setDelivery((d) => ({ ...d, preferredTime: e.target.value }));
if (errors.preferredTime) setErrors((err) => ({ ...err, preferredTime: '' }));
}}
placeholder={labels.preferredTimePlaceholder}
className={inputClass}
/>
{errors.preferredTime && (
<p className="mt-1 text-xs text-[#B45309]" role="alert">{errors.preferredTime}</p>
)}
</div>
</>
)}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 rounded-full border border-[#EDE6D9] py-3 text-sm font-medium text-[#6B665F] hover:bg-[#F8F5F0]"
>
{labels.cancel}
</button>
<button
type="submit"
className="btn-primary flex flex-1 items-center justify-center gap-2 rounded-full py-3 text-sm font-medium tracking-wide"
>
<MessageCircle className="h-4 w-4" aria-hidden />
{submitLabel}
</button>
</div>
</form>
</div>
</div>
</>
);
}
+122
View File
@@ -0,0 +1,122 @@
'use client';
import { dishImageUrl } from '@/lib/assets';
import type { CateringPackage } from '@/lib/catering-data';
import {
CATERING_CATEGORY_ORDER,
groupDishesByCategory,
} from '@/lib/catering-data';
import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations';
import { Users, UtensilsCrossed } from 'lucide-react';
import { motion } from 'framer-motion';
interface CateringPackageCardProps {
pkg: CateringPackage;
index?: number;
}
export default function CateringPackageCard({ pkg, index = 0 }: CateringPackageCardProps) {
const { language } = useLanguage();
const t = getTranslation(language);
const grouped = groupDishesByCategory(pkg.includedDishes);
const packageName =
(t.catering.packages as Record<string, string>)?.[pkg.id] ?? pkg.name;
const packageDescription =
(t.catering.packageDescriptions as Record<string, string>)?.[pkg.id] ?? pkg.description;
return (
<motion.article
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-40px' }}
transition={{ duration: 0.55, delay: index * 0.08, ease: [0.25, 1, 0.5, 1] }}
className={`luxury-card flex h-full flex-col overflow-hidden rounded-[2rem] border bg-white transition-all duration-300 ${
pkg.highlight
? 'border-[#c99a2e]/50 shadow-lg shadow-[#c99a2e]/15 ring-1 ring-[#c99a2e]/25'
: 'border-[#EDE6D9] hover:border-[#c99a2e]/35'
}`}
>
{pkg.image && (
<div className="relative h-48 overflow-hidden bg-[#F2EDE4] sm:h-52">
<img
src={dishImageUrl(pkg.image)}
alt={packageName}
className="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#101724]/50 via-transparent to-transparent" />
{pkg.highlight && (
<span className="absolute top-4 left-4 rounded-full bg-gradient-to-r from-[#c99a2e] to-[#e8c56a] px-3 py-1 text-[10px] font-bold uppercase tracking-[0.2em] text-[#241806] shadow-md">
{t.catering.mostPopular}
</span>
)}
</div>
)}
<div className="flex flex-1 flex-col p-6 sm:p-7">
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h3 className="text-[22px] leading-tight tracking-[-0.4px] text-[#101724]">
{packageName}
</h3>
<p className="mt-2 text-sm leading-relaxed text-[#6B665F]">
{packageDescription}
</p>
</div>
</div>
<div className="mb-5 rounded-2xl border border-[#c99a2e]/25 bg-[#fffdf8] px-4 py-3.5">
<div className="flex items-baseline gap-2">
<span className="text-2xl font-medium tabular-nums tracking-[-0.5px] text-[#B38B4D]">
{pkg.pricePerHead}
</span>
<span className="text-sm font-semibold text-[#8a6a25]">
{t.catering.perHead} · {pkg.currency}
</span>
</div>
</div>
<div className="mb-2 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.18em] text-[#8a6a25]">
<UtensilsCrossed className="h-3.5 w-3.5 text-[#c99a2e]" />
{t.catering.includedDishes}
</div>
<div className="flex-1 space-y-4">
{CATERING_CATEGORY_ORDER.map((category) => {
const dishes = grouped[category];
if (dishes.length === 0) return null;
const categoryLabel =
(t.catering.categories as Record<string, string>)?.[category] ?? category;
return (
<div key={category}>
<p className="mb-1.5 text-[10px] font-bold uppercase tracking-[0.22em] text-[#c99a2e]">
{categoryLabel}
</p>
<ul className="space-y-1">
{dishes.map((dish) => (
<li
key={dish.id}
className="flex items-center gap-2 text-sm text-[#3d4654]"
>
<span className="h-1 w-1 shrink-0 rounded-full bg-[#c99a2e]" aria-hidden />
{dish.name}
</li>
))}
</ul>
</div>
);
})}
</div>
<div className="mt-6 flex items-center gap-2 border-t border-[#EDE6D9] pt-4 text-xs text-[#8A8478]">
<Users className="h-3.5 w-3.5 text-[#B38B4D]" />
{t.catering.guestNote}
</div>
</div>
</motion.article>
);
}
+152
View File
@@ -0,0 +1,152 @@
'use client';
import { useEffect, useCallback } from 'react';
import { X } from 'lucide-react';
import type { MenuItem } from '@/domain/menu/entities';
import { getDishIngredients } from '@/domain/menu/dish-details';
import {
applyNextImageFallback,
dishImageUrl,
getMenuPosterCandidates,
getMenuVideoSrc,
} from '@/lib/assets';
interface DishDetailsModalProps {
item: MenuItem;
description: string;
isOpen: boolean;
onClose: () => void;
labels: {
ingredients: string;
close: string;
};
}
export default function DishDetailsModal({
item,
description,
isOpen,
onClose,
labels,
}: DishDetailsModalProps) {
const videoSrc = getMenuVideoSrc(item);
const imageSrc = item.image ? dishImageUrl(item.image) : null;
const ingredients = getDishIngredients(item.id);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
},
[onClose],
);
useEffect(() => {
if (!isOpen) return;
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, handleKeyDown]);
if (!isOpen) return null;
return (
<>
<div
className="fixed inset-0 z-[1000] bg-black/45 backdrop-blur-[2px]"
onClick={onClose}
aria-hidden
/>
<div
className="fixed inset-0 z-[1010] flex items-end sm:items-center justify-center p-0 sm:p-6 pointer-events-none"
role="dialog"
aria-modal="true"
aria-labelledby="dish-detail-title"
>
<div
className="pointer-events-auto w-full sm:max-w-lg max-h-[92dvh] sm:max-h-[88dvh] overflow-y-auto rounded-t-3xl sm:rounded-2xl bg-[#FFFCF7] border border-[#EDE6D9] shadow-2xl shadow-[#101724]/15"
onClick={(e) => e.stopPropagation()}
>
<div className="relative bg-[#F2EDE4]">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
loop
muted
playsInline
className="w-full h-[240px] sm:h-[280px] object-cover bg-[#F2EDE4]"
/>
) : imageSrc ? (
<img
src={imageSrc}
alt={item.name}
className="w-full h-[240px] sm:h-[280px] object-cover bg-[#F2EDE4]"
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getMenuPosterCandidates(item),
(e.target as HTMLImageElement).src,
);
}}
/>
) : (
<div className="w-full aspect-[4/3] flex items-center justify-center text-sm text-[#8A8478]">
No media available
</div>
)}
<button
type="button"
onClick={onClose}
aria-label={labels.close}
className="absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 text-[#2C2A26] shadow-md border border-[#EDE6D9] hover:bg-white hover:text-[#0f5a4a] transition-colors"
>
<X className="h-4 w-4" aria-hidden />
</button>
</div>
<div className="p-6 sm:p-7 space-y-5">
<h2
id="dish-detail-title"
className="font-serif text-2xl sm:text-[26px] tracking-[-0.4px] text-[#2C2A26]"
>
{item.name}
</h2>
{description && (
<p className="text-[#6B665F] text-[15px] leading-relaxed">
{description}
</p>
)}
{ingredients.length > 0 && (
<div>
<h3 className="text-xs font-semibold uppercase tracking-[0.2em] text-[#c99a2e] mb-3">
{labels.ingredients}
</h3>
<ul className="space-y-2">
{ingredients.map((ingredient) => (
<li
key={ingredient}
className="flex items-start gap-2.5 text-[15px] text-[#2C2A26] leading-snug"
>
<span
className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-[#B38B4D]"
aria-hidden
/>
{ingredient}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
</>
);
}
+261
View File
@@ -0,0 +1,261 @@
'use client';
import type { ReactNode } from 'react';
/**
* GLOBAL FOOTER — Shahi Kitchen
* Cream + gold palette matching the header for a cohesive look.
*/
import Link from 'next/link';
import { logoUrl } from '@/lib/assets';
import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations';
import {
MapPin,
Clock,
Phone,
Mail,
Heart,
} from 'lucide-react';
function InstagramIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="2" y="2" width="20" height="20" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none" />
</svg>
);
}
function FacebookIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
);
}
const FOOTER_COPY: Record<
string,
{
openingHours: string;
experience: string;
contactReserve: string;
madeWith: string;
}
> = {
sv: {
openingHours: 'Öppettider',
experience: 'Vår Upplevelse',
contactReserve: 'Kontakt & Boka',
madeWith: 'Tillagat med tradition och hjärta.',
},
en: {
openingHours: 'Opening Hours',
experience: 'Our Experience',
contactReserve: 'Contact & Reserve',
madeWith: 'Made with tradition and heart.',
},
ar: {
openingHours: 'ساعات العمل',
experience: 'تجربتنا',
contactReserve: 'تواصل واحجز',
madeWith: 'مُعدّ بتقاليد ومن القلب.',
},
tr: {
openingHours: 'Çalışma Saatleri',
experience: 'Deneyimimiz',
contactReserve: 'İletişim & Rezervasyon',
madeWith: 'Gelenek ve sevgiyle hazırlandı.',
},
hi: {
openingHours: 'खुलने का समय',
experience: 'हमारा अनुभव',
contactReserve: 'संपर्क और बुकिंग',
madeWith: 'परंपरा और दिल से बनाया गया।',
},
ur: {
openingHours: 'کھلنے کے اوقات',
experience: 'ہمارا تجربہ',
contactReserve: 'رابطہ اور بکنگ',
madeWith: 'روایت اور دل سے تیار۔',
},
};
function FooterHeading({ children }: { children: ReactNode }) {
return (
<h3 className="footer-heading mb-5 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.22em] text-[#8a6a25]">
<span className="footer-heading-line" aria-hidden="true" />
{children}
</h3>
);
}
export default function Footer() {
const { language } = useLanguage();
const t = getTranslation(language);
const copy = FOOTER_COPY[language] ?? FOOTER_COPY.en;
return (
<footer id="contact" className="scroll-mt-header shahi-footer relative mt-auto overflow-hidden">
<div className="footer-top-rail" aria-hidden="true" />
<div className="footer-main relative px-6 pb-12 pt-16">
<div className="footer-glow footer-glow--gold" aria-hidden="true" />
<div className="footer-glow footer-glow--emerald" aria-hidden="true" />
<div className="relative mx-auto max-w-7xl">
<div className="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4 lg:gap-8">
{/* Brand */}
<div className="lg:col-span-1">
<div className="mb-5 flex items-center gap-3">
<div className="footer-logo-ring grid h-14 w-14 place-items-center rounded-2xl border p-1.5">
<img
src={logoUrl()}
alt="Shahi Kitchen"
className="h-full w-full object-contain"
/>
</div>
<div>
<span className="block font-serif text-xl tracking-tight text-[#101724]">
Shahi Kitchen
</span>
<span className="text-[10px] font-semibold uppercase tracking-[0.28em] text-[#8a6a25]">
Göteborg
</span>
</div>
</div>
<p className="text-sm leading-relaxed text-[#4b5563]">{t.footer.tagline}</p>
</div>
{/* Locations */}
<div>
<FooterHeading>{t.footer.locations}</FooterHeading>
<div className="space-y-4">
<div className="footer-info-card">
<div className="mb-2 flex items-center gap-2">
<MapPin className="h-4 w-4 shrink-0 text-[#c99a2e]" />
<span className="text-sm font-semibold text-[#101724]">
Shahi Kitchen (Askim)
</span>
</div>
<p className="text-sm leading-relaxed text-[#4b5563]">
Datavägen 10A, 436 32 Askim
</p>
</div>
<div className="footer-info-card">
<div className="mb-2 flex items-center gap-2">
<MapPin className="h-4 w-4 shrink-0 text-[#c99a2e]" />
<span className="text-sm font-semibold text-[#101724]">
Shahi Sweets (Backaplan)
</span>
</div>
<p className="text-sm leading-relaxed text-[#4b5563]">
Krokegårdsgatan 5, 417 30 Göteborg
</p>
</div>
<div className="flex flex-col gap-2 pt-1">
<a
href="tel:0739381089"
className="footer-link inline-flex items-center gap-2 text-sm font-medium"
>
<Phone className="h-3.5 w-3.5 text-[#c99a2e]" />
0739-381089
</a>
<a
href="mailto:hello@shahikitchen.se"
className="footer-link inline-flex items-center gap-2 text-sm font-medium"
>
<Mail className="h-3.5 w-3.5 text-[#c99a2e]" />
hello@shahikitchen.se
</a>
</div>
</div>
</div>
{/* Hours */}
<div>
<FooterHeading>{copy.openingHours}</FooterHeading>
<div className="footer-info-card space-y-4">
<div className="flex gap-3">
<Clock className="mt-0.5 h-4 w-4 shrink-0 text-[#c99a2e]" />
<div>
<p className="text-sm font-semibold text-[#101724]">Askim</p>
<p className="text-sm text-[#4b5563]">Mon Sun · 11:00 21:00</p>
</div>
</div>
<div className="flex gap-3">
<Clock className="mt-0.5 h-4 w-4 shrink-0 text-[#c99a2e]" />
<div>
<p className="text-sm font-semibold text-[#101724]">Backaplan</p>
<p className="text-sm text-[#4b5563]">Mon Sun · 11:00 21:00</p>
</div>
</div>
</div>
</div>
{/* Links + Social */}
<div>
<FooterHeading>{t.footer.explore}</FooterHeading>
<nav className="mb-8 flex flex-col gap-2.5">
<Link href="/" className="footer-link text-sm font-medium">
{t.nav.home}
</Link>
<Link href="/menu" className="footer-link text-sm font-medium">
{t.nav.menu}
</Link>
<Link href="/catering" className="footer-link text-sm font-medium">
{t.nav.catering}
</Link>
<Link href="/locations" className="footer-link text-sm font-medium">
{t.nav.locations}
</Link>
<Link href="/#experience" className="footer-link text-sm font-medium">
{copy.experience}
</Link>
<Link href="/reserve" className="footer-link text-sm font-medium">
{copy.contactReserve}
</Link>
</nav>
<FooterHeading>{t.footer.follow}</FooterHeading>
<div className="flex gap-3">
<a
href="https://www.instagram.com/Shahikitchen/"
target="_blank"
rel="noopener noreferrer"
aria-label="Instagram"
className="footer-social-btn"
>
<InstagramIcon className="h-5 w-5" />
</a>
<a
href="https://www.facebook.com/shahikitchengbg/"
target="_blank"
rel="noopener noreferrer"
aria-label="Facebook"
className="footer-social-btn"
>
<FacebookIcon className="h-5 w-5" />
</a>
</div>
</div>
</div>
</div>
</div>
{/* Bottom bar */}
<div className="footer-bottom relative px-6 py-6">
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-3 text-center text-[11px] uppercase tracking-[0.18em] text-[#6B665F] sm:flex-row sm:text-left">
<p>© {new Date().getFullYear()} Shahi Kitchen · Göteborg</p>
<p className="normal-case tracking-normal text-[#4b5563] italic flex items-center gap-1.5">
<Heart className="h-3.5 w-3.5 text-[#c99a2e] fill-[#c99a2e]/20" aria-hidden="true" />
{copy.madeWith}
</p>
</div>
</div>
</footer>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { HEADER_TOTAL_HEIGHT } from '@/components/LanguageSwitcher';
/** Reserves vertical space for the fixed language bar + navbar (116px). */
export default function HeaderSpacer() {
return (
<div
aria-hidden
className="shrink-0"
style={{ height: HEADER_TOTAL_HEIGHT }}
/>
);
}
+59
View File
@@ -0,0 +1,59 @@
'use client';
import { useLanguage } from '@/lib/language-context';
import { languages, Language } from '@/lib/translations';
import { Globe } from 'lucide-react';
export const LANGUAGE_BANNER_HEIGHT = 48;
export const NAVBAR_HEIGHT = 68;
export const HEADER_TOTAL_HEIGHT = LANGUAGE_BANNER_HEIGHT + NAVBAR_HEIGHT;
export default function LanguageSwitcher() {
const { language, setLanguage } = useLanguage();
const handleSelect = (lang: Language) => {
setLanguage(lang);
};
return (
<div
className="lang-banner w-full border-b border-[#c99a2e]/25 bg-gradient-to-r from-[#0a4a3d] via-[#0f5a4a] to-[#0a4a3d]"
style={{ height: LANGUAGE_BANNER_HEIGHT }}
role="navigation"
aria-label="Language selection"
>
<div className="mx-auto flex h-full max-w-7xl items-center gap-2 px-3 sm:px-6">
<div className="hidden shrink-0 items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.2em] text-[#d4a73d]/90 sm:flex">
<Globe className="h-3.5 w-3.5" />
<span>Language</span>
</div>
<div className="flex flex-1 items-center justify-center gap-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:gap-1.5">
{languages.map((lang) => {
const isActive = language === lang.code;
return (
<button
key={lang.code}
type="button"
onClick={() => handleSelect(lang.code)}
aria-pressed={isActive}
aria-label={`${lang.name} (${lang.native})`}
className={`flex shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-semibold transition-all sm:gap-1.5 sm:px-3 sm:py-1.5 sm:text-xs ${
isActive
? 'bg-gradient-to-r from-[#c99a2e] to-[#e8c56a] text-[#1a1206] shadow-lg shadow-[#c99a2e]/35 ring-1 ring-[#f4d47f]/50'
: 'text-white/80 hover:bg-white/12 hover:text-white'
}`}
>
<span className="text-sm sm:text-base" aria-hidden="true">
{lang.flag}
</span>
<span className="whitespace-nowrap">{lang.native}</span>
</button>
);
})}
</div>
</div>
</div>
);
}
+328
View File
@@ -0,0 +1,328 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useCart } from "./CartContext";
import { useWishlist } from "./WishlistContext";
import { ShoppingBag, Heart, ArrowRight, Home, UtensilsCrossed, MapPin, Star, Phone, X, LogIn, ChefHat } from "lucide-react";
import LanguageSwitcher from "./LanguageSwitcher";
import HeaderSpacer from "./HeaderSpacer";
import { logoUrl } from "@/lib/assets";
import { useLanguage } from "@/lib/language-context";
import { getTranslation } from "@/lib/translations";
import { motion, AnimatePresence } from "framer-motion";
import { usePathname } from "next/navigation";
/**
* =============================================================================
* GLOBAL NAVIGATION BAR — Shahi Kitchen (Luxury Edition)
* =============================================================================
*/
interface NavbarProps {
variant?: "default" | "menu";
}
export default function Navbar({ variant = "default" }: NavbarProps) {
const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { totalItems, openCart } = useCart();
const { totalItems: wishlistCount, openWishlist } = useWishlist();
const { language } = useLanguage();
const t = getTranslation(language);
const pathname = usePathname();
// Scroll effect - only for subtle visual polish, NOT height (height must stay consistent)
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navLinks = [
{ href: "/", label: t.nav.home },
{ href: "/menu", label: t.nav.menu },
{ href: "/catering", label: t.nav.catering },
{ href: "/locations", label: t.nav.locations },
{ href: "/login", label: t.nav.login },
{ href: "/#experience", label: t.nav.experience },
{ href: "/#contact", label: t.nav.contact },
];
// Determine active link (supports hash links)
const isActive = (href: string) => {
if (href === "/") return pathname === "/";
if (href.includes("#")) return false; // hash links handled separately
return pathname === href;
};
const closeMenu = () => setIsOpen(false);
return (
<>
<header className="fixed top-0 left-0 right-0 z-[50]">
<LanguageSwitcher />
<nav
className="h-[68px] border-b border-[#c99a2e]/25 bg-gradient-to-b from-[#fffcf7]/95 to-[#fbf7ef]/90 shadow-[0_4px_24px_-4px_rgba(16,23,36,0.08)] backdrop-blur-xl"
>
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between h-full">
{/* Top subtle gold line for extra frame separation */}
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e]/20 to-transparent" />
{/* Premium Animated Logo */}
<Link href="/" className="group flex items-center gap-3">
<div className="relative">
<motion.span
whileHover={{ scale: 1.08, rotate: 2 }}
transition={{ type: "spring", stiffness: 300, damping: 15 }}
className="grid h-12 w-12 place-items-center overflow-hidden rounded-2xl border border-[#c99a2e]/30 bg-white shadow-xl shadow-[#0f5a4a]/10 p-1 transition-all duration-300 group-hover:border-[#c99a2e]/70 group-hover:shadow-[#c99a2e]/25"
>
<img
src={logoUrl()}
alt="Shahi Kitchen Logo"
className="h-10 w-10 object-contain transition-all duration-500"
/>
</motion.span>
<div className="absolute inset-0 rounded-2xl bg-[#c99a2e]/0 group-hover:bg-[#c99a2e]/15 blur-2xl transition-all duration-500 pointer-events-none" />
</div>
<span className="hidden leading-none sm:block">
<span className="block font-serif text-[21px] tracking-[-0.5px] text-[#101724] transition-colors group-hover:text-[#0f5a4a]">Shahi Kitchen</span>
<span className="text-[10px] font-semibold uppercase tracking-[0.32em] text-[#8a6a25]">Shahi Taste Gothenburg</span>
</span>
</Link>
{/* Desktop Navigation - Premium Mesmerizing Design */}
<div className="hidden md:block flex-shrink-0">
<div className="flex items-center rounded-full border border-[#c99a2e]/20 bg-white/60 px-2 py-1.5 backdrop-blur-3xl shadow-sm">
<div className="relative flex items-center gap-1 text-sm font-medium text-[#101724]">
{navLinks.map((link, index) => {
const active = isActive(link.href);
return (
<Link
key={index}
href={link.href}
className="relative px-4 py-2 rounded-full transition-colors hover:text-[#0f5a4a] z-10 whitespace-nowrap"
>
<span className="relative z-10">{link.label}</span>
{/* Sliding Active Indicator - Very Premium */}
{active && (
<motion.div
layoutId="activeNavPill"
className="absolute inset-0 rounded-full bg-gradient-to-r from-[#c99a2e] to-[#d4a73d] shadow-md"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
{/* Mesmerizing Gold Underline on Hover */}
<motion.span
className="absolute bottom-1 left-1/2 h-[1.5px] w-0 bg-gradient-to-r from-[#c99a2e] to-[#f4d47f] rounded-full"
whileHover={{ width: "60%", x: "-30%" }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
</Link>
);
})}
</div>
</div>
</div>
{/* Desktop Actions — Stunning Mesmerizing Buttons */}
<div className="hidden md:flex items-center gap-3">
{/* Wishlist Button */}
<button
onClick={openWishlist}
className="group relative flex items-center gap-2 rounded-full border border-[#c99a2e]/30 bg-white/70 px-4 py-2.5 text-sm font-semibold text-[#101724] backdrop-blur-xl transition-all hover:border-[#c99a2e] hover:bg-white hover:shadow-lg active:scale-[0.985]"
aria-label={t.wishlist}
>
<Heart className={`h-4 w-4 transition-transform group-hover:scale-110 ${wishlistCount > 0 ? 'fill-[#c99a2e]/30 text-[#c99a2e]' : ''}`} />
{t.wishlist}
{wishlistCount > 0 && (
<span className="ml-0.5 rounded-full bg-[#c99a2e] px-2 py-px text-[10px] font-black text-white">{wishlistCount}</span>
)}
</button>
{/* Cart Button - Elegant dark with gold accent */}
<button
onClick={openCart}
className="group flex items-center gap-2.5 rounded-full border border-[#c99a2e]/30 bg-white/70 px-5 py-2.5 text-sm font-semibold text-[#101724] backdrop-blur-xl transition-all hover:border-[#c99a2e] hover:bg-white hover:shadow-lg active:scale-[0.985]"
>
<ShoppingBag className="h-4 w-4 transition-transform group-hover:scale-110" />
{t.cart}
{totalItems > 0 && (
<span className="ml-0.5 rounded-full bg-[#c99a2e] px-2 py-px text-[10px] font-black text-white">{totalItems}</span>
)}
</button>
{/* Reserve Table - The star of the nav, with mesmerizing gold gradient + shine */}
<Link
href="/reserve"
className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#c99a2e] via-[#d4a73d] to-[#c99a2e] px-7 py-2.5 text-sm font-bold text-[#241806] shadow-lg shadow-[#c99a2e]/25 transition-all hover:scale-[1.02] active:scale-[0.985] bg-[length:200%_100%] hover:bg-right"
>
<span className="relative z-10 flex items-center gap-2 tracking-[0.3px]">
{t.reserve}
<ArrowRight className="h-4 w-4" />
</span>
{/* Subtle shine sweep on hover */}
<span className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent opacity-0 group-hover:animate-[shimmer_1.2s_ease] group-hover:opacity-100" />
</Link>
</div>
{/* Mobile Hamburger + Wishlist + Cart (compact, high touch target) */}
<div className="md:hidden flex items-center gap-2">
{/* MOBILE WISHLIST ICON */}
<button
onClick={openWishlist}
className="relative flex items-center justify-center w-10 h-10 rounded-full hover:bg-[#F5F1E9] active:bg-[#EDE6D9] transition-colors"
aria-label={t.wishlist}
>
<Heart className={`h-5 w-5 ${wishlistCount > 0 ? 'text-[#c99a2e] fill-[#c99a2e]/30' : 'text-[#101724]'}`} />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-[#c99a2e] text-white text-[10px] font-bold min-w-[18px] h-[18px] rounded-full flex items-center justify-center px-1 tabular-nums">
{wishlistCount}
</span>
)}
</button>
{/* MOBILE CART ICON (always visible) */}
<button
onClick={openCart}
className="relative flex items-center justify-center w-10 h-10 rounded-full hover:bg-[#F5F1E9] active:bg-[#EDE6D9] transition-colors"
aria-label="Open cart"
>
<ShoppingBag className="h-5 w-5 text-[#101724]" />
{totalItems > 0 && (
<span className="absolute -top-1 -right-1 bg-[#B38B4D] text-white text-[10px] font-bold min-w-[18px] h-[18px] rounded-full flex items-center justify-center px-1 tabular-nums">
{totalItems}
</span>
)}
</button>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-[#B38B4D] p-2 -mr-1 active:text-[#8C6B3A] transition-colors"
aria-label="Toggle menu"
aria-expanded={isOpen}
>
<div className="space-y-1.5">
<span className={`block h-px w-6 bg-current transition-all duration-200 ${isOpen ? "rotate-45 translate-y-1.5" : ""}`} />
<span className={`block h-px w-6 bg-current transition-all duration-200 ${isOpen ? "opacity-0" : ""}`} />
<span className={`block h-px w-6 bg-current transition-all duration-200 ${isOpen ? "-rotate-45 -translate-y-1.5" : ""}`} />
</div>
</button>
</div>
</div>
</nav>
</header>
<HeaderSpacer />
{/* Modern Mobile Menu Drawer (slide-in, animated, high-contrast, touch-friendly) - high z to always appear in front of hero banner video etc. */}
<AnimatePresence>
{isOpen && (
<div className="md:hidden fixed inset-0 z-[9999]">
{/* Backdrop */}
<motion.div
className="absolute inset-0 bg-[#101724]/70 backdrop-blur-md"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
onClick={closeMenu}
/>
{/* Sliding Panel - modern, full-bleed on small phones, elegant on larger */}
<motion.div
className="absolute right-0 top-0 bottom-0 w-[82%] max-w-[340px] bg-[#fbf7ef] shadow-2xl border-l border-[#c99a2e]/10 flex flex-col overflow-y-auto z-[10000]"
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', stiffness: 320, damping: 32 }}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]">
<div className="flex items-center gap-3">
<img
src={logoUrl()}
alt="Shahi Kitchen"
className="h-10 w-10 rounded-xl object-contain"
/>
<div>
<div className="font-serif text-[19px] leading-none text-[#101724]">Shahi Kitchen</div>
<div className="text-[10px] uppercase tracking-[1.5px] text-[#8a6a25] -mt-0.5">Gothenburg</div>
</div>
</div>
<button
onClick={closeMenu}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/70 active:bg-[#EDE6D9] text-[#101724] transition-colors"
aria-label="Close menu"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Primary Nav Links - modern list with icons + active state for visibility */}
<div className="px-3 py-4">
{navLinks.map((link) => {
const active = isActive(link.href);
const Icon =
link.href === '/' ? Home :
link.href === '/menu' ? UtensilsCrossed :
link.href === '/catering' ? ChefHat :
link.href === '/locations' ? MapPin :
link.href === '/login' ? LogIn :
link.href.includes('experience') ? Star : Phone;
return (
<Link
key={link.href}
href={link.href}
onClick={closeMenu}
className={`flex items-center gap-4 px-4 py-3.5 mx-1 my-0.5 rounded-2xl text-[17px] font-medium transition-all active:scale-[0.985] ${
active
? 'bg-[#101724] text-white shadow-sm'
: 'text-[#101724] hover:bg-white active:bg-[#EDE6D9]'
}`}
>
<Icon className={`h-5 w-5 flex-shrink-0 ${active ? 'text-[#c99a2e]' : 'text-[#B38B4D]'}`} />
<span className="whitespace-nowrap">{link.label}</span>
{active && (
<span className="ml-auto text-xs tracking-widest opacity-70">CURRENT</span>
)}
</Link>
);
})}
</div>
{/* Secondary actions */}
<div className="mt-auto px-5 pb-8 pt-4 border-t border-[#EDE6D9] bg-white/40 space-y-3">
<Link
href="/reserve"
onClick={closeMenu}
className="block w-full rounded-2xl bg-gradient-to-r from-[#c99a2e] via-[#d4a73d] to-[#c99a2e] py-4 text-center text-[15px] font-bold text-[#241806] shadow active:scale-[0.985] transition-transform"
>
{t.reserve}
</Link>
<button
onClick={() => { openWishlist(); closeMenu(); }}
className="block w-full rounded-2xl border-2 border-[#c99a2e]/30 bg-white py-4 text-[15px] font-semibold text-[#101724] active:bg-[#F5F1E9] active:border-[#c99a2e] transition-all"
>
{t.wishlist} {wishlistCount > 0 && `(${wishlistCount})`}
</button>
<button
onClick={() => { openCart(); closeMenu(); }}
className="block w-full rounded-2xl border-2 border-[#c99a2e]/40 bg-white py-4 text-[15px] font-semibold text-[#101724] active:bg-[#F5F1E9] active:border-[#c99a2e] transition-all"
>
{t.cart} {totalItems > 0 && `(${totalItems})`}
</button>
<p className="text-center text-[11px] text-[#8A8478] pt-1">Tap to open WhatsApp for orders &amp; bookings</p>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
}
+507
View File
@@ -0,0 +1,507 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import {
Clock, Users, Bike, Package, UtensilsCrossed, CheckCircle,
AlertCircle, Timer, ArrowRight
} from 'lucide-react';
import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations';
import type {
KitchenOrder as Order,
KitchenOrderItem as OrderItem,
OrderStatus,
} from '@/domain/kitchen-order/entities';
import { ORDER_STATUS_PIPELINE } from '@/domain/kitchen-order/entities';
export type { Order, OrderItem };
const STATUS_ORDER = ORDER_STATUS_PIPELINE;
function getStatusMeta(status: OrderStatus, t: any) {
const statuses = t.screen?.statuses || {
received: 'Order Received',
review: 'In Review',
preparing: 'Preparing',
packing: 'Packing / Plating',
ready: 'Ready',
};
const colors: Record<OrderStatus, string> = {
received: 'bg-blue-100 text-blue-800 border-blue-200',
review: 'bg-amber-100 text-amber-800 border-amber-200',
preparing: 'bg-orange-100 text-orange-800 border-orange-200',
packing: 'bg-purple-100 text-purple-800 border-purple-200',
ready: 'bg-emerald-100 text-emerald-800 border-emerald-200',
};
const icons: Record<OrderStatus, React.ReactNode> = {
received: <AlertCircle className="w-4 h-4" />,
review: <Clock className="w-4 h-4" />,
preparing: <UtensilsCrossed className="w-4 h-4" />,
packing: <Package className="w-4 h-4" />,
ready: <CheckCircle className="w-4 h-4" />,
};
return {
label: statuses[status] || status,
color: colors[status],
icon: icons[status],
progress: STATUS_ORDER.indexOf(status) / (STATUS_ORDER.length - 1),
};
}
function getSourceMeta(source: Order['source']) {
if (source === 'table') return { icon: <Users className="w-4 h-4" />, label: 'Restaurant', color: '#B38B4D' };
if (source === 'foodora') return { icon: <Bike className="w-4 h-4" />, label: 'Foodora', color: '#E30613' };
if (source === 'uber') return { icon: <Bike className="w-4 h-4" />, label: 'Uber Eats', color: '#000000' };
if (source === 'wolt') return { icon: <Bike className="w-4 h-4" />, label: 'Wolt', color: '#00C2E0' };
if (source === 'pickup') return { icon: <Package className="w-4 h-4" />, label: 'Pickup', color: '#B38B4D' };
return { icon: <Bike className="w-4 h-4" />, label: 'Delivery', color: '#B38B4D' };
}
// Beautiful subtle particles (adapted from ScreenDisplay for order theme - gold "steam" specks)
function OrderParticles() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) return;
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const particles = Array.from({ length: 28 }, () => ({
x: Math.random() * width,
y: Math.random() * height * 0.85,
vx: (Math.random() - 0.5) * 0.12,
vy: -0.08 - Math.random() * 0.12,
size: 1.0 + Math.random() * 1.6,
alpha: 0.08 + Math.random() * 0.16,
phase: Math.random() * Math.PI * 2,
speed: 0.012 + Math.random() * 0.009,
}));
const onResize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
};
window.addEventListener('resize', onResize);
let rafId: number;
const draw = () => {
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.x += p.vx + Math.sin(p.phase) * 0.04;
p.y += p.vy;
p.phase += p.speed;
if (p.y < -8) {
p.y = height * 0.88 + Math.random() * 30;
p.x = Math.random() * width;
}
if (p.x < 0) p.x = width;
if (p.x > width) p.x = 0;
ctx.fillStyle = `hsla(38, 68%, 52%, ${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
rafId = requestAnimationFrame(draw);
};
draw();
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener('resize', onResize);
};
}, []);
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none z-[1] opacity-60 mix-blend-multiply"
/>
);
}
export default function OrderDisplay() {
const { language } = useLanguage();
const t = getTranslation(language);
const [orders, setOrders] = useState<Order[]>(() => {
const now = new Date();
return [
// Restaurant orders (tables) - 8 orders
{ id: 'T-05', source: 'table', label: 'Table 5', customer: 'Table 5', items: [{name: 'Butter Chicken', qty: 1}, {name: 'Garlic Naan', qty: 2}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*14), etaMinutes: 6 },
{ id: 'T-12', source: 'table', label: 'Table 12', customer: 'Table 12', items: [{name: 'Lamb Rogan Josh', qty: 2}, {name: 'Biryani', qty: 1}], status: 'ready', receivedAt: new Date(now.getTime() - 1000*60*22), etaMinutes: 0 },
{ id: 'T-03', source: 'table', label: 'Table 3', customer: 'Table 3', items: [{name: 'Samosa Chat', qty: 2}, {name: 'Chana Chat', qty: 1}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*11), etaMinutes: 4 },
{ id: 'T-08', source: 'table', label: 'Table 8', customer: 'Table 8', items: [{name: 'Daal Makhani', qty: 1}, {name: 'Rice', qty: 2}], status: 'ready', receivedAt: new Date(now.getTime() - 1000*60*18), etaMinutes: 0 },
{ id: 'T-01', source: 'table', label: 'Table 1', customer: 'Table 1', items: [{name: 'Shahi Paneer', qty: 1}, {name: 'Naan', qty: 3}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*8), etaMinutes: 7 },
{ id: 'T-04', source: 'table', label: 'Table 4', customer: 'Table 4', items: [{name: 'Chicken Tikka', qty: 2}, {name: 'Rice', qty: 1}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*5), etaMinutes: 10 },
{ id: 'T-06', source: 'table', label: 'Table 6', customer: 'Table 6', items: [{name: 'Lahore Sizzler', qty: 1}, {name: 'Naan', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*3), etaMinutes: 5 },
{ id: 'T-07', source: 'table', label: 'Table 7', customer: 'Table 7', items: [{name: 'Tikka Boti', qty: 1}, {name: 'Salad', qty: 1}], status: 'received', receivedAt: new Date(now.getTime() - 1000*60*1), etaMinutes: 12 },
// Delivery Partners (online) - 8 orders (mix of platforms)
{ id: 'FD-3921', source: 'foodora', label: 'Foodora #FD-3921', customer: 'Aisha K.', items: [{name: 'Lahore Pizza', qty: 1}, {name: 'Mango Lassi', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*9), etaMinutes: 3 },
{ id: 'W-884', source: 'wolt', label: 'Wolt #W-884', customer: 'Erik S.', items: [{name: 'Paneer Roll', qty: 3}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*4), etaMinutes: 11 },
{ id: 'FD-3928', source: 'foodora', label: 'Foodora #FD-3928', customer: 'Maria L.', items: [{name: 'Shahi Burger', qty: 2}, {name: 'Coca-Cola', qty: 2}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*7), etaMinutes: 8 },
{ id: 'UB-551', source: 'uber', label: 'Uber Eats #UB-551', customer: 'Johan P.', items: [{name: 'Chicken Tikka', qty: 1}, {name: 'Keema Naan', qty: 1}], status: 'received', receivedAt: new Date(now.getTime() - 1000*60*2), etaMinutes: 14 },
{ id: 'FD-3935', source: 'foodora', label: 'Foodora #FD-3935', customer: 'Omar B.', items: [{name: 'Butter Chicken', qty: 1}, {name: 'Naan', qty: 2}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*10), etaMinutes: 4 },
{ id: 'W-892', source: 'wolt', label: 'Wolt #W-892', customer: 'Lina T.', items: [{name: 'Veg Pizza', qty: 1}, {name: 'Lassi', qty: 1}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*6), etaMinutes: 6 },
{ id: 'UB-562', source: 'uber', label: 'Uber Eats #UB-562', customer: 'Peter H.', items: [{name: 'Lamb Rogan Josh', qty: 1}, {name: 'Rice', qty: 2}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*12), etaMinutes: 9 },
{ id: 'FD-3940', source: 'foodora', label: 'Foodora #FD-3940', customer: 'Sara N.', items: [{name: 'Tikka Boti Roll', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*15), etaMinutes: 2 },
// Pickup orders (whatsapp/phone) - 8 orders
{ id: 'WA-4521', source: 'pickup', label: 'Pickup WA-4521', customer: 'Ahmed R.', items: [{name: 'Chicken Biryani', qty: 2}, {name: 'Raita', qty: 1}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*13), etaMinutes: 5 },
{ id: 'PH-783', source: 'pickup', label: 'Pickup PH-783', customer: 'Sara M.', items: [{name: 'Veg Pizza', qty: 1}, {name: 'Garlic Naan', qty: 2}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*6), etaMinutes: 2 },
{ id: 'WA-4530', source: 'pickup', label: 'Pickup WA-4530', customer: 'Omar K.', items: [{name: 'Tikka Boti', qty: 1}], status: 'ready', receivedAt: new Date(now.getTime() - 1000*60*20), etaMinutes: 0 },
{ id: 'WA-4535', source: 'pickup', label: 'Pickup WA-4535', customer: 'Fatima L.', items: [{name: 'Shahi Paneer', qty: 1}, {name: 'Naan', qty: 1}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*8), etaMinutes: 7 },
{ id: 'PH-791', source: 'pickup', label: 'Pickup PH-791', customer: 'Hassan Q.', items: [{name: 'Daal Makhani', qty: 1}, {name: 'Rice', qty: 2}], status: 'review', receivedAt: new Date(now.getTime() - 1000*60*11), etaMinutes: 8 },
{ id: 'WA-4542', source: 'pickup', label: 'Pickup WA-4542', customer: 'Ayesha S.', items: [{name: 'Samosa Keema', qty: 3}], status: 'packing', receivedAt: new Date(now.getTime() - 1000*60*4), etaMinutes: 3 },
{ id: 'PH-802', source: 'pickup', label: 'Pickup PH-802', customer: 'Bilal M.', items: [{name: 'Lamm Vindaloo', qty: 1}, {name: 'Naan', qty: 2}], status: 'preparing', receivedAt: new Date(now.getTime() - 1000*60*9), etaMinutes: 6 },
{ id: 'WA-4550', source: 'pickup', label: 'Pickup WA-4550', customer: 'Zainab P.', items: [{name: 'Jalebi', qty: 2}, {name: 'Kulfi', qty: 1}], status: 'received', receivedAt: new Date(now.getTime() - 1000*60*1), etaMinutes: 13 },
];
});
const [lastUpdate, setLastUpdate] = useState(new Date());
const [liveClock, setLiveClock] = useState(new Date());
// Live clock
useEffect(() => {
const id = setInterval(() => setLiveClock(new Date()), 1000);
return () => clearInterval(id);
}, []);
// Simulate live backend updates (replace with real fetch / WebSocket / polling when ready)
useEffect(() => {
const interval = setInterval(() => {
setOrders(prev => {
let updated = [...prev];
const now = new Date();
// Occasionally advance a random non-ready order
if (Math.random() < 0.65) {
const candidates = updated
.map((o, i) => ({o, i}))
.filter(({o}) => o.status !== 'ready');
if (candidates.length > 0) {
const pick = candidates[Math.floor(Math.random() * candidates.length)];
const currIdx = STATUS_ORDER.indexOf(pick.o.status);
if (currIdx < STATUS_ORDER.length - 1) {
updated[pick.i] = {
...pick.o,
status: STATUS_ORDER[currIdx + 1],
};
}
}
}
// Occasionally add a new incoming order (allow up to ~30 total so each category can have 6-10+ for reels)
if (Math.random() < 0.28 && updated.length < 30) {
const sources: Order['source'][] = ['table', 'foodora', 'wolt', 'uber', 'pickup'];
const src = sources[Math.floor(Math.random() * sources.length)];
const isTable = src === 'table';
const isPickup = src === 'pickup';
const num = Math.floor(1000 + Math.random() * 9000);
const newOrder: Order = {
id: isTable ? `T-${Math.floor(1+Math.random()*14)}` : isPickup ? `WA-${num}` : `${src.toUpperCase().slice(0,2)}-${num}`,
source: src,
label: isTable ? `Table ${Math.floor(1+Math.random()*14)}` : isPickup ? `Pickup WA-${num}` : `${src.charAt(0).toUpperCase() + src.slice(1)} #${src.slice(0,2).toUpperCase()}-${num}`,
customer: isTable ? `Table ${Math.floor(1+Math.random()*14)}` : isPickup ? ['Ahmed R.', 'Sara M.', 'Omar K.', 'Fatima L.'][Math.floor(Math.random()*4)] : ['A. Khan', 'S. Patel', 'L. Berg', 'M. Lind'][Math.floor(Math.random()*4)],
items: [
{ name: ['Butter Chicken','Lahore Pizza','Lamb Rogan Josh','Shahi Paneer','Tikka Boti Roll'][Math.floor(Math.random()*5)], qty: 1 + Math.floor(Math.random()*2) },
{ name: ['Naan','Rice','Lassi','Samosa','Salad'][Math.floor(Math.random()*5)], qty: 1 },
],
status: 'received',
receivedAt: now,
etaMinutes: 10 + Math.floor(Math.random() * 8),
};
updated = [newOrder, ...updated].slice(0, 30);
}
// Occasionally remove a very old ready order (simulates pickup / serve)
if (Math.random() < 0.22) {
const readyIdx = updated.findIndex(o => o.status === 'ready' && (now.getTime() - o.receivedAt.getTime()) > 1000*60*3);
if (readyIdx !== -1) updated.splice(readyIdx, 1);
}
return updated;
});
setLastUpdate(new Date());
}, 6200); // ~every 6s for mesmerizing live feel
return () => clearInterval(interval);
}, []);
// Group into the three required sections
const restaurantOrders = orders.filter(o => o.source === 'table').sort((a,b) => b.receivedAt.getTime() - a.receivedAt.getTime());
const deliveryOrders = orders.filter(o => ['foodora', 'uber', 'wolt', 'other'].includes(o.source)).sort((a,b) => b.receivedAt.getTime() - a.receivedAt.getTime());
const pickupOrders = orders.filter(o => o.source === 'pickup').sort((a,b) => b.receivedAt.getTime() - a.receivedAt.getTime());
const readyOrders = orders.filter(o => o.status === 'ready');
const formatTime = (date: Date) => date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const getElapsed = (date: Date) => {
const mins = Math.floor((Date.now() - date.getTime()) / 60000);
return `${mins} ${t.screen?.elapsed || 'min'}`;
};
const screenLabel = t.screen?.display3 || 'DISPLAY 3';
// Helper to render a vertical looping reel for a list of orders.
// Viewport h-full (fills remaining screen height after headers).
// Cards fixed px height => browser zoom changes # visible (fewer on zoom-in, more on zoom-out), like screen2.
// Duplicates list for seamless upward CSS loop when count > visible.
// Always fills screen top-to-bottom with no whitespace.
const renderOrderReel = (ordersList: Order[], sectionLabel: string, sectionIcon: React.ReactNode) => {
const count = ordersList.length;
const shouldLoop = count > 3;
const displayList = shouldLoop ? [...ordersList, ...ordersList] : ordersList;
const duration = shouldLoop ? `${count * REEL_TIME_PER_ITEM}s` : undefined;
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-3 mb-3 px-1 flex-none">
{sectionIcon}
<div className="font-medium tracking-[2px] text-sm text-[#B38B4D]">{sectionLabel}</div>
<div className="flex-1 h-px bg-[#EDE6D9]" />
<div className="text-xs text-[#6B665F] tabular-nums">{count} ACTIVE</div>
</div>
<div
className="reel-viewport h-full overflow-hidden"
>
<div
className="order-reel-track"
style={{
gap: '8px',
animationDuration: duration,
// If not looping, no animation
animation: shouldLoop ? undefined : 'none'
}}
>
{displayList.map((order, idx) => {
const meta = getStatusMeta(order.status, t);
const srcMeta = getSourceMeta(order.source);
const elapsed = getElapsed(order.receivedAt);
const isReady = order.status === 'ready';
// Card with FIXED height in px. + h-full viewport (fills screen) means zoom in: fewer visible/column, zoom out: more visible. No whitespace, max top-bottom.
return (
<div
key={`${order.id}-${idx}`}
className={`group relative bg-white rounded-2xl border shadow-md overflow-hidden flex flex-col ${isReady ? 'border-emerald-300 ring-1 ring-emerald-200' : 'border-[#EDE6D9]'}`}
style={{ height: `${REEL_CARD_HEIGHT}px` }}
>
<div className="px-3 pt-2 pb-1 flex items-start justify-between gap-2 flex-none">
<div className="min-w-0">
<div className="font-mono text-lg tracking-[-0.5px] font-semibold text-[#2C2A26] leading-none truncate">
{order.label}
</div>
<div className="text-[10px] text-[#6B665F] mt-0.5 tracking-[0.3px] truncate">{order.customer} {elapsed}</div>
</div>
<div className={`px-2 py-0.5 rounded-full text-[10px] font-medium tracking-wider border flex items-center gap-1 whitespace-nowrap flex-none ${meta.color}`}>
{meta.icon} {meta.label.toUpperCase().slice(0,6)}
</div>
</div>
{/* Compact items */}
<div className="px-3 pb-1 text-xs text-[#2C2A26]/90 flex-1 overflow-hidden">
{order.items.slice(0,2).map((it, i) => (
<div key={i} className="flex justify-between leading-tight py-px">
<span className="truncate">{it.qty}× {it.name}</span>
</div>
))}
{order.items.length > 2 && <div className="text-[10px] text-[#6B665F]">+{order.items.length-2} more</div>}
</div>
{/* Compact progress */}
<div className="px-3 pb-2 pt-0.5 border-t border-[#EDE6D9]/60 bg-[#F8F5F0]/50 flex items-center gap-2 flex-none">
<div className="flex-1 h-1 bg-[#EDE6D9] rounded overflow-hidden">
<div
className="h-full transition-all"
style={{
width: `${meta.progress * 100}%`,
background: srcMeta.color
}}
/>
</div>
<div className="text-[10px] font-medium whitespace-nowrap tabular-nums" style={{ color: srcMeta.color }}>
{isReady ? 'READY' : `${order.etaMinutes}m`}
</div>
</div>
{isReady && (
<div className="absolute top-1 right-1 px-2 py-0.5 bg-emerald-600 text-white text-[9px] tracking-widest rounded font-semibold">READY</div>
)}
</div>
);
})}
</div>
</div>
</div>
);
};
// Reel settings for screen3 vertical marquees - dynamic with zoom like screen2
// Fixed card height in px so that browser zoom in/out changes how many cards fit vertically in the column (less when zoom in, more when zoom out).
// The reel viewport uses 100% of available column height (after section header) to fill the screen with no whitespace.
const REEL_CARD_HEIGHT = 120; // px per order card - tune for ~3-5 at 100% on 43" TV. Zoom affects visible count.
const REEL_TIME_PER_ITEM = 6; // seconds per order in the animation - slow upward movement
return (
<div className="h-screen bg-[#F8F5F0] text-[#2C2A26] overflow-hidden select-none flex flex-col screen-root">
<OrderParticles />
{/* Mesmerizing top bar - luxurious, welcoming, eye-catching for customers & riders. Taller, pulsing LIVE, centered tagline, elegant depth for 43" kiosk appeal. */}
<div className="relative z-20 h-20 border-b border-[#EDE6D9]/50 bg-gradient-to-b from-[#F8F5F0] via-[#F8F5F0]/98 to-[#F8F5F0]/95 backdrop-blur-xl flex items-center px-10 text-sm flex-none shadow-[0_2px_8px_rgba(0,0,0,0.03)]">
<div className="flex items-center gap-5">
{/* Brand with depth and elegance */}
<div className="flex items-baseline gap-1.5">
<div className="font-serif text-[28px] tracking-[-2.2px] text-[#2C2A26] drop-shadow-sm">SHAHI KITCHEN</div>
<div className="text-[9px] text-[#B38B4D] tracking-[2px] font-medium -mb-1">EST. 2016</div>
</div>
{/* Elegant status badges */}
<div className="flex items-center gap-2">
<div className="text-[11px] px-3.5 py-1 rounded-full border border-[#B38B4D]/60 text-[#B38B4D] tracking-[3px] font-medium bg-white/60 backdrop-blur-sm">
{screenLabel}
</div>
<motion.div
animate={{ scale: [1, 1.08, 1], opacity: [0.9, 1, 0.9] }}
transition={{ duration: 2.2, repeat: Infinity, ease: "easeInOut" }}
className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#B38B4D] text-white text-[10px] tracking-[2.5px] font-semibold shadow-sm"
>
<div className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
LIVE
</motion.div>
</div>
</div>
<div className="flex-1" />
{/* Centered mesmerizing tagline */}
<div className="hidden md:block text-center">
<div className="text-[11px] tracking-[4.5px] text-[#B38B4D]/70 font-medium">YOUR JOURNEY TO FLAVOR REAL-TIME MAGIC</div>
</div>
<div className="flex-1" />
{/* Right info - elegant and clear */}
<div className="flex items-center gap-7 text-[#6B665F] tracking-[1.5px] text-xs">
<div className="flex items-center gap-2.5">
<div>ASKIM</div>
<div className="w-px h-2.5 bg-[#EDE6D9]" />
<div>BACKAPLAN</div>
</div>
<div className="h-3 w-px bg-[#EDE6D9]/70" />
<div className="font-mono text-[#B38B4D] text-sm tracking-widest tabular-nums">
{liveClock.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}
</div>
<div className="text-[10px] px-2.5 py-0.5 rounded bg-[#B38B4D]/10 text-[#B38B4D] tracking-[1.5px]">
{t.screen?.prepared?.toUpperCase() || 'FRESHLY PREPARED'}
</div>
</div>
</div>
{/* Hero header with beautiful typography and live feel */}
<div className="relative z-10 bg-[#101724] text-white flex-none py-5 px-9 border-b border-white/10">
<div className="max-w-[1680px] mx-auto flex items-end justify-between">
<div>
<div className="inline-flex items-center gap-2 rounded-full bg-white/10 px-4 py-1 text-xs tracking-[3.5px] mb-2 border border-white/20">
{t.screen?.orderBoard || 'LIVE ORDER BOARD'}
</div>
<h1 className="font-serif text-[42px] md:text-[52px] leading-[0.9] tracking-[-2.2px] text-white">
{t.screen?.orderSubtitle || 'Delivery • Pickup • Restaurant — live for riders & guests'}
</h1>
<p className="mt-1.5 text-white/60 tracking-[0.5px] text-sm">
Delivery Partners Pickup (WhatsApp/Phone) Restaurant Tables Real-time kitchen progress
</p>
</div>
<div className="text-right text-sm">
<div className="text-[#B38B4D] tracking-[2px] text-xs mb-0.5">LAST SYNC</div>
<div className="font-mono text-lg text-white/90">{formatTime(lastUpdate)}</div>
<div className="text-[10px] text-white/50 mt-0.5">{t.screen?.liveUpdates || 'Live updates • Backend integration coming soon'}</div>
</div>
</div>
</div>
{/* Stats bar - beautiful metrics with motion - three sections */}
<div className="relative z-10 bg-[#F8F5F0] border-b border-[#EDE6D9]/60 py-3 px-9 flex-none">
<div className="max-w-[1680px] mx-auto grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div className="flex items-center gap-3">
<div className="text-[#B38B4D]"><Users className="w-5 h-5" /></div>
<div>
<div className="font-medium text-lg leading-none">{restaurantOrders.length}</div>
<div className="text-[#6B665F] text-xs tracking-[1px]">RESTAURANT ORDERS</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-[#B38B4D]"><Bike className="w-5 h-5" /></div>
<div>
<div className="font-medium text-lg leading-none">{deliveryOrders.length}</div>
<div className="text-[#6B665F] text-xs tracking-[1px]">DELIVERY PARTNERS</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="text-[#B38B4D]"><Package className="w-5 h-5" /></div>
<div>
<div className="font-medium text-lg leading-none">{pickupOrders.length}</div>
<div className="text-[#6B665F] text-xs tracking-[1px]">PICKUP ORDERS (WA/PHONE)</div>
</div>
</div>
<div className="flex items-center gap-3 text-[#6B665F]">
<Timer className="w-5 h-5 text-[#B38B4D]" />
<div className="text-xs tracking-[1.5px] leading-tight">
AVERAGE PREP TIME<br />
<span className="text-[#2C2A26] font-medium">12-16 MIN</span>
</div>
</div>
</div>
</div>
{/* Main beautiful content area - THREE COLUMNS with vertical reels filling entire remaining screen height */}
{/* Reels use h-full after fixed headers; cards fixed px height => zoom in: fewer visible per column, zoom out: more visible. No white space, max top to bottom like screen2 max left to right. */}
<div className="flex-1 min-h-0 overflow-hidden px-4 py-2 md:px-6 md:py-3 bg-[#F8F5F0]">
<div className="max-w-[1680px] mx-auto h-full grid grid-cols-1 lg:grid-cols-3 gap-4" style={{ minHeight: 0 }}>
{/* 1. DELIVERY PARTNERS - reel column */}
<div className="flex flex-col h-full">
{renderOrderReel(deliveryOrders, 'DELIVERY PARTNERS', <Bike className="w-5 h-5 text-[#B38B4D]" />)}
</div>
{/* 2. PICKUP ORDERS (WHATSAPP/PHONE) - reel column */}
<div className="flex flex-col h-full">
{renderOrderReel(pickupOrders, 'PICKUP ORDERS (WHATSAPP/PHONE)', <Package className="w-5 h-5 text-[#B38B4D]" />)}
</div>
{/* 3. RESTAURANT ORDERS - reel column */}
<div className="flex flex-col h-full">
{renderOrderReel(restaurantOrders, 'RESTAURANT ORDERS', <Users className="w-5 h-5 text-[#B38B4D]" />)}
</div>
</div>
</div>
{/* Bottom elegant bar / marquee for future rider messages */}
<div className="relative z-10 py-2.5 text-center text-[11px] tracking-[3px] text-[#8A8478] border-t border-[#EDE6D9]/60 bg-[#F8F5F0] flex-none flex items-center justify-center gap-4">
<div>SHAHI KITCHEN GOTHENBURG FRESH EVERY DAY</div>
<div className="h-px w-6 bg-[#EDE6D9]" />
<div className="text-[#B38B4D] font-medium">INTEGRATING WITH FOODORA WOLT UBER EATS</div>
<div className="h-px w-6 bg-[#EDE6D9]" />
<div>BACKEND API READY SOON LIVE DATA FEED</div>
</div>
</div>
);
}
+46
View File
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react';
interface PageHeaderProps {
eyebrow?: string;
title: string;
subtitle?: string;
centered?: boolean;
className?: string;
children?: ReactNode;
}
export default function PageHeader({
eyebrow,
title,
subtitle,
centered = false,
className = '',
children,
}: PageHeaderProps) {
return (
<div
className={`mx-auto max-w-7xl px-6 pt-5 pb-6 ${centered ? 'text-center' : ''} ${className}`}
>
<div className={`${centered ? 'max-w-3xl mx-auto' : 'max-w-3xl'}`}>
{eyebrow && (
<div className="text-[#B38B4D] text-[11px] tracking-[2.5px] mb-2 font-medium uppercase">
{eyebrow}
</div>
)}
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
{title}
</h1>
{subtitle && (
<p
className={`text-sm md:text-[15px] text-[#4b5563] leading-relaxed ${
centered ? 'mx-auto max-w-xl' : 'max-w-2xl'
}`}
>
{subtitle}
</p>
)}
{children}
</div>
</div>
);
}
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { chefExpressionUrl } from "@/lib/assets";
import { motion, AnimatePresence } from "framer-motion";
import React, { useState, useEffect } from "react";
/**
* Shahi Kitchen Hero - Focused on the Chef Logo
* - Large central logo with life (wink + smile cycle)
* - Cream background circle so the logo blends/absorbs into the site color
* - No more small floating dishes
*/
export default function PlayfulHeroScene() {
const [expression, setExpression] = useState<'normal' | 'wink' | 'smile'>('normal');
// Cycle expressions for life (wink and smile)
useEffect(() => {
const interval = setInterval(() => {
setExpression(prev => {
const rand = Math.random();
if (rand < 0.45) return 'wink';
if (rand < 0.9) return 'smile';
return 'normal';
});
}, 2400);
return () => clearInterval(interval);
}, []);
const getLogoSrc = () => {
return chefExpressionUrl(expression);
};
return (
<div className="relative w-full h-full min-h-[520px] md:min-h-[620px] flex items-center justify-center overflow-hidden">
{/* Large cream background circle - matches website exactly so logo absorbs */}
<div className="absolute left-1/2 top-[58%] -translate-x-1/2 -translate-y-1/2
w-[440px] h-[440px] md:w-[540px] md:h-[540px] lg:w-[620px] lg:h-[620px]
bg-[#fbf7ef] rounded-full blur-[130px] opacity-95" />
{/* Central Chef Logo with life */}
<div className="relative z-30">
<AnimatePresence mode="wait">
<motion.div
key={expression}
initial={{ opacity: 0.7, scale: 0.97 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -24, 0],
rotate: [-3.5, 3.5, -3.5]
}}
exit={{ opacity: 0.7, scale: 0.97 }}
transition={{
y: { duration: 6.6, repeat: Infinity, ease: "easeInOut" },
rotate: { duration: 6.6, repeat: Infinity, ease: "easeInOut" },
opacity: { duration: 0.4 },
scale: { duration: 0.4 }
}}
className="w-[300px] h-[300px] md:w-[380px] md:h-[380px] lg:w-[440px] lg:h-[440px]"
>
<img
src={getLogoSrc()}
alt="Shahi Kitchen Chef"
className="w-full h-full object-contain drop-shadow-[0_25px_55px_rgba(0,0,0,0.35)]"
/>
</motion.div>
</AnimatePresence>
{/* Enhanced Shahi glows */}
<div className="absolute -top-14 left-1/2 -translate-x-1/2 w-44 h-44 bg-[#f4d47f] rounded-full blur-3xl opacity-48" />
<div className="absolute -top-7 left-1/2 -translate-x-1/2 w-24 h-24 bg-[#c99a2e] rounded-full blur-2xl opacity-32" />
<div className="absolute -bottom-9 left-1/2 -translate-x-1/2 w-32 h-16 bg-[#c99a2e] rounded-full blur-3xl opacity-22" />
</div>
{/* Subtle steam for life */}
<motion.div
className="absolute left-[41%] top-[36%] w-4 h-11 opacity-32"
animate={{ y: [0, -36, 0], opacity: [0.28, 0.52, 0.28] }}
transition={{ duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
>
<div className="w-full h-full bg-gradient-to-t from-[#f4d47f] to-transparent rounded-full blur-md" />
</motion.div>
<motion.div
className="absolute right-[40%] top-[41%] w-3 h-9 opacity-27"
animate={{ y: [0, -30, 0], opacity: [0.22, 0.48, 0.22] }}
transition={{ duration: 4.2, repeat: Infinity, ease: "easeInOut", delay: 1.5 }}
>
<div className="w-full h-full bg-gradient-to-t from-[#c99a2e] to-transparent rounded-full blur-md" />
</motion.div>
</div>
);
}
+564
View File
@@ -0,0 +1,564 @@
'use client';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { gsap } from 'gsap';
import {
getMenuPosterCandidates,
getMenuPosterSrc,
getMenuVideoSources,
applyNextImageFallback,
} from '@/lib/assets';
import { menuCategories, allMenuItems, type MenuItem, type MenuCategory } from '@/lib/menu-data';
import { useLanguage } from '@/lib/language-context';
import { getTranslation } from '@/lib/translations';
interface ScreenDisplayProps {
screen: 1 | 2;
}
const SCREEN1_CATEGORY_IDS = ['street-food', 'vegetarian', 'meat', 'chicken', 'burger-sandwich'];
const SCREEN2_CATEGORY_IDS = ['pizza', 'naan-roll', 'sweets', 'drinks'];
function SpiceParticles() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) return;
let width = window.innerWidth;
let height = window.innerHeight;
canvas.width = width;
canvas.height = height;
const particles = Array.from({ length: 42 }, () => ({
x: Math.random() * width,
y: Math.random() * height * 0.9,
vx: (Math.random() - 0.5) * 0.22,
vy: -0.12 - Math.random() * 0.22,
size: 1.1 + Math.random() * 1.9,
alpha: 0.12 + Math.random() * 0.22,
phase: Math.random() * Math.PI * 2,
speed: 0.018 + Math.random() * 0.012,
}));
const onResize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
};
window.addEventListener('resize', onResize);
let rafId: number;
const draw = () => {
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.x += p.vx + Math.sin(p.phase) * 0.07;
p.y += p.vy;
p.phase += p.speed;
if (p.y < -10) {
p.y = height * 0.92 + Math.random() * 40;
p.x = Math.random() * width;
}
if (p.x < 0) p.x = width;
if (p.x > width) p.x = 0;
ctx.fillStyle = `hsla(38, 68%, 52%, ${p.alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
// occasional tiny sparkle
if (Math.random() < 0.014) {
ctx.fillStyle = `hsla(42, 90%, 78%, ${p.alpha * 0.9})`;
ctx.fillRect(p.x - 0.6, p.y - 0.6, 1.2, 1.2);
}
}
rafId = requestAnimationFrame(draw);
};
draw();
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener('resize', onResize);
};
}, []);
return (
<canvas
ref={canvasRef}
className="fixed inset-0 pointer-events-none z-[2] opacity-70 mix-blend-multiply"
/>
);
}
function MenuCard({ item, isHighlighted, t }: { item: MenuItem; isHighlighted?: boolean; t: any }) {
const poster = getMenuPosterSrc(item, 'optimized');
const desc = (t as any).menuDescriptions?.[item.id] || item.description || '';
return (
<motion.div
className={`group relative flex flex-col overflow-hidden rounded-2xl border bg-white transition-all duration-300 ${
isHighlighted
? 'border-[#B38B4D] ring-2 ring-[#B38B4D]/70 shadow-xl scale-[1.015] z-10'
: 'border-[#EDE6D9] hover:border-[#B38B4D]/40'
}`}
animate={isHighlighted ? { scale: 1.015 } : { scale: 1 }}
transition={{ type: 'spring', stiffness: 220, damping: 26 }}
>
<div className="relative h-36 bg-[#F2EDE4] overflow-hidden">
<motion.img
src={poster}
alt={item.name}
className="absolute inset-0 w-full h-full object-cover"
animate={{ scale: [1, 1.018, 1] }}
transition={{ duration: 8.5, repeat: Infinity, ease: 'easeInOut' }}
onError={(e) => {
applyNextImageFallback(
e.currentTarget,
getMenuPosterCandidates(item),
e.currentTarget.src
);
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/35" />
{/* Gold price pill */}
<div className="absolute top-3 right-3 px-3 py-1 rounded-full bg-white/95 text-[#B38B4D] text-lg font-semibold tracking-tight shadow-sm border border-[#EDE6D9]">
{item.price} <span className="text-[10px] font-normal tracking-widest align-super">KR</span>
</div>
{item.isVegetarian && (
<div className="absolute top-3 left-3 text-[10px] px-2.5 py-0.5 rounded-full bg-[#3F5C4A]/90 text-white tracking-[1px]">
VEG
</div>
)}
</div>
<div className="p-3.5 flex-1 flex flex-col">
<div className="font-medium tracking-[-0.3px] text-[15px] leading-tight text-[#2C2A26] mb-1 line-clamp-1 group-hover:text-[#B38B4D] transition-colors">
{item.name}
</div>
<p className="text-[#6B665F] text-[12px] leading-snug line-clamp-2 flex-1">
{desc}
</p>
</div>
</motion.div>
);
}
export default function ScreenDisplay({ screen }: ScreenDisplayProps) {
const { language } = useLanguage();
const t = getTranslation(language);
const screenLabel = screen === 1 ? t.screen.display1 : t.screen.display2;
const isScreen1 = screen === 1;
// Filtered data for this screen
const screenCategories = useMemo(() => {
const ids = isScreen1 ? SCREEN1_CATEGORY_IDS : SCREEN2_CATEGORY_IDS;
return menuCategories.filter((c) => ids.includes(c.id));
}, [isScreen1]);
// Cycle through ALL items for this screen, one by one in the large hero (not just signatures)
const cyclingItems = useMemo(() => {
return screenCategories.flatMap((category) => category.items);
}, [screenCategories]);
// Cycling through all menu items (large top animation)
const [currentIndex, setCurrentIndex] = useState(0);
const current = cyclingItems[currentIndex] || cyclingItems[0];
const heroTiltRef = useRef<HTMLDivElement>(null);
// Auto cycle through every item (large top hero shows all, one by one)
const CYCLE_INTERVAL_MS = 8200;
useEffect(() => {
const id = setInterval(() => {
setCurrentIndex((i) => (i + 1) % cyclingItems.length);
}, CYCLE_INTERVAL_MS);
return () => clearInterval(id);
}, [cyclingItems.length]);
// Beautiful 3D tilt on hero (like signature cards on homepage)
useEffect(() => {
const el = heroTiltRef.current;
if (!el) return;
const onMouseMove = (e: MouseEvent) => {
const rect = el.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
const y = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
gsap.to(el, {
rotationY: x * 7,
rotationX: -y * 5.5,
transformPerspective: 1400,
duration: 0.36,
ease: 'power2.out',
overwrite: true,
});
};
const onMouseLeave = () => {
gsap.to(el, {
rotationY: 0,
rotationX: 0,
duration: 1.35,
ease: 'elastic.out(1, 0.48)',
});
};
el.addEventListener('mousemove', onMouseMove);
el.addEventListener('mouseleave', onMouseLeave);
return () => {
el.removeEventListener('mousemove', onMouseMove);
el.removeEventListener('mouseleave', onMouseLeave);
};
}, []);
// Professional kiosk mode for screen1/screen2:
// Force clean fullscreen-like window (no side scrollbars) even in F11 or kiosk browser.
// Applies to html/body so the window itself has no scrollbar.
// User zooms the browser in/out to change reel density (# visible items) instead of scrolling.
// Cleanup restores previous state (good if ever navigating away in dev).
useEffect(() => {
const htmlEl = document.documentElement;
const bodyEl = document.body;
const prevHtmlClass = htmlEl.className;
const prevBodyClass = bodyEl.className;
const prevHtmlOverflow = htmlEl.style.overflow;
const prevBodyOverflow = bodyEl.style.overflow;
htmlEl.classList.add('screen-kiosk');
bodyEl.classList.add('screen-kiosk');
htmlEl.style.overflow = 'hidden';
bodyEl.style.overflow = 'hidden';
return () => {
// restore
htmlEl.style.overflow = prevHtmlOverflow;
bodyEl.style.overflow = prevBodyOverflow;
// remove only if we added (simple: toggle off)
htmlEl.classList.remove('screen-kiosk');
bodyEl.classList.remove('screen-kiosk');
};
}, []);
const translatedDesc = current ? ((t as any).menuDescriptions?.[current.id] || current.description) : '';
// =====================================================
// CONTINUOUS MENU REEL (one line, loop like a film reel) - items move LEFT
// Enter from RIGHT, exit LEFT. Number of visible items is dynamic (more when zoom out, fewer when zoom in).
// The reel viewport is always full width (edge to edge, no max-w), no side whitespace.
// Posters only (no videos) for seamless movement. Numbers for easy ordering (screen1:1-26, screen2:27+).
// CSS marquee for stable seamless poster reel.
// Card sizes fixed px for consistent distance viewing; # visible auto-adjusts to current container width on zoom.
// Sized large for 43" TV distance viewing in restaurant.
// =====================================================
const allMenuItemsForReel = screenCategories.flatMap((c) => c.items);
const numItems = allMenuItemsForReel.length;
const startNumber = isScreen1 ? 1 : 27;
// Duplicate for seamless infinite loop (scroll 50% to repeat perfectly)
const reelItems = [...allMenuItemsForReel, ...allMenuItemsForReel];
// Card width tuned for 43" screen (~1920px), 3 visible + gaps ~1872px at default zoom.
// Poster 320px + bottom ~70px (py-5 + one-line name+right price) so cards fill the ~390px reel band vertically.
const REEL_CARD_WIDTH = 620; // px
const REEL_CARD_GAP = 16; // px (Tailwind gap-4)
// ReelCard: large poster + number+name on left, price on right (opposite side, same line) so price is always clearly visible as cards move left in the reel. Sized big for 43" TV distance viewing.
function ReelCard({ item, number }: { item: MenuItem; number: number }) {
const numStr = String(number).padStart(2, '0');
return (
<div
className="flex-shrink-0 bg-white rounded-3xl overflow-hidden border border-[#EDE6D9] shadow-xl flex flex-col"
style={{ width: `${REEL_CARD_WIDTH}px`, transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
>
<div className="relative bg-[#F2EDE4]" style={{ height: '320px' }}>
<motion.img
src={getMenuPosterSrc(item, 'optimized')}
alt={item.name}
className="w-full h-full object-cover"
animate={{ scale: [1, 1.015, 1] }}
transition={{ duration: 6, repeat: Infinity, ease: 'easeInOut' }}
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/5 to-black/25" />
{/* Prominent number badge for easy ordering by number e.g. "give me number 13" */}
<div className="absolute top-2 left-2 z-10 px-3.5 py-1 rounded-full bg-black/75 text-[#E8D4A3] text-3xl font-mono font-bold tracking-[3px] shadow-lg border border-[#E8D4A3]/40">
{numStr}
</div>
</div>
<div className="px-4 py-5 flex items-baseline justify-between gap-4">
<div className="font-serif text-3xl tracking-[-0.5px] leading-none text-[#2C2A26]">
{numStr}. {item.name}
</div>
<div className="text-[#B38B4D] text-2xl font-semibold tracking-tight whitespace-nowrap">
{item.price} kr
</div>
</div>
</div>
);
}
return (
<div className="h-screen bg-[#F8F5F0] text-[#2C2A26] overflow-hidden select-none flex flex-col screen-root">
<SpiceParticles />
{/* Elegant top bar */}
<div className="relative z-20 h-14 border-b border-[#EDE6D9]/70 bg-[#F8F5F0]/95 backdrop-blur-md flex items-center px-9 text-sm flex-none">
<div className="flex items-center gap-3">
<div className="font-serif text-2xl tracking-[-1.5px] text-[#2C2A26]">SHAHI KITCHEN</div>
<div className="text-[10px] px-2.5 py-px rounded-full border border-[#B38B4D]/50 text-[#B38B4D] tracking-[2.5px] font-medium">
{screenLabel}
</div>
</div>
<div className="flex-1" />
<div className="flex items-center gap-6 text-[#6B665F] tracking-[1px] text-xs">
<div>ASKIM BACKAPLAN</div>
<div className="h-2.5 w-px bg-[#EDE6D9]" />
<div>{t.screen.prepared.toUpperCase()}</div>
</div>
</div>
{/* HERO — Large cycling animation showing ALL items one by one (not just signatures).
flex-1 so it absorbs extra viewport height on tall displays / when zoomed out.
On short effective viewports (zoomed in) it shrinks gracefully; text sized for fit. */}
<div className="flex-1 min-h-[260px] relative z-10 bg-[#101724] flex items-center justify-center overflow-hidden">
<div
ref={heroTiltRef}
className="absolute inset-0 will-change-transform"
style={{ transformStyle: 'preserve-3d' }}
>
{/* Media switches with crossfade + fresh start each cycle. Video when available, else large animated poster (Ken Burns). */}
<AnimatePresence mode="wait">
{current && (
current.video ? (
<motion.div
key={`${current.id}-video`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.45 }}
className="absolute inset-0"
>
<video
className="absolute inset-0 w-full h-full object-cover"
autoPlay
muted
loop
playsInline
preload="auto"
>
{(() => {
const srcs = getMenuVideoSources(current.video);
return (
<>
{srcs.webm && <source src={srcs.webm} type="video/webm" />}
{srcs.mp4 && <source src={srcs.mp4} type="video/mp4" />}
{srcs.fallback && <source src={srcs.fallback} type="video/mp4" />}
</>
);
})()}
</video>
</motion.div>
) : (
<motion.img
key={`${current.id}-poster`}
src={getMenuPosterSrc(current, 'optimized')}
alt={current.name}
className="absolute inset-0 w-full h-full object-cover"
initial={{ opacity: 0, scale: 1 }}
animate={{ opacity: 1, scale: [1, 1.06, 1] }}
exit={{ opacity: 0 }}
transition={{ opacity: { duration: 0.45 }, scale: { duration: 8, repeat: Infinity, ease: 'easeInOut' } }}
onError={(e) => {
applyNextImageFallback(
e.currentTarget,
getMenuPosterCandidates(current),
e.currentTarget.src
);
}}
/>
)
)}
</AnimatePresence>
{/* Rich overlays for text legibility + luxury feel (stay on top of changing media) */}
<div className="absolute inset-0 bg-gradient-to-b from-black/25 via-black/15 to-black/55" />
<div className="absolute inset-0 bg-[radial-gradient(#B38B4D_0.6px,transparent_1px)] bg-[length:5px_5px] opacity-[0.035]" />
</div>
{/* Animated dish info */}
<div className="relative z-10 max-w-4xl px-10 text-white">
<AnimatePresence mode="wait">
{current && (
<motion.div
key={current.id}
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -14 }}
transition={{ duration: 0.55, ease: [0.21, 0.92, 0.26, 1] }}
>
<div className="inline-flex items-center gap-2 rounded-full bg-white/10 px-4 py-1 text-xs tracking-[3.5px] mb-4 border border-white/20">
{t.screen.menuTitle}
</div>
<h1 className="font-serif text-[56px] md:text-[64px] leading-[0.88] tracking-[-3.2px] mb-2 text-[#B38B4D] drop-shadow-[0_0_10px_rgba(179,139,77,0.85)]">
{current.name}
</h1>
<div className="flex items-baseline gap-2 mb-4">
<div className="text-5xl font-medium tracking-[-1.5px] text-[#E8D4A3]">
{current.price}
</div>
<div className="text-2xl text-white/70 tracking-[1px]">KR</div>
</div>
<p className="max-w-[620px] text-[17px] leading-tight text-white/90 tracking-[-0.1px] drop-shadow">
{translatedDesc}
</p>
<div className="mt-5 text-[11px] tracking-[3px] text-white/60">
{t.screen.prepared}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Cycle progress + counter (clean for many items) */}
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 text-white/70 text-xs tracking-[2px]">
<div>{currentIndex + 1} / {cyclingItems.length}</div>
<div className="w-32 h-px bg-white/20 rounded overflow-hidden">
<motion.div
key={currentIndex}
className="h-px bg-[#E8D4A3]"
initial={{ width: '0%' }}
animate={{ width: '100%' }}
transition={{ duration: CYCLE_INTERVAL_MS / 1000, ease: 'linear' }}
/>
</div>
</div>
</div>
{/* MENU REEL - continuous LEFT-moving reel (items move left: enter from right, exit left), entire menu in one line.
Dynamic # of items visible based on zoom (more when zoomed out). Always full width edge-to-edge (no side whitespace).
~8s per item, seamless loop with posters only. Fixed band height so layout sums <=100vh with flex-1 hero. */}
<div className="relative z-10 bg-[#F8F5F0] border-t border-[#EDE6D9]/60 overflow-hidden w-full flex-none">
<div className="max-w-[1680px] mx-auto px-4 py-1.5">
<div className="text-[#B38B4D] text-xs tracking-[4px] font-medium mb-1">THE MENU REEL</div>
</div>
{/* Full-width viewport for the reel; cards will fill based on container width. Height tuned to match card (poster + bottom text area) so items fill the band vertically with no large empty space under as they rotate left. */}
<div style={{ height: '390px' }} className="overflow-hidden w-full">
<div
className="menu-reel-track flex gap-4"
style={{
width: `${reelItems.length * (REEL_CARD_WIDTH + REEL_CARD_GAP)}px`,
animationDuration: `${numItems * 8}s`
}}
>
{reelItems.map((item, index) => {
const displayNumber = ((index % numItems) + startNumber);
return (
<ReelCard key={`${item.id}-${index}`} item={item} number={displayNumber} />
);
})}
</div>
</div>
</div>
{/* COMBO MEALS & DISCOUNT OFFERS - compact vertical footprint so total page height always <= 100vh in fullscreen (no scrollbar) */}
<div className="relative z-10 bg-[#F8F5F0] py-3 border-t border-[#EDE6D9]/60 flex-none">
<div className="max-w-[1680px] mx-auto px-8">
<div className="flex items-center gap-4 mb-3">
<div className="text-[#B38B4D] text-xs tracking-[3.5px] font-medium">COMBO MEALS & OFFERS</div>
<div className="flex-1 h-px bg-[#B38B4D]/25" />
<div className="text-xs text-[#6B665F]">Ask at counter by offer number</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Offer 1 */}
<div className="bg-white rounded-3xl border border-[#EDE6D9] shadow-xl p-4 flex flex-col">
<div className="flex items-center justify-between mb-1.5">
<div className="text-[#B38B4D] text-xs tracking-[2px] font-semibold">OFFER 1</div>
<div className="text-[10px] px-2 py-0.5 rounded-full bg-[#B38B4D] text-white tracking-wider">BEST VALUE</div>
</div>
<div className="font-serif text-xl tracking-tight text-[#101724] leading-tight">SHAHI FAMILY COMBO</div>
<div className="text-xs text-[#6B665F] mt-0.5 flex-1">2 Chicken Curries + 2 Veg Curries + 4 Naan + 2 Rice + 4 Drinks</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-semibold text-[#B38B4D]">799 kr</span>
<span className="text-base line-through text-[#8A8478]">999 kr</span>
</div>
<div className="text-[10px] text-[#B38B4D] font-medium">SAVE 200 KR Serves 4</div>
</div>
{/* Offer 2 */}
<div className="bg-white rounded-3xl border border-[#EDE6D9] shadow-xl p-4 flex flex-col">
<div className="flex items-center justify-between mb-1.5">
<div className="text-[#B38B4D] text-xs tracking-[2px] font-semibold">OFFER 2</div>
</div>
<div className="font-serif text-xl tracking-tight text-[#101724] leading-tight">LUNCH THALI SPECIAL</div>
<div className="text-xs text-[#6B665F] mt-0.5 flex-1">Any Curry + Rice + 2 Naan + Salad + Drink (11am3pm)</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-semibold text-[#B38B4D]">149 kr</span>
<span className="text-base line-through text-[#8A8478]">189 kr</span>
</div>
<div className="text-[10px] text-[#B38B4D] font-medium">SAVE 40 KR MonFri only</div>
</div>
{/* Offer 3 */}
<div className="bg-white rounded-3xl border border-[#EDE6D9] shadow-xl p-4 flex flex-col">
<div className="flex items-center justify-between mb-1.5">
<div className="text-[#B38B4D] text-xs tracking-[2px] font-semibold">OFFER 3</div>
</div>
<div className="font-serif text-xl tracking-tight text-[#101724] leading-tight">PIZZA & ROLL DEAL</div>
<div className="text-xs text-[#6B665F] mt-0.5 flex-1">Any 2 Pizzas + Any 2 Rolls + 2 Drinks</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-semibold text-[#B38B4D]">229 kr</span>
<span className="text-base line-through text-[#8A8478]">299 kr</span>
</div>
<div className="text-[10px] text-[#B38B4D] font-medium">SAVE 70 KR Great for sharing</div>
</div>
{/* Offer 4 */}
<div className="bg-white rounded-3xl border border-[#EDE6D9] shadow-xl p-4 flex flex-col">
<div className="flex items-center justify-between mb-1.5">
<div className="text-[#B38B4D] text-xs tracking-[2px] font-semibold">OFFER 4</div>
<div className="text-[10px] px-2 py-0.5 rounded-full bg-[#3F5C4A] text-white tracking-wider">VEG</div>
</div>
<div className="font-serif text-xl tracking-tight text-[#101724] leading-tight">VEG FEAST FOR 4</div>
<div className="text-xs text-[#6B665F] mt-0.5 flex-1">4 Veg Curries + 4 Naan + 2 Rice + 4 Drinks</div>
<div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-semibold text-[#B38B4D]">599 kr</span>
<span className="text-base line-through text-[#8A8478]">749 kr</span>
</div>
<div className="text-[10px] text-[#B38B4D] font-medium">SAVE 150 KR Pure veg delight</div>
</div>
</div>
</div>
</div>
{/* Very subtle footer branding */}
<div className="relative z-10 py-2 text-center text-[10px] tracking-[2.5px] text-[#8A8478] border-t border-[#EDE6D9]/60 bg-[#F8F5F0] flex-none">
SHAHI KITCHEN GOTHENBURG FRESH EVERY DAY
</div>
</div>
);
}
+137
View File
@@ -0,0 +1,137 @@
'use client';
import type { MenuItem } from '@/domain/menu/entities';
import WishlistButton from '@/components/WishlistButton';
import {
applyNextImageFallback,
getMenuPosterCandidates,
getMenuPosterSrc,
} from '@/lib/assets';
export type SignatureDish = MenuItem & {
desc: string;
};
interface SignatureMenuMarqueeProps {
dishes: SignatureDish[];
addToCartLabel: string;
perHalfKgLabel: string;
perKgLabel: string;
onAdd: (dish: SignatureDish) => void;
}
function renderMarqueePrice(
item: MenuItem,
perHalfKgLabel: string,
perKgLabel: string
) {
if (item.pricing === 'weight') {
return (
<div className="shrink-0 text-right">
<div className="text-[#B38B4D] text-lg font-medium tracking-[-0.5px] tabular-nums leading-tight">
{item.pricePerHalfKg ?? item.price}
</div>
<div className="text-[10px] text-[#8A8478] tracking-wide">{perHalfKgLabel}</div>
<div className="text-[#B38B4D] text-sm font-medium tabular-nums mt-0.5">
{item.pricePerKg} {perKgLabel}
</div>
</div>
);
}
return (
<div className="shrink-0 text-right">
<div className="text-[#B38B4D] text-xl font-medium tracking-[-0.5px] tabular-nums">
{item.price}
</div>
<div className="text-[10px] text-[#8A8478] tracking-widest -mt-1">KR</div>
</div>
);
}
const CARD_WIDTH = 240;
const CARD_GAP = 20;
export default function SignatureMenuMarquee({
dishes,
addToCartLabel,
perHalfKgLabel,
perKgLabel,
onAdd,
}: SignatureMenuMarqueeProps) {
if (dishes.length === 0) return null;
const reelItems = [...dishes, ...dishes];
const trackWidth = reelItems.length * (CARD_WIDTH + CARD_GAP);
const durationSeconds = Math.max(50, dishes.length * 2.8);
return (
<div className="signature-menu-reel relative -mx-6 overflow-hidden lg:-mx-8">
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-[#fffdf8] to-transparent sm:w-24"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-[#fffdf8] to-transparent sm:w-24"
/>
<div
className="signature-menu-reel-track flex w-max gap-5 py-2 pl-6 lg:pl-8"
style={{
width: `${trackWidth}px`,
animationDuration: `${durationSeconds}s`,
}}
>
{reelItems.map((dish, index) => (
<article
key={`${dish.id}-${index}`}
className="signature-card group flex w-[240px] shrink-0 flex-col overflow-hidden rounded-[1.75rem] border border-[#EDE6D9] bg-white shadow-sm transition-shadow hover:border-[#c99a2e]/45 hover:shadow-md"
>
<div className="relative h-40 overflow-hidden bg-[#F2EDE4]">
<img
src={getMenuPosterSrc(dish)}
alt={dish.name}
className="signature-image h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
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/20" />
<div className="absolute top-3 left-3 z-10">
<WishlistButton
item={{ id: dish.id, name: dish.name, price: dish.price, image: dish.image }}
size="sm"
/>
</div>
</div>
<div className="flex flex-1 flex-col p-4">
<div className="mb-2 flex items-start justify-between gap-3">
<h4 className="min-w-0 line-clamp-2 text-[17px] leading-tight tracking-[-0.3px] text-[#101724] group-hover:text-[#B38B4D]">
{dish.name}
</h4>
{renderMarqueePrice(dish, perHalfKgLabel, perKgLabel)}
</div>
<p className="mb-3 line-clamp-2 flex-1 text-[12px] leading-snug text-[#6B665F]">
{dish.desc}
</p>
<button
type="button"
onClick={() => onAdd(dish)}
className="w-full rounded-full border border-[#B38B4D] py-2.5 text-xs font-medium tracking-wide text-[#B38B4D] transition-all hover:bg-[#B38B4D] hover:text-white active:scale-[0.98]"
>
{addToCartLabel}
</button>
</div>
</article>
))}
</div>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
'use client';
import { Heart } from 'lucide-react';
import { useWishlist } from '@/presentation/providers/wishlist-provider';
import type { NewWishlistItem } from '@/domain/wishlist/entities';
interface WishlistButtonProps {
item: NewWishlistItem;
size?: 'sm' | 'md';
className?: string;
}
export default function WishlistButton({ item, size = 'md', className = '' }: WishlistButtonProps) {
const { toggleWishlist, isWishlisted } = useWishlist();
const saved = isWishlisted(item.id);
const sizeClasses = size === 'sm'
? 'h-8 w-8'
: 'h-10 w-10';
const iconSize = size === 'sm' ? 'h-4 w-4' : 'h-5 w-5';
return (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleWishlist(item);
}}
aria-label={saved ? 'Remove from wishlist' : 'Add to wishlist'}
aria-pressed={saved}
className={`group/wish flex items-center justify-center rounded-full border transition-all active:scale-95 ${sizeClasses} ${
saved
? 'border-[#c99a2e] bg-[#c99a2e]/15 text-[#c99a2e] shadow-sm'
: 'border-[#EDE6D9] bg-white/95 text-[#8A8478] hover:border-[#c99a2e]/50 hover:text-[#c99a2e]'
} ${className}`}
>
<Heart
className={`${iconSize} transition-transform group-hover/wish:scale-110 ${saved ? 'fill-[#c99a2e]' : ''}`}
/>
</button>
);
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @deprecated Import from `@/presentation/providers/wishlist-provider`.
* Kept for backward compatibility during Clean Architecture migration.
*/
export {
WishlistProvider,
useWishlist,
type WishlistItem,
} from '@/presentation/providers/wishlist-provider';
+172
View File
@@ -0,0 +1,172 @@
'use client';
import Link from 'next/link';
import { Heart, ShoppingBag } from 'lucide-react';
import { useWishlist } from '@/presentation/providers/wishlist-provider';
import { useCart } from '@/presentation/providers/cart-provider';
import { useLanguage } from '@/presentation/providers/language-provider';
import { getTranslation } from '@/presentation/i18n/translations';
import { getMenuPosterSrc, applyNextImageFallback, getMenuPosterCandidates } from '@/lib/assets';
import { getMenuItemById } from '@/lib/menu-data';
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
export default function WishlistDrawer() {
const {
items,
isOpen,
closeWishlist,
removeFromWishlist,
clearWishlist,
} = useWishlist();
const { addToCart } = useCart();
const { language } = useLanguage();
const t = getTranslation(language);
if (!isOpen) return null;
const handleAddToCart = (item: (typeof items)[number]) => {
const menuItem = getMenuItemById(item.id);
addToCart(menuItem ? buildCartLineFromMenuItem(menuItem) : {
id: item.id,
name: item.name,
price: item.price,
image: item.image,
pricingMode: 'standard',
});
};
return (
<>
<div
className="fixed inset-0 bg-black/40 z-[985]"
onClick={closeWishlist}
/>
<div className="fixed top-0 right-0 h-full w-full max-w-md bg-[#F8F5F0] z-[985] shadow-2xl flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-[#EDE6D9]">
<div className="flex items-center gap-3">
<Heart className="h-5 w-5 text-[#c99a2e] fill-[#c99a2e]/20" />
<h2 className="text-2xl tracking-[-0.5px] text-[#101724]">{t.wishlistDrawer.title}</h2>
{items.length > 0 && (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-[#EDE6D9] text-[#6B665F]">
{items.length} {items.length === 1 ? t.wishlistDrawer.item : t.wishlistDrawer.items}
</span>
)}
</div>
<div className="flex items-center gap-3">
{items.length > 0 && (
<button
onClick={clearWishlist}
className="text-xs text-[#B38B4D] hover:underline"
>
{t.wishlistDrawer.clear}
</button>
)}
<button
onClick={closeWishlist}
className="text-[#6B665F] hover:text-[#2C2A26] text-2xl leading-none pl-1"
aria-label="Close wishlist"
>
×
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6">
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<Heart className="h-16 w-16 text-[#c99a2e]/30 mb-4" />
<p className="text-lg text-[#6B665F]">{t.wishlistDrawer.empty}</p>
<Link
href="/menu"
onClick={closeWishlist}
className="mt-6 text-[#B38B4D] hover:underline"
>
{t.wishlistDrawer.browseMenu}
</Link>
</div>
) : (
<div className="space-y-5">
{items.map((item) => (
<div
key={item.id}
className="flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-4"
>
<div className="h-20 w-20 shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]">
<img
src={getMenuPosterSrc(item)}
alt={item.name}
className="h-full w-full object-cover"
loading="lazy"
onError={(e) => {
applyNextImageFallback(
e.target as HTMLImageElement,
getMenuPosterCandidates(item),
(e.target as HTMLImageElement).src
);
}}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between gap-2">
<h4 className="font-medium tracking-[-0.3px] text-[#101724] truncate">
{item.name}
</h4>
<span className="shrink-0 text-right font-medium text-[#B38B4D] tabular-nums text-sm leading-tight">
{(() => {
const menuItem = getMenuItemById(item.id);
if (menuItem?.pricing === 'weight') {
return (
<>
<span className="block">{menuItem.pricePerHalfKg} kr / ½ kg</span>
<span className="block text-xs text-[#8A8478]">{menuItem.pricePerKg} {t.menu.perKg}</span>
</>
);
}
return <span>{item.price} kr</span>;
})()}
</span>
</div>
<div className="flex items-center gap-2 mt-3">
<button
onClick={() => handleAddToCart(item)}
className="inline-flex items-center gap-1.5 rounded-full bg-gradient-to-r from-[#c99a2e] to-[#d4a73d] px-4 py-2 text-xs font-bold text-[#241806] transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<ShoppingBag className="h-3.5 w-3.5" />
{t.wishlistDrawer.addToCart}
</button>
<button
onClick={() => removeFromWishlist(item.id)}
className="text-xs text-[#B38B4D] hover:underline ml-auto"
>
{t.wishlistDrawer.remove}
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{items.length > 0 && (
<div className="p-6 border-t border-[#EDE6D9] bg-white">
<p className="text-xs text-center text-[#6B665F] mb-4">
{t.wishlistDrawer.hint}
</p>
<Link
href="/menu"
onClick={closeWishlist}
className="block w-full text-center text-sm text-[#B38B4D] hover:underline"
>
{t.wishlistDrawer.continueBrowsing}
</Link>
</div>
)}
</div>
</>
);
}
+79
View File
@@ -0,0 +1,79 @@
# Shahi Kitchen - High Quality Video Prompts (Realistic + Web Optimized)
Use these prompts in Grok's video generation (or similar tools).
**Recommended settings for all videos:**
- Duration: 5 seconds
- Resolution: 480p or 540p
- Style: Photorealistic, professional food photography
- Motion: Natural, subtle, loop-friendly
- Target file size after optimization: 500900 KB
---
## Butter Chicken
Photorealistic close-up of authentic Indian Butter Chicken in a traditional dark bowl. Rich, glossy, creamy orange-red tomato gravy with tender chicken pieces. Delicate natural steam gently rising from the hot dish. Warm, soft restaurant lighting with beautiful golden highlights on the sauce. Shot from a 3/4 angle, highly appetizing and luxurious. Shallow depth of field. Smooth 5-second loop, web optimized.
## Samosa Chat
Photorealistic close-up of Indian Samosa Chaat. Crispy broken samosas topped with chickpeas, yogurt, colorful chutneys, sev, onions and pomegranate. Fresh, vibrant street food presentation. Natural daylight or warm restaurant lighting. Shallow depth of field, highly detailed and appetizing. Smooth 5-second loop, web optimized.
## Palak Paneer
Photorealistic close-up of Palak Paneer. Fresh vibrant green spinach gravy with soft white paneer cubes, garnished with cream and ginger. Subtle natural steam rising. Warm elegant restaurant lighting. Professional food photography, shallow depth of field. Smooth 5-second loop, web optimized.
## Malai Kofta
Photorealistic close-up of Malai Kofta in rich creamy cashew gravy. Soft golden vegetable dumplings, garnished with cream and nuts. Gentle steam rising. Luxurious warm lighting. High-end Indian restaurant food photography style. Shallow depth of field. Smooth 5-second loop, web optimized.
## Daal Makhani
Photorealistic close-up of Daal Makhani. Thick, creamy, buttery black lentils with rich tomato gravy. Glossy surface with subtle steam. Warm cozy restaurant lighting. Professional appetizing food photography. Shallow depth of field. Smooth 5-second loop, web optimized.
## Lahore Chana
Photorealistic close-up of Lahore Chana (spiced chickpeas). Tangy onion-tomato gravy with chickpeas, garnished with green chilies, ginger and cilantro. Natural steam rising. Warm vibrant lighting. Traditional Punjabi restaurant style. Shallow depth of field. Smooth 5-second loop, web optimized.
## Lamm Palak
Photorealistic close-up of Lamm Palak. Tender lamb pieces in creamy spinach gravy, garnished with cream and ginger. Subtle steam. Warm rich lighting. Professional Indian restaurant food photography. Shallow depth of field. Smooth 5-second loop, web optimized.
## Bong Nihari
Photorealistic close-up of traditional Bong Nihari (slow-cooked shank). Rich aromatic gravy with tender meat, garnished with fresh ginger, green chilies and cilantro. Natural steam. Warm, moody, authentic Pakistani restaurant lighting. Highly detailed and appetizing. Smooth 5-second loop, web optimized.
## Shami Sandwich
Photorealistic close-up of a Shami Sandwich cut in half. Spiced shami kebab in soft bread with onions, chutney and fresh herbs. Appetizing cross-section view. Warm natural lighting. Professional street food / cafe photography style. Short 5-second loop, web optimized.
## Kebab Roll
Photorealistic close-up of a Kebab Roll cut open. Juicy seekh kebab inside soft naan with onions, chutney and salad. Slight steam and fresh ingredients visible. Warm appetizing lighting. Professional fast-casual food photography. Smooth 5-second loop, web optimized.
## Falafel Roll
Photorealistic close-up of a Falafel Roll cut in half. Crispy falafel with fresh vegetables, tahini and chutney inside soft flatbread. Vibrant and fresh presentation. Warm natural lighting. Professional street food photography style. Short 5-second loop, web optimized.
## Paneer Roll
Photorealistic close-up of a Paneer Roll cut open. Grilled paneer tikka with vegetables and sauces inside soft naan. Appetizing cross-section. Warm restaurant lighting. Professional food photography. Smooth 5-second loop, web optimized.
## Namakpare
Photorealistic close-up of traditional Namakpare (savory fried snacks). Golden-brown crunchy diamond-shaped pieces. Fresh, crispy texture. Warm traditional Indian lighting. Professional sweet shop photography style. Short 5-second loop, web optimized.
## Rasmalai
Photorealistic close-up of Rasmalai. Soft spongy cottage cheese dumplings floating in rich saffron and cardamom sweet milk. Garnished with pistachios and saffron strands. Creamy luxurious texture. Elegant warm lighting. High-end Indian dessert photography. Smooth 5-second loop, web optimized.
---
## Notes for Generation
- Always mention "photorealistic, professional food photography, shallow depth of field".
- For dishes with steam/sizzle/pouring, explicitly mention "natural delicate steam rising" or "subtle motion".
- Keep prompts focused on the dish itself (minimal background).
- After generation, optimize heavily using the optimization script or ffmpeg (see `optimize-videos.sh`).
Use these prompts when regenerating videos for better realism.
-365
View File
@@ -1,365 +0,0 @@
# Kött Gård — Clean Architecture Guide
This document explains **what we changed**, **why**, and **how to work in the new structure**.
---
## What is Clean Architecture?
Clean Architecture splits code into **layers**. Inner layers hold business rules. Outer layers hold frameworks (React, Next.js, Zustand, localStorage).
**Golden rule:** Dependencies point **inward only**.
```
Presentation → Application → Domain
Infrastructure → Application → Domain
Domain imports NOTHING from outside itself.
```
---
## Before vs After
### Before (mixed responsibilities)
```
page.tsx → filter products, calculate delivery fee, create orders
store/cart.ts → persistence + merge logic + totals
lib/products.ts → data + queries in one file
```
Problems:
- Delivery fee `total > 500 ? 0 : 49` was copy-pasted in cart **and** checkout
- Shop filtering logic lived inside the React page
- Hard to swap localStorage for a real API later
### After (separated layers)
| Layer | Folder | Job |
|-------|--------|-----|
| **Domain** | `src/domain/` | Business rules — cart math, delivery fee, order IDs |
| **Application** | `src/application/` | Use cases + ports (interfaces) |
| **Infrastructure** | `src/infrastructure/` | Zustand, product data file, i18n adapter |
| **Presentation** | `src/app/`, `src/components/`, `src/presentation/hooks/` | UI only — calls use cases |
---
## Layer diagram
```mermaid
flowchart TB
subgraph Presentation
P1[app/pages]
P2[components]
P3[presentation/hooks]
end
subgraph Application
A1[use-cases]
A2[ports interfaces]
A3[container.ts]
end
subgraph Domain
D1[entities]
D2[domain services]
end
subgraph Infrastructure
I1[Zustand stores]
I2[InMemoryProductRepository]
I3[products.data.ts]
end
P1 --> P3
P3 --> A3
A3 --> A1
A1 --> A2
A1 --> D2
A2 -.implemented by.- I1
A2 -.implemented by.- I2
I2 --> I3
D2 --> D1
```
---
## Step-by-step: what we did
### Step 1 — Domain entities (`src/domain/entities/`)
**What:** Moved `Product`, `CartItem`, `Order`, `User`, etc. from `types/` into the domain.
**Why:** Entities are the core vocabulary of the business. They must not depend on React or Next.js.
**Example:** `Product` describes a meat item (price, category, slug) — not how it is rendered.
```
src/domain/entities/index.ts
```
`src/types/index.ts` now **re-exports** domain entities so old imports still work.
---
### Step 2 — Domain services (`src/domain/services/`)
**What:** Pure functions / classes for business rules.
| Service | Rule extracted from |
|---------|---------------------|
| `CartDomainService` | `store/cart.ts` — add item, merge duplicates, totals |
| `DeliveryFeeService` | `cart/page.tsx` + `checkout/page.tsx` — 49 kr fee, free over 500 kr |
| `CustomizationDomainService` | `lib/customization.ts` — cut styles, cart line keys |
| `OrderDomainService` | `checkout/page.tsx` — order ID format, order object shape |
| `AuthDomainService` | `store/auth.ts` — demo password check, user creation |
| `CatalogDomainService` | `shop/page.tsx` — filter by category, search, sort |
**Why:** One place per rule. Change delivery threshold once → cart and checkout both update.
**Example — delivery fee (single source of truth):**
```typescript
// src/domain/constants/commerce.ts
export const FREE_DELIVERY_THRESHOLD_SEK = 500;
export const STANDARD_DELIVERY_FEE_SEK = 49;
// src/domain/services/DeliveryFeeService.ts
static calculateDeliveryFee(subtotal: number): number {
return subtotal > FREE_DELIVERY_THRESHOLD_SEK ? 0 : STANDARD_DELIVERY_FEE_SEK;
}
```
---
### Step 3 — Application ports (`src/application/ports/`)
**What:** TypeScript **interfaces** describing what the app needs — not how it is stored.
| Port | Contract |
|------|----------|
| `IProductRepository` | `findAll()`, `findBySlug()`, `findFeatured()` |
| `ICartRepository` | `getItems()`, `addItem()`, `clearCart()` |
| `IAuthRepository` | `login()`, `register()`, `addOrder()` |
| `IWishlistRepository` | `toggleItem()`, `isInWishlist()` |
| `ITranslationService` | `translate(key)` |
**Why:** Today products live in a static array. Tomorrow they might come from Shopify or a database. **Only the infrastructure adapter changes** — use cases stay the same.
---
### Step 4 — Use cases (`src/application/use-cases/`)
**What:** One class per user action. Orchestrates domain + ports.
| Use case | Replaces logic in |
|----------|-------------------|
| `FilterProductsUseCase` | Shop page filtering/sorting |
| `GetProductBySlugUseCase` | `getProductBySlug()` calls |
| `LocalizeProductUseCase` | `lib/product-i18n.ts` |
| `AddToCartUseCase` | Cart add button |
| `GetCartSummaryUseCase` | Cart totals + delivery |
| `PlaceOrderUseCase` | Checkout submit handler |
| `LoginUseCase` / `RegisterUseCase` | Login page |
**Example — checkout flow:**
```
User clicks Pay
→ PlaceOrderUseCase.execute()
→ OrderDomainService.createOrder() [domain]
→ authRepository.addOrder() [port]
→ cartRepository.clearCart() [port]
```
File: `src/application/use-cases/checkout/PlaceOrder.ts`
---
### Step 5 — Infrastructure (`src/infrastructure/`)
**What:** Concrete implementations of ports + framework code.
| File | Role |
|------|------|
| `data/products.data.ts` | Static 16-product catalog |
| `repositories/InMemoryProductRepository.ts` | Implements `IProductRepository` |
| `persistence/zustand/cartStore.ts` | Zustand + `ZustandCartRepository` |
| `persistence/zustand/authStore.ts` | Auth persistence + demo user |
| `persistence/zustand/wishlistStore.ts` | Wishlist persistence |
| `i18n/I18nTranslationService.ts` | Wraps existing `i18n/` dictionaries |
**Why Zustand stays:** It is an **infrastructure detail** (browser storage). The application layer only sees `ICartRepository`.
---
### Step 6 — Composition root (`src/application/container.ts`)
**What:** One file that wires everything together.
```typescript
export const container = new ApplicationContainer();
// container.addToCart.execute(...)
// container.getCartSummary.execute()
// container.placeOrder.execute(...)
```
**Why:** Pages and hooks do not `new InMemoryProductRepository()` themselves. That would couple UI to infrastructure. The container is the **only** place that knows concrete classes.
---
### Step 7 — Presentation hooks (`src/presentation/hooks/`)
**What:** React-friendly API on top of the container.
| Hook | Use in |
|------|--------|
| `useCart()` | Cart page, Header, product page |
| `useAuth()` | Login, account, checkout |
| `useWishlist()` | Product card, wishlist page |
| `useCheckout()` | Checkout submit |
| `useCatalogFilter()` | Shop page |
**Example:**
```typescript
// Old (page knew about Zustand internals + business math)
const { getTotal } = useCartStore();
const deliveryFee = total > 500 ? 0 : 49;
// New (page uses use case result)
const { summary } = useCart();
const { subtotal, deliveryFee, grandTotal } = summary;
```
---
### Step 8 — Backward-compatible facades
Old paths still work so nothing breaks silently:
| Old import | Now points to |
|------------|---------------|
| `@/types` | `@/domain/entities` |
| `@/lib/customization` | `CustomizationDomainService` |
| `@/lib/products` | `container.productRepository` |
| `@/store/cart` | infrastructure + `useCart` hook |
---
## New folder map
```
src/
├── domain/
│ ├── entities/ # Product, Order, CartItem…
│ ├── constants/ # FREE_DELIVERY_THRESHOLD_SEK
│ └── services/ # CartDomainService, DeliveryFeeService…
├── application/
│ ├── ports/ # IProductRepository, ICartRepository…
│ ├── use-cases/ # AddToCart, PlaceOrder, FilterProducts…
│ ├── dtos/ # LocalizedProduct, CartSummary
│ └── container.ts # ★ wires everything
├── infrastructure/
│ ├── data/ # products.data.ts, demo-orders.data.ts
│ ├── repositories/ # InMemoryProductRepository
│ ├── persistence/zustand/ # Stores + repository adapters
│ └── i18n/ # I18nTranslationService
├── presentation/
│ └── hooks/ # useCart, useAuth, useCatalog…
├── app/ # Thin pages (UI + hooks)
├── components/ # Visual components
├── hooks/useTranslation.ts # Still used for t('key')
├── lib/ # Facades (backward compat)
├── store/ # Re-exports (backward compat)
└── types/ # Re-exports (backward compat)
```
---
## How to add a feature (cheat sheet)
### Change delivery fee rule
1. Edit `src/domain/constants/commerce.ts`
2. Done — `GetCartSummaryUseCase` and `PlaceOrderUseCase` pick it up automatically
### Add a new product
1. Add entry in `src/infrastructure/data/products.data.ts`
2. Add translations in `src/i18n/locales/*.ts`
### Add a new page action (e.g. "apply coupon")
1. Add rule in `src/domain/services/` if it is business logic
2. Add port method if it needs storage
3. Create `src/application/use-cases/cart/ApplyCoupon.ts`
4. Register in `container.ts`
5. Expose via `useCart()` or new hook
6. Call from page/component
### Replace static products with an API
1. Create `src/infrastructure/repositories/ApiProductRepository.ts` implementing `IProductRepository`
2. In `container.ts`, swap `InMemoryProductRepository``ApiProductRepository`
3. **No changes** to shop page, use cases, or domain
---
## Dependency rules (memorize this)
| Layer | Can import |
|-------|------------|
| Domain | Only domain |
| Application | Domain + its own ports/DTOs |
| Infrastructure | Application ports + Domain |
| Presentation | Application container + hooks + React |
| Layer | Cannot import |
|-------|---------------|
| Domain | React, Next, Zustand, `app/`, `components/` |
| Application | Zustand, `page.tsx`, JSX |
| Use cases | Concrete repositories (only interfaces) |
---
## Request flow example: Add to cart
```mermaid
sequenceDiagram
participant Page as product/page.tsx
participant Hook as useCart()
participant UC as AddToCartUseCase
participant Repo as ZustandCartRepository
participant Domain as CartDomainService
participant Store as Zustand persist
Page->>Hook: addItem(product, customization, label)
Hook->>UC: execute(...)
UC->>Repo: addItem(...)
Repo->>Domain: addItem(items, product, ...)
Domain-->>Repo: newItems[]
Repo->>Store: setItems(newItems)
Store-->>Page: React re-render
```
---
## Verify the project
```bash
cd /Users/apple/Desktop/code/Kottgard
npm run build # must pass
npm run dev # test at http://localhost:3000
```
Demo login: `demo@kottgard.se` / `demo123`
---
## Further reading
- Uncle Bob — Clean Architecture (concentric circles diagram)
- This project's annotated code: `docs/annotated/src/`
- Beginner folder map: `~/Desktop/Kottgard-Project-Learn/`
Binary file not shown.
-195
View File
@@ -1,195 +0,0 @@
/**
* Generates line-by-line annotated copies of every src/ file.
* Output: docs/annotated/src/** (mirrors src/ structure)
* Run: node docs/annotate-all.mjs
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const SRC = path.join(ROOT, 'src');
const OUT = path.join(__dirname, 'annotated', 'src');
function explainLine(line, lineNum, relPath) {
const t = line.trim();
const indent = line.match(/^(\s*)/)[1];
if (t === '') return `${indent}// (blank line — separates logical blocks for readability)`;
if (t.startsWith('/**') || t.startsWith('*') || t.startsWith('*/'))
return `${indent}// Block comment — documents the file or function below`;
if (t.startsWith('//')) return line;
if (t === "'use client';" || t === '"use client";')
return `${indent}// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)`;
if (t.startsWith('import type '))
return `${indent}// Type-only import — erased at compile time; no JavaScript bundle cost`;
if (t.startsWith('import ')) {
if (t.includes("from 'react'") || t.includes('from "react"'))
return `${indent}// Import React — core UI library (components, hooks, JSX)`;
if (t.includes('next/link'))
return `${indent}// Next.js Link — fast client-side navigation without full page reload`;
if (t.includes('next/image'))
return `${indent}// Next.js Image — optimized images (lazy load, WebP/AVIF)`;
if (t.includes('next/navigation'))
return `${indent}// Next.js App Router hooks — useRouter, useParams, useSearchParams`;
if (t.includes('next/font'))
return `${indent}// Self-hosted Google fonts — better performance than external CSS`;
if (t.includes('lucide-react'))
return `${indent}// Lucide icons — lightweight SVG icon components`;
if (t.includes('zustand'))
return `${indent}// Zustand — simple global state store (cart, auth, locale)`;
if (t.includes('@/'))
return `${indent}// Import project module (@/ alias = src/ folder in tsconfig)`;
if (t.includes("'./") || t.includes('"./'))
return `${indent}// Import from a relative file in the same project`;
return `${indent}// Import external package or local module`;
}
if (t.startsWith('export type ') || t.startsWith('export interface '))
return `${indent}// Export TypeScript type — defines data shape used across the app`;
if (t.startsWith('interface '))
return `${indent}// TypeScript interface — contract for object properties and methods`;
if (t.startsWith('type '))
return `${indent}// TypeScript type alias — union or shorthand for complex types`;
if (t.startsWith('export default function'))
return `${indent}// Default export — main component/page Next.js or other files import`;
if (t.startsWith('export function'))
return `${indent}// Named export — utility function other files can import`;
if (t.startsWith('export const '))
return `${indent}// Named export constant — shared config/data imported elsewhere`;
if (t.startsWith('const ') && t.includes('= create<'))
return `${indent}// Create Zustand store — global state hook (useXxxStore)`;
if (t.startsWith('const ') && t.includes('useState'))
return `${indent}// React useState — local component state that triggers re-render on change`;
if (t.startsWith('const ') && t.includes('useMemo'))
return `${indent}// React useMemo — cache expensive computed value until dependencies change`;
if (t.startsWith('const ') && t.includes('useCallback'))
return `${indent}// React useCallback — stable function reference for useEffect/useMemo deps`;
if (t.startsWith('const ') && t.includes('useEffect'))
return `${indent}// React useEffect — run side effect after render (sync URL, redirect, etc.)`;
if (t.startsWith('const ') && t.includes('useRouter'))
return `${indent}// Next.js router — programmatic navigation (router.push)`;
if (t.startsWith('const ') && t.includes('useSearchParams'))
return `${indent}// Read URL query string (?category=beef&q=steak)`;
if (t.startsWith('const ') && t.includes('useParams'))
return `${indent}// Read dynamic route segment ([slug] from URL)`;
if (t.startsWith('const ') && t.includes('useTranslation'))
return `${indent}// Custom hook — returns t() translator and current locale`;
if (t.startsWith('const ') && t.includes('useCartStore') || t.startsWith('const ') && t.includes('useAuthStore') || t.startsWith('const ') && t.includes('useWishlistStore') || t.startsWith('const ') && t.includes('useLocaleStore'))
return `${indent}// Zustand selector — subscribe to slice of global store`;
if (t.startsWith('function '))
return `${indent}// Function declaration — reusable logic in this file`;
if (t.startsWith('return ('))
return `${indent}// Return JSX — describes UI tree React renders to the DOM`;
if (t.startsWith('return '))
return `${indent}// Return value from function`;
if (t.startsWith('if (') || t.startsWith('} else if ('))
return `${indent}// Conditional branch — different behavior based on runtime value`;
if (t.startsWith('switch ('))
return `${indent}// Switch — multiple branches on one variable (e.g. sort order)`;
if (t.startsWith('case '))
return `${indent}// Switch case — handle one specific value`;
if (t.startsWith('default:'))
return `${indent}// Switch default — fallback when no case matches`;
if (t.startsWith('useEffect('))
return `${indent}// Side effect hook — runs after paint; deps array controls when it re-runs`;
if (t.includes('.map('))
return `${indent}// Array.map — transform each item (often render a list of components)`;
if (t.includes('.filter('))
return `${indent}// Array.filter — keep items matching condition (search, category)`;
if (t.includes('.sort('))
return `${indent}// Array.sort — reorder items (price, name, featured)`;
if (t.includes('.find('))
return `${indent}// Array.find — get first matching item or undefined`;
if (t.includes('.reduce('))
return `${indent}// Array.reduce — accumulate single value (cart total, item count)`;
if (t.startsWith('<') && !t.startsWith('<>'))
return `${indent}// JSX element — HTML-like tag becomes React component in browser`;
if (t.startsWith('<>') || t === '</>')
return `${indent}// React Fragment — group elements without extra wrapper DOM node`;
if (t.startsWith('{/*'))
return `${indent}// JSX comment — not visible in browser`;
if (t.startsWith('className='))
return `${indent}// Tailwind CSS utility classes — styling (colors, spacing, layout)`;
if (t.startsWith('href='))
return `${indent}// Link target URL — internal route or external https://`;
if (t.startsWith('onClick='))
return `${indent}// Click handler — runs when user clicks (must be client component)`;
if (t.startsWith('onChange='))
return `${indent}// Change handler — runs when input/select value changes`;
if (t.startsWith('aria-'))
return `${indent}// Accessibility attribute — screen readers and assistive tech`;
if (t.startsWith('}'))
return `${indent}// Closing brace — end of block (function, if, object, JSX)`;
if (t.startsWith(']') || t.startsWith('];'))
return `${indent}// End of array literal`;
if (t.endsWith(',') && !t.includes('//'))
return `${indent}// Property or array item — trailing comma allowed in TypeScript`;
if (relPath.includes('locales/') && t.match(/^\s{2}\w+:/))
return `${indent}// i18n translation section — keys used by t('section.key') in components`;
if (relPath.includes('locales/') && t.includes("name:") || relPath.includes('locales/') && t.includes("title:"))
return `${indent}// Translated UI string for current language (sv / en / ur)`;
if (relPath.includes('products.ts'))
return `${indent}// Product catalog entry — demo data shown in shop and product pages`;
if (t.includes('persist('))
return `${indent}// Zustand persist — save store to localStorage between visits`;
return `${indent}// Line ${lineNum}: ${t.length > 60 ? t.slice(0, 57) + '...' : t || 'code'}`;
}
function annotateFile(content, relPath) {
const lines = content.split('\n');
const ext = relPath.endsWith('.tsx') ? 'tsx' : 'ts';
const header = [
`/**`,
` * ANNOTATED COPY — every line explained`,
` * Source: src/${relPath}`,
` * NOT used by the app — read this to learn how the real file works`,
` */`,
'',
];
const body = lines.map((line, i) => {
const comment = explainLine(line, i + 1, relPath);
if (line.trim().startsWith('//') && comment === line) return line;
if (line.trim() === '' && comment.includes('blank')) return comment;
return `${comment}\n${line}`;
});
return header.join('\n') + body.join('\n') + '\n';
}
function walk(dir, base = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
let count = 0;
for (const e of entries) {
const rel = base ? `${base}/${e.name}` : e.name;
const full = path.join(dir, e.name);
if (e.isDirectory()) {
count += walk(full, rel);
} else if (/\.(ts|tsx)$/.test(e.name)) {
const content = fs.readFileSync(full, 'utf8');
const annotated = annotateFile(content, rel);
const outName = e.name.replace(/\.(tsx?)$/, '.annotated.$1');
const outDir = path.join(OUT, base);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, outName), annotated);
count++;
}
}
return count;
}
// Remove old flat annotated files (replaced by src/ mirror)
const oldFlat = path.join(__dirname, 'annotated');
for (const f of fs.readdirSync(oldFlat)) {
if (f.endsWith('.annotated.ts') || f.endsWith('.annotated.tsx')) {
if (!f.startsWith('0')) continue;
try { fs.unlinkSync(path.join(oldFlat, f)); } catch (_) {}
}
}
const n = walk(SRC);
console.log(`Annotated ${n} files → docs/annotated/src/`);
-63
View File
@@ -1,63 +0,0 @@
# Annotated Source Code — Every Line Explained
This folder contains **annotated copies** of **every** file in `src/`. Each line of the original code is followed or preceded by a comment explaining:
- **What** the syntax means (TypeScript, React, Next.js)
- **Why** it exists in the Kött Gård project
Production code in `src/` stays clean. Learning comments live here only.
## Regenerate all annotations
```bash
node docs/annotate-all.mjs
```
Run this after you change source files to refresh the annotated copies.
## Folder mirror
```
docs/annotated/src/ ← you are here (annotated)
src/ ← real app code (no line comments)
```
| Annotated path | Original path |
|----------------|---------------|
| `docs/annotated/src/app/page.annotated.tsx` | `src/app/page.tsx` |
| `docs/annotated/src/lib/products.annotated.ts` | `src/lib/products.ts` |
| `docs/annotated/src/store/cart.annotated.ts` | `src/store/cart.ts` |
| … | (52 files total) |
## File categories
| Folder | What it contains |
|--------|------------------|
| `app/` | Pages and routes (Next.js App Router) |
| `components/` | Reusable UI (home sections, layout, product cards) |
| `lib/` | Data, images, constants, helpers |
| `store/` | Zustand global state (cart, auth, wishlist, locale) |
| `i18n/` | Translations (Swedish, English, Urdu) |
| `hooks/` | Custom React hooks |
| `types/` | TypeScript interfaces |
## How to read
1. Open the **original** file in `src/` in your editor.
2. Open the matching **`.annotated.ts`** or **`.annotated.tsx`** file side-by-side.
3. Read the gray `//` comment above each line, then the code.
## Programming languages in this project
| Language | Role |
|----------|------|
| **TypeScript** | Types catch errors before run; interfaces for Product, Cart, Order |
| **React 18** | Components + JSX UI; hooks for state and effects |
| **Next.js 13** | File-based routing, layouts, metadata, image optimization |
| **Tailwind CSS** | Utility classes for burgundy/cream/gold design |
| **Zustand** | Global stores with `persist` → localStorage |
## Related documents
- Word guide: `docs/Kottgard-Website-Guide.docx`
- Visual guide: `../kottgard documentation/16-website-visual-and-code-guide.md`
@@ -1,249 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/about/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { ShieldCheck, Leaf, Award, Truck, Phone, Mail, MapPin } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import external package or local module
import {
// Property or array item — trailing comma allowed in TypeScript
SITE_ADDRESS,
// Property or array item — trailing comma allowed in TypeScript
SITE_EMAIL,
// Property or array item — trailing comma allowed in TypeScript
SITE_PHONE_DISPLAY,
// Property or array item — trailing comma allowed in TypeScript
SITE_HOURS,
// Closing brace — end of block (function, if, object, JSX)
} from '@/lib/constants';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function AboutPage() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// Line 15: const telHref = `tel:${SITE_PHONE_DISPLAY.replace(/\s/g, ...
const telHref = `tel:${SITE_PHONE_DISPLAY.replace(/\s/g, '')}`;
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-gradient-to-br from-brand-900 to-brand-950 px-4 py-20 text-white sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-3xl text-center">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-4 font-display text-4xl font-bold">
// Line 22: {t('about.title', { name: t('site.name') })}
{t('about.title', { name: t('site.name') })}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-lg text-brand-100">{t('about.subtitle')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<section className="mb-16">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-4">{t('about.ourStory')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-4 leading-relaxed text-gray-600">
// Line 32: {t('about.storyP1', { name: t('site.name') })}
{t('about.storyP1', { name: t('site.name') })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="leading-relaxed text-gray-600">{t('about.storyP2')}</p>
// JSX element — HTML-like tag becomes React component in browser
</section>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<section id="halal" className="mb-16">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 flex items-center gap-3">
// JSX element — HTML-like tag becomes React component in browser
<ShieldCheck className="h-8 w-8 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading">{t('about.halalTitle')}</h2>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<p className="leading-relaxed text-gray-600">
// Line 43: {t('about.halalDesc', { name: t('site.name') })}
{t('about.halalDesc', { name: t('site.name') })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</section>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<section className="mb-16">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-6 sm:grid-cols-3">
// Line 49: {[
{[
// Property or array item — trailing comma allowed in TypeScript
{ icon: Leaf, title: t('about.freshDaily'), desc: t('about.freshDailyDesc') },
// Line 51: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: Award,
// Property or array item — trailing comma allowed in TypeScript
title: t('about.premiumQuality'),
// Property or array item — trailing comma allowed in TypeScript
desc: t('about.premiumQualityDesc'),
// Closing brace — end of block (function, if, object, JSX)
},
// Line 56: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: Truck,
// Property or array item — trailing comma allowed in TypeScript
title: t('about.fastDelivery'),
// Property or array item — trailing comma allowed in TypeScript
desc: t('about.fastDeliveryDesc'),
// Closing brace — end of block (function, if, object, JSX)
},
// Array.map — transform each item (often render a list of components)
].map((item) => (
// JSX element — HTML-like tag becomes React component in browser
<div key={item.title} className="card-premium p-6 text-center">
// JSX element — HTML-like tag becomes React component in browser
<item.icon className="mx-auto mb-3 h-8 w-8 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-1 font-semibold text-brand-900">{item.title}</h3>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm text-gray-500">{item.desc}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 67: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<section id="delivery" className="mb-16">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-4">{t('about.deliveryTitle')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-3 text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<p>{t('about.delivery1')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p>{t('about.delivery2')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p>{t('about.delivery3')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p>{t('about.delivery4')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<section id="contact" className="card-premium mb-16 p-8">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-6">{t('about.contactTitle')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-4">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-3 text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<Phone className="h-5 w-5 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<a href={telHref} className="hover:text-brand-800">
// Line 87: {SITE_PHONE_DISPLAY}
{SITE_PHONE_DISPLAY}
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-3 text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<Mail className="h-5 w-5 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<a href={`mailto:${SITE_EMAIL}`} className="hover:text-brand-800">
// Line 93: {SITE_EMAIL}
{SITE_EMAIL}
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-start gap-3 text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<MapPin className="mt-0.5 h-5 w-5 shrink-0 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<span>
// Line 99: {SITE_ADDRESS}
{SITE_ADDRESS}
// JSX element — HTML-like tag becomes React component in browser
<br />
// Line 101: {t('footer.hours', { hours: SITE_HOURS })}
{t('footer.hours', { hours: SITE_HOURS })}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-6">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/shop"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="text-sm font-semibold text-brand-700 hover:text-brand-900"
// Line 109: >
>
// Line 110: {t('cta.button')} →
{t('cta.button')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<section id="privacy" className="mb-16">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-4">{t('about.privacyTitle')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="leading-relaxed text-gray-600">{t('about.privacyText')}</p>
// JSX element — HTML-like tag becomes React component in browser
</section>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<section id="terms" className="mb-8">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-4">{t('about.termsTitle')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="leading-relaxed text-gray-600">{t('about.termsText')}</p>
// JSX element — HTML-like tag becomes React component in browser
</section>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 126: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,321 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/account/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useEffect } from 'react';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Next.js App Router hooks — useRouter, useParams, useSearchParams
import { useRouter } from 'next/navigation';
// Lucide icons — lightweight SVG icon components
import { User, Package, MapPin, Phone, Mail, LogOut, Heart } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useAuthStore } from '@/store/auth';
// Import project module (@/ alias = src/ folder in tsconfig)
import { formatPrice, formatDate, getFormatLocale } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function AccountPage() {
// Next.js router — programmatic navigation (router.push)
const router = useRouter();
// Zustand selector — subscribe to slice of global store
const { user, isAuthenticated, orders, logout } = useAuthStore();
// Custom hook — returns t() translator and current locale
const { t, locale } = useTranslation();
// (blank line — separates logical blocks for readability)
// Side effect hook — runs after paint; deps array controls when it re-runs
useEffect(() => {
// Conditional branch — different behavior based on runtime value
if (!isAuthenticated) {
// Line 20: router.push('/login');
router.push('/login');
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
}, [isAuthenticated, router]);
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (!isAuthenticated || !user) {
// Return value from function
return null;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 28: const handleLogout = () => {
const handleLogout = () => {
// Line 29: logout();
logout();
// Line 30: router.push('/');
router.push('/');
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Line 33: const fmt = (n: number) => formatPrice(n, getFormatLocale...
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<div className="border-b border-gray-100 bg-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="section-heading">{t('account.title')}</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-gray-500">{t('account.welcome', { name: user.name })}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-8 lg:grid-cols-3">
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-6">
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-6">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-4 flex items-center gap-3">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-brand-100">
// JSX element — HTML-like tag becomes React component in browser
<User className="h-6 w-6 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="font-display text-lg font-semibold">{user.name}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm text-gray-500">
// Line 55: {t('account.memberSince', { date: formatDate(user.created...
{t('account.memberSince', { date: formatDate(user.createdAt) })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-3 border-t border-gray-100 pt-4">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-2 text-sm text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<Mail className="h-4 w-4 text-brand-600" />
// Line 63: {user.email}
{user.email}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-2 text-sm text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<Phone className="h-4 w-4 text-brand-600" />
// Line 67: {user.phone}
{user.phone}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-start gap-2 text-sm text-gray-600">
// JSX element — HTML-like tag becomes React component in browser
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-brand-600" />
// Line 71: {user.address.street}, {user.address.city}, {user.address...
{user.address.street}, {user.address.city}, {user.address.state}{' '}
// Line 72: {user.address.zip}
{user.address.zip}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-4">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/wishlist"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-brand-50 hover:text-brand-700"
// Line 81: >
>
// JSX element — HTML-like tag becomes React component in browser
<Heart className="h-4 w-4" />
// Line 83: {t('account.myWishlist')}
{t('account.myWishlist')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={handleLogout}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-red-500 transition-colors hover:bg-red-50"
// Line 88: >
>
// JSX element — HTML-like tag becomes React component in browser
<LogOut className="h-4 w-4" />
// Line 90: {t('account.signOut')}
{t('account.signOut')}
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="lg:col-span-2" id="orders">
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-6">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Package className="h-5 w-5 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h2 className="font-display text-lg font-semibold">
// Line 100: {t('account.orderHistory')}
{t('account.orderHistory')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// Line 104: {orders.length === 0 ? (
{orders.length === 0 ? (
// JSX element — HTML-like tag becomes React component in browser
<div className="py-12 text-center">
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-2 text-gray-500">{t('account.noOrders')}</p>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="primary">{t('account.startShopping')}</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 111: ) : (
) : (
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-4">
// Array.map — transform each item (often render a list of components)
{orders.map((order) => (
// JSX element — HTML-like tag becomes React component in browser
<div
// Line 115: key={order.id}
key={order.id}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-xl border border-gray-100 p-4 transition-colors hover:border-brand-200"
// Line 117: >
>
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-3 flex items-center justify-between">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm font-semibold text-brand-900">{order.id}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-gray-400">
// Line 122: {formatDate(order.createdAt)}
{formatDate(order.createdAt)}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="text-end">
// JSX element — HTML-like tag becomes React component in browser
<p className="font-bold text-brand-800">{fmt(order.total)}</p>
// JSX element — HTML-like tag becomes React component in browser
<span className="inline-block rounded-full bg-brand-50 px-2 py-0.5 text-xs font-medium capitalize text-brand-700">
// Line 128: {t(`orderStatus.${order.status}`)}
{t(`orderStatus.${order.status}`)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-1">
// Array.map — transform each item (often render a list of components)
{order.items.map((item) => {
// Line 134: const localized = localizeProduct(item.product, t);
const localized = localizeProduct(item.product, t);
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div
// Line 137: key={item.id}
key={item.id}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex justify-between text-sm text-gray-600"
// Line 139: >
>
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={`/product/${item.product.slug}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="hover:text-brand-700"
// Line 143: >
>
// Line 144: {localized.name} × {item.quantity}
{localized.name} × {item.quantity}
// JSX element — HTML-like tag becomes React component in browser
<span className="ms-2 text-xs text-gold-600">
// Line 146: ({item.customizationLabel})
({item.customizationLabel})
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<span>{fmt(item.product.price * item.quantity)}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 151: );
);
// Closing brace — end of block (function, if, object, JSX)
})}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 155: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 157: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 163: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,348 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/cart/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { Minus, Plus, Trash2, ShoppingBag, ArrowRight } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useCartStore } from '@/store/cart';
// Import project module (@/ alias = src/ folder in tsconfig)
import { formatPrice, getFormatLocale } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function CartPage() {
// Zustand selector — subscribe to slice of global store
const { items, updateQuantity, removeItem, getTotal } = useCartStore();
// Custom hook — returns t() translator and current locale
const { t, locale } = useTranslation();
// Line 15: const total = getTotal();
const total = getTotal();
// Line 16: const deliveryFee = total > 500 ? 0 : 49;
const deliveryFee = total > 500 ? 0 : 49;
// Line 17: const grandTotal = total + deliveryFee;
const grandTotal = total + deliveryFee;
// Line 18: const fmt = (n: number) => formatPrice(n, getFormatLocale...
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (items.length === 0) {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-brand-50">
// JSX element — HTML-like tag becomes React component in browser
<ShoppingBag className="h-10 w-10 text-brand-300" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-2 font-display text-2xl font-bold text-brand-900">
// Line 27: {t('cart.empty')}
{t('cart.empty')}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-8 text-gray-500">{t('cart.emptyHint')}</p>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="primary" size="lg">
// Line 32: {t('cart.startShopping')}
{t('cart.startShopping')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 36: );
);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<div className="border-b border-gray-100 bg-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="section-heading">{t('cart.title')}</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-gray-500">
// Line 45: {t('cart.itemsCount', { count: items.length })}
{t('cart.itemsCount', { count: items.length })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-8 lg:grid-cols-3">
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-4 lg:col-span-2">
// Array.map — transform each item (often render a list of components)
{items.map((item) => {
// Line 54: const localized = localizeProduct(item.product, t);
const localized = localizeProduct(item.product, t);
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div key={item.id} className="card-premium flex gap-4 p-4 sm:p-6">
// JSX element — HTML-like tag becomes React component in browser
<div className="relative h-24 w-24 shrink-0 overflow-hidden rounded-xl sm:h-28 sm:w-28">
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 59: src={item.product.image}
src={item.product.image}
// Line 60: alt={localized.name}
alt={localized.name}
// Line 61: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover"
// Line 63: sizes="(max-width: 640px) 96px, 112px"
sizes="(max-width: 640px) 96px, 112px"
// Line 64: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-1 flex-col">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-start justify-between">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={`/product/${item.product.slug}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="font-display text-lg font-semibold text-brand-900 hover:text-brand-700"
// Line 73: >
>
// Line 74: {localized.name}
{localized.name}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-1 text-xs uppercase tracking-wider text-brand-600">
// Line 77: {t(`categories.${item.product.category}.name`)}
{t(`categories.${item.product.category}.name`)}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => removeItem(item.id)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg p-2 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('cart.removeItem')}
// Line 84: >
>
// JSX element — HTML-like tag becomes React component in browser
<Trash2 className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-2 inline-flex items-center gap-1.5 rounded-full bg-gold-50 px-3 py-1">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xs font-medium text-gold-700">
// Line 91: {t('cart.customization')}
{t('cart.customization')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xs font-semibold text-gold-800">
// Line 94: {item.customizationLabel}
{item.customizationLabel}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-auto flex items-center justify-between pt-3">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center rounded-lg border border-gray-200">
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => updateQuantity(item.id, item.quantity - 1)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex h-9 w-9 items-center justify-center text-gray-500 hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('product.decreaseQty')}
// Line 104: >
>
// JSX element — HTML-like tag becomes React component in browser
<Minus className="h-3 w-3" />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
<span className="w-8 text-center text-sm font-semibold">
// Line 108: {item.quantity}
{item.quantity}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => updateQuantity(item.id, item.quantity + 1)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex h-9 w-9 items-center justify-center text-gray-500 hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('product.increaseQty')}
// Line 114: >
>
// JSX element — HTML-like tag becomes React component in browser
<Plus className="h-3 w-3" />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-lg font-bold text-brand-800">
// Line 119: {fmt(item.product.price * item.quantity)}
{fmt(item.product.price * item.quantity)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 124: );
);
// Closing brace — end of block (function, if, object, JSX)
})}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium sticky top-24 p-6">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="mb-4 font-display text-lg font-semibold text-brand-900">
// Line 131: {t('cart.orderSummary')}
{t('cart.orderSummary')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-3 border-b border-gray-100 pb-4">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex justify-between text-sm">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-gray-500">{t('cart.subtotal')}</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="font-medium">{fmt(total)}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex justify-between text-sm">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-gray-500">{t('cart.delivery')}</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="font-medium">
// Line 142: {deliveryFee === 0 ? (
{deliveryFee === 0 ? (
// JSX element — HTML-like tag becomes React component in browser
<span className="text-brand-700">{t('cart.free')}</span>
// Line 144: ) : (
) : (
// Line 145: fmt(deliveryFee)
fmt(deliveryFee)
// Line 146: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 149: {deliveryFee > 0 && (
{deliveryFee > 0 && (
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-gray-400">{t('cart.freeDeliveryHint')}</p>
// Line 151: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex justify-between py-4">
// JSX element — HTML-like tag becomes React component in browser
<span className="font-semibold text-brand-900">{t('cart.total')}</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xl font-bold text-brand-800">{fmt(grandTotal)}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link href="/checkout">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="gold" size="lg" className="w-full">
// Line 161: {t('cart.proceedCheckout')}
{t('cart.proceedCheckout')}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/shop"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="mt-3 block text-center text-sm font-medium text-brand-600 hover:text-brand-800"
// Line 169: >
>
// Line 170: {t('cart.continueShopping')}
{t('cart.continueShopping')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 177: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,701 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/checkout/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useState } from 'react';
// Next.js App Router hooks — useRouter, useParams, useSearchParams
import { useRouter } from 'next/navigation';
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { Lock, CreditCard, Truck, CheckCircle, ChevronLeft } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useCartStore } from '@/store/cart';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useAuthStore } from '@/store/auth';
// Import project module (@/ alias = src/ folder in tsconfig)
import { formatPrice, getFormatLocale } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// Import project module (@/ alias = src/ folder in tsconfig)
import { Order } from '@/types';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function CheckoutPage() {
// Next.js router — programmatic navigation (router.push)
const router = useRouter();
// Zustand selector — subscribe to slice of global store
const { items, getTotal, clearCart } = useCartStore();
// Zustand selector — subscribe to slice of global store
const { user, isAuthenticated, addOrder } = useAuthStore();
// Custom hook — returns t() translator and current locale
const { t, locale } = useTranslation();
// (blank line — separates logical blocks for readability)
// React useState — local component state that triggers re-render on change
const [paymentMethod, setPaymentMethod] = useState('card');
// React useState — local component state that triggers re-render on change
const [isProcessing, setIsProcessing] = useState(false);
// React useState — local component state that triggers re-render on change
const [orderComplete, setOrderComplete] = useState(false);
// React useState — local component state that triggers re-render on change
const [orderId, setOrderId] = useState('');
// (blank line — separates logical blocks for readability)
// React useState — local component state that triggers re-render on change
const [form, setForm] = useState({
// Property or array item — trailing comma allowed in TypeScript
name: user?.name || '',
// Property or array item — trailing comma allowed in TypeScript
email: user?.email || '',
// Property or array item — trailing comma allowed in TypeScript
phone: user?.phone || '',
// Property or array item — trailing comma allowed in TypeScript
street: user?.address.street || '',
// Property or array item — trailing comma allowed in TypeScript
city: user?.address.city || '',
// Property or array item — trailing comma allowed in TypeScript
state: user?.address.state || '',
// Property or array item — trailing comma allowed in TypeScript
zip: user?.address.zip || '',
// Property or array item — trailing comma allowed in TypeScript
cardNumber: '',
// Property or array item — trailing comma allowed in TypeScript
expiry: '',
// Property or array item — trailing comma allowed in TypeScript
cvv: '',
// Closing brace — end of block (function, if, object, JSX)
});
// (blank line — separates logical blocks for readability)
// Line 40: const total = getTotal();
const total = getTotal();
// Line 41: const deliveryFee = total > 500 ? 0 : 49;
const deliveryFee = total > 500 ? 0 : 49;
// Line 42: const grandTotal = total + deliveryFee;
const grandTotal = total + deliveryFee;
// Line 43: const fmt = (n: number) => formatPrice(n, getFormatLocale...
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (items.length === 0 && !orderComplete) {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-20 text-center">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-4 font-display text-2xl font-bold">{t('checkout.noItems')}</h1>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button>{t('checkout.goToShop')}</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 53: );
);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (orderComplete) {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-lg px-4 py-20 text-center">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-brand-50">
// JSX element — HTML-like tag becomes React component in browser
<CheckCircle className="h-12 w-12 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-2 font-display text-3xl font-bold text-brand-900">
// Line 63: {t('checkout.orderConfirmed')}
{t('checkout.orderConfirmed')}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-2 text-gray-500">{t('checkout.thankYou')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-8 text-sm font-medium text-brand-700">
// Line 67: {t('checkout.orderId', { id: orderId })}
{t('checkout.orderId', { id: orderId })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col gap-3 sm:flex-row sm:justify-center">
// JSX element — HTML-like tag becomes React component in browser
<Link href="/account">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="primary">{t('checkout.viewOrders')}</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="secondary">{t('cart.continueShopping')}</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 78: );
);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 81: const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
// Line 82: e.preventDefault();
e.preventDefault();
// Line 83: setIsProcessing(true);
setIsProcessing(true);
// (blank line — separates logical blocks for readability)
// Line 85: await new Promise((resolve) => setTimeout(resolve, 1500));
await new Promise((resolve) => setTimeout(resolve, 1500));
// (blank line — separates logical blocks for readability)
// Line 87: const newOrderId = `KG-${Date.now().toString(36).toUpperC...
const newOrderId = `KG-${Date.now().toString(36).toUpperCase()}`;
// Line 88: const order: Order = {
const order: Order = {
// Property or array item — trailing comma allowed in TypeScript
id: newOrderId,
// Property or array item — trailing comma allowed in TypeScript
items: [...items],
// Property or array item — trailing comma allowed in TypeScript
total: grandTotal,
// Property or array item — trailing comma allowed in TypeScript
status: 'confirmed',
// Property or array item — trailing comma allowed in TypeScript
createdAt: new Date().toISOString(),
// Line 94: deliveryAddress: {
deliveryAddress: {
// Property or array item — trailing comma allowed in TypeScript
street: form.street,
// Property or array item — trailing comma allowed in TypeScript
city: form.city,
// Property or array item — trailing comma allowed in TypeScript
state: form.state,
// Property or array item — trailing comma allowed in TypeScript
zip: form.zip,
// Closing brace — end of block (function, if, object, JSX)
},
// Property or array item — trailing comma allowed in TypeScript
paymentMethod,
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Line 103: addOrder(order);
addOrder(order);
// Line 104: clearCart();
clearCart();
// Line 105: setOrderId(newOrderId);
setOrderId(newOrderId);
// Line 106: setOrderComplete(true);
setOrderComplete(true);
// Line 107: setIsProcessing(false);
setIsProcessing(false);
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Line 110: const updateField = (field: string, value: string) => {
const updateField = (field: string, value: string) => {
// Line 111: setForm((prev) => ({ ...prev, [field]: value }));
setForm((prev) => ({ ...prev, [field]: value }));
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/cart"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="mb-6 inline-flex items-center gap-1 text-sm font-medium text-gray-500 hover:text-brand-700"
// Line 120: >
>
// JSX element — HTML-like tag becomes React component in browser
<ChevronLeft className="h-4 w-4 rtl:rotate-180" />
// Line 122: {t('checkout.backToCart')}
{t('checkout.backToCart')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<h1 className="section-heading mb-8">{t('checkout.title')}</h1>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<form onSubmit={handleSubmit}>
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-8 lg:grid-cols-3">
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-6 lg:col-span-2">
// Line 130: {!isAuthenticated && (
{!isAuthenticated && (
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-6">
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm text-gray-600">
// Line 133: {t('checkout.haveAccount')}{' '}
{t('checkout.haveAccount')}{' '}
// JSX element — HTML-like tag becomes React component in browser
<Link href="/login" className="font-semibold text-brand-700 hover:underline">
// Line 135: {t('checkout.signIn')}
{t('checkout.signIn')}
// JSX element — HTML-like tag becomes React component in browser
</Link>{' '}
// Line 137: {t('checkout.fasterCheckout')}
{t('checkout.fasterCheckout')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 140: )}
)}
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-6">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-4 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Truck className="h-5 w-5 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h2 className="font-display text-lg font-semibold">
// Line 146: {t('checkout.deliveryDetails')}
{t('checkout.deliveryDetails')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-4 sm:grid-cols-2">
// JSX element — HTML-like tag becomes React component in browser
<div className="sm:col-span-2">
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.fullName')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 153: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 155: value={form.name}
value={form.name}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('name', e.target.value)}
// Line 157: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.email')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 162: required
required
// Line 163: type="email"
type="email"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 165: value={form.email}
value={form.email}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('email', e.target.value)}
// Line 167: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.phone')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 172: required
required
// Line 173: type="tel"
type="tel"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 175: value={form.phone}
value={form.phone}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('phone', e.target.value)}
// Line 177: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="sm:col-span-2">
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.street')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 182: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 184: value={form.street}
value={form.street}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('street', e.target.value)}
// Line 186: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.city')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 191: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 193: value={form.city}
value={form.city}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('city', e.target.value)}
// Line 195: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.state')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 200: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 202: value={form.state}
value={form.state}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('state', e.target.value)}
// Line 204: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.zip')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 209: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 211: value={form.zip}
value={form.zip}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('zip', e.target.value)}
// Line 213: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-6">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-4 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<CreditCard className="h-5 w-5 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h2 className="font-display text-lg font-semibold">{t('checkout.payment')}</h2>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-4 flex gap-3">
// Array.map — transform each item (often render a list of components)
{['card', 'cod'].map((method) => (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 227: key={method}
key={method}
// Line 228: type="button"
type="button"
// Click handler — runs when user clicks (must be client component)
onClick={() => setPaymentMethod(method)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={`rounded-lg border-2 px-4 py-2 text-sm font-medium transition-all ${
// Line 231: paymentMethod === method
paymentMethod === method
// Line 232: ? 'border-brand-700 bg-brand-700 text-white'
? 'border-brand-700 bg-brand-700 text-white'
// Line 233: : 'border-gray-200 text-gray-600 hover:border-brand-300'
: 'border-gray-200 text-gray-600 hover:border-brand-300'
// Closing brace — end of block (function, if, object, JSX)
}`}
// Line 235: >
>
// Line 236: {method === 'card'
{method === 'card'
// Line 237: ? t('checkout.creditCard')
? t('checkout.creditCard')
// Line 238: : t('checkout.cashOnDelivery')}
: t('checkout.cashOnDelivery')}
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 240: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// Line 243: {paymentMethod === 'card' && (
{paymentMethod === 'card' && (
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-4 sm:grid-cols-2">
// JSX element — HTML-like tag becomes React component in browser
<div className="sm:col-span-2">
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.cardNumber')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 248: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 250: placeholder="1234 5678 9012 3456"
placeholder="1234 5678 9012 3456"
// Line 251: value={form.cardNumber}
value={form.cardNumber}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('cardNumber', e.target.value)}
// Line 253: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.expiry')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 258: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 260: placeholder="MM/YY"
placeholder="MM/YY"
// Line 261: value={form.expiry}
value={form.expiry}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('expiry', e.target.value)}
// Line 263: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('checkout.cvv')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 268: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 270: placeholder="123"
placeholder="123"
// Line 271: value={form.cvv}
value={form.cvv}
// Change handler — runs when input/select value changes
onChange={(e) => updateField('cvv', e.target.value)}
// Line 273: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 276: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium sticky top-24 p-6">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="mb-4 font-display text-lg font-semibold">
// Line 283: {t('cart.orderSummary')}
{t('cart.orderSummary')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-4 max-h-60 space-y-3 overflow-y-auto">
// Array.map — transform each item (often render a list of components)
{items.map((item) => {
// Line 288: const localized = localizeProduct(item.product, t);
const localized = localizeProduct(item.product, t);
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div key={item.id} className="flex gap-3">
// JSX element — HTML-like tag becomes React component in browser
<div className="relative h-12 w-12 shrink-0 overflow-hidden rounded-lg">
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 293: src={item.product.image}
src={item.product.image}
// Line 294: alt={localized.name}
alt={localized.name}
// Line 295: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover"
// Line 297: sizes="48px"
sizes="48px"
// Line 298: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex-1">
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm font-medium text-brand-900">
// Line 302: {localized.name}
{localized.name}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-gold-700">
// Line 305: {item.customizationLabel}
{item.customizationLabel}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-gray-400">
// Line 308: {t('checkout.qty', { count: item.quantity })}
{t('checkout.qty', { count: item.quantity })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-sm font-medium">
// Line 312: {fmt(item.product.price * item.quantity)}
{fmt(item.product.price * item.quantity)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 315: );
);
// Closing brace — end of block (function, if, object, JSX)
})}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-2 border-t border-gray-100 pt-4">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex justify-between text-sm">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-gray-500">{t('cart.subtotal')}</span>
// JSX element — HTML-like tag becomes React component in browser
<span>{fmt(total)}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex justify-between text-sm">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-gray-500">{t('cart.delivery')}</span>
// JSX element — HTML-like tag becomes React component in browser
<span>
// Line 327: {deliveryFee === 0 ? t('cart.free') : fmt(deliveryFee)}
{deliveryFee === 0 ? t('cart.free') : fmt(deliveryFee)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex justify-between pt-2 text-lg font-bold">
// JSX element — HTML-like tag becomes React component in browser
<span>{t('cart.total')}</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-brand-800">{fmt(grandTotal)}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Button
// Line 337: type="submit"
type="submit"
// Line 338: variant="gold"
variant="gold"
// Line 339: size="lg"
size="lg"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="mt-6 w-full"
// Line 341: disabled={isProcessing}
disabled={isProcessing}
// Line 342: >
>
// JSX element — HTML-like tag becomes React component in browser
<Lock className="h-4 w-4" />
// Line 344: {isProcessing
{isProcessing
// Line 345: ? t('checkout.processing')
? t('checkout.processing')
// Line 346: : t('checkout.pay', { amount: fmt(grandTotal) })}
: t('checkout.pay', { amount: fmt(grandTotal) })}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-3 flex items-center justify-center gap-1 text-xs text-gray-400">
// JSX element — HTML-like tag becomes React component in browser
<Lock className="h-3 w-3" />
// Line 351: {t('checkout.secure')}
{t('checkout.secure')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</form>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 359: );
);
// Closing brace — end of block (function, if, object, JSX)
}
-174
View File
@@ -1,174 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/layout.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Type-only import — erased at compile time; no JavaScript bundle cost
import type { Metadata } from 'next';
// Self-hosted Google fonts — better performance than external CSS
import { Inter, Playfair_Display, Noto_Nastaliq_Urdu } from 'next/font/google';
// Import project module (@/ alias = src/ folder in tsconfig)
import LanguageBanner from '@/components/layout/LanguageBanner';
// Import project module (@/ alias = src/ folder in tsconfig)
import Header from '@/components/layout/Header';
// Import project module (@/ alias = src/ folder in tsconfig)
import Footer from '@/components/layout/Footer';
// Import project module (@/ alias = src/ folder in tsconfig)
import LocaleAttributes from '@/components/layout/LocaleAttributes';
// Import project module (@/ alias = src/ folder in tsconfig)
import { SITE_NAME } from '@/lib/constants';
// Import from a relative file in the same project
import './globals.css';
// (blank line — separates logical blocks for readability)
// Line 10: const inter = Inter({
const inter = Inter({
// Property or array item — trailing comma allowed in TypeScript
subsets: ['latin'],
// Property or array item — trailing comma allowed in TypeScript
variable: '--font-inter',
// Property or array item — trailing comma allowed in TypeScript
display: 'swap',
// Closing brace — end of block (function, if, object, JSX)
});
// (blank line — separates logical blocks for readability)
// Line 16: const playfair = Playfair_Display({
const playfair = Playfair_Display({
// Property or array item — trailing comma allowed in TypeScript
subsets: ['latin'],
// Property or array item — trailing comma allowed in TypeScript
variable: '--font-playfair',
// Property or array item — trailing comma allowed in TypeScript
display: 'swap',
// Closing brace — end of block (function, if, object, JSX)
});
// (blank line — separates logical blocks for readability)
// Line 22: const notoUrdu = Noto_Nastaliq_Urdu({
const notoUrdu = Noto_Nastaliq_Urdu({
// Property or array item — trailing comma allowed in TypeScript
subsets: ['arabic'],
// Property or array item — trailing comma allowed in TypeScript
variable: '--font-urdu',
// Property or array item — trailing comma allowed in TypeScript
weight: ['400', '500', '600', '700'],
// Property or array item — trailing comma allowed in TypeScript
display: 'swap',
// Closing brace — end of block (function, if, object, JSX)
});
// (blank line — separates logical blocks for readability)
// Line 29: const siteDescription =
const siteDescription =
// Line 30: 'Premium 100% Halal meat delivery. Fresh and frozen chick...
'Premium 100% Halal meat delivery. Fresh and frozen chicken, beef, lamb, and fish — customized to your preference and delivered to your door.';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const viewport = {
// Property or array item — trailing comma allowed in TypeScript
themeColor: '#8B1F1F',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const metadata: Metadata = {
// Line 37: title: {
title: {
// Switch default — fallback when no case matches
default: `${SITE_NAME} — Premium Halal Meat Delivery`,
// Property or array item — trailing comma allowed in TypeScript
template: `%s | ${SITE_NAME}`,
// Closing brace — end of block (function, if, object, JSX)
},
// Property or array item — trailing comma allowed in TypeScript
description: siteDescription,
// Line 42: keywords: [
keywords: [
// Property or array item — trailing comma allowed in TypeScript
'halal meat',
// Property or array item — trailing comma allowed in TypeScript
'halal chicken',
// Property or array item — trailing comma allowed in TypeScript
'halal beef',
// Property or array item — trailing comma allowed in TypeScript
'halal lamb',
// Property or array item — trailing comma allowed in TypeScript
'meat delivery',
// Property or array item — trailing comma allowed in TypeScript
'fresh meat',
// Property or array item — trailing comma allowed in TypeScript
'Kött Gård',
// Property or array item — trailing comma allowed in TypeScript
'kottgard',
// End of array literal
],
// Line 52: openGraph: {
openGraph: {
// Property or array item — trailing comma allowed in TypeScript
title: `${SITE_NAME} — Premium Halal Meat Delivery`,
// Property or array item — trailing comma allowed in TypeScript
description: siteDescription,
// Property or array item — trailing comma allowed in TypeScript
type: 'website',
// Property or array item — trailing comma allowed in TypeScript
locale: 'sv_SE',
// Property or array item — trailing comma allowed in TypeScript
siteName: SITE_NAME,
// Closing brace — end of block (function, if, object, JSX)
},
// Line 59: robots: {
robots: {
// Property or array item — trailing comma allowed in TypeScript
index: true,
// Property or array item — trailing comma allowed in TypeScript
follow: true,
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function RootLayout({
// Property or array item — trailing comma allowed in TypeScript
children,
// Closing brace — end of block (function, if, object, JSX)
}: {
// Line 68: children: React.ReactNode;
children: React.ReactNode;
// Closing brace — end of block (function, if, object, JSX)
}) {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<html
// Line 72: lang="sv"
lang="sv"
// Line 73: dir="ltr"
dir="ltr"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={`${inter.variable} ${playfair.variable} ${notoUrdu.variable}`}
// Line 75: suppressHydrationWarning
suppressHydrationWarning
// Line 76: >
>
// JSX element — HTML-like tag becomes React component in browser
<body className="flex min-h-screen flex-col font-sans">
// JSX element — HTML-like tag becomes React component in browser
<LocaleAttributes />
// JSX element — HTML-like tag becomes React component in browser
<div className="sticky top-0 z-50">
// JSX element — HTML-like tag becomes React component in browser
<LanguageBanner />
// JSX element — HTML-like tag becomes React component in browser
<Header />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<main className="flex-1">{children}</main>
// JSX element — HTML-like tag becomes React component in browser
<Footer />
// JSX element — HTML-like tag becomes React component in browser
</body>
// JSX element — HTML-like tag becomes React component in browser
</html>
// Line 87: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,384 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/login/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useState } from 'react';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Next.js App Router hooks — useRouter, useParams, useSearchParams
import { useRouter } from 'next/navigation';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useAuthStore } from '@/store/auth';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { DEMO_EMAIL } from '@/lib/constants';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function LoginPage() {
// Next.js router — programmatic navigation (router.push)
const router = useRouter();
// Zustand selector — subscribe to slice of global store
const { login, register } = useAuthStore();
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// React useState — local component state that triggers re-render on change
const [isRegister, setIsRegister] = useState(false);
// React useState — local component state that triggers re-render on change
const [error, setError] = useState('');
// React useState — local component state that triggers re-render on change
const [form, setForm] = useState({
// Property or array item — trailing comma allowed in TypeScript
name: '',
// Property or array item — trailing comma allowed in TypeScript
email: '',
// Property or array item — trailing comma allowed in TypeScript
password: '',
// Property or array item — trailing comma allowed in TypeScript
phone: '',
// Property or array item — trailing comma allowed in TypeScript
street: '',
// Property or array item — trailing comma allowed in TypeScript
city: '',
// Property or array item — trailing comma allowed in TypeScript
state: '',
// Property or array item — trailing comma allowed in TypeScript
zip: '',
// Closing brace — end of block (function, if, object, JSX)
});
// (blank line — separates logical blocks for readability)
// Line 28: const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = (e: React.FormEvent) => {
// Line 29: e.preventDefault();
e.preventDefault();
// Line 30: setError('');
setError('');
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (isRegister) {
// Line 33: const success = register({
const success = register({
// Property or array item — trailing comma allowed in TypeScript
name: form.name,
// Property or array item — trailing comma allowed in TypeScript
email: form.email,
// Property or array item — trailing comma allowed in TypeScript
password: form.password,
// Property or array item — trailing comma allowed in TypeScript
phone: form.phone,
// Line 38: address: {
address: {
// Property or array item — trailing comma allowed in TypeScript
street: form.street,
// Property or array item — trailing comma allowed in TypeScript
city: form.city,
// Property or array item — trailing comma allowed in TypeScript
state: form.state,
// Property or array item — trailing comma allowed in TypeScript
zip: form.zip,
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
});
// Conditional branch — different behavior based on runtime value
if (success) router.push('/account');
// Closing brace — end of block (function, if, object, JSX)
} else {
// Line 47: const success = login(form.email, form.password);
const success = login(form.email, form.password);
// Conditional branch — different behavior based on runtime value
if (success) {
// Line 49: router.push('/account');
router.push('/account');
// Closing brace — end of block (function, if, object, JSX)
} else {
// Line 51: setError(t('auth.invalidCredentials', { email: DEMO_EMAIL...
setError(t('auth.invalidCredentials', { email: DEMO_EMAIL }));
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="flex min-h-[70vh] items-center justify-center bg-gray-50 px-4 py-12">
// JSX element — HTML-like tag becomes React component in browser
<div className="w-full max-w-md">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-8 text-center">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand-700">
// JSX element — HTML-like tag becomes React component in browser
<span className="font-display text-xl font-bold text-gold-400">
// Line 62: {t('site.initials')}
{t('site.initials')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<h1 className="font-display text-2xl font-bold text-brand-900">
// Line 66: {isRegister ? t('auth.createAccount') : t('auth.welcomeBa...
{isRegister ? t('auth.createAccount') : t('auth.welcomeBack')}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-1 text-sm text-gray-500">
// Line 69: {isRegister
{isRegister
// Line 70: ? t('auth.joinTagline', { name: t('site.name') })
? t('auth.joinTagline', { name: t('site.name') })
// Line 71: : t('auth.signInTagline', { name: t('site.name') })}
: t('auth.signInTagline', { name: t('site.name') })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium p-8">
// JSX element — HTML-like tag becomes React component in browser
<form onSubmit={handleSubmit} className="space-y-4">
// Line 77: {isRegister && (
{isRegister && (
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.fullName')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 81: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 83: value={form.name}
value={form.name}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, name: e.target.value })}
// Line 85: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 87: )}
)}
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.email')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 92: required
required
// Line 93: type="email"
type="email"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 95: value={form.email}
value={form.email}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, email: e.target.value })}
// Line 97: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.password')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 103: required
required
// Line 104: type="password"
type="password"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 106: value={form.password}
value={form.password}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, password: e.target.value })}
// Line 108: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// Line 111: {isRegister && (
{isRegister && (
// React Fragment — group elements without extra wrapper DOM node
<>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.phone')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 116: required
required
// Line 117: type="tel"
type="tel"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 119: value={form.phone}
value={form.phone}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, phone: e.target.value })}
// Line 121: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.street')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 126: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 128: value={form.street}
value={form.street}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, street: e.target.value })}
// Line 130: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="grid grid-cols-2 gap-3">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.city')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 136: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 138: value={form.city}
value={form.city}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, city: e.target.value })}
// Line 140: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.state')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 145: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 147: value={form.state}
value={form.state}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, state: e.target.value })}
// Line 149: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label className="label-text">{t('auth.zip')}</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 155: required
required
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 157: value={form.zip}
value={form.zip}
// Change handler — runs when input/select value changes
onChange={(e) => setForm({ ...form, zip: e.target.value })}
// Line 159: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</>
// Line 162: )}
)}
// (blank line — separates logical blocks for readability)
// Line 164: {error && (
{error && (
// JSX element — HTML-like tag becomes React component in browser
<p className="rounded-lg bg-red-50 px-4 py-2 text-sm text-red-600">{error}</p>
// Line 166: )}
)}
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Button type="submit" variant="primary" size="lg" className="w-full">
// Line 169: {isRegister ? t('auth.register') : t('auth.signIn')}
{isRegister ? t('auth.register') : t('auth.signIn')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</form>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-6 text-center">
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => {
// Line 176: setIsRegister(!isRegister);
setIsRegister(!isRegister);
// Line 177: setError('');
setError('');
// Closing brace — end of block (function, if, object, JSX)
}}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="text-sm font-medium text-brand-600 hover:text-brand-800"
// Line 180: >
>
// Line 181: {isRegister ? t('auth.hasAccount') : t('auth.noAccount')}
{isRegister ? t('auth.hasAccount') : t('auth.noAccount')}
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// Line 185: {!isRegister && (
{!isRegister && (
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-4 rounded-lg bg-brand-50 px-4 py-3 text-center">
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-brand-700">
// Line 188: {t('auth.demo', { email: DEMO_EMAIL })}
{t('auth.demo', { email: DEMO_EMAIL })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 191: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 195: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,44 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/not-found.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function NotFound() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4 text-center">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-2 font-display text-6xl font-bold text-brand-900">
// Line 13: {t('notFound.title')}
{t('notFound.title')}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-6 text-lg text-gray-500">{t('notFound.message')}</p>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/">
// JSX element — HTML-like tag becomes React component in browser
<Button>{t('notFound.goHome')}</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 20: );
);
// Closing brace — end of block (function, if, object, JSX)
}
-58
View File
@@ -1,58 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import Hero from '@/components/home/Hero';
// Import project module (@/ alias = src/ folder in tsconfig)
import TrustBadges from '@/components/home/TrustBadges';
// Import project module (@/ alias = src/ folder in tsconfig)
import AboutPreview from '@/components/home/AboutPreview';
// Import project module (@/ alias = src/ folder in tsconfig)
import CategoryGrid from '@/components/home/CategoryGrid';
// Import project module (@/ alias = src/ folder in tsconfig)
import FeaturedProducts from '@/components/home/FeaturedProducts';
// Import project module (@/ alias = src/ folder in tsconfig)
import HowItWorks from '@/components/home/HowItWorks';
// Import project module (@/ alias = src/ folder in tsconfig)
import WeeklyOffers from '@/components/home/WeeklyOffers';
// Import project module (@/ alias = src/ folder in tsconfig)
import SocialFollow from '@/components/home/SocialFollow';
// Import project module (@/ alias = src/ folder in tsconfig)
import ContactPreview from '@/components/home/ContactPreview';
// Import project module (@/ alias = src/ folder in tsconfig)
import CTA from '@/components/home/CTA';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function HomePage() {
// Return JSX — describes UI tree React renders to the DOM
return (
// React Fragment — group elements without extra wrapper DOM node
<>
// JSX element — HTML-like tag becomes React component in browser
<Hero />
// JSX element — HTML-like tag becomes React component in browser
<TrustBadges />
// JSX element — HTML-like tag becomes React component in browser
<AboutPreview />
// JSX element — HTML-like tag becomes React component in browser
<CategoryGrid />
// JSX element — HTML-like tag becomes React component in browser
<FeaturedProducts />
// JSX element — HTML-like tag becomes React component in browser
<HowItWorks />
// JSX element — HTML-like tag becomes React component in browser
<WeeklyOffers />
// JSX element — HTML-like tag becomes React component in browser
<CTA />
// JSX element — HTML-like tag becomes React component in browser
<SocialFollow />
// JSX element — HTML-like tag becomes React component in browser
<ContactPreview />
// JSX element — HTML-like tag becomes React component in browser
</>
// Line 26: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,434 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/product/[slug]/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useState } from 'react';
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Next.js App Router hooks — useRouter, useParams, useSearchParams
import { notFound, useParams } from 'next/navigation';
// Import external package or local module
import {
// Property or array item — trailing comma allowed in TypeScript
ShoppingCart,
// Property or array item — trailing comma allowed in TypeScript
Heart,
// Property or array item — trailing comma allowed in TypeScript
ChevronLeft,
// Property or array item — trailing comma allowed in TypeScript
Minus,
// Property or array item — trailing comma allowed in TypeScript
Plus,
// Property or array item — trailing comma allowed in TypeScript
Check,
// Property or array item — trailing comma allowed in TypeScript
ShieldCheck,
// Closing brace — end of block (function, if, object, JSX)
} from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import CustomizationSelector from '@/components/product/CustomizationSelector';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { getProductBySlug } from '@/lib/products';
// Import project module (@/ alias = src/ folder in tsconfig)
import { getDefaultCustomization, getCustomizationLabel } from '@/lib/customization';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// Import project module (@/ alias = src/ folder in tsconfig)
import { formatPrice, getFormatLocale } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useCartStore } from '@/store/cart';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useWishlistStore } from '@/store/wishlist';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { ProductCustomization } from '@/types';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function ProductPage() {
// Read dynamic route segment ([slug] from URL)
const params = useParams();
// Line 29: const slug = params.slug as string;
const slug = params.slug as string;
// Line 30: const rawProduct = getProductBySlug(slug);
const rawProduct = getProductBySlug(slug);
// Custom hook — returns t() translator and current locale
const { t, locale } = useTranslation();
// (blank line — separates logical blocks for readability)
// React useState — local component state that triggers re-render on change
const [customization, setCustomization] = useState<ProductCustomization>(
// Line 34: rawProduct ? getDefaultCustomization(rawProduct.category)...
rawProduct ? getDefaultCustomization(rawProduct.category) : { type: 'fish' }
// Line 35: );
);
// React useState — local component state that triggers re-render on change
const [quantity, setQuantity] = useState(1);
// React useState — local component state that triggers re-render on change
const [selectedImage, setSelectedImage] = useState(0);
// React useState — local component state that triggers re-render on change
const [added, setAdded] = useState(false);
// (blank line — separates logical blocks for readability)
// Zustand selector — subscribe to slice of global store
const addItem = useCartStore((s) => s.addItem);
// Zustand selector — subscribe to slice of global store
const { isInWishlist, toggleItem } = useWishlistStore();
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (!rawProduct) {
// Line 44: notFound();
notFound();
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 47: const product = localizeProduct(rawProduct, t);
const product = localizeProduct(rawProduct, t);
// Line 48: const inWishlist = isInWishlist(product.id);
const inWishlist = isInWishlist(product.id);
// (blank line — separates logical blocks for readability)
// Line 50: const handleAddToCart = () => {
const handleAddToCart = () => {
// Line 51: const label = getCustomizationLabel(customization, t);
const label = getCustomizationLabel(customization, t);
// Line 52: addItem(rawProduct, customization, label, quantity);
addItem(rawProduct, customization, label, quantity);
// Line 53: setAdded(true);
setAdded(true);
// Line 54: setTimeout(() => setAdded(false), 2000);
setTimeout(() => setAdded(false), 2000);
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/shop"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="mb-6 inline-flex items-center gap-1 text-sm font-medium text-gray-500 transition-colors hover:text-brand-700"
// Line 63: >
>
// JSX element — HTML-like tag becomes React component in browser
<ChevronLeft className="h-4 w-4 rtl:rotate-180" />
// Line 65: {t('product.backToShop')}
{t('product.backToShop')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-10 lg:grid-cols-2">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="relative mb-4 aspect-square overflow-hidden rounded-2xl bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 72: src={product.images[selectedImage] || product.image}
src={product.images[selectedImage] || product.image}
// Line 73: alt={product.name}
alt={product.name}
// Line 74: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover"
// Line 76: priority
priority
// Line 77: sizes="(max-width: 1024px) 100vw, 640px"
sizes="(max-width: 1024px) 100vw, 640px"
// Line 78: />
/>
// Line 79: {product.badge && (
{product.badge && (
// JSX element — HTML-like tag becomes React component in browser
<span className="absolute start-4 top-4 rounded-full bg-gold-500 px-4 py-1.5 text-sm font-semibold text-white shadow-gold">
// Line 81: {product.badge}
{product.badge}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 83: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 85: {product.images.length > 1 && (
{product.images.length > 1 && (
// JSX element — HTML-like tag becomes React component in browser
<div className="flex gap-3">
// Array.map — transform each item (often render a list of components)
{product.images.map((img, i) => (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 89: key={img}
key={img}
// Click handler — runs when user clicks (must be client component)
onClick={() => setSelectedImage(i)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={`relative h-20 w-20 overflow-hidden rounded-lg border-2 transition-all ${
// Line 92: selectedImage === i
selectedImage === i
// Line 93: ? 'border-brand-700'
? 'border-brand-700'
// Line 94: : 'border-transparent opacity-60 hover:opacity-100'
: 'border-transparent opacity-60 hover:opacity-100'
// Closing brace — end of block (function, if, object, JSX)
}`}
// Line 96: >
>
// JSX element — HTML-like tag becomes React component in browser
<AppImage src={img} alt="" fill className="object-cover" sizes="80px" />
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 99: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 101: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-2 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={`/shop?category=${product.category}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-full bg-brand-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-brand-700 transition-colors hover:bg-brand-100"
// Line 109: >
>
// Line 110: {t(`categories.${product.category}.name`)}
{t(`categories.${product.category}.name`)}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// Line 112: {product.inStock ? (
{product.inStock ? (
// JSX element — HTML-like tag becomes React component in browser
<span className="flex items-center gap-1 text-xs font-medium text-brand-700">
// JSX element — HTML-like tag becomes React component in browser
<Check className="h-3 w-3" /> {t('product.inStock')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 116: ) : (
) : (
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xs font-medium text-red-500">
// Line 118: {t('product.outOfStock')}
{t('product.outOfStock')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 120: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-2 font-display text-3xl font-bold text-brand-900 lg:text-4xl">
// Line 124: {product.name}
{product.name}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-4 text-gray-500">{product.description}</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 flex items-baseline gap-2">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-3xl font-bold text-brand-800">
// Line 131: {formatPrice(product.price, getFormatLocale(locale))}
{formatPrice(product.price, getFormatLocale(locale))}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-sm text-gray-400">{product.priceUnit}</span>
// Line 134: {product.weight && (
{product.weight && (
// JSX element — HTML-like tag becomes React component in browser
<span className="text-sm text-gray-400">· {product.weight}</span>
// Line 136: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 flex items-center gap-2 rounded-lg border border-brand-100 bg-brand-50/50 px-4 py-3">
// JSX element — HTML-like tag becomes React component in browser
<ShieldCheck className="h-5 w-5 shrink-0 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<span className="text-sm text-brand-800">{t('product.halalTrust')}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 rounded-2xl border border-gray-100 bg-gray-50 p-6">
// JSX element — HTML-like tag becomes React component in browser
<CustomizationSelector
// Line 146: category={product.category}
category={product.category}
// Line 147: customization={customization}
customization={customization}
// Change handler — runs when input/select value changes
onChange={setCustomization}
// Line 149: />
/>
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-4 rounded-lg bg-white px-4 py-3">
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-gray-500">{t('product.yourSelection')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm font-semibold text-brand-800">
// Line 153: {getCustomizationLabel(customization, t)}
{getCustomizationLabel(customization, t)}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 flex items-center gap-4">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center rounded-lg border border-gray-200">
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => setQuantity(Math.max(1, quantity - 1))}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex h-12 w-12 items-center justify-center text-gray-500 transition-colors hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('product.decreaseQty')}
// Line 164: >
>
// JSX element — HTML-like tag becomes React component in browser
<Minus className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
<span className="w-12 text-center font-semibold">{quantity}</span>
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => setQuantity(quantity + 1)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex h-12 w-12 items-center justify-center text-gray-500 transition-colors hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('product.increaseQty')}
// Line 172: >
>
// JSX element — HTML-like tag becomes React component in browser
<Plus className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Button
// Click handler — runs when user clicks (must be client component)
onClick={handleAddToCart}
// Line 179: disabled={!product.inStock}
disabled={!product.inStock}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex-1"
// Line 181: size="lg"
size="lg"
// Line 182: >
>
// Line 183: {added ? (
{added ? (
// React Fragment — group elements without extra wrapper DOM node
<>
// JSX element — HTML-like tag becomes React component in browser
<Check className="h-4 w-4" />
// Line 186: {t('product.addedToCart')}
{t('product.addedToCart')}
// JSX element — HTML-like tag becomes React component in browser
</>
// Line 188: ) : (
) : (
// React Fragment — group elements without extra wrapper DOM node
<>
// JSX element — HTML-like tag becomes React component in browser
<ShoppingCart className="h-4 w-4" />
// Line 191: {t('product.addToCart')}
{t('product.addToCart')}
// JSX element — HTML-like tag becomes React component in browser
</>
// Line 193: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => toggleItem(rawProduct)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={`flex h-12 w-12 items-center justify-center rounded-lg border-2 transition-all ${
// Line 199: inWishlist
inWishlist
// Line 200: ? 'border-red-200 bg-red-50 text-red-500'
? 'border-red-200 bg-red-50 text-red-500'
// Line 201: : 'border-gray-200 text-gray-400 hover:border-brand-300 h...
: 'border-gray-200 text-gray-400 hover:border-brand-300 hover:text-brand-700'
// Closing brace — end of block (function, if, object, JSX)
}`}
// Accessibility attribute — screen readers and assistive tech
aria-label={
// Line 204: inWishlist ? t('product.removeWishlist') : t('product.add...
inWishlist ? t('product.removeWishlist') : t('product.addWishlist')
// Closing brace — end of block (function, if, object, JSX)
}
// Line 206: >
>
// JSX element — HTML-like tag becomes React component in browser
<Heart className={`h-5 w-5 ${inWishlist ? 'fill-current' : ''}`} />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="border-t border-gray-100 pt-6">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="mb-3 font-display text-lg font-semibold text-brand-900">
// Line 213: {t('product.aboutProduct')}
{t('product.aboutProduct')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm leading-relaxed text-gray-600">
// Line 216: {product.longDescription}
{product.longDescription}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 223: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,48 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/shop/layout.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Import React — core UI library (components, hooks, JSX)
import { Suspense } from 'react';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const metadata = {
// Property or array item — trailing comma allowed in TypeScript
title: 'Shop',
// Line 5: description:
description:
// Property or array item — trailing comma allowed in TypeScript
'Browse premium halal chicken, beef, lamb, and fish. Customized cuts delivered fresh to your door.',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function ShopLayout({
// Property or array item — trailing comma allowed in TypeScript
children,
// Closing brace — end of block (function, if, object, JSX)
}: {
// Line 12: children: React.ReactNode;
children: React.ReactNode;
// Closing brace — end of block (function, if, object, JSX)
}) {
// Return value from function
return <Suspense fallback={<ShopLoading />}>{children}</Suspense>;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Function declaration — reusable logic in this file
function ShopLoading() {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="flex min-h-[50vh] items-center justify-center">
// JSX element — HTML-like tag becomes React component in browser
<div className="h-8 w-8 animate-spin rounded-full border-4 border-brand-200 border-t-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 22: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,275 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/shop/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useCallback, useEffect, useMemo, useState } from 'react';
// Next.js App Router hooks — useRouter, useParams, useSearchParams
import { useRouter, useSearchParams } from 'next/navigation';
// Import project module (@/ alias = src/ folder in tsconfig)
import ProductCard from '@/components/product/ProductCard';
// Import project module (@/ alias = src/ folder in tsconfig)
import ShopFilters from '@/components/shop/ShopFilters';
// Import project module (@/ alias = src/ folder in tsconfig)
import { products } from '@/lib/products';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// Import project module (@/ alias = src/ folder in tsconfig)
import { Category, SortOption } from '@/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Line 12: const CATEGORIES: Category[] = ['chicken', 'beef', 'lamb'...
const CATEGORIES: Category[] = ['chicken', 'beef', 'lamb', 'fish'];
// (blank line — separates logical blocks for readability)
// Function declaration — reusable logic in this file
function parseCategory(value: string | null): Category | 'all' {
// Return value from function
return CATEGORIES.includes(value as Category) ? (value as Category) : 'all';
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function ShopPage() {
// Next.js router — programmatic navigation (router.push)
const router = useRouter();
// Read URL query string (?category=beef&q=steak)
const searchParams = useSearchParams();
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// React useState — local component state that triggers re-render on change
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>(
// Line 24: parseCategory(searchParams.get('category'))
parseCategory(searchParams.get('category'))
// Line 25: );
);
// React useState — local component state that triggers re-render on change
const [sortBy, setSortBy] = useState<SortOption>('featured');
// React useState — local component state that triggers re-render on change
const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '');
// (blank line — separates logical blocks for readability)
// Side effect hook — runs after paint; deps array controls when it re-runs
useEffect(() => {
// Line 30: setSelectedCategory(parseCategory(searchParams.get('categ...
setSelectedCategory(parseCategory(searchParams.get('category')));
// Line 31: setSearchQuery(searchParams.get('q') || '');
setSearchQuery(searchParams.get('q') || '');
// Closing brace — end of block (function, if, object, JSX)
}, [searchParams]);
// (blank line — separates logical blocks for readability)
// React useCallback — stable function reference for useEffect/useMemo deps
const pushParams = useCallback(
// Line 35: (updates: { category?: Category | 'all'; q?: string }) => {
(updates: { category?: Category | 'all'; q?: string }) => {
// Line 36: const params = new URLSearchParams(searchParams.toString());
const params = new URLSearchParams(searchParams.toString());
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (updates.category !== undefined) {
// Conditional branch — different behavior based on runtime value
if (updates.category === 'all') params.delete('category');
// Line 40: else params.set('category', updates.category);
else params.set('category', updates.category);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (updates.q !== undefined) {
// Conditional branch — different behavior based on runtime value
if (updates.q) params.set('q', updates.q);
// Line 45: else params.delete('q');
else params.delete('q');
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 48: const qs = params.toString();
const qs = params.toString();
// Line 49: router.push(qs ? `/shop?${qs}` : '/shop', { scroll: false...
router.push(qs ? `/shop?${qs}` : '/shop', { scroll: false });
// Closing brace — end of block (function, if, object, JSX)
},
// Line 51: [router, searchParams]
[router, searchParams]
// Line 52: );
);
// (blank line — separates logical blocks for readability)
// Line 54: const handleCategoryChange = (category: Category | 'all')...
const handleCategoryChange = (category: Category | 'all') => {
// Line 55: setSelectedCategory(category);
setSelectedCategory(category);
// Line 56: pushParams({ category });
pushParams({ category });
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Line 59: const handleSearchChange = (query: string) => {
const handleSearchChange = (query: string) => {
// Line 60: setSearchQuery(query);
setSearchQuery(query);
// Line 61: pushParams({ q: query });
pushParams({ q: query });
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// React useMemo — cache expensive computed value until dependencies change
const filteredProducts = useMemo(() => {
// Array.map — transform each item (often render a list of components)
let result = products.map((p) => localizeProduct(p, t));
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (selectedCategory !== 'all') {
// Array.filter — keep items matching condition (search, category)
result = result.filter((p) => p.category === selectedCategory);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (searchQuery.trim()) {
// Line 72: const query = searchQuery.toLowerCase();
const query = searchQuery.toLowerCase();
// Array.filter — keep items matching condition (search, category)
result = result.filter(
// Line 74: (p) =>
(p) =>
// Line 75: p.name.toLowerCase().includes(query) ||
p.name.toLowerCase().includes(query) ||
// Line 76: p.description.toLowerCase().includes(query) ||
p.description.toLowerCase().includes(query) ||
// Line 77: p.tags.some((tag) => tag.includes(query))
p.tags.some((tag) => tag.includes(query))
// Line 78: );
);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Switch — multiple branches on one variable (e.g. sort order)
switch (sortBy) {
// Switch case — handle one specific value
case 'price-asc':
// Array.sort — reorder items (price, name, featured)
result.sort((a, b) => a.price - b.price);
// Line 84: break;
break;
// Switch case — handle one specific value
case 'price-desc':
// Array.sort — reorder items (price, name, featured)
result.sort((a, b) => b.price - a.price);
// Line 87: break;
break;
// Switch case — handle one specific value
case 'name':
// Array.sort — reorder items (price, name, featured)
result.sort((a, b) => a.name.localeCompare(b.name));
// Line 90: break;
break;
// Switch case — handle one specific value
case 'featured':
// Switch default — fallback when no case matches
default:
// Array.sort — reorder items (price, name, featured)
result.sort((a, b) => (b.featured ? 1 : 0) - (a.featured ? 1 : 0));
// Line 94: break;
break;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Return value from function
return result;
// Closing brace — end of block (function, if, object, JSX)
}, [selectedCategory, sortBy, searchQuery, t]);
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<div className="border-b border-gray-100 bg-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="section-heading mb-2">{t('shop.title')}</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-gray-500">{t('shop.subtitle')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col gap-8 lg:flex-row">
// JSX element — HTML-like tag becomes React component in browser
<aside className="lg:w-64 lg:shrink-0">
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium sticky top-24 p-6">
// JSX element — HTML-like tag becomes React component in browser
<ShopFilters
// Line 114: selectedCategory={selectedCategory}
selectedCategory={selectedCategory}
// Line 115: onCategoryChange={handleCategoryChange}
onCategoryChange={handleCategoryChange}
// Line 116: sortBy={sortBy}
sortBy={sortBy}
// Line 117: onSortChange={setSortBy}
onSortChange={setSortBy}
// Line 118: searchQuery={searchQuery}
searchQuery={searchQuery}
// Line 119: onSearchChange={handleSearchChange}
onSearchChange={handleSearchChange}
// Line 120: totalResults={filteredProducts.length}
totalResults={filteredProducts.length}
// Line 121: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</aside>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex-1">
// Line 126: {filteredProducts.length === 0 ? (
{filteredProducts.length === 0 ? (
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium flex flex-col items-center justify-center p-16 text-center">
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-2 text-lg font-semibold text-brand-900">
// Line 129: {t('shop.noProducts')}
{t('shop.noProducts')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm text-gray-500">{t('shop.noProductsHint')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 133: ) : (
) : (
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
// Array.map — transform each item (often render a list of components)
{filteredProducts.map((product) => (
// JSX element — HTML-like tag becomes React component in browser
<ProductCard key={product.id} product={product} />
// Line 137: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 139: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 144: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,103 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/app/wishlist/page.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { Heart } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import ProductCard from '@/components/product/ProductCard';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useWishlistStore } from '@/store/wishlist';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function WishlistPage() {
// Zustand selector — subscribe to slice of global store
const items = useWishlistStore((s) => s.items);
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// Array.map — transform each item (often render a list of components)
const localizedItems = items.map((p) => localizeProduct(p, t));
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<div className="border-b border-gray-100 bg-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<h1 className="section-heading">{t('wishlist.title')}</h1>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-gray-500">
// Line 22: {t('wishlist.saved', { count: items.length })}
{t('wishlist.saved', { count: items.length })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
// Line 28: {items.length === 0 ? (
{items.length === 0 ? (
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium flex flex-col items-center justify-center p-16 text-center">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-50">
// JSX element — HTML-like tag becomes React component in browser
<Heart className="h-8 w-8 text-red-300" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="mb-2 text-lg font-semibold text-brand-900">
// Line 34: {t('wishlist.empty')}
{t('wishlist.empty')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-6 text-sm text-gray-500">{t('wishlist.emptyHint')}</p>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button>{t('wishlist.browse')}</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 41: ) : (
) : (
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
// Array.map — transform each item (often render a list of components)
{localizedItems.map((product) => (
// JSX element — HTML-like tag becomes React component in browser
<ProductCard key={product.id} product={product} />
// Line 45: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 47: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 50: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,132 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/AboutPreview.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { ArrowRight } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { IMAGES } from '@/lib/images';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function AboutPreview() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Line 13: const stats = [
const stats = [
// Property or array item — trailing comma allowed in TypeScript
{ value: t('aboutPreview.statHalal'), label: t('aboutPreview.statHalalLabel') },
// Property or array item — trailing comma allowed in TypeScript
{ value: t('aboutPreview.statDays'), label: t('aboutPreview.statDaysLabel') },
// Property or array item — trailing comma allowed in TypeScript
{ value: t('aboutPreview.statDelivery'), label: t('aboutPreview.statDeliveryLabel') },
// Property or array item — trailing comma allowed in TypeScript
{ value: t('aboutPreview.statFresh'), label: t('aboutPreview.statFreshLabel') },
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="bg-white py-20">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
// Line 26: {t('aboutPreview.label')}
{t('aboutPreview.label')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-6">{t('aboutPreview.title')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-4 leading-relaxed text-gray-600">{t('aboutPreview.p1')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-4 leading-relaxed text-gray-600">{t('aboutPreview.p2')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-8 leading-relaxed text-gray-600">{t('aboutPreview.p3')}</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-8 grid grid-cols-2 gap-4 sm:grid-cols-4">
// Array.map — transform each item (often render a list of components)
{stats.map((stat) => (
// JSX element — HTML-like tag becomes React component in browser
<div
// Line 36: key={stat.label}
key={stat.label}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-xl border border-cream-300/80 bg-cream-50 p-4 text-center"
// Line 38: >
>
// JSX element — HTML-like tag becomes React component in browser
<p className="font-display text-2xl font-bold text-brand-800">{stat.value}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-1 text-xs font-medium text-gray-500">{stat.label}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 42: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link href="/about">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="secondary">
// Line 47: {t('aboutPreview.readMore')}
{t('aboutPreview.readMore')}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl shadow-premium-lg">
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 55: src={IMAGES.aboutMeat}
src={IMAGES.aboutMeat}
// Line 56: alt={t('aboutPreview.imageAlt')}
alt={t('aboutPreview.imageAlt')}
// Line 57: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover"
// Line 59: sizes="(max-width: 1024px) 100vw, 640px"
sizes="(max-width: 1024px) 100vw, 640px"
// Line 60: />
/>
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/30 via-transparent to-transparent" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 66: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,95 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/CTA.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Lucide icons — lightweight SVG icon components
import { ArrowRight, MessageCircle } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { WHATSAPP_URL } from '@/lib/constants';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function CTA() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="py-20">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-brand-700 to-brand-900 px-8 py-16 text-center sm:px-16">
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute -right-20 -top-20 h-60 w-60 rounded-full bg-gold-500/10" />
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute -bottom-16 -left-16 h-48 w-48 rounded-full bg-gold-500/10" />
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="relative">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="mb-4 font-display text-3xl font-bold text-white md:text-4xl">
// Line 21: {t('cta.title')}
{t('cta.title')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mx-auto mb-8 max-w-xl text-brand-100">{t('cta.subtitle')}</p>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="gold" size="lg">
// Line 27: {t('cta.button')}
{t('cta.button')}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
// JSX element — HTML-like tag becomes React component in browser
<Button
// Line 33: variant="secondary"
variant="secondary"
// Line 34: size="lg"
size="lg"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15"
// Line 36: >
>
// JSX element — HTML-like tag becomes React component in browser
<MessageCircle className="h-4 w-4" />
// Line 38: {t('cta.whatsapp')}
{t('cta.whatsapp')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 46: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,110 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/CategoryGrid.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { ArrowRight } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { CATEGORY_IDS, CATEGORY_IMAGES } from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Default export — main component/page Next.js or other files import
export default function CategoryGrid() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="bg-cream-50 py-20">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-12 text-center">
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-3">{t('categories.title')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mx-auto max-w-2xl text-gray-500">{t('categories.subtitle')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
// Array.map — transform each item (often render a list of components)
{CATEGORY_IDS.map((id) => (
// JSX element — HTML-like tag becomes React component in browser
<Link
// Line 22: key={id}
key={id}
// Link target URL — internal route or external https://
href={`/shop?category=${id}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="group relative overflow-hidden rounded-2xl shadow-premium transition-all duration-300 hover:shadow-premium-lg"
// Line 25: >
>
// JSX element — HTML-like tag becomes React component in browser
<div className="relative aspect-[3/4]">
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 28: src={CATEGORY_IMAGES[id]}
src={CATEGORY_IMAGES[id]}
// Line 29: alt={t(`categories.${id}.name`)}
alt={t(`categories.${id}.name`)}
// Line 30: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover transition-transform duration-500 group-hover:scale-110"
// Line 32: sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw...
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 320px"
// Line 33: />
/>
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/92 via-brand-800/50 to-brand-700/10" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute bottom-0 left-0 right-0 p-6">
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-1 font-display text-2xl font-bold text-white">
// Line 38: {t(`categories.${id}.name`)}
{t(`categories.${id}.name`)}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-3 text-sm text-brand-200">
// Line 41: {t(`categories.${id}.description`)}
{t(`categories.${id}.description`)}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<span className="inline-flex items-center gap-1 text-sm font-semibold text-gold-400 transition-colors group-hover:text-gold-300">
// Line 44: {t('categories.shop', { name: t(`categories.${id}.name`) })}
{t('categories.shop', { name: t(`categories.${id}.name`) })}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1 rtl:rotate-180 rtl:group-hover:-translate-x-1" />
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// Line 49: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 53: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,233 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/ContactPreview.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { Phone, MapPin, Clock, MessageCircle, ExternalLink } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import Logo from '@/components/ui/Logo';
// Import external package or local module
import {
// Property or array item — trailing comma allowed in TypeScript
SITE_ADDRESS,
// Property or array item — trailing comma allowed in TypeScript
SITE_PHONE_DISPLAY,
// Property or array item — trailing comma allowed in TypeScript
SITE_HOURS,
// Property or array item — trailing comma allowed in TypeScript
WHATSAPP_URL,
// Property or array item — trailing comma allowed in TypeScript
MAPS_URL,
// Closing brace — end of block (function, if, object, JSX)
} from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function ContactPreview() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// Line 18: const telHref = `tel:+46725855050`;
const telHref = `tel:+46725855050`;
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="bg-brand-950 py-20 text-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-12 lg:grid-cols-2 lg:gap-16">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-400">
// Line 26: {t('contact.label')}
{t('contact.label')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="mb-8 font-display text-3xl font-bold md:text-4xl">
// Line 29: {t('contact.title')}
{t('contact.title')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<ul className="mb-8 space-y-5">
// JSX element — HTML-like tag becomes React component in browser
<li className="flex gap-4">
// JSX element — HTML-like tag becomes React component in browser
<MapPin className="mt-0.5 h-5 w-5 shrink-0 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs font-semibold uppercase tracking-wider text-gold-400">
// Line 37: {t('contact.addressLabel')}
{t('contact.addressLabel')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-brand-100">{SITE_ADDRESS}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</li>
// JSX element — HTML-like tag becomes React component in browser
<li className="flex gap-4">
// JSX element — HTML-like tag becomes React component in browser
<Phone className="mt-0.5 h-5 w-5 shrink-0 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs font-semibold uppercase tracking-wider text-gold-400">
// Line 46: {t('contact.phoneLabel')}
{t('contact.phoneLabel')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<a href={telHref} className="text-brand-100 hover:text-white">
// Line 49: {SITE_PHONE_DISPLAY}
{SITE_PHONE_DISPLAY}
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</li>
// JSX element — HTML-like tag becomes React component in browser
<li className="flex gap-4">
// JSX element — HTML-like tag becomes React component in browser
<Clock className="mt-0.5 h-5 w-5 shrink-0 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs font-semibold uppercase tracking-wider text-gold-400">
// Line 57: {t('contact.hoursLabel')}
{t('contact.hoursLabel')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-brand-100">
// Line 60: {t('contact.hoursValue', { hours: SITE_HOURS })}
{t('contact.hoursValue', { hours: SITE_HOURS })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</li>
// JSX element — HTML-like tag becomes React component in browser
</ul>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col gap-3 sm:flex-row">
// JSX element — HTML-like tag becomes React component in browser
<Link href="/about#contact">
// JSX element — HTML-like tag becomes React component in browser
<Button
// Line 69: variant="secondary"
variant="secondary"
// Line 70: size="lg"
size="lg"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="w-full border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15 sm:w-auto"
// Line 72: >
>
// Line 73: {t('contact.learnMore')}
{t('contact.learnMore')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="gold" size="lg" className="w-full sm:w-auto">
// JSX element — HTML-like tag becomes React component in browser
<MessageCircle className="h-4 w-4" />
// Line 79: {t('contact.writeUs')}
{t('contact.writeUs')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
<a href={telHref}>
// JSX element — HTML-like tag becomes React component in browser
<Button
// Line 84: variant="secondary"
variant="secondary"
// Line 85: size="lg"
size="lg"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="w-full border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15 sm:w-auto"
// Line 87: >
>
// JSX element — HTML-like tag becomes React component in browser
<Phone className="h-4 w-4" />
// Line 89: {t('contact.callUs')}
{t('contact.callUs')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col justify-center">
// JSX element — HTML-like tag becomes React component in browser
<a
// Link target URL — internal route or external https://
href={MAPS_URL}
// Line 98: target="_blank"
target="_blank"
// Line 99: rel="noopener noreferrer"
rel="noopener noreferrer"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="card-premium group flex flex-col items-center gap-4 bg-brand-900/50 p-8 text-center transition-all hover:bg-brand-900/70"
// Line 101: >
>
// JSX element — HTML-like tag becomes React component in browser
<Logo size="lg" className="ring-gold-400/30" />
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="font-display text-xl font-bold">{t('site.name')}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-1 text-sm text-brand-200">{SITE_ADDRESS}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<span className="inline-flex items-center gap-2 text-sm font-semibold text-gold-400 transition-colors group-hover:text-gold-300">
// Line 108: {t('contact.openMaps')}
{t('contact.openMaps')}
// JSX element — HTML-like tag becomes React component in browser
<ExternalLink className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 116: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,89 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/FeaturedProducts.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Import project module (@/ alias = src/ folder in tsconfig)
import ProductCard from '@/components/product/ProductCard';
// Import project module (@/ alias = src/ folder in tsconfig)
import { getFeaturedProducts } from '@/lib/products';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Lucide icons — lightweight SVG icon components
import { ArrowRight } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { localizeProduct } from '@/lib/product-i18n';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function FeaturedProducts() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// Line 13: const featured = getFeaturedProducts()
const featured = getFeaturedProducts()
// Line 14: .slice(0, 4)
.slice(0, 4)
// Array.map — transform each item (often render a list of components)
.map((p) => localizeProduct(p, t));
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="py-20">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-12 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-end">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
// Line 23: {t('featured.label')}
{t('featured.label')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading">{t('featured.title')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-2 max-w-lg text-gray-500">{t('featured.subtitle')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="secondary">
// Line 30: {t('featured.viewAll')}
{t('featured.viewAll')}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
// Array.map — transform each item (often render a list of components)
{featured.map((product) => (
// JSX element — HTML-like tag becomes React component in browser
<ProductCard key={product.id} product={product} />
// Line 39: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 43: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,184 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/Hero.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import Logo from '@/components/ui/Logo';
// Lucide icons — lightweight SVG icon components
import { ArrowRight, ShieldCheck, MapPin, Clock } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { IMAGES } from '@/lib/images';
// Import project module (@/ alias = src/ folder in tsconfig)
import { SITE_LOCATION, SITE_HOURS, SITE_ADDRESS, WHATSAPP_URL } from '@/lib/constants';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function Hero() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="relative min-h-[85vh] overflow-hidden bg-brand-950">
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute inset-0">
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 19: src={IMAGES.hero}
src={IMAGES.hero}
// Line 20: alt=""
alt=""
// Line 21: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover"
// Line 23: priority
priority
// Line 24: sizes="100vw"
sizes="100vw"
// Line 25: placeholder="blur"
placeholder="blur"
// Line 26: blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAA...
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUH/8QAIhAAAgEDBQAAAAAAAAAAAAAAAQIDAAQRBQYhIjFB/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAZEQACAwEAAAAAAAAAAAAAAAAAAQIRITH/2gAMAwEAAhEDEEA/ALextba0t0t7eJI4kHVEU4AqT/9k="
// Line 27: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute inset-0 bg-gradient-to-r from-brand-950/95 via-brand-900/85 to-brand-900/40" />
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/60 via-transparent to-transparent" />
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="relative mx-auto flex min-h-[85vh] max-w-7xl flex-col justify-center px-4 py-20 sm:px-6 lg:px-8 lg:py-28">
// JSX element — HTML-like tag becomes React component in browser
<div className="max-w-2xl animate-slide-up">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-8 flex items-center gap-4">
// JSX element — HTML-like tag becomes React component in browser
<Logo size="lg" className="ring-gold-400/40 shadow-premium-lg" />
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm font-medium text-gold-400">{SITE_LOCATION}</p>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-brand-200">{t('hero.taglineShort')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-gold-500/30 bg-brand-800/50 px-4 py-1.5 backdrop-blur-sm">
// JSX element — HTML-like tag becomes React component in browser
<ShieldCheck className="h-4 w-4 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xs font-semibold uppercase tracking-wider text-gold-300">
// Line 45: {t('hero.badge')}
{t('hero.badge')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<h1 className="mb-4 font-display text-4xl font-bold leading-tight text-white sm:text-5xl lg:text-6xl">
// Line 50: {t('hero.title')}
{t('hero.title')}
// JSX element — HTML-like tag becomes React component in browser
</h1>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-2 text-xl font-medium text-gold-300 sm:text-2xl">
// Line 54: {t('hero.subtitleShort')}
{t('hero.subtitleShort')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-8 text-base leading-relaxed text-brand-100 sm:text-lg">
// Line 58: {t('hero.subtitle')}
{t('hero.subtitle')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-8 flex flex-wrap gap-4 text-sm text-brand-200">
// JSX element — HTML-like tag becomes React component in browser
<span className="flex items-center gap-1.5">
// JSX element — HTML-like tag becomes React component in browser
<Clock className="h-4 w-4 text-gold-400" />
// Line 64: {t('hero.hours', { hours: SITE_HOURS })}
{t('hero.hours', { hours: SITE_HOURS })}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="flex items-center gap-1.5">
// JSX element — HTML-like tag becomes React component in browser
<MapPin className="h-4 w-4 text-gold-400" />
// Line 68: {SITE_ADDRESS}
{SITE_ADDRESS}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col gap-4 sm:flex-row">
// JSX element — HTML-like tag becomes React component in browser
<Link href="/shop">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="gold" size="lg">
// Line 75: {t('hero.shopNow')}
{t('hero.shopNow')}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
// JSX element — HTML-like tag becomes React component in browser
<Button
// Line 81: variant="secondary"
variant="secondary"
// Line 82: size="lg"
size="lg"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15"
// Line 84: >
>
// Line 85: {t('hero.whatsapp')}
{t('hero.whatsapp')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-cream-50 to-transparent" />
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 94: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,139 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/HowItWorks.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Lucide icons — lightweight SVG icon components
import { MessageCircle, ClipboardCheck, Store } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import Button from '@/components/ui/Button';
// Import project module (@/ alias = src/ folder in tsconfig)
import { WHATSAPP_URL } from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function HowItWorks() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Line 11: const steps = [
const steps = [
// Line 12: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: MessageCircle,
// Property or array item — trailing comma allowed in TypeScript
step: '01',
// Property or array item — trailing comma allowed in TypeScript
title: t('howItWorks.step1Title'),
// Property or array item — trailing comma allowed in TypeScript
description: t('howItWorks.step1Desc'),
// Closing brace — end of block (function, if, object, JSX)
},
// Line 18: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: ClipboardCheck,
// Property or array item — trailing comma allowed in TypeScript
step: '02',
// Property or array item — trailing comma allowed in TypeScript
title: t('howItWorks.step2Title'),
// Property or array item — trailing comma allowed in TypeScript
description: t('howItWorks.step2Desc'),
// Closing brace — end of block (function, if, object, JSX)
},
// Line 24: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: Store,
// Property or array item — trailing comma allowed in TypeScript
step: '03',
// Property or array item — trailing comma allowed in TypeScript
title: t('howItWorks.step3Title'),
// Property or array item — trailing comma allowed in TypeScript
description: t('howItWorks.step3Desc'),
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="bg-brand-950 py-20 text-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-12 text-center">
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-400">
// Line 37: {t('howItWorks.label')}
{t('howItWorks.label')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="font-display text-3xl font-bold md:text-4xl">
// Line 40: {t('howItWorks.title')}
{t('howItWorks.title')}
// JSX element — HTML-like tag becomes React component in browser
</h2>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-12 grid gap-8 md:grid-cols-3">
// Array.map — transform each item (often render a list of components)
{steps.map((step) => (
// JSX element — HTML-like tag becomes React component in browser
<div key={step.step} className="relative text-center">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-brand-800">
// JSX element — HTML-like tag becomes React component in browser
<step.icon className="h-7 w-7 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-xs font-bold uppercase tracking-widest text-gold-500">
// Line 51: {t('howItWorks.step', { n: step.step })}
{t('howItWorks.step', { n: step.step })}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-2 font-display text-xl font-bold">{step.title}</h3>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm leading-relaxed text-brand-200">{step.description}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 56: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="text-center">
// JSX element — HTML-like tag becomes React component in browser
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
// JSX element — HTML-like tag becomes React component in browser
<Button variant="gold" size="lg">
// JSX element — HTML-like tag becomes React component in browser
<MessageCircle className="h-4 w-4" />
// Line 63: {t('howItWorks.whatsapp')}
{t('howItWorks.whatsapp')}
// JSX element — HTML-like tag becomes React component in browser
</Button>
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 69: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,132 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/SocialFollow.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Lucide icons — lightweight SVG icon components
import { Facebook, Instagram, MessageCircle } from 'lucide-react';
// Import external package or local module
import {
// Property or array item — trailing comma allowed in TypeScript
FACEBOOK_URL,
// Property or array item — trailing comma allowed in TypeScript
INSTAGRAM_URL,
// Property or array item — trailing comma allowed in TypeScript
WHATSAPP_URL,
// Closing brace — end of block (function, if, object, JSX)
} from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Line 11: const socialLinks = [
const socialLinks = [
// Line 12: {
{
// Property or array item — trailing comma allowed in TypeScript
key: 'facebook',
// Property or array item — trailing comma allowed in TypeScript
href: FACEBOOK_URL,
// Property or array item — trailing comma allowed in TypeScript
icon: Facebook,
// Line 16: className:
className:
// Property or array item — trailing comma allowed in TypeScript
'border-blue-600/20 bg-blue-600/10 text-blue-700 hover:border-blue-600/40 hover:bg-blue-600/15',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 19: {
{
// Property or array item — trailing comma allowed in TypeScript
key: 'instagram',
// Property or array item — trailing comma allowed in TypeScript
href: INSTAGRAM_URL,
// Property or array item — trailing comma allowed in TypeScript
icon: Instagram,
// Line 23: className:
className:
// Property or array item — trailing comma allowed in TypeScript
'border-pink-600/20 bg-pink-600/10 text-pink-700 hover:border-pink-600/40 hover:bg-pink-600/15',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 26: {
{
// Property or array item — trailing comma allowed in TypeScript
key: 'whatsapp',
// Property or array item — trailing comma allowed in TypeScript
href: WHATSAPP_URL,
// Property or array item — trailing comma allowed in TypeScript
icon: MessageCircle,
// Line 30: className:
className:
// Property or array item — trailing comma allowed in TypeScript
'border-green-600/20 bg-green-600/10 text-green-700 hover:border-green-600/40 hover:bg-green-600/15',
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
] as const;
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function SocialFollow() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="py-20">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="rounded-3xl border border-cream-300/80 bg-white px-8 py-12 text-center shadow-premium sm:px-16">
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
// Line 43: {t('social.label')}
{t('social.label')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-3">{t('social.title')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mx-auto mb-8 max-w-xl text-gray-500">{t('social.subtitle')}</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
// Array.map — transform each item (often render a list of components)
{socialLinks.map(({ key, href, icon: Icon, className }) => (
// JSX element — HTML-like tag becomes React component in browser
<a
// Line 51: key={key}
key={key}
// Link target URL — internal route or external https://
href={href}
// Line 53: target="_blank"
target="_blank"
// Line 54: rel="noopener noreferrer"
rel="noopener noreferrer"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={`inline-flex items-center gap-2 rounded-xl border-2 px-6 py-3 text-sm font-semibold transition-all ${className}`}
// Line 56: >
>
// JSX element — HTML-like tag becomes React component in browser
<Icon className="h-5 w-5" />
// Line 58: {t(`social.${key}`)}
{t(`social.${key}`)}
// JSX element — HTML-like tag becomes React component in browser
</a>
// Line 60: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 65: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,109 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/TrustBadges.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { ShieldCheck, Leaf, Award } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function TrustBadges() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Line 10: const badges = [
const badges = [
// Line 11: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: ShieldCheck,
// Property or array item — trailing comma allowed in TypeScript
title: t('trust.halal'),
// Property or array item — trailing comma allowed in TypeScript
description: t('trust.halalDesc'),
// Property or array item — trailing comma allowed in TypeScript
href: '/about#halal',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 17: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: Leaf,
// Property or array item — trailing comma allowed in TypeScript
title: t('trust.fresh'),
// Property or array item — trailing comma allowed in TypeScript
description: t('trust.freshDesc'),
// Property or array item — trailing comma allowed in TypeScript
href: '/about#delivery',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 23: {
{
// Property or array item — trailing comma allowed in TypeScript
icon: Award,
// Property or array item — trailing comma allowed in TypeScript
title: t('trust.premium'),
// Property or array item — trailing comma allowed in TypeScript
description: t('trust.premiumDesc'),
// Property or array item — trailing comma allowed in TypeScript
href: '/about',
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="bg-cream-100 py-16">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-8 md:grid-cols-3">
// Array.map — transform each item (often render a list of components)
{badges.map((badge) => (
// JSX element — HTML-like tag becomes React component in browser
<Link
// Line 37: key={badge.title}
key={badge.title}
// Link target URL — internal route or external https://
href={badge.href}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="group rounded-2xl border border-cream-300/80 bg-white p-8 text-center shadow-premium transition-all duration-300 hover:border-brand-200 hover:shadow-premium-lg"
// Line 40: >
>
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand-50 transition-colors group-hover:bg-gold-50">
// JSX element — HTML-like tag becomes React component in browser
<badge.icon className="h-7 w-7 text-brand-700 transition-colors group-hover:text-gold-600" />
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-2 font-display text-xl font-bold text-brand-900">
// Line 45: {badge.title}
{badge.title}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm leading-relaxed text-gray-500">{badge.description}</p>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// Line 49: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 53: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,255 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/home/WeeklyOffers.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Lucide icons — lightweight SVG icon components
import { MessageCircle, ArrowRight } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { weeklyOffers } from '@/lib/offers';
// Import project module (@/ alias = src/ folder in tsconfig)
import { IMAGES } from '@/lib/images';
// Import project module (@/ alias = src/ folder in tsconfig)
import { whatsappOrderUrl } from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { formatPrice, getFormatLocale } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function WeeklyOffers() {
// Custom hook — returns t() translator and current locale
const { t, locale } = useTranslation();
// Line 14: const fmt = (n: number) => formatPrice(n, getFormatLocale...
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<section className="bg-cream-100 py-20">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
// Line 22: {t('offers.label')}
{t('offers.label')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<h2 className="section-heading mb-3">{t('offers.title')}</h2>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-8 text-gray-500">{t('offers.subtitle')}</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-4">
// Array.map — transform each item (often render a list of components)
{weeklyOffers.map((offer) => (
// JSX element — HTML-like tag becomes React component in browser
<div
// Line 30: key={offer.id}
key={offer.id}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="group flex gap-4 rounded-2xl border border-cream-300/80 bg-white p-4 shadow-premium transition-all duration-300 hover:border-brand-200 hover:shadow-premium-lg sm:p-5"
// Line 32: >
>
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={`/product/${offer.productSlug}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="relative h-20 w-20 shrink-0 overflow-hidden rounded-xl sm:h-24 sm:w-24"
// Line 36: >
>
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 38: src={offer.image}
src={offer.image}
// Line 39: alt={t(`${offer.nameKey}.name`)}
alt={t(`${offer.nameKey}.name`)}
// Line 40: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover transition-transform duration-500 group-hover:scale-105"
// Line 42: sizes="(max-width: 640px) 96px, 128px"
sizes="(max-width: 640px) 96px, 128px"
// Line 43: />
/>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="min-w-0 flex-1">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-1 flex flex-wrap items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<span
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={
// Line 50: offer.badgeKey === 'halal'
offer.badgeKey === 'halal'
// Line 51: ? 'rounded-full bg-brand-700 px-2.5 py-0.5 text-[10px] fo...
? 'rounded-full bg-brand-700 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white'
// Line 52: : 'rounded-full bg-gold-500 px-2.5 py-0.5 text-[10px] fon...
: 'rounded-full bg-gold-500 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white'
// Closing brace — end of block (function, if, object, JSX)
}
// Line 54: >
>
// Line 55: {t(`offers.badge.${offer.badgeKey}`)}
{t(`offers.badge.${offer.badgeKey}`)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link href={`/product/${offer.productSlug}`}>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-2 font-display text-lg font-bold text-brand-900 transition-colors hover:text-brand-700">
// Line 61: {t(`${offer.nameKey}.name`)}
{t(`${offer.nameKey}.name`)}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-wrap items-end justify-between gap-3">
// JSX element — HTML-like tag becomes React component in browser
<div>
// Line 67: {offer.originalPrice != null && (
{offer.originalPrice != null && (
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm text-gray-400 line-through">
// Line 69: {t('offers.was')}: {fmt(offer.originalPrice)}/
{t('offers.was')}: {fmt(offer.originalPrice)}/
// Line 70: {t(`priceUnit.${offer.priceUnitKey}`)}
{t(`priceUnit.${offer.priceUnitKey}`)}
// JSX element — HTML-like tag becomes React component in browser
</p>
// Line 72: )}
)}
// JSX element — HTML-like tag becomes React component in browser
<p className="font-display text-xl font-bold text-brand-800">
// Line 74: {offer.originalPrice != null && (
{offer.originalPrice != null && (
// JSX element — HTML-like tag becomes React component in browser
<span className="me-2 text-sm font-semibold uppercase text-gold-600">
// Line 76: {t('offers.now')}
{t('offers.now')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 78: )}
)}
// Line 79: {fmt(offer.price)}
{fmt(offer.price)}
// JSX element — HTML-like tag becomes React component in browser
<span className="text-sm font-normal text-gray-500">
// Line 81: /{t(`priceUnit.${offer.priceUnitKey}`)}
/{t(`priceUnit.${offer.priceUnitKey}`)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-wrap gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={`/product/${offer.productSlug}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="inline-flex items-center gap-1 rounded-lg border border-brand-200 px-3 py-2 text-xs font-semibold text-brand-700 transition-colors hover:bg-brand-50"
// Line 90: >
>
// Line 91: {t('offers.viewProduct')}
{t('offers.viewProduct')}
// JSX element — HTML-like tag becomes React component in browser
<ArrowRight className="h-3 w-3 rtl:rotate-180" />
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<a
// Link target URL — internal route or external https://
href={whatsappOrderUrl(offer.whatsappProduct)}
// Line 96: target="_blank"
target="_blank"
// Line 97: rel="noopener noreferrer"
rel="noopener noreferrer"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="inline-flex items-center gap-1.5 rounded-lg bg-brand-700 px-4 py-2 text-xs font-semibold text-white transition-colors hover:bg-brand-800"
// Line 99: >
>
// JSX element — HTML-like tag becomes React component in browser
<MessageCircle className="h-3.5 w-3.5" />
// Line 101: {t('offers.order')}
{t('offers.order')}
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 107: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-6 text-xs text-gray-400">{t('offers.disclaimer')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/shop"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="relative hidden aspect-[4/5] overflow-hidden rounded-2xl shadow-premium-lg lg:block"
// Line 116: >
>
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 118: src={IMAGES.hero}
src={IMAGES.hero}
// Line 119: alt=""
alt=""
// Line 120: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover transition-transform duration-500 hover:scale-105"
// Line 122: sizes="(max-width: 1024px) 0px, 640px"
sizes="(max-width: 1024px) 0px, 640px"
// Line 123: />
/>
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/50 via-transparent to-transparent" />
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</section>
// Line 129: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,355 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/layout/Footer.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { ShieldCheck, Leaf, Award, Phone, Mail, MapPin, Facebook, Instagram, MessageCircle } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import Logo from '@/components/ui/Logo';
// Import external package or local module
import {
// Property or array item — trailing comma allowed in TypeScript
SITE_PHONE_DISPLAY,
// Property or array item — trailing comma allowed in TypeScript
SITE_ADDRESS,
// Property or array item — trailing comma allowed in TypeScript
SITE_HOURS,
// Property or array item — trailing comma allowed in TypeScript
SITE_EMAIL,
// Property or array item — trailing comma allowed in TypeScript
FACEBOOK_URL,
// Property or array item — trailing comma allowed in TypeScript
INSTAGRAM_URL,
// Property or array item — trailing comma allowed in TypeScript
WHATSAPP_URL,
// Closing brace — end of block (function, if, object, JSX)
} from '@/lib/constants';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function Footer() {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Line 20: const footerLinks = {
const footerLinks = {
// Line 21: shop: [
shop: [
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.chicken'), href: '/shop?category=chicken' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.beef'), href: '/shop?category=beef' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.lamb'), href: '/shop?category=lamb' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.fish'), href: '/shop?category=fish' },
// End of array literal
],
// Line 27: company: [
company: [
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.about'), href: '/about' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.halalCert'), href: '/about#halal' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.delivery'), href: '/about#delivery' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.contact'), href: '/about#contact' },
// End of array literal
],
// Line 33: account: [
account: [
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.myAccount'), href: '/account' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.orderHistory'), href: '/account#orders' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.wishlist'), href: '/wishlist' },
// Property or array item — trailing comma allowed in TypeScript
{ label: t('nav.cart'), href: '/cart' },
// End of array literal
],
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<footer className="border-t border-gray-100 bg-brand-950 text-white">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="grid gap-12 md:grid-cols-2 lg:grid-cols-4">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/" className="mb-4 flex items-center gap-3">
// JSX element — HTML-like tag becomes React component in browser
<Logo size="sm" className="ring-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<span className="font-display text-xl font-bold">{t('site.name')}</span>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-6 text-sm leading-relaxed text-brand-200">
// Line 51: {t('footer.tagline')}
{t('footer.tagline')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-6 flex flex-wrap gap-4">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-1.5 text-xs text-gold-400">
// JSX element — HTML-like tag becomes React component in browser
<ShieldCheck className="h-4 w-4" />
// Line 56: {t('trust.halal')}
{t('trust.halal')}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-1.5 text-xs text-gold-400">
// JSX element — HTML-like tag becomes React component in browser
<Leaf className="h-4 w-4" />
// Line 60: {t('trust.fresh')}
{t('trust.fresh')}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-1.5 text-xs text-gold-400">
// JSX element — HTML-like tag becomes React component in browser
<Award className="h-4 w-4" />
// Line 64: {t('trust.premium')}
{t('trust.premium')}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex gap-3">
// JSX element — HTML-like tag becomes React component in browser
<a
// Link target URL — internal route or external https://
href={FACEBOOK_URL}
// Line 70: target="_blank"
target="_blank"
// Line 71: rel="noopener noreferrer"
rel="noopener noreferrer"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('social.facebook')}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg bg-brand-800 p-2.5 text-brand-200 transition-colors hover:bg-brand-700 hover:text-white"
// Line 74: >
>
// JSX element — HTML-like tag becomes React component in browser
<Facebook className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
<a
// Link target URL — internal route or external https://
href={INSTAGRAM_URL}
// Line 79: target="_blank"
target="_blank"
// Line 80: rel="noopener noreferrer"
rel="noopener noreferrer"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('social.instagram')}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg bg-brand-800 p-2.5 text-brand-200 transition-colors hover:bg-brand-700 hover:text-white"
// Line 83: >
>
// JSX element — HTML-like tag becomes React component in browser
<Instagram className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
<a
// Link target URL — internal route or external https://
href={WHATSAPP_URL}
// Line 88: target="_blank"
target="_blank"
// Line 89: rel="noopener noreferrer"
rel="noopener noreferrer"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('social.whatsapp')}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg bg-brand-800 p-2.5 text-brand-200 transition-colors hover:bg-brand-700 hover:text-white"
// Line 92: >
>
// JSX element — HTML-like tag becomes React component in browser
<MessageCircle className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gold-400">
// Line 100: {t('footer.shop')}
{t('footer.shop')}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
<ul className="space-y-2">
// Array.map — transform each item (often render a list of components)
{footerLinks.shop.map((link) => (
// JSX element — HTML-like tag becomes React component in browser
<li key={link.href}>
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={link.href}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="text-sm text-brand-200 transition-colors hover:text-white"
// Line 108: >
>
// Line 109: {link.label}
{link.label}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</li>
// Line 112: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</ul>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gold-400">
// Line 118: {t('footer.company')}
{t('footer.company')}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
<ul className="space-y-2">
// Array.map — transform each item (often render a list of components)
{footerLinks.company.map((link) => (
// JSX element — HTML-like tag becomes React component in browser
<li key={link.href}>
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={link.href}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="text-sm text-brand-200 transition-colors hover:text-white"
// Line 126: >
>
// Line 127: {link.label}
{link.label}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</li>
// Line 130: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</ul>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gold-400">
// Line 136: {t('footer.contact')}
{t('footer.contact')}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
<ul className="space-y-3">
// JSX element — HTML-like tag becomes React component in browser
<li className="flex items-center gap-2 text-sm text-brand-200">
// JSX element — HTML-like tag becomes React component in browser
<Phone className="h-4 w-4 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<a href={`tel:${SITE_PHONE_DISPLAY.replace(/\s/g, '')}`} className="hover:text-white">
// Line 142: {SITE_PHONE_DISPLAY}
{SITE_PHONE_DISPLAY}
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</li>
// JSX element — HTML-like tag becomes React component in browser
<li className="flex items-center gap-2 text-sm text-brand-200">
// JSX element — HTML-like tag becomes React component in browser
<Mail className="h-4 w-4 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<a href={`mailto:${SITE_EMAIL}`} className="hover:text-white">
// Line 148: {SITE_EMAIL}
{SITE_EMAIL}
// JSX element — HTML-like tag becomes React component in browser
</a>
// JSX element — HTML-like tag becomes React component in browser
</li>
// JSX element — HTML-like tag becomes React component in browser
<li className="flex items-start gap-2 text-sm text-brand-200">
// JSX element — HTML-like tag becomes React component in browser
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-gold-400" />
// JSX element — HTML-like tag becomes React component in browser
<span>
// Line 154: {SITE_ADDRESS}
{SITE_ADDRESS}
// JSX element — HTML-like tag becomes React component in browser
<br />
// Line 156: {t('footer.hours', { hours: SITE_HOURS })}
{t('footer.hours', { hours: SITE_HOURS })}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</li>
// JSX element — HTML-like tag becomes React component in browser
</ul>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-brand-800 pt-8 sm:flex-row">
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-brand-300">
// Line 165: &copy; {new Date().getFullYear()} {t('site.name')}. {t('f...
&copy; {new Date().getFullYear()} {t('site.name')}. {t('footer.rights')}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex gap-6 text-xs text-brand-300">
// JSX element — HTML-like tag becomes React component in browser
<Link href="/about#privacy" className="hover:text-white">
// Line 169: {t('nav.privacy')}
{t('nav.privacy')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
<Link href="/about#terms" className="hover:text-white">
// Line 172: {t('nav.terms')}
{t('nav.terms')}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</footer>
// Line 178: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,254 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/layout/Header.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Import React — core UI library (components, hooks, JSX)
import { useState } from 'react';
// Lucide icons — lightweight SVG icon components
import { ShoppingCart, Heart, User, Menu, X, Search } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useCartStore } from '@/store/cart';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useWishlistStore } from '@/store/wishlist';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useAuthStore } from '@/store/auth';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import Logo from '@/components/ui/Logo';
// Import project module (@/ alias = src/ folder in tsconfig)
import { SITE_LOCATION } from '@/lib/constants';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function Header() {
// React useState — local component state that triggers re-render on change
const [mobileOpen, setMobileOpen] = useState(false);
// Zustand selector — subscribe to slice of global store
const cartCount = useCartStore((s) => s.getItemCount());
// Zustand selector — subscribe to slice of global store
const wishlistCount = useWishlistStore((s) => s.items.length);
// Zustand selector — subscribe to slice of global store
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Line 20: const navLinks = [
const navLinks = [
// Property or array item — trailing comma allowed in TypeScript
{ href: '/shop', label: t('nav.shop') },
// Property or array item — trailing comma allowed in TypeScript
{ href: '/shop?category=chicken', label: t('nav.chicken') },
// Property or array item — trailing comma allowed in TypeScript
{ href: '/shop?category=beef', label: t('nav.beef') },
// Property or array item — trailing comma allowed in TypeScript
{ href: '/shop?category=lamb', label: t('nav.lamb') },
// Property or array item — trailing comma allowed in TypeScript
{ href: '/shop?category=fish', label: t('nav.fish') },
// Property or array item — trailing comma allowed in TypeScript
{ href: '/about', label: t('nav.about') },
// Property or array item — trailing comma allowed in TypeScript
{ href: '/about#contact', label: t('nav.contact') },
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<header className="border-b border-cream-300/80 bg-cream-50/95 backdrop-blur-md">
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex h-16 items-center justify-between lg:h-20">
// JSX element — HTML-like tag becomes React component in browser
<Link href="/" className="flex items-center gap-3">
// JSX element — HTML-like tag becomes React component in browser
<Logo size="sm" />
// JSX element — HTML-like tag becomes React component in browser
<div className="hidden sm:block">
// JSX element — HTML-like tag becomes React component in browser
<span className="font-display text-xl font-bold text-brand-800">
// Line 38: {t('site.name')}
{t('site.name')}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-[10px] font-medium uppercase tracking-widest text-brand-600">
// Line 41: {SITE_LOCATION}
{SITE_LOCATION}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<nav className="hidden items-center gap-8 lg:flex">
// Array.map — transform each item (often render a list of components)
{navLinks.map((link) => (
// JSX element — HTML-like tag becomes React component in browser
<Link
// Line 49: key={link.href}
key={link.href}
// Link target URL — internal route or external https://
href={link.href}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="text-sm font-medium text-gray-600 transition-colors hover:text-brand-700"
// Line 52: >
>
// Line 53: {link.label}
{link.label}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// Line 55: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</nav>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-1 sm:gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/shop?q="
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="hidden rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700 sm:block"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.searchProducts')}
// Line 63: >
>
// JSX element — HTML-like tag becomes React component in browser
<Search className="h-5 w-5" />
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/wishlist"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="relative rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.wishlist')}
// Line 71: >
>
// JSX element — HTML-like tag becomes React component in browser
<Heart className="h-5 w-5" />
// Line 73: {wishlistCount > 0 && (
{wishlistCount > 0 && (
// JSX element — HTML-like tag becomes React component in browser
<span className="absolute -end-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-gold-500 text-[10px] font-bold text-white">
// Line 75: {wishlistCount}
{wishlistCount}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 77: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href="/cart"
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="relative rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.cart')}
// Line 84: >
>
// JSX element — HTML-like tag becomes React component in browser
<ShoppingCart className="h-5 w-5" />
// Line 86: {cartCount > 0 && (
{cartCount > 0 && (
// JSX element — HTML-like tag becomes React component in browser
<span className="absolute -end-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-brand-700 text-[10px] font-bold text-white">
// Line 88: {cartCount}
{cartCount}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 90: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={isAuthenticated ? '/account' : '/login'}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.account')}
// Line 97: >
>
// JSX element — HTML-like tag becomes React component in browser
<User className="h-5 w-5" />
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<button
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 lg:hidden"
// Click handler — runs when user clicks (must be client component)
onClick={() => setMobileOpen(!mobileOpen)}
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.toggleMenu')}
// Line 105: >
>
// Line 106: {mobileOpen ? <X className="h-5 w-5" /> : <Menu className...
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// Line 112: {mobileOpen && (
{mobileOpen && (
// JSX element — HTML-like tag becomes React component in browser
<div className="border-t border-gray-100 bg-white lg:hidden">
// JSX element — HTML-like tag becomes React component in browser
<nav className="flex flex-col px-4 py-4">
// Array.map — transform each item (often render a list of components)
{navLinks.map((link) => (
// JSX element — HTML-like tag becomes React component in browser
<Link
// Line 117: key={link.href}
key={link.href}
// Link target URL — internal route or external https://
href={link.href}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="rounded-lg px-4 py-3 text-sm font-medium text-gray-600 transition-colors hover:bg-brand-50 hover:text-brand-700"
// Click handler — runs when user clicks (must be client component)
onClick={() => setMobileOpen(false)}
// Line 121: >
>
// Line 122: {link.label}
{link.label}
// JSX element — HTML-like tag becomes React component in browser
</Link>
// Line 124: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</nav>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 127: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</header>
// Line 129: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,129 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/layout/LanguageBanner.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Lucide icons — lightweight SVG icon components
import { Globe } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { LOCALES, Locale } from '@/i18n/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useLocaleStore } from '@/store/locale';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function LanguageBanner() {
// Zustand selector — subscribe to slice of global store
const { locale, setLocale } = useLocaleStore();
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="border-b border-brand-800 bg-brand-950 text-white"
// Line 16: role="navigation"
role="navigation"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.language')}
// Line 18: >
>
// JSX element — HTML-like tag becomes React component in browser
<div className="mx-auto flex max-w-7xl flex-col items-center gap-2 px-4 py-2 sm:flex-row sm:justify-between sm:px-6 lg:px-8">
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center gap-2 text-xs font-medium text-brand-200">
// JSX element — HTML-like tag becomes React component in browser
<Globe className="h-3.5 w-3.5 text-gold-400" aria-hidden />
// JSX element — HTML-like tag becomes React component in browser
<span>{t('languageBanner.choose')}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex w-full max-w-md items-center justify-center gap-1 rounded-full bg-brand-900 p-1 ring-1 ring-brand-800 sm:w-auto sm:max-w-none"
// Line 27: role="group"
role="group"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.language')}
// Line 29: >
>
// Array.map — transform each item (often render a list of components)
{LOCALES.map((lang) => {
// Line 31: const active = locale === lang.code;
const active = locale === lang.code;
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 34: key={lang.code}
key={lang.code}
// Line 35: type="button"
type="button"
// Click handler — runs when user clicks (must be client component)
onClick={() => setLocale(lang.code as Locale)}
// Accessibility attribute — screen readers and assistive tech
aria-pressed={active}
// Accessibility attribute — screen readers and assistive tech
aria-label={`${lang.label} (${lang.nativeLabel})`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'flex min-w-0 flex-1 items-center justify-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold transition-all sm:flex-initial sm:px-4 sm:py-2 sm:text-sm',
// Line 41: active
active
// Line 42: ? 'bg-gold-500 text-brand-950 shadow-gold'
? 'bg-gold-500 text-brand-950 shadow-gold'
// Line 43: : 'text-brand-200 hover:bg-brand-800 hover:text-white'
: 'text-brand-200 hover:bg-brand-800 hover:text-white'
// Line 44: )}
)}
// Line 45: >
>
// JSX element — HTML-like tag becomes React component in browser
<span className={lang.code === 'ur' ? 'font-[family-name:var(--font-urdu)]' : ''}>
// Line 47: {lang.nativeLabel}
{lang.nativeLabel}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<span
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'hidden text-[10px] font-normal uppercase tracking-wide sm:inline',
// Line 52: active ? 'text-brand-900/70' : 'text-brand-400'
active ? 'text-brand-900/70' : 'text-brand-400'
// Line 53: )}
)}
// Line 54: >
>
// Line 55: {lang.label}
{lang.label}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 58: );
);
// Closing brace — end of block (function, if, object, JSX)
})}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 63: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,135 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/layout/LanguageSwitcher.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useState, useRef, useEffect } from 'react';
// Lucide icons — lightweight SVG icon components
import { Globe, Check } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { LOCALES, Locale } from '@/i18n/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useLocaleStore } from '@/store/locale';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function LanguageSwitcher() {
// React useState — local component state that triggers re-render on change
const [open, setOpen] = useState(false);
// Line 12: const ref = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLDivElement>(null);
// Zustand selector — subscribe to slice of global store
const { locale, setLocale } = useLocaleStore();
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Array.find — get first matching item or undefined
const current = LOCALES.find((l) => l.code === locale);
// (blank line — separates logical blocks for readability)
// Side effect hook — runs after paint; deps array controls when it re-runs
useEffect(() => {
// Function declaration — reusable logic in this file
function handleClick(e: MouseEvent) {
// Conditional branch — different behavior based on runtime value
if (ref.current && !ref.current.contains(e.target as Node)) {
// Line 21: setOpen(false);
setOpen(false);
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
}
// Line 24: document.addEventListener('mousedown', handleClick);
document.addEventListener('mousedown', handleClick);
// Return JSX — describes UI tree React renders to the DOM
return () => document.removeEventListener('mousedown', handleClick);
// Closing brace — end of block (function, if, object, JSX)
}, []);
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div ref={ref} className="relative">
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => setOpen(!open)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex items-center gap-1.5 rounded-lg px-2 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-700"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('nav.language')}
// Line 34: >
>
// JSX element — HTML-like tag becomes React component in browser
<Globe className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
<span className="hidden sm:inline">{current?.nativeLabel}</span>
// JSX element — HTML-like tag becomes React component in browser
</button>
// (blank line — separates logical blocks for readability)
// Line 39: {open && (
{open && (
// JSX element — HTML-like tag becomes React component in browser
<div className="absolute end-0 top-full z-50 mt-1 min-w-[10rem] overflow-hidden rounded-xl border border-gray-100 bg-white py-1 shadow-premium-lg">
// Array.map — transform each item (often render a list of components)
{LOCALES.map((lang) => (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 43: key={lang.code}
key={lang.code}
// Click handler — runs when user clicks (must be client component)
onClick={() => {
// Line 45: setLocale(lang.code as Locale);
setLocale(lang.code as Locale);
// Line 46: setOpen(false);
setOpen(false);
// Closing brace — end of block (function, if, object, JSX)
}}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'flex w-full items-center justify-between gap-3 px-4 py-2.5 text-sm transition-colors hover:bg-brand-50',
// Line 50: locale === lang.code
locale === lang.code
// Line 51: ? 'font-semibold text-brand-700'
? 'font-semibold text-brand-700'
// Line 52: : 'text-gray-600'
: 'text-gray-600'
// Line 53: )}
)}
// Line 54: >
>
// JSX element — HTML-like tag becomes React component in browser
<span>
// Line 56: {lang.nativeLabel}
{lang.nativeLabel}
// JSX element — HTML-like tag becomes React component in browser
<span className="ms-2 text-xs text-gray-400">{lang.label}</span>
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 59: {locale === lang.code && (
{locale === lang.code && (
// JSX element — HTML-like tag becomes React component in browser
<Check className="h-4 w-4 shrink-0 text-brand-700" />
// Line 61: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 63: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 65: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 67: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,33 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/layout/LocaleAttributes.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useEffect } from 'react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useLocaleStore } from '@/store/locale';
// Import project module (@/ alias = src/ folder in tsconfig)
import { getHtmlLang, isRtl } from '@/i18n';
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function LocaleAttributes() {
// Zustand selector — subscribe to slice of global store
const locale = useLocaleStore((s) => s.locale);
// (blank line — separates logical blocks for readability)
// Side effect hook — runs after paint; deps array controls when it re-runs
useEffect(() => {
// Line 11: document.documentElement.lang = getHtmlLang(locale);
document.documentElement.lang = getHtmlLang(locale);
// Line 12: document.documentElement.dir = isRtl(locale) ? 'rtl' : 'l...
document.documentElement.dir = isRtl(locale) ? 'rtl' : 'ltr';
// Closing brace — end of block (function, if, object, JSX)
}, [locale]);
// (blank line — separates logical blocks for readability)
// Return value from function
return null;
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,202 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/product/CustomizationSelector.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import project module (@/ alias = src/ folder in tsconfig)
import { Category, ProductCustomization } from '@/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { CUT_COUNTS, CUTTING_STYLE_KEYS } from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { updateMeatCustomization } from '@/lib/customization';
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// Lucide icons — lightweight SVG icon components
import { Scissors, Hash } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface CustomizationSelectorProps {
// Line 11: category: Category;
category: Category;
// Line 12: customization: ProductCustomization;
customization: ProductCustomization;
// Line 13: onChange: (customization: ProductCustomization) => void;
onChange: (customization: ProductCustomization) => void;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function CustomizationSelector({
// Property or array item — trailing comma allowed in TypeScript
category,
// Property or array item — trailing comma allowed in TypeScript
customization,
// Property or array item — trailing comma allowed in TypeScript
onChange,
// Closing brace — end of block (function, if, object, JSX)
}: CustomizationSelectorProps) {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (category === 'fish') {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="rounded-xl border border-brand-100 bg-brand-50/50 p-4">
// JSX element — HTML-like tag becomes React component in browser
<p className="text-sm text-brand-700">{t('product.fishNote')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 28: );
);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 31: const meat = updateMeatCustomization(customization, categ...
const meat = updateMeatCustomization(customization, category, {});
// Line 32: const selectedCuts = meat.cuts;
const selectedCuts = meat.cuts;
// Line 33: const selectedStyle = meat.cuttingStyle;
const selectedStyle = meat.cuttingStyle;
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-6">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-3 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Hash className="h-4 w-4 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h3 className="text-sm font-semibold text-brand-900">
// Line 41: {t('product.howManyCuts')}
{t('product.howManyCuts')}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-wrap gap-2">
// Array.map — transform each item (often render a list of components)
{CUT_COUNTS.map((cuts) => (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 47: key={cuts}
key={cuts}
// Line 48: type="button"
type="button"
// Click handler — runs when user clicks (must be client component)
onClick={() =>
// Line 50: onChange(updateMeatCustomization(customization, category,...
onChange(updateMeatCustomization(customization, category, { cuts }))
// Closing brace — end of block (function, if, object, JSX)
}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'min-w-[3.5rem] rounded-lg border-2 px-4 py-2.5 text-sm font-semibold transition-all',
// Line 54: selectedCuts === cuts
selectedCuts === cuts
// Line 55: ? 'border-brand-700 bg-brand-700 text-white shadow-premium'
? 'border-brand-700 bg-brand-700 text-white shadow-premium'
// Line 56: : 'border-gray-200 bg-white text-gray-700 hover:border-br...
: 'border-gray-200 bg-white text-gray-700 hover:border-brand-300 hover:bg-brand-50'
// Line 57: )}
)}
// Line 58: >
>
// Line 59: {cuts}
{cuts}
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 61: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-2 text-xs text-gray-500">{t('product.howManyCutsHint')}</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-3 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<Scissors className="h-4 w-4 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h3 className="text-sm font-semibold text-brand-900">
// Line 70: {t('product.selectCutting')}
{t('product.selectCutting')}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
// Array.map — transform each item (often render a list of components)
{CUTTING_STYLE_KEYS.map((style) => (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 76: key={style}
key={style}
// Line 77: type="button"
type="button"
// Click handler — runs when user clicks (must be client component)
onClick={() =>
// Line 79: onChange(
onChange(
// Line 80: updateMeatCustomization(customization, category, { cuttin...
updateMeatCustomization(customization, category, { cuttingStyle: style })
// Line 81: )
)
// Closing brace — end of block (function, if, object, JSX)
}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'rounded-lg border-2 px-4 py-3 text-start text-sm font-medium transition-all',
// Line 85: selectedStyle === style
selectedStyle === style
// Line 86: ? 'border-brand-700 bg-brand-700 text-white shadow-premium'
? 'border-brand-700 bg-brand-700 text-white shadow-premium'
// Line 87: : 'border-gray-200 bg-white text-gray-700 hover:border-br...
: 'border-gray-200 bg-white text-gray-700 hover:border-brand-300 hover:bg-brand-50'
// Line 88: )}
)}
// Line 89: >
>
// Line 90: {t(`cutting.${style}`)}
{t(`cutting.${style}`)}
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 92: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<p className="mt-2 text-xs text-gray-500">
// Line 95: {t('product.selectCuttingHint', {
{t('product.selectCuttingHint', {
// Property or array item — trailing comma allowed in TypeScript
category: t(`categories.${category}.name`),
// Closing brace — end of block (function, if, object, JSX)
})}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 101: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,174 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/product/ProductCard.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Next.js Link — fast client-side navigation without full page reload
import Link from 'next/link';
// Lucide icons — lightweight SVG icon components
import { Heart, ShoppingCart } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { LocalizedProduct } from '@/lib/product-i18n';
// Import project module (@/ alias = src/ folder in tsconfig)
import { formatPrice, getFormatLocale } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useWishlistStore } from '@/store/wishlist';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// Import project module (@/ alias = src/ folder in tsconfig)
import { products } from '@/lib/products';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface ProductCardProps {
// Line 14: product: LocalizedProduct;
product: LocalizedProduct;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function ProductCard({ product }: ProductCardProps) {
// Zustand selector — subscribe to slice of global store
const { isInWishlist, toggleItem } = useWishlistStore();
// Custom hook — returns t() translator and current locale
const { t, locale } = useTranslation();
// Array.find — get first matching item or undefined
const rawProduct = products.find((p) => p.id === product.id)!;
// Line 21: const inWishlist = isInWishlist(product.id);
const inWishlist = isInWishlist(product.id);
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="card-premium group overflow-hidden">
// JSX element — HTML-like tag becomes React component in browser
<div className="relative aspect-[4/3] overflow-hidden bg-gray-50">
// JSX element — HTML-like tag becomes React component in browser
<Link href={`/product/${product.slug}`}>
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 28: src={product.image}
src={product.image}
// Line 29: alt={product.name}
alt={product.name}
// Line 30: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover transition-transform duration-500 group-hover:scale-105"
// Line 32: sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw...
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 320px"
// Line 33: />
/>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// Line 35: {product.badge && (
{product.badge && (
// JSX element — HTML-like tag becomes React component in browser
<span className="absolute start-3 top-3 rounded-full bg-gold-500 px-3 py-1 text-xs font-semibold text-white shadow-gold">
// Line 37: {product.badge}
{product.badge}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 39: )}
)}
// JSX element — HTML-like tag becomes React component in browser
<button
// Click handler — runs when user clicks (must be client component)
onClick={() => toggleItem(rawProduct)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'absolute end-3 top-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 shadow-sm backdrop-blur transition-all hover:scale-110',
// Line 44: inWishlist && 'text-red-500'
inWishlist && 'text-red-500'
// Line 45: )}
)}
// Accessibility attribute — screen readers and assistive tech
aria-label={inWishlist ? t('product.removeWishlist') : t('product.addWishlist')}
// Line 47: >
>
// JSX element — HTML-like tag becomes React component in browser
<Heart className={cn('h-4 w-4', inWishlist && 'fill-current')} />
// JSX element — HTML-like tag becomes React component in browser
</button>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="p-5">
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-1 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xs font-medium uppercase tracking-wider text-brand-600">
// Line 55: {t(`categories.${product.category}.name`)}
{t(`categories.${product.category}.name`)}
// JSX element — HTML-like tag becomes React component in browser
</span>
// Line 57: {product.weight && (
{product.weight && (
// JSX element — HTML-like tag becomes React component in browser
<span className="text-xs text-gray-400">· {product.weight}</span>
// Line 59: )}
)}
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link href={`/product/${product.slug}`}>
// JSX element — HTML-like tag becomes React component in browser
<h3 className="mb-1 font-display text-lg font-semibold text-brand-900 transition-colors group-hover:text-brand-700">
// Line 64: {product.name}
{product.name}
// JSX element — HTML-like tag becomes React component in browser
</h3>
// JSX element — HTML-like tag becomes React component in browser
</Link>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<p className="mb-4 line-clamp-2 text-sm text-gray-500">{product.description}</p>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div className="flex items-center justify-between">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<span className="text-lg font-bold text-brand-800">
// Line 73: {formatPrice(product.price, getFormatLocale(locale))}
{formatPrice(product.price, getFormatLocale(locale))}
// JSX element — HTML-like tag becomes React component in browser
</span>
// JSX element — HTML-like tag becomes React component in browser
<span className="ms-1 text-xs text-gray-400">{product.priceUnit}</span>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<Link
// Link target URL — internal route or external https://
href={`/product/${product.slug}`}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="flex h-10 w-10 items-center justify-center rounded-full bg-brand-700 text-white transition-all hover:bg-brand-800 hover:shadow-premium"
// Accessibility attribute — screen readers and assistive tech
aria-label={t('product.viewProduct', { name: product.name })}
// Line 82: >
>
// JSX element — HTML-like tag becomes React component in browser
<ShoppingCart className="h-4 w-4" />
// JSX element — HTML-like tag becomes React component in browser
</Link>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 88: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,214 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/shop/ShopFilters.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import project module (@/ alias = src/ folder in tsconfig)
import { Category, SortOption } from '@/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// Lucide icons — lightweight SVG icon components
import { SlidersHorizontal } from 'lucide-react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useTranslation } from '@/hooks/useTranslation';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface ShopFiltersProps {
// Line 9: selectedCategory: Category | 'all';
selectedCategory: Category | 'all';
// Line 10: onCategoryChange: (category: Category | 'all') => void;
onCategoryChange: (category: Category | 'all') => void;
// Line 11: sortBy: SortOption;
sortBy: SortOption;
// Line 12: onSortChange: (sort: SortOption) => void;
onSortChange: (sort: SortOption) => void;
// Line 13: searchQuery: string;
searchQuery: string;
// Line 14: onSearchChange: (query: string) => void;
onSearchChange: (query: string) => void;
// Line 15: totalResults: number;
totalResults: number;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function ShopFilters({
// Property or array item — trailing comma allowed in TypeScript
selectedCategory,
// Property or array item — trailing comma allowed in TypeScript
onCategoryChange,
// Property or array item — trailing comma allowed in TypeScript
sortBy,
// Property or array item — trailing comma allowed in TypeScript
onSortChange,
// Property or array item — trailing comma allowed in TypeScript
searchQuery,
// Property or array item — trailing comma allowed in TypeScript
onSearchChange,
// Property or array item — trailing comma allowed in TypeScript
totalResults,
// Closing brace — end of block (function, if, object, JSX)
}: ShopFiltersProps) {
// Custom hook — returns t() translator and current locale
const { t } = useTranslation();
// (blank line — separates logical blocks for readability)
// Line 29: const categories: { value: Category | 'all'; label: strin...
const categories: { value: Category | 'all'; label: string }[] = [
// Property or array item — trailing comma allowed in TypeScript
{ value: 'all', label: t('shop.all') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'chicken', label: t('nav.chicken') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'beef', label: t('nav.beef') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'lamb', label: t('nav.lamb') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'fish', label: t('nav.fish') },
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Line 37: const sortOptions: { value: SortOption; label: string }[]...
const sortOptions: { value: SortOption; label: string }[] = [
// Property or array item — trailing comma allowed in TypeScript
{ value: 'featured', label: t('shop.sortFeatured') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'price-asc', label: t('shop.sortPriceAsc') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'price-desc', label: t('shop.sortPriceDesc') },
// Property or array item — trailing comma allowed in TypeScript
{ value: 'name', label: t('shop.sortName') },
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div className="space-y-6">
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<div className="mb-3 flex items-center gap-2">
// JSX element — HTML-like tag becomes React component in browser
<SlidersHorizontal className="h-4 w-4 text-brand-700" />
// JSX element — HTML-like tag becomes React component in browser
<h2 className="text-sm font-semibold text-brand-900">{t('shop.filters')}</h2>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
<p className="text-xs text-gray-500">
// Line 52: {t('shop.productsFound', { count: totalResults })}
{t('shop.productsFound', { count: totalResults })}
// JSX element — HTML-like tag becomes React component in browser
</p>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label htmlFor="search" className="label-text">
// Line 58: {t('shop.search')}
{t('shop.search')}
// JSX element — HTML-like tag becomes React component in browser
</label>
// JSX element — HTML-like tag becomes React component in browser
<input
// Line 61: id="search"
id="search"
// Line 62: type="text"
type="text"
// Line 63: placeholder={t('shop.searchPlaceholder')}
placeholder={t('shop.searchPlaceholder')}
// Line 64: value={searchQuery}
value={searchQuery}
// Change handler — runs when input/select value changes
onChange={(e) => onSearchChange(e.target.value)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 67: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<p className="label-text">{t('shop.category')}</p>
// JSX element — HTML-like tag becomes React component in browser
<div className="flex flex-wrap gap-2">
// Array.map — transform each item (often render a list of components)
{categories.map((cat) => (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 75: key={cat.value}
key={cat.value}
// Click handler — runs when user clicks (must be client component)
onClick={() => onCategoryChange(cat.value)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'rounded-full px-4 py-2 text-sm font-medium transition-all',
// Line 79: selectedCategory === cat.value
selectedCategory === cat.value
// Line 80: ? 'bg-brand-700 text-white shadow-premium'
? 'bg-brand-700 text-white shadow-premium'
// Line 81: : 'bg-gray-100 text-gray-600 hover:bg-brand-50 hover:text...
: 'bg-gray-100 text-gray-600 hover:bg-brand-50 hover:text-brand-700'
// Line 82: )}
)}
// Line 83: >
>
// Line 84: {cat.label}
{cat.label}
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 86: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// (blank line — separates logical blocks for readability)
// JSX element — HTML-like tag becomes React component in browser
<div>
// JSX element — HTML-like tag becomes React component in browser
<label htmlFor="sort" className="label-text">
// Line 92: {t('shop.sortBy')}
{t('shop.sortBy')}
// JSX element — HTML-like tag becomes React component in browser
</label>
// JSX element — HTML-like tag becomes React component in browser
<select
// Line 95: id="sort"
id="sort"
// Line 96: value={sortBy}
value={sortBy}
// Change handler — runs when input/select value changes
onChange={(e) => onSortChange(e.target.value as SortOption)}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="input-field"
// Line 99: >
>
// Array.map — transform each item (often render a list of components)
{sortOptions.map((opt) => (
// JSX element — HTML-like tag becomes React component in browser
<option key={opt.value} value={opt.value}>
// Line 102: {opt.label}
{opt.label}
// JSX element — HTML-like tag becomes React component in browser
</option>
// Line 104: ))}
))}
// JSX element — HTML-like tag becomes React component in browser
</select>
// JSX element — HTML-like tag becomes React component in browser
</div>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 108: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,25 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/ui/AppImage.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Next.js Image — optimized images (lazy load, WebP/AVIF)
import Image, { ImageProps } from 'next/image';
// Import project module (@/ alias = src/ folder in tsconfig)
import { IMAGE_QUALITY } from '@/lib/images';
// (blank line — separates logical blocks for readability)
// TypeScript type alias — union or shorthand for complex types
type AppImageProps = ImageProps & {
// Line 5: quality?: number;
quality?: number;
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Block comment — documents the file or function below
/** Site-wide Image wrapper with consistent high quality defaults */
// Default export — main component/page Next.js or other files import
export default function AppImage({ quality = IMAGE_QUALITY, ...props }: AppImageProps) {
// Return value from function
return <Image quality={quality} {...props} />;
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,76 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/ui/Button.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// Import React — core UI library (components, hooks, JSX)
import { ButtonHTMLAttributes, forwardRef } from 'react';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
// Line 5: variant?: 'primary' | 'secondary' | 'gold' | 'ghost';
variant?: 'primary' | 'secondary' | 'gold' | 'ghost';
// Line 6: size?: 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg';
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 9: const Button = forwardRef<HTMLButtonElement, ButtonProps>(
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
// Line 10: ({ className, variant = 'primary', size = 'md', children,...
({ className, variant = 'primary', size = 'md', children, ...props }, ref) => {
// Line 11: const variants = {
const variants = {
// Property or array item — trailing comma allowed in TypeScript
primary: 'btn-primary',
// Property or array item — trailing comma allowed in TypeScript
secondary: 'btn-secondary',
// Property or array item — trailing comma allowed in TypeScript
gold: 'btn-gold',
// Line 15: ghost:
ghost:
// Property or array item — trailing comma allowed in TypeScript
'inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-brand-700',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Line 19: const sizes = {
const sizes = {
// Property or array item — trailing comma allowed in TypeScript
sm: 'px-4 py-2 text-xs',
// Property or array item — trailing comma allowed in TypeScript
md: '',
// Property or array item — trailing comma allowed in TypeScript
lg: 'px-8 py-4 text-base',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<button
// Line 27: ref={ref}
ref={ref}
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(variants[variant], sizes[size], className)}
// Line 29: {...props}
{...props}
// Line 30: >
>
// Line 31: {children}
{children}
// JSX element — HTML-like tag becomes React component in browser
</button>
// Line 33: );
);
// Closing brace — end of block (function, if, object, JSX)
}
// Line 35: );
);
// (blank line — separates logical blocks for readability)
// Line 37: Button.displayName = 'Button';
Button.displayName = 'Button';
// Line 38: export default Button;
export default Button;
@@ -1,74 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/components/ui/Logo.tsx
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import AppImage from '@/components/ui/AppImage';
// Import project module (@/ alias = src/ folder in tsconfig)
import { IMAGES } from '@/lib/images';
// Import project module (@/ alias = src/ folder in tsconfig)
import { cn } from '@/lib/utils';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface LogoProps {
// Line 6: size?: 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg';
// Line 7: className?: string;
className?: string;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 10: const sizes = {
const sizes = {
// Property or array item — trailing comma allowed in TypeScript
sm: 'h-10 w-10',
// Property or array item — trailing comma allowed in TypeScript
md: 'h-12 w-12',
// Property or array item — trailing comma allowed in TypeScript
lg: 'h-20 w-20',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Default export — main component/page Next.js or other files import
export default function Logo({ size = 'md', className }: LogoProps) {
// Return JSX — describes UI tree React renders to the DOM
return (
// JSX element — HTML-like tag becomes React component in browser
<div
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className={cn(
// Property or array item — trailing comma allowed in TypeScript
'relative shrink-0 overflow-hidden rounded-full ring-2 ring-cream-200',
// Property or array item — trailing comma allowed in TypeScript
sizes[size],
// Line 22: className
className
// Line 23: )}
)}
// Line 24: >
>
// JSX element — HTML-like tag becomes React component in browser
<AppImage
// Line 26: src={IMAGES.logo}
src={IMAGES.logo}
// Line 27: alt="Kött Gård"
alt="Kött Gård"
// Line 28: fill
fill
// Tailwind CSS utility classes — styling (colors, spacing, layout)
className="object-cover"
// Line 30: sizes="(max-width: 640px) 40px, 80px"
sizes="(max-width: 640px) 40px, 80px"
// Line 31: priority
priority
// Line 32: quality={95}
quality={95}
// Line 33: />
/>
// JSX element — HTML-like tag becomes React component in browser
</div>
// Line 35: );
);
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,27 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/hooks/useTranslation.ts
* NOT used by the app — read this to learn how the real file works
*/
// Next.js: this file runs in the BROWSER (needs useState, onClick, localStorage, etc.)
'use client';
// (blank line — separates logical blocks for readability)
// Import React — core UI library (components, hooks, JSX)
import { useMemo } from 'react';
// Import project module (@/ alias = src/ folder in tsconfig)
import { createTranslator } from '@/i18n';
// Import project module (@/ alias = src/ folder in tsconfig)
import { useLocaleStore } from '@/store/locale';
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function useTranslation() {
// Zustand selector — subscribe to slice of global store
const locale = useLocaleStore((s) => s.locale);
// (blank line — separates logical blocks for readability)
// React useMemo — cache expensive computed value until dependencies change
const t = useMemo(() => createTranslator(locale), [locale]);
// (blank line — separates logical blocks for readability)
// Return value from function
return { t, locale };
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,85 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/i18n/index.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import from a relative file in the same project
import { Locale, TranslationDict } from './types';
// Import from a relative file in the same project
import { en } from './locales/en';
// Import from a relative file in the same project
import { sv } from './locales/sv';
// Import from a relative file in the same project
import { ur } from './locales/ur';
// (blank line — separates logical blocks for readability)
// Line 6: const dictionaries: Record<Locale, TranslationDict> = { e...
const dictionaries: Record<Locale, TranslationDict> = { en, sv, ur };
// (blank line — separates logical blocks for readability)
// Function declaration — reusable logic in this file
function resolve(obj: TranslationDict, path: string): string {
// Line 9: const keys = path.split('.');
const keys = path.split('.');
// Line 10: let current: string | TranslationDict = obj;
let current: string | TranslationDict = obj;
// (blank line — separates logical blocks for readability)
// Line 12: for (const key of keys) {
for (const key of keys) {
// Conditional branch — different behavior based on runtime value
if (typeof current !== 'object' || current === null || !(key in current)) {
// Return value from function
return path;
// Closing brace — end of block (function, if, object, JSX)
}
// Line 16: current = current[key];
current = current[key];
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Return value from function
return typeof current === 'string' ? current : path;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function createTranslator(locale: Locale) {
// Line 23: const dict = dictionaries[locale] ?? dictionaries.en;
const dict = dictionaries[locale] ?? dictionaries.en;
// (blank line — separates logical blocks for readability)
// Return value from function
return function t(path: string, params?: Record<string, string | number>): string {
// Line 26: let text = resolve(dict, path);
let text = resolve(dict, path);
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (params) {
// Line 29: Object.entries(params).forEach(([key, value]) => {
Object.entries(params).forEach(([key, value]) => {
// Line 30: text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), Stri...
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value));
// Closing brace — end of block (function, if, object, JSX)
});
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Return value from function
return text;
// Closing brace — end of block (function, if, object, JSX)
};
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function isRtl(locale: Locale): boolean {
// Return value from function
return locale === 'ur';
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getHtmlLang(locale: Locale): string {
// Line 43: const map: Record<Locale, string> = { en: 'en', sv: 'sv',...
const map: Record<Locale, string> = { en: 'en', sv: 'sv', ur: 'ur' };
// Return value from function
return map[locale];
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,934 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/i18n/locales/en.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import external package or local module
import { TranslationDict } from '../types';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const en: TranslationDict = {
// Line 4: site: {
site: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Kött Gård',
// Property or array item — trailing comma allowed in TypeScript
tagline: 'Premium Halal',
// Line 7: description:
description:
// Property or array item — trailing comma allowed in TypeScript
'Premium 100% Halal meat delivery. Fresh and frozen chicken, beef, lamb, and fish — customized to your preference and delivered to your door.',
// Property or array item — trailing comma allowed in TypeScript
metaTitle: 'Premium Halal Meat Delivery',
// Property or array item — trailing comma allowed in TypeScript
initials: 'KG',
// Property or array item — trailing comma allowed in TypeScript
email: 'hello@kottgard.se',
// Property or array item — trailing comma allowed in TypeScript
demoEmail: 'demo@kottgard.se',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 14: nav: {
nav: {
// Property or array item — trailing comma allowed in TypeScript
shop: 'Shop',
// Property or array item — trailing comma allowed in TypeScript
chicken: 'Chicken',
// Property or array item — trailing comma allowed in TypeScript
beef: 'Beef',
// Property or array item — trailing comma allowed in TypeScript
lamb: 'Lamb',
// Property or array item — trailing comma allowed in TypeScript
fish: 'Fish',
// Property or array item — trailing comma allowed in TypeScript
about: 'About Us',
// Property or array item — trailing comma allowed in TypeScript
halalCert: 'Halal Certification',
// Property or array item — trailing comma allowed in TypeScript
delivery: 'Delivery Info',
// Property or array item — trailing comma allowed in TypeScript
contact: 'Contact',
// Property or array item — trailing comma allowed in TypeScript
myAccount: 'My Account',
// Property or array item — trailing comma allowed in TypeScript
orderHistory: 'Order History',
// Property or array item — trailing comma allowed in TypeScript
wishlist: 'Wishlist',
// Property or array item — trailing comma allowed in TypeScript
cart: 'Cart',
// Property or array item — trailing comma allowed in TypeScript
privacy: 'Privacy Policy',
// Property or array item — trailing comma allowed in TypeScript
terms: 'Terms of Service',
// Property or array item — trailing comma allowed in TypeScript
searchProducts: 'Search products',
// Property or array item — trailing comma allowed in TypeScript
toggleMenu: 'Toggle menu',
// Property or array item — trailing comma allowed in TypeScript
account: 'Account',
// Property or array item — trailing comma allowed in TypeScript
language: 'Language',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 35: languageBanner: {
languageBanner: {
// Property or array item — trailing comma allowed in TypeScript
choose: 'Choose your language',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 38: hero: {
hero: {
// Property or array item — trailing comma allowed in TypeScript
badge: '100% Halal Certified',
// Property or array item — trailing comma allowed in TypeScript
taglineShort: 'Naturally Pure',
// Property or array item — trailing comma allowed in TypeScript
title: 'Premium Halal Meat',
// Property or array item — trailing comma allowed in TypeScript
titleHighlight: '',
// Property or array item — trailing comma allowed in TypeScript
titleEnd: '',
// Property or array item — trailing comma allowed in TypeScript
subtitleShort: 'Fresh. Quality. Reliable.',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Halal certified · Fresh daily · Home delivery · Open every day',
// Property or array item — trailing comma allowed in TypeScript
hours: 'Open every day {hours}',
// Property or array item — trailing comma allowed in TypeScript
location: 'Tingvallavägen 11, Märsta',
// Property or array item — trailing comma allowed in TypeScript
shopNow: 'Browse Our Selection',
// Property or array item — trailing comma allowed in TypeScript
browseChicken: 'Browse Chicken',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'Order via WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 53: aboutPreview: {
aboutPreview: {
// Property or array item — trailing comma allowed in TypeScript
label: 'About Us',
// Property or array item — trailing comma allowed in TypeScript
title: "Märsta's Finest Butcher Shop",
// Property or array item — trailing comma allowed in TypeScript
p1: 'Kött Gård is more than a butcher shop — we are a promise of quality. All our meat is 100% Halal certified and delivered fresh every day.',
// Property or array item — trailing comma allowed in TypeScript
p2: 'We source lamb from Ireland and New Zealand, chicken and beef from reputable producers, and help you find the right cut for dinner, celebrations, or Sunday roast.',
// Property or array item — trailing comma allowed in TypeScript
p3: 'Visit us at Tingvallavägen, tell us what you are looking for — we cut and pack to your specifications.',
// Property or array item — trailing comma allowed in TypeScript
statHalal: '100%',
// Property or array item — trailing comma allowed in TypeScript
statHalalLabel: 'Halal certified',
// Property or array item — trailing comma allowed in TypeScript
statDays: '7 days',
// Property or array item — trailing comma allowed in TypeScript
statDaysLabel: 'Open weekly',
// Property or array item — trailing comma allowed in TypeScript
statDelivery: 'Daily',
// Property or array item — trailing comma allowed in TypeScript
statDeliveryLabel: 'Delivery',
// Property or array item — trailing comma allowed in TypeScript
statFresh: 'Fresh',
// Property or array item — trailing comma allowed in TypeScript
statFreshLabel: 'Every day',
// Property or array item — trailing comma allowed in TypeScript
readMore: 'Read more about us',
// Property or array item — trailing comma allowed in TypeScript
imageAlt: 'Fresh meat cuts on a cutting board from Kött Gård',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 70: trust: {
trust: {
// Property or array item — trailing comma allowed in TypeScript
halal: '100% Halal',
// Property or array item — trailing comma allowed in TypeScript
halalDesc: 'Certified halal sourcing with full traceability and compliance.',
// Property or array item — trailing comma allowed in TypeScript
fresh: 'Fresh Daily',
// Property or array item — trailing comma allowed in TypeScript
freshDesc: 'Sourced fresh every morning and delivered at peak quality.',
// Property or array item — trailing comma allowed in TypeScript
premium: 'Premium Quality',
// Property or array item — trailing comma allowed in TypeScript
premiumDesc: 'Hand-selected cuts from trusted farms, prepared by expert butchers.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 78: categories: {
categories: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Our Selection',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'Hand-picked meat — every day. Fresh delivery. Halal. Cut to order.',
// Property or array item — trailing comma allowed in TypeScript
shop: 'Shop {name}',
// Line 82: chicken: {
chicken: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Chicken',
// Property or array item — trailing comma allowed in TypeScript
description: 'Breast fillet, wings, drumsticks and whole chicken. Fresh every morning.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 86: beef: {
beef: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Beef & Veal',
// Property or array item — trailing comma allowed in TypeScript
description: 'Mince, bone marrow and premium cuts. High marbling, consistent quality.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 90: lamb: {
lamb: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lamb',
// Property or array item — trailing comma allowed in TypeScript
description: 'Shoulder, neck, roast and rack. From Ireland and New Zealand.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 94: fish: {
fish: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Fish',
// Property or array item — trailing comma allowed in TypeScript
description: 'Fresh catch, cleaned and ready to cook.',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 99: featured: {
featured: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Curated Selection',
// Property or array item — trailing comma allowed in TypeScript
title: 'Featured Products',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'Our most popular cuts, loved by families across the city.',
// Property or array item — trailing comma allowed in TypeScript
viewAll: 'View All',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 105: howItWorks: {
howItWorks: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Order',
// Property or array item — trailing comma allowed in TypeScript
title: 'How easy it is to order',
// Property or array item — trailing comma allowed in TypeScript
step1Title: 'Contact us',
// Line 109: step1Desc:
step1Desc:
// Property or array item — trailing comma allowed in TypeScript
'Send us a WhatsApp message with what you want — we reply quickly.',
// Property or array item — trailing comma allowed in TypeScript
step2Title: 'We confirm',
// Line 112: step2Desc:
step2Desc:
// Property or array item — trailing comma allowed in TypeScript
'We confirm your order, give you the price, and tell you when it is ready.',
// Property or array item — trailing comma allowed in TypeScript
step3Title: 'Pick up or delivery',
// Line 115: step3Desc:
step3Desc:
// Property or array item — trailing comma allowed in TypeScript
'Pick up in store at Tingvallavägen 11 or choose home delivery.',
// Property or array item — trailing comma allowed in TypeScript
step: 'Step {n}',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'Order on WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 120: offers: {
offers: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Order',
// Property or array item — trailing comma allowed in TypeScript
title: 'Weekly offers',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'We update regularly with fresh deals. Follow us on social media for the latest prices.',
// Property or array item — trailing comma allowed in TypeScript
disclaimer: 'Price valid while supplies last',
// Property or array item — trailing comma allowed in TypeScript
was: 'Was',
// Property or array item — trailing comma allowed in TypeScript
now: 'NOW',
// Property or array item — trailing comma allowed in TypeScript
order: 'Order',
// Property or array item — trailing comma allowed in TypeScript
viewProduct: 'View product',
// Line 130: badge: {
badge: {
// Property or array item — trailing comma allowed in TypeScript
fresh: 'FRESH',
// Property or array item — trailing comma allowed in TypeScript
halal: 'HALAL',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 134: items: {
items: {
// Property or array item — trailing comma allowed in TypeScript
chickenWings: { name: 'Fresh chicken wings PL' },
// Property or array item — trailing comma allowed in TypeScript
lambSteak: { name: 'Fresh lamb roast Ireland' },
// Property or array item — trailing comma allowed in TypeScript
beefMince: { name: 'Beef mince 5% fat IRL' },
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 140: social: {
social: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Follow us',
// Property or array item — trailing comma allowed in TypeScript
title: 'Follow us',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'1,100+ followers on Facebook · 249 posts · Daily updates',
// Property or array item — trailing comma allowed in TypeScript
facebook: 'Facebook',
// Property or array item — trailing comma allowed in TypeScript
instagram: 'Instagram',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 149: contact: {
contact: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Contact',
// Property or array item — trailing comma allowed in TypeScript
title: 'Visit us',
// Property or array item — trailing comma allowed in TypeScript
addressLabel: 'Address',
// Property or array item — trailing comma allowed in TypeScript
phoneLabel: 'Phone',
// Property or array item — trailing comma allowed in TypeScript
hoursLabel: 'Opening hours',
// Property or array item — trailing comma allowed in TypeScript
hoursValue: 'Every day: {hours}',
// Property or array item — trailing comma allowed in TypeScript
writeUs: 'Message us',
// Property or array item — trailing comma allowed in TypeScript
callUs: 'Call us',
// Property or array item — trailing comma allowed in TypeScript
openMaps: 'Open in Google Maps',
// Property or array item — trailing comma allowed in TypeScript
learnMore: 'Contact details',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 161: cta: {
cta: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Ready for Premium Halal Meat?',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Order today and experience the difference of truly fresh, customized halal meat delivered to your doorstep.',
// Property or array item — trailing comma allowed in TypeScript
button: 'Browse Our Selection',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'Order on WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 168: shop: {
shop: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Shop All Products',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'Premium halal meat, customized to your preference',
// Property or array item — trailing comma allowed in TypeScript
noProducts: 'No products found',
// Property or array item — trailing comma allowed in TypeScript
noProductsHint: 'Try adjusting your filters or search query',
// Property or array item — trailing comma allowed in TypeScript
filters: 'Filters',
// Property or array item — trailing comma allowed in TypeScript
productsFound: '{count} products found',
// Property or array item — trailing comma allowed in TypeScript
search: 'Search',
// Property or array item — trailing comma allowed in TypeScript
searchPlaceholder: 'Search products...',
// Property or array item — trailing comma allowed in TypeScript
category: 'Category',
// Property or array item — trailing comma allowed in TypeScript
sortBy: 'Sort By',
// Property or array item — trailing comma allowed in TypeScript
all: 'All',
// Property or array item — trailing comma allowed in TypeScript
sortFeatured: 'Featured',
// Property or array item — trailing comma allowed in TypeScript
sortPriceAsc: 'Price: Low to High',
// Property or array item — trailing comma allowed in TypeScript
sortPriceDesc: 'Price: High to Low',
// Property or array item — trailing comma allowed in TypeScript
sortName: 'Name AZ',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 185: product: {
product: {
// Property or array item — trailing comma allowed in TypeScript
backToShop: 'Back to Shop',
// Property or array item — trailing comma allowed in TypeScript
inStock: 'In Stock',
// Property or array item — trailing comma allowed in TypeScript
outOfStock: 'Out of Stock',
// Property or array item — trailing comma allowed in TypeScript
halalTrust: '100% Halal certified · Fresh daily · Premium quality',
// Property or array item — trailing comma allowed in TypeScript
yourSelection: 'Your selection',
// Property or array item — trailing comma allowed in TypeScript
aboutProduct: 'About This Product',
// Property or array item — trailing comma allowed in TypeScript
addToCart: 'Add to Cart',
// Property or array item — trailing comma allowed in TypeScript
addedToCart: 'Added to Cart',
// Property or array item — trailing comma allowed in TypeScript
decreaseQty: 'Decrease quantity',
// Property or array item — trailing comma allowed in TypeScript
increaseQty: 'Increase quantity',
// Property or array item — trailing comma allowed in TypeScript
removeWishlist: 'Remove from wishlist',
// Property or array item — trailing comma allowed in TypeScript
addWishlist: 'Add to wishlist',
// Property or array item — trailing comma allowed in TypeScript
viewProduct: 'View {name}',
// Property or array item — trailing comma allowed in TypeScript
pieces: '{count} pieces',
// Property or array item — trailing comma allowed in TypeScript
standardCut: 'Standard cut',
// Property or array item — trailing comma allowed in TypeScript
howManyCuts: 'How Many Cuts Do You Want?',
// Property or array item — trailing comma allowed in TypeScript
howManyCutsHint: 'Select the number of cuts for your order',
// Property or array item — trailing comma allowed in TypeScript
cutsAndStyle: '{cuts} cuts · {style}',
// Property or array item — trailing comma allowed in TypeScript
selectCutting: 'Select Cutting Style',
// Property or array item — trailing comma allowed in TypeScript
selectCuttingHint: 'Our butchers will prepare your {category} exactly to your preferred cut',
// Line 206: fishNote:
fishNote:
// Property or array item — trailing comma allowed in TypeScript
'Fish products are prepared with our standard professional cut — cleaned, scaled, and ready to cook.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 209: cutting: {
cutting: {
// Property or array item — trailing comma allowed in TypeScript
nihari: 'Nihari cut',
// Property or array item — trailing comma allowed in TypeScript
karahi: 'Karahi cut',
// Property or array item — trailing comma allowed in TypeScript
qeema: 'Qeema (minced)',
// Property or array item — trailing comma allowed in TypeScript
boneless: 'Boneless',
// Property or array item — trailing comma allowed in TypeScript
steak: 'Steak cut',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 216: priceUnit: {
priceUnit: {
// Property or array item — trailing comma allowed in TypeScript
perBird: 'per bird',
// Property or array item — trailing comma allowed in TypeScript
perPack: 'per pack',
// Property or array item — trailing comma allowed in TypeScript
perKg: 'per kg',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 221: badges: {
badges: {
// Property or array item — trailing comma allowed in TypeScript
bestseller: 'Bestseller',
// Property or array item — trailing comma allowed in TypeScript
chefsPick: "Chef's Pick",
// Property or array item — trailing comma allowed in TypeScript
premium: 'Premium',
// Property or array item — trailing comma allowed in TypeScript
popular: 'Popular',
// Property or array item — trailing comma allowed in TypeScript
freshCatch: 'Fresh Catch',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 228: products: {
products: {
// Line 229: 'chicken-whole': {
'chicken-whole': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Whole Chicken',
// Property or array item — trailing comma allowed in TypeScript
description: 'Farm-fresh whole halal chicken, perfect for roasting or curry.',
// Line 232: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Our whole chickens are sourced from certified halal farms and delivered at peak freshness. Each bird is hand-selected for quality, with tender meat and clean processing. Choose your preferred piece count and we will prepare it exactly how you need.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 235: 'chicken-breast': {
'chicken-breast': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Chicken Breast',
// Property or array item — trailing comma allowed in TypeScript
description: 'Lean, boneless chicken breast — ideal for grilling and healthy meals.',
// Line 238: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium boneless chicken breast, trimmed and ready to cook. Perfect for kebabs, stir-fries, and healthy weeknight dinners. Select your piece count and enjoy consistent quality every time.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 241: 'chicken-thighs': {
'chicken-thighs': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Chicken Thighs',
// Property or array item — trailing comma allowed in TypeScript
description: 'Juicy halal chicken thighs with rich flavor for curries and grills.',
// Line 244: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Our chicken thighs are known for their succulence and depth of flavor. Whether you are making a traditional karahi or a weekend BBQ, these thighs deliver every time.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 247: 'chicken-wings': {
'chicken-wings': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Chicken Wings',
// Property or array item — trailing comma allowed in TypeScript
description: 'Party-ready halal chicken wings for frying, baking, or grilling.',
// Line 250: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Crispy, flavorful chicken wings prepared halal and delivered fresh. A crowd favorite for game nights and family gatherings.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 253: 'beef-nihari': {
'beef-nihari': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Beef for Nihari',
// Property or array item — trailing comma allowed in TypeScript
description: 'Slow-cook ready beef cuts, perfect for traditional nihari.',
// Line 256: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Specially selected beef cuts ideal for slow-cooked nihari. Rich in collagen and flavor, these cuts break down beautifully over hours of simmering for an authentic, melt-in-your-mouth experience.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 259: 'beef-steak': {
'beef-steak': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Premium Beef Steak',
// Property or array item — trailing comma allowed in TypeScript
description: 'Restaurant-quality halal steak cuts for the perfect sear.',
// Line 262: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Hand-cut premium beef steaks from the finest halal sources. Marbled, tender, and ready for your grill or cast-iron pan. Choose your preferred cutting style.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 265: 'beef-mince': {
'beef-mince': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Beef Mince',
// Property or array item — trailing comma allowed in TypeScript
description: 'Fresh halal beef mince for kebabs, burgers, and qeema.',
// Line 268: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Finely ground halal beef mince with the perfect fat ratio for juicy kebabs, flavorful qeema, and homemade burgers. Ground fresh daily.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 271: 'beef-boneless': {
'beef-boneless': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Boneless Beef Cubes',
// Property or array item — trailing comma allowed in TypeScript
description: 'Versatile boneless beef cubes for karahi, biryani, and stews.',
// Line 274: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Uniform boneless beef cubes cut to perfection for quick-cooking dishes. Ideal for karahi, pulao, and stir-fries where consistent sizing matters.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 277: 'lamb-shoulder': {
'lamb-shoulder': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lamb Shoulder',
// Property or array item — trailing comma allowed in TypeScript
description: 'Rich, flavorful lamb shoulder for slow roasts and curries.',
// Line 280: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium halal lamb shoulder with beautiful marbling. Perfect for slow-roasted feasts, hearty curries, and traditional family meals. Customized to your preferred cut.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 283: 'lamb-leg': {
'lamb-leg': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lamb Leg',
// Property or array item — trailing comma allowed in TypeScript
description: 'Tender lamb leg for roasts, grills, and special occasions.',
// Line 286: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Whole or portioned lamb leg from certified halal sources. A centerpiece cut for Eid celebrations, dinner parties, and Sunday roasts.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 289: 'lamb-chops': {
'lamb-chops': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lamb Chops',
// Property or array item — trailing comma allowed in TypeScript
description: 'Premium lamb chops for grilling and fine dining at home.',
// Line 292: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Thick-cut halal lamb chops with perfect fat caps for grilling. Restaurant quality, delivered to your kitchen.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 295: 'lamb-mince': {
'lamb-mince': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lamb Mince',
// Property or array item — trailing comma allowed in TypeScript
description: 'Fresh halal lamb mince for kebabs, samosas, and qeema.',
// Line 298: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Finely ground lamb mince with rich flavor. Essential for seekh kebabs, lamb qeema, and stuffed parathas.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 301: 'fish-salmon': {
'fish-salmon': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Atlantic Salmon Fillet',
// Property or array item — trailing comma allowed in TypeScript
description: 'Buttery salmon fillets, skin-on and ready to pan-sear.',
// Line 304: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium Atlantic salmon fillets with rich omega-3 content. Cleaned, portioned, and vacuum-sealed for maximum freshness.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 307: 'fish-rohu': {
'fish-rohu': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Rohu Fish',
// Property or array item — trailing comma allowed in TypeScript
description: 'Whole rohu fish, cleaned and scaled — a South Asian favorite.',
// Line 310: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Fresh rohu fish, a staple in South Asian cuisine. Cleaned, scaled, and gutted. Perfect for fish curry, fried fish, and traditional recipes.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 313: 'fish-prawns': {
'fish-prawns': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Jumbo Prawns',
// Property or array item — trailing comma allowed in TypeScript
description: 'Large shell-on prawns for grilling, curries, and biryanis.',
// Line 316: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium jumbo prawns, deveined and ready to cook. Sweet, firm flesh that holds up beautifully in curries and tandoori preparations.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 319: 'fish-basa': {
'fish-basa': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Basa Fillet',
// Property or array item — trailing comma allowed in TypeScript
description: 'Mild, flaky basa fillets — perfect for beginners and kids.',
// Line 322: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Boneless basa fillets with a mild, delicate flavor. Easy to cook and versatile — great for fish tacos, baking, and light curries.',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 326: cart: {
cart: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Your Cart',
// Property or array item — trailing comma allowed in TypeScript
itemsCount: '{count} item(s) in your cart',
// Property or array item — trailing comma allowed in TypeScript
empty: 'Your cart is empty',
// Property or array item — trailing comma allowed in TypeScript
emptyHint: 'Browse our premium halal selection and add items to your cart.',
// Property or array item — trailing comma allowed in TypeScript
startShopping: 'Start Shopping',
// Property or array item — trailing comma allowed in TypeScript
customization: 'Customization:',
// Property or array item — trailing comma allowed in TypeScript
orderSummary: 'Order Summary',
// Property or array item — trailing comma allowed in TypeScript
subtotal: 'Subtotal',
// Property or array item — trailing comma allowed in TypeScript
delivery: 'Delivery',
// Property or array item — trailing comma allowed in TypeScript
free: 'Free',
// Property or array item — trailing comma allowed in TypeScript
freeDeliveryHint: 'Free delivery on orders over 500 kr',
// Property or array item — trailing comma allowed in TypeScript
total: 'Total',
// Property or array item — trailing comma allowed in TypeScript
proceedCheckout: 'Proceed to Checkout',
// Property or array item — trailing comma allowed in TypeScript
continueShopping: 'Continue Shopping',
// Property or array item — trailing comma allowed in TypeScript
removeItem: 'Remove item',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 343: checkout: {
checkout: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Secure Checkout',
// Property or array item — trailing comma allowed in TypeScript
backToCart: 'Back to Cart',
// Property or array item — trailing comma allowed in TypeScript
noItems: 'No items to checkout',
// Property or array item — trailing comma allowed in TypeScript
goToShop: 'Go to Shop',
// Property or array item — trailing comma allowed in TypeScript
orderConfirmed: 'Order Confirmed!',
// Property or array item — trailing comma allowed in TypeScript
thankYou: 'Thank you for your order. Your premium halal meat is being prepared.',
// Property or array item — trailing comma allowed in TypeScript
orderId: 'Order ID: {id}',
// Property or array item — trailing comma allowed in TypeScript
viewOrders: 'View Orders',
// Property or array item — trailing comma allowed in TypeScript
haveAccount: 'Have an account?',
// Property or array item — trailing comma allowed in TypeScript
signIn: 'Sign in',
// Property or array item — trailing comma allowed in TypeScript
fasterCheckout: 'for faster checkout.',
// Property or array item — trailing comma allowed in TypeScript
deliveryDetails: 'Delivery Details',
// Property or array item — trailing comma allowed in TypeScript
fullName: 'Full Name',
// Property or array item — trailing comma allowed in TypeScript
email: 'Email',
// Property or array item — trailing comma allowed in TypeScript
phone: 'Phone',
// Property or array item — trailing comma allowed in TypeScript
street: 'Street Address',
// Property or array item — trailing comma allowed in TypeScript
city: 'City',
// Property or array item — trailing comma allowed in TypeScript
state: 'State',
// Property or array item — trailing comma allowed in TypeScript
zip: 'ZIP Code',
// Property or array item — trailing comma allowed in TypeScript
payment: 'Payment',
// Property or array item — trailing comma allowed in TypeScript
creditCard: 'Credit Card',
// Property or array item — trailing comma allowed in TypeScript
cashOnDelivery: 'Cash on Delivery',
// Property or array item — trailing comma allowed in TypeScript
cardNumber: 'Card Number',
// Property or array item — trailing comma allowed in TypeScript
expiry: 'Expiry',
// Property or array item — trailing comma allowed in TypeScript
cvv: 'CVV',
// Property or array item — trailing comma allowed in TypeScript
qty: 'Qty: {count}',
// Property or array item — trailing comma allowed in TypeScript
processing: 'Processing...',
// Property or array item — trailing comma allowed in TypeScript
pay: 'Pay {amount}',
// Property or array item — trailing comma allowed in TypeScript
secure: 'Secure 256-bit SSL encryption',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 374: auth: {
auth: {
// Property or array item — trailing comma allowed in TypeScript
welcomeBack: 'Welcome Back',
// Property or array item — trailing comma allowed in TypeScript
createAccount: 'Create Account',
// Property or array item — trailing comma allowed in TypeScript
joinTagline: 'Join {name} for a premium shopping experience',
// Property or array item — trailing comma allowed in TypeScript
signInTagline: 'Sign in to your {name} account',
// Property or array item — trailing comma allowed in TypeScript
fullName: 'Full Name',
// Property or array item — trailing comma allowed in TypeScript
email: 'Email',
// Property or array item — trailing comma allowed in TypeScript
password: 'Password',
// Property or array item — trailing comma allowed in TypeScript
phone: 'Phone',
// Property or array item — trailing comma allowed in TypeScript
street: 'Street Address',
// Property or array item — trailing comma allowed in TypeScript
city: 'City',
// Property or array item — trailing comma allowed in TypeScript
state: 'State',
// Property or array item — trailing comma allowed in TypeScript
zip: 'ZIP Code',
// Property or array item — trailing comma allowed in TypeScript
signIn: 'Sign In',
// Property or array item — trailing comma allowed in TypeScript
register: 'Create Account',
// Property or array item — trailing comma allowed in TypeScript
hasAccount: 'Already have an account? Sign in',
// Property or array item — trailing comma allowed in TypeScript
noAccount: "Don't have an account? Register",
// Property or array item — trailing comma allowed in TypeScript
invalidCredentials: 'Invalid email or password. Try {email} / demo123',
// Property or array item — trailing comma allowed in TypeScript
demo: 'Demo: {email} / demo123',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 394: account: {
account: {
// Property or array item — trailing comma allowed in TypeScript
title: 'My Account',
// Property or array item — trailing comma allowed in TypeScript
welcome: 'Welcome back, {name}',
// Property or array item — trailing comma allowed in TypeScript
memberSince: 'Member since {date}',
// Property or array item — trailing comma allowed in TypeScript
myWishlist: 'My Wishlist',
// Property or array item — trailing comma allowed in TypeScript
signOut: 'Sign Out',
// Property or array item — trailing comma allowed in TypeScript
orderHistory: 'Order History',
// Property or array item — trailing comma allowed in TypeScript
noOrders: 'No orders yet',
// Property or array item — trailing comma allowed in TypeScript
startShopping: 'Start Shopping',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 404: wishlist: {
wishlist: {
// Property or array item — trailing comma allowed in TypeScript
title: 'My Wishlist',
// Property or array item — trailing comma allowed in TypeScript
saved: '{count} saved item(s)',
// Property or array item — trailing comma allowed in TypeScript
empty: 'Your wishlist is empty',
// Property or array item — trailing comma allowed in TypeScript
emptyHint: 'Save your favorite products to buy them later.',
// Property or array item — trailing comma allowed in TypeScript
browse: 'Browse Products',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 411: about: {
about: {
// Property or array item — trailing comma allowed in TypeScript
title: 'About {name}',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Bringing premium, 100% Halal meat to your table — fresh, customized, and delivered with care.',
// Property or array item — trailing comma allowed in TypeScript
ourStory: 'Our Story',
// Line 416: storyP1:
storyP1:
// Property or array item — trailing comma allowed in TypeScript
'{name} was founded with a simple mission: make premium halal meat accessible to every family, without compromising on quality, freshness, or religious compliance. We understand that for many households, the right cut prepared the right way is not a luxury — it is essential.',
// Line 418: storyP2:
storyP2:
// Property or array item — trailing comma allowed in TypeScript
'From selecting piece counts for chicken to choosing Nihari or Karahi cuts for beef and lamb, we put customization at the heart of every order. Our expert butchers prepare each order by hand, and our temperature-controlled delivery ensures your meat arrives as fresh as the day it was cut.',
// Property or array item — trailing comma allowed in TypeScript
halalTitle: 'Halal Certification',
// Line 421: halalDesc:
halalDesc:
// Property or array item — trailing comma allowed in TypeScript
'Every product at {name} is sourced from certified halal suppliers. Our supply chain is fully traceable, and we maintain strict compliance with halal slaughter and processing standards. We work exclusively with farms and processors that share our commitment to ethical, religiously compliant meat production.',
// Property or array item — trailing comma allowed in TypeScript
freshDaily: 'Fresh Daily',
// Property or array item — trailing comma allowed in TypeScript
freshDailyDesc: 'Sourced every morning from trusted farms',
// Property or array item — trailing comma allowed in TypeScript
premiumQuality: 'Premium Quality',
// Property or array item — trailing comma allowed in TypeScript
premiumQualityDesc: 'Hand-selected cuts by expert butchers',
// Property or array item — trailing comma allowed in TypeScript
fastDelivery: 'Fast Delivery',
// Property or array item — trailing comma allowed in TypeScript
fastDeliveryDesc: 'Temperature-controlled same-day delivery',
// Property or array item — trailing comma allowed in TypeScript
deliveryTitle: 'Delivery Information',
// Property or array item — trailing comma allowed in TypeScript
delivery1: 'We deliver within a 25-mile radius of our processing facility.',
// Property or array item — trailing comma allowed in TypeScript
delivery2: 'Orders placed before 2 PM are eligible for same-day delivery.',
// Property or array item — trailing comma allowed in TypeScript
delivery3: 'Free delivery on orders over 500 kr. Standard delivery fee: 49 kr.',
// Property or array item — trailing comma allowed in TypeScript
delivery4: 'All products are vacuum-sealed and transported in insulated packaging.',
// Property or array item — trailing comma allowed in TypeScript
contactTitle: 'Contact Us',
// Property or array item — trailing comma allowed in TypeScript
address: 'Tingvallavägen 11, 195 31 Märsta',
// Property or array item — trailing comma allowed in TypeScript
privacyTitle: 'Privacy Policy',
// Line 437: privacyText:
privacyText:
// Property or array item — trailing comma allowed in TypeScript
'We collect only the information needed to process your orders and improve our service — name, contact details, and delivery address. We do not sell your data to third parties. Payment details are handled securely by our payment partners.',
// Property or array item — trailing comma allowed in TypeScript
termsTitle: 'Terms of Service',
// Line 440: termsText:
termsText:
// Property or array item — trailing comma allowed in TypeScript
'All prices are shown in SEK and may change without notice. Orders are subject to availability. Halal certification applies to all meat products listed. Delivery times are estimates and may vary during peak periods.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 443: footer: {
footer: {
// Line 444: tagline:
tagline:
// Property or array item — trailing comma allowed in TypeScript
'Premium 100% Halal meat delivery. Fresh, customized cuts delivered to your door with uncompromising quality.',
// Property or array item — trailing comma allowed in TypeScript
shop: 'Shop',
// Property or array item — trailing comma allowed in TypeScript
company: 'Company',
// Property or array item — trailing comma allowed in TypeScript
contact: 'Contact',
// Property or array item — trailing comma allowed in TypeScript
rights: 'All rights reserved.',
// Property or array item — trailing comma allowed in TypeScript
phone: '072-585 50 50',
// Property or array item — trailing comma allowed in TypeScript
hours: 'Open every day {hours}',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 453: notFound: {
notFound: {
// Property or array item — trailing comma allowed in TypeScript
title: '404',
// Property or array item — trailing comma allowed in TypeScript
message: 'Page not found',
// Property or array item — trailing comma allowed in TypeScript
goHome: 'Go Home',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 458: orderStatus: {
orderStatus: {
// Property or array item — trailing comma allowed in TypeScript
pending: 'pending',
// Property or array item — trailing comma allowed in TypeScript
confirmed: 'confirmed',
// Property or array item — trailing comma allowed in TypeScript
preparing: 'preparing',
// Property or array item — trailing comma allowed in TypeScript
'out-for-delivery': 'out for delivery',
// Property or array item — trailing comma allowed in TypeScript
delivered: 'delivered',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
};
@@ -1,934 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/i18n/locales/sv.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import external package or local module
import { TranslationDict } from '../types';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const sv: TranslationDict = {
// Line 4: site: {
site: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Kött Gård',
// Property or array item — trailing comma allowed in TypeScript
tagline: 'Premium Halal',
// Line 7: description:
description:
// Property or array item — trailing comma allowed in TypeScript
'Premium 100 % halal köttleverans. Färskt och fryst kyckling, nötkött, lamm och fisk — anpassat efter dina önskemål och levererat till din dörr.',
// Property or array item — trailing comma allowed in TypeScript
metaTitle: 'Premium Halal Köttleverans',
// Property or array item — trailing comma allowed in TypeScript
initials: 'KG',
// Property or array item — trailing comma allowed in TypeScript
email: 'hello@kottgard.se',
// Property or array item — trailing comma allowed in TypeScript
demoEmail: 'demo@kottgard.se',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 14: nav: {
nav: {
// Property or array item — trailing comma allowed in TypeScript
shop: 'Butik',
// Property or array item — trailing comma allowed in TypeScript
chicken: 'Kyckling',
// Property or array item — trailing comma allowed in TypeScript
beef: 'Nötkött',
// Property or array item — trailing comma allowed in TypeScript
lamb: 'Lamm',
// Property or array item — trailing comma allowed in TypeScript
fish: 'Fisk',
// Property or array item — trailing comma allowed in TypeScript
about: 'Om oss',
// Property or array item — trailing comma allowed in TypeScript
halalCert: 'Halalcertifiering',
// Property or array item — trailing comma allowed in TypeScript
delivery: 'Leveransinfo',
// Property or array item — trailing comma allowed in TypeScript
contact: 'Kontakt',
// Property or array item — trailing comma allowed in TypeScript
myAccount: 'Mitt konto',
// Property or array item — trailing comma allowed in TypeScript
orderHistory: 'Orderhistorik',
// Property or array item — trailing comma allowed in TypeScript
wishlist: 'Önskelista',
// Property or array item — trailing comma allowed in TypeScript
cart: 'Varukorg',
// Property or array item — trailing comma allowed in TypeScript
privacy: 'Integritetspolicy',
// Property or array item — trailing comma allowed in TypeScript
terms: 'Användarvillkor',
// Property or array item — trailing comma allowed in TypeScript
searchProducts: 'Sök produkter',
// Property or array item — trailing comma allowed in TypeScript
toggleMenu: 'Växla meny',
// Property or array item — trailing comma allowed in TypeScript
account: 'Konto',
// Property or array item — trailing comma allowed in TypeScript
language: 'Språk',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 35: languageBanner: {
languageBanner: {
// Property or array item — trailing comma allowed in TypeScript
choose: 'Välj språk',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 38: hero: {
hero: {
// Property or array item — trailing comma allowed in TypeScript
badge: '100 % Halal-certifierat',
// Property or array item — trailing comma allowed in TypeScript
taglineShort: 'Naturligt Rent',
// Property or array item — trailing comma allowed in TypeScript
title: 'Premium Halal Kött',
// Property or array item — trailing comma allowed in TypeScript
titleHighlight: '',
// Property or array item — trailing comma allowed in TypeScript
titleEnd: '',
// Property or array item — trailing comma allowed in TypeScript
subtitleShort: 'Färskt. Kvalitet. Tillförlitligt.',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Halal-certifierat · Färskt dagligen · Hemleverans · Öppet alla dagar',
// Property or array item — trailing comma allowed in TypeScript
hours: 'Öppet alla dagar {hours}',
// Property or array item — trailing comma allowed in TypeScript
location: 'Tingvallavägen 11, Märsta',
// Property or array item — trailing comma allowed in TypeScript
shopNow: 'Se vårt sortiment',
// Property or array item — trailing comma allowed in TypeScript
browseChicken: 'Bläddra kyckling',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'Beställ via WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 53: aboutPreview: {
aboutPreview: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Om oss',
// Property or array item — trailing comma allowed in TypeScript
title: 'Märstas finaste köttbutik',
// Property or array item — trailing comma allowed in TypeScript
p1: 'Kött Gård är mer än en köttbutik — vi är ett löfte om kvalitet. Allt vårt kött är 100 % Halal-certifierat och färskt levererat varje dag.',
// Property or array item — trailing comma allowed in TypeScript
p2: 'Vi handlar lamm från Irland och Nya Zeeland, kyckling och nötkött från välrenommerade producenter, och hjälper dig hitta rätt detalj för middagen, festen eller söndagssteken.',
// Property or array item — trailing comma allowed in TypeScript
p3: 'Kom in i butiken på Tingvallavägen, prata med oss om vad du letar efter — vi styckar och packar efter dina önskemål.',
// Property or array item — trailing comma allowed in TypeScript
statHalal: '100 %',
// Property or array item — trailing comma allowed in TypeScript
statHalalLabel: 'Halal-certifierat',
// Property or array item — trailing comma allowed in TypeScript
statDays: '7 dagar',
// Property or array item — trailing comma allowed in TypeScript
statDaysLabel: 'Öppet i veckan',
// Property or array item — trailing comma allowed in TypeScript
statDelivery: 'Daglig',
// Property or array item — trailing comma allowed in TypeScript
statDeliveryLabel: 'Leverans',
// Property or array item — trailing comma allowed in TypeScript
statFresh: 'Färskt',
// Property or array item — trailing comma allowed in TypeScript
statFreshLabel: 'Varje dag',
// Property or array item — trailing comma allowed in TypeScript
readMore: 'Läs mer om oss',
// Property or array item — trailing comma allowed in TypeScript
imageAlt: 'Färska köttdetaljer på skärbräda från Kött Gård',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 70: trust: {
trust: {
// Property or array item — trailing comma allowed in TypeScript
halal: '100 % Halal',
// Property or array item — trailing comma allowed in TypeScript
halalDesc: 'Certifierad halal-källa med full spårbarhet och efterlevnad.',
// Property or array item — trailing comma allowed in TypeScript
fresh: 'Färskt Dagligen',
// Property or array item — trailing comma allowed in TypeScript
freshDesc: 'Hämtas färskt varje morgon och levereras i toppskick.',
// Property or array item — trailing comma allowed in TypeScript
premium: 'Premiumkvalitet',
// Property or array item — trailing comma allowed in TypeScript
premiumDesc: 'Handplockade styckningar från betrodda gårdar, förberedda av expertslaktare.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 78: categories: {
categories: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Vårt sortiment',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'Handplockat kött — varje dag. Färskt levererat. Halal. Styckat efter önskemål.',
// Property or array item — trailing comma allowed in TypeScript
shop: 'Handla {name}',
// Line 82: chicken: {
chicken: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Kyckling',
// Property or array item — trailing comma allowed in TypeScript
description: 'Kycklingbröstfilé, vingar, klubba och hel kyckling. Färsk varje morgon.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 86: beef: {
beef: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Nötkött & Kalv',
// Property or array item — trailing comma allowed in TypeScript
description: 'Nötfärs, kalv bone marrow och premiumskär. Hög marmorering, jämn kvalitet.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 90: lamb: {
lamb: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lamm',
// Property or array item — trailing comma allowed in TypeScript
description: 'Lammbringa, lammhals, lammstek och lammrygg. Från Irland och Nya Zeeland.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 94: fish: {
fish: {
// Property or array item — trailing comma allowed in TypeScript
name: 'Fisk',
// Property or array item — trailing comma allowed in TypeScript
description: 'Färsk fångst, rengjord och redo att tillagas.',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 99: featured: {
featured: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Utvalt Sortiment',
// Property or array item — trailing comma allowed in TypeScript
title: 'Utvalda Produkter',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'Våra mest populära styckningar, älskade av familjer i hela staden.',
// Property or array item — trailing comma allowed in TypeScript
viewAll: 'Visa alla',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 105: howItWorks: {
howItWorks: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Beställ',
// Property or array item — trailing comma allowed in TypeScript
title: 'Så enkelt beställer du',
// Property or array item — trailing comma allowed in TypeScript
step1Title: 'Kontakta oss',
// Line 109: step1Desc:
step1Desc:
// Property or array item — trailing comma allowed in TypeScript
'Skicka ett meddelande på WhatsApp med vad du vill ha — vi svarar snabbt.',
// Property or array item — trailing comma allowed in TypeScript
step2Title: 'Vi bekräftar',
// Line 112: step2Desc:
step2Desc:
// Property or array item — trailing comma allowed in TypeScript
'Vi bekräftar din beställning, ger pris och säger när den är klar.',
// Property or array item — trailing comma allowed in TypeScript
step3Title: 'Hämta eller leverans',
// Line 115: step3Desc:
step3Desc:
// Property or array item — trailing comma allowed in TypeScript
'Hämta i butik på Tingvallavägen 11 eller välj hemleverans.',
// Property or array item — trailing comma allowed in TypeScript
step: 'Steg {n}',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'Beställ på WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 120: offers: {
offers: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Beställ',
// Property or array item — trailing comma allowed in TypeScript
title: 'Veckans erbjudanden',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Vi uppdaterar löpande med färska erbjudanden. Följ oss på sociala medier för senaste priserna.',
// Property or array item — trailing comma allowed in TypeScript
disclaimer: 'Pris gäller så långt lagret räcker',
// Property or array item — trailing comma allowed in TypeScript
was: 'Före',
// Property or array item — trailing comma allowed in TypeScript
now: 'NU',
// Property or array item — trailing comma allowed in TypeScript
order: 'Beställ',
// Property or array item — trailing comma allowed in TypeScript
viewProduct: 'Visa produkt',
// Line 130: badge: {
badge: {
// Property or array item — trailing comma allowed in TypeScript
fresh: 'FÄRSK',
// Property or array item — trailing comma allowed in TypeScript
halal: 'HALAL',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 134: items: {
items: {
// Property or array item — trailing comma allowed in TypeScript
chickenWings: { name: 'Kycklingvingar färsk PL' },
// Property or array item — trailing comma allowed in TypeScript
lambSteak: { name: 'Lammstek färsk Ireland' },
// Property or array item — trailing comma allowed in TypeScript
beefMince: { name: 'Nötfärs 5% fett IRL' },
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 140: social: {
social: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Följ oss',
// Property or array item — trailing comma allowed in TypeScript
title: 'Följ oss',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'1 100+ följare på Facebook · 249 inlägg · Dagliga uppdateringar',
// Property or array item — trailing comma allowed in TypeScript
facebook: 'Facebook',
// Property or array item — trailing comma allowed in TypeScript
instagram: 'Instagram',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 149: contact: {
contact: {
// Property or array item — trailing comma allowed in TypeScript
label: 'Kontakt',
// Property or array item — trailing comma allowed in TypeScript
title: 'Besök oss',
// Property or array item — trailing comma allowed in TypeScript
addressLabel: 'Adress',
// Property or array item — trailing comma allowed in TypeScript
phoneLabel: 'Telefon',
// Property or array item — trailing comma allowed in TypeScript
hoursLabel: 'Öppettider',
// Property or array item — trailing comma allowed in TypeScript
hoursValue: 'Alla dagar: {hours}',
// Property or array item — trailing comma allowed in TypeScript
writeUs: 'Skriv till oss',
// Property or array item — trailing comma allowed in TypeScript
callUs: 'Ring oss',
// Property or array item — trailing comma allowed in TypeScript
openMaps: 'Öppna i Google Maps',
// Property or array item — trailing comma allowed in TypeScript
learnMore: 'Kontaktuppgifter',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 161: cta: {
cta: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Redo för Premium Halal Kött?',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Beställ idag och upplev skillnaden med verkligt färskt, anpassat halal kött levererat till din dörr.',
// Property or array item — trailing comma allowed in TypeScript
button: 'Se vårt sortiment',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'Beställ på WhatsApp',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 168: shop: {
shop: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Alla Produkter',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'Premium halal kött, anpassat efter dina önskemål',
// Property or array item — trailing comma allowed in TypeScript
noProducts: 'Inga produkter hittades',
// Property or array item — trailing comma allowed in TypeScript
noProductsHint: 'Prova att justera dina filter eller sökfråga',
// Property or array item — trailing comma allowed in TypeScript
filters: 'Filter',
// Property or array item — trailing comma allowed in TypeScript
productsFound: '{count} produkter hittades',
// Property or array item — trailing comma allowed in TypeScript
search: 'Sök',
// Property or array item — trailing comma allowed in TypeScript
searchPlaceholder: 'Sök produkter...',
// Property or array item — trailing comma allowed in TypeScript
category: 'Kategori',
// Property or array item — trailing comma allowed in TypeScript
sortBy: 'Sortera efter',
// Property or array item — trailing comma allowed in TypeScript
all: 'Alla',
// Property or array item — trailing comma allowed in TypeScript
sortFeatured: 'Utvalda',
// Property or array item — trailing comma allowed in TypeScript
sortPriceAsc: 'Pris: Lägst till högst',
// Property or array item — trailing comma allowed in TypeScript
sortPriceDesc: 'Pris: Högst till lägst',
// Property or array item — trailing comma allowed in TypeScript
sortName: 'Namn A–Ö',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 185: product: {
product: {
// Property or array item — trailing comma allowed in TypeScript
backToShop: 'Tillbaka till butiken',
// Property or array item — trailing comma allowed in TypeScript
inStock: 'I lager',
// Property or array item — trailing comma allowed in TypeScript
outOfStock: 'Slut i lager',
// Property or array item — trailing comma allowed in TypeScript
halalTrust: '100 % Halal-certifierad · Färskt dagligen · Premiumkvalitet',
// Property or array item — trailing comma allowed in TypeScript
yourSelection: 'Ditt val',
// Property or array item — trailing comma allowed in TypeScript
aboutProduct: 'Om denna produkt',
// Property or array item — trailing comma allowed in TypeScript
addToCart: 'Lägg i varukorg',
// Property or array item — trailing comma allowed in TypeScript
addedToCart: 'Tillagd i varukorg',
// Property or array item — trailing comma allowed in TypeScript
decreaseQty: 'Minska antal',
// Property or array item — trailing comma allowed in TypeScript
increaseQty: 'Öka antal',
// Property or array item — trailing comma allowed in TypeScript
removeWishlist: 'Ta bort från önskelista',
// Property or array item — trailing comma allowed in TypeScript
addWishlist: 'Lägg till i önskelista',
// Property or array item — trailing comma allowed in TypeScript
viewProduct: 'Visa {name}',
// Property or array item — trailing comma allowed in TypeScript
pieces: '{count} bitar',
// Property or array item — trailing comma allowed in TypeScript
standardCut: 'Standardstyckning',
// Property or array item — trailing comma allowed in TypeScript
howManyCuts: 'Hur Många Styckningar Vill Du Ha?',
// Property or array item — trailing comma allowed in TypeScript
howManyCutsHint: 'Välj antal styckningar för din beställning',
// Property or array item — trailing comma allowed in TypeScript
cutsAndStyle: '{cuts} styckningar · {style}',
// Property or array item — trailing comma allowed in TypeScript
selectCutting: 'Välj skärstil',
// Property or array item — trailing comma allowed in TypeScript
selectCuttingHint: 'Våra slaktare förbereder ditt {category} exakt enligt din önskade styckning',
// Line 206: fishNote:
fishNote:
// Property or array item — trailing comma allowed in TypeScript
'Fiskprodukter förbereds med vår standard professionella styckning — rengjord, fjällad och redo att tillagas.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 209: cutting: {
cutting: {
// Property or array item — trailing comma allowed in TypeScript
nihari: 'Nihari-styckning',
// Property or array item — trailing comma allowed in TypeScript
karahi: 'Karahi-styckning',
// Property or array item — trailing comma allowed in TypeScript
qeema: 'Qeema (färs)',
// Property or array item — trailing comma allowed in TypeScript
boneless: 'Benfri',
// Property or array item — trailing comma allowed in TypeScript
steak: 'Biffstyckning',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 216: priceUnit: {
priceUnit: {
// Property or array item — trailing comma allowed in TypeScript
perBird: 'per kyckling',
// Property or array item — trailing comma allowed in TypeScript
perPack: 'per förpackning',
// Property or array item — trailing comma allowed in TypeScript
perKg: 'per kg',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 221: badges: {
badges: {
// Property or array item — trailing comma allowed in TypeScript
bestseller: 'Bästsäljare',
// Property or array item — trailing comma allowed in TypeScript
chefsPick: 'Kockens val',
// Property or array item — trailing comma allowed in TypeScript
premium: 'Premium',
// Property or array item — trailing comma allowed in TypeScript
popular: 'Populär',
// Property or array item — trailing comma allowed in TypeScript
freshCatch: 'Färsk fångst',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 228: products: {
products: {
// Line 229: 'chicken-whole': {
'chicken-whole': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Hel Kyckling',
// Property or array item — trailing comma allowed in TypeScript
description: 'Gårdsfärsk hel halal kyckling, perfekt för stekning eller curry.',
// Line 232: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Våra hela kycklingar kommer från certifierade halalgårdar och levereras i toppskick. Varje fågel är handplockad för kvalitet, med mört kött och ren bearbetning. Välj önskat antal bitar så förbereder vi den precis som du behöver.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 235: 'chicken-breast': {
'chicken-breast': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Kycklingbröst',
// Property or array item — trailing comma allowed in TypeScript
description: 'Magert, benfritt kycklingbröst — idealiskt för grillning och hälsosamma måltider.',
// Line 238: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium benfritt kycklingbröst, trimmat och redo att tillagas. Perfekt för kebab, wok och hälsosamma vardagsmiddagar. Välj antal bitar och njut av konsekvent kvalitet varje gång.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 241: 'chicken-thighs': {
'chicken-thighs': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Kycklinglår',
// Property or array item — trailing comma allowed in TypeScript
description: 'Saftiga halal kycklinglår med rik smak för curry och grill.',
// Line 244: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Våra kycklinglår är kända för sin saftighet och djupa smak. Oavsett om du lagar traditionell karahi eller helg-BBQ levererar dessa lår varje gång.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 247: 'chicken-wings': {
'chicken-wings': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Kycklingvingar',
// Property or array item — trailing comma allowed in TypeScript
description: 'Festfärdiga halal kycklingvingar för stekning, bakning eller grillning.',
// Line 250: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Krispiga, smakrika kycklingvingar tillagade enligt halal och levererade färskt. En favorit för matchkvällar och familjesammankomster.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 253: 'beef-nihari': {
'beef-nihari': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Nötkött för Nihari',
// Property or array item — trailing comma allowed in TypeScript
description: 'Långkoksklara nötköttsstyckningar, perfekta för traditionell nihari.',
// Line 256: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Särskilt utvalda nötköttsstyckningar idealiska för långkokt nihari. Rika på kollagen och smak, dessa styckningar bryts ner vackert under timmar av sjudning för en autentisk, smältande upplevelse.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 259: 'beef-steak': {
'beef-steak': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Premium Nötbiff',
// Property or array item — trailing comma allowed in TypeScript
description: 'Restaurangkvalitet halal biffstyckningar för perfekt stekyta.',
// Line 262: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Handskurna premium biffar från de finaste halal-källorna. Marmorering, mörhet och redo för din grill eller gjutjärnspanna. Välj önskad skärstil.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 265: 'beef-mince': {
'beef-mince': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Nötfärs',
// Property or array item — trailing comma allowed in TypeScript
description: 'Färsk halal nötfärs för kebab, burgare och qeema.',
// Line 268: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Finmalen halal nötfärs med perfekt fettförhållande för saftiga kebab, smakrik qeema och hemlagade burgare. Malas färskt dagligen.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 271: 'beef-boneless': {
'beef-boneless': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Benfria Nötköttskuber',
// Property or array item — trailing comma allowed in TypeScript
description: 'Mångsidiga benfria nötköttskuber för karahi, biryani och grytor.',
// Line 274: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Enhetliga benfria nötköttskuber skurna till perfektion för snabbkokta rätter. Idealiska för karahi, pulao och wok där konsekvent storlek är viktigt.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 277: 'lamb-shoulder': {
'lamb-shoulder': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lammbog',
// Property or array item — trailing comma allowed in TypeScript
description: 'Rik, smakrik lammbog för långstekning och curry.',
// Line 280: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium halal lammbog med vacker marmorering. Perfekt för långstekta festmåltider, rejäla curryrätter och traditionella familjemiddagar. Anpassad efter din önskade styckning.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 283: 'lamb-leg': {
'lamb-leg': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lammlägg',
// Property or array item — trailing comma allowed in TypeScript
description: 'Mört lammlägg för stekning, grillning och speciella tillfällen.',
// Line 286: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Hela eller portionerade lammlägg från certifierade halal-källor. En centerstyckning för Eid-firanden, middagsbjudningar och söndagsstek.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 289: 'lamb-chops': {
'lamb-chops': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lammkotletter',
// Property or array item — trailing comma allowed in TypeScript
description: 'Premium lammkotletter för grillning och finmiddag hemma.',
// Line 292: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Tjockskurna halal lammkotletter med perfekt fettlock för grillning. Restaurangkvalitet, levererad till ditt kök.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 295: 'lamb-mince': {
'lamb-mince': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Lammfärs',
// Property or array item — trailing comma allowed in TypeScript
description: 'Färsk halal lammfärs för kebab, samosas och qeema.',
// Line 298: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Finmalen lammfärs med rik smak. Nödvändig för seekh kebab, lamm qeema och fyllda parathas.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 301: 'fish-salmon': {
'fish-salmon': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Atlantisk Laxfilé',
// Property or array item — trailing comma allowed in TypeScript
description: 'Smörig laxfilé, med skinn och redo att steka i panna.',
// Line 304: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium atlantisk laxfilé med rikt omega-3-innehåll. Rengjord, portionerad och vakuumförpackad för maximal färskhet.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 307: 'fish-rohu': {
'fish-rohu': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Rohu-fisk',
// Property or array item — trailing comma allowed in TypeScript
description: 'Hel rohu-fisk, rengjord och fjällad — en sydasiatisk favorit.',
// Line 310: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Färsk rohu-fisk, en stapel i sydasiatisk matlagning. Rengjord, fjällad och urtagen. Perfekt för fiskcurry, stekt fisk och traditionella recept.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 313: 'fish-prawns': {
'fish-prawns': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Jätteräkor',
// Property or array item — trailing comma allowed in TypeScript
description: 'Stora räkor med skal för grillning, curry och biryani.',
// Line 316: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Premium jätteräkor, urtagna och redo att tillagas. Söt, fast kött som håller sig vackert i curry och tandoori-rätter.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 319: 'fish-basa': {
'fish-basa': {
// Property or array item — trailing comma allowed in TypeScript
name: 'Basa-filé',
// Property or array item — trailing comma allowed in TypeScript
description: 'Mild, flagnig basa-filé — perfekt för nybörjare och barn.',
// Line 322: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'Benfria basa-filéer med mild, delikat smak. Lätt att tillaga och mångsidig — utmärkt för fisk-tacos, bakning och lätta curryrätter.',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 326: cart: {
cart: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Din Varukorg',
// Property or array item — trailing comma allowed in TypeScript
itemsCount: '{count} artikel/artiklar i din varukorg',
// Property or array item — trailing comma allowed in TypeScript
empty: 'Din varukorg är tom',
// Property or array item — trailing comma allowed in TypeScript
emptyHint: 'Bläddra i vårt premium halal-sortiment och lägg till artiklar.',
// Property or array item — trailing comma allowed in TypeScript
startShopping: 'Börja handla',
// Property or array item — trailing comma allowed in TypeScript
customization: 'Anpassning:',
// Property or array item — trailing comma allowed in TypeScript
orderSummary: 'Ordersammanfattning',
// Property or array item — trailing comma allowed in TypeScript
subtotal: 'Delsumma',
// Property or array item — trailing comma allowed in TypeScript
delivery: 'Leverans',
// Property or array item — trailing comma allowed in TypeScript
free: 'Gratis',
// Property or array item — trailing comma allowed in TypeScript
freeDeliveryHint: 'Fri leverans på beställningar över 500 kr',
// Property or array item — trailing comma allowed in TypeScript
total: 'Totalt',
// Property or array item — trailing comma allowed in TypeScript
proceedCheckout: 'Gå till kassan',
// Property or array item — trailing comma allowed in TypeScript
continueShopping: 'Fortsätt handla',
// Property or array item — trailing comma allowed in TypeScript
removeItem: 'Ta bort artikel',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 343: checkout: {
checkout: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Säker Kassa',
// Property or array item — trailing comma allowed in TypeScript
backToCart: 'Tillbaka till varukorg',
// Property or array item — trailing comma allowed in TypeScript
noItems: 'Inga artiklar att betala',
// Property or array item — trailing comma allowed in TypeScript
goToShop: 'Gå till butiken',
// Property or array item — trailing comma allowed in TypeScript
orderConfirmed: 'Beställning bekräftad!',
// Property or array item — trailing comma allowed in TypeScript
thankYou: 'Tack för din beställning. Ditt premium halal kött förbereds.',
// Property or array item — trailing comma allowed in TypeScript
orderId: 'Order-ID: {id}',
// Property or array item — trailing comma allowed in TypeScript
viewOrders: 'Visa beställningar',
// Property or array item — trailing comma allowed in TypeScript
haveAccount: 'Har du ett konto?',
// Property or array item — trailing comma allowed in TypeScript
signIn: 'Logga in',
// Property or array item — trailing comma allowed in TypeScript
fasterCheckout: 'för snabbare utcheckning.',
// Property or array item — trailing comma allowed in TypeScript
deliveryDetails: 'Leveransuppgifter',
// Property or array item — trailing comma allowed in TypeScript
fullName: 'Fullständigt namn',
// Property or array item — trailing comma allowed in TypeScript
email: 'E-post',
// Property or array item — trailing comma allowed in TypeScript
phone: 'Telefon',
// Property or array item — trailing comma allowed in TypeScript
street: 'Gatuadress',
// Property or array item — trailing comma allowed in TypeScript
city: 'Stad',
// Property or array item — trailing comma allowed in TypeScript
state: 'Län',
// Property or array item — trailing comma allowed in TypeScript
zip: 'Postnummer',
// Property or array item — trailing comma allowed in TypeScript
payment: 'Betalning',
// Property or array item — trailing comma allowed in TypeScript
creditCard: 'Kreditkort',
// Property or array item — trailing comma allowed in TypeScript
cashOnDelivery: 'Kontant vid leverans',
// Property or array item — trailing comma allowed in TypeScript
cardNumber: 'Kortnummer',
// Property or array item — trailing comma allowed in TypeScript
expiry: 'Giltig till',
// Property or array item — trailing comma allowed in TypeScript
cvv: 'CVV',
// Property or array item — trailing comma allowed in TypeScript
qty: 'Antal: {count}',
// Property or array item — trailing comma allowed in TypeScript
processing: 'Bearbetar...',
// Property or array item — trailing comma allowed in TypeScript
pay: 'Betala {amount}',
// Property or array item — trailing comma allowed in TypeScript
secure: 'Säker 256-bitars SSL-kryptering',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 374: auth: {
auth: {
// Property or array item — trailing comma allowed in TypeScript
welcomeBack: 'Välkommen tillbaka',
// Property or array item — trailing comma allowed in TypeScript
createAccount: 'Skapa konto',
// Property or array item — trailing comma allowed in TypeScript
joinTagline: 'Gå med i {name} för en premium shoppingupplevelse',
// Property or array item — trailing comma allowed in TypeScript
signInTagline: 'Logga in på ditt {name}-konto',
// Property or array item — trailing comma allowed in TypeScript
fullName: 'Fullständigt namn',
// Property or array item — trailing comma allowed in TypeScript
email: 'E-post',
// Property or array item — trailing comma allowed in TypeScript
password: 'Lösenord',
// Property or array item — trailing comma allowed in TypeScript
phone: 'Telefon',
// Property or array item — trailing comma allowed in TypeScript
street: 'Gatuadress',
// Property or array item — trailing comma allowed in TypeScript
city: 'Stad',
// Property or array item — trailing comma allowed in TypeScript
state: 'Län',
// Property or array item — trailing comma allowed in TypeScript
zip: 'Postnummer',
// Property or array item — trailing comma allowed in TypeScript
signIn: 'Logga in',
// Property or array item — trailing comma allowed in TypeScript
register: 'Skapa konto',
// Property or array item — trailing comma allowed in TypeScript
hasAccount: 'Har du redan ett konto? Logga in',
// Property or array item — trailing comma allowed in TypeScript
noAccount: 'Har du inget konto? Registrera dig',
// Property or array item — trailing comma allowed in TypeScript
invalidCredentials: 'Ogiltig e-post eller lösenord. Prova {email} / demo123',
// Property or array item — trailing comma allowed in TypeScript
demo: 'Demo: {email} / demo123',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 394: account: {
account: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Mitt Konto',
// Property or array item — trailing comma allowed in TypeScript
welcome: 'Välkommen tillbaka, {name}',
// Property or array item — trailing comma allowed in TypeScript
memberSince: 'Medlem sedan {date}',
// Property or array item — trailing comma allowed in TypeScript
myWishlist: 'Min önskelista',
// Property or array item — trailing comma allowed in TypeScript
signOut: 'Logga ut',
// Property or array item — trailing comma allowed in TypeScript
orderHistory: 'Orderhistorik',
// Property or array item — trailing comma allowed in TypeScript
noOrders: 'Inga beställningar ännu',
// Property or array item — trailing comma allowed in TypeScript
startShopping: 'Börja handla',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 404: wishlist: {
wishlist: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Min Önskelista',
// Property or array item — trailing comma allowed in TypeScript
saved: '{count} sparad(e) artikel/artiklar',
// Property or array item — trailing comma allowed in TypeScript
empty: 'Din önskelista är tom',
// Property or array item — trailing comma allowed in TypeScript
emptyHint: 'Spara dina favoritprodukter för att köpa dem senare.',
// Property or array item — trailing comma allowed in TypeScript
browse: 'Bläddra produkter',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 411: about: {
about: {
// Property or array item — trailing comma allowed in TypeScript
title: 'Om {name}',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'Vi levererar premium 100 % halal kött till ditt bord — färskt, anpassat och med omsorg.',
// Property or array item — trailing comma allowed in TypeScript
ourStory: 'Vår Historia',
// Line 416: storyP1:
storyP1:
// Property or array item — trailing comma allowed in TypeScript
'{name} grundades med ett enkelt uppdrag: göra premium halal kött tillgängligt för varje familj, utan att kompromissa med kvalitet, färskhet eller religiös efterlevnad. Vi förstår att för många hushåll är rätt styckning tillagad på rätt sätt inte en lyx — det är nödvändigt.',
// Line 418: storyP2:
storyP2:
// Property or array item — trailing comma allowed in TypeScript
'Från att välja antal bitar för kyckling till att välja Nihari- eller Karahi-styckning för nötkött och lamm, sätter vi anpassning i centrum för varje beställning. Våra expertslaktare förbereder varje order för hand, och vår temperaturkontrollerade leverans säkerställer att ditt kött anländer lika färskt som dagen det styckades.',
// Property or array item — trailing comma allowed in TypeScript
halalTitle: 'Halalcertifiering',
// Line 421: halalDesc:
halalDesc:
// Property or array item — trailing comma allowed in TypeScript
'Varje produkt hos {name} kommer från certifierade halal-leverantörer. Vår leveranskedja är fullt spårbar och vi upprätthåller strikt efterlevnad av halal-slakt och -bearbetningsstandarder. Vi arbetar uteslutande med gårdar och bearbetare som delar vårt engagemang för etiskt, religiöst korrekt köttproduktion.',
// Property or array item — trailing comma allowed in TypeScript
freshDaily: 'Färskt Dagligen',
// Property or array item — trailing comma allowed in TypeScript
freshDailyDesc: 'Hämtas varje morgon från betrodda gårdar',
// Property or array item — trailing comma allowed in TypeScript
premiumQuality: 'Premiumkvalitet',
// Property or array item — trailing comma allowed in TypeScript
premiumQualityDesc: 'Handplockade styckningar av expertslaktare',
// Property or array item — trailing comma allowed in TypeScript
fastDelivery: 'Snabb Leverans',
// Property or array item — trailing comma allowed in TypeScript
fastDeliveryDesc: 'Temperaturkontrollerad leverans samma dag',
// Property or array item — trailing comma allowed in TypeScript
deliveryTitle: 'Leveransinformation',
// Property or array item — trailing comma allowed in TypeScript
delivery1: 'Vi levererar inom en radie på 40 km från vår anläggning.',
// Property or array item — trailing comma allowed in TypeScript
delivery2: 'Beställningar före kl. 14 är berättigade till leverans samma dag.',
// Property or array item — trailing comma allowed in TypeScript
delivery3: 'Fri leverans på beställningar över 500 kr. Standardleverans: 49 kr.',
// Property or array item — trailing comma allowed in TypeScript
delivery4: 'Alla produkter vakuumförpackas och transporteras i isolerad förpackning.',
// Property or array item — trailing comma allowed in TypeScript
contactTitle: 'Kontakta Oss',
// Property or array item — trailing comma allowed in TypeScript
address: 'Tingvallavägen 11, 195 31 Märsta',
// Property or array item — trailing comma allowed in TypeScript
privacyTitle: 'Integritetspolicy',
// Line 437: privacyText:
privacyText:
// Property or array item — trailing comma allowed in TypeScript
'Vi samlar endast den information som behövs för att behandla dina beställningar — namn, kontaktuppgifter och leveransadress. Vi säljer inte dina uppgifter till tredje part. Betalningsuppgifter hanteras säkert av våra betalpartners.',
// Property or array item — trailing comma allowed in TypeScript
termsTitle: 'Användarvillkor',
// Line 440: termsText:
termsText:
// Property or array item — trailing comma allowed in TypeScript
'Alla priser visas i SEK och kan ändras utan föregående meddelande. Beställningar är beroende av tillgång. Halalcertifiering gäller för alla köttprodukter. Leveranstider är uppskattningar och kan variera under högtrafik.',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 443: footer: {
footer: {
// Line 444: tagline:
tagline:
// Property or array item — trailing comma allowed in TypeScript
'Premium 100 % halal köttleverans. Färskt, anpassat kött levererat till din dörr med kompromisslös kvalitet.',
// Property or array item — trailing comma allowed in TypeScript
shop: 'Butik',
// Property or array item — trailing comma allowed in TypeScript
company: 'Företag',
// Property or array item — trailing comma allowed in TypeScript
contact: 'Kontakt',
// Property or array item — trailing comma allowed in TypeScript
rights: 'Alla rättigheter förbehållna.',
// Property or array item — trailing comma allowed in TypeScript
phone: '072-585 50 50',
// Property or array item — trailing comma allowed in TypeScript
hours: 'Öppet alla dagar {hours}',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 453: notFound: {
notFound: {
// Property or array item — trailing comma allowed in TypeScript
title: '404',
// Property or array item — trailing comma allowed in TypeScript
message: 'Sidan hittades inte',
// Property or array item — trailing comma allowed in TypeScript
goHome: 'Gå till startsidan',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 458: orderStatus: {
orderStatus: {
// Property or array item — trailing comma allowed in TypeScript
pending: 'väntande',
// Property or array item — trailing comma allowed in TypeScript
confirmed: 'bekräftad',
// Property or array item — trailing comma allowed in TypeScript
preparing: 'förbereds',
// Property or array item — trailing comma allowed in TypeScript
'out-for-delivery': 'ute för leverans',
// Property or array item — trailing comma allowed in TypeScript
delivered: 'levererad',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
};
@@ -1,936 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/i18n/locales/ur.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import external package or local module
import { TranslationDict } from '../types';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const ur: TranslationDict = {
// Line 4: site: {
site: {
// Property or array item — trailing comma allowed in TypeScript
name: 'کوٹ گارڈ',
// Property or array item — trailing comma allowed in TypeScript
tagline: 'پریمیم حلال',
// Line 7: description:
description:
// Property or array item — trailing comma allowed in TypeScript
'پریمیم 100% حلال گوشت کی ڈیلیوری۔ تازہ اور منجمد مرغی، گائے کا گوشت، بکرے کا گوشت اور مچھلی — آپ کی پسند کے مطابق تیار کر کے آپ کے دروازے تک پہنچائی جاتی ہے۔',
// Property or array item — trailing comma allowed in TypeScript
metaTitle: 'پریمیم حلال گوشت ڈیلیوری',
// Property or array item — trailing comma allowed in TypeScript
initials: 'KG',
// Property or array item — trailing comma allowed in TypeScript
email: 'hello@kottgard.se',
// Property or array item — trailing comma allowed in TypeScript
demoEmail: 'demo@kottgard.se',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 14: nav: {
nav: {
// Property or array item — trailing comma allowed in TypeScript
shop: 'خریداری',
// Property or array item — trailing comma allowed in TypeScript
chicken: 'مرغی',
// Property or array item — trailing comma allowed in TypeScript
beef: 'گائے کا گوشت',
// Property or array item — trailing comma allowed in TypeScript
lamb: 'بکرے کا گوشت',
// Property or array item — trailing comma allowed in TypeScript
fish: 'مچھلی',
// Property or array item — trailing comma allowed in TypeScript
about: 'ہمارے بارے میں',
// Property or array item — trailing comma allowed in TypeScript
halalCert: 'حلال سرٹیفیکیشن',
// Property or array item — trailing comma allowed in TypeScript
delivery: 'ڈیلیوری کی معلومات',
// Property or array item — trailing comma allowed in TypeScript
contact: 'رابطہ',
// Property or array item — trailing comma allowed in TypeScript
myAccount: 'میرا اکاؤنٹ',
// Property or array item — trailing comma allowed in TypeScript
orderHistory: 'آرڈر کی تاریخ',
// Property or array item — trailing comma allowed in TypeScript
wishlist: 'پسندیدہ',
// Property or array item — trailing comma allowed in TypeScript
cart: 'ٹوکری',
// Property or array item — trailing comma allowed in TypeScript
privacy: 'رازداری کی پالیسی',
// Property or array item — trailing comma allowed in TypeScript
terms: 'شرائط و ضوابط',
// Property or array item — trailing comma allowed in TypeScript
searchProducts: 'مصنوعات تلاش کریں',
// Property or array item — trailing comma allowed in TypeScript
toggleMenu: 'مینو کھولیں',
// Property or array item — trailing comma allowed in TypeScript
account: 'اکاؤنٹ',
// Property or array item — trailing comma allowed in TypeScript
language: 'زبان',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 35: languageBanner: {
languageBanner: {
// Property or array item — trailing comma allowed in TypeScript
choose: 'اپنی زبان منتخب کریں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 38: hero: {
hero: {
// Property or array item — trailing comma allowed in TypeScript
badge: '100% حلال تصدیق شدہ',
// Property or array item — trailing comma allowed in TypeScript
taglineShort: 'قدرتی طور پر خالص',
// Property or array item — trailing comma allowed in TypeScript
title: 'پریمیم حلال گوشت',
// Property or array item — trailing comma allowed in TypeScript
titleHighlight: '',
// Property or array item — trailing comma allowed in TypeScript
titleEnd: '',
// Property or array item — trailing comma allowed in TypeScript
subtitleShort: 'تازہ۔ معیار۔ قابل اعتماد۔',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'حلال تصدیق شدہ · روزانہ تازہ · گھر کی ڈیلیوری · ہر دن کھلا',
// Property or array item — trailing comma allowed in TypeScript
hours: 'ہر دن کھلا {hours}',
// Property or array item — trailing comma allowed in TypeScript
location: 'Tingvallavägen 11, Märsta',
// Property or array item — trailing comma allowed in TypeScript
shopNow: 'ہمارا مجموعہ دیکھیں',
// Property or array item — trailing comma allowed in TypeScript
browseChicken: 'مرغی دیکھیں',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'واٹس ایپ سے آرڈر کریں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 53: aboutPreview: {
aboutPreview: {
// Property or array item — trailing comma allowed in TypeScript
label: 'ہمارے بارے میں',
// Property or array item — trailing comma allowed in TypeScript
title: 'مائرسٹا کی بہترین گوشت کی دکان',
// Property or array item — trailing comma allowed in TypeScript
p1: 'کوٹ گارڈ صرف ایک گوشت کی دکان نہیں — ہم معیار کا وعدہ ہیں۔ ہمارا تمام گوشت 100% حلال تصدیق شدہ ہے اور روزانہ تازہ پہنچایا جاتا ہے۔',
// Property or array item — trailing comma allowed in TypeScript
p2: 'ہم آئرلینڈ اور نیوزی لینڈ سے بکرے کا گوشت، معتبر پیدا کنندگان سے مرغی اور گائے کا گوشت حاصل کرتے ہیں، اور آپ کو رات کے کھانے، تقریب یا اتوار کی روست کے لیے صحیح کٹ تلاش کرنے میں مدد کرتے ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
p3: 'Tingvallavägen پر ہماری دکان میں آئیں، بتائیں آپ کیا تلاش کر رہے ہیں — ہم آپ کی خواہش کے مطابق کاٹتے اور پیک کرتے ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
statHalal: '100%',
// Property or array item — trailing comma allowed in TypeScript
statHalalLabel: 'حلال تصدیق شدہ',
// Property or array item — trailing comma allowed in TypeScript
statDays: '7 دن',
// Property or array item — trailing comma allowed in TypeScript
statDaysLabel: 'ہفتے میں کھلا',
// Property or array item — trailing comma allowed in TypeScript
statDelivery: 'روزانہ',
// Property or array item — trailing comma allowed in TypeScript
statDeliveryLabel: 'ڈیلیوری',
// Property or array item — trailing comma allowed in TypeScript
statFresh: 'تازہ',
// Property or array item — trailing comma allowed in TypeScript
statFreshLabel: 'ہر دن',
// Property or array item — trailing comma allowed in TypeScript
readMore: 'مزید پڑھیں',
// Property or array item — trailing comma allowed in TypeScript
imageAlt: 'کوٹ گارڈ سے تازہ گوشت کی کٹس',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 70: trust: {
trust: {
// Property or array item — trailing comma allowed in TypeScript
halal: '100% حلال',
// Property or array item — trailing comma allowed in TypeScript
halalDesc: 'مکمل سراغ رسانی اور تعمیل کے ساتھ تصدیق شدہ حلال ذرائع۔',
// Property or array item — trailing comma allowed in TypeScript
fresh: 'روزانہ تازہ',
// Property or array item — trailing comma allowed in TypeScript
freshDesc: 'ہر صبح تازہ حاصل کیا جاتا ہے اور بہترین حالت میں پہنچایا جاتا ہے۔',
// Property or array item — trailing comma allowed in TypeScript
premium: 'پریمیم معیار',
// Property or array item — trailing comma allowed in TypeScript
premiumDesc: 'قابل اعتماد فارموں سے منتخب کردہ کٹس، ماہر قصابوں کے ذریعے تیار۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 78: categories: {
categories: {
// Property or array item — trailing comma allowed in TypeScript
title: 'زمرے کے لحاظ سے خریداری',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'روزمرہ کی مرغی سے لے کر پریمیم بکرے کے کٹlets تک — ہر کٹ آپ کی پسند کے مطابق۔',
// Property or array item — trailing comma allowed in TypeScript
shop: '{name} خریدیں',
// Line 83: chicken: {
chicken: {
// Property or array item — trailing comma allowed in TypeScript
name: 'مرغی',
// Property or array item — trailing comma allowed in TypeScript
description: 'فارم سے تازہ حلال مرغی، آپ کی پسند کے مطابق کاٹی گئی',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 87: beef: {
beef: {
// Property or array item — trailing comma allowed in TypeScript
name: 'گائے کا گوشت',
// Property or array item — trailing comma allowed in TypeScript
description: 'روایتی اور جدید کٹس میں پریمیم حلال گائے کا گوشت',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 91: lamb: {
lamb: {
// Property or array item — trailing comma allowed in TypeScript
name: 'بکرے کا گوشت',
// Property or array item — trailing comma allowed in TypeScript
description: 'ہر موقع کے لیے نرم حلال بکرے کا گوشت',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 95: fish: {
fish: {
// Property or array item — trailing comma allowed in TypeScript
name: 'مچھلی',
// Property or array item — trailing comma allowed in TypeScript
description: 'تازہ پکڑ، صاف کی گئی اور پکانے کے لیے تیار',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 100: featured: {
featured: {
// Property or array item — trailing comma allowed in TypeScript
label: 'منتخب مجموعہ',
// Property or array item — trailing comma allowed in TypeScript
title: 'نمایاں مصنوعات',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'ہماری سب سے مقبول کٹس، شہر بھر کے خاندانوں کی پسندیدہ۔',
// Property or array item — trailing comma allowed in TypeScript
viewAll: 'سب دیکھیں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 106: howItWorks: {
howItWorks: {
// Property or array item — trailing comma allowed in TypeScript
label: 'آرڈر',
// Property or array item — trailing comma allowed in TypeScript
title: 'آرڈر کرنا کتنا آسان ہے',
// Property or array item — trailing comma allowed in TypeScript
step1Title: 'ہم سے رابطہ کریں',
// Line 110: step1Desc:
step1Desc:
// Property or array item — trailing comma allowed in TypeScript
'واٹس ایپ پر پیغام بھیجیں کہ آپ کیا چاہتے ہیں — ہم جلدی جواب دیتے ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
step2Title: 'ہم تصدیق کرتے ہیں',
// Line 113: step2Desc:
step2Desc:
// Property or array item — trailing comma allowed in TypeScript
'ہم آپ کے آرڈر کی تصدیق کرتے ہیں، قیمت بتاتے ہیں اور بتاتے ہیں کہ کب تیار ہوگا۔',
// Property or array item — trailing comma allowed in TypeScript
step3Title: 'وصول یا ڈیلیوری',
// Line 116: step3Desc:
step3Desc:
// Property or array item — trailing comma allowed in TypeScript
'Tingvallavägen 11 پر دکان سے وصول کریں یا گھر کی ڈیلیوری منتخب کریں۔',
// Property or array item — trailing comma allowed in TypeScript
step: 'مرحلہ {n}',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'واٹس ایپ پر آرڈر کریں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 121: offers: {
offers: {
// Property or array item — trailing comma allowed in TypeScript
label: 'آرڈر',
// Property or array item — trailing comma allowed in TypeScript
title: 'ہفتہ وار پیشکشیں',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'ہم مسلسل تازہ پیشکشوں کے ساتھ اپ ڈیٹ کرتے ہیں۔ تازہ ترین قیمتوں کے لیے سوشل میڈیا پر فالو کریں۔',
// Property or array item — trailing comma allowed in TypeScript
disclaimer: 'قیمت اس وقت تک جب تک اسٹاک موجود ہے',
// Property or array item — trailing comma allowed in TypeScript
was: 'پہلے',
// Property or array item — trailing comma allowed in TypeScript
now: 'اب',
// Property or array item — trailing comma allowed in TypeScript
order: 'آرڈر',
// Property or array item — trailing comma allowed in TypeScript
viewProduct: 'مصنوعات دیکھیں',
// Line 131: badge: {
badge: {
// Property or array item — trailing comma allowed in TypeScript
fresh: 'تازہ',
// Property or array item — trailing comma allowed in TypeScript
halal: 'حلال',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 135: items: {
items: {
// Property or array item — trailing comma allowed in TypeScript
chickenWings: { name: 'تازہ چکن ونگز PL' },
// Property or array item — trailing comma allowed in TypeScript
lambSteak: { name: 'تازہ لیمب روست آئرلینڈ' },
// Property or array item — trailing comma allowed in TypeScript
beefMince: { name: 'بیف منس 5% چکنائی IRL' },
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 141: social: {
social: {
// Property or array item — trailing comma allowed in TypeScript
label: 'ہمیں فالو کریں',
// Property or array item — trailing comma allowed in TypeScript
title: 'ہمیں فالو کریں',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'فیس بک پر 1,100+ فالوورز · 249 پوسٹس · روزانہ اپ ڈیٹس',
// Property or array item — trailing comma allowed in TypeScript
facebook: 'فیس بک',
// Property or array item — trailing comma allowed in TypeScript
instagram: 'انسٹاگرام',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'واٹس ایپ',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 150: contact: {
contact: {
// Property or array item — trailing comma allowed in TypeScript
label: 'رابطہ',
// Property or array item — trailing comma allowed in TypeScript
title: 'ہم سے ملیں',
// Property or array item — trailing comma allowed in TypeScript
addressLabel: 'پتہ',
// Property or array item — trailing comma allowed in TypeScript
phoneLabel: 'فون',
// Property or array item — trailing comma allowed in TypeScript
hoursLabel: 'اوقات',
// Property or array item — trailing comma allowed in TypeScript
hoursValue: 'ہر دن: {hours}',
// Property or array item — trailing comma allowed in TypeScript
writeUs: 'ہمیں لکھیں',
// Property or array item — trailing comma allowed in TypeScript
callUs: 'ہمیں کال کریں',
// Property or array item — trailing comma allowed in TypeScript
openMaps: 'گوگل میپس میں کھولیں',
// Property or array item — trailing comma allowed in TypeScript
learnMore: 'رابطے کی تفصیلات',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 162: cta: {
cta: {
// Property or array item — trailing comma allowed in TypeScript
title: 'پریمیم حلال گوشت کے لیے تیار ہیں؟',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'آج ہی آرڈر کریں اور حقیقی تازہ، حسب ضرورت حلال گوشت کا فرق محسوس کریں جو آپ کے دروازے تک پہنچایا جائے۔',
// Property or array item — trailing comma allowed in TypeScript
button: 'ہمارا مجموعہ دیکھیں',
// Property or array item — trailing comma allowed in TypeScript
whatsapp: 'واٹس ایپ پر آرڈر کریں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 169: shop: {
shop: {
// Property or array item — trailing comma allowed in TypeScript
title: 'تمام مصنوعات',
// Property or array item — trailing comma allowed in TypeScript
subtitle: 'پریمیم حلال گوشت، آپ کی پسند کے مطابق',
// Property or array item — trailing comma allowed in TypeScript
noProducts: 'کوئی مصنوعات نہیں ملیں',
// Property or array item — trailing comma allowed in TypeScript
noProductsHint: 'اپنے فلٹرز یا تلاش کی کوشش کو ایڈجسٹ کریں',
// Property or array item — trailing comma allowed in TypeScript
filters: 'فلٹرز',
// Property or array item — trailing comma allowed in TypeScript
productsFound: '{count} مصنوعات ملیں',
// Property or array item — trailing comma allowed in TypeScript
search: 'تلاش',
// Property or array item — trailing comma allowed in TypeScript
searchPlaceholder: 'مصنوعات تلاش کریں...',
// Property or array item — trailing comma allowed in TypeScript
category: 'زمرہ',
// Property or array item — trailing comma allowed in TypeScript
sortBy: 'ترتیب دیں',
// Property or array item — trailing comma allowed in TypeScript
all: 'سب',
// Property or array item — trailing comma allowed in TypeScript
sortFeatured: 'نمایاں',
// Property or array item — trailing comma allowed in TypeScript
sortPriceAsc: 'قیمت: کم سے زیادہ',
// Property or array item — trailing comma allowed in TypeScript
sortPriceDesc: 'قیمت: زیادہ سے کم',
// Property or array item — trailing comma allowed in TypeScript
sortName: 'نام الف سے ی',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 186: product: {
product: {
// Property or array item — trailing comma allowed in TypeScript
backToShop: 'خریداری پر واپس',
// Property or array item — trailing comma allowed in TypeScript
inStock: 'دستیاب',
// Property or array item — trailing comma allowed in TypeScript
outOfStock: 'اسٹاک ختم',
// Property or array item — trailing comma allowed in TypeScript
halalTrust: '100% حلال تصدیق شدہ · روزانہ تازہ · پریمیم معیار',
// Property or array item — trailing comma allowed in TypeScript
yourSelection: 'آپ کا انتخاب',
// Property or array item — trailing comma allowed in TypeScript
aboutProduct: 'اس مصنوع کے بارے میں',
// Property or array item — trailing comma allowed in TypeScript
addToCart: 'ٹوکری میں شامل کریں',
// Property or array item — trailing comma allowed in TypeScript
addedToCart: 'ٹوکری میں شامل ہو گیا',
// Property or array item — trailing comma allowed in TypeScript
decreaseQty: 'مقدار کم کریں',
// Property or array item — trailing comma allowed in TypeScript
increaseQty: 'مقدار بڑھائیں',
// Property or array item — trailing comma allowed in TypeScript
removeWishlist: 'پسندیدہ سے ہٹائیں',
// Property or array item — trailing comma allowed in TypeScript
addWishlist: 'پسندیدہ میں شامل کریں',
// Property or array item — trailing comma allowed in TypeScript
viewProduct: '{name} دیکھیں',
// Property or array item — trailing comma allowed in TypeScript
pieces: '{count} ٹکڑے',
// Property or array item — trailing comma allowed in TypeScript
standardCut: 'معیاری کٹ',
// Property or array item — trailing comma allowed in TypeScript
howManyCuts: 'آپ کتنے کٹ چاہتے ہیں؟',
// Property or array item — trailing comma allowed in TypeScript
howManyCutsHint: 'اپنے آرڈر کے لیے کٹوں کی تعداد منتخب کریں',
// Property or array item — trailing comma allowed in TypeScript
cutsAndStyle: '{cuts} کٹ · {style}',
// Property or array item — trailing comma allowed in TypeScript
selectCutting: 'کاٹنے کا انداز منتخب کریں',
// Property or array item — trailing comma allowed in TypeScript
selectCuttingHint: 'ہمارے قصاب آپ کے {category} کو بالکل آپ کی پسند کے مطابق تیار کریں گے',
// Line 207: fishNote:
fishNote:
// Property or array item — trailing comma allowed in TypeScript
'مچھلی کی مصنوعات ہمارے معیاری پیشہ ورانہ کٹ سے تیار کی جاتی ہیں — صاف، چھلکے اتارے اور پکانے کے لیے تیار۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 210: cutting: {
cutting: {
// Property or array item — trailing comma allowed in TypeScript
nihari: 'نہاری کٹ',
// Property or array item — trailing comma allowed in TypeScript
karahi: 'کڑاہی کٹ',
// Property or array item — trailing comma allowed in TypeScript
qeema: 'قیمہ (کима)',
// Property or array item — trailing comma allowed in TypeScript
boneless: 'بغیر ہڈی',
// Property or array item — trailing comma allowed in TypeScript
steak: 'اسٹیک کٹ',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 217: priceUnit: {
priceUnit: {
// Property or array item — trailing comma allowed in TypeScript
perBird: 'فی مرغی',
// Property or array item — trailing comma allowed in TypeScript
perPack: 'فی پیک',
// Property or array item — trailing comma allowed in TypeScript
perKg: 'فی کلو',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 222: badges: {
badges: {
// Property or array item — trailing comma allowed in TypeScript
bestseller: 'سب سے زیادہ فروخت',
// Property or array item — trailing comma allowed in TypeScript
chefsPick: 'شیف کی پسند',
// Property or array item — trailing comma allowed in TypeScript
premium: 'پریمیم',
// Property or array item — trailing comma allowed in TypeScript
popular: 'مقبول',
// Property or array item — trailing comma allowed in TypeScript
freshCatch: 'تازہ پکڑ',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 229: products: {
products: {
// Line 230: 'chicken-whole': {
'chicken-whole': {
// Property or array item — trailing comma allowed in TypeScript
name: 'پوری مرغی',
// Property or array item — trailing comma allowed in TypeScript
description: 'فارم سے تازہ پوری حلال مرغی، بھوننے یا کڑی کے لیے بہترین۔',
// Line 233: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'ہماری پوری مرغیاں تصدیق شدہ حلال فارموں سے حاصل کی جاتی ہیں اور بہترین تازگی میں پہنچائی جاتی ہیں۔ ہر پرندہ معیار کے لیے ہاتھ سے منتخب کیا جاتا ہے، نرم گوشت اور صاف پروسیسنگ کے ساتھ۔ اپنی پسندیدہ ٹکڑوں کی تعداد منتخب کریں اور ہم اسے بالکل ویسے تیار کریں گے جیسا آپ چاہیں۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 236: 'chicken-breast': {
'chicken-breast': {
// Property or array item — trailing comma allowed in TypeScript
name: 'مرغی کا سینہ',
// Property or array item — trailing comma allowed in TypeScript
description: 'دبلا، بغیر ہڈی کا مرغی کا سینہ — گرل اور صحت مند کھانوں کے لیے بہترین۔',
// Line 239: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'پریمیم بغیر ہڈی کا مرغی کا سینہ، تراشا ہوا اور پکانے کے لیے تیار۔ کباب، سٹیر فرائی اور صحت مند ہفتے کی رات کے کھانوں کے لیے بہترین۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 242: 'chicken-thighs': {
'chicken-thighs': {
// Property or array item — trailing comma allowed in TypeScript
name: 'مرغی کی ران',
// Property or array item — trailing comma allowed in TypeScript
description: 'کڑی اور گرل کے لیے بھرپور ذائقے والی رس بھرے حلال مرغی کی ران۔',
// Line 245: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'ہماری مرغی کی ران اپنی رس اور گہرے ذائقے کے لیے مشہور ہے۔ چاہے روایتی کڑاہی بنائیں یا ہفتے کے آخر میں BBQ، یہ ران ہر بار بہترین نتائج دیتی ہے۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 248: 'chicken-wings': {
'chicken-wings': {
// Property or array item — trailing comma allowed in TypeScript
name: 'مرغی کے بازو',
// Property or array item — trailing comma allowed in TypeScript
description: 'تلی، بیکنگ یا گرل کے لیے پارٹی کے لیے تیار حلال مرغی کے بازو۔',
// Line 251: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'کرکرے، ذائقے دار مرغی کے بازو حلال طریقے سے تیار اور تازہ پہنچائے جاتے ہیں۔ میچ کی راتوں اور خاندانی محفلوں کی پسندیدہ۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 254: 'beef-nihari': {
'beef-nihari': {
// Property or array item — trailing comma allowed in TypeScript
name: 'نہاری کے لیے گائے کا گوشت',
// Property or array item — trailing comma allowed in TypeScript
description: 'آہستہ پکانے کے لیے تیار گائے کے گوشت کے کٹس، روایتی نہاری کے لیے بہترین۔',
// Line 257: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'خاص طور پر منتخب گائے کے گوشت کے کٹس جو آہستہ پکی ہوئی نہاری کے لیے بہترین ہیں۔ کولیجن اور ذائقے سے بھرپور، یہ کٹس گھنٹوں کی دھیمی آنچ پر خوبصورتی سے گل جاتے ہیں۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 260: 'beef-steak': {
'beef-steak': {
// Property or array item — trailing comma allowed in TypeScript
name: 'پریمیم گائے کا اسٹیک',
// Property or array item — trailing comma allowed in TypeScript
description: 'ریستوران معیار کے حلال اسٹیک کٹس، بہترین سیک کے لیے۔',
// Line 263: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'بہترین حلال ذرائع سے ہاتھ سے کاٹے گئے پریمیم اسٹیک۔ چربیلے، نرم اور آپ کی گرل یا کاسٹ آئرن پین کے لیے تیار۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 266: 'beef-mince': {
'beef-mince': {
// Property or array item — trailing comma allowed in TypeScript
name: 'گائے کا قیمہ',
// Property or array item — trailing comma allowed in TypeScript
description: 'کباب، برگر اور قیمہ کے لیے تازہ حلال گائے کا قیمہ۔',
// Line 269: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'باریک پیسا ہوا حلال گائے کا قیمہ بہترین چربی کے تناسب کے ساتھ رسیلے کباب، ذائقے دار قیمہ اور گھریلو برگرز کے لیے۔ روزانہ تازہ پیسا جاتا ہے۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 272: 'beef-boneless': {
'beef-boneless': {
// Property or array item — trailing comma allowed in TypeScript
name: 'بغیر ہڈی کے گائے کے مکعب',
// Property or array item — trailing comma allowed in TypeScript
description: 'کڑاہی، بریانی اور اسٹیو کے لیے کثیر الاستعمال بغیر ہڈی کے مکعب۔',
// Line 275: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'یکساں بغیر ہڈی کے گائے کے مکعب تیز پکوان کے لیے کامل سائز میں کاٹے گئے۔ کڑاہی، پلاؤ اور سٹیر فرائی کے لیے بہترین۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 278: 'lamb-shoulder': {
'lamb-shoulder': {
// Property or array item — trailing comma allowed in TypeScript
name: 'بکرے کا کندھا',
// Property or array item — trailing comma allowed in TypeScript
description: 'آہستہ بھوننے اور کڑی کے لیے بھرپور ذائقے والا بکرے کا کندھا۔',
// Line 281: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'خوبصورت چربی کے نمونے والے پریمیم حلال بکرے کا کندھا۔ آہستہ بھونی ہوئی دعوتوں، بھرپور کڑیوں اور روایتی خاندانی کھانوں کے لیے بہترین۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 284: 'lamb-leg': {
'lamb-leg': {
// Property or array item — trailing comma allowed in TypeScript
name: 'بکرے کی ٹانگ',
// Property or array item — trailing comma allowed in TypeScript
description: 'بھوننے، گرل اور خاص مواقع کے لیے نرم بکرے کی ٹانگ۔',
// Line 287: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'تصدیق شدہ حلال ذرائع سے پوری یا حصوں میں بکرے کی ٹانگ۔ عید کی تقریبات، ڈنر پارٹیوں اور اتوار کی بھوننے کے لیے مرکزی کٹ۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 290: 'lamb-chops': {
'lamb-chops': {
// Property or array item — trailing comma allowed in TypeScript
name: 'بکرے کے کٹlets',
// Property or array item — trailing comma allowed in TypeScript
description: 'گرل اور گھر میں فائن ڈائننگ کے لیے پریمیم بکرے کے کٹlets۔',
// Line 293: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'موٹے کاٹے گئے حلال بکرے کے کٹlets گرل کے لیے بہترین چربی کی تہہ کے ساتھ۔ ریستوران معیار، آپ کے باورچی خانے تک پہنچایا گیا۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 296: 'lamb-mince': {
'lamb-mince': {
// Property or array item — trailing comma allowed in TypeScript
name: 'بکرے کا قیمہ',
// Property or array item — trailing comma allowed in TypeScript
description: 'کباب، سموسے اور قیمہ کے لیے تازہ حلال بکرے کا قیمہ۔',
// Line 299: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'باریک پیسا ہوا بکرے کا قیمہ بھرپور ذائقے کے ساتھ۔ سیخ کباب، بکرے کا قیمہ اور بھرے ہوئے پراٹھوں کے لیے ضروری۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 302: 'fish-salmon': {
'fish-salmon': {
// Property or array item — trailing comma allowed in TypeScript
name: 'اٹلانٹک سامن فلیٹ',
// Property or array item — trailing comma allowed in TypeScript
description: 'مکھن جیسے نرم سامن فلیٹس، جلد سمیت اور پین میں سیکنے کے لیے تیار۔',
// Line 305: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'پریمیم اٹلانٹک سامن فلیٹس بھرپور اومیگا 3 کے ساتھ۔ صاف، حصوں میں تقسیم اور زیادہ سے زیادہ تازگی کے لیے ویکیوم سیلڈ۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 308: 'fish-rohu': {
'fish-rohu': {
// Property or array item — trailing comma allowed in TypeScript
name: 'روہو مچھلی',
// Property or array item — trailing comma allowed in TypeScript
description: 'پوری روہو مچھلی، صاف اور چھلکے اتارے — جنوبی ایشیائی پسندیدہ۔',
// Line 311: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'تازہ روہو مچھلی، جنوبی ایشیائی کھانوں کا ایک بنیادی جزو۔ صاف، چھلکے اتارے اور اندر صاف کی گئی۔ مچھلی کی کڑی، تلی ہوئی مچھلی اور روایتی ترکیبوں کے لیے بہترین۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 314: 'fish-prawns': {
'fish-prawns': {
// Property or array item — trailing comma allowed in TypeScript
name: 'بڑی جھینگے',
// Property or array item — trailing comma allowed in TypeScript
description: 'گرل، کڑی اور بریانی کے لیے بڑے چھلکے سمیت جھینگے۔',
// Line 317: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'پریمیم بڑی جھینگے، صاف کی گئی اور پکانے کے لیے تیار۔ میٹھا، مضبوط گوشت جو کڑی اور تندوری تیاریوں میں خوبصورتی سے کھڑا رہتا ہے۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 320: 'fish-basa': {
'fish-basa': {
// Property or array item — trailing comma allowed in TypeScript
name: 'باسا فلیٹ',
// Property or array item — trailing comma allowed in TypeScript
description: 'ہلکا، پرت دار باسا فلیٹس — ابتدائیوں اور بچوں کے لیے بہترین۔',
// Line 323: longDescription:
longDescription:
// Property or array item — trailing comma allowed in TypeScript
'بغیر ہڈی کے باسا فلیٹس ہلکے، نفیس ذائقے کے ساتھ۔ پکانا آسان اور کثیر الاستعمال — مچھلی کے ٹیکو، بیکنگ اور ہلکی کڑیوں کے لیے بہترین۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
},
// Line 327: cart: {
cart: {
// Property or array item — trailing comma allowed in TypeScript
title: 'آپ کی ٹوکری',
// Property or array item — trailing comma allowed in TypeScript
itemsCount: 'آپ کی ٹوکری میں {count} آئٹم',
// Property or array item — trailing comma allowed in TypeScript
empty: 'آپ کی ٹوکری خالی ہے',
// Property or array item — trailing comma allowed in TypeScript
emptyHint: 'ہمارے پریمیم حلال مجموعے میں سے دیکھیں اور آئٹمز شامل کریں۔',
// Property or array item — trailing comma allowed in TypeScript
startShopping: 'خریداری شروع کریں',
// Property or array item — trailing comma allowed in TypeScript
customization: 'حسب ضرورت:',
// Property or array item — trailing comma allowed in TypeScript
orderSummary: 'آرڈر کا خلاصہ',
// Property or array item — trailing comma allowed in TypeScript
subtotal: 'ذیلی کل',
// Property or array item — trailing comma allowed in TypeScript
delivery: 'ڈیلیوری',
// Property or array item — trailing comma allowed in TypeScript
free: 'مفت',
// Property or array item — trailing comma allowed in TypeScript
freeDeliveryHint: '500 کرون سے زیادہ کے آرڈرز پر مفت ڈیلیوری',
// Property or array item — trailing comma allowed in TypeScript
total: 'کل',
// Property or array item — trailing comma allowed in TypeScript
proceedCheckout: 'چیک آؤٹ پر جائیں',
// Property or array item — trailing comma allowed in TypeScript
continueShopping: 'خریداری جاری رکھیں',
// Property or array item — trailing comma allowed in TypeScript
removeItem: 'آئٹم ہٹائیں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 344: checkout: {
checkout: {
// Property or array item — trailing comma allowed in TypeScript
title: 'محفوظ چیک آؤٹ',
// Property or array item — trailing comma allowed in TypeScript
backToCart: 'ٹوکری پر واپس',
// Property or array item — trailing comma allowed in TypeScript
noItems: 'چیک آؤٹ کے لیے کوئی آئٹم نہیں',
// Property or array item — trailing comma allowed in TypeScript
goToShop: 'خریداری پر جائیں',
// Property or array item — trailing comma allowed in TypeScript
orderConfirmed: 'آرڈر کی تصدیق ہو گئی!',
// Property or array item — trailing comma allowed in TypeScript
thankYou: 'آپ کے آرڈر کا شکریہ۔ آپ کا پریمیم حلال گوشت تیار کیا جا رہا ہے۔',
// Property or array item — trailing comma allowed in TypeScript
orderId: 'آرڈر آئی ڈی: {id}',
// Property or array item — trailing comma allowed in TypeScript
viewOrders: 'آرڈرز دیکھیں',
// Property or array item — trailing comma allowed in TypeScript
haveAccount: 'اکاؤنٹ ہے؟',
// Property or array item — trailing comma allowed in TypeScript
signIn: 'سائن ان کریں',
// Property or array item — trailing comma allowed in TypeScript
fasterCheckout: 'تیز چیک آؤٹ کے لیے۔',
// Property or array item — trailing comma allowed in TypeScript
deliveryDetails: 'ڈیلیوری کی تفصیلات',
// Property or array item — trailing comma allowed in TypeScript
fullName: 'پورا نام',
// Property or array item — trailing comma allowed in TypeScript
email: 'ای میل',
// Property or array item — trailing comma allowed in TypeScript
phone: 'فون',
// Property or array item — trailing comma allowed in TypeScript
street: 'گلی کا پتہ',
// Property or array item — trailing comma allowed in TypeScript
city: 'شہر',
// Property or array item — trailing comma allowed in TypeScript
state: 'صوبہ',
// Property or array item — trailing comma allowed in TypeScript
zip: 'پوسٹل کوڈ',
// Property or array item — trailing comma allowed in TypeScript
payment: 'ادائیگی',
// Property or array item — trailing comma allowed in TypeScript
creditCard: 'کریڈٹ کارڈ',
// Property or array item — trailing comma allowed in TypeScript
cashOnDelivery: 'ڈیلیوری پر نقد',
// Property or array item — trailing comma allowed in TypeScript
cardNumber: 'کارڈ نمبر',
// Property or array item — trailing comma allowed in TypeScript
expiry: 'میعاد',
// Property or array item — trailing comma allowed in TypeScript
cvv: 'CVV',
// Property or array item — trailing comma allowed in TypeScript
qty: 'مقدار: {count}',
// Property or array item — trailing comma allowed in TypeScript
processing: 'پروسیسنگ...',
// Property or array item — trailing comma allowed in TypeScript
pay: '{amount} ادا کریں',
// Property or array item — trailing comma allowed in TypeScript
secure: 'محفوظ 256 بٹ SSL انکرپشن',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 375: auth: {
auth: {
// Property or array item — trailing comma allowed in TypeScript
welcomeBack: 'خوش آمدید',
// Property or array item — trailing comma allowed in TypeScript
createAccount: 'اکاؤنٹ بنائیں',
// Property or array item — trailing comma allowed in TypeScript
joinTagline: 'پریمیم شاپنگ کے تجربے کے لیے {name} میں شامل ہوں',
// Property or array item — trailing comma allowed in TypeScript
signInTagline: 'اپنے {name} اکاؤنٹ میں سائن ان کریں',
// Property or array item — trailing comma allowed in TypeScript
fullName: 'پورا نام',
// Property or array item — trailing comma allowed in TypeScript
email: 'ای میل',
// Property or array item — trailing comma allowed in TypeScript
password: 'پاس ورڈ',
// Property or array item — trailing comma allowed in TypeScript
phone: 'فون',
// Property or array item — trailing comma allowed in TypeScript
street: 'گلی کا پتہ',
// Property or array item — trailing comma allowed in TypeScript
city: 'شہر',
// Property or array item — trailing comma allowed in TypeScript
state: 'صوبہ',
// Property or array item — trailing comma allowed in TypeScript
zip: 'پوسٹل کوڈ',
// Property or array item — trailing comma allowed in TypeScript
signIn: 'سائن ان',
// Property or array item — trailing comma allowed in TypeScript
register: 'اکاؤنٹ بنائیں',
// Property or array item — trailing comma allowed in TypeScript
hasAccount: 'پہلے سے اکاؤنٹ ہے؟ سائن ان کریں',
// Property or array item — trailing comma allowed in TypeScript
noAccount: 'اکاؤنٹ نہیں ہے؟ رجسٹر کریں',
// Property or array item — trailing comma allowed in TypeScript
invalidCredentials: 'غلط ای میل یا پاس ورڈ۔ {email} / demo123 آزمائیں',
// Property or array item — trailing comma allowed in TypeScript
demo: 'ڈیمو: {email} / demo123',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 395: account: {
account: {
// Property or array item — trailing comma allowed in TypeScript
title: 'میرا اکاؤنٹ',
// Property or array item — trailing comma allowed in TypeScript
welcome: 'خوش آمدید، {name}',
// Property or array item — trailing comma allowed in TypeScript
memberSince: '{date} سے رکن',
// Property or array item — trailing comma allowed in TypeScript
myWishlist: 'میری پسندیدہ فہرست',
// Property or array item — trailing comma allowed in TypeScript
signOut: 'سائن آؤٹ',
// Property or array item — trailing comma allowed in TypeScript
orderHistory: 'آرڈر کی تاریخ',
// Property or array item — trailing comma allowed in TypeScript
noOrders: 'ابھی تک کوئی آرڈر نہیں',
// Property or array item — trailing comma allowed in TypeScript
startShopping: 'خریداری شروع کریں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 405: wishlist: {
wishlist: {
// Property or array item — trailing comma allowed in TypeScript
title: 'میری پسندیدہ فہرست',
// Property or array item — trailing comma allowed in TypeScript
saved: '{count} محفوظ آئٹم',
// Property or array item — trailing comma allowed in TypeScript
empty: 'آپ کی پسندیدہ فہرست خالی ہے',
// Property or array item — trailing comma allowed in TypeScript
emptyHint: 'اپنی پسندیدہ مصنوعات محفوظ کریں تاکہ بعد میں خرید سکیں۔',
// Property or array item — trailing comma allowed in TypeScript
browse: 'مصنوعات دیکھیں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 412: about: {
about: {
// Property or array item — trailing comma allowed in TypeScript
title: '{name} کے بارے میں',
// Translated UI string for current language (sv / en / ur)
subtitle:
// Property or array item — trailing comma allowed in TypeScript
'پریمیم 100% حلال گوشت آپ کی میز تک — تازہ، حسب ضرورت اور احتیاط سے پہنچایا گیا۔',
// Property or array item — trailing comma allowed in TypeScript
ourStory: 'ہماری کہانی',
// Line 417: storyP1:
storyP1:
// Property or array item — trailing comma allowed in TypeScript
'{name} ایک سادہ مشن کے ساتھ قائم ہوا: ہر خاندان کے لیے پریمیم حلال گوشت قابل رسائی بنانا، معیار، تازگی یا مذہبی تعمیل سے سمجھوتہ کیے بغیر۔ ہم سمجھتے ہیں کہ بہت سے گھروں کے لیے صحیح کٹ صحیح طریقے سے تیار کرنا عیش نہیں — یہ ضروری ہے۔',
// Line 419: storyP2:
storyP2:
// Property or array item — trailing comma allowed in TypeScript
'مرغی کے لیے ٹکڑوں کی تعداد منتخب کرنے سے لے کر گائے اور بکرے کے گوشت کے لیے نہاری یا کڑاہی کٹس کا انتخاب کرنے تک، ہم ہر آرڈر کے مرکز میں حسب ضرورت بناتے ہیں۔ ہمارے ماہر قصاب ہر آرڈر ہاتھ سے تیار کرتے ہیں، اور ہماری درجہ حرارت کنٹرولڈ ڈیلیوری یقینی بناتی ہے کہ آپ کا گوشت اسی دن کی طرح تازہ پہنچے جیسے کاٹا گیا تھا۔',
// Property or array item — trailing comma allowed in TypeScript
halalTitle: 'حلال سرٹیفیکیشن',
// Line 422: halalDesc:
halalDesc:
// Property or array item — trailing comma allowed in TypeScript
'{name} کی ہر مصنوع تصدیق شدہ حلال سپلائرز سے حاصل کی جاتی ہے۔ ہمارا سپلائی چین مکمل طور پر قابل سراغ ہے، اور ہم حلال ذبح اور پروسیسنگ کے معیارات کی سخت تعمیل برقرار رکھتے ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
freshDaily: 'روزانہ تازہ',
// Property or array item — trailing comma allowed in TypeScript
freshDailyDesc: 'ہر صبح قابل اعتماد فارموں سے حاصل',
// Property or array item — trailing comma allowed in TypeScript
premiumQuality: 'پریمیم معیار',
// Property or array item — trailing comma allowed in TypeScript
premiumQualityDesc: 'ماہر قصابوں کے ذریعے منتخب کردہ کٹس',
// Property or array item — trailing comma allowed in TypeScript
fastDelivery: 'تیز ڈیلیوری',
// Property or array item — trailing comma allowed in TypeScript
fastDeliveryDesc: 'درجہ حرارت کنٹرولڈ اسی دن کی ڈیلیوری',
// Property or array item — trailing comma allowed in TypeScript
deliveryTitle: 'ڈیلیوری کی معلومات',
// Property or array item — trailing comma allowed in TypeScript
delivery1: 'ہم اپنی پروسیسنگ سہولت سے 40 کلومیٹر کے دائرے میں ڈیلیوری کرتے ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
delivery2: 'دوپہر 2 بجے سے پہلے کے آرڈرز اسی دن کی ڈیلیوری کے اہل ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
delivery3: '500 کرون سے زیادہ کے آرڈرز پر مفت ڈیلیوری۔ معیاری ڈیلیوری فیس: 49 کرون۔',
// Property or array item — trailing comma allowed in TypeScript
delivery4: 'تمام مصنوعات ویکیوم سیلڈ اور انسولیٹڈ پیکجنگ میں منتقل کی جاتی ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
contactTitle: 'ہم سے رابطہ کریں',
// Property or array item — trailing comma allowed in TypeScript
address: 'Tingvallavägen 11, 195 31 Märsta',
// Property or array item — trailing comma allowed in TypeScript
privacyTitle: 'رازداری کی پالیسی',
// Line 438: privacyText:
privacyText:
// Property or array item — trailing comma allowed in TypeScript
'ہم صرف وہ معلومات جمع کرتے ہیں جو آپ کے آرڈرز پر کارروائی کے لیے ضروری ہیں — نام، رابطے کی تفصیلات اور ڈیلیوری کا پتہ۔ ہم آپ کا ڈیٹا تیسرے فریق کو نہیں بیچتے۔ ادائیگی کی تفصیلات محفوظ طریقے سے ہمارے پارٹنرز کے ذریعے سنبھالی جاتی ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
termsTitle: 'سروس کی شرائط',
// Line 441: termsText:
termsText:
// Property or array item — trailing comma allowed in TypeScript
'تمام قیمتیں SEK میں دکھائی جاتی ہیں اور بغیر اطلاع کے تبدیل ہو سکتی ہیں۔ آرڈرز دستیابی پر منحصر ہیں۔ تمام گوشت کی مصنوعات پر حلال سرٹیفیکیشن لاگو ہوتا ہے۔ ڈیلیوری کے اوقات تخمینے ہیں اور مصروف اوقات میں مختلف ہو سکتے ہیں۔',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 444: footer: {
footer: {
// Line 445: tagline:
tagline:
// Property or array item — trailing comma allowed in TypeScript
'پریمیم 100% حلال گوشت ڈیلیوری۔ تازہ، حسب ضرورت کٹس آپ کے دروازے تک بغیر کسی سمجھوتے کے معیار کے ساتھ پہنچائی جاتی ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
shop: 'خریداری',
// Property or array item — trailing comma allowed in TypeScript
company: 'کمپنی',
// Property or array item — trailing comma allowed in TypeScript
contact: 'رابطہ',
// Property or array item — trailing comma allowed in TypeScript
rights: 'جملہ حقوق محفوظ ہیں۔',
// Property or array item — trailing comma allowed in TypeScript
phone: '072-585 50 50',
// Property or array item — trailing comma allowed in TypeScript
hours: 'ہر دن کھلا {hours}',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 454: notFound: {
notFound: {
// Property or array item — trailing comma allowed in TypeScript
title: '404',
// Property or array item — trailing comma allowed in TypeScript
message: 'صفحہ نہیں ملا',
// Property or array item — trailing comma allowed in TypeScript
goHome: 'ہوم پر جائیں',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 459: orderStatus: {
orderStatus: {
// Property or array item — trailing comma allowed in TypeScript
pending: 'زیر التوا',
// Property or array item — trailing comma allowed in TypeScript
confirmed: 'تصدیق شدہ',
// Property or array item — trailing comma allowed in TypeScript
preparing: 'تیاری میں',
// Property or array item — trailing comma allowed in TypeScript
'out-for-delivery': 'ڈیلیوری کے لیے روانہ',
// Property or array item — trailing comma allowed in TypeScript
delivered: 'پہنچا دیا گیا',
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
};
@@ -1,28 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/i18n/types.ts
* NOT used by the app — read this to learn how the real file works
*/
// Export TypeScript type — defines data shape used across the app
export type Locale = 'en' | 'sv' | 'ur';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const LOCALES: { code: Locale; label: string; nativeLabel: string }[] = [
// Property or array item — trailing comma allowed in TypeScript
{ code: 'sv', label: 'Swedish', nativeLabel: 'Svenska' },
// Property or array item — trailing comma allowed in TypeScript
{ code: 'en', label: 'English', nativeLabel: 'English' },
// Property or array item — trailing comma allowed in TypeScript
{ code: 'ur', label: 'Urdu', nativeLabel: 'اردو' },
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const DEFAULT_LOCALE: Locale = 'sv';
// (blank line — separates logical blocks for readability)
// Export TypeScript type — defines data shape used across the app
export interface TranslationDict {
// Line 12: [key: string]: string | TranslationDict;
[key: string]: string | TranslationDict;
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,74 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/constants.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { CutCount, CuttingStyleKey } from '@/types';
// Import from a relative file in the same project
import { IMAGES } from './images';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const CUT_COUNTS: CutCount[] = [4, 8, 10, 12];
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const CUTTING_STYLE_KEYS: CuttingStyleKey[] = [
// Property or array item — trailing comma allowed in TypeScript
'nihari',
// Property or array item — trailing comma allowed in TypeScript
'karahi',
// Property or array item — trailing comma allowed in TypeScript
'qeema',
// Property or array item — trailing comma allowed in TypeScript
'boneless',
// Property or array item — trailing comma allowed in TypeScript
'steak',
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const MEAT_CATEGORIES = ['chicken', 'beef', 'lamb'] as const;
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const CATEGORY_IDS = ['chicken', 'beef', 'lamb', 'fish'] as const;
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const CATEGORY_IMAGES = IMAGES.categories;
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const SITE_NAME = 'Kött Gård';
// Named export constant — shared config/data imported elsewhere
export const SITE_EMAIL = 'hello@kottgard.se';
// Named export constant — shared config/data imported elsewhere
export const DEMO_EMAIL = 'demo@kottgard.se';
// Named export constant — shared config/data imported elsewhere
export const SITE_PHONE = '+46 72 585 50 50';
// Named export constant — shared config/data imported elsewhere
export const SITE_PHONE_DISPLAY = '072-585 50 50';
// Named export constant — shared config/data imported elsewhere
export const SITE_ADDRESS = 'Tingvallavägen 11, 195 31 Märsta';
// Named export constant — shared config/data imported elsewhere
export const SITE_LOCATION = 'Märsta · Sverige';
// Named export constant — shared config/data imported elsewhere
export const SITE_HOURS = '10:0019:00';
// Named export constant — shared config/data imported elsewhere
export const FACEBOOK_URL = 'https://www.facebook.com/kottgard/';
// Named export constant — shared config/data imported elsewhere
export const INSTAGRAM_URL = 'https://www.instagram.com/kottgard';
// Named export constant — shared config/data imported elsewhere
export const MAPS_URL = 'https://share.google/fUkkhDNlhDTcImKo5';
// (blank line — separates logical blocks for readability)
// Line 32: const WHATSAPP_BASE = 'Hej Kött Gård! Jag vill beställa';
const WHATSAPP_BASE = 'Hej Kött Gård! Jag vill beställa';
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function whatsappOrderUrl(product?: string): string {
// Line 35: const text = product ? `${WHATSAPP_BASE} ${product}.` : `...
const text = product ? `${WHATSAPP_BASE} ${product}.` : `${WHATSAPP_BASE}.`;
// Return value from function
return `https://wa.me/46725855050?text=${encodeURIComponent(text)}`;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const WHATSAPP_URL = whatsappOrderUrl();
@@ -1,114 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/customization.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { Category, MeatCustomization, ProductCustomization } from '@/types';
// (blank line — separates logical blocks for readability)
// TypeScript type alias — union or shorthand for complex types
type Translator = (path: string, params?: Record<string, string | number>) => string;
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getDefaultCustomization(category: Category): ProductCustomization {
// Conditional branch — different behavior based on runtime value
if (category === 'fish') {
// Return value from function
return { type: 'fish' };
// Closing brace — end of block (function, if, object, JSX)
}
// Return value from function
return {
// Property or array item — trailing comma allowed in TypeScript
type: category,
// Property or array item — trailing comma allowed in TypeScript
cuts: 8,
// Property or array item — trailing comma allowed in TypeScript
cuttingStyle: 'karahi',
// Closing brace — end of block (function, if, object, JSX)
};
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getCustomizationKey(customization: ProductCustomization): string {
// Conditional branch — different behavior based on runtime value
if (customization.type === 'fish') {
// Return value from function
return 'standard';
// Closing brace — end of block (function, if, object, JSX)
}
// Return value from function
return `cuts-${customization.cuts}::${customization.cuttingStyle}`;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getCustomizationLabel(
// Property or array item — trailing comma allowed in TypeScript
customization: ProductCustomization,
// Line 25: t: Translator
t: Translator
// Line 26: ): string {
): string {
// Conditional branch — different behavior based on runtime value
if (customization.type === 'fish') {
// Return value from function
return t('product.standardCut');
// Closing brace — end of block (function, if, object, JSX)
}
// Return value from function
return t('product.cutsAndStyle', {
// Property or array item — trailing comma allowed in TypeScript
cuts: customization.cuts,
// Property or array item — trailing comma allowed in TypeScript
style: t(`cutting.${customization.cuttingStyle}`),
// Closing brace — end of block (function, if, object, JSX)
});
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getCartItemKey(
// Property or array item — trailing comma allowed in TypeScript
productId: string,
// Line 38: customization: ProductCustomization
customization: ProductCustomization
// Line 39: ): string {
): string {
// Return value from function
return `${productId}::${getCustomizationKey(customization)}`;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function updateMeatCustomization(
// Property or array item — trailing comma allowed in TypeScript
current: ProductCustomization,
// Property or array item — trailing comma allowed in TypeScript
category: Category,
// Line 46: update: Partial<Pick<MeatCustomization, 'cuts' | 'cutting...
update: Partial<Pick<MeatCustomization, 'cuts' | 'cuttingStyle'>>
// Line 47: ): MeatCustomization {
): MeatCustomization {
// Line 48: const base =
const base =
// Line 49: current.type !== 'fish' && current.type === category
current.type !== 'fish' && current.type === category
// Line 50: ? current
? current
// Line 51: : (getDefaultCustomization(category) as MeatCustomization);
: (getDefaultCustomization(category) as MeatCustomization);
// (blank line — separates logical blocks for readability)
// Return value from function
return {
// Property or array item — trailing comma allowed in TypeScript
type: category as MeatCustomization['type'],
// Property or array item — trailing comma allowed in TypeScript
cuts: update.cuts ?? base.cuts,
// Property or array item — trailing comma allowed in TypeScript
cuttingStyle: update.cuttingStyle ?? base.cuttingStyle,
// Closing brace — end of block (function, if, object, JSX)
};
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,128 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/demo-orders.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { Order } from '@/types';
// Import from a relative file in the same project
import { products } from './products';
// Import from a relative file in the same project
import { getDefaultCustomization, getCartItemKey } from './customization';
// (blank line — separates logical blocks for readability)
// Function declaration — reusable logic in this file
function orderItem(
// Property or array item — trailing comma allowed in TypeScript
productId: string,
// Property or array item — trailing comma allowed in TypeScript
quantity: number,
// Line 8: customizationLabel: string
customizationLabel: string
// Line 9: ) {
) {
// Array.find — get first matching item or undefined
const product = products.find((p) => p.id === productId)!;
// Line 11: const customization = getDefaultCustomization(product.cat...
const customization = getDefaultCustomization(product.category);
// Return value from function
return {
// Property or array item — trailing comma allowed in TypeScript
id: getCartItemKey(productId, customization),
// Property or array item — trailing comma allowed in TypeScript
product,
// Property or array item — trailing comma allowed in TypeScript
quantity,
// Property or array item — trailing comma allowed in TypeScript
customization,
// Property or array item — trailing comma allowed in TypeScript
customizationLabel,
// Closing brace — end of block (function, if, object, JSX)
};
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 21: const demoAddress = {
const demoAddress = {
// Property or array item — trailing comma allowed in TypeScript
street: 'Tingvallavägen 11',
// Property or array item — trailing comma allowed in TypeScript
city: 'Märsta',
// Property or array item — trailing comma allowed in TypeScript
state: 'Stockholm',
// Property or array item — trailing comma allowed in TypeScript
zip: '195 31',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const DEMO_ORDERS: Order[] = [
// Line 29: {
{
// Property or array item — trailing comma allowed in TypeScript
id: 'KG-2026-0042',
// Line 31: items: [
items: [
// Property or array item — trailing comma allowed in TypeScript
orderItem('beef-steak', 2, '8 cuts · Karahi'),
// Property or array item — trailing comma allowed in TypeScript
orderItem('lamb-shoulder', 1, '8 cuts · Nihari'),
// End of array literal
],
// Property or array item — trailing comma allowed in TypeScript
total: 647,
// Property or array item — trailing comma allowed in TypeScript
status: 'delivered',
// Property or array item — trailing comma allowed in TypeScript
createdAt: '2026-06-10T14:30:00.000Z',
// Property or array item — trailing comma allowed in TypeScript
deliveryAddress: demoAddress,
// Property or array item — trailing comma allowed in TypeScript
paymentMethod: 'card',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 41: {
{
// Property or array item — trailing comma allowed in TypeScript
id: 'KG-2026-0051',
// Line 43: items: [
items: [
// Property or array item — trailing comma allowed in TypeScript
orderItem('chicken-whole', 2, '8 cuts · Karahi'),
// Property or array item — trailing comma allowed in TypeScript
orderItem('beef-mince', 1, '8 cuts · Qeema'),
// Property or array item — trailing comma allowed in TypeScript
orderItem('fish-prawns', 1, 'Standard cut'),
// End of array literal
],
// Property or array item — trailing comma allowed in TypeScript
total: 556,
// Property or array item — trailing comma allowed in TypeScript
status: 'out-for-delivery',
// Property or array item — trailing comma allowed in TypeScript
createdAt: '2026-06-16T09:15:00.000Z',
// Property or array item — trailing comma allowed in TypeScript
deliveryAddress: demoAddress,
// Property or array item — trailing comma allowed in TypeScript
paymentMethod: 'swish',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 54: {
{
// Property or array item — trailing comma allowed in TypeScript
id: 'KG-2026-0058',
// Property or array item — trailing comma allowed in TypeScript
items: [orderItem('lamb-leg', 1, '8 cuts · Steak'), orderItem('fish-salmon', 1, 'Standard cut')],
// Property or array item — trailing comma allowed in TypeScript
total: 418,
// Property or array item — trailing comma allowed in TypeScript
status: 'preparing',
// Property or array item — trailing comma allowed in TypeScript
createdAt: '2026-06-17T08:00:00.000Z',
// Property or array item — trailing comma allowed in TypeScript
deliveryAddress: demoAddress,
// Property or array item — trailing comma allowed in TypeScript
paymentMethod: 'card',
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
];
-170
View File
@@ -1,170 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/images.ts
* NOT used by the app — read this to learn how the real file works
*/
// Block comment — documents the file or function below
/**
// Block comment — documents the file or function below
* All imagery: raw, fresh, uncooked meat & seafood only.
// Block comment — documents the file or function below
* High-quality Unsplash photos (verified URLs).
// Block comment — documents the file or function below
*/
// (blank line — separates logical blocks for readability)
// TypeScript type alias — union or shorthand for complex types
type UnsplashOptions = {
// Line 7: height?: number;
height?: number;
// Line 8: crop?: 'center' | 'top' | 'bottom' | 'entropy';
crop?: 'center' | 'top' | 'bottom' | 'entropy';
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function unsplashUrl(
// Property or array item — trailing comma allowed in TypeScript
path: string,
// Property or array item — trailing comma allowed in TypeScript
width: number,
// Line 14: options?: UnsplashOptions
options?: UnsplashOptions
// Line 15: ): string {
): string {
// Line 16: const id = path.startsWith('photo-') ? path : `photo-${pa...
const id = path.startsWith('photo-') ? path : `photo-${path}`;
// Line 17: const params = new URLSearchParams({
const params = new URLSearchParams({
// Property or array item — trailing comma allowed in TypeScript
auto: 'format',
// Property or array item — trailing comma allowed in TypeScript
fit: 'crop',
// Property or array item — trailing comma allowed in TypeScript
w: String(width),
// Property or array item — trailing comma allowed in TypeScript
q: '90',
// Closing brace — end of block (function, if, object, JSX)
});
// Conditional branch — different behavior based on runtime value
if (options?.height) params.set('h', String(options.height));
// Conditional branch — different behavior based on runtime value
if (options?.crop) params.set('crop', options.crop);
// Return value from function
return `https://images.unsplash.com/${id}?${params.toString()}`;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Block comment — documents the file or function below
/** Verified raw-meat & seafood Unsplash IDs */
// Line 29: const RAW = {
const RAW = {
// Property or array item — trailing comma allowed in TypeScript
butcherCounter: 'photo-1607623814075-e51df1bdc82f',
// Property or array item — trailing comma allowed in TypeScript
meatPrep: 'photo-1529692236671-f1f6cf9683ba',
// Property or array item — trailing comma allowed in TypeScript
chickenPieces: 'photo-1587593810167-a84920ea0781',
// Property or array item — trailing comma allowed in TypeScript
rawChicken: 'photo-1621996346565-e3dbc646d9a9',
// Property or array item — trailing comma allowed in TypeScript
beefSteaks: 'photo-1558030006-450675393462',
// Property or array item — trailing comma allowed in TypeScript
beefRibeye: 'photo-1559847844-5315695dadae',
// Property or array item — trailing comma allowed in TypeScript
lambRack: 'photo-1615937657715-bc7b4b7962c1',
// Property or array item — trailing comma allowed in TypeScript
beefCubes: 'photo-1546833999-b9f581a1996d',
// Property or array item — trailing comma allowed in TypeScript
beefMince: 'photo-1603048297172-c92544798d5a',
// Property or array item — trailing comma allowed in TypeScript
lambLeg: 'photo-1544025162-d76694265947',
// Property or array item — trailing comma allowed in TypeScript
lambChops: 'photo-1574672280600-4accfa5b6f98',
// Property or array item — trailing comma allowed in TypeScript
salmonFillet: 'photo-1544551763-46a013bb70d5',
// Property or array item — trailing comma allowed in TypeScript
rawPrawns: 'photo-1565680018434-b513d5e5fd47',
// Property or array item — trailing comma allowed in TypeScript
seafoodDisplay: 'photo-1504674900247-0877df9cc836',
// Closing brace — end of block (function, if, object, JSX)
} as const;
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const IMAGES = {
// Property or array item — trailing comma allowed in TypeScript
logo: '/images/kottgard-logo.jpeg',
// Property or array item — trailing comma allowed in TypeScript
hero: unsplashUrl(RAW.butcherCounter, 2560),
// Property or array item — trailing comma allowed in TypeScript
aboutMeat: unsplashUrl(RAW.meatPrep, 1920),
// Line 50: categories: {
categories: {
// Property or array item — trailing comma allowed in TypeScript
chicken: unsplashUrl(RAW.chickenPieces, 1400),
// Property or array item — trailing comma allowed in TypeScript
beef: unsplashUrl(RAW.beefSteaks, 1400),
// Property or array item — trailing comma allowed in TypeScript
lamb: unsplashUrl(RAW.lambRack, 1400),
// Property or array item — trailing comma allowed in TypeScript
fish: unsplashUrl(RAW.salmonFillet, 1400),
// Closing brace — end of block (function, if, object, JSX)
},
// Line 56: products: {
products: {
// Property or array item — trailing comma allowed in TypeScript
'chicken-whole': unsplashUrl(RAW.rawChicken, 1400),
// Line 58: 'chicken-breast': unsplashUrl(RAW.chickenPieces, 1400, {
'chicken-breast': unsplashUrl(RAW.chickenPieces, 1400, {
// Property or array item — trailing comma allowed in TypeScript
height: 1000,
// Property or array item — trailing comma allowed in TypeScript
crop: 'center',
// Closing brace — end of block (function, if, object, JSX)
}),
// Line 62: 'chicken-thighs': unsplashUrl(RAW.rawChicken, 1400, {
'chicken-thighs': unsplashUrl(RAW.rawChicken, 1400, {
// Property or array item — trailing comma allowed in TypeScript
height: 1100,
// Property or array item — trailing comma allowed in TypeScript
crop: 'entropy',
// Closing brace — end of block (function, if, object, JSX)
}),
// Line 66: 'chicken-wings': unsplashUrl(RAW.chickenPieces, 1400, {
'chicken-wings': unsplashUrl(RAW.chickenPieces, 1400, {
// Property or array item — trailing comma allowed in TypeScript
height: 900,
// Property or array item — trailing comma allowed in TypeScript
crop: 'top',
// Closing brace — end of block (function, if, object, JSX)
}),
// Property or array item — trailing comma allowed in TypeScript
'beef-nihari': unsplashUrl(RAW.beefCubes, 1400),
// Property or array item — trailing comma allowed in TypeScript
'beef-steak': unsplashUrl(RAW.beefRibeye, 1400),
// Property or array item — trailing comma allowed in TypeScript
'beef-mince': unsplashUrl(RAW.beefMince, 1400),
// Property or array item — trailing comma allowed in TypeScript
'beef-boneless': unsplashUrl(RAW.beefSteaks, 1400, { height: 1000, crop: 'center' }),
// Property or array item — trailing comma allowed in TypeScript
'lamb-shoulder': unsplashUrl(RAW.lambRack, 1400),
// Property or array item — trailing comma allowed in TypeScript
'lamb-leg': unsplashUrl(RAW.lambLeg, 1400),
// Property or array item — trailing comma allowed in TypeScript
'lamb-chops': unsplashUrl(RAW.lambChops, 1400),
// Property or array item — trailing comma allowed in TypeScript
'lamb-mince': unsplashUrl(RAW.lambChops, 1400, { height: 1000, crop: 'center' }),
// Property or array item — trailing comma allowed in TypeScript
'fish-salmon': unsplashUrl(RAW.salmonFillet, 1400),
// Property or array item — trailing comma allowed in TypeScript
'fish-rohu': unsplashUrl(RAW.seafoodDisplay, 1400, { height: 1100, crop: 'entropy' }),
// Property or array item — trailing comma allowed in TypeScript
'fish-prawns': unsplashUrl(RAW.rawPrawns, 1400),
// Property or array item — trailing comma allowed in TypeScript
'fish-basa': unsplashUrl(RAW.salmonFillet, 1400, { height: 900, crop: 'center' }),
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
} as const;
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const IMAGE_QUALITY = 90;
@@ -1,78 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/offers.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { WeeklyOffer } from '@/types';
// Import from a relative file in the same project
import { IMAGES } from './images';
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const weeklyOffers: WeeklyOffer[] = [
// Line 5: {
{
// Property or array item — trailing comma allowed in TypeScript
id: 'chicken-wings-pl',
// Property or array item — trailing comma allowed in TypeScript
nameKey: 'offers.items.chickenWings',
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'fresh',
// Property or array item — trailing comma allowed in TypeScript
price: 49.99,
// Property or array item — trailing comma allowed in TypeScript
originalPrice: 59.99,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: IMAGES.products['chicken-wings'],
// Property or array item — trailing comma allowed in TypeScript
whatsappProduct: 'Kycklingvingar färsk PL',
// Property or array item — trailing comma allowed in TypeScript
productSlug: 'chicken-wings',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 16: {
{
// Property or array item — trailing comma allowed in TypeScript
id: 'lamb-steak-ireland',
// Property or array item — trailing comma allowed in TypeScript
nameKey: 'offers.items.lambSteak',
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'halal',
// Property or array item — trailing comma allowed in TypeScript
price: 204.99,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: IMAGES.categories.lamb,
// Property or array item — trailing comma allowed in TypeScript
whatsappProduct: 'Lammstek färsk Ireland',
// Property or array item — trailing comma allowed in TypeScript
productSlug: 'lamb-leg',
// Closing brace — end of block (function, if, object, JSX)
},
// Line 26: {
{
// Property or array item — trailing comma allowed in TypeScript
id: 'beef-mince-irl',
// Property or array item — trailing comma allowed in TypeScript
nameKey: 'offers.items.beefMince',
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'fresh',
// Property or array item — trailing comma allowed in TypeScript
price: 139.99,
// Property or array item — trailing comma allowed in TypeScript
originalPrice: 159.99,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: IMAGES.products['beef-mince'],
// Property or array item — trailing comma allowed in TypeScript
whatsappProduct: 'Nötfärs 5% fett IRL',
// Property or array item — trailing comma allowed in TypeScript
productSlug: 'beef-mince',
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
];
@@ -1,88 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/product-i18n.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { Product } from '@/types';
// (blank line — separates logical blocks for readability)
// TypeScript type alias — union or shorthand for complex types
type Translator = (path: string, params?: Record<string, string | number>) => string;
// (blank line — separates logical blocks for readability)
// Export TypeScript type — defines data shape used across the app
export interface LocalizedProduct {
// Line 6: id: string;
id: string;
// Line 7: slug: string;
slug: string;
// Line 8: category: Product['category'];
category: Product['category'];
// Line 9: name: string;
name: string;
// Line 10: description: string;
description: string;
// Line 11: longDescription: string;
longDescription: string;
// Line 12: price: number;
price: number;
// Line 13: priceUnit: string;
priceUnit: string;
// Line 14: image: string;
image: string;
// Line 15: images: string[];
images: string[];
// Line 16: badge?: string;
badge?: string;
// Line 17: inStock: boolean;
inStock: boolean;
// Line 18: featured: boolean;
featured: boolean;
// Line 19: weight?: string;
weight?: string;
// Line 20: tags: string[];
tags: string[];
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function localizeProduct(product: Product, t: Translator): LocalizedProduct {
// Line 24: const base = `products.${product.id}`;
const base = `products.${product.id}`;
// (blank line — separates logical blocks for readability)
// Return value from function
return {
// Property or array item — trailing comma allowed in TypeScript
...product,
// Property or array item — trailing comma allowed in TypeScript
name: t(`${base}.name`),
// Property or array item — trailing comma allowed in TypeScript
description: t(`${base}.description`),
// Property or array item — trailing comma allowed in TypeScript
longDescription: t(`${base}.longDescription`),
// Property or array item — trailing comma allowed in TypeScript
priceUnit: t(`priceUnit.${product.priceUnitKey}`),
// Property or array item — trailing comma allowed in TypeScript
badge: product.badgeKey ? t(`badges.${product.badgeKey}`) : undefined,
// Closing brace — end of block (function, if, object, JSX)
};
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function localizeCategory(
// Property or array item — trailing comma allowed in TypeScript
categoryId: Product['category'],
// Line 38: t: Translator
t: Translator
// Line 39: ): { name: string; description: string } {
): { name: string; description: string } {
// Return value from function
return {
// Property or array item — trailing comma allowed in TypeScript
name: t(`categories.${categoryId}.name`),
// Property or array item — trailing comma allowed in TypeScript
description: t(`categories.${categoryId}.description`),
// Closing brace — end of block (function, if, object, JSX)
};
// Closing brace — end of block (function, if, object, JSX)
}
@@ -1,464 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/products.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import project module (@/ alias = src/ folder in tsconfig)
import { Product } from '@/types';
// Import from a relative file in the same project
import { IMAGES } from './images';
// (blank line — separates logical blocks for readability)
// Product catalog entry — demo data shown in shop and product pages
const P = IMAGES.products;
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const products: Product[] = [
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'chicken-whole',
// Property or array item — trailing comma allowed in TypeScript
slug: 'whole-chicken',
// Property or array item — trailing comma allowed in TypeScript
category: 'chicken',
// Property or array item — trailing comma allowed in TypeScript
price: 129,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perBird',
// Property or array item — trailing comma allowed in TypeScript
image: P['chicken-whole'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['chicken-whole'], P['chicken-breast']],
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'bestseller',
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '1.21.5 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'whole', 'popular'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'chicken-breast',
// Property or array item — trailing comma allowed in TypeScript
slug: 'chicken-breast',
// Property or array item — trailing comma allowed in TypeScript
category: 'chicken',
// Property or array item — trailing comma allowed in TypeScript
price: 99,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perPack',
// Property or array item — trailing comma allowed in TypeScript
image: P['chicken-breast'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['chicken-breast'], P['chicken-whole']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g1kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'boneless', 'lean'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'chicken-thighs',
// Property or array item — trailing comma allowed in TypeScript
slug: 'chicken-thighs',
// Property or array item — trailing comma allowed in TypeScript
category: 'chicken',
// Property or array item — trailing comma allowed in TypeScript
price: 85,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perPack',
// Property or array item — trailing comma allowed in TypeScript
image: P['chicken-thighs'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['chicken-thighs']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g1kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'juicy'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'chicken-wings',
// Property or array item — trailing comma allowed in TypeScript
slug: 'chicken-wings',
// Property or array item — trailing comma allowed in TypeScript
category: 'chicken',
// Property or array item — trailing comma allowed in TypeScript
price: 79,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perPack',
// Property or array item — trailing comma allowed in TypeScript
image: P['chicken-wings'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['chicken-wings']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'party'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'beef-nihari',
// Property or array item — trailing comma allowed in TypeScript
slug: 'beef-for-nihari',
// Property or array item — trailing comma allowed in TypeScript
category: 'beef',
// Property or array item — trailing comma allowed in TypeScript
price: 149,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['beef-nihari'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['beef-nihari'], P['beef-steak']],
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'chefsPick',
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '1 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'traditional'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'beef-steak',
// Property or array item — trailing comma allowed in TypeScript
slug: 'premium-beef-steak',
// Property or array item — trailing comma allowed in TypeScript
category: 'beef',
// Property or array item — trailing comma allowed in TypeScript
price: 229,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['beef-steak'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['beef-steak'], P['beef-nihari']],
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'premium',
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '1 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['premium', 'steak'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'beef-mince',
// Property or array item — trailing comma allowed in TypeScript
slug: 'beef-mince',
// Property or array item — trailing comma allowed in TypeScript
category: 'beef',
// Property or array item — trailing comma allowed in TypeScript
price: 119,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['beef-mince'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['beef-mince']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g1kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'mince'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'beef-boneless',
// Property or array item — trailing comma allowed in TypeScript
slug: 'boneless-beef-cubes',
// Property or array item — trailing comma allowed in TypeScript
category: 'beef',
// Property or array item — trailing comma allowed in TypeScript
price: 169,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['beef-boneless'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['beef-boneless']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '1 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'boneless'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'lamb-shoulder',
// Property or array item — trailing comma allowed in TypeScript
slug: 'lamb-shoulder',
// Property or array item — trailing comma allowed in TypeScript
category: 'lamb',
// Property or array item — trailing comma allowed in TypeScript
price: 189,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['lamb-shoulder'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['lamb-shoulder'], P['lamb-leg']],
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'popular',
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '1 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'traditional'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'lamb-leg',
// Property or array item — trailing comma allowed in TypeScript
slug: 'lamb-leg',
// Property or array item — trailing comma allowed in TypeScript
category: 'lamb',
// Property or array item — trailing comma allowed in TypeScript
price: 219,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['lamb-leg'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['lamb-leg']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '12 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['premium', 'roast'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'lamb-chops',
// Property or array item — trailing comma allowed in TypeScript
slug: 'lamb-chops',
// Property or array item — trailing comma allowed in TypeScript
category: 'lamb',
// Property or array item — trailing comma allowed in TypeScript
price: 249,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['lamb-chops'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['lamb-chops']],
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'premium',
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g',
// Property or array item — trailing comma allowed in TypeScript
tags: ['premium', 'grill'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'lamb-mince',
// Property or array item — trailing comma allowed in TypeScript
slug: 'lamb-mince',
// Property or array item — trailing comma allowed in TypeScript
category: 'lamb',
// Property or array item — trailing comma allowed in TypeScript
price: 159,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['lamb-mince'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['lamb-mince']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g1kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'mince'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'fish-salmon',
// Property or array item — trailing comma allowed in TypeScript
slug: 'atlantic-salmon-fillet',
// Property or array item — trailing comma allowed in TypeScript
category: 'fish',
// Property or array item — trailing comma allowed in TypeScript
price: 199,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['fish-salmon'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['fish-salmon'], P['fish-rohu']],
// Property or array item — trailing comma allowed in TypeScript
badgeKey: 'freshCatch',
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'fillet'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'fish-rohu',
// Property or array item — trailing comma allowed in TypeScript
slug: 'rohu-fish',
// Property or array item — trailing comma allowed in TypeScript
category: 'fish',
// Property or array item — trailing comma allowed in TypeScript
price: 119,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['fish-rohu'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['fish-rohu']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: true,
// Property or array item — trailing comma allowed in TypeScript
weight: '11.5 kg',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'whole'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'fish-prawns',
// Property or array item — trailing comma allowed in TypeScript
slug: 'jumbo-prawns',
// Property or array item — trailing comma allowed in TypeScript
category: 'fish',
// Property or array item — trailing comma allowed in TypeScript
price: 179,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['fish-prawns'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['fish-prawns']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'seafood'],
// Closing brace — end of block (function, if, object, JSX)
},
// Product catalog entry — demo data shown in shop and product pages
{
// Property or array item — trailing comma allowed in TypeScript
id: 'fish-basa',
// Property or array item — trailing comma allowed in TypeScript
slug: 'basa-fillet',
// Property or array item — trailing comma allowed in TypeScript
category: 'fish',
// Property or array item — trailing comma allowed in TypeScript
price: 109,
// Property or array item — trailing comma allowed in TypeScript
priceUnitKey: 'perKg',
// Property or array item — trailing comma allowed in TypeScript
image: P['fish-basa'],
// Property or array item — trailing comma allowed in TypeScript
images: [P['fish-basa']],
// Property or array item — trailing comma allowed in TypeScript
inStock: true,
// Property or array item — trailing comma allowed in TypeScript
featured: false,
// Property or array item — trailing comma allowed in TypeScript
weight: '500g',
// Property or array item — trailing comma allowed in TypeScript
tags: ['fresh', 'fillet', 'mild'],
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
];
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getProductBySlug(slug: string): Product | undefined {
// Return value from function
return products.find((p) => p.slug === slug);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getProductsByCategory(category: string): Product[] {
// Return value from function
return products.filter((p) => p.category === category);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getFeaturedProducts(): Product[] {
// Return value from function
return products.filter((p) => p.featured);
// Closing brace — end of block (function, if, object, JSX)
}
-59
View File
@@ -1,59 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/lib/utils.ts
* NOT used by the app — read this to learn how the real file works
*/
// Import external package or local module
import { clsx, type ClassValue } from 'clsx';
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function cn(...inputs: ClassValue[]) {
// Return value from function
return clsx(inputs);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function formatPrice(price: number, locale = 'sv-SE'): string {
// Return value from function
return new Intl.NumberFormat(locale, {
// Property or array item — trailing comma allowed in TypeScript
style: 'currency',
// Property or array item — trailing comma allowed in TypeScript
currency: 'SEK',
// Closing brace — end of block (function, if, object, JSX)
}).format(price);
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function getFormatLocale(locale: string): string {
// Line 15: const map: Record<string, string> = {
const map: Record<string, string> = {
// Property or array item — trailing comma allowed in TypeScript
en: 'en-US',
// Property or array item — trailing comma allowed in TypeScript
sv: 'sv-SE',
// Property or array item — trailing comma allowed in TypeScript
ur: 'ur-PK',
// Closing brace — end of block (function, if, object, JSX)
};
// Return value from function
return map[locale] ?? 'en-US';
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export — utility function other files can import
export function formatDate(date: string): string {
// Return value from function
return new Intl.DateTimeFormat('en-US', {
// Property or array item — trailing comma allowed in TypeScript
year: 'numeric',
// Property or array item — trailing comma allowed in TypeScript
month: 'long',
// Property or array item — trailing comma allowed in TypeScript
day: 'numeric',
// Closing brace — end of block (function, if, object, JSX)
}).format(new Date(date));
// Closing brace — end of block (function, if, object, JSX)
}
-179
View File
@@ -1,179 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/store/auth.ts
* NOT used by the app — read this to learn how the real file works
*/
// Zustand — simple global state store (cart, auth, locale)
import { create } from 'zustand';
// Zustand — simple global state store (cart, auth, locale)
import { persist } from 'zustand/middleware';
// Import project module (@/ alias = src/ folder in tsconfig)
import { Address, Order, User } from '@/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { DEMO_EMAIL } from '@/lib/constants';
// Import project module (@/ alias = src/ folder in tsconfig)
import { DEMO_ORDERS } from '@/lib/demo-orders';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface AuthState {
// Line 8: user: User | null;
user: User | null;
// Line 9: orders: Order[];
orders: Order[];
// Line 10: isAuthenticated: boolean;
isAuthenticated: boolean;
// Line 11: login: (email: string, password: string) => boolean;
login: (email: string, password: string) => boolean;
// Line 12: register: (data: {
register: (data: {
// Line 13: name: string;
name: string;
// Line 14: email: string;
email: string;
// Line 15: password: string;
password: string;
// Line 16: phone: string;
phone: string;
// Line 17: address: Address;
address: Address;
// Closing brace — end of block (function, if, object, JSX)
}) => boolean;
// Line 19: logout: () => void;
logout: () => void;
// Line 20: updateProfile: (data: Partial<User>) => void;
updateProfile: (data: Partial<User>) => void;
// Line 21: addOrder: (order: Order) => void;
addOrder: (order: Order) => void;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Line 24: const DEMO_USER: User = {
const DEMO_USER: User = {
// Property or array item — trailing comma allowed in TypeScript
id: 'demo-1',
// Property or array item — trailing comma allowed in TypeScript
name: 'Ahmed Khan',
// Property or array item — trailing comma allowed in TypeScript
email: DEMO_EMAIL,
// Property or array item — trailing comma allowed in TypeScript
phone: '+46 72 585 50 50',
// Line 29: address: {
address: {
// Property or array item — trailing comma allowed in TypeScript
street: 'Tingvallavägen 11',
// Property or array item — trailing comma allowed in TypeScript
city: 'Märsta',
// Property or array item — trailing comma allowed in TypeScript
state: 'Stockholm',
// Property or array item — trailing comma allowed in TypeScript
zip: '195 31',
// Closing brace — end of block (function, if, object, JSX)
},
// Property or array item — trailing comma allowed in TypeScript
createdAt: '2025-03-15T10:00:00.000Z',
// Closing brace — end of block (function, if, object, JSX)
};
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const useAuthStore = create<AuthState>()(
// Zustand persist — save store to localStorage between visits
persist(
// Line 40: (set, get) => ({
(set, get) => ({
// Property or array item — trailing comma allowed in TypeScript
user: null,
// Property or array item — trailing comma allowed in TypeScript
orders: [],
// Property or array item — trailing comma allowed in TypeScript
isAuthenticated: false,
// (blank line — separates logical blocks for readability)
// Line 45: login: (email, password) => {
login: (email, password) => {
// Conditional branch — different behavior based on runtime value
if (email === DEMO_EMAIL && password === 'demo123') {
// Line 47: const existingOrders = get().orders;
const existingOrders = get().orders;
// Line 48: set({
set({
// Property or array item — trailing comma allowed in TypeScript
user: DEMO_USER,
// Property or array item — trailing comma allowed in TypeScript
isAuthenticated: true,
// Property or array item — trailing comma allowed in TypeScript
orders: existingOrders.length > 0 ? existingOrders : DEMO_ORDERS,
// Closing brace — end of block (function, if, object, JSX)
});
// Return value from function
return true;
// Closing brace — end of block (function, if, object, JSX)
}
// Line 55: const stored = get().user;
const stored = get().user;
// Conditional branch — different behavior based on runtime value
if (stored && stored.email === email) {
// Line 57: set({ isAuthenticated: true });
set({ isAuthenticated: true });
// Return value from function
return true;
// Closing brace — end of block (function, if, object, JSX)
}
// Return value from function
return false;
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Line 63: register: (data) => {
register: (data) => {
// Line 64: const user: User = {
const user: User = {
// Property or array item — trailing comma allowed in TypeScript
id: `user-${Date.now()}`,
// Property or array item — trailing comma allowed in TypeScript
name: data.name,
// Property or array item — trailing comma allowed in TypeScript
email: data.email,
// Property or array item — trailing comma allowed in TypeScript
phone: data.phone,
// Property or array item — trailing comma allowed in TypeScript
address: data.address,
// Property or array item — trailing comma allowed in TypeScript
createdAt: new Date().toISOString(),
// Closing brace — end of block (function, if, object, JSX)
};
// Line 72: set({ user, isAuthenticated: true });
set({ user, isAuthenticated: true });
// Return value from function
return true;
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Property or array item — trailing comma allowed in TypeScript
logout: () => set({ isAuthenticated: false }),
// (blank line — separates logical blocks for readability)
// Line 78: updateProfile: (data) => {
updateProfile: (data) => {
// Line 79: const current = get().user;
const current = get().user;
// Conditional branch — different behavior based on runtime value
if (current) {
// Line 81: set({ user: { ...current, ...data } });
set({ user: { ...current, ...data } });
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Line 85: addOrder: (order) => {
addOrder: (order) => {
// Line 86: set({ orders: [order, ...get().orders] });
set({ orders: [order, ...get().orders] });
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
}),
// Line 89: { name: 'kott-gard-auth' }
{ name: 'kott-gard-auth' }
// Line 90: )
)
// Line 91: );
);
-164
View File
@@ -1,164 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/store/cart.ts
* NOT used by the app — read this to learn how the real file works
*/
// Zustand — simple global state store (cart, auth, locale)
import { create } from 'zustand';
// Zustand — simple global state store (cart, auth, locale)
import { persist } from 'zustand/middleware';
// Import project module (@/ alias = src/ folder in tsconfig)
import { CartItem, Product, ProductCustomization } from '@/types';
// Import project module (@/ alias = src/ folder in tsconfig)
import { getCartItemKey } from '@/lib/customization';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface CartState {
// Line 7: items: CartItem[];
items: CartItem[];
// Line 8: addItem: (
addItem: (
// Property or array item — trailing comma allowed in TypeScript
product: Product,
// Property or array item — trailing comma allowed in TypeScript
customization: ProductCustomization,
// Property or array item — trailing comma allowed in TypeScript
customizationLabel: string,
// Line 12: quantity?: number
quantity?: number
// Line 13: ) => void;
) => void;
// Line 14: removeItem: (id: string) => void;
removeItem: (id: string) => void;
// Line 15: updateQuantity: (id: string, quantity: number) => void;
updateQuantity: (id: string, quantity: number) => void;
// Line 16: clearCart: () => void;
clearCart: () => void;
// Line 17: getTotal: () => number;
getTotal: () => number;
// Line 18: getItemCount: () => number;
getItemCount: () => number;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const useCartStore = create<CartState>()(
// Zustand persist — save store to localStorage between visits
persist(
// Line 23: (set, get) => ({
(set, get) => ({
// Property or array item — trailing comma allowed in TypeScript
items: [],
// (blank line — separates logical blocks for readability)
// Line 26: addItem: (product, customization, customizationLabel, qua...
addItem: (product, customization, customizationLabel, quantity = 1) => {
// Line 27: const id = getCartItemKey(product.id, customization);
const id = getCartItemKey(product.id, customization);
// Line 28: const label = customizationLabel;
const label = customizationLabel;
// Array.find — get first matching item or undefined
const existing = get().items.find((item) => item.id === id);
// (blank line — separates logical blocks for readability)
// Conditional branch — different behavior based on runtime value
if (existing) {
// Line 32: set({
set({
// Array.map — transform each item (often render a list of components)
items: get().items.map((item) =>
// Line 34: item.id === id
item.id === id
// Line 35: ? { ...item, quantity: item.quantity + quantity }
? { ...item, quantity: item.quantity + quantity }
// Line 36: : item
: item
// Property or array item — trailing comma allowed in TypeScript
),
// Closing brace — end of block (function, if, object, JSX)
});
// Closing brace — end of block (function, if, object, JSX)
} else {
// Line 40: set({
set({
// Line 41: items: [
items: [
// Property or array item — trailing comma allowed in TypeScript
...get().items,
// Line 43: {
{
// Property or array item — trailing comma allowed in TypeScript
id,
// Property or array item — trailing comma allowed in TypeScript
product,
// Property or array item — trailing comma allowed in TypeScript
quantity,
// Property or array item — trailing comma allowed in TypeScript
customization,
// Property or array item — trailing comma allowed in TypeScript
customizationLabel: label,
// Closing brace — end of block (function, if, object, JSX)
},
// End of array literal
],
// Closing brace — end of block (function, if, object, JSX)
});
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Line 55: removeItem: (id) => {
removeItem: (id) => {
// Array.filter — keep items matching condition (search, category)
set({ items: get().items.filter((item) => item.id !== id) });
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Line 59: updateQuantity: (id, quantity) => {
updateQuantity: (id, quantity) => {
// Conditional branch — different behavior based on runtime value
if (quantity <= 0) {
// Line 61: get().removeItem(id);
get().removeItem(id);
// Line 62: return;
return;
// Closing brace — end of block (function, if, object, JSX)
}
// Line 64: set({
set({
// Array.map — transform each item (often render a list of components)
items: get().items.map((item) =>
// Line 66: item.id === id ? { ...item, quantity } : item
item.id === id ? { ...item, quantity } : item
// Property or array item — trailing comma allowed in TypeScript
),
// Closing brace — end of block (function, if, object, JSX)
});
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Property or array item — trailing comma allowed in TypeScript
clearCart: () => set({ items: [] }),
// (blank line — separates logical blocks for readability)
// Line 73: getTotal: () =>
getTotal: () =>
// Array.reduce — accumulate single value (cart total, item count)
get().items.reduce(
// Property or array item — trailing comma allowed in TypeScript
(sum, item) => sum + item.product.price * item.quantity,
// Line 76: 0
0
// Property or array item — trailing comma allowed in TypeScript
),
// (blank line — separates logical blocks for readability)
// Line 79: getItemCount: () =>
getItemCount: () =>
// Array.reduce — accumulate single value (cart total, item count)
get().items.reduce((sum, item) => sum + item.quantity, 0),
// Closing brace — end of block (function, if, object, JSX)
}),
// Line 82: { name: 'kott-gard-cart' }
{ name: 'kott-gard-cart' }
// Line 83: )
)
// Line 84: );
);
@@ -1,39 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/store/locale.ts
* NOT used by the app — read this to learn how the real file works
*/
// Zustand — simple global state store (cart, auth, locale)
import { create } from 'zustand';
// Zustand — simple global state store (cart, auth, locale)
import { persist } from 'zustand/middleware';
// Import project module (@/ alias = src/ folder in tsconfig)
import { DEFAULT_LOCALE, Locale } from '@/i18n/types';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface LocaleState {
// Line 6: locale: Locale;
locale: Locale;
// Line 7: setLocale: (locale: Locale) => void;
setLocale: (locale: Locale) => void;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const useLocaleStore = create<LocaleState>()(
// Zustand persist — save store to localStorage between visits
persist(
// Line 12: (set) => ({
(set) => ({
// Property or array item — trailing comma allowed in TypeScript
locale: DEFAULT_LOCALE,
// Property or array item — trailing comma allowed in TypeScript
setLocale: (locale) => set({ locale }),
// Closing brace — end of block (function, if, object, JSX)
}),
// Line 16: { name: 'kott-gard-locale' }
{ name: 'kott-gard-locale' }
// Line 17: )
)
// Line 18: );
);
@@ -1,81 +0,0 @@
/**
* ANNOTATED COPY — every line explained
* Source: src/store/wishlist.ts
* NOT used by the app — read this to learn how the real file works
*/
// Zustand — simple global state store (cart, auth, locale)
import { create } from 'zustand';
// Zustand — simple global state store (cart, auth, locale)
import { persist } from 'zustand/middleware';
// Import project module (@/ alias = src/ folder in tsconfig)
import { Product } from '@/types';
// (blank line — separates logical blocks for readability)
// TypeScript interface — contract for object properties and methods
interface WishlistState {
// Line 6: items: Product[];
items: Product[];
// Line 7: addItem: (product: Product) => void;
addItem: (product: Product) => void;
// Line 8: removeItem: (productId: string) => void;
removeItem: (productId: string) => void;
// Line 9: isInWishlist: (productId: string) => boolean;
isInWishlist: (productId: string) => boolean;
// Line 10: toggleItem: (product: Product) => void;
toggleItem: (product: Product) => void;
// Closing brace — end of block (function, if, object, JSX)
}
// (blank line — separates logical blocks for readability)
// Named export constant — shared config/data imported elsewhere
export const useWishlistStore = create<WishlistState>()(
// Zustand persist — save store to localStorage between visits
persist(
// Line 15: (set, get) => ({
(set, get) => ({
// Property or array item — trailing comma allowed in TypeScript
items: [],
// (blank line — separates logical blocks for readability)
// Line 18: addItem: (product) => {
addItem: (product) => {
// Conditional branch — different behavior based on runtime value
if (!get().isInWishlist(product.id)) {
// Line 20: set({ items: [...get().items, product] });
set({ items: [...get().items, product] });
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Line 24: removeItem: (productId) => {
removeItem: (productId) => {
// Array.filter — keep items matching condition (search, category)
set({ items: get().items.filter((p) => p.id !== productId) });
// Closing brace — end of block (function, if, object, JSX)
},
// (blank line — separates logical blocks for readability)
// Line 28: isInWishlist: (productId) =>
isInWishlist: (productId) =>
// Property or array item — trailing comma allowed in TypeScript
get().items.some((p) => p.id === productId),
// (blank line — separates logical blocks for readability)
// Line 31: toggleItem: (product) => {
toggleItem: (product) => {
// Conditional branch — different behavior based on runtime value
if (get().isInWishlist(product.id)) {
// Line 33: get().removeItem(product.id);
get().removeItem(product.id);
// Closing brace — end of block (function, if, object, JSX)
} else {
// Line 35: get().addItem(product);
get().addItem(product);
// Closing brace — end of block (function, if, object, JSX)
}
// Closing brace — end of block (function, if, object, JSX)
},
// Closing brace — end of block (function, if, object, JSX)
}),
// Line 39: { name: 'kott-gard-wishlist' }
{ name: 'kott-gard-wishlist' }
// Line 40: )
)
// Line 41: );
);

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