Replace entire repo content with kottgard-production-v1.1.zip (for shahikitchen-prod repo)
This commit is contained in:
+5
-26
@@ -1,14 +1,8 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
@@ -28,28 +22,13 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
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
|
||||
next-env.d.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
<!-- 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,100 +1,69 @@
|
||||
# Shahikitchen — Animated Restaurant Website
|
||||
# Kött Gård — Premium Halal Meat Delivery
|
||||
|
||||
This directory contains the official website for **Shahi Kitchen**, a popular Indian & Pakistani restaurant in Gothenburg (Göteborg), Sweden.
|
||||
A modern, premium e-commerce website for 100% Halal meat delivery. Built with Next.js, TypeScript, and Tailwind CSS.
|
||||
|
||||
**Domain**: [shahikitchen.se](https://shahikitchen.se)
|
||||
## Features
|
||||
|
||||
---
|
||||
- **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
|
||||
|
||||
## About Shahi Kitchen
|
||||
|
||||
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.
|
||||
|
||||
### Key Information
|
||||
|
||||
| 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) |
|
||||
|
||||
### Current Online Presence
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **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 (Planned)
|
||||
|
||||
```
|
||||
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/
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
### Getting Started
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) in your browser.
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
### Planned Features (in order)
|
||||
### Demo Account
|
||||
|
||||
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
|
||||
- Email: `demo@kottgard.se`
|
||||
- Password: `demo123`
|
||||
|
||||
---
|
||||
### Languages
|
||||
|
||||
## License
|
||||
Use the language bar or globe icon to switch between:
|
||||
- **Svenska** (Swedish) — default
|
||||
- **English**
|
||||
- **اردو** (Urdu) — RTL
|
||||
- **العربية** (Arabic) — RTL
|
||||
- **فارسی** (Persian) — RTL
|
||||
- **Türkçe** (Turkish)
|
||||
|
||||
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 -->
|
||||
## 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
|
||||
|
||||
## Project Structure (Clean Architecture)
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
See **[docs/CLEAN-ARCHITECTURE.md](docs/CLEAN-ARCHITECTURE.md)** for the full step-by-step guide.
|
||||
@@ -1,107 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Navbar from '@/components/Navbar';
|
||||
import Footer from '@/components/Footer';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import CateringPackageCard from '@/components/CateringPackageCard';
|
||||
import {
|
||||
cateringPackages,
|
||||
CATERING_DISH_CATALOG,
|
||||
CATERING_CATEGORY_ORDER,
|
||||
} from '@/lib/catering-data';
|
||||
import { useLanguage } from '@/lib/language-context';
|
||||
import { getTranslation } from '@/lib/translations';
|
||||
import { container } from '@/infrastructure/di/container';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
|
||||
export default function CateringPage() {
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
const handleSendInquiry = () => {
|
||||
container.messagingGateway.openWhatsApp(t.catering.inquiryMessage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
<main>
|
||||
<PageHeader
|
||||
eyebrow={t.catering.badge}
|
||||
title={t.catering.title}
|
||||
subtitle={t.catering.subtitle}
|
||||
/>
|
||||
|
||||
{/* Packages grid */}
|
||||
<section className="max-w-7xl mx-auto px-6 pb-16">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
{cateringPackages.map((pkg, index) => (
|
||||
<CateringPackageCard key={pkg.id} pkg={pkg} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Full dish catalog reference */}
|
||||
<section className="border-y border-[#EDE6D9] bg-[#fffdf8] px-6 py-12">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-8">
|
||||
<div className="mb-3 flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-[#c99a2e]">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{t.catering.catalogBadge}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-xl sm:text-[1.35rem] tracking-[-0.4px] text-[#101724]">
|
||||
{t.catering.catalogTitle}
|
||||
</h2>
|
||||
<p className="mt-1.5 max-w-xl text-sm leading-relaxed text-[#6B665F]">
|
||||
{t.catering.catalogSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSendInquiry}
|
||||
className="btn-primary inline-flex shrink-0 items-center justify-center self-start rounded-full px-7 py-3 text-sm font-bold tracking-wide sm:self-center"
|
||||
>
|
||||
{t.catering.sendInquiry}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||
{CATERING_CATEGORY_ORDER.map((category) => {
|
||||
const dishes = CATERING_DISH_CATALOG.filter((d) => d.category === category);
|
||||
const categoryLabel =
|
||||
(t.catering.categories as Record<string, string>)?.[category] ?? category;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={category}
|
||||
className="rounded-2xl border border-[#EDE6D9] bg-white p-5 shadow-sm"
|
||||
>
|
||||
<h3 className="mb-3 text-xs font-bold uppercase tracking-[0.2em] text-[#8a6a25]">
|
||||
{categoryLabel}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{dishes.map((dish) => (
|
||||
<li
|
||||
key={dish.id}
|
||||
className="text-sm font-medium text-[#101724]"
|
||||
>
|
||||
{dish.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
-482
@@ -1,482 +0,0 @@
|
||||
/**
|
||||
* =============================================================================
|
||||
* GLOBAL DESIGN SYSTEM — Shahi Kitchen
|
||||
* =============================================================================
|
||||
*
|
||||
* Shahi cream + gold visual identity for Shahi Kitchen.
|
||||
* Bright, warm, appetizing, and luxurious without being dark.
|
||||
*/
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
/* === Refined Shahi Light Palette - Warm Cream + Gold === */
|
||||
|
||||
/* Backgrounds */
|
||||
--bg: #F8F5F0;
|
||||
--bg-elevated: #F5F1E9;
|
||||
--surface: #FFFFFF;
|
||||
--surface-subtle: #F2EDE4;
|
||||
|
||||
/* Gold System */
|
||||
--gold: #B38B4D;
|
||||
--gold-dark: #8C6B3A;
|
||||
--gold-light: #C9A46B;
|
||||
--gold-muted: #d4a73d;
|
||||
|
||||
/* Emerald for contrast */
|
||||
--emerald: #0f5a4a;
|
||||
--emerald-deep: #0a4a3d;
|
||||
|
||||
/* Text */
|
||||
--text: #2C2520;
|
||||
--text-muted: #6B665F;
|
||||
--text-light: #8A8478;
|
||||
|
||||
/* Borders */
|
||||
--border: #EDE6D9;
|
||||
|
||||
/* Typography */
|
||||
--font-display: var(--font-playfair);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-arabic: var(--font-arabic);
|
||||
|
||||
/* Motion */
|
||||
--ease: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
/* Layout — language banner (48px) + navbar (68px) */
|
||||
--header-height: 116px;
|
||||
--page-top-gap: 1rem;
|
||||
}
|
||||
|
||||
/* Anchor links (/#menu, /#experience) land below the fixed header */
|
||||
.scroll-mt-header {
|
||||
scroll-margin-top: var(--header-height);
|
||||
}
|
||||
|
||||
/* Base */
|
||||
body,
|
||||
.shahi-body {
|
||||
font-family: var(--font-sans);
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(201, 154, 46, 0.08), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(15, 90, 74, 0.04), transparent);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* RTL typography */
|
||||
html[dir="rtl"] body {
|
||||
font-family: var(--font-arabic), var(--font-sans);
|
||||
}
|
||||
|
||||
html[dir="rtl"] h1,
|
||||
html[dir="rtl"] h2,
|
||||
html[dir="rtl"] h3,
|
||||
html[dir="rtl"] h4 {
|
||||
font-family: var(--font-arabic), var(--font-display);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Premium Buttons */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--gold), var(--gold-muted));
|
||||
color: #241806;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
border: 1px solid var(--gold);
|
||||
color: var(--gold-dark);
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--gold);
|
||||
color: #241806;
|
||||
}
|
||||
|
||||
/* Glass effect helper */
|
||||
.glass {
|
||||
background: rgba(255, 253, 248, 0.72);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
/* Luxury card style */
|
||||
.luxury-card {
|
||||
border-radius: 2rem;
|
||||
transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
|
||||
}
|
||||
|
||||
.luxury-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.12);
|
||||
}
|
||||
|
||||
/* Signature dish cards */
|
||||
.signature-card {
|
||||
box-shadow: 0 4px 24px -4px rgb(16 23 36 / 0.06);
|
||||
transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), border-color 0.35s var(--ease);
|
||||
}
|
||||
|
||||
.signature-card:hover {
|
||||
box-shadow: 0 20px 40px -12px rgb(179 139 77 / 0.18);
|
||||
border-color: rgba(201, 154, 46, 0.45);
|
||||
}
|
||||
|
||||
/* Section ornament */
|
||||
.section-ornament {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-ornament::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, var(--gold-muted), transparent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Hero overlay text */
|
||||
.hero-glass {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 253, 248, 0.92) 0%,
|
||||
rgba(255, 250, 240, 0.88) 50%,
|
||||
rgba(248, 245, 240, 0.85) 100%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(201, 154, 46, 0.2);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(16, 23, 36, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
* FOOTER — Matches header cream + gold palette
|
||||
* ============================================================================= */
|
||||
.shahi-footer {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#fffcf7 0%,
|
||||
#fbf7ef 55%,
|
||||
#f5f0e6 100%
|
||||
);
|
||||
color: #101724;
|
||||
border-top: 1px solid rgba(201, 154, 46, 0.25);
|
||||
box-shadow: 0 -4px 24px -4px rgba(16, 23, 36, 0.06);
|
||||
}
|
||||
|
||||
.footer-top-rail {
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
#8a6a25 15%,
|
||||
#c99a2e 35%,
|
||||
#f4d47f 50%,
|
||||
#c99a2e 65%,
|
||||
#8a6a25 85%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
filter: blur(80px);
|
||||
}
|
||||
|
||||
.footer-glow--gold {
|
||||
top: -40px;
|
||||
right: 10%;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background: rgba(201, 154, 46, 0.08);
|
||||
}
|
||||
|
||||
.footer-glow--emerald {
|
||||
bottom: 0;
|
||||
left: -5%;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: rgba(15, 90, 74, 0.05);
|
||||
}
|
||||
|
||||
.footer-heading-line {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #c99a2e, #f4d47f);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.footer-info-card {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(201, 154, 46, 0.22);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
padding: 1rem 1.125rem;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: border-color 0.25s var(--ease), background 0.25s var(--ease), box-shadow 0.25s var(--ease);
|
||||
}
|
||||
|
||||
.footer-info-card:hover {
|
||||
border-color: rgba(201, 154, 46, 0.45);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 16px -6px rgba(16, 23, 36, 0.08);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: #3d4654;
|
||||
transition: color 0.2s var(--ease), transform 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #0f5a4a;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .footer-link:hover {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
html[dir="ltr"] .footer-link:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.footer-social-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border-radius: 0.875rem;
|
||||
border: 1px solid rgba(201, 154, 46, 0.35);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
color: #8a6a25;
|
||||
transition: all 0.25s var(--ease);
|
||||
}
|
||||
|
||||
.footer-social-btn:hover {
|
||||
background: linear-gradient(135deg, #c99a2e, #e8c56a);
|
||||
color: #241806;
|
||||
border-color: #c99a2e;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px -4px rgba(201, 154, 46, 0.35);
|
||||
}
|
||||
|
||||
.footer-logo-ring {
|
||||
border-color: rgba(201, 154, 46, 0.35);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 20px -6px rgba(16, 23, 36, 0.1);
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border-top: 1px solid rgba(201, 154, 46, 0.2);
|
||||
}
|
||||
|
||||
.footer-cta {
|
||||
backdrop-filter: blur(12px);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.92) 0%,
|
||||
rgba(255, 252, 247, 0.88) 100%
|
||||
);
|
||||
border-color: rgba(201, 154, 46, 0.35);
|
||||
}
|
||||
|
||||
/* Language banner subtle shine */
|
||||
.lang-banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lang-banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.06) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Mesmerizing shimmer animation for premium buttons (used in Navbar) */
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
|
||||
/* Continuous menu reel marquee for 43" displays - items move LEFT (enter from right, exit left), seamless loop with 2x duplication */
|
||||
@keyframes menu-reel {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.menu-reel-track {
|
||||
will-change: transform;
|
||||
animation: menu-reel linear infinite;
|
||||
/* Force GPU layer for stable poster/video during transform */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Homepage signature menu — dishes scroll left to right */
|
||||
@keyframes signature-menu-reel-ltr {
|
||||
0% { transform: translateX(-50%); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.signature-menu-reel-track {
|
||||
will-change: transform;
|
||||
animation: signature-menu-reel-ltr linear infinite;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.signature-menu-reel:hover .signature-menu-reel-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.signature-menu-reel-track {
|
||||
animation: none;
|
||||
flex-wrap: wrap;
|
||||
width: 100% !important;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
* KIOSK / FULLSCREEN DISPLAY PAGES (screen1 & screen2)
|
||||
* Professional clean TV/window look: no scrollbars ever, even in browser
|
||||
* fullscreen (F11) or kiosk mode. User zooms browser (in/out) to adjust
|
||||
* visible reel density instead of scrolling. Content sized to fit 100vh.
|
||||
* ============================================================================= */
|
||||
html.screen-kiosk,
|
||||
body.screen-kiosk {
|
||||
overflow: hidden !important;
|
||||
overscroll-behavior: none;
|
||||
/* Hide scrollbars cross-browser */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
}
|
||||
|
||||
html.screen-kiosk::-webkit-scrollbar,
|
||||
body.screen-kiosk::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari/Opera */
|
||||
}
|
||||
|
||||
.screen-root {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Extra insurance: hide any descendant scrollbars */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.screen-root::-webkit-scrollbar,
|
||||
.screen-root *::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Subtle polish for pure display (no accidental text selection or pointer) */
|
||||
.screen-root {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* =============================================================================
|
||||
* VERTICAL REEL for screen3 order columns
|
||||
* Orders move UP (from below to above) in a loop, like screen2 horizontal but vertical.
|
||||
* Cards have fixed px height; viewport h-full of available column space (after header) =>
|
||||
* fills entire screen height with no white space (max top to bottom).
|
||||
* Zoom in browser: fewer items visible per column (larger cards relative).
|
||||
* Zoom out: more items visible per column (smaller cards relative).
|
||||
* When > visible count, the track loops seamlessly (duplicated list + 50% translateY).
|
||||
* ============================================================================= */
|
||||
@keyframes order-reel-up {
|
||||
0% { transform: translateY(0); }
|
||||
100% { transform: translateY(-50%); }
|
||||
}
|
||||
|
||||
.order-reel-track {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
will-change: transform;
|
||||
animation: order-reel-up linear infinite;
|
||||
/* Force GPU layer for stable animation */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.reel-viewport {
|
||||
overflow: hidden;
|
||||
/* Height 100% of available (flex h-full after section header) to fill screen.
|
||||
Fixed card heights + zoom changes visible count dynamically. */
|
||||
}
|
||||
|
||||
/* Menu page — smooth category filter scrolling */
|
||||
.menu-category-scroll {
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.menu-category-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.menu-category-scroll {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d4c9b8 transparent;
|
||||
}
|
||||
|
||||
.menu-category-scroll::-webkit-scrollbar {
|
||||
display: block;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.menu-category-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.menu-category-scroll::-webkit-scrollbar-thumb {
|
||||
background: #d4c9b8;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.menu-category-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: #c99a2e;
|
||||
}
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* ROOT LAYOUT — Shahi Kitchen (shahikitchen.se)
|
||||
*
|
||||
* This is the single root layout for the entire Next.js App Router application.
|
||||
*
|
||||
* RESPONSIBILITIES:
|
||||
* - Set up global metadata (title, description, favicon) for SEO and social sharing
|
||||
* - Load the premium typography system:
|
||||
* • Playfair Display (display/serif) → used for headings via --font-playfair
|
||||
* • Geist Sans (system UI) → body text
|
||||
* • Geist Mono → code / tabular data
|
||||
* - Initialize the GLOBAL CART SYSTEM:
|
||||
* • CartProvider wraps the whole tree so any page/component can use `useCart()`
|
||||
* • CartDrawer is rendered once at the root (slide-in basket panel)
|
||||
* • Sonner <Toaster> provides beautiful toast notifications used by the cart
|
||||
*
|
||||
* ARCHITECTURAL NOTES:
|
||||
* - We deliberately render <CartDrawer /> and <Toaster /> at the root level instead of
|
||||
* inside individual pages. This guarantees only ONE instance exists and prevents
|
||||
* duplicate drawers/toasts when navigating.
|
||||
* - The cream/gold Shahi theme tokens live in globals.css and are referenced via
|
||||
* Tailwind arbitrary values (e.g. bg-[#F8F5F0]).
|
||||
*
|
||||
* FUTURE DEVELOPERS:
|
||||
* - To add a new global provider (theme, analytics, etc.), wrap it here.
|
||||
* - WhatsApp number for orders is currently hardcoded in CartDrawer.tsx (46739381089).
|
||||
* - If you ever split the cart into a separate modal library, keep the provider here.
|
||||
*/
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Playfair_Display, Geist, Geist_Mono, Noto_Sans_Arabic } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "sonner";
|
||||
import CartDrawer from "@/components/CartDrawer";
|
||||
import WishlistDrawer from "@/components/WishlistDrawer";
|
||||
import { CartProvider } from "@/presentation/providers/cart-provider";
|
||||
import { WishlistProvider } from "@/presentation/providers/wishlist-provider";
|
||||
import { LanguageProvider } from "@/presentation/providers/language-provider";
|
||||
|
||||
const playfair = Playfair_Display({
|
||||
variable: "--font-playfair",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
style: ["normal", "italic"],
|
||||
});
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600"],
|
||||
});
|
||||
|
||||
const notoArabic = Noto_Sans_Arabic({
|
||||
variable: "--font-arabic",
|
||||
subsets: ["arabic"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Next.js Metadata API (static)
|
||||
* These values power:
|
||||
* - Browser tab title
|
||||
* - Search engine results
|
||||
* - Social cards (Open Graph / Twitter) when shared
|
||||
*
|
||||
* For dynamic per-page metadata, use `generateMetadata()` in page files.
|
||||
*/
|
||||
export const metadata: Metadata = {
|
||||
title: "Shahi Kitchen | Authentic Indian & Pakistani Restaurant in Gothenburg",
|
||||
description: "Experience Shahi flavors at Shahi Kitchen in Askim, Gothenburg. Authentic Indian & Pakistani cuisine, famous lunch buffet, and traditional sweets since 2016.",
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* RootLayout
|
||||
*
|
||||
* The ONLY place in the app where we initialize:
|
||||
* 1. CartProvider → gives every descendant access to `useCart()` hook
|
||||
* 2. CartDrawer → the actual slide-in basket UI (controlled via context)
|
||||
* 3. Toaster → global toast surface used by cart (Sonner library)
|
||||
*
|
||||
* IMPORTANT:
|
||||
* - Never wrap the entire app in another CartProvider — it will break the singleton.
|
||||
* - The body uses the Shahi cream background (#F8F5F0) as the base canvas.
|
||||
* - `antialiased` + font variables are applied once at the root for consistency.
|
||||
*/
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="sv"
|
||||
suppressHydrationWarning
|
||||
className={`${playfair.variable} ${geistSans.variable} ${geistMono.variable} ${notoArabic.variable} h-full antialiased`}
|
||||
>
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className="min-h-full flex flex-col shahi-body text-[#2C2A26]"
|
||||
>
|
||||
<LanguageProvider>
|
||||
<CartProvider>
|
||||
<WishlistProvider>
|
||||
{children}
|
||||
<CartDrawer />
|
||||
<WishlistDrawer />
|
||||
<Toaster position="top-center" richColors closeButton />
|
||||
</WishlistProvider>
|
||||
</CartProvider>
|
||||
</LanguageProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* LOCATIONS PAGE
|
||||
*
|
||||
* Dedicated page for the restaurant's two physical branches.
|
||||
*
|
||||
* ARCHITECTURE:
|
||||
* - Two branches are modeled as distinct concepts:
|
||||
* 1. Shahi Kitchen Askim (Sisjön) → Full restaurant + famous lunch buffet
|
||||
* 2. Shahi Sweets Backaplan → Sweets, snacks, café, halwa puri etc.
|
||||
* - Both are operated by the same team (explicitly stated at the bottom)
|
||||
* - Google Maps embeds are intentionally simple (lazy loaded)
|
||||
*
|
||||
* FUTURE:
|
||||
* - When real online ordering launches, this page could show "Order from Askim"
|
||||
* vs "Order from Backaplan" with different delivery radii.
|
||||
* - Both branches now have the same opening hours.
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import { useLanguage } from "@/lib/language-context";
|
||||
import { getTranslation } from "@/lib/translations";
|
||||
|
||||
export default function LocationsPage() {
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
<PageHeader
|
||||
className="max-w-5xl pb-4"
|
||||
centered
|
||||
eyebrow="WHERE TO FIND US"
|
||||
title={language === 'sv' ? 'Våra Platser' : 'Our Locations'}
|
||||
subtitle={
|
||||
language === 'sv'
|
||||
? 'Två restauranger i Göteborg — båda serverar autentiska smaker med samma shahi gästfrihet.'
|
||||
: 'Two branches in Gothenburg — both serving authentic flavors with the same Shahi hospitality.'
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="max-w-5xl mx-auto px-6 pb-16">
|
||||
|
||||
<div className="flex justify-center mb-8">
|
||||
<a href="/reserve" className="btn-primary px-8 py-3 rounded-full text-sm tracking-[0.5px]">
|
||||
{t.reserve}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{/* Branch 1: Askim */}
|
||||
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
|
||||
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">RESTAURANT & BUFFET</div>
|
||||
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Kitchen – Askim (Sisjön)</h2>
|
||||
|
||||
<div className="space-y-5 text-[15px]">
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
|
||||
<div>Datavägen 10A<br />436 32 Askim, Gothenburg</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
|
||||
<a href="tel:031288910" className="block hover:text-[#B38B4D]">031-28 89 10</a>
|
||||
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
|
||||
<div>Monday – Sunday • 11:00 – 21:00</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
|
||||
<div className="text-[#6B665F]">Full restaurant experience with our popular lunch buffet, à la carte, and traditional Pakistani & Indian dishes.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130.5!2d11.95!3d57.65!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sDatav%C3%A4gen%2010A%2C%20436%2032%20Askim!5e0!3m2!1sen!2sse!4v1700000000000"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branch 2: Backaplan */}
|
||||
<div className="bg-white border border-[#EDE6D9] rounded-2xl p-8">
|
||||
<div className="uppercase text-[#B38B4D] text-xs tracking-[2px] mb-2">SWEETS, SNACKS & CAFÉ</div>
|
||||
<h2 className="text-4xl tracking-[-1.5px] mb-4">Shahi Sweets – Backaplan</h2>
|
||||
|
||||
<div className="space-y-5 text-[15px]">
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">ADDRESS</div>
|
||||
<div>Krokegårdsgatan 5<br />417 30 Göteborg (Backaplan)</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">PHONE</div>
|
||||
<a href="tel:0739381089" className="block hover:text-[#B38B4D]">0739-381089</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">OPENING HOURS</div>
|
||||
<div>Monday – Sunday • 11:00 – 21:00</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B38B4D] text-xs tracking-widest mb-1">WHAT TO EXPECT</div>
|
||||
<div className="text-[#6B665F]">Fresh mithai (sweets), savory snacks, halwa puri, biryani, nihari, and café-style seating.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 aspect-video rounded-xl overflow-hidden border border-[#EDE6D9]">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2130!2d11.92!3d57.72!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x464ff3c8b8b8b8b8%3A0x1234567890abcdef!2sKrokeg%C3%A5rdsgatan%205%2C%20417%2030%20G%C3%B6teborg!5e0!3m2!1sen!2sse!4v1700000000000"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center text-sm text-[#6B665F]">
|
||||
Both branches are operated by the same team and share the same passion for authentic flavors.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { logoUrl } from "@/lib/assets";
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
import { useLanguage } from "@/lib/language-context";
|
||||
import { getTranslation } from "@/lib/translations";
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { User, Lock, MapPin, LogIn, Phone, MessageCircle } from "lucide-react";
|
||||
|
||||
/**
|
||||
* =============================================================================
|
||||
* STAFF LOGIN / AUTHENTICATION PAGE
|
||||
* =============================================================================
|
||||
*
|
||||
* Beautiful, theme-matched placeholder login experience.
|
||||
* - Fully language compatible (sv default + en/hi/ur)
|
||||
* - No real backend yet — always shows friendly "under construction" message
|
||||
* - Mobile-first: large tap targets, excellent spacing, easy form on phones
|
||||
* - Desktop: elegant split layout with dark luxury graphic panel + form
|
||||
* - Uses existing design tokens (gold accents, cream bg, rounded cards, serif titles)
|
||||
*
|
||||
* Future: when real auth is added, this will become the entry to a protected staff area
|
||||
* (per-site menus, orders, inventory etc. for Askim vs Backaplan).
|
||||
*/
|
||||
|
||||
export default function LoginPage() {
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
site: '',
|
||||
});
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
// Placeholder behaviour: always show the construction notice.
|
||||
// No validation beyond native required fields — real auth coming later.
|
||||
setIsSubmitted(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({ username: '', password: '', site: '' });
|
||||
setIsSubmitted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
<main className="pb-20">
|
||||
{/* Elegant header */}
|
||||
<div className="max-w-5xl mx-auto px-6 text-center">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
|
||||
STAFF PORTAL
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
|
||||
{t.auth.title}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-md text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
|
||||
{t.auth.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content area — beautiful split layout on desktop, clean single column on mobile */}
|
||||
<div className="mx-auto max-w-5xl px-6 mt-10">
|
||||
<div className="grid items-start gap-8 md:grid-cols-5">
|
||||
{/* LEFT: Premium dark graphic panel (desktop only). Matches Shahi theme with gold details. */}
|
||||
<div className="hidden md:col-span-2 md:block">
|
||||
<div className="sticky top-[116px] flex h-[520px] flex-col justify-between overflow-hidden rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-10 text-white shadow-2xl">
|
||||
{/* Subtle gold frame lines */}
|
||||
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e] to-transparent" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-[#c99a2e]/50 to-transparent" />
|
||||
|
||||
<div>
|
||||
<div className="mb-8 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur">
|
||||
<img
|
||||
src={logoUrl()}
|
||||
alt="Shahi Kitchen"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="font-serif text-[42px] leading-[0.95] tracking-[-1.8px]">
|
||||
Shahi<br />Kitchen
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-medium tracking-[2.5px] text-[#c99a2e]">
|
||||
STAFF ACCESS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="my-6 h-px w-10 bg-[#c99a2e]/40" />
|
||||
<p className="max-w-[220px] text-sm leading-relaxed text-white/70">
|
||||
Two locations serving Shahi Indian & Pakistani hospitality in Gothenburg since 2016.
|
||||
</p>
|
||||
<div className="mt-5 text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">
|
||||
ASKIM • BACKAPLAN
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT / MOBILE: The actual elegant login card */}
|
||||
<div className="md:col-span-3">
|
||||
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-8 shadow-xl md:p-10">
|
||||
{/* Mobile-only logo accent for visual continuity */}
|
||||
<div className="mb-6 flex justify-center md:hidden">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-[#F8F5F0] p-2.5">
|
||||
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{!isSubmitted ? (
|
||||
<motion.div
|
||||
key="login-form"
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Username */}
|
||||
<div>
|
||||
<label htmlFor="username" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
|
||||
{t.auth.usernameLabel}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
placeholder={t.auth.usernamePlaceholder}
|
||||
required
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label htmlFor="password" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
|
||||
{t.auth.passwordLabel}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
placeholder={t.auth.passwordPlaceholder}
|
||||
required
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Site / Location dropdown — Askim or Backaplan */}
|
||||
<div>
|
||||
<label htmlFor="site" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">
|
||||
{t.auth.siteLabel}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
|
||||
<select
|
||||
id="site"
|
||||
name="site"
|
||||
value={formData.site}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
>
|
||||
<option value="">{t.auth.sitePlaceholder}</option>
|
||||
<option value="askim">{t.auth.askim}</option>
|
||||
<option value="backaplan">{t.auth.backaplan}</option>
|
||||
</select>
|
||||
{/* Custom dropdown arrow for polish */}
|
||||
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]">▾</div>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] text-[#8A8478]">Select the location you are working at today.</p>
|
||||
</div>
|
||||
|
||||
{/* Primary gold action button — large & mobile friendly */}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
|
||||
>
|
||||
<LogIn className="h-5 w-5" />
|
||||
{t.auth.submit}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-8 text-center text-[11px] tracking-wide text-[#8A8478]">
|
||||
{t.auth.footerNote}
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
/* Beautiful result state — always shows the construction notice */
|
||||
<motion.div
|
||||
key="construction-result"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-[#B38B4D]/10">
|
||||
<LogIn className="h-8 w-8 text-[#B38B4D]" />
|
||||
</div>
|
||||
|
||||
<h2 className="mb-3 font-serif text-3xl tracking-[-1px]">
|
||||
{t.auth.construction.title}
|
||||
</h2>
|
||||
|
||||
<p className="mx-auto max-w-md text-[15px] leading-relaxed text-[#6B665F]">
|
||||
{t.auth.construction.message}
|
||||
</p>
|
||||
|
||||
{/* Helpful contact actions (real numbers from the site) */}
|
||||
<div className="mt-8 space-y-3">
|
||||
<a
|
||||
href="tel:031288910"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-[#EDE6D9] py-3.5 text-sm font-medium transition active:bg-[#F8F5F0] hover:bg-[#F8F5F0]"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
{t.auth.construction.contact} — 031-28 89 10
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://wa.me/46739381089"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-[#EDE6D9] py-3.5 text-sm font-medium transition active:bg-[#F8F5F0] hover:bg-[#F8F5F0]"
|
||||
>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
WhatsApp +46 73 938 10 89
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={resetForm}
|
||||
className="mt-2 w-full rounded-2xl bg-[#101724] py-3.5 text-sm font-semibold text-white active:bg-black transition"
|
||||
>
|
||||
{t.auth.construction.tryAgain}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-[10px] text-[#8A8478]">
|
||||
We appreciate your patience while we build a seamless experience.
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Small trust line */}
|
||||
<p className="mt-5 text-center text-[10px] text-[#8A8478] tracking-[1px]">
|
||||
SHAHI KITCHEN • GOTHENBURG • EST 2016
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* MENU PAGE — Premium Sidebar Navigation
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { menuCategories } from "@/lib/menu-data";
|
||||
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
|
||||
import type { MenuItem } from "@/domain/menu/entities";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
import { motion } from "framer-motion";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import Footer from "@/components/Footer";
|
||||
import { useCart } from "@/components/CartContext";
|
||||
import WishlistButton from "@/components/WishlistButton";
|
||||
import { useLanguage } from "@/lib/language-context";
|
||||
import { getTranslation } from "@/lib/translations";
|
||||
import {
|
||||
getMenuPosterSrc,
|
||||
getMenuPosterCandidates,
|
||||
applyNextImageFallback,
|
||||
} from "@/lib/assets";
|
||||
import { playHoverSound } from "@/lib/sounds";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
export default function MenuPage() {
|
||||
const [activeCategory, setActiveCategory] = useState("All");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [showVegetarianOnly, setShowVegetarianOnly] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const categoryListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { addToCart } = useCart();
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
// Sidebar categories
|
||||
const sidebarCategories = [
|
||||
{ id: "All", name: t.menu.allDishes || "All Dishes" },
|
||||
...menuCategories.map((cat) => ({ id: cat.id, name: cat.name })),
|
||||
];
|
||||
|
||||
// Filtering logic
|
||||
const filteredCategories = useMemo(() => {
|
||||
return menuCategories
|
||||
.map((category) => {
|
||||
let items = category.items;
|
||||
|
||||
// Sidebar filter
|
||||
if (activeCategory !== "All" && category.id !== activeCategory) {
|
||||
items = [];
|
||||
}
|
||||
|
||||
// Search
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
items = items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(q) ||
|
||||
(item.description && item.description.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
|
||||
// Vegetarian
|
||||
if (showVegetarianOnly) {
|
||||
items = items.filter((item) => item.isVegetarian);
|
||||
}
|
||||
|
||||
return { ...category, items };
|
||||
})
|
||||
.filter((category) => category.items.length > 0);
|
||||
}, [searchQuery, showVegetarianOnly, activeCategory]);
|
||||
|
||||
const handleCategorySelect = (id: string) => {
|
||||
setActiveCategory(id);
|
||||
window.scrollTo({ top: 220, behavior: "smooth" });
|
||||
};
|
||||
|
||||
const renderMenuPrice = (item: MenuItem) => {
|
||||
if (item.pricing === "weight") {
|
||||
return (
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="text-[#B38B4D] text-xl font-medium tracking-[-0.5px] tabular-nums leading-tight">
|
||||
{item.pricePerHalfKg ?? item.price}
|
||||
</div>
|
||||
<div className="text-[10px] text-[#8A8478] tracking-wide">{t.menu.perHalfKg}</div>
|
||||
<div className="text-[#B38B4D] text-sm font-medium tabular-nums mt-0.5">
|
||||
{item.pricePerKg} {t.menu.perKg}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="text-[#B38B4D] text-2xl font-medium tracking-[-0.5px] tabular-nums">
|
||||
{item.price}
|
||||
</div>
|
||||
<div className="text-[10px] text-[#8A8478] tracking-widest -mt-1">KR</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// isMobile detection - used to enable advanced desktop hover effects + sounds only on desktop
|
||||
useEffect(() => {
|
||||
const updateMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
updateMobile();
|
||||
window.addEventListener("resize", updateMobile);
|
||||
return () => window.removeEventListener("resize", updateMobile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = categoryListRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const activeButton = container.querySelector<HTMLElement>(
|
||||
`[data-category-id="${activeCategory}"]`
|
||||
);
|
||||
if (!activeButton) return;
|
||||
|
||||
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
activeButton.scrollIntoView({
|
||||
behavior: prefersReducedMotion ? "auto" : "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
}, [activeCategory]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
<PageHeader
|
||||
eyebrow="AUTHENTIC • GENEROUS • ROYAL"
|
||||
title={t.menu.title}
|
||||
subtitle={t.menu.subtitle}
|
||||
/>
|
||||
|
||||
{/* Main Layout */}
|
||||
<div className="max-w-7xl mx-auto px-6 pb-20">
|
||||
<div className="flex flex-col lg:flex-row gap-10">
|
||||
|
||||
{/* BEAUTIFUL SIDEBAR */}
|
||||
<div className="lg:w-72 flex-shrink-0">
|
||||
<div className="sticky top-[116px]">
|
||||
<div className="mb-6">
|
||||
<div className="text-xs tracking-[3px] text-[#c99a2e] mb-2">EXPLORE OUR</div>
|
||||
<h3 className="font-serif text-3xl tracking-tight">Signature Categories</h3>
|
||||
</div>
|
||||
|
||||
{/* Smooth scroll: horizontal on mobile, vertical on desktop when list exceeds viewport */}
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={categoryListRef}
|
||||
className="menu-category-scroll flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth overscroll-x-contain overscroll-y-contain pb-3 pr-1 snap-x snap-mandatory scroll-px-1 [-webkit-overflow-scrolling:touch] lg:flex-col lg:overflow-x-hidden lg:overflow-y-auto lg:max-h-[calc(100dvh-13.5rem)] lg:snap-none lg:scroll-px-0 lg:pb-2 lg:pr-2"
|
||||
>
|
||||
{sidebarCategories.map((cat) => {
|
||||
const isActive = activeCategory === cat.id;
|
||||
const itemCount = cat.id === "All"
|
||||
? menuCategories.reduce((sum, c) => sum + c.items.length, 0)
|
||||
: menuCategories.find(c => c.id === cat.id)?.items.length || 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
data-category-id={cat.id}
|
||||
onClick={() => handleCategorySelect(cat.id)}
|
||||
className={`flex-shrink-0 snap-start min-w-[140px] lg:min-w-0 lg:w-full flex items-center justify-between px-5 py-3 lg:py-3.5 rounded-2xl text-left transition-all active:scale-[0.985] group ${
|
||||
isActive
|
||||
? "bg-[#101724] text-white shadow-lg"
|
||||
: "hover:bg-white hover:shadow-sm active:bg-[#EDE6D9] text-[#101724] border border-transparent hover:border-[#e5e1d7]"
|
||||
}`}
|
||||
>
|
||||
<span className={`font-medium tracking-[-0.1px] text-sm lg:text-base ${isActive ? "" : "group-hover:text-[#0f5a4a]"}`}>
|
||||
{cat.name}
|
||||
</span>
|
||||
<span className={`text-[10px] lg:text-xs px-2 py-0.5 rounded-full font-mono tabular-nums ${
|
||||
isActive ? "bg-white/20" : "bg-[#e5e1d7] text-[#68717f]"
|
||||
}`}>
|
||||
{itemCount}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-[#F8F5F0] to-transparent lg:hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN CONTENT */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Search + Vegetarian Filter */}
|
||||
<div className="mb-8 flex flex-col md:flex-row gap-4 items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.menu.searchPlaceholder}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full md:w-80 rounded-2xl border border-[#e5e1d7] bg-white px-5 py-3 text-sm placeholder:text-[#8A8478] focus:border-[#c99a2e] focus:ring-2 focus:ring-[#c99a2e]/20 transition-all"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => setShowVegetarianOnly(!showVegetarianOnly)}
|
||||
className={`px-6 py-3 rounded-2xl text-sm font-medium border transition-all active:scale-[0.985] ${
|
||||
showVegetarianOnly
|
||||
? "bg-[#0f5a4a] text-white border-[#0f5a4a]"
|
||||
: "border-[#e5e1d7] hover:border-[#c99a2e] text-[#101724] bg-white"
|
||||
}`}
|
||||
>
|
||||
{showVegetarianOnly ? t.menu.vegetarianOnly : t.menu.showVegetarian}
|
||||
</button>
|
||||
|
||||
{(searchQuery || showVegetarianOnly || activeCategory !== "All") && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setShowVegetarianOnly(false);
|
||||
setActiveCategory("All");
|
||||
}}
|
||||
className="text-sm font-medium text-[#c99a2e] hover:text-[#8f6b22] active:underline"
|
||||
>
|
||||
{t.menu.clearFilters}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Menu Items */}
|
||||
{filteredCategories.length === 0 ? (
|
||||
<div className="text-center py-16 text-[#6B665F]">
|
||||
{t.menu.noResults}
|
||||
</div>
|
||||
) : (
|
||||
filteredCategories.map((category) => (
|
||||
<div key={category.id} className="mb-16">
|
||||
<div className="flex items-center gap-4 mb-7">
|
||||
<div className="text-3xl tracking-[-1px] text-[#101724]">{category.name}</div>
|
||||
<div className="flex-1 h-px bg-gradient-to-r from-[#e5e1d7] to-transparent" />
|
||||
<div className="text-sm font-medium text-[#68717f] tracking-widest">
|
||||
{category.items.length} {t.menu.dishes}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{category.items.map((item) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
data-id={item.id}
|
||||
className="menu-card group bg-white border border-[#EDE6D9] rounded-2xl overflow-hidden flex flex-col hover:border-[#c99a2e]/40 active:border-[#c99a2e] active:scale-[0.985] transition-all duration-150 cursor-pointer touch-manipulation"
|
||||
onClick={() => addToCart(buildCartLineFromMenuItem(item))}
|
||||
whileHover={!isMobile ? {
|
||||
y: -4,
|
||||
boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
||||
transition: { type: "spring", stiffness: 300, damping: 20 }
|
||||
} : {}}
|
||||
whileTap={{ scale: 0.985 }}
|
||||
>
|
||||
{/* Media - Always poster for performance. Advanced desktop hover effect (scale + lift + glow) + subtle sound. Mobile uses simple scale. */}
|
||||
<div
|
||||
className="relative h-52 overflow-hidden bg-[#F2EDE4] cursor-pointer"
|
||||
onMouseEnter={() => {
|
||||
if (!isMobile) playHoverSound();
|
||||
}}
|
||||
>
|
||||
<motion.img
|
||||
src={getMenuPosterSrc(item)}
|
||||
alt={item.name}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
whileHover={!isMobile ? {
|
||||
scale: 1.08,
|
||||
y: -3,
|
||||
transition: { type: "spring", stiffness: 260, damping: 20 }
|
||||
} : {}}
|
||||
onError={(e) => {
|
||||
applyNextImageFallback(
|
||||
e.target as HTMLImageElement,
|
||||
getMenuPosterCandidates(item),
|
||||
(e.target as HTMLImageElement).src
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* Subtle gradient + hover shine overlay for advanced desktop feel */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-black/10 to-black/25" />
|
||||
{!isMobile && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
)}
|
||||
<div className="absolute top-3 right-3 z-10">
|
||||
<WishlistButton
|
||||
item={{ id: item.id, name: item.name, price: item.price, image: item.image }}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-1">
|
||||
<div className="flex justify-between items-start gap-4 mb-4">
|
||||
<h3 className="text-[22px] leading-tight tracking-[-0.4px] text-[#2C2A26]">
|
||||
{item.name}
|
||||
</h3>
|
||||
{renderMenuPrice(item)}
|
||||
</div>
|
||||
|
||||
{item.description && (
|
||||
<p className="text-[#6B665F] text-[15px] leading-relaxed mb-4">
|
||||
{(t as any).menuDescriptions?.[item.id] || item.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-auto space-y-3">
|
||||
{item.isVegetarian && (
|
||||
<span className="inline-block text-xs px-3 py-1 rounded-full bg-[#3F5C4A]/10 text-[#3F5C4A] tracking-wider">
|
||||
VEGETARIAN
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
addToCart(buildCartLineFromMenuItem(item));
|
||||
}}
|
||||
className="w-full py-3.5 text-sm tracking-[0.6px] border border-[#B38B4D] text-[#B38B4D] rounded-full hover:bg-[#B38B4D] hover:text-white active:bg-[#8C6B3A] active:text-white active:scale-[0.985] font-medium transition-all touch-manipulation"
|
||||
>
|
||||
{t.wishlistDrawer.addToCart}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* /orderfromtable — Single reusable table ordering page (QR driven)
|
||||
*
|
||||
* This page is the ONLY customer-facing order page for physical tables.
|
||||
* All 80 table QR codes point here with ?table=backaplan-001 etc.
|
||||
*
|
||||
* - Strict validation of table param (branch + 001-040)
|
||||
* - Local item selection (independent of global cart)
|
||||
* - On "Send order" we only show confirmation (no backend call yet)
|
||||
* - Payload is prepared exactly in the shape the future POST /api/table-orders will expect
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { filterMenuItems } from '@/application/menu/filter-menu';
|
||||
import { getMenuPosterSrc, logoUrl, SITE_ASSETS } from '@/lib/assets';
|
||||
import { buildTableOrderPayload } from '@/application/table-order/table-order-use-cases';
|
||||
import { parseTableId } from '@/domain/table/table-id';
|
||||
import {
|
||||
addOrderLine,
|
||||
calculateLineTotal,
|
||||
calculateOrderTotal,
|
||||
formatLineQuantityLabel,
|
||||
removeOrderLine,
|
||||
updateOrderLineQuantity,
|
||||
type OrderLine,
|
||||
} from '@/domain/shared/order-line';
|
||||
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
|
||||
import { container } from '@/infrastructure/di/container';
|
||||
import type { MenuItem } from '@/domain/menu/entities';
|
||||
import { playAddSound, playSuccessSound } from '@/infrastructure/audio/web-audio-sound-adapter';
|
||||
import { useLanguage } from '@/presentation/providers/language-provider';
|
||||
import { getTranslation } from '@/presentation/i18n/translations';
|
||||
import Navbar from '@/components/Navbar';
|
||||
|
||||
import Footer from '@/components/Footer';
|
||||
import { Search, Plus, Minus, X, CheckCircle, UtensilsCrossed } from 'lucide-react';
|
||||
|
||||
export default function OrderFromTablePage() {
|
||||
const searchParams = useSearchParams();
|
||||
const tableParam = searchParams.get('table');
|
||||
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
const to = t.tableOrder;
|
||||
|
||||
const parsed = useMemo(() => parseTableId(tableParam), [tableParam]);
|
||||
const allMenuItems = container.menuRepository.getAllItems();
|
||||
|
||||
// Local order state (NOT the global cart)
|
||||
const [selected, setSelected] = useState<OrderLine[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [vegOnly, setVegOnly] = useState(false);
|
||||
const [notes, setNotes] = useState('');
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const isValid = parsed.valid;
|
||||
|
||||
const branchLabel = isValid
|
||||
? (parsed.branch === 'askim' ? to.branchAskim : to.branchBackaplan)
|
||||
: '';
|
||||
const tableDisplay = isValid ? parsed.tableNumber : '';
|
||||
|
||||
// Filtered dishes for the selector grid (same data source as everywhere else)
|
||||
const filteredDishes = useMemo(
|
||||
() =>
|
||||
filterMenuItems(allMenuItems, {
|
||||
searchQuery: search,
|
||||
vegetarianOnly: vegOnly,
|
||||
}),
|
||||
[allMenuItems, search, vegOnly]
|
||||
);
|
||||
|
||||
const selectedTotal = calculateOrderTotal(selected);
|
||||
|
||||
function addDish(dish: MenuItem) {
|
||||
playAddSound();
|
||||
setSelected((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
|
||||
}
|
||||
|
||||
function updateQty(id: string, newQty: number) {
|
||||
setSelected((prev) => updateOrderLineQuantity(prev, id, newQty));
|
||||
}
|
||||
|
||||
function removeItem(id: string) {
|
||||
setSelected((prev) => removeOrderLine(prev, id));
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
setSelected([]);
|
||||
setNotes('');
|
||||
}
|
||||
|
||||
async function handleSendOrder() {
|
||||
if (!isValid || selected.length === 0) return;
|
||||
|
||||
setIsSending(true);
|
||||
|
||||
const orderPayload = buildTableOrderPayload(
|
||||
parsed.branch,
|
||||
parsed.tableNumber,
|
||||
parsed.tableIdentifier,
|
||||
selected,
|
||||
notes
|
||||
);
|
||||
|
||||
// For now we do NOT call any backend.
|
||||
// This log + comment makes future integration trivial.
|
||||
// TODO: replace the block below with real fetch when backend is ready.
|
||||
console.log('[TABLE ORDER] Prepared payload (will be POSTed to /api/table-orders):', orderPayload);
|
||||
|
||||
// Simulate a tiny delay for nice UX (feels like it is being sent)
|
||||
await new Promise((r) => setTimeout(r, 420));
|
||||
|
||||
playSuccessSound();
|
||||
setIsSending(false);
|
||||
setSent(true);
|
||||
}
|
||||
|
||||
function resetForAnotherOrder() {
|
||||
setSent(false);
|
||||
setSelected([]);
|
||||
setNotes('');
|
||||
setSearch('');
|
||||
setVegOnly(false);
|
||||
}
|
||||
|
||||
// ====================== INVALID QR STATE ======================
|
||||
if (!isValid) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
<div className="flex min-h-[70vh] items-center justify-center px-6">
|
||||
<div className="max-w-md text-center">
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-red-100/70">
|
||||
<X className="h-10 w-10 text-red-600" />
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-3">{to.invalidTitle}</h1>
|
||||
<p className="text-sm md:text-[15px] text-[#6B665F] mb-8">{to.invalidMessage}</p>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-[#101724] px-8 py-3 text-sm font-semibold text-white active:bg-black"
|
||||
>
|
||||
Go to homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== CONFIRMATION STATE ======================
|
||||
if (sent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
<div className="flex min-h-[70vh] items-center justify-center px-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.96, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
className="max-w-lg text-center"
|
||||
>
|
||||
<div className="mx-auto mb-6 flex h-24 w-24 items-center justify-center rounded-full bg-[#B38B4D]/10">
|
||||
<CheckCircle className="h-14 w-14 text-[#B38B4D]" />
|
||||
</div>
|
||||
|
||||
<div className="mb-2 text-sm tracking-[3px] text-[#B38B4D] font-medium">
|
||||
{branchLabel.toUpperCase()} • {to.tableLabel.toUpperCase()} {tableDisplay}
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-4">{to.confirmationTitle}</h1>
|
||||
|
||||
<p className="text-sm md:text-[15px] text-[#6B665F] leading-relaxed mb-10">
|
||||
{to.confirmationMessage}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button
|
||||
onClick={resetForAnotherOrder}
|
||||
className="btn-primary px-10 py-4 rounded-2xl text-base font-semibold tracking-[0.5px] active:scale-[0.985]"
|
||||
>
|
||||
{to.orderAnother}
|
||||
</button>
|
||||
<a
|
||||
href="/menu"
|
||||
className="btn-outline px-10 py-4 rounded-2xl text-base font-semibold tracking-[0.5px]"
|
||||
>
|
||||
Browse full menu
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-xs text-[#8A8478]">
|
||||
{branchLabel} — {to.tableLabel} {tableDisplay}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ====================== MAIN ORDERING UI ======================
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
{/* Table header bar */}
|
||||
<div className="sticky top-[116px] z-40 border-b border-[#EDE6D9] bg-[#F8F5F0]/95 backdrop-blur-xl">
|
||||
<div className="max-w-6xl mx-auto px-6 py-4 flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-[#c99a2e]/30 bg-white p-1.5">
|
||||
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-serif text-2xl tracking-[-1px] leading-none">{branchLabel}</div>
|
||||
<div className="text-[#B38B4D] text-sm font-medium tracking-[1px] mt-0.5">
|
||||
{to.tableLabel} {tableDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sm:ml-auto flex items-center gap-2 text-xs uppercase tracking-[2px] text-[#8A8478]">
|
||||
<UtensilsCrossed className="h-3.5 w-3.5" />
|
||||
ORDER FROM TABLE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-6 pt-8 pb-24">
|
||||
{/* Intro */}
|
||||
<div className="mb-8">
|
||||
<div className="text-[#B38B4D] text-xs tracking-[3.5px] mb-2 font-medium">{to.tableLabel.toUpperCase()} {tableDisplay}</div>
|
||||
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">{to.selectTitle}</h1>
|
||||
<p className="text-sm md:text-[15px] text-[#6B665F] max-w-xl leading-relaxed">{to.selectSubtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-12 gap-8">
|
||||
{/* DISH SELECTOR */}
|
||||
<div className="lg:col-span-7">
|
||||
{/* Filters */}
|
||||
<div className="mb-5 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="pointer-events-none absolute left-4 top-3.5 h-4 w-4 text-[#B38B4D]" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={to.searchPlaceholder}
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-white py-3 pl-11 pr-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:ring-1 focus:ring-[#B38B4D]/20 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setVegOnly(!vegOnly)}
|
||||
className={`px-5 py-3 rounded-2xl text-sm font-medium border transition active:scale-[0.985] whitespace-nowrap ${
|
||||
vegOnly
|
||||
? 'bg-[#0f5a4a] text-white border-[#0f5a4a]'
|
||||
: 'border-[#EDE6D9] bg-white text-[#101724] hover:border-[#c99a2e]'
|
||||
}`}
|
||||
>
|
||||
{vegOnly ? '✓ ' : ''}{to.showVegetarian}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dish grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<AnimatePresence>
|
||||
{filteredDishes.length > 0 ? (
|
||||
filteredDishes.map((dish) => {
|
||||
const inOrder = selected.some((s) => s.id === dish.id);
|
||||
const poster = getMenuPosterSrc(dish, 'optimized');
|
||||
return (
|
||||
<motion.button
|
||||
key={dish.id}
|
||||
whileTap={{ scale: 0.985 }}
|
||||
onClick={() => addDish(dish)}
|
||||
className="group text-left flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-3 active:border-[#B38B4D] hover:border-[#c99a2e]/50 transition overflow-hidden"
|
||||
>
|
||||
<div className="relative h-20 w-20 flex-shrink-0 overflow-hidden rounded-xl bg-[#F2EDE4]">
|
||||
<img
|
||||
src={poster}
|
||||
alt={dish.name}
|
||||
className="absolute inset-0 h-full w-full object-cover group-active:scale-105 transition"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget as HTMLImageElement;
|
||||
target.src = SITE_ASSETS.dishes.defaultPoster;
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/5 to-black/20" />
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col py-0.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="font-medium tracking-[-0.2px] text-[15px] leading-tight line-clamp-2 pr-1">
|
||||
{dish.name}
|
||||
</div>
|
||||
<div className="shrink-0 text-right font-semibold text-[#B38B4D] tabular-nums text-sm leading-tight">
|
||||
{dish.pricing === 'weight' ? (
|
||||
<>
|
||||
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
|
||||
<span className="block text-[10px] font-normal text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{dish.price}
|
||||
<span className="text-[10px] font-normal ml-px">kr</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{dish.description && (
|
||||
<div className="mt-1 text-xs text-[#6B665F] line-clamp-2">
|
||||
{dish.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto pt-2">
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-[#B38B4D] group-active:text-[#8C6B3A]">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{inOrder ? 'Add another' : 'Add to order'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="col-span-full py-12 text-center text-[#6B665F]">
|
||||
No dishes match your search.
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ORDER SUMMARY (sticky on desktop) */}
|
||||
<div className="lg:col-span-5">
|
||||
<div className="lg:sticky lg:top-24">
|
||||
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="font-semibold tracking-tight text-lg">{to.yourOrder}</div>
|
||||
{selected.length > 0 && (
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-xs text-[#B38B4D] hover:underline active:text-[#8C6B3A]"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selected.length === 0 ? (
|
||||
<div className="py-8 text-center">
|
||||
<div className="text-5xl mb-3 opacity-50">🍽️</div>
|
||||
<p className="text-[#6B665F] text-sm">{to.emptyOrder}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 mb-5 max-h-[320px] overflow-auto pr-1">
|
||||
{selected.map((item) => (
|
||||
<div key={item.id} className="flex items-center gap-3 rounded-2xl bg-[#F8F5F0] px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-[15px] tracking-[-0.2px]">{item.name}</div>
|
||||
<div className="text-xs text-[#6B665F] tabular-nums">
|
||||
{item.pricingMode === 'weight'
|
||||
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
|
||||
: `${item.price} kr × ${item.quantity}`}
|
||||
{' '}= {calculateLineTotal(item)} kr
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => updateQty(item.id, item.quantity - 1)}
|
||||
className="h-8 w-8 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95"
|
||||
aria-label="Decrease"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span className="min-w-12 text-center font-medium tabular-nums">
|
||||
{formatLineQuantityLabel(item)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => updateQty(item.id, item.quantity + 1)}
|
||||
className="h-8 w-8 flex items-center justify-center rounded-full border border-[#EDE6D9] bg-white active:bg-white text-lg leading-none active:scale-95"
|
||||
aria-label="Increase"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="ml-1.5 text-[#B38B4D] p-1 active:text-red-600"
|
||||
aria-label="Remove"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div className="mt-2">
|
||||
<label className="block text-xs font-semibold tracking-widest text-[#6B665F] mb-1.5">
|
||||
{to.notesLabel}
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder={to.notesPlaceholder}
|
||||
rows={2}
|
||||
className="w-full resize-y rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] p-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Total + Send */}
|
||||
<div className="mt-5 border-t border-[#EDE6D9] pt-5">
|
||||
<div className="flex items-baseline justify-between text-lg mb-4">
|
||||
<span className="text-[#6B665F]">{to.total}</span>
|
||||
<span className="font-semibold tabular-nums">{selectedTotal} kr</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSendOrder}
|
||||
disabled={selected.length === 0 || isSending}
|
||||
className="w-full rounded-2xl bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] py-4 text-base font-semibold tracking-[0.6px] text-white shadow-lg active:brightness-95 disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{isSending ? 'Sending...' : to.sendOrder}
|
||||
</button>
|
||||
|
||||
<p className="mt-3 text-center text-[11px] text-[#8A8478]">
|
||||
Staff will come to your table to confirm.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Small context line */}
|
||||
<div className="mt-3 text-center text-xs text-[#8A8478] tracking-wider">
|
||||
{branchLabel} • {to.tableLabel} {tableDisplay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Suspense } from 'react';
|
||||
import Navbar from '@/components/Navbar';
|
||||
import OrderFromTableClient from './OrderFromTableClient';
|
||||
|
||||
/**
|
||||
* Server wrapper for /orderfromtable
|
||||
*
|
||||
* We must wrap any component that uses useSearchParams() in a Suspense boundary
|
||||
* (Next.js 16 requirement for pages that read search params).
|
||||
*
|
||||
* This keeps the actual ordering UI fully client-side while satisfying the framework.
|
||||
*/
|
||||
export default function OrderFromTablePage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
<div className="flex min-h-[50vh] items-center justify-center px-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-4 h-8 w-8 animate-pulse rounded-full bg-[#B38B4D]/30" />
|
||||
<p className="text-sm tracking-widest text-[#8A8478]">Loading table...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<OrderFromTableClient />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* SHAHI KITCHEN HOMEPAGE
|
||||
* Version with advanced Signature Menu + elegant hero (pre full Sultan redesign)
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Lenis from "lenis";
|
||||
import { ArrowRight, UtensilsCrossed } from "lucide-react";
|
||||
import Navbar from "@/components/Navbar";
|
||||
import Footer from "@/components/Footer";
|
||||
import { useCart } from "@/components/CartContext";
|
||||
import { useLanguage } from "@/lib/language-context";
|
||||
import { heroBannerSources } from "@/lib/assets";
|
||||
import { menuCategories } from "@/lib/menu-data";
|
||||
import { buildCartLineFromMenuItem } from "@/application/cart/cart-line-builder";
|
||||
import type { MenuItem } from "@/domain/menu/entities";
|
||||
import { getTranslation } from "@/lib/translations";
|
||||
import SignatureMenuMarquee, { type SignatureDish } from "@/components/SignatureMenuMarquee";
|
||||
|
||||
type SignatureFilter = "All" | "Rice" | "Curry" | "Meat" | "Street" | "Roll" | "Sweets";
|
||||
type HomepageDish = SignatureDish & { filterTag: SignatureFilter | null };
|
||||
|
||||
export default function ShahiKitchenHomepage() {
|
||||
const { addToCart } = useCart();
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
const [menuFilter, setMenuFilter] = useState<SignatureFilter>("All");
|
||||
|
||||
// Lenis smooth scroll
|
||||
useEffect(() => {
|
||||
const lenis = new Lenis({
|
||||
duration: 1.1,
|
||||
easing: (t: number) => Math.min(1, 1.001 * (-Math.pow(2, -10 * t) + 1)),
|
||||
smoothWheel: true,
|
||||
});
|
||||
const raf = (time: number) => { lenis.raf(time); requestAnimationFrame(raf); };
|
||||
requestAnimationFrame(raf);
|
||||
return () => lenis.destroy();
|
||||
}, []);
|
||||
|
||||
const getDishFilterTag = (item: MenuItem, categoryId: string): SignatureFilter | null => {
|
||||
if (categoryId === "sweets") return "Sweets";
|
||||
if (categoryId === "street-food") return "Street";
|
||||
if (categoryId === "naan-roll") return "Roll";
|
||||
if (categoryId === "meat") return "Meat";
|
||||
if (item.id.includes("biryani")) return "Rice";
|
||||
if (categoryId === "vegetarian" || categoryId === "chicken") return "Curry";
|
||||
return null;
|
||||
};
|
||||
|
||||
const allDishes = useMemo<HomepageDish[]>(
|
||||
() =>
|
||||
menuCategories.flatMap((category) =>
|
||||
category.items.map((item) => ({
|
||||
...item,
|
||||
desc: item.description ?? "",
|
||||
filterTag: getDishFilterTag(item, category.id),
|
||||
}))
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (menuFilter === "All") return allDishes;
|
||||
return allDishes.filter((dish) => dish.filterTag === menuFilter);
|
||||
}, [allDishes, menuFilter]);
|
||||
const banner = heroBannerSources();
|
||||
|
||||
const addDish = (d: SignatureDish) => {
|
||||
addToCart(buildCartLineFromMenuItem(d));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
{/* HERO - Full Banner Video (no text overlay) */}
|
||||
<section
|
||||
className="relative min-h-[100dvh] overflow-hidden bg-[#fbf7ef]"
|
||||
aria-label="Hero"
|
||||
>
|
||||
|
||||
{/* Banner Video - Responsive sources for proper mobile/iPad visualization (portrait asset) + desktop (landscape).
|
||||
Mobile/iPad (<=1024px) uses the compressed banner_mobile-optimized versions (WebM primary for size/speed, MP4 fallback).
|
||||
These were generated from the provided banner_mobile.mp4 (original kept as source asset). */}
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
className="absolute inset-0 z-0 w-full h-full object-cover
|
||||
object-[50%_10%] sm:object-[50%_12%] md:object-[50%_16%]
|
||||
lg:object-[50%_20%] xl:object-[50%_24%]"
|
||||
onError={(e) => console.error('Hero banner video failed to load', e)}
|
||||
>
|
||||
{/* Mobile + iPad (portrait-friendly framing for better visualization on small screens) */}
|
||||
<source
|
||||
src={banner.mobileWebm}
|
||||
type="video/webm"
|
||||
media="(max-width: 1024px)"
|
||||
/>
|
||||
<source
|
||||
src={banner.mobileMp4}
|
||||
type="video/mp4"
|
||||
media="(max-width: 1024px)"
|
||||
/>
|
||||
{/* Desktop / large screens */}
|
||||
<source src={banner.desktopMp4} type="video/mp4" />
|
||||
</video>
|
||||
|
||||
{/* Cinematic overlays */}
|
||||
<div className="absolute inset-0 z-[1] bg-gradient-to-b from-[#0f5a4a]/30 via-transparent to-[#101724]/50" />
|
||||
<div className="absolute inset-0 z-[2] bg-[radial-gradient(ellipse_at_center,transparent_0%,rgba(16,23,36,0.25)_100%)]" />
|
||||
</section>
|
||||
|
||||
{/* ADVANCED SIGNATURE MENU */}
|
||||
<section id="menu" className="scroll-mt-header section-ornament bg-[#fffdf8] px-6 py-20 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10">
|
||||
<div>
|
||||
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-[#fff6dc] px-4 py-1.5 text-sm font-semibold text-[#60420d]">
|
||||
<UtensilsCrossed className="h-4 w-4" /> {t.signatureMenu.title}
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-[1.75rem] md:text-3xl leading-[1.15] tracking-[-0.8px] break-words text-[#101724]">{t.signatureMenu.title}</h2>
|
||||
</div>
|
||||
<p className="max-w-md text-sm md:text-[15px] font-medium text-[#4b5563]">{t.signatureMenu.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* Filters - modern horizontal scroller on mobile for easy thumb use */}
|
||||
<div className="mb-8 -mx-1 px-1 flex gap-2 overflow-x-auto snap-x snap-mandatory pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{(["All", "Rice", "Curry", "Meat", "Street", "Roll", "Sweets"] as const).map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setMenuFilter(cat)}
|
||||
className={`flex-shrink-0 snap-start rounded-full px-5 py-2.5 text-sm font-bold transition-all active:scale-[0.97] touch-manipulation ${menuFilter === cat ? "bg-[#101724] text-white" : "border border-[#e5e1d7] bg-white text-[#182235] hover:border-[#c99a2e] hover:bg-[#fff6dc] active:bg-[#fff6dc]"}`}
|
||||
>
|
||||
{(t.signatureMenu as any)[`filter${cat}`] || cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SignatureMenuMarquee
|
||||
dishes={filtered}
|
||||
addToCartLabel={t.wishlistDrawer.addToCart}
|
||||
perHalfKgLabel={t.menu.perHalfKg}
|
||||
perKgLabel={t.menu.perKg}
|
||||
onAdd={addDish}
|
||||
/>
|
||||
|
||||
<div className="mt-10 text-center">
|
||||
<a href="/menu" className="inline-flex items-center gap-2 text-sm font-bold tracking-wider text-[#B38B4D] hover:text-[#8C6B3A]">
|
||||
{t.signatureMenu.viewFullMenu} <ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* THE SHAHI EXPERIENCE */}
|
||||
<section id="experience" className="scroll-mt-header section bg-white border-y border-[#EDE6D9]">
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<div className="text-center mb-14">
|
||||
<div className="text-[#B38B4D] text-xs tracking-[3px] mb-3">{language === 'sv' ? 'SHAHI-SÄTTET' : 'THE SHAHI WAY'}</div>
|
||||
<h3 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] whitespace-pre-line">{language === 'sv' ? 'Mer än en måltid.\nEtt ögonblick av Shahi.' : 'More than a meal.\nA moment of Shahi.'}</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{(language === 'sv' ? [
|
||||
{ title: "Den Legendariska Buffén", desc: "Vår berömda lunchbuffé har över 20 roterande rätter — curry, biryani, färsk naan och sötsaker." },
|
||||
{ title: "Shahi Sötsaker", desc: "Hemgjord mithai dagligen. Från färsk jalebi till rasmalai — det perfekta söta avslutet." },
|
||||
{ title: "Varm Gästfrihet", desc: "Oavsett om du är här för en snabb lunch eller familjefest, behandlas du alltid som shahi." },
|
||||
] : [
|
||||
{ title: "The Legendary Buffet", desc: "Our famous lunch buffet features over 20 rotating dishes — curries, biryanis, fresh naan, and sweets." },
|
||||
{ title: "Shahi Sweets", desc: "Homemade mithai made daily. From fresh Jalebi to Rasmalai — the perfect sweet ending." },
|
||||
{ title: "Warm Hospitality", desc: "Whether you're here for a quick lunch or a family celebration, you will always be treated like Shahi." },
|
||||
]).map((item, index) => (
|
||||
<div key={index} className="experience-card group relative border border-[#EDE6D9] p-9 rounded-2xl bg-[#F8F5F0] overflow-hidden">
|
||||
<div className="text-[#B38B4D] text-6xl font-light mb-9 tracking-[-2px]">0{index + 1}</div>
|
||||
<h4 className="text-[29px] tracking-[-0.8px] mb-5 text-[#2C2A26] leading-tight">{item.title}</h4>
|
||||
<p className="text-[#6B665F] text-[15.5px] leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,544 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
import Footer from "@/components/Footer";
|
||||
import { buildBookingWhatsAppMessage } from '@/application/messaging/whatsapp-message-builder';
|
||||
import { isBookingComplete } from '@/domain/booking/validation';
|
||||
import type { BookingDetails, PreOrderLine } from '@/domain/booking/entities';
|
||||
import {
|
||||
addOrderLine,
|
||||
calculateLineTotal,
|
||||
calculateOrderTotal,
|
||||
formatLineQuantityLabel,
|
||||
removeOrderLine,
|
||||
updateOrderLineQuantity,
|
||||
} from '@/domain/shared/order-line';
|
||||
import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder';
|
||||
import type { MenuItem } from '@/domain/menu/entities';
|
||||
import {
|
||||
getMenuPosterSrc,
|
||||
getMenuPosterCandidates,
|
||||
logoUrl,
|
||||
applyNextImageFallback,
|
||||
SITE_ASSETS,
|
||||
} from '@/lib/assets';
|
||||
import { container } from '@/infrastructure/di/container';
|
||||
import { useLanguage } from '@/presentation/providers/language-provider';
|
||||
import { getTranslation } from '@/presentation/i18n/translations';
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { MapPin, Calendar, Clock, Users, User, Phone, Mail, MessageCircle, Plus, Minus, X, ArrowRight, Search, CheckCircle } from "lucide-react";
|
||||
import { menuCategories } from "@/lib/menu-data";
|
||||
import { playAddSound, playHoverSound, playSuccessSound } from "@/lib/sounds";
|
||||
|
||||
/**
|
||||
* =============================================================================
|
||||
* TABLE RESERVATION / BOOKING PAGE
|
||||
* =============================================================================
|
||||
*
|
||||
* Beautiful, theme-matched table booking experience with optional pre-order.
|
||||
* - Swap implemented: Reserve Table now in central nav pills, Login is the gold CTA.
|
||||
* - Form first (location, date, time, guests, name, phone, email, notes).
|
||||
* - After form: full menu selector for pre-ordering (reuse menu-data).
|
||||
* - End: builds rich WhatsApp inquiry (booking + pre-order items) using same number as cart.
|
||||
* - Fully translated (sv default + en/hi/ur).
|
||||
* - Mobile-first, large targets, elegant cards matching /login and site theme.
|
||||
* - Uses framer-motion for reveals, lucide icons.
|
||||
* - No backend: client-side only, like staff login and cart WhatsApp.
|
||||
*
|
||||
* Linked from navbar (after swap) and locations.
|
||||
*/
|
||||
|
||||
export default function ReservePage() {
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
const [booking, setBooking] = useState<BookingDetails>({
|
||||
location: '',
|
||||
date: '',
|
||||
time: '',
|
||||
guests: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
notes: '',
|
||||
});
|
||||
const [preOrder, setPreOrder] = useState<PreOrderLine[]>([]);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
const handleBookingChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setBooking((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => isBookingComplete(booking);
|
||||
|
||||
const handleContinue = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (validateForm()) {
|
||||
setShowMenu(true);
|
||||
// Smooth scroll to menu on mobile
|
||||
setTimeout(() => {
|
||||
const menuEl = document.getElementById('preorder-menu');
|
||||
if (menuEl) menuEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 100);
|
||||
} else {
|
||||
alert('Please fill in all required fields (location, date, time, guests, name, phone).');
|
||||
}
|
||||
};
|
||||
|
||||
// Flatten menu for selector (with search)
|
||||
const allDishes = menuCategories.flatMap((cat) => cat.items);
|
||||
|
||||
const filteredDishes = allDishes.filter((dish) =>
|
||||
dish.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(dish.description && dish.description.toLowerCase().includes(search.toLowerCase()))
|
||||
);
|
||||
|
||||
const addToPreOrder = (dish: MenuItem) => {
|
||||
setPreOrder((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish)));
|
||||
};
|
||||
|
||||
const updatePreOrderQty = (id: string, newQty: number) => {
|
||||
setPreOrder((prev) => updateOrderLineQuantity(prev, id, newQty));
|
||||
};
|
||||
|
||||
const removeFromPreOrder = (id: string) => {
|
||||
setPreOrder((prev) => removeOrderLine(prev, id));
|
||||
};
|
||||
|
||||
const getPreOrderTotal = () => calculateOrderTotal(preOrder);
|
||||
|
||||
const handleSendInquiry = () => {
|
||||
if (!validateForm()) {
|
||||
alert('Please complete the booking form first.');
|
||||
return;
|
||||
}
|
||||
|
||||
playSuccessSound();
|
||||
|
||||
const message = buildBookingWhatsAppMessage(booking, preOrder, {
|
||||
askim: t.booking.askim,
|
||||
backaplan: t.booking.backaplan,
|
||||
});
|
||||
if (!message) return;
|
||||
container.messagingGateway.openWhatsApp(message);
|
||||
setSent(true);
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
setBooking({ location: '', date: '', time: '', guests: '', name: '', phone: '', email: '', notes: '' });
|
||||
setPreOrder([]);
|
||||
setShowMenu(false);
|
||||
setSearch('');
|
||||
setSent(false);
|
||||
};
|
||||
|
||||
const editBooking = () => {
|
||||
setShowMenu(false);
|
||||
setSent(false);
|
||||
};
|
||||
|
||||
// Location options from translations
|
||||
const locationOptions = [
|
||||
{ value: 'askim', label: t.booking.askim },
|
||||
{ value: 'backaplan', label: t.booking.backaplan },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8F5F0] text-[#2C2A26]">
|
||||
<Navbar />
|
||||
|
||||
<main className="pb-20">
|
||||
{/* Elegant Header */}
|
||||
<div className="max-w-5xl mx-auto px-6 text-center">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-[#B38B4D]/10 px-4 py-1 text-[#B38B4D] text-[10px] tracking-[3.5px] font-medium mb-3">
|
||||
EXCLUSIVE EXPERIENCE
|
||||
</div>
|
||||
<h1 className="text-2xl sm:text-[1.75rem] md:text-3xl tracking-[-0.8px] leading-[1.15] mb-2 text-[#101724] break-words">
|
||||
{t.booking.title}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-xl text-sm md:text-[15px] text-[#6B665F] leading-relaxed">
|
||||
{t.booking.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-5xl mx-auto px-6 mt-10">
|
||||
{/* Decorative header info - now ABOVE the form (not on side) so form can be wider */}
|
||||
<div className="mb-10 flex justify-center">
|
||||
<div className="w-full max-w-2xl rounded-3xl border border-[#c99a2e]/20 bg-gradient-to-br from-[#101724] via-[#1a1816] to-[#2C2A26] p-6 sm:p-8 text-white shadow-2xl text-center">
|
||||
<div className="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl border border-[#c99a2e]/30 bg-white/5 p-3 backdrop-blur mx-auto">
|
||||
<img src={logoUrl()} alt="Shahi Kitchen" className="h-full w-full object-contain" />
|
||||
</div>
|
||||
<div className="font-serif text-4xl tracking-[-1.5px] mb-1">Shahi Kitchen</div>
|
||||
<div className="text-sm font-medium tracking-[2.5px] text-[#c99a2e] mb-4">TABLE RESERVATIONS</div>
|
||||
<p className="text-sm leading-relaxed text-white/70 max-w-xs mx-auto mb-4">
|
||||
Two locations. Unforgettable evenings. Pre-order your favorites for a perfect arrival.
|
||||
</p>
|
||||
<div className="text-[10px] uppercase tracking-[2.5px] text-[#c99a2e]/60">ASKIM • BACKAPLAN</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{!sent ? (
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Booking Form Card - now wider (full in max-w-4xl) */}
|
||||
<div className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10">
|
||||
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.formTitle}</h2>
|
||||
<p className="text-[#6B665F] mb-8">Fill in your details to secure your table.</p>
|
||||
|
||||
<form onSubmit={handleContinue} className="space-y-6">
|
||||
{/* Location */}
|
||||
<div>
|
||||
<label htmlFor="location" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.locationLabel}</label>
|
||||
<div className="relative">
|
||||
<MapPin className="pointer-events-none absolute left-4 top-4 z-10 h-5 w-5 text-[#B38B4D]" />
|
||||
<select
|
||||
id="location"
|
||||
name="location"
|
||||
value={booking.location}
|
||||
onChange={handleBookingChange}
|
||||
required
|
||||
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
>
|
||||
<option value="">{t.booking.locationPlaceholder}</option>
|
||||
{locationOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]">▾</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{/* Date */}
|
||||
<div>
|
||||
<label htmlFor="date" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.dateLabel}</label>
|
||||
<div className="relative">
|
||||
<Calendar className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="date"
|
||||
name="date"
|
||||
type="date"
|
||||
value={booking.date}
|
||||
onChange={handleBookingChange}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
required
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<div>
|
||||
<label htmlFor="time" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.timeLabel}</label>
|
||||
<div className="relative">
|
||||
<Clock className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="time"
|
||||
name="time"
|
||||
type="time"
|
||||
value={booking.time}
|
||||
onChange={handleBookingChange}
|
||||
required
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Guests */}
|
||||
<div>
|
||||
<label htmlFor="guests" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.guestsLabel}</label>
|
||||
<div className="relative">
|
||||
<Users className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<select
|
||||
id="guests"
|
||||
name="guests"
|
||||
value={booking.guests}
|
||||
onChange={handleBookingChange}
|
||||
required
|
||||
className="w-full cursor-pointer appearance-none rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-10 text-[17px] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
>
|
||||
<option value="">Select</option>
|
||||
{[1,2,3,4,5,6,7,8,9,10].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-4 top-4 text-[#B38B4D]">▾</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.nameLabel}</label>
|
||||
<div className="relative">
|
||||
<User className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={booking.name}
|
||||
onChange={handleBookingChange}
|
||||
placeholder={t.booking.namePlaceholder}
|
||||
required
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label htmlFor="phone" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.phoneLabel}</label>
|
||||
<div className="relative">
|
||||
<Phone className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
value={booking.phone}
|
||||
onChange={handleBookingChange}
|
||||
placeholder={t.booking.phonePlaceholder}
|
||||
required
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.emailLabel}</label>
|
||||
<div className="relative">
|
||||
<Mail className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={booking.email}
|
||||
onChange={handleBookingChange}
|
||||
placeholder={t.booking.emailPlaceholder}
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label htmlFor="notes" className="mb-2 block text-sm font-semibold tracking-wide text-[#6B665F]">{t.booking.notesLabel}</label>
|
||||
<div className="relative">
|
||||
<MessageCircle className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<textarea
|
||||
id="notes"
|
||||
name="notes"
|
||||
value={booking.notes}
|
||||
onChange={handleBookingChange}
|
||||
placeholder={t.booking.notesPlaceholder}
|
||||
rows={3}
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20 resize-y"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary mt-2 flex w-full items-center justify-center gap-3 rounded-2xl py-4 text-base font-semibold tracking-[0.5px] active:scale-[0.985] transition"
|
||||
>
|
||||
{t.booking.continueBtn}
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Pre-Order Menu Section - revealed after form */}
|
||||
<AnimatePresence>
|
||||
{showMenu && (
|
||||
<motion.div
|
||||
id="preorder-menu"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="rounded-3xl border border-[#EDE6D9] bg-white p-6 sm:p-8 shadow-xl md:p-10"
|
||||
>
|
||||
<h2 className="text-3xl tracking-tight font-serif mb-2">{t.booking.menuTitle}</h2>
|
||||
<p className="text-[#6B665F] mb-6">{t.booking.menuSubtitle}</p>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative mb-6">
|
||||
<Search className="pointer-events-none absolute left-4 top-4 h-5 w-5 text-[#B38B4D]" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t.booking.searchPlaceholder}
|
||||
className="w-full rounded-2xl border border-[#EDE6D9] bg-[#F8F5F0] py-4 pl-12 pr-4 text-[17px] placeholder:text-[#8A8478] transition-all outline-none focus:border-[#B38B4D] focus:bg-white focus:ring-1 focus:ring-[#B38B4D]/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dishes Grid - improved visualization: Name, Poster below, Desc under poster, price, nice Add button */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[320px] sm:max-h-[420px] overflow-auto pr-2">
|
||||
{filteredDishes.length > 0 ? (
|
||||
filteredDishes.map((dish) => {
|
||||
const posterSrc = getMenuPosterSrc(dish);
|
||||
const shortDesc = dish.description ? (dish.description.length > 80 ? dish.description.slice(0, 80) + '...' : dish.description) : '';
|
||||
const isAdded = preOrder.some((p) => p.id === dish.id);
|
||||
return (
|
||||
<motion.div
|
||||
key={dish.id}
|
||||
whileHover={{ y: -2, boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" }}
|
||||
className="flex flex-col border border-[#EDE6D9] rounded-2xl p-3 bg-white hover:border-[#B38B4D]/50 transition-all overflow-hidden"
|
||||
onMouseEnter={() => playHoverSound()}
|
||||
>
|
||||
{/* Item Name */}
|
||||
<div className="font-semibold tracking-[-0.3px] text-base mb-1 text-[#2C2A26] line-clamp-1">{dish.name}</div>
|
||||
|
||||
{/* Poster under name */}
|
||||
<div className="relative w-full h-24 sm:h-28 md:h-32 rounded-xl overflow-hidden border border-[#EDE6D9] mb-2 bg-[#F8F5F0]">
|
||||
<img
|
||||
src={posterSrc}
|
||||
alt={dish.name}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
applyNextImageFallback(
|
||||
e.target as HTMLImageElement,
|
||||
getMenuPosterCandidates(dish),
|
||||
(e.target as HTMLImageElement).src
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/5 via-transparent to-black/10" />
|
||||
</div>
|
||||
|
||||
{/* Description under the poster */}
|
||||
<div className="text-xs text-[#6B665F] leading-snug mb-2 flex-1 line-clamp-2">{shortDesc}</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-auto pt-1">
|
||||
<span className="text-[#B38B4D] font-semibold text-sm tabular-nums leading-tight text-right">
|
||||
{dish.pricing === 'weight' ? (
|
||||
<>
|
||||
<span className="block">{dish.pricePerHalfKg} kr / ½ kg</span>
|
||||
<span className="block text-xs text-[#8A8478]">{dish.pricePerKg} {t.menu.perKg}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{dish.price} kr</span>
|
||||
)}
|
||||
</span>
|
||||
{/* Improved graphics for add button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
playAddSound();
|
||||
addToPreOrder(dish);
|
||||
}}
|
||||
className={`flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold shadow-sm active:scale-[0.96] transition-all ${
|
||||
isAdded
|
||||
? "bg-[#3F5C4A] text-white hover:bg-[#2a4033]"
|
||||
: "bg-[#B38B4D] text-white hover:bg-[#8f6b22] hover:shadow-md"
|
||||
}`}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{isAdded ? "Add More" : t.booking.addToPreOrder}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-[#6B665F] col-span-full text-center py-8">No dishes match your search.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pre-Order Summary */}
|
||||
<div className="mt-8 border-t border-[#EDE6D9] pt-6">
|
||||
<h3 className="font-semibold tracking-wide mb-4 flex items-center gap-2">{t.booking.yourPreOrder} <span className="text-xs text-[#B38B4D] font-normal">({preOrder.length})</span></h3>
|
||||
|
||||
{preOrder.length === 0 ? (
|
||||
<p className="text-sm text-[#6B665F] italic">{t.booking.emptyPreOrder}</p>
|
||||
) : (
|
||||
<div className="space-y-3 mb-4">
|
||||
{preOrder.map((item) => {
|
||||
const dishInfo = allDishes.find((d) => d.id === item.id);
|
||||
const thumb = dishInfo ? getMenuPosterSrc(dishInfo) : SITE_ASSETS.fallbacks.logo;
|
||||
return (
|
||||
<div key={item.id} className="flex items-center gap-3 bg-[#F8F5F0] rounded-2xl px-3 py-2">
|
||||
{/* Small poster thumbnail in summary for better visualization */}
|
||||
<img
|
||||
src={thumb}
|
||||
alt={item.name}
|
||||
className="w-10 h-10 object-cover rounded-lg border border-[#EDE6D9] flex-shrink-0"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm truncate">{item.name}</div>
|
||||
<div className="text-[10px] text-[#6B665F]">
|
||||
{item.pricingMode === 'weight'
|
||||
? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}`
|
||||
: `${item.price} kr × ${item.quantity}`}
|
||||
{' '}= {calculateLineTotal(item)} kr
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button onClick={() => updatePreOrderQty(item.id, item.quantity - 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">-</button>
|
||||
<span className="min-w-10 text-center font-medium text-sm tabular-nums">
|
||||
{formatLineQuantityLabel(item)}
|
||||
</span>
|
||||
<button onClick={() => updatePreOrderQty(item.id, item.quantity + 1)} className="w-7 h-7 flex items-center justify-center border border-[#EDE6D9] rounded text-sm hover:bg-white active:bg-white active:scale-95">+</button>
|
||||
<button onClick={() => removeFromPreOrder(item.id)} className="ml-1 text-[#B38B4D] hover:text-red-600 p-0.5 active:scale-95"><X className="h-4 w-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between text-lg font-medium border-t border-[#EDE6D9] pt-4">
|
||||
<span>{t.booking.total}</span>
|
||||
<span>{getPreOrderTotal()} kr</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSendInquiry}
|
||||
disabled={!validateForm()}
|
||||
className="mt-6 w-full py-4 rounded-2xl text-base font-semibold tracking-[0.5px] flex items-center justify-center gap-3 bg-gradient-to-r from-[#B38B4D] via-[#c99a2e] to-[#B38B4D] text-white shadow-lg hover:brightness-105 active:brightness-95 active:scale-[0.985] transition-all disabled:opacity-60 disabled:cursor-not-allowed disabled:brightness-100"
|
||||
>
|
||||
{t.booking.sendInquiry}
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
</button>
|
||||
<p className="mt-3 text-center text-[11px] text-[#8A8478]">{t.booking.inquiryNote}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
) : (
|
||||
/* Success State */
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="max-w-md mx-auto text-center py-12"
|
||||
>
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-[#B38B4D]/10">
|
||||
<CheckCircle className="h-10 w-10 text-[#B38B4D]" />
|
||||
</div>
|
||||
<h2 className="text-4xl tracking-tight font-serif mb-4">{t.booking.successTitle}</h2>
|
||||
<p className="text-[#6B665F] mb-8 leading-relaxed">{t.booking.successMessage}</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button onClick={editBooking} className="btn-outline px-8 py-3 rounded-full text-sm font-medium">
|
||||
{t.booking.editBooking}
|
||||
</button>
|
||||
<button onClick={resetAll} className="btn-primary px-8 py-3 rounded-full text-sm font-medium">
|
||||
{t.booking.backHome}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import ScreenDisplay from '@/components/ScreenDisplay';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Shahi Kitchen | Display Screen 1',
|
||||
description: 'In-restaurant menu visualization — Screen 1',
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Screen1Page() {
|
||||
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
|
||||
// Makes screen1 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
|
||||
// Pairs with the .screen-root + useEffect in ScreenDisplay for robustness.
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
|
||||
}}
|
||||
/>
|
||||
<ScreenDisplay screen={1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import ScreenDisplay from '@/components/ScreenDisplay';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Shahi Kitchen | Display Screen 2',
|
||||
description: 'In-restaurant menu visualization — Screen 2',
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Screen2Page() {
|
||||
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
|
||||
// Makes screen2 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
|
||||
// Pairs with the .screen-root + useEffect in ScreenDisplay for robustness.
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
|
||||
}}
|
||||
/>
|
||||
<ScreenDisplay screen={2} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Metadata } from 'next';
|
||||
import OrderDisplay from '@/components/OrderDisplay';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Shahi Kitchen | Live Order Display',
|
||||
description: 'Live kitchen order board — in-house tables & online delivery (Foodora etc.) for customers and riders. Real-time status.',
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Screen3Page() {
|
||||
// Early inline script: ensures html/body get kiosk no-scrollbar rules BEFORE React hydrates.
|
||||
// Makes screen3 a perfectly clean full-window display (no web browser scrollbar) even on first paint / F11.
|
||||
// Pairs with .screen-root for the order board.
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function(){var h=document.documentElement,b=document.body;h.classList.add('screen-kiosk');b.classList.add('screen-kiosk');h.style.overflow='hidden';b.style.overflow='hidden';})();`,
|
||||
}}
|
||||
/>
|
||||
<OrderDisplay />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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',
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
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 {
|
||||
if (item.video) {
|
||||
return dishPosterFromVideo(item.video, variant);
|
||||
}
|
||||
if (item.image) {
|
||||
return dishImageUrl(item.image);
|
||||
}
|
||||
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[] {
|
||||
if (!item.video) {
|
||||
return item.image
|
||||
? [dishImageUrl(item.image), SITE_ASSETS.fallbacks.logo]
|
||||
: [SITE_ASSETS.fallbacks.logo];
|
||||
}
|
||||
|
||||
const base = videoBaseName(item.video);
|
||||
return [
|
||||
dishPosterFromVideo(item.video, 'standard'),
|
||||
dishPosterFromVideo(item.video, 'optimized'),
|
||||
...(item.image ? [dishImageUrl(item.image)] : []),
|
||||
mediaUrl(MEDIA_FOLDERS.dishes, `${base}.jpg`),
|
||||
SITE_ASSETS.dishes.defaultPoster,
|
||||
SITE_ASSETS.fallbacks.dishPoster,
|
||||
];
|
||||
}
|
||||
|
||||
/** 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';
|
||||
@@ -1,45 +0,0 @@
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* @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';
|
||||
@@ -1,4 +0,0 @@
|
||||
/** Port: external messaging channel (WhatsApp, SMS, etc.). */
|
||||
export interface MessagingGateway {
|
||||
openWhatsApp(message: string): void;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { BookingDetails } from '@/domain/booking/entities';
|
||||
import { isBookingComplete } from '@/domain/booking/validation';
|
||||
import type { Language } from '@/domain/language/entities';
|
||||
import {
|
||||
calculateOrderTotal,
|
||||
formatOrderLinesForMessage,
|
||||
type OrderLine,
|
||||
} from '@/domain/shared/order-line';
|
||||
import { PICKUP_LEAD_TIME_MINUTES } from '@/domain/shared/constants';
|
||||
|
||||
export interface CartDrawerCopy {
|
||||
messageHello: string;
|
||||
messageIntro: 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 buildCartWhatsAppMessage(
|
||||
items: OrderLine[],
|
||||
copy: CartDrawerCopy,
|
||||
language: Language
|
||||
): string | null {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const lines = formatOrderLinesForMessage(items);
|
||||
const total = calculateOrderTotal(items);
|
||||
const suggestedTime = getSuggestedPickupTime(language);
|
||||
|
||||
return `${copy.messageHello}
|
||||
|
||||
${copy.messageIntro}
|
||||
|
||||
${lines}
|
||||
${copy.messageConfirm.replace('{time}', suggestedTime)}
|
||||
|
||||
${copy.messageTotal}: ${total} kr
|
||||
|
||||
${copy.messageThanks}`;
|
||||
}
|
||||
|
||||
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!`;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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(),
|
||||
};
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* @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';
|
||||
@@ -1,230 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* =============================================================================
|
||||
* CART DRAWER (SLIDE-IN BASKET PANEL)
|
||||
* =============================================================================
|
||||
*
|
||||
* This is the visual "basket" that slides in from the right when the user
|
||||
* clicks the cart icon in the navbar or the "View Cart" toast action.
|
||||
*
|
||||
* KEY DESIGN CHOICES:
|
||||
* - Fixed position, full-height, max-w-md (beautiful on both mobile and desktop)
|
||||
* - Backdrop click closes it (standard mobile pattern)
|
||||
* - z-[990] sits above almost everything (including the sticky category nav) (below mobile menu)
|
||||
* - All cart mutations go through the context — this component is "dumb" UI only
|
||||
*
|
||||
* THE ORDERING FLOW (very important for restaurant context):
|
||||
* Instead of a traditional Stripe checkout, we generate a pre-filled WhatsApp
|
||||
* message containing every line item + quantities + grand total.
|
||||
* This matches the restaurant's current real-world ordering process.
|
||||
* The number 46739381089 is the same one used in the homepage reservation form.
|
||||
*
|
||||
* FUTURE ENHANCEMENTS (documented here so nothing is forgotten):
|
||||
* - Add "Special requests" textarea per order
|
||||
* - Show estimated preparation time
|
||||
* - Allow "Order for later" date/time picker
|
||||
* - Split "Pickup" vs "Delivery" with different messaging
|
||||
* - After WhatsApp opens, optionally clear the cart (or keep it — current choice)
|
||||
*/
|
||||
|
||||
import Link from 'next/link';
|
||||
import { buildCartWhatsAppMessage } from '@/application/messaging/whatsapp-message-builder';
|
||||
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';
|
||||
|
||||
export default function CartDrawer() {
|
||||
const {
|
||||
items,
|
||||
isOpen,
|
||||
closeCart,
|
||||
totalPrice,
|
||||
removeFromCart,
|
||||
updateQuantity,
|
||||
clearCart
|
||||
} = useCart();
|
||||
|
||||
const { language } = useLanguage();
|
||||
const t = getTranslation(language);
|
||||
|
||||
/**
|
||||
* WHATSAPP DEEP LINK ORDERING
|
||||
*
|
||||
* Builds a human-readable, copy-paste friendly message that the restaurant staff
|
||||
* can immediately understand and action.
|
||||
*
|
||||
* The format deliberately mirrors how the restaurant currently receives orders
|
||||
* over the phone or via Instagram DMs.
|
||||
*/
|
||||
const orderViaWhatsApp = () => {
|
||||
const message = buildCartWhatsAppMessage(items, t.cartDrawer, language);
|
||||
if (!message) return;
|
||||
container.messagingGateway.openWhatsApp(message);
|
||||
};
|
||||
|
||||
// Guard clause — drawer only renders when explicitly opened via context
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* SEMI-TRANSPARENT BACKDROP */}
|
||||
{/* Clicking anywhere outside the drawer closes it (standard mobile pattern) */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/40 z-[980]"
|
||||
onClick={closeCart}
|
||||
/>
|
||||
|
||||
{/* THE ACTUAL DRAWER — slides in from right */}
|
||||
{/* z-[990] ensures it sits above sticky nav, category pills, and most other UI (below mobile menu) */}
|
||||
<div className="fixed top-0 right-0 h-full w-full max-w-md bg-[#F8F5F0] z-[990] shadow-2xl flex flex-col">
|
||||
{/* HEADER — Title + item count + quick clear + close button */}
|
||||
<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>
|
||||
|
||||
{/* SCROLLABLE CONTENT AREA */}
|
||||
{/* Empty state is friendly and routes the user back to the menu */}
|
||||
<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>
|
||||
|
||||
{/* Quantity controls */}
|
||||
<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>
|
||||
|
||||
{/* STICKY FOOTER — always visible when cart has items */}
|
||||
{/* Contains the two primary actions: WhatsApp (primary) + Phone (secondary) */}
|
||||
{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-3">
|
||||
{t.cartDrawer.pickupNote}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={orderViaWhatsApp}
|
||||
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"
|
||||
>
|
||||
{t.cartDrawer.sendViaWhatsApp}
|
||||
</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>
|
||||
|
||||
<p className="text-[10px] text-center text-[#8A8478] mt-4">
|
||||
{t.cartDrawer.whatsappHint}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
"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 & bookings</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
'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 (11am–3pm)</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 • Mon–Fri 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>
|
||||
);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
'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 cursor-pointer flex-col overflow-hidden rounded-[1.75rem] border border-[#EDE6D9] bg-white shadow-sm transition-shadow hover:border-[#c99a2e]/45 hover:shadow-md"
|
||||
onClick={() => onAdd(dish)}
|
||||
>
|
||||
<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={(e) => {
|
||||
e.stopPropagation();
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* @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';
|
||||
@@ -1,172 +0,0 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
# 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: 500–900 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.
|
||||
@@ -0,0 +1,365 @@
|
||||
# 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.
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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/`);
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* 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: © {new Date().getFullYear()} {t('site.name')}. {t('f...
|
||||
© {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)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
* 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 A–Z',
|
||||
// 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)
|
||||
};
|
||||
@@ -0,0 +1,934 @@
|
||||
/**
|
||||
* 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)
|
||||
};
|
||||
@@ -0,0 +1,936 @@
|
||||
/**
|
||||
* 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)
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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:00–19: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();
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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
|
||||
];
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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
|
||||
];
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* 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.2–1.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: '500g–1kg',
|
||||
// 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: '500g–1kg',
|
||||
// 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: '500g–1kg',
|
||||
// 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: '1–2 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: '500g–1kg',
|
||||
// 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: '1–1.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)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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: );
|
||||
);
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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: );
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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: );
|
||||
);
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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: );
|
||||
);
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* ANNOTATED COPY — every line explained
|
||||
* Source: src/types/index.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 Category = 'chicken' | 'beef' | 'lamb' | 'fish';
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type CutCount = 4 | 8 | 10 | 12;
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type CuttingStyleKey = 'nihari' | 'karahi' | 'qeema' | 'boneless' | 'steak';
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type MeatCategory = 'chicken' | 'beef' | 'lamb';
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface MeatCustomization {
|
||||
// Line 10: type: MeatCategory;
|
||||
type: MeatCategory;
|
||||
// Line 11: cuts: CutCount;
|
||||
cuts: CutCount;
|
||||
// Line 12: cuttingStyle: CuttingStyleKey;
|
||||
cuttingStyle: CuttingStyleKey;
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type ProductCustomization = MeatCustomization | { type: 'fish' };
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Named export — utility function other files can import
|
||||
export function isMeatCustomization(
|
||||
// Line 18: customization: ProductCustomization
|
||||
customization: ProductCustomization
|
||||
// Line 19: ): customization is MeatCustomization {
|
||||
): customization is MeatCustomization {
|
||||
// Return value from function
|
||||
return customization.type !== 'fish';
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type PriceUnitKey = 'perBird' | 'perPack' | 'perKg';
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type BadgeKey = 'bestseller' | 'chefsPick' | 'premium' | 'popular' | 'freshCatch';
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface Product {
|
||||
// Line 27: id: string;
|
||||
id: string;
|
||||
// Line 28: slug: string;
|
||||
slug: string;
|
||||
// Line 29: category: Category;
|
||||
category: Category;
|
||||
// Line 30: price: number;
|
||||
price: number;
|
||||
// Line 31: priceUnitKey: PriceUnitKey;
|
||||
priceUnitKey: PriceUnitKey;
|
||||
// Line 32: image: string;
|
||||
image: string;
|
||||
// Line 33: images: string[];
|
||||
images: string[];
|
||||
// Line 34: badgeKey?: BadgeKey;
|
||||
badgeKey?: BadgeKey;
|
||||
// Line 35: inStock: boolean;
|
||||
inStock: boolean;
|
||||
// Line 36: featured: boolean;
|
||||
featured: boolean;
|
||||
// Line 37: weight?: string;
|
||||
weight?: string;
|
||||
// Line 38: tags: string[];
|
||||
tags: string[];
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface CartItem {
|
||||
// Line 42: id: string;
|
||||
id: string;
|
||||
// Line 43: product: Product;
|
||||
product: Product;
|
||||
// Line 44: quantity: number;
|
||||
quantity: number;
|
||||
// Line 45: customization: ProductCustomization;
|
||||
customization: ProductCustomization;
|
||||
// Line 46: customizationLabel: string;
|
||||
customizationLabel: string;
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface User {
|
||||
// Line 50: id: string;
|
||||
id: string;
|
||||
// Line 51: name: string;
|
||||
name: string;
|
||||
// Line 52: email: string;
|
||||
email: string;
|
||||
// Line 53: phone: string;
|
||||
phone: string;
|
||||
// Line 54: address: Address;
|
||||
address: Address;
|
||||
// Line 55: createdAt: string;
|
||||
createdAt: string;
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface Address {
|
||||
// Line 59: street: string;
|
||||
street: string;
|
||||
// Line 60: city: string;
|
||||
city: string;
|
||||
// Line 61: state: string;
|
||||
state: string;
|
||||
// Line 62: zip: string;
|
||||
zip: string;
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface Order {
|
||||
// Line 66: id: string;
|
||||
id: string;
|
||||
// Line 67: items: CartItem[];
|
||||
items: CartItem[];
|
||||
// Line 68: total: number;
|
||||
total: number;
|
||||
// Line 69: status: 'pending' | 'confirmed' | 'preparing' | 'out-for-...
|
||||
status: 'pending' | 'confirmed' | 'preparing' | 'out-for-delivery' | 'delivered';
|
||||
// Line 70: createdAt: string;
|
||||
createdAt: string;
|
||||
// Line 71: deliveryAddress: Address;
|
||||
deliveryAddress: Address;
|
||||
// Line 72: paymentMethod: string;
|
||||
paymentMethod: string;
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type SortOption = 'featured' | 'price-asc' | 'price-desc' | 'name';
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export type OfferBadgeKey = 'fresh' | 'halal';
|
||||
// (blank line — separates logical blocks for readability)
|
||||
// Export TypeScript type — defines data shape used across the app
|
||||
export interface WeeklyOffer {
|
||||
// Line 80: id: string;
|
||||
id: string;
|
||||
// Line 81: nameKey: string;
|
||||
nameKey: string;
|
||||
// Line 82: badgeKey: OfferBadgeKey;
|
||||
badgeKey: OfferBadgeKey;
|
||||
// Line 83: price: number;
|
||||
price: number;
|
||||
// Line 84: originalPrice?: number;
|
||||
originalPrice?: number;
|
||||
// Line 85: priceUnitKey: PriceUnitKey;
|
||||
priceUnitKey: PriceUnitKey;
|
||||
// Line 86: image: string;
|
||||
image: string;
|
||||
// Line 87: whatsappProduct: string;
|
||||
whatsappProduct: string;
|
||||
// Line 88: productSlug: string;
|
||||
productSlug: string;
|
||||
// Closing brace — end of block (function, if, object, JSX)
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Generates Kottgard-Website-Guide.docx
|
||||
* Run: node docs/generate-guide-docx.mjs
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
HeadingLevel,
|
||||
AlignmentType,
|
||||
BorderStyle,
|
||||
WidthType,
|
||||
ShadingType,
|
||||
PageBreak,
|
||||
LevelFormat,
|
||||
} from 'docx';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const outPath = path.join(__dirname, 'Kottgard-Website-Guide.docx');
|
||||
|
||||
const border = { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' };
|
||||
const borders = { top: border, bottom: border, left: border, right: border };
|
||||
const tableWidth = 9360;
|
||||
|
||||
function cell(text, width, fill = 'FFFFFF') {
|
||||
return new TableCell({
|
||||
borders,
|
||||
width: { size: width, type: WidthType.DXA },
|
||||
shading: { fill, type: ShadingType.CLEAR },
|
||||
margins: { top: 80, bottom: 80, left: 120, right: 120 },
|
||||
children: [new Paragraph({ children: [new TextRun(text)] })],
|
||||
});
|
||||
}
|
||||
|
||||
function headerRow(cols, widths) {
|
||||
return new TableRow({
|
||||
children: cols.map((t, i) => cell(t, widths[i], 'D5E8F0')),
|
||||
});
|
||||
}
|
||||
|
||||
function dataRow(cols, widths) {
|
||||
return new TableRow({
|
||||
children: cols.map((t, i) => cell(t, widths[i])),
|
||||
});
|
||||
}
|
||||
|
||||
function table(columnWidths, rows) {
|
||||
return new Table({
|
||||
width: { size: tableWidth, type: WidthType.DXA },
|
||||
columnWidths,
|
||||
rows,
|
||||
});
|
||||
}
|
||||
|
||||
function h1(text) {
|
||||
return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun(text)] });
|
||||
}
|
||||
function h2(text) {
|
||||
return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun(text)] });
|
||||
}
|
||||
function p(text) {
|
||||
return new Paragraph({ spacing: { after: 200 }, children: [new TextRun(text)] });
|
||||
}
|
||||
function bullet(ref, text) {
|
||||
return new Paragraph({
|
||||
numbering: { reference: ref, level: 0 },
|
||||
children: [new TextRun(text)],
|
||||
});
|
||||
}
|
||||
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: 'Arial', size: 22 } } },
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: 'Heading1',
|
||||
name: 'Heading 1',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
quickFormat: true,
|
||||
run: { size: 32, bold: true, font: 'Arial' },
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 },
|
||||
},
|
||||
{
|
||||
id: 'Heading2',
|
||||
name: 'Heading 2',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
quickFormat: true,
|
||||
run: { size: 26, bold: true, font: 'Arial' },
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
numbering: {
|
||||
config: [
|
||||
{
|
||||
reference: 'bullets',
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.BULLET,
|
||||
text: '\u2022',
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
page: {
|
||||
size: { width: 12240, height: 15840 },
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
|
||||
},
|
||||
},
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 400 },
|
||||
children: [
|
||||
new TextRun({ text: 'Kött Gård', bold: true, size: 48 }),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
spacing: { after: 600 },
|
||||
children: [
|
||||
new TextRun({
|
||||
text: 'Website Visual & Programming Guide',
|
||||
size: 32,
|
||||
color: '8B1F1F',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
p('Premium Halal meat e-commerce — Märsta, Sweden'),
|
||||
p('Document version: June 2026'),
|
||||
p('Companion files: docs/annotated/ (line-by-line code comments)'),
|
||||
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1('1. What is this website?'),
|
||||
p(
|
||||
'Kött Gård is a Next.js e-commerce front-end for ordering fresh Halal chicken, beef, lamb, and fish. Customers browse products, customize cuts (Nihari, Karahi, piece counts), add to cart, checkout, and view order history. Languages: Swedish, English, Urdu (RTL).'
|
||||
),
|
||||
|
||||
h2('Demo login'),
|
||||
table(
|
||||
[3120, 6240],
|
||||
[
|
||||
headerRow(['Field', 'Value'], [3120, 6240]),
|
||||
dataRow(['Email', 'demo@kottgard.se'], [3120, 6240]),
|
||||
dataRow(['Password', 'demo123'], [3120, 6240]),
|
||||
dataRow(['Orders', '3 sample orders in Account'], [3120, 6240]),
|
||||
]
|
||||
),
|
||||
|
||||
h1('2. Homepage layout (visual map)'),
|
||||
p('When a customer opens https://kottgard.se/, they scroll through these sections:'),
|
||||
table(
|
||||
[800, 2200, 3160, 3200],
|
||||
[
|
||||
headerRow(['#', 'Section', 'File', 'Customer action'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['1', 'Hero', 'Hero.tsx', 'Shop now / WhatsApp'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['2', 'Trust badges', 'TrustBadges.tsx', 'Click → About'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['3', 'About preview', 'AboutPreview.tsx', 'Read more → /about'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['4', 'Categories', 'CategoryGrid.tsx', 'Chicken/Beef/Lamb/Fish'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['5', 'Featured', 'FeaturedProducts.tsx', 'Product cards'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['6', 'How it works', 'HowItWorks.tsx', 'Order steps'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['7', 'Weekly offers', 'WeeklyOffers.tsx', 'Deals → products'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['8', 'CTA', 'CTA.tsx', 'Browse selection'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['9', 'Social', 'SocialFollow.tsx', 'Facebook / Instagram'], [800, 2200, 3160, 3200]),
|
||||
dataRow(['10', 'Contact', 'ContactPreview.tsx', 'Map / phone / about'], [800, 2200, 3160, 3200]),
|
||||
]
|
||||
),
|
||||
|
||||
h1('3. All pages & routes'),
|
||||
table(
|
||||
[2800, 3360, 3200],
|
||||
[
|
||||
headerRow(['URL', 'File', 'Description'], [2800, 3360, 3200]),
|
||||
dataRow(['/', 'app/page.tsx', 'Homepage'], [2800, 3360, 3200]),
|
||||
dataRow(['/shop', 'app/shop/page.tsx', 'Catalog + search + filters'], [2800, 3360, 3200]),
|
||||
dataRow(['/product/[slug]', 'app/product/[slug]/page.tsx', 'Detail + customize'], [2800, 3360, 3200]),
|
||||
dataRow(['/cart', 'app/cart/page.tsx', 'Shopping cart'], [2800, 3360, 3200]),
|
||||
dataRow(['/checkout', 'app/checkout/page.tsx', 'Place order (demo)'], [2800, 3360, 3200]),
|
||||
dataRow(['/login', 'app/login/page.tsx', 'Auth'], [2800, 3360, 3200]),
|
||||
dataRow(['/account', 'app/account/page.tsx', 'Profile + orders'], [2800, 3360, 3200]),
|
||||
dataRow(['/wishlist', 'app/wishlist/page.tsx', 'Saved items'], [2800, 3360, 3200]),
|
||||
dataRow(['/about', 'app/about/page.tsx', 'Company + privacy + terms'], [2800, 3360, 3200]),
|
||||
]
|
||||
),
|
||||
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1('4. Programming languages & frameworks'),
|
||||
h2('TypeScript'),
|
||||
p(
|
||||
'TypeScript adds types to JavaScript. Example: Product interface ensures every product has id, slug, price. The compiler errors if you typo product.slugg.'
|
||||
),
|
||||
h2('React'),
|
||||
p(
|
||||
'React builds UI from components (functions returning JSX). State (useState) updates what users see. Props pass data parent → child.'
|
||||
),
|
||||
h2('Next.js 13 App Router'),
|
||||
bullet('bullets', 'app/ folder = routes. page.tsx = page, layout.tsx = wrapper.'),
|
||||
bullet('bullets', 'Server Components default — less JavaScript sent to browser.'),
|
||||
bullet('bullets', "'use client' = component runs in browser (hooks, clicks)."),
|
||||
h2('Zustand'),
|
||||
p(
|
||||
'Global stores: useCartStore, useAuthStore, useWishlistStore. persist middleware saves to localStorage so cart survives refresh.'
|
||||
),
|
||||
h2('Tailwind CSS'),
|
||||
p(
|
||||
'Classes like bg-brand-700 text-white px-4 py-2 style elements. Colors defined in tailwind.config.ts (burgundy, cream, gold).'
|
||||
),
|
||||
|
||||
h1('5. Data flow diagram (text)'),
|
||||
p('Homepage → Shop (filter URL) → Product page → addItem() → Cart store → Checkout → Order in Auth store → Account page'),
|
||||
p('Images: lib/images.ts → products.ts → ProductCard / Hero → AppImage → next/image → Unsplash CDN'),
|
||||
p('Text: i18n/locales/sv|en|ur.ts → useTranslation() → t("key") in components'),
|
||||
|
||||
h1('6. Annotated code (line-by-line)'),
|
||||
p('Location: Kottgard/docs/annotated/'),
|
||||
table(
|
||||
[4200, 5160],
|
||||
[
|
||||
headerRow(['File', 'Explains'], [4200, 5160]),
|
||||
dataRow(['01-homepage.annotated.tsx', 'How homepage composes sections'], [4200, 5160]),
|
||||
dataRow(['02-root-layout.annotated.tsx', 'Fonts, SEO, shell layout'], [4200, 5160]),
|
||||
dataRow(['03-shop-page.annotated.tsx', 'URL sync, useMemo filtering'], [4200, 5160]),
|
||||
dataRow(['04-header.annotated.tsx', 'Navigation + Zustand badges'], [4200, 5160]),
|
||||
dataRow(['05-images.annotated.ts', 'Unsplash URL builder'], [4200, 5160]),
|
||||
dataRow(['06-cart-store.annotated.ts', 'Cart logic + persist'], [4200, 5160]),
|
||||
]
|
||||
),
|
||||
p(
|
||||
'Each line has comments explaining WHAT the code does and WHY. These are learning copies — not imported by the live app.'
|
||||
),
|
||||
|
||||
h1('7. Brand colors'),
|
||||
table(
|
||||
[3120, 3120, 3120],
|
||||
[
|
||||
headerRow(['Name', 'Hex', 'Use'], [3120, 3120, 3120]),
|
||||
dataRow(['Burgundy (brand)', '#8B1F1F', 'Header, buttons, footer'], [3120, 3120, 3120]),
|
||||
dataRow(['Cream', 'Warm off-white', 'Page backgrounds'], [3120, 3120, 3120]),
|
||||
dataRow(['Gold', 'Accent', 'Prices, badges, CTAs'], [3120, 3120, 3120]),
|
||||
]
|
||||
),
|
||||
|
||||
h1('8. How to run locally'),
|
||||
bullet('bullets', 'npm install'),
|
||||
bullet('bullets', 'npm run dev → http://localhost:3000'),
|
||||
bullet('bullets', 'npm run build → production check'),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const buffer = await Packer.toBuffer(doc);
|
||||
fs.writeFileSync(outPath, buffer);
|
||||
console.log('Written:', outPath);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user