Initial commit: Kottgard production website code

This commit is contained in:
2026-06-19 15:46:23 +00:00
commit e0273c46d7
196 changed files with 29799 additions and 0 deletions
+67
View File
@@ -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>
);
}
+47
View File
@@ -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>
);
}
+55
View File
@@ -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>
);
}
+117
View File
@@ -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>
);
}
+44
View File
@@ -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>
);
}
+95
View File
@@ -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>
);
}
+70
View File
@@ -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>
);
}
+66
View File
@@ -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>
);
}
+54
View File
@@ -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>
);
}
+130
View File
@@ -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>
);
}
+179
View File
@@ -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">
&copy; {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>
);
}
+132
View File
@@ -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>
);
}
+73
View File
@@ -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>
);
}
+89
View File
@@ -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>
);
}
+109
View File
@@ -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>
);
}
+11
View File
@@ -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} />;
}
+38
View File
@@ -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;
+36
View File
@@ -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>
);
}