diff --git a/next.config.js b/next.config.js index b55722b..f27b9ca 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,16 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + // Bundle zustand reliably — avoids missing ./vendor-chunks/zustand.js in dev + transpilePackages: ['zustand'], + async redirects() { + return [ + { + source: '/index', + destination: '/', + permanent: true, + }, + ]; + }, images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 2560], diff --git a/package.json b/package.json index c971b98..095a4e0 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,15 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "clean": "rm -rf .next", + "dev": "bash scripts/dev.sh", + "build": "bash scripts/build.sh", "start": "next start", "lint": "next lint", "annotate": "node docs/annotate-all.mjs", "images:populate": "bash scripts/populate-site-images.sh", - "images:fish": "bash scripts/update-fish-images.sh" + "images:fish": "bash scripts/update-fish-images.sh", + "images:brand": "bash -c '.venv-images/bin/python scripts/process-all-site-images.py 2>/dev/null || python3 scripts/process-all-site-images.py'" }, "dependencies": { "clsx": "^2.1.0", diff --git a/public/images/site/about.jpg b/public/images/site/about.jpg index 032a9cc..00cbc70 100644 Binary files a/public/images/site/about.jpg and b/public/images/site/about.jpg differ diff --git a/public/images/site/beef-boneless.jpg b/public/images/site/beef-boneless.jpg index c1e70f5..7286687 100644 Binary files a/public/images/site/beef-boneless.jpg and b/public/images/site/beef-boneless.jpg differ diff --git a/public/images/site/beef-mince.jpg b/public/images/site/beef-mince.jpg index 95921ba..68a15cf 100644 Binary files a/public/images/site/beef-mince.jpg and b/public/images/site/beef-mince.jpg differ diff --git a/public/images/site/beef-nihari-2.jpg b/public/images/site/beef-nihari-2.jpg index f768e81..72b4ea9 100644 Binary files a/public/images/site/beef-nihari-2.jpg and b/public/images/site/beef-nihari-2.jpg differ diff --git a/public/images/site/beef-nihari.jpg b/public/images/site/beef-nihari.jpg index aff6f88..72b4ea9 100644 Binary files a/public/images/site/beef-nihari.jpg and b/public/images/site/beef-nihari.jpg differ diff --git a/public/images/site/beef-steak-2.jpg b/public/images/site/beef-steak-2.jpg index aff6f88..b1f2c21 100644 Binary files a/public/images/site/beef-steak-2.jpg and b/public/images/site/beef-steak-2.jpg differ diff --git a/public/images/site/beef-steak.jpg b/public/images/site/beef-steak.jpg index f768e81..b1f2c21 100644 Binary files a/public/images/site/beef-steak.jpg and b/public/images/site/beef-steak.jpg differ diff --git a/public/images/site/category-beef.jpg b/public/images/site/category-beef.jpg index 126e98c..7286687 100644 Binary files a/public/images/site/category-beef.jpg and b/public/images/site/category-beef.jpg differ diff --git a/public/images/site/category-chicken.jpg b/public/images/site/category-chicken.jpg index 21afdfa..3119be2 100644 Binary files a/public/images/site/category-chicken.jpg and b/public/images/site/category-chicken.jpg differ diff --git a/public/images/site/category-fish.jpg b/public/images/site/category-fish.jpg index eb31de8..80ec1f3 100644 Binary files a/public/images/site/category-fish.jpg and b/public/images/site/category-fish.jpg differ diff --git a/public/images/site/category-lamb.jpg b/public/images/site/category-lamb.jpg index dfcdb2d..7ebfbf7 100644 Binary files a/public/images/site/category-lamb.jpg and b/public/images/site/category-lamb.jpg differ diff --git a/public/images/site/chicken-breast copy.jpg b/public/images/site/chicken-breast copy.jpg new file mode 100644 index 0000000..c4110ee Binary files /dev/null and b/public/images/site/chicken-breast copy.jpg differ diff --git a/public/images/site/chicken-breast-2.jpg b/public/images/site/chicken-breast-2.jpg index abd5ecd..e63fa49 100644 Binary files a/public/images/site/chicken-breast-2.jpg and b/public/images/site/chicken-breast-2.jpg differ diff --git a/public/images/site/chicken-breast.jpg b/public/images/site/chicken-breast.jpg index d7de864..e63fa49 100644 Binary files a/public/images/site/chicken-breast.jpg and b/public/images/site/chicken-breast.jpg differ diff --git a/public/images/site/chicken-thighs.jpg b/public/images/site/chicken-thighs.jpg index 9029e8f..74cae3b 100644 Binary files a/public/images/site/chicken-thighs.jpg and b/public/images/site/chicken-thighs.jpg differ diff --git a/public/images/site/chicken-whole-1.jpg b/public/images/site/chicken-whole-1.jpg new file mode 100644 index 0000000..1309d47 Binary files /dev/null and b/public/images/site/chicken-whole-1.jpg differ diff --git a/public/images/site/chicken-whole-2.jpg b/public/images/site/chicken-whole-2.jpg index d7de864..1309d47 100644 Binary files a/public/images/site/chicken-whole-2.jpg and b/public/images/site/chicken-whole-2.jpg differ diff --git a/public/images/site/chicken-whole.jpg b/public/images/site/chicken-whole.jpg index abd5ecd..1309d47 100644 Binary files a/public/images/site/chicken-whole.jpg and b/public/images/site/chicken-whole.jpg differ diff --git a/public/images/site/chicken-wings.jpg b/public/images/site/chicken-wings.jpg index 40c29ce..8429377 100644 Binary files a/public/images/site/chicken-wings.jpg and b/public/images/site/chicken-wings.jpg differ diff --git a/public/images/site/fish-basa.jpg b/public/images/site/fish-basa.jpg index 1fcbe16..d6a301d 100644 Binary files a/public/images/site/fish-basa.jpg and b/public/images/site/fish-basa.jpg differ diff --git a/public/images/site/fish-prawns.jpg b/public/images/site/fish-prawns.jpg index 51a2496..779efbb 100644 Binary files a/public/images/site/fish-prawns.jpg and b/public/images/site/fish-prawns.jpg differ diff --git a/public/images/site/fish-rohu.jpg b/public/images/site/fish-rohu.jpg index b8f45e9..3371998 100644 Binary files a/public/images/site/fish-rohu.jpg and b/public/images/site/fish-rohu.jpg differ diff --git a/public/images/site/fish-salmon-2.jpg b/public/images/site/fish-salmon-2.jpg index ed0aacf..97edff6 100644 Binary files a/public/images/site/fish-salmon-2.jpg and b/public/images/site/fish-salmon-2.jpg differ diff --git a/public/images/site/fish-salmon.jpg b/public/images/site/fish-salmon.jpg index f8197a5..41585ad 100644 Binary files a/public/images/site/fish-salmon.jpg and b/public/images/site/fish-salmon.jpg differ diff --git a/public/images/site/hero.jpg b/public/images/site/hero.jpg index 77ee3ae..392e164 100644 Binary files a/public/images/site/hero.jpg and b/public/images/site/hero.jpg differ diff --git a/public/images/site/lamb-chops.jpg b/public/images/site/lamb-chops.jpg index 1635e44..6e5910b 100644 Binary files a/public/images/site/lamb-chops.jpg and b/public/images/site/lamb-chops.jpg differ diff --git a/public/images/site/lamb-leg copy.jpg b/public/images/site/lamb-leg copy.jpg new file mode 100644 index 0000000..41fda1f Binary files /dev/null and b/public/images/site/lamb-leg copy.jpg differ diff --git a/public/images/site/lamb-leg.jpg b/public/images/site/lamb-leg.jpg index 322ac2d..41fda1f 100644 Binary files a/public/images/site/lamb-leg.jpg and b/public/images/site/lamb-leg.jpg differ diff --git a/public/images/site/lamb-mince.jpg b/public/images/site/lamb-mince.jpg index 5e4b8c2..68a15cf 100644 Binary files a/public/images/site/lamb-mince.jpg and b/public/images/site/lamb-mince.jpg differ diff --git a/public/images/site/lamb-shoulder-2.jpg b/public/images/site/lamb-shoulder-2.jpg index 322ac2d..7ebfbf7 100644 Binary files a/public/images/site/lamb-shoulder-2.jpg and b/public/images/site/lamb-shoulder-2.jpg differ diff --git a/public/images/site/lamb-shoulder.jpg b/public/images/site/lamb-shoulder.jpg index dfcdb2d..7ebfbf7 100644 Binary files a/public/images/site/lamb-shoulder.jpg and b/public/images/site/lamb-shoulder.jpg differ diff --git a/public/images/site/weekly-offers.jpg b/public/images/site/weekly-offers.jpg index 77ee3ae..392e164 100644 Binary files a/public/images/site/weekly-offers.jpg and b/public/images/site/weekly-offers.jpg differ diff --git a/scripts/branded-beef-boneless.py b/scripts/branded-beef-boneless.py new file mode 100644 index 0000000..8e136d2 --- /dev/null +++ b/scripts/branded-beef-boneless.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Prepare a sharp, full-frame beef boneless product photo for the shop.""" + +from __future__ import annotations + +import argparse +import urllib.request +from pathlib import Path + +from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageStat + +ROOT = Path(__file__).resolve().parents[1] +SITE = ROOT / "public" / "images" / "site" +OUT_PATH = SITE / "beef-boneless.jpg" +SOURCE_CACHE = SITE / "_beef_boneless_src.jpg" + +# High-res premium raw boneless beef (Unsplash) +PRIMARY_URL = ( + "https://images.unsplash.com/photo-1603048297172-c92544798d5a" + "?auto=format&fit=crop&w=1800&h=1800&crop=center&q=95" +) +# Original product reference (lower res fallback) +FALLBACK_URL = ( + "https://www.hotcurrymarket.fi/wp-content/uploads/2025/01/" + "FRESH-BEEF-BONELESS-WITHOUT-FAT-1KG.jpg" +) + +OUTPUT_SIZE = (1400, 1400) + + +def download(url: str, dest: Path) -> None: + req = urllib.request.Request( + url, + headers={"User-Agent": "Mozilla/5.0 (compatible; Kottgard-site-builder/1.0)"}, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + dest.write_bytes(resp.read()) + + +def trim_near_white(img: Image.Image, tolerance: int = 18) -> Image.Image: + """Remove excess light borders from the source photo.""" + rgb = img.convert("RGB") + pixels = rgb.load() + w, h = rgb.size + + def row_is_border(y: int) -> bool: + samples = [pixels[x, y] for x in range(0, w, max(1, w // 24))] + return all(min(p) >= 255 - tolerance for p in samples) + + def col_is_border(x: int) -> bool: + samples = [pixels[x, y] for y in range(0, h, max(1, h // 24))] + return all(min(p) >= 255 - tolerance for p in samples) + + top = 0 + while top < h and row_is_border(top): + top += 1 + bottom = h - 1 + while bottom > top and row_is_border(bottom): + bottom -= 1 + left = 0 + while left < w and col_is_border(left): + left += 1 + right = w - 1 + while right > left and col_is_border(right): + right -= 1 + + if right - left > 80 and bottom - top > 80: + return rgb.crop((left, top, right + 1, bottom + 1)) + return rgb + + +def enhance_photo(img: Image.Image) -> Image.Image: + img = ImageOps.autocontrast(img, cutoff=0.5) + img = ImageEnhance.Brightness(img).enhance(1.03) + img = ImageEnhance.Contrast(img).enhance(1.08) + img = ImageEnhance.Color(img).enhance(1.1) + img = ImageEnhance.Sharpness(img).enhance(1.35) + return img.filter(ImageFilter.UnsharpMask(radius=1.0, percent=110, threshold=2)) + + +def crop_center_cover(img: Image.Image, size: tuple[int, int]) -> Image.Image: + tw, th = size + w, h = img.size + scale = max(tw / w, th / h) + nw, nh = int(w * scale), int(h * scale) + resized = img.resize((nw, nh), Image.Resampling.LANCZOS) + left = (nw - tw) // 2 + top = (nh - th) // 2 + return resized.crop((left, top, left + tw, top + th)) + + +def prepare_product_image(source: Path, output: Path = OUT_PATH) -> None: + beef = Image.open(source).convert("RGB") + beef = trim_near_white(beef) + beef = enhance_photo(beef) + # Full-frame crop — meat fills the product image (no tiny subject in white void) + beef = crop_center_cover(beef, OUTPUT_SIZE) + beef.save(output, "JPEG", quality=96, optimize=True, subsampling=0) + print(f"Saved product photo {OUTPUT_SIZE[0]}x{OUTPUT_SIZE[1]} → {output}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Prepare beef boneless product photo") + parser.add_argument("--url", default=PRIMARY_URL) + parser.add_argument("--input", type=Path) + parser.add_argument("--output", type=Path, default=OUT_PATH) + parser.add_argument("--use-cache", action="store_true") + parser.add_argument("--fallback", action="store_true", help="Use hot curry source") + args = parser.parse_args() + + if args.input: + src = args.input + elif args.use_cache and SOURCE_CACHE.exists(): + src = SOURCE_CACHE + print(f"Using cached source → {src}") + else: + url = FALLBACK_URL if args.fallback else args.url + print(f"Downloading source image…\n {url}") + try: + download(url, SOURCE_CACHE) + except Exception as exc: + print(f"Primary download failed ({exc}), trying fallback…") + download(FALLBACK_URL, SOURCE_CACHE) + src = SOURCE_CACHE + + prepare_product_image(src, args.output) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..462df67 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Production build must not reuse a dev .next folder. +set -euo pipefail +cd "$(dirname "$0")/.." + +pkill -f "next dev" 2>/dev/null || true +rm -rf .next +echo "Building production bundle (clean cache)…" +exec npx next build \ No newline at end of file diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..34ec266 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Always start dev with a fresh .next cache — prevents missing vendor-chunks (zustand.js). +set -euo pipefail +cd "$(dirname "$0")/.." + +pkill -f "next dev" 2>/dev/null || true +if lsof -ti:3000 >/dev/null 2>&1; then + lsof -ti:3000 | xargs kill -9 2>/dev/null || true + sleep 1 +fi + +rm -rf .next +echo "Starting Next.js dev server (clean cache)…" +exec npx next dev \ No newline at end of file diff --git a/scripts/process-all-site-images.py b/scripts/process-all-site-images.py new file mode 100644 index 0000000..7cc1c36 --- /dev/null +++ b/scripts/process-all-site-images.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Normalize site images: square canvas, HALAL badge + Kött Gård logo. Colors preserved.""" + +from __future__ import annotations + +import argparse +import shutil +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[1] +SITE = ROOT / "public" / "images" / "site" +BACKUP = SITE / "_originals" +LOGO_PATH = SITE / "logo.jpeg" +BRAND_NAME = "Kött Gård" + +SIZE = (1400, 1400) +BG = (255, 255, 255) +BURGUNDY = (139, 31, 31) +GOLD = (212, 175, 55) + +SKIP = {"logo.jpeg", "README.txt"} +SKIP_PATTERNS = (" copy", "-1.jpg") # duplicate uploads from user +FILL_PRODUCT = 0.9 +FILL_PAGE = 0.94 + +PAGE_IMAGES = {"hero.jpg", "about.jpg", "weekly-offers.jpg"} +CATEGORY_IMAGES = { + "category-chicken.jpg", + "category-beef.jpg", + "category-lamb.jpg", + "category-fish.jpg", +} + + +def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + paths = [ + "/System/Library/Fonts/Supplemental/Arial Bold.ttf" + if bold + else "/System/Library/Fonts/Supplemental/Arial.ttf", + "/Library/Fonts/Arial.ttf", + ] + for p in paths: + try: + return ImageFont.truetype(p, size) + except OSError: + continue + return ImageFont.load_default() + + +def preserve_colors(img: Image.Image) -> Image.Image: + """Keep original photo pixels — no contrast, brightness, or color tweaks.""" + return img.convert("RGB") + + +def prep_logo(diameter: int) -> Image.Image: + logo = Image.open(LOGO_PATH).convert("RGBA") + w, h = logo.size + side = min(w, h) + logo = logo.crop(((w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2)) + logo = logo.resize((diameter, diameter), Image.Resampling.LANCZOS) + mask = Image.new("L", (diameter, diameter), 0) + ImageDraw.Draw(mask).ellipse((0, 0, diameter - 1, diameter - 1), fill=255) + logo.putalpha(mask) + return logo + + +def draw_halal_badge(canvas: Image.Image, x: int, y: int, size: int = 130) -> None: + layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(layer) + + draw.ellipse((x + 4, y + 5, x + size + 4, y + size + 5), fill=(0, 0, 0, 100)) + draw.ellipse((x - 2, y - 2, x + size + 2, y + size + 2), fill=(*GOLD, 255)) + draw.ellipse((x + 6, y + 6, x + size - 6, y + size - 6), fill=(255, 255, 255, 255)) + draw.ellipse((x + 10, y + 10, x + size - 10, y + size - 10), outline=(*BURGUNDY, 255), width=4) + + f_ar = load_font(24, bold=True) + f_halal = load_font(28, bold=True) + f_sub = load_font(11, bold=True) + + ar = draw.textbbox((0, 0), "حلال", font=f_ar) + draw.text((x + (size - ar[2] + ar[0]) // 2, y + 22), "حلال", fill=(*BURGUNDY, 255), font=f_ar) + hl = draw.textbbox((0, 0), "HALAL", font=f_halal) + draw.text((x + (size - hl[2] + hl[0]) // 2, y + 52), "HALAL", fill=(*BURGUNDY, 255), font=f_halal) + ce = draw.textbbox((0, 0), "CERTIFIED", font=f_sub) + draw.text((x + (size - ce[2] + ce[0]) // 2, y + 92), "CERTIFIED", fill=(*GOLD, 255), font=f_sub) + + canvas.alpha_composite(layer) + + +def draw_logo_badge(canvas: Image.Image, logo: Image.Image, x: int, y: int) -> None: + d = logo.size[0] + layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(layer) + draw.ellipse((x + 3, y + 4, x + d + 3, y + d + 4), fill=(0, 0, 0, 90)) + draw.ellipse((x - 4, y - 4, x + d + 4, y + d + 4), fill=(255, 255, 255, 245), outline=(*GOLD, 255), width=3) + canvas.alpha_composite(layer) + canvas.alpha_composite(logo, (x, y)) + + +def draw_brand_name(canvas: Image.Image, x: int, y: int) -> None: + """Brand name pill beside the logo badge.""" + layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(layer) + font = load_font(22, bold=True) + bbox = draw.textbbox((0, 0), BRAND_NAME, font=font) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + pad_x, pad_y = 14, 8 + box = (x - tw - pad_x * 2, y, x - 8, y + th + pad_y * 2) + draw.rounded_rectangle(box, radius=10, fill=(255, 255, 255, 235), outline=(*GOLD, 255), width=2) + draw.text((box[0] + pad_x, box[1] + pad_y - 2), BRAND_NAME, fill=(*BURGUNDY, 255), font=font) + canvas.alpha_composite(layer) + + +def already_branded(path: Path) -> bool: + """Skip images already at catalog size (user uploads with logos baked in).""" + with Image.open(path) as img: + return img.size == SIZE + + +def should_skip(path: Path) -> bool: + if path.name in SKIP or path.name.startswith("_"): + return True + return any(p in path.name for p in SKIP_PATTERNS) + + +def compose(source: Path, dest: Path, logo: Image.Image) -> None: + photo = preserve_colors(Image.open(source)) + fill = FILL_PAGE if dest.name in PAGE_IMAGES else FILL_PRODUCT + if dest.name in CATEGORY_IMAGES: + fill = 0.92 + + canvas = Image.new("RGBA", SIZE, (*BG, 255)) + max_side = int(SIZE[0] * fill) + photo.thumbnail((max_side, max_side), Image.Resampling.LANCZOS) + + shadow = Image.new("RGBA", photo.size, (0, 0, 0, 0)) + sh_draw = ImageDraw.Draw(shadow) + sh_draw.rounded_rectangle( + (8, 12, photo.width - 8, photo.height - 4), + radius=18, + fill=(0, 0, 0, 35), + ) + px = (SIZE[0] - photo.width) // 2 + py = (SIZE[1] - photo.height) // 2 - 16 + canvas.alpha_composite(shadow, (px, py + 6)) + canvas.alpha_composite(photo.convert("RGBA"), (px, py)) + + margin = 28 + draw_halal_badge(canvas, margin, margin) + logo_size = 108 + logo_x = SIZE[0] - logo_size - margin + logo_y = SIZE[1] - logo_size - margin + draw_logo_badge(canvas, logo, logo_x, logo_y) + draw_brand_name(canvas, logo_x, logo_y + logo_size // 2 - 12) + + canvas.convert("RGB").save(dest, "JPEG", quality=94, optimize=True, subsampling=0) + + +def ensure_catalog_files() -> None: + pairs = [ + ("beef-steak.jpg", "beef-steak-2.jpg"), + ("beef-nihari-2.jpg", "beef-nihari.jpg"), + ("category-lamb.jpg", "lamb-shoulder.jpg"), + ] + for target, source in pairs: + t, s = SITE / target, SITE / source + if not t.exists() and s.exists(): + shutil.copy2(s, t) + print(f"Created missing {target} from {source}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Brand site images (colors preserved)") + parser.add_argument("--force", action="store_true", help="Re-process even if already 1400×1400") + parser.add_argument("--only", nargs="*", help="Process only these filenames") + args = parser.parse_args() + + if not LOGO_PATH.exists(): + raise SystemExit(f"Missing logo: {LOGO_PATH}") + + BACKUP.mkdir(exist_ok=True) + ensure_catalog_files() + + logo = prep_logo(108) + files = sorted(SITE.glob("*.jpg")) + processed = 0 + skipped = 0 + + for path in files: + if should_skip(path): + continue + if args.only and path.name not in args.only: + continue + if not args.force and already_branded(path): + print(f"○ {path.name} (already branded, skipped)") + skipped += 1 + continue + + backup = BACKUP / path.name + if not backup.exists(): + shutil.copy2(path, backup) + source = backup if backup.exists() else path + compose(source, path, logo) + print(f"✓ {path.name}") + processed += 1 + + print(f"\nDone — {processed} processed, {skipped} skipped. Originals in {BACKUP}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx new file mode 100644 index 0000000..5faf43f --- /dev/null +++ b/src/app/home/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next'; +import HomeView from '@/components/home/HomeView'; +import { SITE_NAME } from '@/lib/constants'; + +export const metadata: Metadata = { + title: 'Home', + description: + 'Welcome to Kött Gård — premium 100% Halal meat delivery. Fresh chicken, beef, and lamb; frozen fish and seafood only.', + alternates: { + canonical: '/', + }, +}; + +export default function HomeAliasPage() { + return ; +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 34e63b2..b8fc748 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -41,16 +41,13 @@ const notoArabic = Noto_Sans_Arabic({ const siteDescription = 'Premium 100% Halal meat delivery. Fresh chicken, beef, and lamb; frozen fish and seafood only — customized to your preference and delivered to your door.'; -export const viewport = { - themeColor: '#8B1F1F', -}; - export const metadata: Metadata = { title: { default: `${SITE_NAME} — Premium Halal Meat Delivery`, template: `%s | ${SITE_NAME}`, }, description: siteDescription, + themeColor: '#8B1F1F', keywords: [ 'halal meat', 'halal chicken', diff --git a/src/app/page.tsx b/src/app/page.tsx index 87b82e3..1073212 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,27 +1,12 @@ -import Hero from '@/components/home/Hero'; -import TrustBadges from '@/components/home/TrustBadges'; -import AboutPreview from '@/components/home/AboutPreview'; -import CategoryGrid from '@/components/home/CategoryGrid'; -import FeaturedProducts from '@/components/home/FeaturedProducts'; -import HowItWorks from '@/components/home/HowItWorks'; -import WeeklyOffers from '@/components/home/WeeklyOffers'; -import SocialFollow from '@/components/home/SocialFollow'; -import ContactPreview from '@/components/home/ContactPreview'; -import CTA from '@/components/home/CTA'; +import type { Metadata } from 'next'; +import HomeView from '@/components/home/HomeView'; +import { SITE_NAME } from '@/lib/constants'; + +export const metadata: Metadata = { + title: 'Home', + description: `${SITE_NAME} — premium 100% Halal meat delivery. Fresh chicken, beef, and lamb; frozen fish and seafood only.`, +}; export default function HomePage() { - return ( - <> - - - - - - - - - - - - ); + return ; } \ No newline at end of file diff --git a/src/app/product/[slug]/page.tsx b/src/app/product/[slug]/page.tsx index 41b2ab2..757e13c 100644 --- a/src/app/product/[slug]/page.tsx +++ b/src/app/product/[slug]/page.tsx @@ -67,17 +67,17 @@ export default function ProductPage() {
-
+
{product.badge && ( - + {product.badge} )} diff --git a/src/components/home/AboutPreview.tsx b/src/components/home/AboutPreview.tsx index c8fbdc2..73f4824 100644 --- a/src/components/home/AboutPreview.tsx +++ b/src/components/home/AboutPreview.tsx @@ -1,7 +1,6 @@ 'use client'; import AppImage from '@/components/ui/AppImage'; -import Link from 'next/link'; import { ArrowRight } from 'lucide-react'; import Button from '@/components/ui/Button'; import { useTranslation } from '@/hooks/useTranslation'; @@ -42,12 +41,10 @@ export default function AboutPreview() { ))}
- - - +
diff --git a/src/components/home/CTA.tsx b/src/components/home/CTA.tsx index 84956d7..510af54 100644 --- a/src/components/home/CTA.tsx +++ b/src/components/home/CTA.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import Button from '@/components/ui/Button'; import { ArrowRight, MessageCircle } from 'lucide-react'; import { useTranslation } from '@/hooks/useTranslation'; @@ -13,31 +12,29 @@ export default function CTA() {
-
-
+
+
-
+

{t('cta.title')}

{t('cta.subtitle')}

- - - - - - + +
diff --git a/src/components/home/ContactPreview.tsx b/src/components/home/ContactPreview.tsx index 0051928..3a39a9a 100644 --- a/src/components/home/ContactPreview.tsx +++ b/src/components/home/ContactPreview.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import { Phone, MapPin, Clock, MessageCircle, ExternalLink } from 'lucide-react'; import Button from '@/components/ui/Button'; import Logo from '@/components/ui/Logo'; @@ -64,31 +63,27 @@ export default function ContactPreview() {
- - - - - - - - - + + +
diff --git a/src/components/home/FeaturedProducts.tsx b/src/components/home/FeaturedProducts.tsx index 0ee42a9..427cee3 100644 --- a/src/components/home/FeaturedProducts.tsx +++ b/src/components/home/FeaturedProducts.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import ProductCard from '@/components/product/ProductCard'; import { getFeaturedProducts } from '@/lib/products'; import Button from '@/components/ui/Button'; @@ -25,12 +24,10 @@ export default function FeaturedProducts() {

{t('featured.title')}

{t('featured.subtitle')}

- - - +
diff --git a/src/components/home/Hero.tsx b/src/components/home/Hero.tsx index 203ad77..129aeca 100644 --- a/src/components/home/Hero.tsx +++ b/src/components/home/Hero.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import AppImage from '@/components/ui/AppImage'; import Button from '@/components/ui/Button'; import Logo from '@/components/ui/Logo'; @@ -14,7 +13,7 @@ export default function Hero() { return (
-
+
-
-
+
+
-
+
@@ -70,26 +69,24 @@ export default function Hero() {
- - - - - - + +
-
+
); } \ No newline at end of file diff --git a/src/components/home/HomeView.tsx b/src/components/home/HomeView.tsx new file mode 100644 index 0000000..85bba46 --- /dev/null +++ b/src/components/home/HomeView.tsx @@ -0,0 +1,27 @@ +import Hero from '@/components/home/Hero'; +import TrustBadges from '@/components/home/TrustBadges'; +import AboutPreview from '@/components/home/AboutPreview'; +import CategoryGrid from '@/components/home/CategoryGrid'; +import FeaturedProducts from '@/components/home/FeaturedProducts'; +import HowItWorks from '@/components/home/HowItWorks'; +import WeeklyOffers from '@/components/home/WeeklyOffers'; +import SocialFollow from '@/components/home/SocialFollow'; +import ContactPreview from '@/components/home/ContactPreview'; +import CTA from '@/components/home/CTA'; + +export default function HomeView() { + return ( + <> + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/home/HowItWorks.tsx b/src/components/home/HowItWorks.tsx index d600454..191c611 100644 --- a/src/components/home/HowItWorks.tsx +++ b/src/components/home/HowItWorks.tsx @@ -57,12 +57,10 @@ export default function HowItWorks() {
- - - +
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 3fdacaf..07206f1 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -25,6 +25,7 @@ export default function Footer() { { label: t('nav.fish'), href: '/shop?category=fish' }, ], company: [ + { label: t('nav.home'), href: '/' }, { label: t('nav.about'), href: '/about' }, { label: t('nav.halalCert'), href: '/about#halal' }, { label: t('nav.delivery'), href: '/about#delivery' }, diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ee3692e..48197be 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,8 +1,10 @@ 'use client'; import Link from 'next/link'; +import { usePathname } from 'next/navigation'; import { useState } from 'react'; import { ShoppingCart, Heart, User, Menu, X, Search } from 'lucide-react'; +import { cn } from '@/lib/utils'; import { useCart } from '@/presentation/hooks/useCart'; import { useWishlist } from '@/presentation/hooks/useWishlist'; import { useAuth } from '@/presentation/hooks/useAuth'; @@ -11,6 +13,7 @@ import Logo from '@/components/ui/Logo'; import { SITE_LOCATION } from '@/lib/constants'; export default function Header() { + const pathname = usePathname(); const [mobileOpen, setMobileOpen] = useState(false); const { summary } = useCart(); const { items: wishlistItems } = useWishlist(); @@ -19,7 +22,10 @@ export default function Header() { const wishlistCount = wishlistItems.length; const { t } = useTranslation(); + const isHome = pathname === '/' || pathname === '/home'; + const navLinks = [ + { href: '/', label: t('nav.home'), matchHome: true }, { href: '/shop', label: t('nav.shop') }, { href: '/shop?category=chicken', label: t('nav.chicken') }, { href: '/shop?category=beef', label: t('nav.beef') }, @@ -46,15 +52,25 @@ export default function Header() {
@@ -114,16 +130,26 @@ export default function Header() { {mobileOpen && (
)} diff --git a/src/components/product/ProductCard.tsx b/src/components/product/ProductCard.tsx index a12913b..478c0f5 100644 --- a/src/components/product/ProductCard.tsx +++ b/src/components/product/ProductCard.tsx @@ -9,7 +9,6 @@ import { useWishlist } from '@/presentation/hooks/useWishlist'; import { useTranslation } from '@/hooks/useTranslation'; import { cn } from '@/lib/utils'; import { products } from '@/lib/products'; - interface ProductCardProps { product: LocalizedProduct; } @@ -22,13 +21,13 @@ export default function ProductCard({ product }: ProductCardProps) { return (
-
+
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index e78a859..f9821b4 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,31 +1,109 @@ +import Link from 'next/link'; import { cn } from '@/lib/utils'; import { ButtonHTMLAttributes, forwardRef } from 'react'; interface ButtonProps extends ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'gold' | 'ghost'; size?: 'sm' | 'md' | 'lg'; + href?: string; + external?: boolean; } -const Button = forwardRef( - ({ className, variant = 'primary', size = 'md', children, ...props }, ref) => { - const variants = { - primary: 'btn-primary', - secondary: 'btn-secondary', - gold: 'btn-gold', - ghost: - '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', - }; +function getButtonClasses( + variant: ButtonProps['variant'], + size: ButtonProps['size'], + className?: string +) { + const variants = { + primary: 'btn-primary', + secondary: 'btn-secondary', + gold: 'btn-gold', + ghost: + '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', + }; - const sizes = { - sm: 'px-4 py-2 text-xs', - md: '', - lg: 'px-8 py-4 text-base', - }; + const sizes = { + sm: 'px-4 py-2 text-xs', + md: '', + lg: 'px-8 py-4 text-base', + }; + + return cn(variants[variant ?? 'primary'], sizes[size ?? 'md'], className); +} + +function isHttpHref(href: string) { + return /^https?:\/\//i.test(href); +} + +const Button = forwardRef( + ( + { + className, + variant = 'primary', + size = 'md', + children, + href, + external, + type = 'button', + disabled, + form, + formAction, + formEncType, + formMethod, + formNoValidate, + formTarget, + name, + value, + ...props + }, + ref + ) => { + const classes = getButtonClasses(variant, size, className); + + if (href) { + const openInNewTab = external || isHttpHref(href); + const linkClassName = cn(classes, disabled && 'pointer-events-none opacity-50'); + + if (href.startsWith('/') && !openInNewTab) { + return ( + } + className={linkClassName} + aria-disabled={disabled || undefined} + > + {children} + + ); + } + + return ( + } + className={linkClassName} + aria-disabled={disabled || undefined} + {...(openInNewTab ? { target: '_blank' as const, rel: 'noopener noreferrer' } : {})} + > + {children} + + ); + } return (