Initial commit: Kottgard production website code
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
'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';
|
||||
import { PAGE_IMAGES } from '@/infrastructure/images';
|
||||
|
||||
export default function AboutPreview() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const stats = [
|
||||
{ value: t('aboutPreview.statHalal'), label: t('aboutPreview.statHalalLabel') },
|
||||
{ value: t('aboutPreview.statDays'), label: t('aboutPreview.statDaysLabel') },
|
||||
{ value: t('aboutPreview.statDelivery'), label: t('aboutPreview.statDeliveryLabel') },
|
||||
{ value: t('aboutPreview.statFresh'), label: t('aboutPreview.statFreshLabel') },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="bg-white py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div>
|
||||
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
|
||||
{t('aboutPreview.label')}
|
||||
</span>
|
||||
<h2 className="section-heading mb-6">{t('aboutPreview.title')}</h2>
|
||||
<p className="mb-4 leading-relaxed text-gray-600">{t('aboutPreview.p1')}</p>
|
||||
<p className="mb-4 leading-relaxed text-gray-600">{t('aboutPreview.p2')}</p>
|
||||
<p className="mb-8 leading-relaxed text-gray-600">{t('aboutPreview.p3')}</p>
|
||||
|
||||
<div className="mb-8 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{stats.map((stat) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="rounded-xl border border-cream-300/80 bg-cream-50 p-4 text-center"
|
||||
>
|
||||
<p className="font-display text-2xl font-bold text-brand-800">{stat.value}</p>
|
||||
<p className="mt-1 text-xs font-medium text-gray-500">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link href="/about">
|
||||
<Button variant="secondary">
|
||||
{t('aboutPreview.readMore')}
|
||||
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl shadow-premium-lg">
|
||||
<AppImage
|
||||
src={PAGE_IMAGES.aboutMeat}
|
||||
alt={t('aboutPreview.imageAlt')}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 1024px) 100vw, 640px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/30 via-transparent to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { ArrowRight, MessageCircle } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { WHATSAPP_URL } from '@/lib/constants';
|
||||
|
||||
export default function CTA() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<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">
|
||||
<div className="absolute -right-20 -top-20 h-60 w-60 rounded-full bg-gold-500/10" />
|
||||
<div className="absolute -bottom-16 -left-16 h-48 w-48 rounded-full bg-gold-500/10" />
|
||||
|
||||
<div className="relative">
|
||||
<h2 className="mb-4 font-display text-3xl font-bold text-white md:text-4xl">
|
||||
{t('cta.title')}
|
||||
</h2>
|
||||
<p className="mx-auto mb-8 max-w-xl text-brand-100">{t('cta.subtitle')}</p>
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<Link href="/shop">
|
||||
<Button variant="gold" size="lg">
|
||||
{t('cta.button')}
|
||||
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</Link>
|
||||
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15"
|
||||
>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
{t('cta.whatsapp')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { CATEGORY_IDS } from '@/lib/constants';
|
||||
import { CATEGORY_IMAGES } from '@/infrastructure/images';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
export default function CategoryGrid() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="bg-cream-50 py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-12 text-center">
|
||||
<h2 className="section-heading mb-3">{t('categories.title')}</h2>
|
||||
<p className="mx-auto max-w-2xl text-gray-500">{t('categories.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{CATEGORY_IDS.map((id) => (
|
||||
<Link
|
||||
key={id}
|
||||
href={`/shop?category=${id}`}
|
||||
className="group relative overflow-hidden rounded-2xl shadow-premium transition-all duration-300 hover:shadow-premium-lg"
|
||||
>
|
||||
<div className="relative aspect-[3/4]">
|
||||
<AppImage
|
||||
src={CATEGORY_IMAGES[id]}
|
||||
alt={t(`categories.${id}.name`)}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 320px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/92 via-brand-800/50 to-brand-700/10" />
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6">
|
||||
<h3 className="mb-1 font-display text-2xl font-bold text-white">
|
||||
{t(`categories.${id}.name`)}
|
||||
</h3>
|
||||
<p className="mb-3 text-sm text-brand-200">
|
||||
{t(`categories.${id}.description`)}
|
||||
</p>
|
||||
<span className="inline-flex items-center gap-1 text-sm font-semibold text-gold-400 transition-colors group-hover:text-gold-300">
|
||||
{t('categories.shop', { name: t(`categories.${id}.name`) })}
|
||||
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1 rtl:rotate-180 rtl:group-hover:-translate-x-1" />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
'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';
|
||||
import {
|
||||
SITE_ADDRESS,
|
||||
SITE_PHONE_DISPLAY,
|
||||
SITE_HOURS,
|
||||
WHATSAPP_URL,
|
||||
MAPS_URL,
|
||||
} from '@/lib/constants';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function ContactPreview() {
|
||||
const { t } = useTranslation();
|
||||
const telHref = `tel:+46725855050`;
|
||||
|
||||
return (
|
||||
<section className="bg-brand-950 py-20 text-white">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div>
|
||||
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('contact.label')}
|
||||
</span>
|
||||
<h2 className="mb-8 font-display text-3xl font-bold md:text-4xl">
|
||||
{t('contact.title')}
|
||||
</h2>
|
||||
|
||||
<ul className="mb-8 space-y-5">
|
||||
<li className="flex gap-4">
|
||||
<MapPin className="mt-0.5 h-5 w-5 shrink-0 text-gold-400" />
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('contact.addressLabel')}
|
||||
</p>
|
||||
<p className="text-brand-100">{SITE_ADDRESS}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-4">
|
||||
<Phone className="mt-0.5 h-5 w-5 shrink-0 text-gold-400" />
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('contact.phoneLabel')}
|
||||
</p>
|
||||
<a href={telHref} className="text-brand-100 hover:text-white">
|
||||
{SITE_PHONE_DISPLAY}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex gap-4">
|
||||
<Clock className="mt-0.5 h-5 w-5 shrink-0 text-gold-400" />
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('contact.hoursLabel')}
|
||||
</p>
|
||||
<p className="text-brand-100">
|
||||
{t('contact.hoursValue', { hours: SITE_HOURS })}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Link href="/about#contact">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15 sm:w-auto"
|
||||
>
|
||||
{t('contact.learnMore')}
|
||||
</Button>
|
||||
</Link>
|
||||
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button variant="gold" size="lg" className="w-full sm:w-auto">
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
{t('contact.writeUs')}
|
||||
</Button>
|
||||
</a>
|
||||
<a href={telHref}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="w-full border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15 sm:w-auto"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
{t('contact.callUs')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center">
|
||||
<a
|
||||
href={MAPS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
>
|
||||
<Logo size="lg" className="ring-gold-400/30" />
|
||||
<div>
|
||||
<p className="font-display text-xl font-bold">{t('site.name')}</p>
|
||||
<p className="mt-1 text-sm text-brand-200">{SITE_ADDRESS}</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center gap-2 text-sm font-semibold text-gold-400 transition-colors group-hover:text-gold-300">
|
||||
{t('contact.openMaps')}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import ProductCard from '@/components/product/ProductCard';
|
||||
import { getFeaturedProducts } from '@/lib/products';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { localizeProduct } from '@/lib/product-i18n';
|
||||
|
||||
export default function FeaturedProducts() {
|
||||
const { t } = useTranslation();
|
||||
const featured = getFeaturedProducts()
|
||||
.slice(0, 4)
|
||||
.map((p) => localizeProduct(p, t));
|
||||
|
||||
return (
|
||||
<section className="py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-12 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-end">
|
||||
<div>
|
||||
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
|
||||
{t('featured.label')}
|
||||
</span>
|
||||
<h2 className="section-heading">{t('featured.title')}</h2>
|
||||
<p className="mt-2 max-w-lg text-gray-500">{t('featured.subtitle')}</p>
|
||||
</div>
|
||||
<Link href="/shop">
|
||||
<Button variant="secondary">
|
||||
{t('featured.viewAll')}
|
||||
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{featured.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'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';
|
||||
import { ArrowRight, ShieldCheck, MapPin, Clock } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { PAGE_IMAGES } from '@/infrastructure/images';
|
||||
import { SITE_LOCATION, SITE_HOURS, SITE_ADDRESS, WHATSAPP_URL } from '@/lib/constants';
|
||||
|
||||
export default function Hero() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="relative min-h-[85vh] overflow-hidden bg-brand-950">
|
||||
<div className="absolute inset-0">
|
||||
<AppImage
|
||||
src={PAGE_IMAGES.hero}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
sizes="100vw"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUH/8QAIhAAAgEDBQAAAAAAAAAAAAAAAQIDAAQRBQYhIjFB/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAZEQACAwEAAAAAAAAAAAAAAAAAAQIRITH/2gAMAwEAAhEDEEA/ALextba0t0t7eJI4kHVEU4AqT/9k="
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-brand-950/95 via-brand-900/85 to-brand-900/40" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/60 via-transparent to-transparent" />
|
||||
|
||||
<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">
|
||||
<div className="max-w-2xl animate-slide-up">
|
||||
<div className="mb-8 flex items-center gap-4">
|
||||
<Logo size="lg" className="ring-gold-400/40 shadow-premium-lg" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gold-400">{SITE_LOCATION}</p>
|
||||
<p className="text-xs text-brand-200">{t('hero.taglineShort')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<ShieldCheck className="h-4 w-4 text-gold-400" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gold-300">
|
||||
{t('hero.badge')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="mb-4 font-display text-4xl font-bold leading-tight text-white sm:text-5xl lg:text-6xl">
|
||||
{t('hero.title')}
|
||||
</h1>
|
||||
|
||||
<p className="mb-2 text-xl font-medium text-gold-300 sm:text-2xl">
|
||||
{t('hero.subtitleShort')}
|
||||
</p>
|
||||
|
||||
<p className="mb-8 text-base leading-relaxed text-brand-100 sm:text-lg">
|
||||
{t('hero.subtitle')}
|
||||
</p>
|
||||
|
||||
<div className="mb-8 flex flex-wrap gap-4 text-sm text-brand-200">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="h-4 w-4 text-gold-400" />
|
||||
{t('hero.hours', { hours: SITE_HOURS })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<MapPin className="h-4 w-4 text-gold-400" />
|
||||
{SITE_ADDRESS}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<Link href="/shop">
|
||||
<Button variant="gold" size="lg">
|
||||
{t('hero.shopNow')}
|
||||
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</Link>
|
||||
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="border-white/30 bg-white/10 text-white hover:border-gold-400/50 hover:bg-white/15"
|
||||
>
|
||||
{t('hero.whatsapp')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-cream-50 to-transparent" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { MessageCircle, ClipboardCheck, Store } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { WHATSAPP_URL } from '@/lib/constants';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function HowItWorks() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const steps = [
|
||||
{
|
||||
icon: MessageCircle,
|
||||
step: '01',
|
||||
title: t('howItWorks.step1Title'),
|
||||
description: t('howItWorks.step1Desc'),
|
||||
},
|
||||
{
|
||||
icon: ClipboardCheck,
|
||||
step: '02',
|
||||
title: t('howItWorks.step2Title'),
|
||||
description: t('howItWorks.step2Desc'),
|
||||
},
|
||||
{
|
||||
icon: Store,
|
||||
step: '03',
|
||||
title: t('howItWorks.step3Title'),
|
||||
description: t('howItWorks.step3Desc'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="bg-brand-950 py-20 text-white">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-12 text-center">
|
||||
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('howItWorks.label')}
|
||||
</span>
|
||||
<h2 className="font-display text-3xl font-bold md:text-4xl">
|
||||
{t('howItWorks.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-12 grid gap-8 md:grid-cols-3">
|
||||
{steps.map((step) => (
|
||||
<div key={step.step} className="relative text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-brand-800">
|
||||
<step.icon className="h-7 w-7 text-gold-400" />
|
||||
</div>
|
||||
<span className="mb-2 inline-block text-xs font-bold uppercase tracking-widest text-gold-500">
|
||||
{t('howItWorks.step', { n: step.step })}
|
||||
</span>
|
||||
<h3 className="mb-2 font-display text-xl font-bold">{step.title}</h3>
|
||||
<p className="text-sm leading-relaxed text-brand-200">{step.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button variant="gold" size="lg">
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
{t('howItWorks.whatsapp')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import { Facebook, Instagram, MessageCircle } from 'lucide-react';
|
||||
import {
|
||||
FACEBOOK_URL,
|
||||
INSTAGRAM_URL,
|
||||
WHATSAPP_URL,
|
||||
} from '@/lib/constants';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
const socialLinks = [
|
||||
{
|
||||
key: 'facebook',
|
||||
href: FACEBOOK_URL,
|
||||
icon: Facebook,
|
||||
className:
|
||||
'border-blue-600/20 bg-blue-600/10 text-blue-700 hover:border-blue-600/40 hover:bg-blue-600/15',
|
||||
},
|
||||
{
|
||||
key: 'instagram',
|
||||
href: INSTAGRAM_URL,
|
||||
icon: Instagram,
|
||||
className:
|
||||
'border-pink-600/20 bg-pink-600/10 text-pink-700 hover:border-pink-600/40 hover:bg-pink-600/15',
|
||||
},
|
||||
{
|
||||
key: 'whatsapp',
|
||||
href: WHATSAPP_URL,
|
||||
icon: MessageCircle,
|
||||
className:
|
||||
'border-green-600/20 bg-green-600/10 text-green-700 hover:border-green-600/40 hover:bg-green-600/15',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export default function SocialFollow() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section className="py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="rounded-3xl border border-cream-300/80 bg-white px-8 py-12 text-center shadow-premium sm:px-16">
|
||||
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
|
||||
{t('social.label')}
|
||||
</span>
|
||||
<h2 className="section-heading mb-3">{t('social.title')}</h2>
|
||||
<p className="mx-auto mb-8 max-w-xl text-gray-500">{t('social.subtitle')}</p>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
{socialLinks.map(({ key, href, icon: Icon, className }) => (
|
||||
<a
|
||||
key={key}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex items-center gap-2 rounded-xl border-2 px-6 py-3 text-sm font-semibold transition-all ${className}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
{t(`social.${key}`)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { ShieldCheck, Leaf, Award } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function TrustBadges() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const badges = [
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: t('trust.halal'),
|
||||
description: t('trust.halalDesc'),
|
||||
href: '/about#halal',
|
||||
},
|
||||
{
|
||||
icon: Leaf,
|
||||
title: t('trust.fresh'),
|
||||
description: t('trust.freshDesc'),
|
||||
href: '/about#delivery',
|
||||
},
|
||||
{
|
||||
icon: Award,
|
||||
title: t('trust.premium'),
|
||||
description: t('trust.premiumDesc'),
|
||||
href: '/about',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="bg-cream-100 py-16">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{badges.map((badge) => (
|
||||
<Link
|
||||
key={badge.title}
|
||||
href={badge.href}
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<badge.icon className="h-7 w-7 text-brand-700 transition-colors group-hover:text-gold-600" />
|
||||
</div>
|
||||
<h3 className="mb-2 font-display text-xl font-bold text-brand-900">
|
||||
{badge.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-500">{badge.description}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import { MessageCircle, ArrowRight } from 'lucide-react';
|
||||
import { weeklyOffers } from '@/lib/offers';
|
||||
import { PAGE_IMAGES } from '@/infrastructure/images';
|
||||
import { whatsappOrderUrl } from '@/lib/constants';
|
||||
import { formatPrice, getFormatLocale } from '@/lib/utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function WeeklyOffers() {
|
||||
const { t, locale } = useTranslation();
|
||||
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
|
||||
|
||||
return (
|
||||
<section className="bg-cream-100 py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div>
|
||||
<span className="mb-2 inline-block text-sm font-semibold uppercase tracking-wider text-gold-600">
|
||||
{t('offers.label')}
|
||||
</span>
|
||||
<h2 className="section-heading mb-3">{t('offers.title')}</h2>
|
||||
<p className="mb-8 text-gray-500">{t('offers.subtitle')}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{weeklyOffers.map((offer) => (
|
||||
<div
|
||||
key={offer.id}
|
||||
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"
|
||||
>
|
||||
<Link
|
||||
href={`/product/${offer.productSlug}`}
|
||||
className="relative h-20 w-20 shrink-0 overflow-hidden rounded-xl sm:h-24 sm:w-24"
|
||||
>
|
||||
<AppImage
|
||||
src={offer.image}
|
||||
alt={t(`${offer.nameKey}.name`)}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 96px, 128px"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={
|
||||
offer.badgeKey === 'halal'
|
||||
? 'rounded-full bg-brand-700 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white'
|
||||
: 'rounded-full bg-gold-500 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white'
|
||||
}
|
||||
>
|
||||
{t(`offers.badge.${offer.badgeKey}`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Link href={`/product/${offer.productSlug}`}>
|
||||
<h3 className="mb-2 font-display text-lg font-bold text-brand-900 transition-colors hover:text-brand-700">
|
||||
{t(`${offer.nameKey}.name`)}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
{offer.originalPrice != null && (
|
||||
<p className="text-sm text-gray-400 line-through">
|
||||
{t('offers.was')}: {fmt(offer.originalPrice)}/
|
||||
{t(`priceUnit.${offer.priceUnitKey}`)}
|
||||
</p>
|
||||
)}
|
||||
<p className="font-display text-xl font-bold text-brand-800">
|
||||
{offer.originalPrice != null && (
|
||||
<span className="me-2 text-sm font-semibold uppercase text-gold-600">
|
||||
{t('offers.now')}
|
||||
</span>
|
||||
)}
|
||||
{fmt(offer.price)}
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
/{t(`priceUnit.${offer.priceUnitKey}`)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
href={`/product/${offer.productSlug}`}
|
||||
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"
|
||||
>
|
||||
{t('offers.viewProduct')}
|
||||
<ArrowRight className="h-3 w-3 rtl:rotate-180" />
|
||||
</Link>
|
||||
<a
|
||||
href={whatsappOrderUrl(offer.whatsappProduct)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
>
|
||||
<MessageCircle className="h-3.5 w-3.5" />
|
||||
{t('offers.order')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-xs text-gray-400">{t('offers.disclaimer')}</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/shop"
|
||||
className="relative hidden aspect-[4/5] overflow-hidden rounded-2xl shadow-premium-lg lg:block"
|
||||
>
|
||||
<AppImage
|
||||
src={PAGE_IMAGES.weeklyOffersBanner}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 hover:scale-105"
|
||||
sizes="(max-width: 1024px) 0px, 640px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-brand-950/50 via-transparent to-transparent" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { ShieldCheck, Leaf, Award, Phone, Mail, MapPin, Facebook, Instagram, MessageCircle } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Logo from '@/components/ui/Logo';
|
||||
import {
|
||||
SITE_PHONE_DISPLAY,
|
||||
SITE_ADDRESS,
|
||||
SITE_HOURS,
|
||||
SITE_EMAIL,
|
||||
FACEBOOK_URL,
|
||||
INSTAGRAM_URL,
|
||||
WHATSAPP_URL,
|
||||
} from '@/lib/constants';
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const footerLinks = {
|
||||
shop: [
|
||||
{ label: t('nav.chicken'), href: '/shop?category=chicken' },
|
||||
{ label: t('nav.beef'), href: '/shop?category=beef' },
|
||||
{ label: t('nav.lamb'), href: '/shop?category=lamb' },
|
||||
{ label: t('nav.fish'), href: '/shop?category=fish' },
|
||||
],
|
||||
company: [
|
||||
{ label: t('nav.about'), href: '/about' },
|
||||
{ label: t('nav.halalCert'), href: '/about#halal' },
|
||||
{ label: t('nav.delivery'), href: '/about#delivery' },
|
||||
{ label: t('nav.contact'), href: '/about#contact' },
|
||||
],
|
||||
account: [
|
||||
{ label: t('nav.myAccount'), href: '/account' },
|
||||
{ label: t('nav.orderHistory'), href: '/account#orders' },
|
||||
{ label: t('nav.wishlist'), href: '/wishlist' },
|
||||
{ label: t('nav.cart'), href: '/cart' },
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="border-t border-gray-100 bg-brand-950 text-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<div className="grid gap-12 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<Link href="/" className="mb-4 flex items-center gap-3">
|
||||
<Logo size="sm" className="ring-brand-700" />
|
||||
<span className="font-display text-xl font-bold">{t('site.name')}</span>
|
||||
</Link>
|
||||
<p className="mb-6 text-sm leading-relaxed text-brand-200">
|
||||
{t('footer.tagline')}
|
||||
</p>
|
||||
<div className="mb-6 flex flex-wrap gap-4">
|
||||
<div className="flex items-center gap-1.5 text-xs text-gold-400">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
{t('trust.halal')}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gold-400">
|
||||
<Leaf className="h-4 w-4" />
|
||||
{t('trust.fresh')}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gold-400">
|
||||
<Award className="h-4 w-4" />
|
||||
{t('trust.premium')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={FACEBOOK_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t('social.facebook')}
|
||||
className="rounded-lg bg-brand-800 p-2.5 text-brand-200 transition-colors hover:bg-brand-700 hover:text-white"
|
||||
>
|
||||
<Facebook className="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href={INSTAGRAM_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t('social.instagram')}
|
||||
className="rounded-lg bg-brand-800 p-2.5 text-brand-200 transition-colors hover:bg-brand-700 hover:text-white"
|
||||
>
|
||||
<Instagram className="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href={WHATSAPP_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={t('social.whatsapp')}
|
||||
className="rounded-lg bg-brand-800 p-2.5 text-brand-200 transition-colors hover:bg-brand-700 hover:text-white"
|
||||
>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('footer.shop')}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{footerLinks.shop.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-brand-200 transition-colors hover:text-white"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('footer.company')}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
{footerLinks.company.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-brand-200 transition-colors hover:text-white"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gold-400">
|
||||
{t('footer.contact')}
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-center gap-2 text-sm text-brand-200">
|
||||
<Phone className="h-4 w-4 text-gold-400" />
|
||||
<a href={`tel:${SITE_PHONE_DISPLAY.replace(/\s/g, '')}`} className="hover:text-white">
|
||||
{SITE_PHONE_DISPLAY}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-2 text-sm text-brand-200">
|
||||
<Mail className="h-4 w-4 text-gold-400" />
|
||||
<a href={`mailto:${SITE_EMAIL}`} className="hover:text-white">
|
||||
{SITE_EMAIL}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-start gap-2 text-sm text-brand-200">
|
||||
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-gold-400" />
|
||||
<span>
|
||||
{SITE_ADDRESS}
|
||||
<br />
|
||||
{t('footer.hours', { hours: SITE_HOURS })}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex flex-col items-center justify-between gap-4 border-t border-brand-800 pt-8 sm:flex-row">
|
||||
<p className="text-xs text-brand-300">
|
||||
© {new Date().getFullYear()} {t('site.name')}. {t('footer.rights')}
|
||||
</p>
|
||||
<div className="flex gap-6 text-xs text-brand-300">
|
||||
<Link href="/about#privacy" className="hover:text-white">
|
||||
{t('nav.privacy')}
|
||||
</Link>
|
||||
<Link href="/about#terms" className="hover:text-white">
|
||||
{t('nav.terms')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { ShoppingCart, Heart, User, Menu, X, Search } from 'lucide-react';
|
||||
import { useCart } from '@/presentation/hooks/useCart';
|
||||
import { useWishlist } from '@/presentation/hooks/useWishlist';
|
||||
import { useAuth } from '@/presentation/hooks/useAuth';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import Logo from '@/components/ui/Logo';
|
||||
import { SITE_LOCATION } from '@/lib/constants';
|
||||
|
||||
export default function Header() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const { summary } = useCart();
|
||||
const { items: wishlistItems } = useWishlist();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const cartCount = summary.itemCount;
|
||||
const wishlistCount = wishlistItems.length;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/shop', label: t('nav.shop') },
|
||||
{ href: '/shop?category=chicken', label: t('nav.chicken') },
|
||||
{ href: '/shop?category=beef', label: t('nav.beef') },
|
||||
{ href: '/shop?category=lamb', label: t('nav.lamb') },
|
||||
{ href: '/shop?category=fish', label: t('nav.fish') },
|
||||
{ href: '/about', label: t('nav.about') },
|
||||
{ href: '/about#contact', label: t('nav.contact') },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="border-b border-cream-300/80 bg-cream-50/95 backdrop-blur-md">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between lg:h-20">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<Logo size="sm" />
|
||||
<div className="hidden sm:block">
|
||||
<span className="font-display text-xl font-bold text-brand-800">
|
||||
{t('site.name')}
|
||||
</span>
|
||||
<p className="text-[10px] font-medium uppercase tracking-widest text-brand-600">
|
||||
{SITE_LOCATION}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-8 lg:flex">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm font-medium text-gray-600 transition-colors hover:text-brand-700"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<Link
|
||||
href="/shop?q="
|
||||
className="hidden rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700 sm:block"
|
||||
aria-label={t('nav.searchProducts')}
|
||||
>
|
||||
<Search className="h-5 w-5" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/wishlist"
|
||||
className="relative rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700"
|
||||
aria-label={t('nav.wishlist')}
|
||||
>
|
||||
<Heart className="h-5 w-5" />
|
||||
{wishlistCount > 0 && (
|
||||
<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">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/cart"
|
||||
className="relative rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700"
|
||||
aria-label={t('nav.cart')}
|
||||
>
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
{cartCount > 0 && (
|
||||
<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">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={isAuthenticated ? '/account' : '/login'}
|
||||
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-brand-700"
|
||||
aria-label={t('nav.account')}
|
||||
>
|
||||
<User className="h-5 w-5" />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 lg:hidden"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label={t('nav.toggleMenu')}
|
||||
>
|
||||
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mobileOpen && (
|
||||
<div className="border-t border-gray-100 bg-white lg:hidden">
|
||||
<nav className="flex flex-col px-4 py-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="rounded-lg px-4 py-3 text-sm font-medium text-gray-600 transition-colors hover:bg-brand-50 hover:text-brand-700"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { Globe } from 'lucide-react';
|
||||
import { LOCALES, Locale } from '@/i18n/types';
|
||||
import { usesArabicScript } from '@/i18n';
|
||||
import { useLocaleStore } from '@/store/locale';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function LanguageBanner() {
|
||||
const { locale, setLocale } = useLocaleStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border-b border-brand-800 bg-brand-950 text-white"
|
||||
role="navigation"
|
||||
aria-label={t('nav.language')}
|
||||
>
|
||||
<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">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-brand-200">
|
||||
<Globe className="h-3.5 w-3.5 text-gold-400" aria-hidden />
|
||||
<span>{t('languageBanner.choose')}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex w-full max-w-full items-center justify-start gap-1 overflow-x-auto rounded-full bg-brand-900 p-1 ring-1 ring-brand-800 sm:max-w-none sm:justify-center"
|
||||
role="group"
|
||||
aria-label={t('nav.language')}
|
||||
>
|
||||
{LOCALES.map((lang) => {
|
||||
const active = locale === lang.code;
|
||||
const scriptFont =
|
||||
lang.code === 'ur'
|
||||
? 'font-[family-name:var(--font-urdu)]'
|
||||
: lang.code === 'ar' || lang.code === 'fa'
|
||||
? 'font-[family-name:var(--font-arabic)]'
|
||||
: '';
|
||||
return (
|
||||
<button
|
||||
key={lang.code}
|
||||
type="button"
|
||||
onClick={() => setLocale(lang.code as Locale)}
|
||||
aria-pressed={active}
|
||||
aria-label={`${lang.label} (${lang.nativeLabel})`}
|
||||
className={cn(
|
||||
'flex shrink-0 items-center justify-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold transition-all sm:px-4 sm:py-2 sm:text-sm',
|
||||
active
|
||||
? 'bg-gold-500 text-brand-950 shadow-gold'
|
||||
: 'text-brand-200 hover:bg-brand-800 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={usesArabicScript(lang.code as Locale) ? scriptFont : ''}
|
||||
>
|
||||
{lang.nativeLabel}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'hidden text-[10px] font-normal uppercase tracking-wide sm:inline',
|
||||
active ? 'text-brand-900/70' : 'text-brand-400'
|
||||
)}
|
||||
>
|
||||
{lang.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Globe, Check } from 'lucide-react';
|
||||
import { LOCALES, Locale } from '@/i18n/types';
|
||||
import { useLocaleStore } from '@/store/locale';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { locale, setLocale } = useLocaleStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const current = LOCALES.find((l) => l.code === locale);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
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"
|
||||
aria-label={t('nav.language')}
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{current?.nativeLabel}</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<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">
|
||||
{LOCALES.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => {
|
||||
setLocale(lang.code as Locale);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-3 px-4 py-2.5 text-sm transition-colors hover:bg-brand-50',
|
||||
locale === lang.code
|
||||
? 'font-semibold text-brand-700'
|
||||
: 'text-gray-600'
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{lang.nativeLabel}
|
||||
<span className="ms-2 text-xs text-gray-400">{lang.label}</span>
|
||||
</span>
|
||||
{locale === lang.code && (
|
||||
<Check className="h-4 w-4 shrink-0 text-brand-700" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useLocaleStore } from '@/store/locale';
|
||||
import { getHtmlLang, isRtl } from '@/i18n';
|
||||
|
||||
export default function LocaleAttributes() {
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = getHtmlLang(locale);
|
||||
document.documentElement.dir = isRtl(locale) ? 'rtl' : 'ltr';
|
||||
}, [locale]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { Category, ProductCustomization } from '@/types';
|
||||
import { CUT_COUNTS, CUTTING_STYLE_KEYS } from '@/lib/constants';
|
||||
import { updateMeatCustomization } from '@/lib/customization';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Scissors, Hash } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface CustomizationSelectorProps {
|
||||
category: Category;
|
||||
customization: ProductCustomization;
|
||||
onChange: (customization: ProductCustomization) => void;
|
||||
}
|
||||
|
||||
export default function CustomizationSelector({
|
||||
category,
|
||||
customization,
|
||||
onChange,
|
||||
}: CustomizationSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (category === 'fish') {
|
||||
return (
|
||||
<div className="rounded-xl border border-brand-100 bg-brand-50/50 p-4">
|
||||
<p className="text-sm text-brand-700">{t('product.fishNote')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meat = updateMeatCustomization(customization, category, {});
|
||||
const selectedCuts = meat.cuts;
|
||||
const selectedStyle = meat.cuttingStyle;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Hash className="h-4 w-4 text-brand-700" />
|
||||
<h3 className="text-sm font-semibold text-brand-900">
|
||||
{t('product.howManyCuts')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{CUT_COUNTS.map((cuts) => (
|
||||
<button
|
||||
key={cuts}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange(updateMeatCustomization(customization, category, { cuts }))
|
||||
}
|
||||
className={cn(
|
||||
'min-w-[3.5rem] rounded-lg border-2 px-4 py-2.5 text-sm font-semibold transition-all',
|
||||
selectedCuts === cuts
|
||||
? 'border-brand-700 bg-brand-700 text-white shadow-premium'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:border-brand-300 hover:bg-brand-50'
|
||||
)}
|
||||
>
|
||||
{cuts}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500">{t('product.howManyCutsHint')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Scissors className="h-4 w-4 text-brand-700" />
|
||||
<h3 className="text-sm font-semibold text-brand-900">
|
||||
{t('product.selectCutting')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{CUTTING_STYLE_KEYS.map((style) => (
|
||||
<button
|
||||
key={style}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange(
|
||||
updateMeatCustomization(customization, category, { cuttingStyle: style })
|
||||
)
|
||||
}
|
||||
className={cn(
|
||||
'rounded-lg border-2 px-4 py-3 text-start text-sm font-medium transition-all',
|
||||
selectedStyle === style
|
||||
? 'border-brand-700 bg-brand-700 text-white shadow-premium'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:border-brand-300 hover:bg-brand-50'
|
||||
)}
|
||||
>
|
||||
{t(`cutting.${style}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
{t('product.selectCuttingHint', {
|
||||
category: t(`categories.${category}.name`),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import Link from 'next/link';
|
||||
import { Heart, ShoppingCart } from 'lucide-react';
|
||||
import { LocalizedProduct } from '@/lib/product-i18n';
|
||||
import { formatPrice, getFormatLocale } from '@/lib/utils';
|
||||
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;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product }: ProductCardProps) {
|
||||
const { isInWishlist, toggleItem } = useWishlist();
|
||||
const { t, locale } = useTranslation();
|
||||
const rawProduct = products.find((p) => p.id === product.id)!;
|
||||
const inWishlist = isInWishlist(product.id);
|
||||
|
||||
return (
|
||||
<div className="card-premium group overflow-hidden">
|
||||
<div className="relative aspect-[4/3] overflow-hidden bg-gray-50">
|
||||
<Link href={`/product/${product.slug}`}>
|
||||
<AppImage
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 320px"
|
||||
/>
|
||||
</Link>
|
||||
{product.badge && (
|
||||
<span className="absolute start-3 top-3 rounded-full bg-gold-500 px-3 py-1 text-xs font-semibold text-white shadow-gold">
|
||||
{product.badge}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => toggleItem(rawProduct)}
|
||||
className={cn(
|
||||
'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',
|
||||
inWishlist && 'text-red-500'
|
||||
)}
|
||||
aria-label={inWishlist ? t('product.removeWishlist') : t('product.addWishlist')}
|
||||
>
|
||||
<Heart className={cn('h-4 w-4', inWishlist && 'fill-current')} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-5">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-brand-600">
|
||||
{t(`categories.${product.category}.name`)}
|
||||
</span>
|
||||
{product.weight && (
|
||||
<span className="text-xs text-gray-400">· {product.weight}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link href={`/product/${product.slug}`}>
|
||||
<h3 className="mb-1 font-display text-lg font-semibold text-brand-900 transition-colors group-hover:text-brand-700">
|
||||
{product.name}
|
||||
</h3>
|
||||
</Link>
|
||||
|
||||
<p className="mb-4 line-clamp-2 text-sm text-gray-500">{product.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-lg font-bold text-brand-800">
|
||||
{formatPrice(product.price, getFormatLocale(locale))}
|
||||
</span>
|
||||
<span className="ms-1 text-xs text-gray-400">{product.priceUnit}</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/product/${product.slug}`}
|
||||
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"
|
||||
aria-label={t('product.viewProduct', { name: product.name })}
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { Category, SortOption } from '@/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface ShopFiltersProps {
|
||||
selectedCategory: Category | 'all';
|
||||
onCategoryChange: (category: Category | 'all') => void;
|
||||
sortBy: SortOption;
|
||||
onSortChange: (sort: SortOption) => void;
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
totalResults: number;
|
||||
}
|
||||
|
||||
export default function ShopFilters({
|
||||
selectedCategory,
|
||||
onCategoryChange,
|
||||
sortBy,
|
||||
onSortChange,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
totalResults,
|
||||
}: ShopFiltersProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const categories: { value: Category | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: t('shop.all') },
|
||||
{ value: 'chicken', label: t('nav.chicken') },
|
||||
{ value: 'beef', label: t('nav.beef') },
|
||||
{ value: 'lamb', label: t('nav.lamb') },
|
||||
{ value: 'fish', label: t('nav.fish') },
|
||||
];
|
||||
|
||||
const sortOptions: { value: SortOption; label: string }[] = [
|
||||
{ value: 'featured', label: t('shop.sortFeatured') },
|
||||
{ value: 'price-asc', label: t('shop.sortPriceAsc') },
|
||||
{ value: 'price-desc', label: t('shop.sortPriceDesc') },
|
||||
{ value: 'name', label: t('shop.sortName') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<SlidersHorizontal className="h-4 w-4 text-brand-700" />
|
||||
<h2 className="text-sm font-semibold text-brand-900">{t('shop.filters')}</h2>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{t('shop.productsFound', { count: totalResults })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="search" className="label-text">
|
||||
{t('shop.search')}
|
||||
</label>
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
placeholder={t('shop.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="input-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="label-text">{t('shop.category')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.value}
|
||||
onClick={() => onCategoryChange(cat.value)}
|
||||
className={cn(
|
||||
'rounded-full px-4 py-2 text-sm font-medium transition-all',
|
||||
selectedCategory === cat.value
|
||||
? 'bg-brand-700 text-white shadow-premium'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-brand-50 hover:text-brand-700'
|
||||
)}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="sort" className="label-text">
|
||||
{t('shop.sortBy')}
|
||||
</label>
|
||||
<select
|
||||
id="sort"
|
||||
value={sortBy}
|
||||
onChange={(e) => onSortChange(e.target.value as SortOption)}
|
||||
className="input-field"
|
||||
>
|
||||
{sortOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import Image, { ImageProps } from 'next/image';
|
||||
import { IMAGE_QUALITY } from '@/infrastructure/images';
|
||||
|
||||
type AppImageProps = ImageProps & {
|
||||
quality?: number;
|
||||
};
|
||||
|
||||
/** Site-wide Image wrapper with consistent high quality defaults */
|
||||
export default function AppImage({ quality = IMAGE_QUALITY, ...props }: AppImageProps) {
|
||||
return <Image quality={quality} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'gold' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ 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',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-4 py-2 text-xs',
|
||||
md: '',
|
||||
lg: 'px-8 py-4 text-base',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(variants[variant], sizes[size], className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
export default Button;
|
||||
@@ -0,0 +1,36 @@
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import { BRAND_IMAGES } from '@/infrastructure/images';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface LogoProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
sm: 'h-10 w-10',
|
||||
md: 'h-12 w-12',
|
||||
lg: 'h-20 w-20',
|
||||
};
|
||||
|
||||
export default function Logo({ size = 'md', className }: LogoProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative shrink-0 overflow-hidden rounded-full ring-2 ring-cream-200',
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<AppImage
|
||||
src={BRAND_IMAGES.logo}
|
||||
alt="Kött Gård"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 40px, 80px"
|
||||
priority
|
||||
quality={95}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user