Initial commit: Kottgard production website code
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { ShieldCheck, Leaf, Award, Truck, Phone, Mail, MapPin } from 'lucide-react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import {
|
||||
SITE_ADDRESS,
|
||||
SITE_EMAIL,
|
||||
SITE_PHONE_DISPLAY,
|
||||
SITE_HOURS,
|
||||
} from '@/lib/constants';
|
||||
|
||||
export default function AboutPage() {
|
||||
const { t } = useTranslation();
|
||||
const telHref = `tel:${SITE_PHONE_DISPLAY.replace(/\s/g, '')}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-gradient-to-br from-brand-900 to-brand-950 px-4 py-20 text-white sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h1 className="mb-4 font-display text-4xl font-bold">
|
||||
{t('about.title', { name: t('site.name') })}
|
||||
</h1>
|
||||
<p className="text-lg text-brand-100">{t('about.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-4xl px-4 py-16 sm:px-6 lg:px-8">
|
||||
<section className="mb-16">
|
||||
<h2 className="section-heading mb-4">{t('about.ourStory')}</h2>
|
||||
<p className="mb-4 leading-relaxed text-gray-600">
|
||||
{t('about.storyP1', { name: t('site.name') })}
|
||||
</p>
|
||||
<p className="leading-relaxed text-gray-600">{t('about.storyP2')}</p>
|
||||
</section>
|
||||
|
||||
<section id="halal" className="mb-16">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<ShieldCheck className="h-8 w-8 text-brand-700" />
|
||||
<h2 className="section-heading">{t('about.halalTitle')}</h2>
|
||||
</div>
|
||||
<p className="leading-relaxed text-gray-600">
|
||||
{t('about.halalDesc', { name: t('site.name') })}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-16">
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
{[
|
||||
{ icon: Leaf, title: t('about.freshDaily'), desc: t('about.freshDailyDesc') },
|
||||
{
|
||||
icon: Award,
|
||||
title: t('about.premiumQuality'),
|
||||
desc: t('about.premiumQualityDesc'),
|
||||
},
|
||||
{
|
||||
icon: Truck,
|
||||
title: t('about.fastDelivery'),
|
||||
desc: t('about.fastDeliveryDesc'),
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.title} className="card-premium p-6 text-center">
|
||||
<item.icon className="mx-auto mb-3 h-8 w-8 text-brand-700" />
|
||||
<h3 className="mb-1 font-semibold text-brand-900">{item.title}</h3>
|
||||
<p className="text-sm text-gray-500">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="delivery" className="mb-16">
|
||||
<h2 className="section-heading mb-4">{t('about.deliveryTitle')}</h2>
|
||||
<div className="space-y-3 text-gray-600">
|
||||
<p>{t('about.delivery1')}</p>
|
||||
<p>{t('about.delivery2')}</p>
|
||||
<p>{t('about.delivery3')}</p>
|
||||
<p>{t('about.delivery4')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contact" className="card-premium mb-16 p-8">
|
||||
<h2 className="section-heading mb-6">{t('about.contactTitle')}</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 text-gray-600">
|
||||
<Phone className="h-5 w-5 text-brand-700" />
|
||||
<a href={telHref} className="hover:text-brand-800">
|
||||
{SITE_PHONE_DISPLAY}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-gray-600">
|
||||
<Mail className="h-5 w-5 text-brand-700" />
|
||||
<a href={`mailto:${SITE_EMAIL}`} className="hover:text-brand-800">
|
||||
{SITE_EMAIL}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 text-gray-600">
|
||||
<MapPin className="mt-0.5 h-5 w-5 shrink-0 text-brand-700" />
|
||||
<span>
|
||||
{SITE_ADDRESS}
|
||||
<br />
|
||||
{t('footer.hours', { hours: SITE_HOURS })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/shop"
|
||||
className="text-sm font-semibold text-brand-700 hover:text-brand-900"
|
||||
>
|
||||
{t('cta.button')} →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="privacy" className="mb-16">
|
||||
<h2 className="section-heading mb-4">{t('about.privacyTitle')}</h2>
|
||||
<p className="leading-relaxed text-gray-600">{t('about.privacyText')}</p>
|
||||
</section>
|
||||
|
||||
<section id="terms" className="mb-8">
|
||||
<h2 className="section-heading mb-4">{t('about.termsTitle')}</h2>
|
||||
<p className="leading-relaxed text-gray-600">{t('about.termsText')}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { User, Package, MapPin, Phone, Mail, LogOut, Heart } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useAuth } from '@/presentation/hooks/useAuth';
|
||||
import { formatPrice, formatDate, getFormatLocale } from '@/lib/utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { localizeProduct } from '@/lib/product-i18n';
|
||||
|
||||
export default function AccountPage() {
|
||||
const router = useRouter();
|
||||
const { user, isAuthenticated, orders, logout } = useAuth();
|
||||
const { t, locale } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
router.push('/login');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
if (!isAuthenticated || !user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
<div className="border-b border-gray-100 bg-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<h1 className="section-heading">{t('account.title')}</h1>
|
||||
<p className="text-gray-500">{t('account.welcome', { name: user.name })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
<div className="space-y-6">
|
||||
<div className="card-premium p-6">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-brand-100">
|
||||
<User className="h-6 w-6 text-brand-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-display text-lg font-semibold">{user.name}</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('account.memberSince', { date: formatDate(user.createdAt) })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-gray-100 pt-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Mail className="h-4 w-4 text-brand-600" />
|
||||
{user.email}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Phone className="h-4 w-4 text-brand-600" />
|
||||
{user.phone}
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-sm text-gray-600">
|
||||
<MapPin className="mt-0.5 h-4 w-4 shrink-0 text-brand-600" />
|
||||
{user.address.street}, {user.address.city}, {user.address.state}{' '}
|
||||
{user.address.zip}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-premium p-4">
|
||||
<Link
|
||||
href="/wishlist"
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-brand-50 hover:text-brand-700"
|
||||
>
|
||||
<Heart className="h-4 w-4" />
|
||||
{t('account.myWishlist')}
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-red-500 transition-colors hover:bg-red-50"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
{t('account.signOut')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2" id="orders">
|
||||
<div className="card-premium p-6">
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Package className="h-5 w-5 text-brand-700" />
|
||||
<h2 className="font-display text-lg font-semibold">
|
||||
{t('account.orderHistory')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{orders.length === 0 ? (
|
||||
<div className="py-12 text-center">
|
||||
<p className="mb-2 text-gray-500">{t('account.noOrders')}</p>
|
||||
<Link href="/shop">
|
||||
<Button variant="primary">{t('account.startShopping')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{orders.map((order) => (
|
||||
<div
|
||||
key={order.id}
|
||||
className="rounded-xl border border-gray-100 p-4 transition-colors hover:border-brand-200"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-brand-900">{order.id}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatDate(order.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-end">
|
||||
<p className="font-bold text-brand-800">{fmt(order.total)}</p>
|
||||
<span className="inline-block rounded-full bg-brand-50 px-2 py-0.5 text-xs font-medium capitalize text-brand-700">
|
||||
{t(`orderStatus.${order.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{order.items.map((item) => {
|
||||
const localized = localizeProduct(item.product, t);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex justify-between text-sm text-gray-600"
|
||||
>
|
||||
<Link
|
||||
href={`/product/${item.product.slug}`}
|
||||
className="hover:text-brand-700"
|
||||
>
|
||||
{localized.name} × {item.quantity}
|
||||
<span className="ms-2 text-xs text-gold-600">
|
||||
({item.customizationLabel})
|
||||
</span>
|
||||
</Link>
|
||||
<span>{fmt(item.product.price * item.quantity)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
'use client';
|
||||
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import Link from 'next/link';
|
||||
import { Minus, Plus, Trash2, ShoppingBag, ArrowRight } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useCart } from '@/presentation/hooks/useCart';
|
||||
import { formatPrice, getFormatLocale } from '@/lib/utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { localizeProduct } from '@/lib/product-i18n';
|
||||
|
||||
export default function CartPage() {
|
||||
const { items, updateQuantity, removeItem, summary } = useCart();
|
||||
const { t, locale } = useTranslation();
|
||||
const { subtotal: total, deliveryFee, grandTotal } = summary;
|
||||
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 lg:px-8">
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-brand-50">
|
||||
<ShoppingBag className="h-10 w-10 text-brand-300" />
|
||||
</div>
|
||||
<h1 className="mb-2 font-display text-2xl font-bold text-brand-900">
|
||||
{t('cart.empty')}
|
||||
</h1>
|
||||
<p className="mb-8 text-gray-500">{t('cart.emptyHint')}</p>
|
||||
<Link href="/shop">
|
||||
<Button variant="primary" size="lg">
|
||||
{t('cart.startShopping')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
<div className="border-b border-gray-100 bg-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<h1 className="section-heading">{t('cart.title')}</h1>
|
||||
<p className="text-gray-500">
|
||||
{t('cart.itemsCount', { count: items.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
<div className="space-y-4 lg:col-span-2">
|
||||
{items.map((item) => {
|
||||
const localized = localizeProduct(item.product, t);
|
||||
return (
|
||||
<div key={item.id} className="card-premium flex gap-4 p-4 sm:p-6">
|
||||
<div className="relative h-24 w-24 shrink-0 overflow-hidden rounded-xl sm:h-28 sm:w-28">
|
||||
<AppImage
|
||||
src={item.product.image}
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 96px, 112px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<Link
|
||||
href={`/product/${item.product.slug}`}
|
||||
className="font-display text-lg font-semibold text-brand-900 hover:text-brand-700"
|
||||
>
|
||||
{localized.name}
|
||||
</Link>
|
||||
<p className="mt-1 text-xs uppercase tracking-wider text-brand-600">
|
||||
{t(`categories.${item.product.category}.name`)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="rounded-lg p-2 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500"
|
||||
aria-label={t('cart.removeItem')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 inline-flex items-center gap-1.5 rounded-full bg-gold-50 px-3 py-1">
|
||||
<span className="text-xs font-medium text-gold-700">
|
||||
{t('cart.customization')}
|
||||
</span>
|
||||
<span className="text-xs font-semibold text-gold-800">
|
||||
{item.customizationLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex items-center justify-between pt-3">
|
||||
<div className="flex items-center rounded-lg border border-gray-200">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||||
className="flex h-9 w-9 items-center justify-center text-gray-500 hover:text-brand-700"
|
||||
aria-label={t('product.decreaseQty')}
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</button>
|
||||
<span className="w-8 text-center text-sm font-semibold">
|
||||
{item.quantity}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||
className="flex h-9 w-9 items-center justify-center text-gray-500 hover:text-brand-700"
|
||||
aria-label={t('product.increaseQty')}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-brand-800">
|
||||
{fmt(item.product.price * item.quantity)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="card-premium sticky top-24 p-6">
|
||||
<h2 className="mb-4 font-display text-lg font-semibold text-brand-900">
|
||||
{t('cart.orderSummary')}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3 border-b border-gray-100 pb-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">{t('cart.subtotal')}</span>
|
||||
<span className="font-medium">{fmt(total)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">{t('cart.delivery')}</span>
|
||||
<span className="font-medium">
|
||||
{deliveryFee === 0 ? (
|
||||
<span className="text-brand-700">{t('cart.free')}</span>
|
||||
) : (
|
||||
fmt(deliveryFee)
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{deliveryFee > 0 && (
|
||||
<p className="text-xs text-gray-400">{t('cart.freeDeliveryHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between py-4">
|
||||
<span className="font-semibold text-brand-900">{t('cart.total')}</span>
|
||||
<span className="text-xl font-bold text-brand-800">{fmt(grandTotal)}</span>
|
||||
</div>
|
||||
|
||||
<Link href="/checkout">
|
||||
<Button variant="gold" size="lg" className="w-full">
|
||||
{t('cart.proceedCheckout')}
|
||||
<ArrowRight className="h-4 w-4 rtl:rotate-180" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/shop"
|
||||
className="mt-3 block text-center text-sm font-medium text-brand-600 hover:text-brand-800"
|
||||
>
|
||||
{t('cart.continueShopping')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import Link from 'next/link';
|
||||
import { Lock, CreditCard, Truck, CheckCircle, ChevronLeft } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useCart } from '@/presentation/hooks/useCart';
|
||||
import { useAuth } from '@/presentation/hooks/useAuth';
|
||||
import { useCheckout } from '@/presentation/hooks/useCheckout';
|
||||
import { formatPrice, getFormatLocale } from '@/lib/utils';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { localizeProduct } from '@/lib/product-i18n';
|
||||
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
const { items, summary } = useCart();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { placeOrder } = useCheckout();
|
||||
const { t, locale } = useTranslation();
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState('card');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [orderComplete, setOrderComplete] = useState(false);
|
||||
const [orderId, setOrderId] = useState('');
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: user?.phone || '',
|
||||
street: user?.address.street || '',
|
||||
city: user?.address.city || '',
|
||||
state: user?.address.state || '',
|
||||
zip: user?.address.zip || '',
|
||||
cardNumber: '',
|
||||
expiry: '',
|
||||
cvv: '',
|
||||
});
|
||||
|
||||
const { subtotal: total, deliveryFee, grandTotal } = summary;
|
||||
const fmt = (n: number) => formatPrice(n, getFormatLocale(locale));
|
||||
|
||||
if (items.length === 0 && !orderComplete) {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-20 text-center">
|
||||
<h1 className="mb-4 font-display text-2xl font-bold">{t('checkout.noItems')}</h1>
|
||||
<Link href="/shop">
|
||||
<Button>{t('checkout.goToShop')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (orderComplete) {
|
||||
return (
|
||||
<div className="mx-auto max-w-lg px-4 py-20 text-center">
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-brand-50">
|
||||
<CheckCircle className="h-12 w-12 text-brand-700" />
|
||||
</div>
|
||||
<h1 className="mb-2 font-display text-3xl font-bold text-brand-900">
|
||||
{t('checkout.orderConfirmed')}
|
||||
</h1>
|
||||
<p className="mb-2 text-gray-500">{t('checkout.thankYou')}</p>
|
||||
<p className="mb-8 text-sm font-medium text-brand-700">
|
||||
{t('checkout.orderId', { id: orderId })}
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:justify-center">
|
||||
<Link href="/account">
|
||||
<Button variant="primary">{t('checkout.viewOrders')}</Button>
|
||||
</Link>
|
||||
<Link href="/shop">
|
||||
<Button variant="secondary">{t('cart.continueShopping')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsProcessing(true);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const { order } = placeOrder({
|
||||
deliveryAddress: {
|
||||
street: form.street,
|
||||
city: form.city,
|
||||
state: form.state,
|
||||
zip: form.zip,
|
||||
},
|
||||
paymentMethod,
|
||||
});
|
||||
|
||||
setOrderId(order.id);
|
||||
setOrderComplete(true);
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
href="/cart"
|
||||
className="mb-6 inline-flex items-center gap-1 text-sm font-medium text-gray-500 hover:text-brand-700"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 rtl:rotate-180" />
|
||||
{t('checkout.backToCart')}
|
||||
</Link>
|
||||
|
||||
<h1 className="section-heading mb-8">{t('checkout.title')}</h1>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{!isAuthenticated && (
|
||||
<div className="card-premium p-6">
|
||||
<p className="text-sm text-gray-600">
|
||||
{t('checkout.haveAccount')}{' '}
|
||||
<Link href="/login" className="font-semibold text-brand-700 hover:underline">
|
||||
{t('checkout.signIn')}
|
||||
</Link>{' '}
|
||||
{t('checkout.fasterCheckout')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card-premium p-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Truck className="h-5 w-5 text-brand-700" />
|
||||
<h2 className="font-display text-lg font-semibold">
|
||||
{t('checkout.deliveryDetails')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="label-text">{t('checkout.fullName')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.email')}</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
className="input-field"
|
||||
value={form.email}
|
||||
onChange={(e) => updateField('email', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.phone')}</label>
|
||||
<input
|
||||
required
|
||||
type="tel"
|
||||
className="input-field"
|
||||
value={form.phone}
|
||||
onChange={(e) => updateField('phone', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<label className="label-text">{t('checkout.street')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.street}
|
||||
onChange={(e) => updateField('street', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.city')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.city}
|
||||
onChange={(e) => updateField('city', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.state')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.state}
|
||||
onChange={(e) => updateField('state', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.zip')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.zip}
|
||||
onChange={(e) => updateField('zip', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-premium p-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<CreditCard className="h-5 w-5 text-brand-700" />
|
||||
<h2 className="font-display text-lg font-semibold">{t('checkout.payment')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex gap-3">
|
||||
{['card', 'cod'].map((method) => (
|
||||
<button
|
||||
key={method}
|
||||
type="button"
|
||||
onClick={() => setPaymentMethod(method)}
|
||||
className={`rounded-lg border-2 px-4 py-2 text-sm font-medium transition-all ${
|
||||
paymentMethod === method
|
||||
? 'border-brand-700 bg-brand-700 text-white'
|
||||
: 'border-gray-200 text-gray-600 hover:border-brand-300'
|
||||
}`}
|
||||
>
|
||||
{method === 'card'
|
||||
? t('checkout.creditCard')
|
||||
: t('checkout.cashOnDelivery')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{paymentMethod === 'card' && (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="label-text">{t('checkout.cardNumber')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
value={form.cardNumber}
|
||||
onChange={(e) => updateField('cardNumber', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.expiry')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
placeholder="MM/YY"
|
||||
value={form.expiry}
|
||||
onChange={(e) => updateField('expiry', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('checkout.cvv')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
placeholder="123"
|
||||
value={form.cvv}
|
||||
onChange={(e) => updateField('cvv', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="card-premium sticky top-24 p-6">
|
||||
<h2 className="mb-4 font-display text-lg font-semibold">
|
||||
{t('cart.orderSummary')}
|
||||
</h2>
|
||||
|
||||
<div className="mb-4 max-h-60 space-y-3 overflow-y-auto">
|
||||
{items.map((item) => {
|
||||
const localized = localizeProduct(item.product, t);
|
||||
return (
|
||||
<div key={item.id} className="flex gap-3">
|
||||
<div className="relative h-12 w-12 shrink-0 overflow-hidden rounded-lg">
|
||||
<AppImage
|
||||
src={item.product.image}
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="48px"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-brand-900">
|
||||
{localized.name}
|
||||
</p>
|
||||
<p className="text-xs text-gold-700">
|
||||
{item.customizationLabel}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{t('checkout.qty', { count: item.quantity })}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{fmt(item.product.price * item.quantity)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 border-t border-gray-100 pt-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">{t('cart.subtotal')}</span>
|
||||
<span>{fmt(total)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">{t('cart.delivery')}</span>
|
||||
<span>
|
||||
{deliveryFee === 0 ? t('cart.free') : fmt(deliveryFee)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between pt-2 text-lg font-bold">
|
||||
<span>{t('cart.total')}</span>
|
||||
<span className="text-brand-800">{fmt(grandTotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="gold"
|
||||
size="lg"
|
||||
className="mt-6 w-full"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
<Lock className="h-4 w-4" />
|
||||
{isProcessing
|
||||
? t('checkout.processing')
|
||||
: t('checkout.pay', { amount: fmt(grandTotal) })}
|
||||
</Button>
|
||||
|
||||
<p className="mt-3 flex items-center justify-center gap-1 text-xs text-gray-400">
|
||||
<Lock className="h-3 w-3" />
|
||||
{t('checkout.secure')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-cream-50 text-gray-900 antialiased;
|
||||
}
|
||||
|
||||
[lang='ur'] body {
|
||||
font-family: var(--font-urdu), var(--font-arabic), var(--font-inter), system-ui,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
[lang='ar'] body,
|
||||
[lang='fa'] body {
|
||||
font-family: var(--font-arabic), var(--font-inter), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
[lang='ur'] .font-display {
|
||||
font-family: var(--font-urdu), var(--font-playfair), Georgia, serif;
|
||||
}
|
||||
|
||||
[lang='ar'] .font-display,
|
||||
[lang='fa'] .font-display {
|
||||
font-family: var(--font-arabic), var(--font-playfair), Georgia, serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg bg-brand-700 px-6 py-3 text-sm font-semibold text-white shadow-premium transition-all duration-200 hover:bg-brand-800 hover:shadow-premium-lg active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg border-2 border-brand-700 bg-white px-6 py-3 text-sm font-semibold text-brand-700 transition-all duration-200 hover:bg-brand-50 active:scale-[0.98];
|
||||
}
|
||||
|
||||
.btn-gold {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-lg bg-gradient-to-r from-gold-500 to-gold-600 px-6 py-3 text-sm font-semibold text-white shadow-gold transition-all duration-200 hover:from-gold-600 hover:to-gold-700 active:scale-[0.98];
|
||||
}
|
||||
|
||||
.card-premium {
|
||||
@apply rounded-2xl border border-cream-300/60 bg-white shadow-premium transition-all duration-300 hover:border-brand-200 hover:shadow-premium-lg;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply w-full rounded-lg border border-cream-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-400 transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
@apply mb-1.5 block text-sm font-medium text-gray-700;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
@apply font-display text-3xl font-bold tracking-tight text-brand-900 md:text-4xl;
|
||||
}
|
||||
|
||||
.gold-accent {
|
||||
@apply bg-gradient-to-r from-gold-400 to-gold-500 bg-clip-text text-transparent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { Metadata } from 'next';
|
||||
import {
|
||||
Inter,
|
||||
Playfair_Display,
|
||||
Noto_Nastaliq_Urdu,
|
||||
Noto_Sans_Arabic,
|
||||
} from 'next/font/google';
|
||||
import LanguageBanner from '@/components/layout/LanguageBanner';
|
||||
import Header from '@/components/layout/Header';
|
||||
import Footer from '@/components/layout/Footer';
|
||||
import LocaleAttributes from '@/components/layout/LocaleAttributes';
|
||||
import { SITE_NAME } from '@/lib/constants';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const playfair = Playfair_Display({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-playfair',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const notoUrdu = Noto_Nastaliq_Urdu({
|
||||
subsets: ['arabic'],
|
||||
variable: '--font-urdu',
|
||||
weight: ['400', '500', '600', '700'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const notoArabic = Noto_Sans_Arabic({
|
||||
subsets: ['arabic'],
|
||||
variable: '--font-arabic',
|
||||
weight: ['400', '500', '600', '700'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const siteDescription =
|
||||
'Premium 100% Halal meat delivery. Fresh chicken, beef, and lamb; frozen fish and seafood only — customized to your preference and delivered to your door.';
|
||||
|
||||
export const viewport = {
|
||||
themeColor: '#8B1F1F',
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: `${SITE_NAME} — Premium Halal Meat Delivery`,
|
||||
template: `%s | ${SITE_NAME}`,
|
||||
},
|
||||
description: siteDescription,
|
||||
keywords: [
|
||||
'halal meat',
|
||||
'halal chicken',
|
||||
'halal beef',
|
||||
'halal lamb',
|
||||
'meat delivery',
|
||||
'fresh meat',
|
||||
'Kött Gård',
|
||||
'kottgard',
|
||||
],
|
||||
openGraph: {
|
||||
title: `${SITE_NAME} — Premium Halal Meat Delivery`,
|
||||
description: siteDescription,
|
||||
type: 'website',
|
||||
locale: 'sv_SE',
|
||||
siteName: SITE_NAME,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html
|
||||
lang="sv"
|
||||
dir="ltr"
|
||||
className={`${inter.variable} ${playfair.variable} ${notoUrdu.variable} ${notoArabic.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="flex min-h-screen flex-col font-sans">
|
||||
<LocaleAttributes />
|
||||
<div className="sticky top-0 z-50">
|
||||
<LanguageBanner />
|
||||
<Header />
|
||||
</div>
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useAuth } from '@/presentation/hooks/useAuth';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { DEMO_EMAIL } from '@/lib/constants';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login, register } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: '',
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (isRegister) {
|
||||
const success = register({
|
||||
name: form.name,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
phone: form.phone,
|
||||
address: {
|
||||
street: form.street,
|
||||
city: form.city,
|
||||
state: form.state,
|
||||
zip: form.zip,
|
||||
},
|
||||
});
|
||||
if (success) router.push('/account');
|
||||
} else {
|
||||
const success = login(form.email, form.password);
|
||||
if (success) {
|
||||
router.push('/account');
|
||||
} else {
|
||||
setError(t('auth.invalidCredentials', { email: DEMO_EMAIL }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[70vh] items-center justify-center bg-gray-50 px-4 py-12">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<div className="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-brand-700">
|
||||
<span className="font-display text-xl font-bold text-gold-400">
|
||||
{t('site.initials')}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="font-display text-2xl font-bold text-brand-900">
|
||||
{isRegister ? t('auth.createAccount') : t('auth.welcomeBack')}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{isRegister
|
||||
? t('auth.joinTagline', { name: t('site.name') })
|
||||
: t('auth.signInTagline', { name: t('site.name') })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="card-premium p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{isRegister && (
|
||||
<div>
|
||||
<label className="label-text">{t('auth.fullName')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="label-text">{t('auth.email')}</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
className="input-field"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label-text">{t('auth.password')}</label>
|
||||
<input
|
||||
required
|
||||
type="password"
|
||||
className="input-field"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isRegister && (
|
||||
<>
|
||||
<div>
|
||||
<label className="label-text">{t('auth.phone')}</label>
|
||||
<input
|
||||
required
|
||||
type="tel"
|
||||
className="input-field"
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('auth.street')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.street}
|
||||
onChange={(e) => setForm({ ...form, street: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="label-text">{t('auth.city')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.city}
|
||||
onChange={(e) => setForm({ ...form, city: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('auth.state')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.state}
|
||||
onChange={(e) => setForm({ ...form, state: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="label-text">{t('auth.zip')}</label>
|
||||
<input
|
||||
required
|
||||
className="input-field"
|
||||
value={form.zip}
|
||||
onChange={(e) => setForm({ ...form, zip: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="rounded-lg bg-red-50 px-4 py-2 text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" variant="primary" size="lg" className="w-full">
|
||||
{isRegister ? t('auth.register') : t('auth.signIn')}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRegister(!isRegister);
|
||||
setError('');
|
||||
}}
|
||||
className="text-sm font-medium text-brand-600 hover:text-brand-800"
|
||||
>
|
||||
{isRegister ? t('auth.hasAccount') : t('auth.noAccount')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!isRegister && (
|
||||
<div className="mt-4 rounded-lg bg-brand-50 px-4 py-3 text-center">
|
||||
<p className="text-xs text-brand-700">
|
||||
{t('auth.demo', { email: DEMO_EMAIL })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function NotFound() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] flex-col items-center justify-center px-4 text-center">
|
||||
<h1 className="mb-2 font-display text-6xl font-bold text-brand-900">
|
||||
{t('notFound.title')}
|
||||
</h1>
|
||||
<p className="mb-6 text-lg text-gray-500">{t('notFound.message')}</p>
|
||||
<Link href="/">
|
||||
<Button>{t('notFound.goHome')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Hero from '@/components/home/Hero';
|
||||
import TrustBadges from '@/components/home/TrustBadges';
|
||||
import AboutPreview from '@/components/home/AboutPreview';
|
||||
import CategoryGrid from '@/components/home/CategoryGrid';
|
||||
import FeaturedProducts from '@/components/home/FeaturedProducts';
|
||||
import HowItWorks from '@/components/home/HowItWorks';
|
||||
import WeeklyOffers from '@/components/home/WeeklyOffers';
|
||||
import SocialFollow from '@/components/home/SocialFollow';
|
||||
import ContactPreview from '@/components/home/ContactPreview';
|
||||
import CTA from '@/components/home/CTA';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<TrustBadges />
|
||||
<AboutPreview />
|
||||
<CategoryGrid />
|
||||
<FeaturedProducts />
|
||||
<HowItWorks />
|
||||
<WeeklyOffers />
|
||||
<CTA />
|
||||
<SocialFollow />
|
||||
<ContactPreview />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import AppImage from '@/components/ui/AppImage';
|
||||
import Link from 'next/link';
|
||||
import { notFound, useParams } from 'next/navigation';
|
||||
import {
|
||||
ShoppingCart,
|
||||
Heart,
|
||||
ChevronLeft,
|
||||
Minus,
|
||||
Plus,
|
||||
Check,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react';
|
||||
import CustomizationSelector from '@/components/product/CustomizationSelector';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { getProductBySlug } from '@/lib/products';
|
||||
import { getDefaultCustomization, getCustomizationLabel } from '@/lib/customization';
|
||||
import { localizeProduct } from '@/lib/product-i18n';
|
||||
import { formatPrice, getFormatLocale } from '@/lib/utils';
|
||||
import { useCart } from '@/presentation/hooks/useCart';
|
||||
import { useWishlist } from '@/presentation/hooks/useWishlist';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { ProductCustomization } from '@/types';
|
||||
|
||||
export default function ProductPage() {
|
||||
const params = useParams();
|
||||
const slug = params.slug as string;
|
||||
const rawProduct = getProductBySlug(slug);
|
||||
const { t, locale } = useTranslation();
|
||||
|
||||
const [customization, setCustomization] = useState<ProductCustomization>(
|
||||
rawProduct ? getDefaultCustomization(rawProduct.category) : { type: 'fish' }
|
||||
);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [added, setAdded] = useState(false);
|
||||
|
||||
const { addItem } = useCart();
|
||||
const { isInWishlist, toggleItem } = useWishlist();
|
||||
|
||||
if (!rawProduct) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const product = localizeProduct(rawProduct, t);
|
||||
const inWishlist = isInWishlist(product.id);
|
||||
|
||||
const handleAddToCart = () => {
|
||||
const label = getCustomizationLabel(customization, t);
|
||||
addItem(rawProduct, customization, label, quantity);
|
||||
setAdded(true);
|
||||
setTimeout(() => setAdded(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<Link
|
||||
href="/shop"
|
||||
className="mb-6 inline-flex items-center gap-1 text-sm font-medium text-gray-500 transition-colors hover:text-brand-700"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 rtl:rotate-180" />
|
||||
{t('product.backToShop')}
|
||||
</Link>
|
||||
|
||||
<div className="grid gap-10 lg:grid-cols-2">
|
||||
<div>
|
||||
<div className="relative mb-4 aspect-square overflow-hidden rounded-2xl bg-gray-50">
|
||||
<AppImage
|
||||
src={product.images[selectedImage] || product.image}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
sizes="(max-width: 1024px) 100vw, 640px"
|
||||
/>
|
||||
{product.badge && (
|
||||
<span className="absolute start-4 top-4 rounded-full bg-gold-500 px-4 py-1.5 text-sm font-semibold text-white shadow-gold">
|
||||
{product.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{product.images.length > 1 && (
|
||||
<div className="flex gap-3">
|
||||
{product.images.map((img, i) => (
|
||||
<button
|
||||
key={img}
|
||||
onClick={() => setSelectedImage(i)}
|
||||
className={`relative h-20 w-20 overflow-hidden rounded-lg border-2 transition-all ${
|
||||
selectedImage === i
|
||||
? 'border-brand-700'
|
||||
: 'border-transparent opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<AppImage src={img} alt="" fill className="object-cover" sizes="80px" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Link
|
||||
href={`/shop?category=${product.category}`}
|
||||
className="rounded-full bg-brand-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-brand-700 transition-colors hover:bg-brand-100"
|
||||
>
|
||||
{t(`categories.${product.category}.name`)}
|
||||
</Link>
|
||||
{product.inStock ? (
|
||||
<span className="flex items-center gap-1 text-xs font-medium text-brand-700">
|
||||
<Check className="h-3 w-3" /> {t('product.inStock')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs font-medium text-red-500">
|
||||
{t('product.outOfStock')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="mb-2 font-display text-3xl font-bold text-brand-900 lg:text-4xl">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
<p className="mb-4 text-gray-500">{product.description}</p>
|
||||
|
||||
<div className="mb-6 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-brand-800">
|
||||
{formatPrice(product.price, getFormatLocale(locale))}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400">{product.priceUnit}</span>
|
||||
{product.weight && (
|
||||
<span className="text-sm text-gray-400">· {product.weight}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex items-center gap-2 rounded-lg border border-brand-100 bg-brand-50/50 px-4 py-3">
|
||||
<ShieldCheck className="h-5 w-5 shrink-0 text-brand-700" />
|
||||
<span className="text-sm text-brand-800">{t('product.halalTrust')}</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 rounded-2xl border border-gray-100 bg-gray-50 p-6">
|
||||
<CustomizationSelector
|
||||
category={product.category}
|
||||
customization={customization}
|
||||
onChange={setCustomization}
|
||||
/>
|
||||
<div className="mt-4 rounded-lg bg-white px-4 py-3">
|
||||
<p className="text-xs text-gray-500">{t('product.yourSelection')}</p>
|
||||
<p className="text-sm font-semibold text-brand-800">
|
||||
{getCustomizationLabel(customization, t)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div className="flex items-center rounded-lg border border-gray-200">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="flex h-12 w-12 items-center justify-center text-gray-500 transition-colors hover:text-brand-700"
|
||||
aria-label={t('product.decreaseQty')}
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="w-12 text-center font-semibold">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="flex h-12 w-12 items-center justify-center text-gray-500 transition-colors hover:text-brand-700"
|
||||
aria-label={t('product.increaseQty')}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
disabled={!product.inStock}
|
||||
className="flex-1"
|
||||
size="lg"
|
||||
>
|
||||
{added ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
{t('product.addedToCart')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
{t('product.addToCart')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<button
|
||||
onClick={() => toggleItem(rawProduct)}
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-lg border-2 transition-all ${
|
||||
inWishlist
|
||||
? 'border-red-200 bg-red-50 text-red-500'
|
||||
: 'border-gray-200 text-gray-400 hover:border-brand-300 hover:text-brand-700'
|
||||
}`}
|
||||
aria-label={
|
||||
inWishlist ? t('product.removeWishlist') : t('product.addWishlist')
|
||||
}
|
||||
>
|
||||
<Heart className={`h-5 w-5 ${inWishlist ? 'fill-current' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 pt-6">
|
||||
<h2 className="mb-3 font-display text-lg font-semibold text-brand-900">
|
||||
{t('product.aboutProduct')}
|
||||
</h2>
|
||||
<p className="text-sm leading-relaxed text-gray-600">
|
||||
{product.longDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Shop',
|
||||
description:
|
||||
'Browse premium halal chicken, beef, lamb, and frozen fish. Customized cuts delivered to your door.',
|
||||
};
|
||||
|
||||
export default function ShopLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <Suspense fallback={<ShopLoading />}>{children}</Suspense>;
|
||||
}
|
||||
|
||||
function ShopLoading() {
|
||||
return (
|
||||
<div className="flex min-h-[50vh] items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-brand-200 border-t-brand-700" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import ProductCard from '@/components/product/ProductCard';
|
||||
import ShopFilters from '@/components/shop/ShopFilters';
|
||||
import { Category, SortOption } from '@/domain/entities';
|
||||
import { FilterProductsUseCase } from '@/application/use-cases/catalog/FilterProducts';
|
||||
import { useCatalogFilter } from '@/presentation/hooks/useCatalog';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export default function ShopPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | 'all'>(
|
||||
FilterProductsUseCase.parseCategory(searchParams.get('category'))
|
||||
);
|
||||
const [sortBy, setSortBy] = useState<SortOption>('featured');
|
||||
const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '');
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedCategory(
|
||||
FilterProductsUseCase.parseCategory(searchParams.get('category'))
|
||||
);
|
||||
setSearchQuery(searchParams.get('q') || '');
|
||||
}, [searchParams]);
|
||||
|
||||
const pushParams = useCallback(
|
||||
(updates: { category?: Category | 'all'; q?: string }) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (updates.category !== undefined) {
|
||||
if (updates.category === 'all') params.delete('category');
|
||||
else params.set('category', updates.category);
|
||||
}
|
||||
|
||||
if (updates.q !== undefined) {
|
||||
if (updates.q) params.set('q', updates.q);
|
||||
else params.delete('q');
|
||||
}
|
||||
|
||||
const qs = params.toString();
|
||||
router.push(qs ? `/shop?${qs}` : '/shop', { scroll: false });
|
||||
},
|
||||
[router, searchParams]
|
||||
);
|
||||
|
||||
const handleCategoryChange = (category: Category | 'all') => {
|
||||
setSelectedCategory(category);
|
||||
pushParams({ category });
|
||||
};
|
||||
|
||||
const handleSearchChange = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
pushParams({ q: query });
|
||||
};
|
||||
|
||||
const filteredProducts = useCatalogFilter({
|
||||
category: selectedCategory,
|
||||
searchQuery,
|
||||
sortBy,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
<div className="border-b border-gray-100 bg-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<h1 className="section-heading mb-2">{t('shop.title')}</h1>
|
||||
<p className="text-gray-500">{t('shop.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col gap-8 lg:flex-row">
|
||||
<aside className="lg:w-64 lg:shrink-0">
|
||||
<div className="card-premium sticky top-24 p-6">
|
||||
<ShopFilters
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={handleCategoryChange}
|
||||
sortBy={sortBy}
|
||||
onSortChange={setSortBy}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
totalResults={filteredProducts.length}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="flex-1">
|
||||
{filteredProducts.length === 0 ? (
|
||||
<div className="card-premium flex flex-col items-center justify-center p-16 text-center">
|
||||
<p className="mb-2 text-lg font-semibold text-brand-900">
|
||||
{t('shop.noProducts')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{t('shop.noProductsHint')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredProducts.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Heart } from 'lucide-react';
|
||||
import ProductCard from '@/components/product/ProductCard';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { useWishlist } from '@/presentation/hooks/useWishlist';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { localizeProduct } from '@/lib/product-i18n';
|
||||
|
||||
export default function WishlistPage() {
|
||||
const { items } = useWishlist();
|
||||
const { t } = useTranslation();
|
||||
const localizedItems = items.map((p) => localizeProduct(p, t));
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50">
|
||||
<div className="border-b border-gray-100 bg-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<h1 className="section-heading">{t('wishlist.title')}</h1>
|
||||
<p className="text-gray-500">
|
||||
{t('wishlist.saved', { count: items.length })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
|
||||
{items.length === 0 ? (
|
||||
<div className="card-premium flex flex-col items-center justify-center p-16 text-center">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-50">
|
||||
<Heart className="h-8 w-8 text-red-300" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold text-brand-900">
|
||||
{t('wishlist.empty')}
|
||||
</h2>
|
||||
<p className="mb-6 text-sm text-gray-500">{t('wishlist.emptyHint')}</p>
|
||||
<Link href="/shop">
|
||||
<Button>{t('wishlist.browse')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{localizedItems.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* COMPOSITION ROOT — Wires ports to infrastructure and exposes use cases.
|
||||
* This is the only place that should know concrete implementations.
|
||||
*/
|
||||
import { InMemoryProductRepository } from '@/infrastructure/repositories/InMemoryProductRepository';
|
||||
import {
|
||||
ZustandCartRepository,
|
||||
} from '@/infrastructure/persistence/zustand/cartStore';
|
||||
import {
|
||||
ZustandAuthRepository,
|
||||
} from '@/infrastructure/persistence/zustand/authStore';
|
||||
import {
|
||||
ZustandWishlistRepository,
|
||||
} from '@/infrastructure/persistence/zustand/wishlistStore';
|
||||
import { I18nTranslationService } from '@/infrastructure/i18n/I18nTranslationService';
|
||||
import { LocalizeProductUseCase } from '@/application/use-cases/catalog/LocalizeProduct';
|
||||
import { FilterProductsUseCase } from '@/application/use-cases/catalog/FilterProducts';
|
||||
import { GetProductBySlugUseCase } from '@/application/use-cases/catalog/GetProductBySlug';
|
||||
import { AddToCartUseCase } from '@/application/use-cases/cart/AddToCart';
|
||||
import { GetCartSummaryUseCase } from '@/application/use-cases/cart/GetCartSummary';
|
||||
import { UpdateCartQuantityUseCase } from '@/application/use-cases/cart/UpdateCartQuantity';
|
||||
import { RemoveFromCartUseCase } from '@/application/use-cases/cart/RemoveFromCart';
|
||||
import { PlaceOrderUseCase } from '@/application/use-cases/checkout/PlaceOrder';
|
||||
import { LoginUseCase } from '@/application/use-cases/auth/Login';
|
||||
import { RegisterUseCase } from '@/application/use-cases/auth/Register';
|
||||
import { ToggleWishlistUseCase } from '@/application/use-cases/wishlist/ToggleWishlist';
|
||||
import { Locale } from '@/i18n/types';
|
||||
|
||||
class ApplicationContainer {
|
||||
readonly productRepository = new InMemoryProductRepository();
|
||||
readonly cartRepository = new ZustandCartRepository();
|
||||
readonly authRepository = new ZustandAuthRepository();
|
||||
readonly wishlistRepository = new ZustandWishlistRepository();
|
||||
|
||||
createTranslationService(locale: Locale): I18nTranslationService {
|
||||
return new I18nTranslationService(locale);
|
||||
}
|
||||
|
||||
createLocalizeProduct(locale: Locale): LocalizeProductUseCase {
|
||||
return new LocalizeProductUseCase(this.createTranslationService(locale));
|
||||
}
|
||||
|
||||
readonly getProductBySlug = new GetProductBySlugUseCase(this.productRepository);
|
||||
readonly addToCart = new AddToCartUseCase(this.cartRepository);
|
||||
readonly getCartSummary = new GetCartSummaryUseCase(this.cartRepository);
|
||||
readonly updateCartQuantity = new UpdateCartQuantityUseCase(this.cartRepository);
|
||||
readonly removeFromCart = new RemoveFromCartUseCase(this.cartRepository);
|
||||
readonly placeOrder = new PlaceOrderUseCase(
|
||||
this.cartRepository,
|
||||
this.authRepository
|
||||
);
|
||||
readonly login = new LoginUseCase(this.authRepository);
|
||||
readonly register = new RegisterUseCase(this.authRepository);
|
||||
readonly toggleWishlist = new ToggleWishlistUseCase(this.wishlistRepository);
|
||||
|
||||
createFilterProducts(locale: Locale): FilterProductsUseCase {
|
||||
return new FilterProductsUseCase(
|
||||
this.productRepository,
|
||||
this.createLocalizeProduct(locale)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const container = new ApplicationContainer();
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface CartSummary {
|
||||
subtotal: number;
|
||||
deliveryFee: number;
|
||||
grandTotal: number;
|
||||
itemCount: number;
|
||||
isFreeDelivery: boolean;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
|
||||
export interface LocalizedProduct {
|
||||
id: string;
|
||||
slug: string;
|
||||
category: Product['category'];
|
||||
name: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
price: number;
|
||||
priceUnit: string;
|
||||
image: string;
|
||||
images: string[];
|
||||
badge?: string;
|
||||
inStock: boolean;
|
||||
featured: boolean;
|
||||
weight?: string;
|
||||
tags: string[];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Address, Order, User } from '@/domain/entities';
|
||||
import { RegisterInput } from '@/domain/services/AuthDomainService';
|
||||
|
||||
/**
|
||||
* PORT — Contract for user session and orders
|
||||
*/
|
||||
export interface IAuthRepository {
|
||||
getUser(): User | null;
|
||||
getOrders(): Order[];
|
||||
isAuthenticated(): boolean;
|
||||
login(email: string, password: string): boolean;
|
||||
register(data: RegisterInput): boolean;
|
||||
logout(): void;
|
||||
updateProfile(data: Partial<User>): void;
|
||||
addOrder(order: Order): void;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { CartItem, Product, ProductCustomization } from '@/domain/entities';
|
||||
|
||||
/**
|
||||
* PORT — Contract for cart persistence
|
||||
*/
|
||||
export interface ICartRepository {
|
||||
getItems(): CartItem[];
|
||||
setItems(items: CartItem[]): void;
|
||||
addItem(
|
||||
product: Product,
|
||||
customization: ProductCustomization,
|
||||
customizationLabel: string,
|
||||
quantity?: number
|
||||
): void;
|
||||
removeItem(id: string): void;
|
||||
updateQuantity(id: string, quantity: number): void;
|
||||
clearCart(): void;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
|
||||
/**
|
||||
* PORT — Contract for loading products (in-memory today, API/CMS tomorrow)
|
||||
*/
|
||||
export interface IProductRepository {
|
||||
findAll(): Product[];
|
||||
findBySlug(slug: string): Product | undefined;
|
||||
findByCategory(category: string): Product[];
|
||||
findFeatured(): Product[];
|
||||
findById(id: string): Product | undefined;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* PORT — Contract for UI translations
|
||||
*/
|
||||
export interface ITranslationService {
|
||||
translate(
|
||||
path: string,
|
||||
params?: Record<string, string | number>
|
||||
): string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
|
||||
/**
|
||||
* PORT — Contract for wishlist persistence
|
||||
*/
|
||||
export interface IWishlistRepository {
|
||||
getItems(): Product[];
|
||||
addItem(product: Product): void;
|
||||
removeItem(productId: string): void;
|
||||
isInWishlist(productId: string): boolean;
|
||||
toggleItem(product: Product): void;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IAuthRepository } from '@/application/ports/IAuthRepository';
|
||||
|
||||
export class LoginUseCase {
|
||||
constructor(private readonly auth: IAuthRepository) {}
|
||||
|
||||
execute(email: string, password: string): boolean {
|
||||
return this.auth.login(email, password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { RegisterInput } from '@/domain/services/AuthDomainService';
|
||||
import { IAuthRepository } from '@/application/ports/IAuthRepository';
|
||||
|
||||
export class RegisterUseCase {
|
||||
constructor(private readonly auth: IAuthRepository) {}
|
||||
|
||||
execute(data: RegisterInput): boolean {
|
||||
return this.auth.register(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Product, ProductCustomization } from '@/domain/entities';
|
||||
import { ICartRepository } from '@/application/ports/ICartRepository';
|
||||
|
||||
/**
|
||||
* USE CASE — Add a product line to the cart (delegates merge logic to domain + repo)
|
||||
*/
|
||||
export class AddToCartUseCase {
|
||||
constructor(private readonly cart: ICartRepository) {}
|
||||
|
||||
execute(
|
||||
product: Product,
|
||||
customization: ProductCustomization,
|
||||
customizationLabel: string,
|
||||
quantity = 1
|
||||
): void {
|
||||
this.cart.addItem(product, customization, customizationLabel, quantity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CartDomainService } from '@/domain/services/CartDomainService';
|
||||
import { DeliveryFeeService } from '@/domain/services/DeliveryFeeService';
|
||||
import { CartSummary } from '@/application/dtos/CartSummary';
|
||||
import { ICartRepository } from '@/application/ports/ICartRepository';
|
||||
|
||||
/**
|
||||
* USE CASE — Cart totals with delivery fee (single business rule path)
|
||||
*/
|
||||
export class GetCartSummaryUseCase {
|
||||
constructor(private readonly cart: ICartRepository) {}
|
||||
|
||||
execute(): CartSummary {
|
||||
const items = this.cart.getItems();
|
||||
const subtotal = CartDomainService.calculateSubtotal(items);
|
||||
const deliveryFee = DeliveryFeeService.calculateDeliveryFee(subtotal);
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
deliveryFee,
|
||||
grandTotal: subtotal + deliveryFee,
|
||||
itemCount: CartDomainService.calculateItemCount(items),
|
||||
isFreeDelivery: DeliveryFeeService.isFreeDelivery(subtotal),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ICartRepository } from '@/application/ports/ICartRepository';
|
||||
|
||||
export class RemoveFromCartUseCase {
|
||||
constructor(private readonly cart: ICartRepository) {}
|
||||
|
||||
execute(id: string): void {
|
||||
this.cart.removeItem(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ICartRepository } from '@/application/ports/ICartRepository';
|
||||
|
||||
/**
|
||||
* USE CASE — Change quantity or remove when zero
|
||||
*/
|
||||
export class UpdateCartQuantityUseCase {
|
||||
constructor(private readonly cart: ICartRepository) {}
|
||||
|
||||
execute(id: string, quantity: number): void {
|
||||
this.cart.updateQuantity(id, quantity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Category, Product, SortOption } from '@/domain/entities';
|
||||
import {
|
||||
CatalogDomainService,
|
||||
CategoryFilter,
|
||||
} from '@/domain/services/CatalogDomainService';
|
||||
import { IProductRepository } from '@/application/ports/IProductRepository';
|
||||
import { LocalizeProductUseCase } from './LocalizeProduct';
|
||||
import { LocalizedProduct } from '@/application/dtos/LocalizedProduct';
|
||||
|
||||
export interface FilterProductsInput {
|
||||
category: CategoryFilter;
|
||||
searchQuery: string;
|
||||
sortBy: SortOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* USE CASE — Shop page: load, filter, sort, localize products
|
||||
*/
|
||||
export class FilterProductsUseCase {
|
||||
constructor(
|
||||
private readonly products: IProductRepository,
|
||||
private readonly localize: LocalizeProductUseCase
|
||||
) {}
|
||||
|
||||
execute(input: FilterProductsInput): LocalizedProduct[] {
|
||||
const all = this.products.findAll();
|
||||
const searchIndex = all.map((product) => {
|
||||
const localized = this.localize.execute(product);
|
||||
return {
|
||||
product,
|
||||
name: localized.name,
|
||||
description: localized.description,
|
||||
};
|
||||
});
|
||||
|
||||
const filtered = CatalogDomainService.filterAndSort({
|
||||
products: all,
|
||||
category: input.category,
|
||||
searchQuery: input.searchQuery,
|
||||
sortBy: input.sortBy,
|
||||
searchIndex,
|
||||
});
|
||||
|
||||
return this.localize.executeMany(filtered);
|
||||
}
|
||||
|
||||
static parseCategory(value: string | null): CategoryFilter {
|
||||
return CatalogDomainService.parseCategory(value);
|
||||
}
|
||||
|
||||
static readonly CATEGORIES: Category[] = CatalogDomainService.CATEGORIES;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
import { IProductRepository } from '@/application/ports/IProductRepository';
|
||||
|
||||
/**
|
||||
* USE CASE — Load one product for the product detail page
|
||||
*/
|
||||
export class GetProductBySlugUseCase {
|
||||
constructor(private readonly products: IProductRepository) {}
|
||||
|
||||
execute(slug: string): Product | undefined {
|
||||
return this.products.findBySlug(slug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
import { LocalizedProduct } from '@/application/dtos/LocalizedProduct';
|
||||
import { ITranslationService } from '@/application/ports/ITranslationService';
|
||||
|
||||
/**
|
||||
* USE CASE — Turn a Product entity into display-ready LocalizedProduct
|
||||
*/
|
||||
export class LocalizeProductUseCase {
|
||||
constructor(private readonly translation: ITranslationService) {}
|
||||
|
||||
execute(product: Product): LocalizedProduct {
|
||||
const base = `products.${product.id}`;
|
||||
const t = this.translation.translate.bind(this.translation);
|
||||
|
||||
return {
|
||||
...product,
|
||||
name: t(`${base}.name`),
|
||||
description: t(`${base}.description`),
|
||||
longDescription: t(`${base}.longDescription`),
|
||||
priceUnit: t(`priceUnit.${product.priceUnitKey}`),
|
||||
badge: product.badgeKey ? t(`badges.${product.badgeKey}`) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
executeMany(products: Product[]): LocalizedProduct[] {
|
||||
return products.map((p) => this.execute(p));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Address, Order } from '@/domain/entities';
|
||||
import { OrderDomainService } from '@/domain/services/OrderDomainService';
|
||||
import { ICartRepository } from '@/application/ports/ICartRepository';
|
||||
import { IAuthRepository } from '@/application/ports/IAuthRepository';
|
||||
|
||||
export interface PlaceOrderInput {
|
||||
deliveryAddress: Address;
|
||||
paymentMethod: string;
|
||||
}
|
||||
|
||||
export interface PlaceOrderResult {
|
||||
order: Order;
|
||||
}
|
||||
|
||||
/**
|
||||
* USE CASE — Checkout: create order, save to auth, clear cart
|
||||
*/
|
||||
export class PlaceOrderUseCase {
|
||||
constructor(
|
||||
private readonly cart: ICartRepository,
|
||||
private readonly auth: IAuthRepository
|
||||
) {}
|
||||
|
||||
execute(input: PlaceOrderInput): PlaceOrderResult {
|
||||
const items = this.cart.getItems();
|
||||
const order = OrderDomainService.createOrder({
|
||||
items,
|
||||
deliveryAddress: input.deliveryAddress,
|
||||
paymentMethod: input.paymentMethod,
|
||||
});
|
||||
|
||||
this.auth.addOrder(order);
|
||||
this.cart.clearCart();
|
||||
|
||||
return { order };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
import { IWishlistRepository } from '@/application/ports/IWishlistRepository';
|
||||
|
||||
export class ToggleWishlistUseCase {
|
||||
constructor(private readonly wishlist: IWishlistRepository) {}
|
||||
|
||||
execute(product: Product): void {
|
||||
this.wishlist.toggleItem(product);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* DOMAIN LAYER — Business constants (rules that could change by policy, not by framework)
|
||||
*/
|
||||
|
||||
export const FREE_DELIVERY_THRESHOLD_SEK = 500;
|
||||
export const STANDARD_DELIVERY_FEE_SEK = 49;
|
||||
export const DEMO_PASSWORD = 'demo123';
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* DOMAIN LAYER — Entities
|
||||
* Pure business objects. No React, no Next.js, no Zustand, no localStorage.
|
||||
*/
|
||||
|
||||
export type Category = 'chicken' | 'beef' | 'lamb' | 'fish';
|
||||
|
||||
export type CutCount = 4 | 8 | 10 | 12;
|
||||
|
||||
export type CuttingStyleKey = 'nihari' | 'karahi' | 'qeema' | 'boneless' | 'steak';
|
||||
|
||||
export type MeatCategory = 'chicken' | 'beef' | 'lamb';
|
||||
|
||||
export interface MeatCustomization {
|
||||
type: MeatCategory;
|
||||
cuts: CutCount;
|
||||
cuttingStyle: CuttingStyleKey;
|
||||
}
|
||||
|
||||
export type ProductCustomization = MeatCustomization | { type: 'fish' };
|
||||
|
||||
export function isMeatCustomization(
|
||||
customization: ProductCustomization
|
||||
): customization is MeatCustomization {
|
||||
return customization.type !== 'fish';
|
||||
}
|
||||
|
||||
export type PriceUnitKey = 'perBird' | 'perPack' | 'perKg';
|
||||
export type BadgeKey =
|
||||
| 'bestseller'
|
||||
| 'chefsPick'
|
||||
| 'premium'
|
||||
| 'popular'
|
||||
| 'freshCatch'
|
||||
| 'frozen';
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
slug: string;
|
||||
category: Category;
|
||||
price: number;
|
||||
priceUnitKey: PriceUnitKey;
|
||||
image: string;
|
||||
images: string[];
|
||||
badgeKey?: BadgeKey;
|
||||
inStock: boolean;
|
||||
featured: boolean;
|
||||
weight?: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
id: string;
|
||||
product: Product;
|
||||
quantity: number;
|
||||
customization: ProductCustomization;
|
||||
customizationLabel: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: Address;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
street: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
items: CartItem[];
|
||||
total: number;
|
||||
status: 'pending' | 'confirmed' | 'preparing' | 'out-for-delivery' | 'delivered';
|
||||
createdAt: string;
|
||||
deliveryAddress: Address;
|
||||
paymentMethod: string;
|
||||
}
|
||||
|
||||
export type SortOption = 'featured' | 'price-asc' | 'price-desc' | 'name';
|
||||
|
||||
export type OfferBadgeKey = 'fresh' | 'halal';
|
||||
|
||||
export interface WeeklyOffer {
|
||||
id: string;
|
||||
nameKey: string;
|
||||
badgeKey: OfferBadgeKey;
|
||||
price: number;
|
||||
originalPrice?: number;
|
||||
priceUnitKey: PriceUnitKey;
|
||||
image: string;
|
||||
whatsappProduct: string;
|
||||
productSlug: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Address, User } from '@/domain/entities';
|
||||
import { DEMO_PASSWORD } from '@/domain/constants/commerce';
|
||||
|
||||
export interface RegisterInput {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
phone: string;
|
||||
address: Address;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOMAIN SERVICE — Authentication business rules (demo app; no real password hashing)
|
||||
*/
|
||||
export class AuthDomainService {
|
||||
static isDemoCredentials(email: string, password: string): boolean {
|
||||
return password === DEMO_PASSWORD;
|
||||
}
|
||||
|
||||
static createUser(data: RegisterInput): User {
|
||||
return {
|
||||
id: `user-${Date.now()}`,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
address: data.address,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
static emailMatchesStoredUser(stored: User | null, email: string): boolean {
|
||||
return stored !== null && stored.email === email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { CartItem, Product, ProductCustomization } from '@/domain/entities';
|
||||
import { CustomizationDomainService } from './CustomizationDomainService';
|
||||
|
||||
/**
|
||||
* DOMAIN SERVICE — Cart business rules (pure functions, no storage)
|
||||
*/
|
||||
export class CartDomainService {
|
||||
static calculateSubtotal(items: CartItem[]): number {
|
||||
return items.reduce((sum, item) => sum + item.product.price * item.quantity, 0);
|
||||
}
|
||||
|
||||
static calculateItemCount(items: CartItem[]): number {
|
||||
return items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
}
|
||||
|
||||
static addItem(
|
||||
items: CartItem[],
|
||||
product: Product,
|
||||
customization: ProductCustomization,
|
||||
customizationLabel: string,
|
||||
quantity = 1
|
||||
): CartItem[] {
|
||||
const id = CustomizationDomainService.getCartItemKey(product.id, customization);
|
||||
const existing = items.find((item) => item.id === id);
|
||||
|
||||
if (existing) {
|
||||
return items.map((item) =>
|
||||
item.id === id ? { ...item, quantity: item.quantity + quantity } : item
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
...items,
|
||||
{ id, product, quantity, customization, customizationLabel },
|
||||
];
|
||||
}
|
||||
|
||||
static removeItem(items: CartItem[], id: string): CartItem[] {
|
||||
return items.filter((item) => item.id !== id);
|
||||
}
|
||||
|
||||
static updateQuantity(items: CartItem[], id: string, quantity: number): CartItem[] {
|
||||
if (quantity <= 0) {
|
||||
return CartDomainService.removeItem(items, id);
|
||||
}
|
||||
return items.map((item) => (item.id === id ? { ...item, quantity } : item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Category, Product, SortOption } from '@/domain/entities';
|
||||
|
||||
export type CategoryFilter = Category | 'all';
|
||||
|
||||
export interface FilterProductsInput {
|
||||
products: Product[];
|
||||
category: CategoryFilter;
|
||||
searchQuery: string;
|
||||
sortBy: SortOption;
|
||||
/** Resolved display names for search (application layer provides these) */
|
||||
searchIndex?: Array<{
|
||||
product: Product;
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOMAIN SERVICE — Catalog filtering and sorting rules
|
||||
*/
|
||||
export class CatalogDomainService {
|
||||
static readonly CATEGORIES: Category[] = ['chicken', 'beef', 'lamb', 'fish'];
|
||||
|
||||
static parseCategory(value: string | null): CategoryFilter {
|
||||
return CatalogDomainService.CATEGORIES.includes(value as Category)
|
||||
? (value as Category)
|
||||
: 'all';
|
||||
}
|
||||
|
||||
static filterAndSort(input: FilterProductsInput): Product[] {
|
||||
let result = [...input.products];
|
||||
|
||||
if (input.category !== 'all') {
|
||||
result = result.filter((p) => p.category === input.category);
|
||||
}
|
||||
|
||||
const query = input.searchQuery.trim().toLowerCase();
|
||||
if (query && input.searchIndex) {
|
||||
const matchingIds = new Set(
|
||||
input.searchIndex
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.name.toLowerCase().includes(query) ||
|
||||
entry.description.toLowerCase().includes(query) ||
|
||||
entry.product.tags.some((tag) => tag.includes(query))
|
||||
)
|
||||
.map((entry) => entry.product.id)
|
||||
);
|
||||
result = result.filter((p) => matchingIds.has(p.id));
|
||||
}
|
||||
|
||||
switch (input.sortBy) {
|
||||
case 'price-asc':
|
||||
result.sort((a, b) => a.price - b.price);
|
||||
break;
|
||||
case 'price-desc':
|
||||
result.sort((a, b) => b.price - a.price);
|
||||
break;
|
||||
case 'name':
|
||||
if (input.searchIndex) {
|
||||
const nameMap = new Map(
|
||||
input.searchIndex.map((e) => [e.product.id, e.name])
|
||||
);
|
||||
result.sort((a, b) =>
|
||||
(nameMap.get(a.id) ?? '').localeCompare(nameMap.get(b.id) ?? '')
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'featured':
|
||||
default:
|
||||
result.sort((a, b) => (b.featured ? 1 : 0) - (a.featured ? 1 : 0));
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Category,
|
||||
MeatCustomization,
|
||||
ProductCustomization,
|
||||
} from '@/domain/entities';
|
||||
|
||||
export type Translator = (
|
||||
path: string,
|
||||
params?: Record<string, string | number>
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* DOMAIN SERVICE — Product customization rules
|
||||
*/
|
||||
export class CustomizationDomainService {
|
||||
static getDefaultCustomization(category: Category): ProductCustomization {
|
||||
if (category === 'fish') {
|
||||
return { type: 'fish' };
|
||||
}
|
||||
return {
|
||||
type: category,
|
||||
cuts: 8,
|
||||
cuttingStyle: 'karahi',
|
||||
};
|
||||
}
|
||||
|
||||
static getCustomizationKey(customization: ProductCustomization): string {
|
||||
if (customization.type === 'fish') {
|
||||
return 'standard';
|
||||
}
|
||||
return `cuts-${customization.cuts}::${customization.cuttingStyle}`;
|
||||
}
|
||||
|
||||
static getCustomizationLabel(
|
||||
customization: ProductCustomization,
|
||||
t: Translator
|
||||
): string {
|
||||
if (customization.type === 'fish') {
|
||||
return t('product.standardCut');
|
||||
}
|
||||
return t('product.cutsAndStyle', {
|
||||
cuts: customization.cuts,
|
||||
style: t(`cutting.${customization.cuttingStyle}`),
|
||||
});
|
||||
}
|
||||
|
||||
static getCartItemKey(
|
||||
productId: string,
|
||||
customization: ProductCustomization
|
||||
): string {
|
||||
return `${productId}::${CustomizationDomainService.getCustomizationKey(customization)}`;
|
||||
}
|
||||
|
||||
static updateMeatCustomization(
|
||||
current: ProductCustomization,
|
||||
category: Category,
|
||||
update: Partial<Pick<MeatCustomization, 'cuts' | 'cuttingStyle'>>
|
||||
): MeatCustomization {
|
||||
const base =
|
||||
current.type !== 'fish' && current.type === category
|
||||
? current
|
||||
: (CustomizationDomainService.getDefaultCustomization(
|
||||
category
|
||||
) as MeatCustomization);
|
||||
|
||||
return {
|
||||
type: category as MeatCustomization['type'],
|
||||
cuts: update.cuts ?? base.cuts,
|
||||
cuttingStyle: update.cuttingStyle ?? base.cuttingStyle,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
FREE_DELIVERY_THRESHOLD_SEK,
|
||||
STANDARD_DELIVERY_FEE_SEK,
|
||||
} from '@/domain/constants/commerce';
|
||||
|
||||
/**
|
||||
* DOMAIN SERVICE — Delivery pricing rules (single source of truth)
|
||||
*/
|
||||
export class DeliveryFeeService {
|
||||
static calculateDeliveryFee(subtotal: number): number {
|
||||
return subtotal > FREE_DELIVERY_THRESHOLD_SEK ? 0 : STANDARD_DELIVERY_FEE_SEK;
|
||||
}
|
||||
|
||||
static calculateGrandTotal(subtotal: number): number {
|
||||
return subtotal + DeliveryFeeService.calculateDeliveryFee(subtotal);
|
||||
}
|
||||
|
||||
static isFreeDelivery(subtotal: number): boolean {
|
||||
return subtotal > FREE_DELIVERY_THRESHOLD_SEK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Address, CartItem, Order } from '@/domain/entities';
|
||||
import { DeliveryFeeService } from './DeliveryFeeService';
|
||||
import { CartDomainService } from './CartDomainService';
|
||||
|
||||
export interface CreateOrderInput {
|
||||
items: CartItem[];
|
||||
deliveryAddress: Address;
|
||||
paymentMethod: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DOMAIN SERVICE — Order creation rules
|
||||
*/
|
||||
export class OrderDomainService {
|
||||
static generateOrderId(): string {
|
||||
return `KG-${Date.now().toString(36).toUpperCase()}`;
|
||||
}
|
||||
|
||||
static createOrder(input: CreateOrderInput): Order {
|
||||
const subtotal = CartDomainService.calculateSubtotal(input.items);
|
||||
const total = DeliveryFeeService.calculateGrandTotal(subtotal);
|
||||
|
||||
return {
|
||||
id: OrderDomainService.generateOrderId(),
|
||||
items: [...input.items],
|
||||
total,
|
||||
status: 'confirmed',
|
||||
createdAt: new Date().toISOString(),
|
||||
deliveryAddress: input.deliveryAddress,
|
||||
paymentMethod: input.paymentMethod,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { createTranslator } from '@/i18n';
|
||||
import { useLocaleStore } from '@/store/locale';
|
||||
|
||||
export function useTranslation() {
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
|
||||
const t = useMemo(() => createTranslator(locale), [locale]);
|
||||
|
||||
return { t, locale };
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Locale, TranslationDict } from './types';
|
||||
import { en } from './locales/en';
|
||||
import { sv } from './locales/sv';
|
||||
import { ur } from './locales/ur';
|
||||
import { ar } from './locales/ar';
|
||||
import { fa } from './locales/fa';
|
||||
import { tr } from './locales/tr';
|
||||
|
||||
const dictionaries: Record<Locale, TranslationDict> = {
|
||||
en,
|
||||
sv,
|
||||
ur,
|
||||
ar,
|
||||
fa,
|
||||
tr,
|
||||
};
|
||||
|
||||
function resolve(obj: TranslationDict, path: string): string {
|
||||
const keys = path.split('.');
|
||||
let current: string | TranslationDict = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (typeof current !== 'object' || current === null || !(key in current)) {
|
||||
return path;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : path;
|
||||
}
|
||||
|
||||
export function createTranslator(locale: Locale) {
|
||||
const dict = dictionaries[locale] ?? dictionaries.en;
|
||||
|
||||
return function t(path: string, params?: Record<string, string | number>): string {
|
||||
let text = resolve(dict, path);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value));
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
}
|
||||
|
||||
export function isRtl(locale: Locale): boolean {
|
||||
return locale === 'ur' || locale === 'ar' || locale === 'fa';
|
||||
}
|
||||
|
||||
export function getHtmlLang(locale: Locale): string {
|
||||
const map: Record<Locale, string> = {
|
||||
en: 'en',
|
||||
sv: 'sv',
|
||||
ur: 'ur',
|
||||
ar: 'ar',
|
||||
fa: 'fa',
|
||||
tr: 'tr',
|
||||
};
|
||||
return map[locale];
|
||||
}
|
||||
|
||||
export function usesArabicScript(locale: Locale): boolean {
|
||||
return locale === 'ur' || locale === 'ar' || locale === 'fa';
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
import { TranslationDict } from '../types';
|
||||
|
||||
export const ar: TranslationDict = {
|
||||
site: {
|
||||
name: 'كوت غارد',
|
||||
tagline: 'حلال فاخر',
|
||||
description:
|
||||
'توصيل لحوم حلال فاخرة 100%. دجاج ولحم بقر وخروف طازج؛ سمك ومأكولات بحرية مجمدة فقط — مُحضّر حسب تفضيلاتك ويُوصَل إلى باب منزلك.',
|
||||
metaTitle: 'توصيل لحوم حلال فاخرة',
|
||||
initials: 'KG',
|
||||
email: 'hello@kottgard.se',
|
||||
demoEmail: 'demo@kottgard.se',
|
||||
},
|
||||
nav: {
|
||||
shop: 'المتجر',
|
||||
chicken: 'الدجاج',
|
||||
beef: 'لحم البقر',
|
||||
lamb: 'لحم الخروف',
|
||||
fish: 'سمك مجمد',
|
||||
about: 'من نحن',
|
||||
halalCert: 'شهادة الحلال',
|
||||
delivery: 'معلومات التوصيل',
|
||||
contact: 'اتصل بنا',
|
||||
myAccount: 'حسابي',
|
||||
orderHistory: 'سجل الطلبات',
|
||||
wishlist: 'قائمة الأمنيات',
|
||||
cart: 'سلة التسوق',
|
||||
privacy: 'سياسة الخصوصية',
|
||||
terms: 'شروط الخدمة',
|
||||
searchProducts: 'البحث عن المنتجات',
|
||||
toggleMenu: 'فتح القائمة',
|
||||
account: 'الحساب',
|
||||
language: 'اللغة',
|
||||
},
|
||||
languageBanner: {
|
||||
choose: 'اختر لغتك',
|
||||
},
|
||||
hero: {
|
||||
badge: 'حلال معتمد 100%',
|
||||
taglineShort: 'نقاء طبيعي',
|
||||
title: 'لحوم حلال فاخرة',
|
||||
titleHighlight: '',
|
||||
titleEnd: '',
|
||||
subtitleShort: 'طازج. جودة. موثوق.',
|
||||
subtitle:
|
||||
'معتمد حلال · طازج يومياً · توصيل منزلي · مفتوح كل يوم',
|
||||
hours: 'مفتوح كل يوم {hours}',
|
||||
location: 'Tingvallavägen 11, Märsta',
|
||||
shopNow: 'تصفح مجموعتنا',
|
||||
browseChicken: 'تصفح الدجاج',
|
||||
whatsapp: 'اطلب عبر واتساب',
|
||||
},
|
||||
aboutPreview: {
|
||||
label: 'من نحن',
|
||||
title: 'أفضل جزار في ميرستا',
|
||||
p1: 'كوت غارد أكثر من جزار — نحن وعد بالجودة. جميع لحومنا معتمدة حلال 100% وتُوصَل طازجة كل يوم.',
|
||||
p2: 'نستورد لحم الخروف من أيرلندا ونيوزيلندا، والدجاج ولحم البقر من منتجين موثوقين، ونساعدك في اختيار القطعة المناسبة للعشاء أو الاحتفالات أو الشواء الأحد.',
|
||||
p3: 'زرنا في Tingvallavägen، أخبرنا بما تبحث عنه — نقطّع ونغلّف حسب مواصفاتك.',
|
||||
statHalal: '100%',
|
||||
statHalalLabel: 'معتمد حلال',
|
||||
statDays: '7 أيام',
|
||||
statDaysLabel: 'مفتوح أسبوعياً',
|
||||
statDelivery: 'يومياً',
|
||||
statDeliveryLabel: 'التوصيل',
|
||||
statFresh: 'طازج',
|
||||
statFreshLabel: 'كل يوم',
|
||||
readMore: 'اقرأ المزيد عنا',
|
||||
imageAlt: 'قطع لحم طازجة على لوح تقطيع من كوت غارد',
|
||||
},
|
||||
trust: {
|
||||
halal: 'حلال 100%',
|
||||
halalDesc: 'مصادر حلال معتمدة مع تتبع كامل والتزام بالمعايير.',
|
||||
fresh: 'طازج يومياً',
|
||||
freshDesc: 'يُورد طازجاً كل صباح ويُوصَل بأعلى جودة.',
|
||||
premium: 'جودة فاخرة',
|
||||
premiumDesc: 'قطع مختارة يدوياً من مزارع موثوقة، مُحضّرة على يد جزارين خبراء.',
|
||||
},
|
||||
categories: {
|
||||
title: 'مجموعتنا',
|
||||
subtitle: 'لحوم مختارة يدوياً — كل يوم. توصيل طازج. حلال. تقطيع حسب الطلب.',
|
||||
shop: 'تسوق {name}',
|
||||
chicken: {
|
||||
name: 'الدجاج',
|
||||
description: 'صدر فيليه، أجنحة، أفخاذ ودجاجة كاملة. طازج كل صباح.',
|
||||
},
|
||||
beef: {
|
||||
name: 'لحم البقر والعجل',
|
||||
description: 'مفروم، نخاع العظم وقطع فاخرة. تزيين عالي وجودة ثابتة.',
|
||||
},
|
||||
lamb: {
|
||||
name: 'لحم الخروف',
|
||||
description: 'كتف، رقبة، مشوي وضلوع. من أيرلندا ونيوزيلندا.',
|
||||
},
|
||||
fish: {
|
||||
name: 'سمك مجمد',
|
||||
description: 'سمك ومأكولات بحرية مجمدة فقط — معبأة بالتفريغ للمجمد.',
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
label: 'مجموعة مختارة',
|
||||
title: 'منتجات مميزة',
|
||||
subtitle: 'أشهر قطعنا، محبوبة لدى العائلات في جميع أنحاء المدينة.',
|
||||
viewAll: 'عرض الكل',
|
||||
},
|
||||
howItWorks: {
|
||||
label: 'الطلب',
|
||||
title: 'مدى سهولة الطلب',
|
||||
step1Title: 'تواصل معنا',
|
||||
step1Desc:
|
||||
'أرسل لنا رسالة واتساب بما تريده — نرد بسرعة.',
|
||||
step2Title: 'نؤكد طلبك',
|
||||
step2Desc:
|
||||
'نؤكد طلبك ونعطيك السعر ونخبرك متى يكون جاهزاً.',
|
||||
step3Title: 'استلام أو توصيل',
|
||||
step3Desc:
|
||||
'استلم من المتجر في Tingvallavägen 11 أو اختر التوصيل المنزلي.',
|
||||
step: 'الخطوة {n}',
|
||||
whatsapp: 'اطلب عبر واتساب',
|
||||
},
|
||||
offers: {
|
||||
label: 'الطلب',
|
||||
title: 'عروض أسبوعية',
|
||||
subtitle:
|
||||
'نحدّث العروض بانتظام. تابعنا على وسائل التواصل الاجتماعي لأحدث الأسعار.',
|
||||
disclaimer: 'السعر ساري حتى نفاد الكمية',
|
||||
was: 'كان',
|
||||
now: 'الآن',
|
||||
order: 'اطلب',
|
||||
viewProduct: 'عرض المنتج',
|
||||
badge: {
|
||||
fresh: 'طازج',
|
||||
halal: 'حلال',
|
||||
},
|
||||
items: {
|
||||
chickenWings: { name: 'أجنحة دجاج طازجة PL' },
|
||||
lambSteak: { name: 'لحم خروف مشوي طازج أيرلندا' },
|
||||
beefMince: { name: 'لحم بقر مفروم 5% دهن IRL' },
|
||||
},
|
||||
},
|
||||
social: {
|
||||
label: 'تابعنا',
|
||||
title: 'تابعنا',
|
||||
subtitle:
|
||||
'أكثر من 1,100 متابع على فيسبوك · 249 منشوراً · تحديثات يومية',
|
||||
facebook: 'فيسبوك',
|
||||
instagram: 'إنستغرام',
|
||||
whatsapp: 'واتساب',
|
||||
},
|
||||
contact: {
|
||||
label: 'اتصل بنا',
|
||||
title: 'زرنا',
|
||||
addressLabel: 'العنوان',
|
||||
phoneLabel: 'الهاتف',
|
||||
hoursLabel: 'ساعات العمل',
|
||||
hoursValue: 'كل يوم: {hours}',
|
||||
writeUs: 'راسلنا',
|
||||
callUs: 'اتصل بنا',
|
||||
openMaps: 'افتح في خرائط جوجل',
|
||||
learnMore: 'تفاصيل الاتصال',
|
||||
},
|
||||
cta: {
|
||||
title: 'هل أنت مستعد للحوم حلال فاخرة؟',
|
||||
subtitle:
|
||||
'اطلب اليوم واستمتع بفرق اللحوم الحلال الطازجة المُحضّرة حسب رغبتك والمُوصَلة إلى باب منزلك.',
|
||||
button: 'تصفح مجموعتنا',
|
||||
whatsapp: 'اطلب عبر واتساب',
|
||||
},
|
||||
shop: {
|
||||
title: 'تسوق جميع المنتجات',
|
||||
subtitle: 'لحوم حلال فاخرة، مُحضّرة حسب تفضيلاتك',
|
||||
noProducts: 'لم يُعثر على منتجات',
|
||||
noProductsHint: 'جرّب تعديل الفلاتر أو كلمة البحث',
|
||||
filters: 'الفلاتر',
|
||||
productsFound: 'تم العثور على {count} منتجاً',
|
||||
search: 'بحث',
|
||||
searchPlaceholder: 'البحث عن المنتجات...',
|
||||
category: 'الفئة',
|
||||
sortBy: 'ترتيب حسب',
|
||||
all: 'الكل',
|
||||
sortFeatured: 'مميز',
|
||||
sortPriceAsc: 'السعر: من الأقل إلى الأعلى',
|
||||
sortPriceDesc: 'السعر: من الأعلى إلى الأقل',
|
||||
sortName: 'الاسم أ–ي',
|
||||
},
|
||||
product: {
|
||||
backToShop: 'العودة إلى المتجر',
|
||||
inStock: 'متوفر',
|
||||
outOfStock: 'غير متوفر',
|
||||
halalTrust: 'حلال معتمد 100% · طازج يومياً · جودة فاخرة',
|
||||
yourSelection: 'اختيارك',
|
||||
aboutProduct: 'عن هذا المنتج',
|
||||
addToCart: 'أضف إلى السلة',
|
||||
addedToCart: 'تمت الإضافة إلى السلة',
|
||||
decreaseQty: 'تقليل الكمية',
|
||||
increaseQty: 'زيادة الكمية',
|
||||
removeWishlist: 'إزالة من قائمة الأمنيات',
|
||||
addWishlist: 'إضافة إلى قائمة الأمنيات',
|
||||
viewProduct: 'عرض {name}',
|
||||
pieces: '{count} قطعة',
|
||||
standardCut: 'تقطيع قياسي',
|
||||
howManyCuts: 'كم عدد القطع التي تريدها؟',
|
||||
howManyCutsHint: 'اختر عدد القطع لطلبك',
|
||||
cutsAndStyle: '{cuts} قطعة · {style}',
|
||||
selectCutting: 'اختر أسلوب التقطيع',
|
||||
selectCuttingHint: 'جزارونا سيُحضّرون {category} بالضبط حسب التقطيع المفضل لديك',
|
||||
fishNote:
|
||||
'نبيع السمك المجمد فقط. كل السمك مجمد عند المصدر، معبأ احترافياً ويُباع مجمداً — احفظه في المجمد حتى وقت الطبخ.',
|
||||
},
|
||||
cutting: {
|
||||
nihari: 'تقطيع نهاري',
|
||||
karahi: 'تقطيع كراهي',
|
||||
qeema: 'قيمة (مفروم)',
|
||||
boneless: 'بدون عظم',
|
||||
steak: 'تقطيع ستيك',
|
||||
},
|
||||
priceUnit: {
|
||||
perBird: 'لكل دجاجة',
|
||||
perPack: 'لكل عبوة',
|
||||
perKg: 'لكل كيلوغرام',
|
||||
},
|
||||
badges: {
|
||||
bestseller: 'الأكثر مبيعاً',
|
||||
chefsPick: 'اختيار الشيف',
|
||||
premium: 'فاخر',
|
||||
popular: 'شائع',
|
||||
freshCatch: 'صيد طازج',
|
||||
frozen: 'مجمد',
|
||||
},
|
||||
products: {
|
||||
'chicken-whole': {
|
||||
name: 'دجاجة كاملة',
|
||||
description: 'دجاج حلال كامل طازج من المزرعة، مثالي للشواء أو الكاري.',
|
||||
longDescription:
|
||||
'دجاجاتنا الكاملة من مزارع حلال معتمدة وتُوصَل بأعلى درجات الطزاجة. كل دجاجة تُختار يدوياً للجودة، بلحم طري ومعالجة نظيفة. اختر عدد القطع المفضل لديك وسنُحضّرها بالضبط كما تحتاج.',
|
||||
},
|
||||
'chicken-breast': {
|
||||
name: 'صدر الدجاج',
|
||||
description: 'صدر دجاج خالي من العظم وقليل الدهن — مثالي للشواء والوجبات الصحية.',
|
||||
longDescription:
|
||||
'صدر دجاج فاخر بدون عظم، مُشذّب وجاهز للطبخ. مثالي للكباب والمقليات وعشاءات الأسبوع الصحية. اختر عدد القطع واستمتع بجودة ثابتة في كل مرة.',
|
||||
},
|
||||
'chicken-thighs': {
|
||||
name: 'أفخاذ الدجاج',
|
||||
description: 'أفخاذ دجاج حلال عصيرة بنكهة غنية للكاري والشواء.',
|
||||
longDescription:
|
||||
'أفخاذ دجاجنا معروفة بعصارتها وعمق نكهتها. سواء كنت تُحضّر كراهي تقليدية أو شواء نهاية الأسبوع، هذه الأفخاذ تلبي توقعاتك دائماً.',
|
||||
},
|
||||
'chicken-wings': {
|
||||
name: 'أجنحة الدجاج',
|
||||
description: 'أجنحة دجاج حلال جاهزة للحفلات — للقلي أو الخبز أو الشواء.',
|
||||
longDescription:
|
||||
'أجنحة دجاج مقرمشة ولذيذة، مُحضّرة حلال وتُوصَل طازجة. مفضلة لدى الجميع في ليالي المباريات والتجمعات العائلية.',
|
||||
},
|
||||
'beef-nihari': {
|
||||
name: 'لحم بقر للنهاري',
|
||||
description: 'قطع لحم بقر جاهزة للطبخ البطيء، مثالية للنهاري التقليدي.',
|
||||
longDescription:
|
||||
'قطع لحم بقر مختارة خصيصاً للنهاري المطهو ببطء. غنية بالكولاجين والنكهة، تتفتت بجمال خلال ساعات من الغليان لتجربة أصيلة تذوب في الفم.',
|
||||
},
|
||||
'beef-steak': {
|
||||
name: 'ستيك لحم بقر فاخر',
|
||||
description: 'قطع ستيك حلال بجودة المطاعم للحصول على قشرة مثالية.',
|
||||
longDescription:
|
||||
'ستيكات لحم بقر فاخرة مقطوعة يدوياً من أفضل مصادر الحلال. متزينة وطرية وجاهزة للشواء أو المقلاة الحديدية. اختر أسلوب التقطيع المفضل لديك.',
|
||||
},
|
||||
'beef-mince': {
|
||||
name: 'لحم بقر مفروم',
|
||||
description: 'لحم بقر حلال مفروم طازج للكباب والبرغر والقيمة.',
|
||||
longDescription:
|
||||
'لحم بقر حلال مفروم ناعماً بنسبة دهن مثالية لكباب عصير وقيمة لذيذة وبرغر منزلي. يُطحن طازجاً يومياً.',
|
||||
},
|
||||
'beef-boneless': {
|
||||
name: 'مكعبات لحم بقر بدون عظم',
|
||||
description: 'مكعبات لحم بقر متعددة الاستخدامات للكراهي والبرياني واليخنات.',
|
||||
longDescription:
|
||||
'مكعبات لحم بقر موحدة بدون عظم مقطوعة بإتقان للأطباق السريعة. مثالية للكراهي والبلاو والمقليات حيث يهم التوحيد في الحجم.',
|
||||
},
|
||||
'lamb-shoulder': {
|
||||
name: 'كتف الخروف',
|
||||
description: 'كتف خروف غني بالنكهة للشواء البطيء والكاري.',
|
||||
longDescription:
|
||||
'كتف خروف حلال فاخر بتزيين جميل. مثالي للولائم المشوية ببطء والكاري الدسم والوجبات العائلية التقليدية. مُحضّر حسب التقطيع المفضل لديك.',
|
||||
},
|
||||
'lamb-leg': {
|
||||
name: 'فخذ الخروف',
|
||||
description: 'فخذ خروف طري للشواء والمناسبات الخاصة.',
|
||||
longDescription:
|
||||
'فخذ خروف كامل أو مقطّع من مصادر حلال معتمدة. قطعة رئيسية لاحتفالات العيد وحفلات العشاء وشواء الأحد.',
|
||||
},
|
||||
'lamb-chops': {
|
||||
name: 'ضلوع الخروف',
|
||||
description: 'ضلوع خروف فاخرة للشواء وتناول الطعام الراقي في المنزل.',
|
||||
longDescription:
|
||||
'ضلوع خروف حلال سميكة بطبقة دهن مثالية للشواء. جودة مطاعم، تُوصَل إلى مطبخك.',
|
||||
},
|
||||
'lamb-mince': {
|
||||
name: 'لحم خروف مفروم',
|
||||
description: 'لحم خروف حلال مفروم طازج للكباب والسمبوسة والقيمة.',
|
||||
longDescription:
|
||||
'لحم خروف مفروم ناعماً بنكهة غنية. أساسي لكباب السيخ وقيمة الخروف والباراثا المحشوة.',
|
||||
},
|
||||
'fish-salmon': {
|
||||
name: 'فيليه سلمون أطلسي مجمد',
|
||||
description: 'فيليه سلمون مجمد، معبأ بالتفريغ — أذِب واقلِ في المقلاة في المنزل.',
|
||||
longDescription:
|
||||
'فيليه سلمون أطلسي مجمد فاخر، غني بأوميغا 3. مجمد عند المصدر، منظف ومقسّم ومُغلّف بالتفريغ. احفظه مجمداً حتى وقت الطبخ.',
|
||||
},
|
||||
'fish-rohu': {
|
||||
name: 'سمك روهو مجمد',
|
||||
description: 'روهو كامل مجمد — مفضل في جنوب آسيا، يُباع مجمداً فقط.',
|
||||
longDescription:
|
||||
'سمك روهو مجمد، أساسي في مطبخ جنوب آسيا. مجمد عند المصدر ومعبأ للمجمد. أذِب قبل تحضير كاري السمك والوصفات التقليدية.',
|
||||
},
|
||||
'fish-prawns': {
|
||||
name: 'جمبري جامبو مجمد',
|
||||
description: 'جمبري جامبو مجمد للكاري والبرياني والشواء بعد الذوبان.',
|
||||
longDescription:
|
||||
'جمبري جامبو مجمد فاخر، يُباع مجمداً فقط. لحم حلو وصلب — أذِب تماماً قبل الطبخ في الكاري وأطباق التندوري.',
|
||||
},
|
||||
'fish-basa': {
|
||||
name: 'فيليه باسا مجمد',
|
||||
description: 'فيليه باسا مجمد خفيف — سهل الذوبان والطبخ لجميع أفراد العائلة.',
|
||||
longDescription:
|
||||
'فيليه باسا مجمد بدون عظم بنكهة خفيفة. يُباع مجمداً فقط — أذِب قبل الخبز والكاري الخفيف أو تاكو السمك.',
|
||||
},
|
||||
},
|
||||
cart: {
|
||||
title: 'سلة التسوق',
|
||||
itemsCount: '{count} منتج في سلتك',
|
||||
empty: 'سلتك فارغة',
|
||||
emptyHint: 'تصفح مجموعتنا الحلال الفاخرة وأضف منتجات إلى سلتك.',
|
||||
startShopping: 'ابدأ التسوق',
|
||||
customization: 'التخصيص:',
|
||||
orderSummary: 'ملخص الطلب',
|
||||
subtotal: 'المجموع الفرعي',
|
||||
delivery: 'التوصيل',
|
||||
free: 'مجاني',
|
||||
freeDeliveryHint: 'توصيل مجاني للطلبات فوق 500 كرونة',
|
||||
total: 'الإجمالي',
|
||||
proceedCheckout: 'متابعة الدفع',
|
||||
continueShopping: 'متابعة التسوق',
|
||||
removeItem: 'إزالة المنتج',
|
||||
},
|
||||
checkout: {
|
||||
title: 'دفع آمن',
|
||||
backToCart: 'العودة إلى السلة',
|
||||
noItems: 'لا توجد منتجات للدفع',
|
||||
goToShop: 'الذهاب إلى المتجر',
|
||||
orderConfirmed: 'تم تأكيد الطلب!',
|
||||
thankYou: 'شكراً لطلبك. لحومك الحلال الفاخرة قيد التحضير.',
|
||||
orderId: 'رقم الطلب: {id}',
|
||||
viewOrders: 'عرض الطلبات',
|
||||
haveAccount: 'لديك حساب؟',
|
||||
signIn: 'تسجيل الدخول',
|
||||
fasterCheckout: 'لإتمام دفع أسرع.',
|
||||
deliveryDetails: 'تفاصيل التوصيل',
|
||||
fullName: 'الاسم الكامل',
|
||||
email: 'البريد الإلكتروني',
|
||||
phone: 'الهاتف',
|
||||
street: 'عنوان الشارع',
|
||||
city: 'المدينة',
|
||||
state: 'المنطقة',
|
||||
zip: 'الرمز البريدي',
|
||||
payment: 'الدفع',
|
||||
creditCard: 'بطاقة ائتمان',
|
||||
cashOnDelivery: 'الدفع عند الاستلام',
|
||||
cardNumber: 'رقم البطاقة',
|
||||
expiry: 'تاريخ الانتهاء',
|
||||
cvv: 'رمز الأمان',
|
||||
qty: 'الكمية: {count}',
|
||||
processing: 'جارٍ المعالجة...',
|
||||
pay: 'ادفع {amount}',
|
||||
secure: 'تشفير SSL آمن 256 بت',
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: 'مرحباً بعودتك',
|
||||
createAccount: 'إنشاء حساب',
|
||||
joinTagline: 'انضم إلى {name} لتجربة تسوق فاخرة',
|
||||
signInTagline: 'سجّل الدخول إلى حسابك في {name}',
|
||||
fullName: 'الاسم الكامل',
|
||||
email: 'البريد الإلكتروني',
|
||||
password: 'كلمة المرور',
|
||||
phone: 'الهاتف',
|
||||
street: 'عنوان الشارع',
|
||||
city: 'المدينة',
|
||||
state: 'المنطقة',
|
||||
zip: 'الرمز البريدي',
|
||||
signIn: 'تسجيل الدخول',
|
||||
register: 'إنشاء حساب',
|
||||
hasAccount: 'لديك حساب بالفعل؟ سجّل الدخول',
|
||||
noAccount: 'ليس لديك حساب؟ سجّل الآن',
|
||||
invalidCredentials: 'بريد إلكتروني أو كلمة مرور غير صحيحة. جرّب {email} / demo123',
|
||||
demo: 'تجريبي: {email} / demo123',
|
||||
},
|
||||
account: {
|
||||
title: 'حسابي',
|
||||
welcome: 'مرحباً بعودتك، {name}',
|
||||
memberSince: 'عضو منذ {date}',
|
||||
myWishlist: 'قائمة أمنياتي',
|
||||
signOut: 'تسجيل الخروج',
|
||||
orderHistory: 'سجل الطلبات',
|
||||
noOrders: 'لا توجد طلبات بعد',
|
||||
startShopping: 'ابدأ التسوق',
|
||||
},
|
||||
wishlist: {
|
||||
title: 'قائمة أمنياتي',
|
||||
saved: '{count} منتج محفوظ',
|
||||
empty: 'قائمة أمنياتك فارغة',
|
||||
emptyHint: 'احفظ منتجاتك المفضلة لشرائها لاحقاً.',
|
||||
browse: 'تصفح المنتجات',
|
||||
},
|
||||
about: {
|
||||
title: 'عن {name}',
|
||||
subtitle:
|
||||
'نُقدّم لحوماً حلال فاخرة 100% إلى مائدتك — طازجة ومُخصّصة ومُوصَلة بعناية.',
|
||||
ourStory: 'قصتنا',
|
||||
storyP1:
|
||||
'تأسس {name} بمهمة بسيطة: جعل اللحوم الحلال الفاخرة في متناول كل عائلة، دون المساومة على الجودة أو الطزاجة أو الالتزام الديني. ندرك أنه بالنسبة للعديد من الأسر، القطعة المناسبة المُحضّرة بالطريقة الصحيحة ليست رفاهية — بل ضرورة.',
|
||||
storyP2:
|
||||
'من اختيار عدد قطع الدجاج إلى تحديد تقطيع النهاري أو الكراهي للحم البقر والخروف، نضع التخصيص في صميم كل طلب. جزارونا الخبراء يُحضّرون كل طلب يدوياً، وتوصيلنا المُتحكّم بدرجة الحرارة يضمن وصول لحومك طازجة كما يوم تقطيعها.',
|
||||
halalTitle: 'شهادة الحلال',
|
||||
halalDesc:
|
||||
'كل منتج في {name} من موردين حلال معتمدين. سلسلة التوريد لدينا قابلة للتتبع بالكامل، ونلتزم بصرامة بمعايير الذبح والمعالجة الحلال. نعمل حصرياً مع مزارع ومصانع تشاركنا التزامنا بإنتاج لحوم أخلاقي ومتوافق دينياً.',
|
||||
freshDaily: 'طازج يومياً',
|
||||
freshDailyDesc: 'يُورد كل صباح من مزارع موثوقة',
|
||||
premiumQuality: 'جودة فاخرة',
|
||||
premiumQualityDesc: 'قطع مختارة يدوياً على يد جزارين خبراء',
|
||||
fastDelivery: 'توصيل سريع',
|
||||
fastDeliveryDesc: 'توصيل في نفس اليوم مع التحكم بدرجة الحرارة',
|
||||
deliveryTitle: 'معلومات التوصيل',
|
||||
delivery1: 'نوصّل ضمن دائرة نصف قطرها 25 ميلاً من منشأة المعالجة لدينا.',
|
||||
delivery2: 'الطلبات قبل الساعة 2 مساءً مؤهلة للتوصيل في نفس اليوم.',
|
||||
delivery3: 'توصيل مجاني للطلبات فوق 500 كرونة. رسوم التوصيل القياسية: 49 كرونة.',
|
||||
delivery4: 'جميع المنتجات مُغلّفة بالتفريغ وتُنقل في عبوات معزولة.',
|
||||
contactTitle: 'اتصل بنا',
|
||||
address: 'Tingvallavägen 11, 195 31 Märsta',
|
||||
privacyTitle: 'سياسة الخصوصية',
|
||||
privacyText:
|
||||
'نجمع فقط المعلومات اللازمة لمعالجة طلباتك وتحسين خدمتنا — الاسم وبيانات الاتصال وعنوان التوصيل. لا نبيع بياناتك لأطراف ثالثة. تُعالج بيانات الدفع بأمان عبر شركاء الدفع لدينا.',
|
||||
termsTitle: 'شروط الخدمة',
|
||||
termsText:
|
||||
'جميع الأسعار بالكرونة السويدية وقد تتغير دون إشعار. الطلبات خاضعة للتوفر. شهادة الحلال تنطبق على جميع منتجات اللحوم. أوقات التوصيل تقديرية وقد تختلف خلال فترات الذروة.',
|
||||
},
|
||||
footer: {
|
||||
tagline:
|
||||
'توصيل لحوم حلال فاخرة 100%. قطع طازجة مُخصّصة تُوصَل إلى باب منزلك بجودة لا تُساوم.',
|
||||
shop: 'المتجر',
|
||||
company: 'الشركة',
|
||||
contact: 'اتصل بنا',
|
||||
rights: 'جميع الحقوق محفوظة.',
|
||||
phone: '072-585 50 50',
|
||||
hours: 'مفتوح كل يوم {hours}',
|
||||
},
|
||||
notFound: {
|
||||
title: '404',
|
||||
message: 'الصفحة غير موجودة',
|
||||
goHome: 'العودة إلى الرئيسية',
|
||||
},
|
||||
orderStatus: {
|
||||
pending: 'قيد الانتظار',
|
||||
confirmed: 'مؤكد',
|
||||
preparing: 'قيد التحضير',
|
||||
'out-for-delivery': 'في الطريق للتوصيل',
|
||||
delivered: 'تم التوصيل',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,466 @@
|
||||
import { TranslationDict } from '../types';
|
||||
|
||||
export const en: TranslationDict = {
|
||||
site: {
|
||||
name: 'Kött Gård',
|
||||
tagline: 'Premium Halal',
|
||||
description:
|
||||
'Premium 100% Halal meat delivery. Fresh chicken, beef, and lamb; frozen fish and seafood only — customized to your preference and delivered to your door.',
|
||||
metaTitle: 'Premium Halal Meat Delivery',
|
||||
initials: 'KG',
|
||||
email: 'hello@kottgard.se',
|
||||
demoEmail: 'demo@kottgard.se',
|
||||
},
|
||||
nav: {
|
||||
shop: 'Shop',
|
||||
chicken: 'Chicken',
|
||||
beef: 'Beef',
|
||||
lamb: 'Lamb',
|
||||
fish: 'Frozen Fish',
|
||||
about: 'About Us',
|
||||
halalCert: 'Halal Certification',
|
||||
delivery: 'Delivery Info',
|
||||
contact: 'Contact',
|
||||
myAccount: 'My Account',
|
||||
orderHistory: 'Order History',
|
||||
wishlist: 'Wishlist',
|
||||
cart: 'Cart',
|
||||
privacy: 'Privacy Policy',
|
||||
terms: 'Terms of Service',
|
||||
searchProducts: 'Search products',
|
||||
toggleMenu: 'Toggle menu',
|
||||
account: 'Account',
|
||||
language: 'Language',
|
||||
},
|
||||
languageBanner: {
|
||||
choose: 'Choose your language',
|
||||
},
|
||||
hero: {
|
||||
badge: '100% Halal Certified',
|
||||
taglineShort: 'Naturally Pure',
|
||||
title: 'Premium Halal Meat',
|
||||
titleHighlight: '',
|
||||
titleEnd: '',
|
||||
subtitleShort: 'Fresh. Quality. Reliable.',
|
||||
subtitle:
|
||||
'Halal certified · Fresh daily · Home delivery · Open every day',
|
||||
hours: 'Open every day {hours}',
|
||||
location: 'Tingvallavägen 11, Märsta',
|
||||
shopNow: 'Browse Our Selection',
|
||||
browseChicken: 'Browse Chicken',
|
||||
whatsapp: 'Order via WhatsApp',
|
||||
},
|
||||
aboutPreview: {
|
||||
label: 'About Us',
|
||||
title: "Märsta's Finest Butcher Shop",
|
||||
p1: 'Kött Gård is more than a butcher shop — we are a promise of quality. All our meat is 100% Halal certified and delivered fresh every day.',
|
||||
p2: 'We source lamb from Ireland and New Zealand, chicken and beef from reputable producers, and help you find the right cut for dinner, celebrations, or Sunday roast.',
|
||||
p3: 'Visit us at Tingvallavägen, tell us what you are looking for — we cut and pack to your specifications.',
|
||||
statHalal: '100%',
|
||||
statHalalLabel: 'Halal certified',
|
||||
statDays: '7 days',
|
||||
statDaysLabel: 'Open weekly',
|
||||
statDelivery: 'Daily',
|
||||
statDeliveryLabel: 'Delivery',
|
||||
statFresh: 'Fresh',
|
||||
statFreshLabel: 'Every day',
|
||||
readMore: 'Read more about us',
|
||||
imageAlt: 'Fresh meat cuts on a cutting board from Kött Gård',
|
||||
},
|
||||
trust: {
|
||||
halal: '100% Halal',
|
||||
halalDesc: 'Certified halal sourcing with full traceability and compliance.',
|
||||
fresh: 'Fresh Daily',
|
||||
freshDesc: 'Sourced fresh every morning and delivered at peak quality.',
|
||||
premium: 'Premium Quality',
|
||||
premiumDesc: 'Hand-selected cuts from trusted farms, prepared by expert butchers.',
|
||||
},
|
||||
categories: {
|
||||
title: 'Our Selection',
|
||||
subtitle: 'Hand-picked meat — every day. Fresh delivery. Halal. Cut to order.',
|
||||
shop: 'Shop {name}',
|
||||
chicken: {
|
||||
name: 'Chicken',
|
||||
description: 'Breast fillet, wings, drumsticks and whole chicken. Fresh every morning.',
|
||||
},
|
||||
beef: {
|
||||
name: 'Beef & Veal',
|
||||
description: 'Mince, bone marrow and premium cuts. High marbling, consistent quality.',
|
||||
},
|
||||
lamb: {
|
||||
name: 'Lamb',
|
||||
description: 'Shoulder, neck, roast and rack. From Ireland and New Zealand.',
|
||||
},
|
||||
fish: {
|
||||
name: 'Frozen Fish',
|
||||
description: 'Frozen fish and seafood only — vacuum-packed for your freezer.',
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
label: 'Curated Selection',
|
||||
title: 'Featured Products',
|
||||
subtitle: 'Our most popular cuts, loved by families across the city.',
|
||||
viewAll: 'View All',
|
||||
},
|
||||
howItWorks: {
|
||||
label: 'Order',
|
||||
title: 'How easy it is to order',
|
||||
step1Title: 'Contact us',
|
||||
step1Desc:
|
||||
'Send us a WhatsApp message with what you want — we reply quickly.',
|
||||
step2Title: 'We confirm',
|
||||
step2Desc:
|
||||
'We confirm your order, give you the price, and tell you when it is ready.',
|
||||
step3Title: 'Pick up or delivery',
|
||||
step3Desc:
|
||||
'Pick up in store at Tingvallavägen 11 or choose home delivery.',
|
||||
step: 'Step {n}',
|
||||
whatsapp: 'Order on WhatsApp',
|
||||
},
|
||||
offers: {
|
||||
label: 'Order',
|
||||
title: 'Weekly offers',
|
||||
subtitle:
|
||||
'We update regularly with fresh deals. Follow us on social media for the latest prices.',
|
||||
disclaimer: 'Price valid while supplies last',
|
||||
was: 'Was',
|
||||
now: 'NOW',
|
||||
order: 'Order',
|
||||
viewProduct: 'View product',
|
||||
badge: {
|
||||
fresh: 'FRESH',
|
||||
halal: 'HALAL',
|
||||
},
|
||||
items: {
|
||||
chickenWings: { name: 'Fresh chicken wings PL' },
|
||||
lambSteak: { name: 'Fresh lamb roast Ireland' },
|
||||
beefMince: { name: 'Beef mince 5% fat IRL' },
|
||||
},
|
||||
},
|
||||
social: {
|
||||
label: 'Follow us',
|
||||
title: 'Follow us',
|
||||
subtitle:
|
||||
'1,100+ followers on Facebook · 249 posts · Daily updates',
|
||||
facebook: 'Facebook',
|
||||
instagram: 'Instagram',
|
||||
whatsapp: 'WhatsApp',
|
||||
},
|
||||
contact: {
|
||||
label: 'Contact',
|
||||
title: 'Visit us',
|
||||
addressLabel: 'Address',
|
||||
phoneLabel: 'Phone',
|
||||
hoursLabel: 'Opening hours',
|
||||
hoursValue: 'Every day: {hours}',
|
||||
writeUs: 'Message us',
|
||||
callUs: 'Call us',
|
||||
openMaps: 'Open in Google Maps',
|
||||
learnMore: 'Contact details',
|
||||
},
|
||||
cta: {
|
||||
title: 'Ready for Premium Halal Meat?',
|
||||
subtitle:
|
||||
'Order today and experience the difference of truly fresh, customized halal meat delivered to your doorstep.',
|
||||
button: 'Browse Our Selection',
|
||||
whatsapp: 'Order on WhatsApp',
|
||||
},
|
||||
shop: {
|
||||
title: 'Shop All Products',
|
||||
subtitle: 'Premium halal meat, customized to your preference',
|
||||
noProducts: 'No products found',
|
||||
noProductsHint: 'Try adjusting your filters or search query',
|
||||
filters: 'Filters',
|
||||
productsFound: '{count} products found',
|
||||
search: 'Search',
|
||||
searchPlaceholder: 'Search products...',
|
||||
category: 'Category',
|
||||
sortBy: 'Sort By',
|
||||
all: 'All',
|
||||
sortFeatured: 'Featured',
|
||||
sortPriceAsc: 'Price: Low to High',
|
||||
sortPriceDesc: 'Price: High to Low',
|
||||
sortName: 'Name A–Z',
|
||||
},
|
||||
product: {
|
||||
backToShop: 'Back to Shop',
|
||||
inStock: 'In Stock',
|
||||
outOfStock: 'Out of Stock',
|
||||
halalTrust: '100% Halal certified · Fresh daily · Premium quality',
|
||||
yourSelection: 'Your selection',
|
||||
aboutProduct: 'About This Product',
|
||||
addToCart: 'Add to Cart',
|
||||
addedToCart: 'Added to Cart',
|
||||
decreaseQty: 'Decrease quantity',
|
||||
increaseQty: 'Increase quantity',
|
||||
removeWishlist: 'Remove from wishlist',
|
||||
addWishlist: 'Add to wishlist',
|
||||
viewProduct: 'View {name}',
|
||||
pieces: '{count} pieces',
|
||||
standardCut: 'Standard cut',
|
||||
howManyCuts: 'How Many Cuts Do You Want?',
|
||||
howManyCutsHint: 'Select the number of cuts for your order',
|
||||
cutsAndStyle: '{cuts} cuts · {style}',
|
||||
selectCutting: 'Select Cutting Style',
|
||||
selectCuttingHint: 'Our butchers will prepare your {category} exactly to your preferred cut',
|
||||
fishNote:
|
||||
'We sell frozen fish only. All fish is frozen at source, professionally packed, and sold frozen — store in your freezer until ready to cook.',
|
||||
},
|
||||
cutting: {
|
||||
nihari: 'Nihari cut',
|
||||
karahi: 'Karahi cut',
|
||||
qeema: 'Qeema (minced)',
|
||||
boneless: 'Boneless',
|
||||
steak: 'Steak cut',
|
||||
},
|
||||
priceUnit: {
|
||||
perBird: 'per bird',
|
||||
perPack: 'per pack',
|
||||
perKg: 'per kg',
|
||||
},
|
||||
badges: {
|
||||
bestseller: 'Bestseller',
|
||||
chefsPick: "Chef's Pick",
|
||||
premium: 'Premium',
|
||||
popular: 'Popular',
|
||||
freshCatch: 'Fresh Catch',
|
||||
frozen: 'Frozen',
|
||||
},
|
||||
products: {
|
||||
'chicken-whole': {
|
||||
name: 'Whole Chicken',
|
||||
description: 'Farm-fresh whole halal chicken, perfect for roasting or curry.',
|
||||
longDescription:
|
||||
'Our whole chickens are sourced from certified halal farms and delivered at peak freshness. Each bird is hand-selected for quality, with tender meat and clean processing. Choose your preferred piece count and we will prepare it exactly how you need.',
|
||||
},
|
||||
'chicken-breast': {
|
||||
name: 'Chicken Breast',
|
||||
description: 'Lean, boneless chicken breast — ideal for grilling and healthy meals.',
|
||||
longDescription:
|
||||
'Premium boneless chicken breast, trimmed and ready to cook. Perfect for kebabs, stir-fries, and healthy weeknight dinners. Select your piece count and enjoy consistent quality every time.',
|
||||
},
|
||||
'chicken-thighs': {
|
||||
name: 'Chicken Thighs',
|
||||
description: 'Juicy halal chicken thighs with rich flavor for curries and grills.',
|
||||
longDescription:
|
||||
'Our chicken thighs are known for their succulence and depth of flavor. Whether you are making a traditional karahi or a weekend BBQ, these thighs deliver every time.',
|
||||
},
|
||||
'chicken-wings': {
|
||||
name: 'Chicken Wings',
|
||||
description: 'Party-ready halal chicken wings for frying, baking, or grilling.',
|
||||
longDescription:
|
||||
'Crispy, flavorful chicken wings prepared halal and delivered fresh. A crowd favorite for game nights and family gatherings.',
|
||||
},
|
||||
'beef-nihari': {
|
||||
name: 'Beef for Nihari',
|
||||
description: 'Slow-cook ready beef cuts, perfect for traditional nihari.',
|
||||
longDescription:
|
||||
'Specially selected beef cuts ideal for slow-cooked nihari. Rich in collagen and flavor, these cuts break down beautifully over hours of simmering for an authentic, melt-in-your-mouth experience.',
|
||||
},
|
||||
'beef-steak': {
|
||||
name: 'Premium Beef Steak',
|
||||
description: 'Restaurant-quality halal steak cuts for the perfect sear.',
|
||||
longDescription:
|
||||
'Hand-cut premium beef steaks from the finest halal sources. Marbled, tender, and ready for your grill or cast-iron pan. Choose your preferred cutting style.',
|
||||
},
|
||||
'beef-mince': {
|
||||
name: 'Beef Mince',
|
||||
description: 'Fresh halal beef mince for kebabs, burgers, and qeema.',
|
||||
longDescription:
|
||||
'Finely ground halal beef mince with the perfect fat ratio for juicy kebabs, flavorful qeema, and homemade burgers. Ground fresh daily.',
|
||||
},
|
||||
'beef-boneless': {
|
||||
name: 'Boneless Beef Cubes',
|
||||
description: 'Versatile boneless beef cubes for karahi, biryani, and stews.',
|
||||
longDescription:
|
||||
'Uniform boneless beef cubes cut to perfection for quick-cooking dishes. Ideal for karahi, pulao, and stir-fries where consistent sizing matters.',
|
||||
},
|
||||
'lamb-shoulder': {
|
||||
name: 'Lamb Shoulder',
|
||||
description: 'Rich, flavorful lamb shoulder for slow roasts and curries.',
|
||||
longDescription:
|
||||
'Premium halal lamb shoulder with beautiful marbling. Perfect for slow-roasted feasts, hearty curries, and traditional family meals. Customized to your preferred cut.',
|
||||
},
|
||||
'lamb-leg': {
|
||||
name: 'Lamb Leg',
|
||||
description: 'Tender lamb leg for roasts, grills, and special occasions.',
|
||||
longDescription:
|
||||
'Whole or portioned lamb leg from certified halal sources. A centerpiece cut for Eid celebrations, dinner parties, and Sunday roasts.',
|
||||
},
|
||||
'lamb-chops': {
|
||||
name: 'Lamb Chops',
|
||||
description: 'Premium lamb chops for grilling and fine dining at home.',
|
||||
longDescription:
|
||||
'Thick-cut halal lamb chops with perfect fat caps for grilling. Restaurant quality, delivered to your kitchen.',
|
||||
},
|
||||
'lamb-mince': {
|
||||
name: 'Lamb Mince',
|
||||
description: 'Fresh halal lamb mince for kebabs, samosas, and qeema.',
|
||||
longDescription:
|
||||
'Finely ground lamb mince with rich flavor. Essential for seekh kebabs, lamb qeema, and stuffed parathas.',
|
||||
},
|
||||
'fish-salmon': {
|
||||
name: 'Frozen Atlantic Salmon Fillet',
|
||||
description: 'Frozen salmon fillets, vacuum-packed — thaw and pan-sear at home.',
|
||||
longDescription:
|
||||
'Premium frozen Atlantic salmon fillets, rich in omega-3. Frozen at source, cleaned, portioned, and vacuum-sealed. Keep frozen until you are ready to cook.',
|
||||
},
|
||||
'fish-rohu': {
|
||||
name: 'Frozen Rohu Fish',
|
||||
description: 'Frozen whole rohu — a South Asian favourite, sold frozen only.',
|
||||
longDescription:
|
||||
'Frozen rohu fish, a staple in South Asian cuisine. Frozen at source and packed for your freezer. Thaw before preparing fish curry, fried fish, or traditional recipes.',
|
||||
},
|
||||
'fish-prawns': {
|
||||
name: 'Frozen Jumbo Prawns',
|
||||
description: 'Frozen jumbo prawns for curries, biryanis, and grills after thawing.',
|
||||
longDescription:
|
||||
'Premium frozen jumbo prawns, packed and sold frozen only. Sweet, firm flesh — thaw fully before cooking in curries and tandoori dishes.',
|
||||
},
|
||||
'fish-basa': {
|
||||
name: 'Frozen Basa Fillet',
|
||||
description: 'Frozen mild basa fillets — easy to thaw and cook for the whole family.',
|
||||
longDescription:
|
||||
'Frozen boneless basa fillets with a mild, delicate flavour. Sold frozen only — thaw before baking, light curries, or fish tacos.',
|
||||
},
|
||||
},
|
||||
cart: {
|
||||
title: 'Your Cart',
|
||||
itemsCount: '{count} item(s) in your cart',
|
||||
empty: 'Your cart is empty',
|
||||
emptyHint: 'Browse our premium halal selection and add items to your cart.',
|
||||
startShopping: 'Start Shopping',
|
||||
customization: 'Customization:',
|
||||
orderSummary: 'Order Summary',
|
||||
subtotal: 'Subtotal',
|
||||
delivery: 'Delivery',
|
||||
free: 'Free',
|
||||
freeDeliveryHint: 'Free delivery on orders over 500 kr',
|
||||
total: 'Total',
|
||||
proceedCheckout: 'Proceed to Checkout',
|
||||
continueShopping: 'Continue Shopping',
|
||||
removeItem: 'Remove item',
|
||||
},
|
||||
checkout: {
|
||||
title: 'Secure Checkout',
|
||||
backToCart: 'Back to Cart',
|
||||
noItems: 'No items to checkout',
|
||||
goToShop: 'Go to Shop',
|
||||
orderConfirmed: 'Order Confirmed!',
|
||||
thankYou: 'Thank you for your order. Your premium halal meat is being prepared.',
|
||||
orderId: 'Order ID: {id}',
|
||||
viewOrders: 'View Orders',
|
||||
haveAccount: 'Have an account?',
|
||||
signIn: 'Sign in',
|
||||
fasterCheckout: 'for faster checkout.',
|
||||
deliveryDetails: 'Delivery Details',
|
||||
fullName: 'Full Name',
|
||||
email: 'Email',
|
||||
phone: 'Phone',
|
||||
street: 'Street Address',
|
||||
city: 'City',
|
||||
state: 'State',
|
||||
zip: 'ZIP Code',
|
||||
payment: 'Payment',
|
||||
creditCard: 'Credit Card',
|
||||
cashOnDelivery: 'Cash on Delivery',
|
||||
cardNumber: 'Card Number',
|
||||
expiry: 'Expiry',
|
||||
cvv: 'CVV',
|
||||
qty: 'Qty: {count}',
|
||||
processing: 'Processing...',
|
||||
pay: 'Pay {amount}',
|
||||
secure: 'Secure 256-bit SSL encryption',
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: 'Welcome Back',
|
||||
createAccount: 'Create Account',
|
||||
joinTagline: 'Join {name} for a premium shopping experience',
|
||||
signInTagline: 'Sign in to your {name} account',
|
||||
fullName: 'Full Name',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
phone: 'Phone',
|
||||
street: 'Street Address',
|
||||
city: 'City',
|
||||
state: 'State',
|
||||
zip: 'ZIP Code',
|
||||
signIn: 'Sign In',
|
||||
register: 'Create Account',
|
||||
hasAccount: 'Already have an account? Sign in',
|
||||
noAccount: "Don't have an account? Register",
|
||||
invalidCredentials: 'Invalid email or password. Try {email} / demo123',
|
||||
demo: 'Demo: {email} / demo123',
|
||||
},
|
||||
account: {
|
||||
title: 'My Account',
|
||||
welcome: 'Welcome back, {name}',
|
||||
memberSince: 'Member since {date}',
|
||||
myWishlist: 'My Wishlist',
|
||||
signOut: 'Sign Out',
|
||||
orderHistory: 'Order History',
|
||||
noOrders: 'No orders yet',
|
||||
startShopping: 'Start Shopping',
|
||||
},
|
||||
wishlist: {
|
||||
title: 'My Wishlist',
|
||||
saved: '{count} saved item(s)',
|
||||
empty: 'Your wishlist is empty',
|
||||
emptyHint: 'Save your favorite products to buy them later.',
|
||||
browse: 'Browse Products',
|
||||
},
|
||||
about: {
|
||||
title: 'About {name}',
|
||||
subtitle:
|
||||
'Bringing premium, 100% Halal meat to your table — fresh, customized, and delivered with care.',
|
||||
ourStory: 'Our Story',
|
||||
storyP1:
|
||||
'{name} was founded with a simple mission: make premium halal meat accessible to every family, without compromising on quality, freshness, or religious compliance. We understand that for many households, the right cut prepared the right way is not a luxury — it is essential.',
|
||||
storyP2:
|
||||
'From selecting piece counts for chicken to choosing Nihari or Karahi cuts for beef and lamb, we put customization at the heart of every order. Our expert butchers prepare each order by hand, and our temperature-controlled delivery ensures your meat arrives as fresh as the day it was cut.',
|
||||
halalTitle: 'Halal Certification',
|
||||
halalDesc:
|
||||
'Every product at {name} is sourced from certified halal suppliers. Our supply chain is fully traceable, and we maintain strict compliance with halal slaughter and processing standards. We work exclusively with farms and processors that share our commitment to ethical, religiously compliant meat production.',
|
||||
freshDaily: 'Fresh Daily',
|
||||
freshDailyDesc: 'Sourced every morning from trusted farms',
|
||||
premiumQuality: 'Premium Quality',
|
||||
premiumQualityDesc: 'Hand-selected cuts by expert butchers',
|
||||
fastDelivery: 'Fast Delivery',
|
||||
fastDeliveryDesc: 'Temperature-controlled same-day delivery',
|
||||
deliveryTitle: 'Delivery Information',
|
||||
delivery1: 'We deliver within a 25-mile radius of our processing facility.',
|
||||
delivery2: 'Orders placed before 2 PM are eligible for same-day delivery.',
|
||||
delivery3: 'Free delivery on orders over 500 kr. Standard delivery fee: 49 kr.',
|
||||
delivery4: 'All products are vacuum-sealed and transported in insulated packaging.',
|
||||
contactTitle: 'Contact Us',
|
||||
address: 'Tingvallavägen 11, 195 31 Märsta',
|
||||
privacyTitle: 'Privacy Policy',
|
||||
privacyText:
|
||||
'We collect only the information needed to process your orders and improve our service — name, contact details, and delivery address. We do not sell your data to third parties. Payment details are handled securely by our payment partners.',
|
||||
termsTitle: 'Terms of Service',
|
||||
termsText:
|
||||
'All prices are shown in SEK and may change without notice. Orders are subject to availability. Halal certification applies to all meat products listed. Delivery times are estimates and may vary during peak periods.',
|
||||
},
|
||||
footer: {
|
||||
tagline:
|
||||
'Premium 100% Halal meat delivery. Fresh, customized cuts delivered to your door with uncompromising quality.',
|
||||
shop: 'Shop',
|
||||
company: 'Company',
|
||||
contact: 'Contact',
|
||||
rights: 'All rights reserved.',
|
||||
phone: '072-585 50 50',
|
||||
hours: 'Open every day {hours}',
|
||||
},
|
||||
notFound: {
|
||||
title: '404',
|
||||
message: 'Page not found',
|
||||
goHome: 'Go Home',
|
||||
},
|
||||
orderStatus: {
|
||||
pending: 'pending',
|
||||
confirmed: 'confirmed',
|
||||
preparing: 'preparing',
|
||||
'out-for-delivery': 'out for delivery',
|
||||
delivered: 'delivered',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,466 @@
|
||||
import { TranslationDict } from '../types';
|
||||
|
||||
export const fa: TranslationDict = {
|
||||
site: {
|
||||
name: 'کوت گارد',
|
||||
tagline: 'حلال ممتاز',
|
||||
description:
|
||||
'تحویل گوشت حلال ۱۰۰٪ ممتاز. مرغ، گوشت گاو و گوسفند تازه؛ فقط ماهی و غذای دریایی منجمد — مطابق سلیقه شما آماده و تا در منزل تحویل داده میشود.',
|
||||
metaTitle: 'تحویل گوشت حلال ممتاز',
|
||||
initials: 'KG',
|
||||
email: 'hello@kottgard.se',
|
||||
demoEmail: 'demo@kottgard.se',
|
||||
},
|
||||
nav: {
|
||||
shop: 'فروشگاه',
|
||||
chicken: 'مرغ',
|
||||
beef: 'گوشت گاو',
|
||||
lamb: 'گوشت گوسفند',
|
||||
fish: 'ماهی منجمد',
|
||||
about: 'درباره ما',
|
||||
halalCert: 'گواهی حلال',
|
||||
delivery: 'اطلاعات تحویل',
|
||||
contact: 'تماس',
|
||||
myAccount: 'حساب من',
|
||||
orderHistory: 'تاریخچه سفارشها',
|
||||
wishlist: 'علاقهمندیها',
|
||||
cart: 'سبد خرید',
|
||||
privacy: 'سیاست حفظ حریم خصوصی',
|
||||
terms: 'شرایط استفاده',
|
||||
searchProducts: 'جستجوی محصولات',
|
||||
toggleMenu: 'باز و بسته کردن منو',
|
||||
account: 'حساب کاربری',
|
||||
language: 'زبان',
|
||||
},
|
||||
languageBanner: {
|
||||
choose: 'زبان خود را انتخاب کنید',
|
||||
},
|
||||
hero: {
|
||||
badge: '۱۰۰٪ حلال تأییدشده',
|
||||
taglineShort: 'طبیعی و خالص',
|
||||
title: 'گوشت حلال ممتاز',
|
||||
titleHighlight: '',
|
||||
titleEnd: '',
|
||||
subtitleShort: 'تازه. باکیفیت. قابل اعتماد.',
|
||||
subtitle:
|
||||
'حلال تأییدشده · روزانه تازه · تحویل درب منزل · هر روز باز',
|
||||
hours: 'هر روز باز {hours}',
|
||||
location: 'Tingvallavägen 11, Märsta',
|
||||
shopNow: 'مشاهده مجموعه ما',
|
||||
browseChicken: 'مشاهده مرغ',
|
||||
whatsapp: 'سفارش از طریق واتساپ',
|
||||
},
|
||||
aboutPreview: {
|
||||
label: 'درباره ما',
|
||||
title: 'بهترین قصابی مِرستا',
|
||||
p1: 'کوت گارد فراتر از یک قصابی است — ما تعهد به کیفیت هستیم. تمام گوشتهای ما ۱۰۰٪ حلال تأییدشده و هر روز تازه تحویل داده میشوند.',
|
||||
p2: 'گوشت گوسفند را از ایرلند و نیوزیلند، مرغ و گوشت گاو را از تولیدکنندگان معتبر تهیه میکنیم و به شما کمک میکنیم برش مناسب شام، جشن یا کباب آخر هفته را پیدا کنید.',
|
||||
p3: 'به ما در Tingvallavägen سر بزنید، بگویید دنبال چه هستید — ما مطابق خواسته شما برش میزنیم و بستهبندی میکنیم.',
|
||||
statHalal: '۱۰۰٪',
|
||||
statHalalLabel: 'حلال تأییدشده',
|
||||
statDays: '۷ روز',
|
||||
statDaysLabel: 'هفتگی باز',
|
||||
statDelivery: 'روزانه',
|
||||
statDeliveryLabel: 'تحویل',
|
||||
statFresh: 'تازه',
|
||||
statFreshLabel: 'هر روز',
|
||||
readMore: 'بیشتر درباره ما بخوانید',
|
||||
imageAlt: 'برشهای تازه گوشت روی تخته برش از کوت گارد',
|
||||
},
|
||||
trust: {
|
||||
halal: '۱۰۰٪ حلال',
|
||||
halalDesc: 'تأمین حلال گواهیشده با ردیابی کامل و رعایت استانداردها.',
|
||||
fresh: 'روزانه تازه',
|
||||
freshDesc: 'هر صبح تازه تهیه و در بهترین کیفیت تحویل داده میشود.',
|
||||
premium: 'کیفیت ممتاز',
|
||||
premiumDesc: 'برشهای دستچین از مزارع معتبر، آمادهشده توسط قصابان حرفهای.',
|
||||
},
|
||||
categories: {
|
||||
title: 'مجموعه ما',
|
||||
subtitle: 'گوشت دستچین — هر روز. تحویل تازه. حلال. برش سفارشی.',
|
||||
shop: 'خرید {name}',
|
||||
chicken: {
|
||||
name: 'مرغ',
|
||||
description: 'فیله سینه، بال، ران و مرغ کامل. هر صبح تازه.',
|
||||
},
|
||||
beef: {
|
||||
name: 'گوشت گاو و گوساله',
|
||||
description: 'چرخکرده، مغز استخوان و برشهای ممتاز. چربیبندی بالا، کیفیت یکنواخت.',
|
||||
},
|
||||
lamb: {
|
||||
name: 'گوشت گوسفند',
|
||||
description: 'شانه، گردن، کبابی و راسته. از ایرلند و نیوزیلند.',
|
||||
},
|
||||
fish: {
|
||||
name: 'ماهی منجمد',
|
||||
description: 'فقط ماهی و غذای دریایی منجمد — بستهبندی وکیوم برای فریزر.',
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
label: 'انتخاب ویژه',
|
||||
title: 'محصولات ویژه',
|
||||
subtitle: 'محبوبترین برشهای ما که خانوادهها در سراسر شهر دوستشان دارند.',
|
||||
viewAll: 'مشاهده همه',
|
||||
},
|
||||
howItWorks: {
|
||||
label: 'سفارش',
|
||||
title: 'سفارش دادن چقدر آسان است',
|
||||
step1Title: 'با ما تماس بگیرید',
|
||||
step1Desc:
|
||||
'از طریق واتساپ بگویید چه میخواهید — سریع پاسخ میدهیم.',
|
||||
step2Title: 'تأیید میکنیم',
|
||||
step2Desc:
|
||||
'سفارش شما را تأیید میکنیم، قیمت را اعلام میکنیم و زمان آماده شدن را میگوییم.',
|
||||
step3Title: 'تحویل حضوری یا درب منزل',
|
||||
step3Desc:
|
||||
'از فروشگاه در Tingvallavägen 11 تحویل بگیرید یا تحویل درب منزل را انتخاب کنید.',
|
||||
step: 'مرحله {n}',
|
||||
whatsapp: 'سفارش در واتساپ',
|
||||
},
|
||||
offers: {
|
||||
label: 'سفارش',
|
||||
title: 'پیشنهادهای هفتگی',
|
||||
subtitle:
|
||||
'بهطور منظم پیشنهادهای تازه اضافه میکنیم. برای آخرین قیمتها ما را در شبکههای اجتماعی دنبال کنید.',
|
||||
disclaimer: 'قیمت تا پایان موجودی معتبر است',
|
||||
was: 'قبلاً',
|
||||
now: 'اکنون',
|
||||
order: 'سفارش',
|
||||
viewProduct: 'مشاهده محصول',
|
||||
badge: {
|
||||
fresh: 'تازه',
|
||||
halal: 'حلال',
|
||||
},
|
||||
items: {
|
||||
chickenWings: { name: 'بال مرغ تازه PL' },
|
||||
lambSteak: { name: 'کبابی گوسفند تازه ایرلند' },
|
||||
beefMince: { name: 'گوشت چرخکرده گاو ۵٪ چربی IRL' },
|
||||
},
|
||||
},
|
||||
social: {
|
||||
label: 'ما را دنبال کنید',
|
||||
title: 'ما را دنبال کنید',
|
||||
subtitle:
|
||||
'بیش از ۱٬۱۰۰ دنبالکننده در فیسبوک · ۲۴۹ پست · بهروزرسانی روزانه',
|
||||
facebook: 'فیسبوک',
|
||||
instagram: 'اینستاگرام',
|
||||
whatsapp: 'واتساپ',
|
||||
},
|
||||
contact: {
|
||||
label: 'تماس',
|
||||
title: 'به ما سر بزنید',
|
||||
addressLabel: 'آدرس',
|
||||
phoneLabel: 'تلفن',
|
||||
hoursLabel: 'ساعات کاری',
|
||||
hoursValue: 'هر روز: {hours}',
|
||||
writeUs: 'پیام دهید',
|
||||
callUs: 'تماس بگیرید',
|
||||
openMaps: 'باز کردن در گوگل مپ',
|
||||
learnMore: 'جزئیات تماس',
|
||||
},
|
||||
cta: {
|
||||
title: 'آماده گوشت حلال ممتاز هستید؟',
|
||||
subtitle:
|
||||
'امروز سفارش دهید و تفاوت گوشت حلال واقعاً تازه و سفارشی تحویلشده درب منزل را تجربه کنید.',
|
||||
button: 'مشاهده مجموعه ما',
|
||||
whatsapp: 'سفارش در واتساپ',
|
||||
},
|
||||
shop: {
|
||||
title: 'همه محصولات',
|
||||
subtitle: 'گوشت حلال ممتاز، مطابق سلیقه شما',
|
||||
noProducts: 'محصولی یافت نشد',
|
||||
noProductsHint: 'فیلترها یا عبارت جستجو را تغییر دهید',
|
||||
filters: 'فیلترها',
|
||||
productsFound: '{count} محصول یافت شد',
|
||||
search: 'جستجو',
|
||||
searchPlaceholder: 'جستجوی محصولات...',
|
||||
category: 'دستهبندی',
|
||||
sortBy: 'مرتبسازی',
|
||||
all: 'همه',
|
||||
sortFeatured: 'ویژه',
|
||||
sortPriceAsc: 'قیمت: کم به زیاد',
|
||||
sortPriceDesc: 'قیمت: زیاد به کم',
|
||||
sortName: 'نام الف–ی',
|
||||
},
|
||||
product: {
|
||||
backToShop: 'بازگشت به فروشگاه',
|
||||
inStock: 'موجود',
|
||||
outOfStock: 'ناموجود',
|
||||
halalTrust: '۱۰۰٪ حلال تأییدشده · روزانه تازه · کیفیت ممتاز',
|
||||
yourSelection: 'انتخاب شما',
|
||||
aboutProduct: 'درباره این محصول',
|
||||
addToCart: 'افزودن به سبد',
|
||||
addedToCart: 'به سبد اضافه شد',
|
||||
decreaseQty: 'کاهش تعداد',
|
||||
increaseQty: 'افزایش تعداد',
|
||||
removeWishlist: 'حذف از علاقهمندیها',
|
||||
addWishlist: 'افزودن به علاقهمندیها',
|
||||
viewProduct: 'مشاهده {name}',
|
||||
pieces: '{count} تکه',
|
||||
standardCut: 'برش استاندارد',
|
||||
howManyCuts: 'چند برش میخواهید؟',
|
||||
howManyCutsHint: 'تعداد برشهای سفارش خود را انتخاب کنید',
|
||||
cutsAndStyle: '{cuts} برش · {style}',
|
||||
selectCutting: 'انتخاب نوع برش',
|
||||
selectCuttingHint: 'قصابان ما {category} شما را دقیقاً مطابق برش دلخواهتان آماده میکنند',
|
||||
fishNote:
|
||||
'ما فقط ماهی منجمد میفروشیم. همه ماهیها در مبدأ منجمد، بستهبندی حرفهای و بهصورت منجمد فروخته میشوند — تا زمان پخت در فریزر نگه دارید.',
|
||||
},
|
||||
cutting: {
|
||||
nihari: 'برش نیهاری',
|
||||
karahi: 'برش قراهی',
|
||||
qeema: 'چرخکرده (قیمه)',
|
||||
boneless: 'بدون استخوان',
|
||||
steak: 'برش استیک',
|
||||
},
|
||||
priceUnit: {
|
||||
perBird: 'هر مرغ',
|
||||
perPack: 'هر بسته',
|
||||
perKg: 'هر کیلو',
|
||||
},
|
||||
badges: {
|
||||
bestseller: 'پرفروش',
|
||||
chefsPick: 'انتخاب سرآشپز',
|
||||
premium: 'ممتاز',
|
||||
popular: 'محبوب',
|
||||
freshCatch: 'صید تازه',
|
||||
frozen: 'منجمد',
|
||||
},
|
||||
products: {
|
||||
'chicken-whole': {
|
||||
name: 'مرغ کامل',
|
||||
description: 'مرغ حلال تازه مزرعهای، مناسب کبابی یا خورشت.',
|
||||
longDescription:
|
||||
'مرغهای کامل ما از مزارع حلال گواهیشده تهیه و در بهترین تازگی تحویل داده میشوند. هر مرغ برای کیفیت دستچین شده، با گوشت لطیف و فرآوری تمیز. تعداد تکه دلخواه را انتخاب کنید و ما دقیقاً مطابق نیاز شما آماده میکنیم.',
|
||||
},
|
||||
'chicken-breast': {
|
||||
name: 'سینه مرغ',
|
||||
description: 'سینه مرغ لخم و بدون استخوان — مناسب کباب و غذاهای سالم.',
|
||||
longDescription:
|
||||
'سینه مرغ بدون استخوان ممتاز، تمیز و آماده پخت. عالی برای کباب، غذاهای تفتدهی و شامهای سالم هفته. تعداد تکه را انتخاب کنید و هر بار از کیفیت یکنواخت لذت ببرید.',
|
||||
},
|
||||
'chicken-thighs': {
|
||||
name: 'ران مرغ',
|
||||
description: 'ران مرغ حلال آبدار با طعم غنی برای خورشت و کباب.',
|
||||
longDescription:
|
||||
'رانهای مرغ ما به آبداری و عمق طعمشان معروفند. چه قراهی سنتی درست کنید چه کباب آخر هفته، این رانها همیشه عالی از آب درمیآیند.',
|
||||
},
|
||||
'chicken-wings': {
|
||||
name: 'بال مرغ',
|
||||
description: 'بال مرغ حلال آماده مهمانی برای سرخکردن، پخت یا کباب.',
|
||||
longDescription:
|
||||
'بال مرغ ترد و خوشطعم، حلال آماده و تازه تحویل داده میشود. محبوب شبهای تماشای فوتبال و دورهمیهای خانوادگی.',
|
||||
},
|
||||
'beef-nihari': {
|
||||
name: 'گوشت گاو برای نیهاری',
|
||||
description: 'برشهای گاو آماده پخت آهسته، مناسب نیهاری سنتی.',
|
||||
longDescription:
|
||||
'برشهای ویژه گاو برای نیهاری پخت آهسته. سرشار از کلاژن و طعم، این برشها در ساعتها جوشیدن به تجربهای اصیل و نرم در دهان تبدیل میشوند.',
|
||||
},
|
||||
'beef-steak': {
|
||||
name: 'استیک گاو ممتاز',
|
||||
description: 'برشهای استیک حلال با کیفیت رستورانی برای کباب عالی.',
|
||||
longDescription:
|
||||
'استیکهای گاو ممتاز دستبرش از بهترین منابع حلال. چربیبندیدار، لطیف و آماده برای کباب یا ماهیتابه چدنی. نوع برش دلخواه را انتخاب کنید.',
|
||||
},
|
||||
'beef-mince': {
|
||||
name: 'گوشت چرخکرده گاو',
|
||||
description: 'گوشت چرخکرده حلال تازه برای کباب، همبرگر و قیمه.',
|
||||
longDescription:
|
||||
'گوشت چرخکرده حلال با نسبت چربی مناسب برای کباب آبدار، قیمه خوشطعم و همبرگر خانگی. هر روز تازه چرخ میشود.',
|
||||
},
|
||||
'beef-boneless': {
|
||||
name: 'تکههای گاو بدون استخوان',
|
||||
description: 'تکههای گاو بدون استخوان همهکاره برای قراهی، بریانی و خورشت.',
|
||||
longDescription:
|
||||
'تکههای یکدست گاو بدون استخوان با برش دقیق برای غذاهای سریعپز. ایدهآل برای قراهی، پلو و غذاهای تفتدهی که اندازه یکنواخت مهم است.',
|
||||
},
|
||||
'lamb-shoulder': {
|
||||
name: 'شانه گوسفند',
|
||||
description: 'شانه گوسفند پرطعم برای کبابی آهسته و خورشت.',
|
||||
longDescription:
|
||||
'شانه گوسفند حلال ممتاز با چربیبندی زیبا. مناسب مهمانیهای کبابی آهسته، خورشتهای مقوی و غذاهای سنتی خانوادگی. مطابق برش دلخواه شما آماده میشود.',
|
||||
},
|
||||
'lamb-leg': {
|
||||
name: 'ران گوسفند',
|
||||
description: 'ران گوسفند لطیف برای کبابی، مناسبتها و مهمانیها.',
|
||||
longDescription:
|
||||
'ران گوسفند کامل یا تکهای از منابع حلال گواهیشده. برش اصلی جشنهای عید، مهمانیهای شام و کباب آخر هفته.',
|
||||
},
|
||||
'lamb-chops': {
|
||||
name: 'دنده گوسفند',
|
||||
description: 'دنده گوسفند ممتاز برای کباب و شام رستورانی در خانه.',
|
||||
longDescription:
|
||||
'دندههای ضخیم حلال گوسفند با لایه چربی مناسب برای کباب. کیفیت رستورانی، تحویلشده در آشپزخانه شما.',
|
||||
},
|
||||
'lamb-mince': {
|
||||
name: 'گوشت چرخکرده گوسفند',
|
||||
description: 'گوشت چرخکرده حلال تازه برای کباب، سمبوسه و قیمه.',
|
||||
longDescription:
|
||||
'گوشت چرخکرده گوسفند با طعم غنی. ضروری برای کباب سیخی، قیمه گوسفند و پراتای پرشده.',
|
||||
},
|
||||
'fish-salmon': {
|
||||
name: 'فیله سالمون اطلس منجمد',
|
||||
description: 'فیله سالمون منجمد، بسته وکیوم — در خانه آب کنید و در ماهیتابه تفت دهید.',
|
||||
longDescription:
|
||||
'فیله سالمون اطلس منجمد ممتاز، سرشار از امگا-۳. در مبدأ منجمد، تمیز، تکهبندی و وکیوم. تا زمان پخت منجمد نگه دارید.',
|
||||
},
|
||||
'fish-rohu': {
|
||||
name: 'ماهی روهو منجمد',
|
||||
description: 'روهوی کامل منجمد — محبوب آسیای جنوبی، فقط بهصورت منجمد.',
|
||||
longDescription:
|
||||
'ماهی روهو منجمد، اصلی آشپزی آسیای جنوبی. در مبدأ منجمد و برای فریزر بستهبندی شده. قبل از خورشت ماهی و دستورهای سنتی آب کنید.',
|
||||
},
|
||||
'fish-prawns': {
|
||||
name: 'میگو جامبو منجمد',
|
||||
description: 'میگوی جامبو منجمد برای خورشت، بریانی و کباب پس از آب شدن.',
|
||||
longDescription:
|
||||
'میگوی جامبو منجمد ممتاز، فقط بهصورت منجمد فروخته میشود. گوشت شیرین و محکم — قبل از پخت در خورشت کاملاً آب کنید.',
|
||||
},
|
||||
'fish-basa': {
|
||||
name: 'فیله باسا منجمد',
|
||||
description: 'فیله باسای منجمد و ملایم — آب کردن و پخت آسان برای همه خانواده.',
|
||||
longDescription:
|
||||
'فیله باسای منجمد بدون استخوان با طعم ملایم. فقط منجمد — قبل از پخت، خورشت سبک یا تاکوی ماهی آب کنید.',
|
||||
},
|
||||
},
|
||||
cart: {
|
||||
title: 'سبد خرید شما',
|
||||
itemsCount: '{count} قلم در سبد خرید',
|
||||
empty: 'سبد خرید شما خالی است',
|
||||
emptyHint: 'مجموعه حلال ممتاز ما را ببینید و به سبد اضافه کنید.',
|
||||
startShopping: 'شروع خرید',
|
||||
customization: 'سفارشیسازی:',
|
||||
orderSummary: 'خلاصه سفارش',
|
||||
subtotal: 'جمع جزء',
|
||||
delivery: 'تحویل',
|
||||
free: 'رایگان',
|
||||
freeDeliveryHint: 'تحویل رایگان برای سفارشهای بالای ۵۰۰ کرون',
|
||||
total: 'جمع کل',
|
||||
proceedCheckout: 'ادامه به پرداخت',
|
||||
continueShopping: 'ادامه خرید',
|
||||
removeItem: 'حذف قلم',
|
||||
},
|
||||
checkout: {
|
||||
title: 'پرداخت امن',
|
||||
backToCart: 'بازگشت به سبد',
|
||||
noItems: 'قلمی برای پرداخت وجود ندارد',
|
||||
goToShop: 'رفتن به فروشگاه',
|
||||
orderConfirmed: 'سفارش تأیید شد!',
|
||||
thankYou: 'از سفارش شما سپاسگزاریم. گوشت حلال ممتاز شما در حال آمادهسازی است.',
|
||||
orderId: 'شناسه سفارش: {id}',
|
||||
viewOrders: 'مشاهده سفارشها',
|
||||
haveAccount: 'حساب کاربری دارید؟',
|
||||
signIn: 'ورود',
|
||||
fasterCheckout: 'برای پرداخت سریعتر.',
|
||||
deliveryDetails: 'جزئیات تحویل',
|
||||
fullName: 'نام و نام خانوادگی',
|
||||
email: 'ایمیل',
|
||||
phone: 'تلفن',
|
||||
street: 'آدرس خیابان',
|
||||
city: 'شهر',
|
||||
state: 'استان',
|
||||
zip: 'کد پستی',
|
||||
payment: 'پرداخت',
|
||||
creditCard: 'کارت اعتباری',
|
||||
cashOnDelivery: 'پرداخت در محل',
|
||||
cardNumber: 'شماره کارت',
|
||||
expiry: 'تاریخ انقضا',
|
||||
cvv: 'CVV',
|
||||
qty: 'تعداد: {count}',
|
||||
processing: 'در حال پردازش...',
|
||||
pay: 'پرداخت {amount}',
|
||||
secure: 'رمزگذاری امن SSL ۲۵۶ بیتی',
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: 'خوش آمدید',
|
||||
createAccount: 'ایجاد حساب',
|
||||
joinTagline: 'به {name} بپیوندید و تجربه خرید ممتاز داشته باشید',
|
||||
signInTagline: 'به حساب {name} خود وارد شوید',
|
||||
fullName: 'نام و نام خانوادگی',
|
||||
email: 'ایمیل',
|
||||
password: 'رمز عبور',
|
||||
phone: 'تلفن',
|
||||
street: 'آدرس خیابان',
|
||||
city: 'شهر',
|
||||
state: 'استان',
|
||||
zip: 'کد پستی',
|
||||
signIn: 'ورود',
|
||||
register: 'ایجاد حساب',
|
||||
hasAccount: 'قبلاً حساب دارید؟ وارد شوید',
|
||||
noAccount: 'حساب ندارید؟ ثبتنام کنید',
|
||||
invalidCredentials: 'ایمیل یا رمز عبور نادرست است. {email} / demo123 را امتحان کنید',
|
||||
demo: 'نسخه آزمایشی: {email} / demo123',
|
||||
},
|
||||
account: {
|
||||
title: 'حساب من',
|
||||
welcome: 'خوش آمدید، {name}',
|
||||
memberSince: 'عضو از {date}',
|
||||
myWishlist: 'علاقهمندیهای من',
|
||||
signOut: 'خروج',
|
||||
orderHistory: 'تاریخچه سفارشها',
|
||||
noOrders: 'هنوز سفارشی ندارید',
|
||||
startShopping: 'شروع خرید',
|
||||
},
|
||||
wishlist: {
|
||||
title: 'علاقهمندیهای من',
|
||||
saved: '{count} قلم ذخیرهشده',
|
||||
empty: 'لیست علاقهمندیهای شما خالی است',
|
||||
emptyHint: 'محصولات مورد علاقه را ذخیره کنید تا بعداً بخرید.',
|
||||
browse: 'مشاهده محصولات',
|
||||
},
|
||||
about: {
|
||||
title: 'درباره {name}',
|
||||
subtitle:
|
||||
'گوشت حلال ۱۰۰٪ ممتاز را با تازگی، سفارشیسازی و تحویل دلسوزانه به سفره شما میرسانیم.',
|
||||
ourStory: 'داستان ما',
|
||||
storyP1:
|
||||
'{name} با هدفی ساده تأسیس شد: گوشت حلال ممتاز را برای هر خانواده در دسترس قرار دهیم، بدون کوتاه آمدن در کیفیت، تازگی یا رعایت شرعی. ما میدانیم برای بسیاری از خانوادهها، برش درست به شیوه درست، لوکس نیست — ضروری است.',
|
||||
storyP2:
|
||||
'از انتخاب تعداد تکه مرغ تا برش نیهاری یا قراهی برای گاو و گوسفند، سفارشیسازی را در قلب هر سفارش قرار دادهایم. قصابان حرفهای ما هر سفارش را دستی آماده میکنند و تحویل با کنترل دما تضمین میکند گوشت شما بهاندازه روز برش، تازه برسد.',
|
||||
halalTitle: 'گواهی حلال',
|
||||
halalDesc:
|
||||
'هر محصول در {name} از تأمینکنندگان حلال گواهیشده تهیه میشود. زنجیره تأمین ما کاملاً قابل ردیابی است و رعایت دقیق استانداردهای ذبح و فرآوری حلال را حفظ میکنیم. فقط با مزارع و فرآورندههایی کار میکنیم که به تولید اخلاقی و مطابق شرع متعهدند.',
|
||||
freshDaily: 'روزانه تازه',
|
||||
freshDailyDesc: 'هر صبح از مزارع معتبر تهیه میشود',
|
||||
premiumQuality: 'کیفیت ممتاز',
|
||||
premiumQualityDesc: 'برشهای دستچین توسط قصابان حرفهای',
|
||||
fastDelivery: 'تحویل سریع',
|
||||
fastDeliveryDesc: 'تحویل همان روز با کنترل دما',
|
||||
deliveryTitle: 'اطلاعات تحویل',
|
||||
delivery1: 'در شعاع ۲۵ مایلی تأسیسات فرآوری ما تحویل میدهیم.',
|
||||
delivery2: 'سفارشهای قبل از ساعت ۱۴ برای تحویل همان روز واجد شرایطاند.',
|
||||
delivery3: 'تحویل رایگان برای سفارشهای بالای ۵۰۰ کرون. هزینه تحویل استاندارد: ۴۹ کرون.',
|
||||
delivery4: 'همه محصولات وکیوم و در بستهبندی عایق حمل میشوند.',
|
||||
contactTitle: 'تماس با ما',
|
||||
address: 'Tingvallavägen 11, 195 31 Märsta',
|
||||
privacyTitle: 'سیاست حفظ حریم خصوصی',
|
||||
privacyText:
|
||||
'فقط اطلاعات لازم برای پردازش سفارش و بهبود خدمات را جمعآوری میکنیم — نام، اطلاعات تماس و آدرس تحویل. اطلاعات شما را به اشخاص ثالث نمیفروشیم. جزئیات پرداخت بهصورت امن توسط شرکای پرداخت ما مدیریت میشود.',
|
||||
termsTitle: 'شرایط استفاده',
|
||||
termsText:
|
||||
'همه قیمتها به کرون سوئد نمایش داده میشوند و ممکن است بدون اطلاع قبلی تغییر کنند. سفارشها منوط به موجودی است. گواهی حلال برای همه محصولات گوشتی اعمال میشود. زمان تحویل تخمینی است و در دورههای شلوغ ممکن است متفاوت باشد.',
|
||||
},
|
||||
footer: {
|
||||
tagline:
|
||||
'تحویل گوشت حلال ۱۰۰٪ ممتاز. برشهای تازه و سفارشی با کیفیت بینقص تا در منزل.',
|
||||
shop: 'فروشگاه',
|
||||
company: 'شرکت',
|
||||
contact: 'تماس',
|
||||
rights: 'تمامی حقوق محفوظ است.',
|
||||
phone: '072-585 50 50',
|
||||
hours: 'هر روز باز {hours}',
|
||||
},
|
||||
notFound: {
|
||||
title: '۴۰۴',
|
||||
message: 'صفحه یافت نشد',
|
||||
goHome: 'بازگشت به خانه',
|
||||
},
|
||||
orderStatus: {
|
||||
pending: 'در انتظار',
|
||||
confirmed: 'تأییدشده',
|
||||
preparing: 'در حال آمادهسازی',
|
||||
'out-for-delivery': 'در مسیر تحویل',
|
||||
delivered: 'تحویلشده',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,466 @@
|
||||
import { TranslationDict } from '../types';
|
||||
|
||||
export const sv: TranslationDict = {
|
||||
site: {
|
||||
name: 'Kött Gård',
|
||||
tagline: 'Premium Halal',
|
||||
description:
|
||||
'Premium 100 % halal köttleverans. Färskt kyckling, nötkött och lamm; endast fryst fisk och skaldjur — anpassat efter dina önskemål och levererat till din dörr.',
|
||||
metaTitle: 'Premium Halal Köttleverans',
|
||||
initials: 'KG',
|
||||
email: 'hello@kottgard.se',
|
||||
demoEmail: 'demo@kottgard.se',
|
||||
},
|
||||
nav: {
|
||||
shop: 'Butik',
|
||||
chicken: 'Kyckling',
|
||||
beef: 'Nötkött',
|
||||
lamb: 'Lamm',
|
||||
fish: 'Fryst fisk',
|
||||
about: 'Om oss',
|
||||
halalCert: 'Halalcertifiering',
|
||||
delivery: 'Leveransinfo',
|
||||
contact: 'Kontakt',
|
||||
myAccount: 'Mitt konto',
|
||||
orderHistory: 'Orderhistorik',
|
||||
wishlist: 'Önskelista',
|
||||
cart: 'Varukorg',
|
||||
privacy: 'Integritetspolicy',
|
||||
terms: 'Användarvillkor',
|
||||
searchProducts: 'Sök produkter',
|
||||
toggleMenu: 'Växla meny',
|
||||
account: 'Konto',
|
||||
language: 'Språk',
|
||||
},
|
||||
languageBanner: {
|
||||
choose: 'Välj språk',
|
||||
},
|
||||
hero: {
|
||||
badge: '100 % Halal-certifierat',
|
||||
taglineShort: 'Naturligt Rent',
|
||||
title: 'Premium Halal Kött',
|
||||
titleHighlight: '',
|
||||
titleEnd: '',
|
||||
subtitleShort: 'Färskt. Kvalitet. Tillförlitligt.',
|
||||
subtitle:
|
||||
'Halal-certifierat · Färskt dagligen · Hemleverans · Öppet alla dagar',
|
||||
hours: 'Öppet alla dagar {hours}',
|
||||
location: 'Tingvallavägen 11, Märsta',
|
||||
shopNow: 'Se vårt sortiment',
|
||||
browseChicken: 'Bläddra kyckling',
|
||||
whatsapp: 'Beställ via WhatsApp',
|
||||
},
|
||||
aboutPreview: {
|
||||
label: 'Om oss',
|
||||
title: 'Märstas finaste köttbutik',
|
||||
p1: 'Kött Gård är mer än en köttbutik — vi är ett löfte om kvalitet. Allt vårt kött är 100 % Halal-certifierat och färskt levererat varje dag.',
|
||||
p2: 'Vi handlar lamm från Irland och Nya Zeeland, kyckling och nötkött från välrenommerade producenter, och hjälper dig hitta rätt detalj för middagen, festen eller söndagssteken.',
|
||||
p3: 'Kom in i butiken på Tingvallavägen, prata med oss om vad du letar efter — vi styckar och packar efter dina önskemål.',
|
||||
statHalal: '100 %',
|
||||
statHalalLabel: 'Halal-certifierat',
|
||||
statDays: '7 dagar',
|
||||
statDaysLabel: 'Öppet i veckan',
|
||||
statDelivery: 'Daglig',
|
||||
statDeliveryLabel: 'Leverans',
|
||||
statFresh: 'Färskt',
|
||||
statFreshLabel: 'Varje dag',
|
||||
readMore: 'Läs mer om oss',
|
||||
imageAlt: 'Färska köttdetaljer på skärbräda från Kött Gård',
|
||||
},
|
||||
trust: {
|
||||
halal: '100 % Halal',
|
||||
halalDesc: 'Certifierad halal-källa med full spårbarhet och efterlevnad.',
|
||||
fresh: 'Färskt Dagligen',
|
||||
freshDesc: 'Hämtas färskt varje morgon och levereras i toppskick.',
|
||||
premium: 'Premiumkvalitet',
|
||||
premiumDesc: 'Handplockade styckningar från betrodda gårdar, förberedda av expertslaktare.',
|
||||
},
|
||||
categories: {
|
||||
title: 'Vårt sortiment',
|
||||
subtitle: 'Handplockat kött — varje dag. Färskt levererat. Halal. Styckat efter önskemål.',
|
||||
shop: 'Handla {name}',
|
||||
chicken: {
|
||||
name: 'Kyckling',
|
||||
description: 'Kycklingbröstfilé, vingar, klubba och hel kyckling. Färsk varje morgon.',
|
||||
},
|
||||
beef: {
|
||||
name: 'Nötkött & Kalv',
|
||||
description: 'Nötfärs, kalv bone marrow och premiumskär. Hög marmorering, jämn kvalitet.',
|
||||
},
|
||||
lamb: {
|
||||
name: 'Lamm',
|
||||
description: 'Lammbringa, lammhals, lammstek och lammrygg. Från Irland och Nya Zeeland.',
|
||||
},
|
||||
fish: {
|
||||
name: 'Fryst fisk',
|
||||
description: 'Endast fryst fisk och skaldjur — vakuumförpackat för frysen.',
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
label: 'Utvalt Sortiment',
|
||||
title: 'Utvalda Produkter',
|
||||
subtitle: 'Våra mest populära styckningar, älskade av familjer i hela staden.',
|
||||
viewAll: 'Visa alla',
|
||||
},
|
||||
howItWorks: {
|
||||
label: 'Beställ',
|
||||
title: 'Så enkelt beställer du',
|
||||
step1Title: 'Kontakta oss',
|
||||
step1Desc:
|
||||
'Skicka ett meddelande på WhatsApp med vad du vill ha — vi svarar snabbt.',
|
||||
step2Title: 'Vi bekräftar',
|
||||
step2Desc:
|
||||
'Vi bekräftar din beställning, ger pris och säger när den är klar.',
|
||||
step3Title: 'Hämta eller leverans',
|
||||
step3Desc:
|
||||
'Hämta i butik på Tingvallavägen 11 eller välj hemleverans.',
|
||||
step: 'Steg {n}',
|
||||
whatsapp: 'Beställ på WhatsApp',
|
||||
},
|
||||
offers: {
|
||||
label: 'Beställ',
|
||||
title: 'Veckans erbjudanden',
|
||||
subtitle:
|
||||
'Vi uppdaterar löpande med färska erbjudanden. Följ oss på sociala medier för senaste priserna.',
|
||||
disclaimer: 'Pris gäller så långt lagret räcker',
|
||||
was: 'Före',
|
||||
now: 'NU',
|
||||
order: 'Beställ',
|
||||
viewProduct: 'Visa produkt',
|
||||
badge: {
|
||||
fresh: 'FÄRSK',
|
||||
halal: 'HALAL',
|
||||
},
|
||||
items: {
|
||||
chickenWings: { name: 'Kycklingvingar färsk PL' },
|
||||
lambSteak: { name: 'Lammstek färsk Ireland' },
|
||||
beefMince: { name: 'Nötfärs 5% fett IRL' },
|
||||
},
|
||||
},
|
||||
social: {
|
||||
label: 'Följ oss',
|
||||
title: 'Följ oss',
|
||||
subtitle:
|
||||
'1 100+ följare på Facebook · 249 inlägg · Dagliga uppdateringar',
|
||||
facebook: 'Facebook',
|
||||
instagram: 'Instagram',
|
||||
whatsapp: 'WhatsApp',
|
||||
},
|
||||
contact: {
|
||||
label: 'Kontakt',
|
||||
title: 'Besök oss',
|
||||
addressLabel: 'Adress',
|
||||
phoneLabel: 'Telefon',
|
||||
hoursLabel: 'Öppettider',
|
||||
hoursValue: 'Alla dagar: {hours}',
|
||||
writeUs: 'Skriv till oss',
|
||||
callUs: 'Ring oss',
|
||||
openMaps: 'Öppna i Google Maps',
|
||||
learnMore: 'Kontaktuppgifter',
|
||||
},
|
||||
cta: {
|
||||
title: 'Redo för Premium Halal Kött?',
|
||||
subtitle:
|
||||
'Beställ idag och upplev skillnaden med verkligt färskt, anpassat halal kött levererat till din dörr.',
|
||||
button: 'Se vårt sortiment',
|
||||
whatsapp: 'Beställ på WhatsApp',
|
||||
},
|
||||
shop: {
|
||||
title: 'Alla Produkter',
|
||||
subtitle: 'Premium halal kött, anpassat efter dina önskemål',
|
||||
noProducts: 'Inga produkter hittades',
|
||||
noProductsHint: 'Prova att justera dina filter eller sökfråga',
|
||||
filters: 'Filter',
|
||||
productsFound: '{count} produkter hittades',
|
||||
search: 'Sök',
|
||||
searchPlaceholder: 'Sök produkter...',
|
||||
category: 'Kategori',
|
||||
sortBy: 'Sortera efter',
|
||||
all: 'Alla',
|
||||
sortFeatured: 'Utvalda',
|
||||
sortPriceAsc: 'Pris: Lägst till högst',
|
||||
sortPriceDesc: 'Pris: Högst till lägst',
|
||||
sortName: 'Namn A–Ö',
|
||||
},
|
||||
product: {
|
||||
backToShop: 'Tillbaka till butiken',
|
||||
inStock: 'I lager',
|
||||
outOfStock: 'Slut i lager',
|
||||
halalTrust: '100 % Halal-certifierad · Färskt dagligen · Premiumkvalitet',
|
||||
yourSelection: 'Ditt val',
|
||||
aboutProduct: 'Om denna produkt',
|
||||
addToCart: 'Lägg i varukorg',
|
||||
addedToCart: 'Tillagd i varukorg',
|
||||
decreaseQty: 'Minska antal',
|
||||
increaseQty: 'Öka antal',
|
||||
removeWishlist: 'Ta bort från önskelista',
|
||||
addWishlist: 'Lägg till i önskelista',
|
||||
viewProduct: 'Visa {name}',
|
||||
pieces: '{count} bitar',
|
||||
standardCut: 'Standardstyckning',
|
||||
howManyCuts: 'Hur Många Styckningar Vill Du Ha?',
|
||||
howManyCutsHint: 'Välj antal styckningar för din beställning',
|
||||
cutsAndStyle: '{cuts} styckningar · {style}',
|
||||
selectCutting: 'Välj skärstil',
|
||||
selectCuttingHint: 'Våra slaktare förbereder ditt {category} exakt enligt din önskade styckning',
|
||||
fishNote:
|
||||
'Vi säljer endast fryst fisk. All fisk är fryst vid källan, professionellt packad och säljs fryst — förvara i frysen tills du ska laga mat.',
|
||||
},
|
||||
cutting: {
|
||||
nihari: 'Nihari-styckning',
|
||||
karahi: 'Karahi-styckning',
|
||||
qeema: 'Qeema (färs)',
|
||||
boneless: 'Benfri',
|
||||
steak: 'Biffstyckning',
|
||||
},
|
||||
priceUnit: {
|
||||
perBird: 'per kyckling',
|
||||
perPack: 'per förpackning',
|
||||
perKg: 'per kg',
|
||||
},
|
||||
badges: {
|
||||
bestseller: 'Bästsäljare',
|
||||
chefsPick: 'Kockens val',
|
||||
premium: 'Premium',
|
||||
popular: 'Populär',
|
||||
freshCatch: 'Färsk fångst',
|
||||
frozen: 'Fryst',
|
||||
},
|
||||
products: {
|
||||
'chicken-whole': {
|
||||
name: 'Hel Kyckling',
|
||||
description: 'Gårdsfärsk hel halal kyckling, perfekt för stekning eller curry.',
|
||||
longDescription:
|
||||
'Våra hela kycklingar kommer från certifierade halalgårdar och levereras i toppskick. Varje fågel är handplockad för kvalitet, med mört kött och ren bearbetning. Välj önskat antal bitar så förbereder vi den precis som du behöver.',
|
||||
},
|
||||
'chicken-breast': {
|
||||
name: 'Kycklingbröst',
|
||||
description: 'Magert, benfritt kycklingbröst — idealiskt för grillning och hälsosamma måltider.',
|
||||
longDescription:
|
||||
'Premium benfritt kycklingbröst, trimmat och redo att tillagas. Perfekt för kebab, wok och hälsosamma vardagsmiddagar. Välj antal bitar och njut av konsekvent kvalitet varje gång.',
|
||||
},
|
||||
'chicken-thighs': {
|
||||
name: 'Kycklinglår',
|
||||
description: 'Saftiga halal kycklinglår med rik smak för curry och grill.',
|
||||
longDescription:
|
||||
'Våra kycklinglår är kända för sin saftighet och djupa smak. Oavsett om du lagar traditionell karahi eller helg-BBQ levererar dessa lår varje gång.',
|
||||
},
|
||||
'chicken-wings': {
|
||||
name: 'Kycklingvingar',
|
||||
description: 'Festfärdiga halal kycklingvingar för stekning, bakning eller grillning.',
|
||||
longDescription:
|
||||
'Krispiga, smakrika kycklingvingar tillagade enligt halal och levererade färskt. En favorit för matchkvällar och familjesammankomster.',
|
||||
},
|
||||
'beef-nihari': {
|
||||
name: 'Nötkött för Nihari',
|
||||
description: 'Långkoksklara nötköttsstyckningar, perfekta för traditionell nihari.',
|
||||
longDescription:
|
||||
'Särskilt utvalda nötköttsstyckningar idealiska för långkokt nihari. Rika på kollagen och smak, dessa styckningar bryts ner vackert under timmar av sjudning för en autentisk, smältande upplevelse.',
|
||||
},
|
||||
'beef-steak': {
|
||||
name: 'Premium Nötbiff',
|
||||
description: 'Restaurangkvalitet halal biffstyckningar för perfekt stekyta.',
|
||||
longDescription:
|
||||
'Handskurna premium biffar från de finaste halal-källorna. Marmorering, mörhet och redo för din grill eller gjutjärnspanna. Välj önskad skärstil.',
|
||||
},
|
||||
'beef-mince': {
|
||||
name: 'Nötfärs',
|
||||
description: 'Färsk halal nötfärs för kebab, burgare och qeema.',
|
||||
longDescription:
|
||||
'Finmalen halal nötfärs med perfekt fettförhållande för saftiga kebab, smakrik qeema och hemlagade burgare. Malas färskt dagligen.',
|
||||
},
|
||||
'beef-boneless': {
|
||||
name: 'Benfria Nötköttskuber',
|
||||
description: 'Mångsidiga benfria nötköttskuber för karahi, biryani och grytor.',
|
||||
longDescription:
|
||||
'Enhetliga benfria nötköttskuber skurna till perfektion för snabbkokta rätter. Idealiska för karahi, pulao och wok där konsekvent storlek är viktigt.',
|
||||
},
|
||||
'lamb-shoulder': {
|
||||
name: 'Lammbog',
|
||||
description: 'Rik, smakrik lammbog för långstekning och curry.',
|
||||
longDescription:
|
||||
'Premium halal lammbog med vacker marmorering. Perfekt för långstekta festmåltider, rejäla curryrätter och traditionella familjemiddagar. Anpassad efter din önskade styckning.',
|
||||
},
|
||||
'lamb-leg': {
|
||||
name: 'Lammlägg',
|
||||
description: 'Mört lammlägg för stekning, grillning och speciella tillfällen.',
|
||||
longDescription:
|
||||
'Hela eller portionerade lammlägg från certifierade halal-källor. En centerstyckning för Eid-firanden, middagsbjudningar och söndagsstek.',
|
||||
},
|
||||
'lamb-chops': {
|
||||
name: 'Lammkotletter',
|
||||
description: 'Premium lammkotletter för grillning och finmiddag hemma.',
|
||||
longDescription:
|
||||
'Tjockskurna halal lammkotletter med perfekt fettlock för grillning. Restaurangkvalitet, levererad till ditt kök.',
|
||||
},
|
||||
'lamb-mince': {
|
||||
name: 'Lammfärs',
|
||||
description: 'Färsk halal lammfärs för kebab, samosas och qeema.',
|
||||
longDescription:
|
||||
'Finmalen lammfärs med rik smak. Nödvändig för seekh kebab, lamm qeema och fyllda parathas.',
|
||||
},
|
||||
'fish-salmon': {
|
||||
name: 'Fryst atlantisk laxfilé',
|
||||
description: 'Frysta laxfiléer, vakuumförpackade — tinas och steks hemma.',
|
||||
longDescription:
|
||||
'Premium frysta atlantiska laxfiléer med rikt omega-3-innehåll. Fryst vid källan, rengjord, portionerad och vakuumförpackad. Förvara fryst tills du ska laga mat.',
|
||||
},
|
||||
'fish-rohu': {
|
||||
name: 'Fryst rohu-fisk',
|
||||
description: 'Fryst hel rohu — sydasiatisk favorit, säljs endast fryst.',
|
||||
longDescription:
|
||||
'Fryst rohu-fisk, en stapel i sydasiatisk matlagning. Fryst vid källan och packad för din frys. Tina före fiskcurry, stekt fisk eller traditionella recept.',
|
||||
},
|
||||
'fish-prawns': {
|
||||
name: 'Frysta jätteräkor',
|
||||
description: 'Frysta jätteräkor för curry, biryani och grill efter upptining.',
|
||||
longDescription:
|
||||
'Premium frysta jätteräkor, packade och sålda endast frysta. Sött, fast kött — tinas helt före tillagning i curry och tandoori-rätter.',
|
||||
},
|
||||
'fish-basa': {
|
||||
name: 'Fryst basa-filé',
|
||||
description: 'Frysta milda basa-filéer — enkla att tina och laga för hela familjen.',
|
||||
longDescription:
|
||||
'Frysta benfria basa-filéer med mild, delikat smak. Säljs endast fryst — tina före bakning, lätta curryrätter eller fisk-tacos.',
|
||||
},
|
||||
},
|
||||
cart: {
|
||||
title: 'Din Varukorg',
|
||||
itemsCount: '{count} artikel/artiklar i din varukorg',
|
||||
empty: 'Din varukorg är tom',
|
||||
emptyHint: 'Bläddra i vårt premium halal-sortiment och lägg till artiklar.',
|
||||
startShopping: 'Börja handla',
|
||||
customization: 'Anpassning:',
|
||||
orderSummary: 'Ordersammanfattning',
|
||||
subtotal: 'Delsumma',
|
||||
delivery: 'Leverans',
|
||||
free: 'Gratis',
|
||||
freeDeliveryHint: 'Fri leverans på beställningar över 500 kr',
|
||||
total: 'Totalt',
|
||||
proceedCheckout: 'Gå till kassan',
|
||||
continueShopping: 'Fortsätt handla',
|
||||
removeItem: 'Ta bort artikel',
|
||||
},
|
||||
checkout: {
|
||||
title: 'Säker Kassa',
|
||||
backToCart: 'Tillbaka till varukorg',
|
||||
noItems: 'Inga artiklar att betala',
|
||||
goToShop: 'Gå till butiken',
|
||||
orderConfirmed: 'Beställning bekräftad!',
|
||||
thankYou: 'Tack för din beställning. Ditt premium halal kött förbereds.',
|
||||
orderId: 'Order-ID: {id}',
|
||||
viewOrders: 'Visa beställningar',
|
||||
haveAccount: 'Har du ett konto?',
|
||||
signIn: 'Logga in',
|
||||
fasterCheckout: 'för snabbare utcheckning.',
|
||||
deliveryDetails: 'Leveransuppgifter',
|
||||
fullName: 'Fullständigt namn',
|
||||
email: 'E-post',
|
||||
phone: 'Telefon',
|
||||
street: 'Gatuadress',
|
||||
city: 'Stad',
|
||||
state: 'Län',
|
||||
zip: 'Postnummer',
|
||||
payment: 'Betalning',
|
||||
creditCard: 'Kreditkort',
|
||||
cashOnDelivery: 'Kontant vid leverans',
|
||||
cardNumber: 'Kortnummer',
|
||||
expiry: 'Giltig till',
|
||||
cvv: 'CVV',
|
||||
qty: 'Antal: {count}',
|
||||
processing: 'Bearbetar...',
|
||||
pay: 'Betala {amount}',
|
||||
secure: 'Säker 256-bitars SSL-kryptering',
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: 'Välkommen tillbaka',
|
||||
createAccount: 'Skapa konto',
|
||||
joinTagline: 'Gå med i {name} för en premium shoppingupplevelse',
|
||||
signInTagline: 'Logga in på ditt {name}-konto',
|
||||
fullName: 'Fullständigt namn',
|
||||
email: 'E-post',
|
||||
password: 'Lösenord',
|
||||
phone: 'Telefon',
|
||||
street: 'Gatuadress',
|
||||
city: 'Stad',
|
||||
state: 'Län',
|
||||
zip: 'Postnummer',
|
||||
signIn: 'Logga in',
|
||||
register: 'Skapa konto',
|
||||
hasAccount: 'Har du redan ett konto? Logga in',
|
||||
noAccount: 'Har du inget konto? Registrera dig',
|
||||
invalidCredentials: 'Ogiltig e-post eller lösenord. Prova {email} / demo123',
|
||||
demo: 'Demo: {email} / demo123',
|
||||
},
|
||||
account: {
|
||||
title: 'Mitt Konto',
|
||||
welcome: 'Välkommen tillbaka, {name}',
|
||||
memberSince: 'Medlem sedan {date}',
|
||||
myWishlist: 'Min önskelista',
|
||||
signOut: 'Logga ut',
|
||||
orderHistory: 'Orderhistorik',
|
||||
noOrders: 'Inga beställningar ännu',
|
||||
startShopping: 'Börja handla',
|
||||
},
|
||||
wishlist: {
|
||||
title: 'Min Önskelista',
|
||||
saved: '{count} sparad(e) artikel/artiklar',
|
||||
empty: 'Din önskelista är tom',
|
||||
emptyHint: 'Spara dina favoritprodukter för att köpa dem senare.',
|
||||
browse: 'Bläddra produkter',
|
||||
},
|
||||
about: {
|
||||
title: 'Om {name}',
|
||||
subtitle:
|
||||
'Vi levererar premium 100 % halal kött till ditt bord — färskt, anpassat och med omsorg.',
|
||||
ourStory: 'Vår Historia',
|
||||
storyP1:
|
||||
'{name} grundades med ett enkelt uppdrag: göra premium halal kött tillgängligt för varje familj, utan att kompromissa med kvalitet, färskhet eller religiös efterlevnad. Vi förstår att för många hushåll är rätt styckning tillagad på rätt sätt inte en lyx — det är nödvändigt.',
|
||||
storyP2:
|
||||
'Från att välja antal bitar för kyckling till att välja Nihari- eller Karahi-styckning för nötkött och lamm, sätter vi anpassning i centrum för varje beställning. Våra expertslaktare förbereder varje order för hand, och vår temperaturkontrollerade leverans säkerställer att ditt kött anländer lika färskt som dagen det styckades.',
|
||||
halalTitle: 'Halalcertifiering',
|
||||
halalDesc:
|
||||
'Varje produkt hos {name} kommer från certifierade halal-leverantörer. Vår leveranskedja är fullt spårbar och vi upprätthåller strikt efterlevnad av halal-slakt och -bearbetningsstandarder. Vi arbetar uteslutande med gårdar och bearbetare som delar vårt engagemang för etiskt, religiöst korrekt köttproduktion.',
|
||||
freshDaily: 'Färskt Dagligen',
|
||||
freshDailyDesc: 'Hämtas varje morgon från betrodda gårdar',
|
||||
premiumQuality: 'Premiumkvalitet',
|
||||
premiumQualityDesc: 'Handplockade styckningar av expertslaktare',
|
||||
fastDelivery: 'Snabb Leverans',
|
||||
fastDeliveryDesc: 'Temperaturkontrollerad leverans samma dag',
|
||||
deliveryTitle: 'Leveransinformation',
|
||||
delivery1: 'Vi levererar inom en radie på 40 km från vår anläggning.',
|
||||
delivery2: 'Beställningar före kl. 14 är berättigade till leverans samma dag.',
|
||||
delivery3: 'Fri leverans på beställningar över 500 kr. Standardleverans: 49 kr.',
|
||||
delivery4: 'Alla produkter vakuumförpackas och transporteras i isolerad förpackning.',
|
||||
contactTitle: 'Kontakta Oss',
|
||||
address: 'Tingvallavägen 11, 195 31 Märsta',
|
||||
privacyTitle: 'Integritetspolicy',
|
||||
privacyText:
|
||||
'Vi samlar endast den information som behövs för att behandla dina beställningar — namn, kontaktuppgifter och leveransadress. Vi säljer inte dina uppgifter till tredje part. Betalningsuppgifter hanteras säkert av våra betalpartners.',
|
||||
termsTitle: 'Användarvillkor',
|
||||
termsText:
|
||||
'Alla priser visas i SEK och kan ändras utan föregående meddelande. Beställningar är beroende av tillgång. Halalcertifiering gäller för alla köttprodukter. Leveranstider är uppskattningar och kan variera under högtrafik.',
|
||||
},
|
||||
footer: {
|
||||
tagline:
|
||||
'Premium 100 % halal köttleverans. Färskt, anpassat kött levererat till din dörr med kompromisslös kvalitet.',
|
||||
shop: 'Butik',
|
||||
company: 'Företag',
|
||||
contact: 'Kontakt',
|
||||
rights: 'Alla rättigheter förbehållna.',
|
||||
phone: '072-585 50 50',
|
||||
hours: 'Öppet alla dagar {hours}',
|
||||
},
|
||||
notFound: {
|
||||
title: '404',
|
||||
message: 'Sidan hittades inte',
|
||||
goHome: 'Gå till startsidan',
|
||||
},
|
||||
orderStatus: {
|
||||
pending: 'väntande',
|
||||
confirmed: 'bekräftad',
|
||||
preparing: 'förbereds',
|
||||
'out-for-delivery': 'ute för leverans',
|
||||
delivered: 'levererad',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,466 @@
|
||||
import { TranslationDict } from '../types';
|
||||
|
||||
export const tr: TranslationDict = {
|
||||
site: {
|
||||
name: 'Kött Gård',
|
||||
tagline: 'Premium Helal',
|
||||
description:
|
||||
'Premium %100 helal et teslimatı. Taze tavuk, dana ve kuzu; yalnızca donmuş balık ve deniz ürünleri — tercihinize göre hazırlanır ve kapınıza teslim edilir.',
|
||||
metaTitle: 'Premium Helal Et Teslimatı',
|
||||
initials: 'KG',
|
||||
email: 'hello@kottgard.se',
|
||||
demoEmail: 'demo@kottgard.se',
|
||||
},
|
||||
nav: {
|
||||
shop: 'Mağaza',
|
||||
chicken: 'Tavuk',
|
||||
beef: 'Dana',
|
||||
lamb: 'Kuzu',
|
||||
fish: 'Donmuş Balık',
|
||||
about: 'Hakkımızda',
|
||||
halalCert: 'Helal Sertifikası',
|
||||
delivery: 'Teslimat Bilgileri',
|
||||
contact: 'İletişim',
|
||||
myAccount: 'Hesabım',
|
||||
orderHistory: 'Sipariş Geçmişi',
|
||||
wishlist: 'Favoriler',
|
||||
cart: 'Sepet',
|
||||
privacy: 'Gizlilik Politikası',
|
||||
terms: 'Kullanım Koşulları',
|
||||
searchProducts: 'Ürün ara',
|
||||
toggleMenu: 'Menüyü aç/kapat',
|
||||
account: 'Hesap',
|
||||
language: 'Dil',
|
||||
},
|
||||
languageBanner: {
|
||||
choose: 'Dilinizi seçin',
|
||||
},
|
||||
hero: {
|
||||
badge: '%100 Helal Sertifikalı',
|
||||
taglineShort: 'Doğal ve Saf',
|
||||
title: 'Premium Helal Et',
|
||||
titleHighlight: '',
|
||||
titleEnd: '',
|
||||
subtitleShort: 'Taze. Kaliteli. Güvenilir.',
|
||||
subtitle:
|
||||
'Helal sertifikalı · Her gün taze · Eve teslimat · Her gün açık',
|
||||
hours: 'Her gün açık {hours}',
|
||||
location: 'Tingvallavägen 11, Märsta',
|
||||
shopNow: 'Ürünlerimizi İnceleyin',
|
||||
browseChicken: 'Tavuk Ürünlerine Göz Atın',
|
||||
whatsapp: 'WhatsApp ile Sipariş Verin',
|
||||
},
|
||||
aboutPreview: {
|
||||
label: 'Hakkımızda',
|
||||
title: "Märsta'nın En İyi Kasabı",
|
||||
p1: 'Kött Gård sadece bir kasap değil — kalite sözümüzdür. Tüm etlerimiz %100 helal sertifikalıdır ve her gün taze olarak teslim edilir.',
|
||||
p2: 'Kuzumuzu İrlanda ve Yeni Zelanda\'dan, tavuk ve danayı güvenilir üreticilerden temin ediyoruz; akşam yemeği, kutlamalar veya pazar kızartması için doğru kesimi bulmanıza yardımcı oluyoruz.',
|
||||
p3: 'Tingvallavägen\'de bizi ziyaret edin, ne aradığınızı söyleyin — istediğiniz şekilde kesip paketliyoruz.',
|
||||
statHalal: '%100',
|
||||
statHalalLabel: 'Helal sertifikalı',
|
||||
statDays: '7 gün',
|
||||
statDaysLabel: 'Haftalık açık',
|
||||
statDelivery: 'Günlük',
|
||||
statDeliveryLabel: 'Teslimat',
|
||||
statFresh: 'Taze',
|
||||
statFreshLabel: 'Her gün',
|
||||
readMore: 'Hakkımızda daha fazla bilgi',
|
||||
imageAlt: 'Kött Gård\'dan kesme tahtası üzerinde taze et parçaları',
|
||||
},
|
||||
trust: {
|
||||
halal: '%100 Helal',
|
||||
halalDesc: 'Tam izlenebilirlik ve uyumlulukla sertifikalı helal tedarik.',
|
||||
fresh: 'Her Gün Taze',
|
||||
freshDesc: 'Her sabah taze temin edilir ve en iyi kalitede teslim edilir.',
|
||||
premium: 'Premium Kalite',
|
||||
premiumDesc: 'Güvenilir çiftliklerden özenle seçilmiş kesimler, uzman kasaplar tarafından hazırlanır.',
|
||||
},
|
||||
categories: {
|
||||
title: 'Ürünlerimiz',
|
||||
subtitle: 'Özenle seçilmiş et — her gün. Taze teslimat. Helal. Siparişe göre kesim.',
|
||||
shop: '{name} Mağazası',
|
||||
chicken: {
|
||||
name: 'Tavuk',
|
||||
description: 'Göğüs fileto, kanat, baget ve bütün tavuk. Her sabah taze.',
|
||||
},
|
||||
beef: {
|
||||
name: 'Dana ve Buzağı',
|
||||
description: 'Kıyma, kemik iliği ve premium kesimler. Yüksek yağlanma, tutarlı kalite.',
|
||||
},
|
||||
lamb: {
|
||||
name: 'Kuzu',
|
||||
description: 'Omuz, boyun, kızartmalık ve pirzola. İrlanda ve Yeni Zelanda\'dan.',
|
||||
},
|
||||
fish: {
|
||||
name: 'Donmuş Balık',
|
||||
description: 'Yalnızca donmuş balık ve deniz ürünleri — vakumlu, dondurucuya hazır.',
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
label: 'Özenle Seçilmiş',
|
||||
title: 'Öne Çıkan Ürünler',
|
||||
subtitle: 'Şehrin dört bir yanındaki ailelerin en çok sevdiği kesimler.',
|
||||
viewAll: 'Tümünü Gör',
|
||||
},
|
||||
howItWorks: {
|
||||
label: 'Sipariş',
|
||||
title: 'Sipariş vermek ne kadar kolay',
|
||||
step1Title: 'Bize ulaşın',
|
||||
step1Desc:
|
||||
'Ne istediğinizi WhatsApp mesajıyla gönderin — hızlıca yanıt veririz.',
|
||||
step2Title: 'Onaylıyoruz',
|
||||
step2Desc:
|
||||
'Siparişinizi onaylar, fiyatı bildirir ve ne zaman hazır olacağını söyleriz.',
|
||||
step3Title: 'Mağazadan alın veya teslimat',
|
||||
step3Desc:
|
||||
'Tingvallavägen 11\'deki mağazamızdan alın veya eve teslimatı seçin.',
|
||||
step: 'Adım {n}',
|
||||
whatsapp: 'WhatsApp ile Sipariş Verin',
|
||||
},
|
||||
offers: {
|
||||
label: 'Sipariş',
|
||||
title: 'Haftalık fırsatlar',
|
||||
subtitle:
|
||||
'Düzenli olarak yeni kampanyalar ekliyoruz. En güncel fiyatlar için sosyal medyada bizi takip edin.',
|
||||
disclaimer: 'Stoklar tükenene kadar geçerli fiyat',
|
||||
was: 'Eski fiyat',
|
||||
now: 'ŞİMDİ',
|
||||
order: 'Sipariş Ver',
|
||||
viewProduct: 'Ürünü görüntüle',
|
||||
badge: {
|
||||
fresh: 'TAZE',
|
||||
halal: 'HELAL',
|
||||
},
|
||||
items: {
|
||||
chickenWings: { name: 'Taze tavuk kanat PL' },
|
||||
lambSteak: { name: 'Taze kuzu kızartmalık İrlanda' },
|
||||
beefMince: { name: 'Dana kıyma %5 yağ IRL' },
|
||||
},
|
||||
},
|
||||
social: {
|
||||
label: 'Bizi takip edin',
|
||||
title: 'Bizi takip edin',
|
||||
subtitle:
|
||||
'Facebook\'ta 1.100+ takipçi · 249 gönderi · Günlük güncellemeler',
|
||||
facebook: 'Facebook',
|
||||
instagram: 'Instagram',
|
||||
whatsapp: 'WhatsApp',
|
||||
},
|
||||
contact: {
|
||||
label: 'İletişim',
|
||||
title: 'Bizi ziyaret edin',
|
||||
addressLabel: 'Adres',
|
||||
phoneLabel: 'Telefon',
|
||||
hoursLabel: 'Çalışma saatleri',
|
||||
hoursValue: 'Her gün: {hours}',
|
||||
writeUs: 'Bize yazın',
|
||||
callUs: 'Bizi arayın',
|
||||
openMaps: 'Google Haritalar\'da aç',
|
||||
learnMore: 'İletişim bilgileri',
|
||||
},
|
||||
cta: {
|
||||
title: 'Premium Helal Ete Hazır mısınız?',
|
||||
subtitle:
|
||||
'Bugün sipariş verin; gerçekten taze, isteğinize göre hazırlanmış helal etin farkını kapınızda yaşayın.',
|
||||
button: 'Ürünlerimizi İnceleyin',
|
||||
whatsapp: 'WhatsApp ile Sipariş Verin',
|
||||
},
|
||||
shop: {
|
||||
title: 'Tüm Ürünler',
|
||||
subtitle: 'Tercihinize göre hazırlanan premium helal et',
|
||||
noProducts: 'Ürün bulunamadı',
|
||||
noProductsHint: 'Filtreleri veya arama terimini değiştirmeyi deneyin',
|
||||
filters: 'Filtreler',
|
||||
productsFound: '{count} ürün bulundu',
|
||||
search: 'Ara',
|
||||
searchPlaceholder: 'Ürün ara...',
|
||||
category: 'Kategori',
|
||||
sortBy: 'Sırala',
|
||||
all: 'Tümü',
|
||||
sortFeatured: 'Öne çıkanlar',
|
||||
sortPriceAsc: 'Fiyat: Düşükten Yükseğe',
|
||||
sortPriceDesc: 'Fiyat: Yüksekten Düşüğe',
|
||||
sortName: 'İsim A–Z',
|
||||
},
|
||||
product: {
|
||||
backToShop: 'Mağazaya Dön',
|
||||
inStock: 'Stokta',
|
||||
outOfStock: 'Stokta Yok',
|
||||
halalTrust: '%100 Helal sertifikalı · Her gün taze · Premium kalite',
|
||||
yourSelection: 'Seçiminiz',
|
||||
aboutProduct: 'Bu Ürün Hakkında',
|
||||
addToCart: 'Sepete Ekle',
|
||||
addedToCart: 'Sepete Eklendi',
|
||||
decreaseQty: 'Adedi azalt',
|
||||
increaseQty: 'Adedi artır',
|
||||
removeWishlist: 'Favorilerden çıkar',
|
||||
addWishlist: 'Favorilere ekle',
|
||||
viewProduct: '{name} görüntüle',
|
||||
pieces: '{count} parça',
|
||||
standardCut: 'Standart kesim',
|
||||
howManyCuts: 'Kaç Parça İstiyorsunuz?',
|
||||
howManyCutsHint: 'Siparişiniz için parça sayısını seçin',
|
||||
cutsAndStyle: '{cuts} parça · {style}',
|
||||
selectCutting: 'Kesim Stilini Seçin',
|
||||
selectCuttingHint: 'Kasaplarımız {category} ürününüzü tam istediğiniz şekilde hazırlayacak',
|
||||
fishNote:
|
||||
'Yalnızca donmuş balık satıyoruz. Tüm balıklar kaynağında dondurulur, profesyonelce paketlenir ve donmuş satılır — pişirene kadar dondurucuda saklayın.',
|
||||
},
|
||||
cutting: {
|
||||
nihari: 'Nihari kesimi',
|
||||
karahi: 'Karahi kesimi',
|
||||
qeema: 'Kıyma (qeema)',
|
||||
boneless: 'Kemiksiz',
|
||||
steak: 'Biftek kesimi',
|
||||
},
|
||||
priceUnit: {
|
||||
perBird: 'adet başına',
|
||||
perPack: 'paket başına',
|
||||
perKg: 'kg başına',
|
||||
},
|
||||
badges: {
|
||||
bestseller: 'Çok Satan',
|
||||
chefsPick: 'Şefin Seçimi',
|
||||
premium: 'Premium',
|
||||
popular: 'Popüler',
|
||||
freshCatch: 'Taze Av',
|
||||
frozen: 'Donmuş',
|
||||
},
|
||||
products: {
|
||||
'chicken-whole': {
|
||||
name: 'Bütün Tavuk',
|
||||
description: 'Çiftlikten taze bütün helal tavuk; kızartma veya curry için ideal.',
|
||||
longDescription:
|
||||
'Bütün tavuklarımız sertifikalı helal çiftliklerden temin edilir ve en taze haliyle teslim edilir. Her tavuk kalite için el ile seçilir; eti yumuşak ve işleme süreci hijyeniktir. İstediğiniz parça sayısını seçin, tam ihtiyacınıza göre hazırlayalım.',
|
||||
},
|
||||
'chicken-breast': {
|
||||
name: 'Tavuk Göğsü',
|
||||
description: 'Yağsız, kemiksiz tavuk göğsü — ızgara ve sağlıklı yemekler için ideal.',
|
||||
longDescription:
|
||||
'Premium kemiksiz tavuk göğsü, temizlenmiş ve pişirmeye hazır. Kebap, wok ve sağlıklı hafta içi akşam yemekleri için mükemmel. Parça sayınızı seçin ve her seferinde aynı kaliteyi yaşayın.',
|
||||
},
|
||||
'chicken-thighs': {
|
||||
name: 'Tavuk But',
|
||||
description: 'Sulu helal tavuk but; curry ve ızgara için zengin lezzet.',
|
||||
longDescription:
|
||||
'Tavuk butlarımız sulu yapısı ve derin lezzetiyle bilinir. Geleneksel karahi mi yapıyorsunuz, hafta sonu mangal mı — bu butlar her zaman mükemmel sonuç verir.',
|
||||
},
|
||||
'chicken-wings': {
|
||||
name: 'Tavuk Kanat',
|
||||
description: 'Kızartma, fırın veya ızgara için parti sofralarına hazır helal tavuk kanat.',
|
||||
longDescription:
|
||||
'Helal usullerle hazırlanmış, taze teslim edilen çıtır ve lezzetli tavuk kanatlar. Maç geceleri ve aile buluşmalarının vazgeçilmezi.',
|
||||
},
|
||||
'beef-nihari': {
|
||||
name: 'Nihari İçin Dana',
|
||||
description: 'Yavaş pişirmeye hazır dana kesimleri; geleneksel nihari için ideal.',
|
||||
longDescription:
|
||||
'Yavaş pişirilmiş nihari için özel seçilmiş dana kesimleri. Kolajen ve lezzet açısından zengin; saatlerce kaynatıldığında otantik, ağızda eriyen bir doku sunar.',
|
||||
},
|
||||
'beef-steak': {
|
||||
name: 'Premium Dana Biftek',
|
||||
description: 'Mükemmel kızartma için restoran kalitesinde helal biftek kesimleri.',
|
||||
longDescription:
|
||||
'En iyi helal kaynaklardan el ile kesilmiş premium dana biftekler. Yağlanmış, yumuşak ve ızgaranız veya döküm tavanız için hazır. Tercih ettiğiniz kesim stilini seçin.',
|
||||
},
|
||||
'beef-mince': {
|
||||
name: 'Dana Kıyma',
|
||||
description: 'Kebap, burger ve kıyma yemekleri için taze helal dana kıyma.',
|
||||
longDescription:
|
||||
'Sulu kebaplar, lezzetli kıyma yemekleri ve ev yapımı burgerler için ideal yağ oranına sahip ince çekilmiş helal dana kıyma. Her gün taze çekilir.',
|
||||
},
|
||||
'beef-boneless': {
|
||||
name: 'Kemiksiz Dana Küp',
|
||||
description: 'Karahi, pilav ve güveç için çok yönlü kemiksiz dana küpleri.',
|
||||
longDescription:
|
||||
'Hızlı pişen yemekler için mükemmel boyutta kesilmiş kemiksiz dana küpleri. Karahi, pilav ve wok yemeklerinde tutarlı boyut önemli olduğunda idealdir.',
|
||||
},
|
||||
'lamb-shoulder': {
|
||||
name: 'Kuzu Omuz',
|
||||
description: 'Yavaş kızartma ve curry için zengin lezzetli kuzu omuz.',
|
||||
longDescription:
|
||||
'Güzel yağlanmaya sahip premium helal kuzu omuz. Uzun süre kızartılan ziyafetler, doyurucu curryler ve geleneksel aile yemekleri için mükemmel. Tercih ettiğiniz kesime göre hazırlanır.',
|
||||
},
|
||||
'lamb-leg': {
|
||||
name: 'Kuzu But',
|
||||
description: 'Kızartma, ızgara ve özel günler için yumuşak kuzu but.',
|
||||
longDescription:
|
||||
'Sertifikalı helal kaynaklardan bütün veya parçalı kuzu but. Bayram kutlamaları, davet yemekleri ve pazar kızartmaları için göz alıcı bir kesim.',
|
||||
},
|
||||
'lamb-chops': {
|
||||
name: 'Kuzu Pirzola',
|
||||
description: 'Izgara ve evde fine dining için premium kuzu pirzola.',
|
||||
longDescription:
|
||||
'Izgara için mükemmel yağ tabakasına sahip kalın kesim helal kuzu pirzola. Restoran kalitesi, mutfağınıza teslim.',
|
||||
},
|
||||
'lamb-mince': {
|
||||
name: 'Kuzu Kıyma',
|
||||
description: 'Kebap, börek ve kıyma yemekleri için taze helal kuzu kıyma.',
|
||||
longDescription:
|
||||
'Zengin lezzetli ince çekilmiş kuzu kıyma. Şiş kebap, kuzu kıyma yemeği ve dolgulu flatbread için vazgeçilmez.',
|
||||
},
|
||||
'fish-salmon': {
|
||||
name: 'Donmuş Atlantik Somon Fileto',
|
||||
description: 'Donmuş somon filetoları, vakumlu — evde çözün ve tavada kızartın.',
|
||||
longDescription:
|
||||
'Omega-3 açısından zengin premium donmuş Atlantik somon filetoları. Kaynağında dondurulmuş, temizlenmiş, porsiyonlanmış ve vakumlu. Pişirene kadar donmuş saklayın.',
|
||||
},
|
||||
'fish-rohu': {
|
||||
name: 'Donmuş Rohu Balığı',
|
||||
description: 'Donmuş bütün rohu — Güney Asya\'nın favorisi, yalnızca donmuş satılır.',
|
||||
longDescription:
|
||||
'Güney Asya mutfağının vazgeçilmezi donmuş rohu balığı. Kaynağında dondurulmuş ve dondurucu için paketlenmiş. Balık curry\'si ve geleneksel tariflerden önce çözün.',
|
||||
},
|
||||
'fish-prawns': {
|
||||
name: 'Donmuş Jumbo Karides',
|
||||
description: 'Çözündükten sonra curry, pilav ve ızgara için donmuş jumbo karides.',
|
||||
longDescription:
|
||||
'Premium donmuş jumbo karidesler; yalnızca donmuş satılır. Curry ve tandoori yemeklerinden önce tamamen çözün.',
|
||||
},
|
||||
'fish-basa': {
|
||||
name: 'Donmuş Basa Fileto',
|
||||
description: 'Donmuş hafif basa filetoları — tüm aile için kolay çözme ve pişirme.',
|
||||
longDescription:
|
||||
'Hafif lezzetli donmuş kemiksiz basa filetoları. Yalnızca donmuş satılır — fırınlama ve hafif currylerden önce çözün.',
|
||||
},
|
||||
},
|
||||
cart: {
|
||||
title: 'Sepetiniz',
|
||||
itemsCount: 'Sepetinizde {count} ürün var',
|
||||
empty: 'Sepetiniz boş',
|
||||
emptyHint: 'Premium helal ürünlerimizi inceleyin ve sepetinize ekleyin.',
|
||||
startShopping: 'Alışverişe Başla',
|
||||
customization: 'Özelleştirme:',
|
||||
orderSummary: 'Sipariş Özeti',
|
||||
subtotal: 'Ara Toplam',
|
||||
delivery: 'Teslimat',
|
||||
free: 'Ücretsiz',
|
||||
freeDeliveryHint: '500 kr üzeri siparişlerde ücretsiz teslimat',
|
||||
total: 'Toplam',
|
||||
proceedCheckout: 'Ödemeye Geç',
|
||||
continueShopping: 'Alışverişe Devam Et',
|
||||
removeItem: 'Ürünü kaldır',
|
||||
},
|
||||
checkout: {
|
||||
title: 'Güvenli Ödeme',
|
||||
backToCart: 'Sepete Dön',
|
||||
noItems: 'Ödenecek ürün yok',
|
||||
goToShop: 'Mağazaya Git',
|
||||
orderConfirmed: 'Sipariş Onaylandı!',
|
||||
thankYou: 'Siparişiniz için teşekkür ederiz. Premium helal etiniz hazırlanıyor.',
|
||||
orderId: 'Sipariş No: {id}',
|
||||
viewOrders: 'Siparişleri Görüntüle',
|
||||
haveAccount: 'Hesabınız var mı?',
|
||||
signIn: 'Giriş yapın',
|
||||
fasterCheckout: 'daha hızlı ödeme için.',
|
||||
deliveryDetails: 'Teslimat Bilgileri',
|
||||
fullName: 'Ad Soyad',
|
||||
email: 'E-posta',
|
||||
phone: 'Telefon',
|
||||
street: 'Sokak Adresi',
|
||||
city: 'Şehir',
|
||||
state: 'İl/Eyalet',
|
||||
zip: 'Posta Kodu',
|
||||
payment: 'Ödeme',
|
||||
creditCard: 'Kredi Kartı',
|
||||
cashOnDelivery: 'Kapıda Ödeme',
|
||||
cardNumber: 'Kart Numarası',
|
||||
expiry: 'Son Kullanma',
|
||||
cvv: 'CVV',
|
||||
qty: 'Adet: {count}',
|
||||
processing: 'İşleniyor...',
|
||||
pay: '{amount} Öde',
|
||||
secure: 'Güvenli 256-bit SSL şifreleme',
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: 'Tekrar Hoş Geldiniz',
|
||||
createAccount: 'Hesap Oluştur',
|
||||
joinTagline: 'Premium alışveriş deneyimi için {name}\'a katılın',
|
||||
signInTagline: '{name} hesabınıza giriş yapın',
|
||||
fullName: 'Ad Soyad',
|
||||
email: 'E-posta',
|
||||
password: 'Şifre',
|
||||
phone: 'Telefon',
|
||||
street: 'Sokak Adresi',
|
||||
city: 'Şehir',
|
||||
state: 'İl/Eyalet',
|
||||
zip: 'Posta Kodu',
|
||||
signIn: 'Giriş Yap',
|
||||
register: 'Hesap Oluştur',
|
||||
hasAccount: 'Zaten hesabınız var mı? Giriş yapın',
|
||||
noAccount: 'Hesabınız yok mu? Kayıt olun',
|
||||
invalidCredentials: 'Geçersiz e-posta veya şifre. Deneyin: {email} / demo123',
|
||||
demo: 'Demo: {email} / demo123',
|
||||
},
|
||||
account: {
|
||||
title: 'Hesabım',
|
||||
welcome: 'Tekrar hoş geldiniz, {name}',
|
||||
memberSince: '{date} tarihinden beri üye',
|
||||
myWishlist: 'Favorilerim',
|
||||
signOut: 'Çıkış Yap',
|
||||
orderHistory: 'Sipariş Geçmişi',
|
||||
noOrders: 'Henüz sipariş yok',
|
||||
startShopping: 'Alışverişe Başla',
|
||||
},
|
||||
wishlist: {
|
||||
title: 'Favorilerim',
|
||||
saved: '{count} kayıtlı ürün',
|
||||
empty: 'Favori listeniz boş',
|
||||
emptyHint: 'Daha sonra satın almak için favori ürünlerinizi kaydedin.',
|
||||
browse: 'Ürünlere Göz Atın',
|
||||
},
|
||||
about: {
|
||||
title: '{name} Hakkında',
|
||||
subtitle:
|
||||
'Masanıza premium, %100 helal et getiriyoruz — taze, isteğinize göre hazırlanmış ve özenle teslim edilmiş.',
|
||||
ourStory: 'Hikayemiz',
|
||||
storyP1:
|
||||
'{name}, basit bir misyonla kuruldu: premium helal eti her aileye ulaştırmak; kalite, tazelik ve dini uyumluluktan ödün vermeden. Birçok ev için doğru kesimin doğru şekilde hazırlanması lüks değil — gerekliliktir.',
|
||||
storyP2:
|
||||
'Tavuk için parça sayısı seçiminden dana ve kuzu için Nihari veya Karahi kesimine kadar her siparişin merkezine özelleştirmeyi koyuyoruz. Uzman kasaplarımız her siparişi el ile hazırlar; sıcaklık kontrollü teslimatımız etinizin kesildiği günkü tazelikte ulaşmasını sağlar.',
|
||||
halalTitle: 'Helal Sertifikası',
|
||||
halalDesc:
|
||||
'{name}\'daki her ürün sertifikalı helal tedarikçilerden temin edilir. Tedarik zincirimiz tamamen izlenebilir ve helal kesim ile işleme standartlarına sıkı uyum sağlarız. Yalnızca etik ve dini açıdan uyumlu et üretimine ortak olan çiftlikler ve işlemcilerle çalışırız.',
|
||||
freshDaily: 'Her Gün Taze',
|
||||
freshDailyDesc: 'Her sabah güvenilir çiftliklerden temin edilir',
|
||||
premiumQuality: 'Premium Kalite',
|
||||
premiumQualityDesc: 'Uzman kasaplar tarafından özenle seçilmiş kesimler',
|
||||
fastDelivery: 'Hızlı Teslimat',
|
||||
fastDeliveryDesc: 'Sıcaklık kontrollü aynı gün teslimat',
|
||||
deliveryTitle: 'Teslimat Bilgileri',
|
||||
delivery1: 'İşleme tesisimizin 25 mil yarıçapı içinde teslimat yapıyoruz.',
|
||||
delivery2: 'Saat 14:00\'ten önce verilen siparişler aynı gün teslimata uygundur.',
|
||||
delivery3: '500 kr üzeri siparişlerde ücretsiz teslimat. Standart teslimat ücreti: 49 kr.',
|
||||
delivery4: 'Tüm ürünler vakumlu paketlenir ve yalıtımlı ambalajlarla taşınır.',
|
||||
contactTitle: 'Bize Ulaşın',
|
||||
address: 'Tingvallavägen 11, 195 31 Märsta',
|
||||
privacyTitle: 'Gizlilik Politikası',
|
||||
privacyText:
|
||||
'Yalnızca siparişlerinizi işlemek ve hizmetimizi geliştirmek için gerekli bilgileri topluyoruz — ad, iletişim bilgileri ve teslimat adresi. Verilerinizi üçüncü taraflara satmıyoruz. Ödeme bilgileri ödeme ortaklarımız tarafından güvenli şekilde işlenir.',
|
||||
termsTitle: 'Kullanım Koşulları',
|
||||
termsText:
|
||||
'Tüm fiyatlar SEK cinsinden gösterilir ve önceden haber verilmeksizin değişebilir. Siparişler stok durumuna bağlıdır. Helal sertifikası listelenen tüm et ürünleri için geçerlidir. Teslimat süreleri tahminidir ve yoğun dönemlerde değişebilir.',
|
||||
},
|
||||
footer: {
|
||||
tagline:
|
||||
'Premium %100 helal et teslimatı. Taze, isteğinize göre kesilmiş etler, ödün vermeyen kaliteyle kapınıza teslim.',
|
||||
shop: 'Mağaza',
|
||||
company: 'Şirket',
|
||||
contact: 'İletişim',
|
||||
rights: 'Tüm hakları saklıdır.',
|
||||
phone: '072-585 50 50',
|
||||
hours: 'Her gün açık {hours}',
|
||||
},
|
||||
notFound: {
|
||||
title: '404',
|
||||
message: 'Sayfa bulunamadı',
|
||||
goHome: 'Ana Sayfaya Dön',
|
||||
},
|
||||
orderStatus: {
|
||||
pending: 'beklemede',
|
||||
confirmed: 'onaylandı',
|
||||
preparing: 'hazırlanıyor',
|
||||
'out-for-delivery': 'teslimatta',
|
||||
delivered: 'teslim edildi',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,467 @@
|
||||
import { TranslationDict } from '../types';
|
||||
|
||||
export const ur: TranslationDict = {
|
||||
site: {
|
||||
name: 'کوٹ گارڈ',
|
||||
tagline: 'پریمیم حلال',
|
||||
description:
|
||||
'پریمیم 100% حلال گوشت کی ڈیلیوری۔ تازہ مرغی، گائے اور بکرے کا گوشت؛ صرف منجمد مچھلی اور سمندری غذا — آپ کی پسند کے مطابق تیار کر کے آپ کے دروازے تک پہنچائی جاتی ہے۔',
|
||||
metaTitle: 'پریمیم حلال گوشت ڈیلیوری',
|
||||
initials: 'KG',
|
||||
email: 'hello@kottgard.se',
|
||||
demoEmail: 'demo@kottgard.se',
|
||||
},
|
||||
nav: {
|
||||
shop: 'خریداری',
|
||||
chicken: 'مرغی',
|
||||
beef: 'گائے کا گوشت',
|
||||
lamb: 'بکرے کا گوشت',
|
||||
fish: 'منجمد مچھلی',
|
||||
about: 'ہمارے بارے میں',
|
||||
halalCert: 'حلال سرٹیفیکیشن',
|
||||
delivery: 'ڈیلیوری کی معلومات',
|
||||
contact: 'رابطہ',
|
||||
myAccount: 'میرا اکاؤنٹ',
|
||||
orderHistory: 'آرڈر کی تاریخ',
|
||||
wishlist: 'پسندیدہ',
|
||||
cart: 'ٹوکری',
|
||||
privacy: 'رازداری کی پالیسی',
|
||||
terms: 'شرائط و ضوابط',
|
||||
searchProducts: 'مصنوعات تلاش کریں',
|
||||
toggleMenu: 'مینو کھولیں',
|
||||
account: 'اکاؤنٹ',
|
||||
language: 'زبان',
|
||||
},
|
||||
languageBanner: {
|
||||
choose: 'اپنی زبان منتخب کریں',
|
||||
},
|
||||
hero: {
|
||||
badge: '100% حلال تصدیق شدہ',
|
||||
taglineShort: 'قدرتی طور پر خالص',
|
||||
title: 'پریمیم حلال گوشت',
|
||||
titleHighlight: '',
|
||||
titleEnd: '',
|
||||
subtitleShort: 'تازہ۔ معیار۔ قابل اعتماد۔',
|
||||
subtitle:
|
||||
'حلال تصدیق شدہ · روزانہ تازہ · گھر کی ڈیلیوری · ہر دن کھلا',
|
||||
hours: 'ہر دن کھلا {hours}',
|
||||
location: 'Tingvallavägen 11, Märsta',
|
||||
shopNow: 'ہمارا مجموعہ دیکھیں',
|
||||
browseChicken: 'مرغی دیکھیں',
|
||||
whatsapp: 'واٹس ایپ سے آرڈر کریں',
|
||||
},
|
||||
aboutPreview: {
|
||||
label: 'ہمارے بارے میں',
|
||||
title: 'مائرسٹا کی بہترین گوشت کی دکان',
|
||||
p1: 'کوٹ گارڈ صرف ایک گوشت کی دکان نہیں — ہم معیار کا وعدہ ہیں۔ ہمارا تمام گوشت 100% حلال تصدیق شدہ ہے اور روزانہ تازہ پہنچایا جاتا ہے۔',
|
||||
p2: 'ہم آئرلینڈ اور نیوزی لینڈ سے بکرے کا گوشت، معتبر پیدا کنندگان سے مرغی اور گائے کا گوشت حاصل کرتے ہیں، اور آپ کو رات کے کھانے، تقریب یا اتوار کی روست کے لیے صحیح کٹ تلاش کرنے میں مدد کرتے ہیں۔',
|
||||
p3: 'Tingvallavägen پر ہماری دکان میں آئیں، بتائیں آپ کیا تلاش کر رہے ہیں — ہم آپ کی خواہش کے مطابق کاٹتے اور پیک کرتے ہیں۔',
|
||||
statHalal: '100%',
|
||||
statHalalLabel: 'حلال تصدیق شدہ',
|
||||
statDays: '7 دن',
|
||||
statDaysLabel: 'ہفتے میں کھلا',
|
||||
statDelivery: 'روزانہ',
|
||||
statDeliveryLabel: 'ڈیلیوری',
|
||||
statFresh: 'تازہ',
|
||||
statFreshLabel: 'ہر دن',
|
||||
readMore: 'مزید پڑھیں',
|
||||
imageAlt: 'کوٹ گارڈ سے تازہ گوشت کی کٹس',
|
||||
},
|
||||
trust: {
|
||||
halal: '100% حلال',
|
||||
halalDesc: 'مکمل سراغ رسانی اور تعمیل کے ساتھ تصدیق شدہ حلال ذرائع۔',
|
||||
fresh: 'روزانہ تازہ',
|
||||
freshDesc: 'ہر صبح تازہ حاصل کیا جاتا ہے اور بہترین حالت میں پہنچایا جاتا ہے۔',
|
||||
premium: 'پریمیم معیار',
|
||||
premiumDesc: 'قابل اعتماد فارموں سے منتخب کردہ کٹس، ماہر قصابوں کے ذریعے تیار۔',
|
||||
},
|
||||
categories: {
|
||||
title: 'زمرے کے لحاظ سے خریداری',
|
||||
subtitle:
|
||||
'روزمرہ کی مرغی سے لے کر پریمیم بکرے کے کٹlets تک — ہر کٹ آپ کی پسند کے مطابق۔',
|
||||
shop: '{name} خریدیں',
|
||||
chicken: {
|
||||
name: 'مرغی',
|
||||
description: 'فارم سے تازہ حلال مرغی، آپ کی پسند کے مطابق کاٹی گئی',
|
||||
},
|
||||
beef: {
|
||||
name: 'گائے کا گوشت',
|
||||
description: 'روایتی اور جدید کٹس میں پریمیم حلال گائے کا گوشت',
|
||||
},
|
||||
lamb: {
|
||||
name: 'بکرے کا گوشت',
|
||||
description: 'ہر موقع کے لیے نرم حلال بکرے کا گوشت',
|
||||
},
|
||||
fish: {
|
||||
name: 'منجمد مچھلی',
|
||||
description: 'صرف منجمد مچھلی اور سمندری غذا — فریزر کے لیے ویکیوم پیک',
|
||||
},
|
||||
},
|
||||
featured: {
|
||||
label: 'منتخب مجموعہ',
|
||||
title: 'نمایاں مصنوعات',
|
||||
subtitle: 'ہماری سب سے مقبول کٹس، شہر بھر کے خاندانوں کی پسندیدہ۔',
|
||||
viewAll: 'سب دیکھیں',
|
||||
},
|
||||
howItWorks: {
|
||||
label: 'آرڈر',
|
||||
title: 'آرڈر کرنا کتنا آسان ہے',
|
||||
step1Title: 'ہم سے رابطہ کریں',
|
||||
step1Desc:
|
||||
'واٹس ایپ پر پیغام بھیجیں کہ آپ کیا چاہتے ہیں — ہم جلدی جواب دیتے ہیں۔',
|
||||
step2Title: 'ہم تصدیق کرتے ہیں',
|
||||
step2Desc:
|
||||
'ہم آپ کے آرڈر کی تصدیق کرتے ہیں، قیمت بتاتے ہیں اور بتاتے ہیں کہ کب تیار ہوگا۔',
|
||||
step3Title: 'وصول یا ڈیلیوری',
|
||||
step3Desc:
|
||||
'Tingvallavägen 11 پر دکان سے وصول کریں یا گھر کی ڈیلیوری منتخب کریں۔',
|
||||
step: 'مرحلہ {n}',
|
||||
whatsapp: 'واٹس ایپ پر آرڈر کریں',
|
||||
},
|
||||
offers: {
|
||||
label: 'آرڈر',
|
||||
title: 'ہفتہ وار پیشکشیں',
|
||||
subtitle:
|
||||
'ہم مسلسل تازہ پیشکشوں کے ساتھ اپ ڈیٹ کرتے ہیں۔ تازہ ترین قیمتوں کے لیے سوشل میڈیا پر فالو کریں۔',
|
||||
disclaimer: 'قیمت اس وقت تک جب تک اسٹاک موجود ہے',
|
||||
was: 'پہلے',
|
||||
now: 'اب',
|
||||
order: 'آرڈر',
|
||||
viewProduct: 'مصنوعات دیکھیں',
|
||||
badge: {
|
||||
fresh: 'تازہ',
|
||||
halal: 'حلال',
|
||||
},
|
||||
items: {
|
||||
chickenWings: { name: 'تازہ چکن ونگز PL' },
|
||||
lambSteak: { name: 'تازہ لیمب روست آئرلینڈ' },
|
||||
beefMince: { name: 'بیف منس 5% چکنائی IRL' },
|
||||
},
|
||||
},
|
||||
social: {
|
||||
label: 'ہمیں فالو کریں',
|
||||
title: 'ہمیں فالو کریں',
|
||||
subtitle:
|
||||
'فیس بک پر 1,100+ فالوورز · 249 پوسٹس · روزانہ اپ ڈیٹس',
|
||||
facebook: 'فیس بک',
|
||||
instagram: 'انسٹاگرام',
|
||||
whatsapp: 'واٹس ایپ',
|
||||
},
|
||||
contact: {
|
||||
label: 'رابطہ',
|
||||
title: 'ہم سے ملیں',
|
||||
addressLabel: 'پتہ',
|
||||
phoneLabel: 'فون',
|
||||
hoursLabel: 'اوقات',
|
||||
hoursValue: 'ہر دن: {hours}',
|
||||
writeUs: 'ہمیں لکھیں',
|
||||
callUs: 'ہمیں کال کریں',
|
||||
openMaps: 'گوگل میپس میں کھولیں',
|
||||
learnMore: 'رابطے کی تفصیلات',
|
||||
},
|
||||
cta: {
|
||||
title: 'پریمیم حلال گوشت کے لیے تیار ہیں؟',
|
||||
subtitle:
|
||||
'آج ہی آرڈر کریں اور حقیقی تازہ، حسب ضرورت حلال گوشت کا فرق محسوس کریں جو آپ کے دروازے تک پہنچایا جائے۔',
|
||||
button: 'ہمارا مجموعہ دیکھیں',
|
||||
whatsapp: 'واٹس ایپ پر آرڈر کریں',
|
||||
},
|
||||
shop: {
|
||||
title: 'تمام مصنوعات',
|
||||
subtitle: 'پریمیم حلال گوشت، آپ کی پسند کے مطابق',
|
||||
noProducts: 'کوئی مصنوعات نہیں ملیں',
|
||||
noProductsHint: 'اپنے فلٹرز یا تلاش کی کوشش کو ایڈجسٹ کریں',
|
||||
filters: 'فلٹرز',
|
||||
productsFound: '{count} مصنوعات ملیں',
|
||||
search: 'تلاش',
|
||||
searchPlaceholder: 'مصنوعات تلاش کریں...',
|
||||
category: 'زمرہ',
|
||||
sortBy: 'ترتیب دیں',
|
||||
all: 'سب',
|
||||
sortFeatured: 'نمایاں',
|
||||
sortPriceAsc: 'قیمت: کم سے زیادہ',
|
||||
sortPriceDesc: 'قیمت: زیادہ سے کم',
|
||||
sortName: 'نام الف سے ی',
|
||||
},
|
||||
product: {
|
||||
backToShop: 'خریداری پر واپس',
|
||||
inStock: 'دستیاب',
|
||||
outOfStock: 'اسٹاک ختم',
|
||||
halalTrust: '100% حلال تصدیق شدہ · روزانہ تازہ · پریمیم معیار',
|
||||
yourSelection: 'آپ کا انتخاب',
|
||||
aboutProduct: 'اس مصنوع کے بارے میں',
|
||||
addToCart: 'ٹوکری میں شامل کریں',
|
||||
addedToCart: 'ٹوکری میں شامل ہو گیا',
|
||||
decreaseQty: 'مقدار کم کریں',
|
||||
increaseQty: 'مقدار بڑھائیں',
|
||||
removeWishlist: 'پسندیدہ سے ہٹائیں',
|
||||
addWishlist: 'پسندیدہ میں شامل کریں',
|
||||
viewProduct: '{name} دیکھیں',
|
||||
pieces: '{count} ٹکڑے',
|
||||
standardCut: 'معیاری کٹ',
|
||||
howManyCuts: 'آپ کتنے کٹ چاہتے ہیں؟',
|
||||
howManyCutsHint: 'اپنے آرڈر کے لیے کٹوں کی تعداد منتخب کریں',
|
||||
cutsAndStyle: '{cuts} کٹ · {style}',
|
||||
selectCutting: 'کاٹنے کا انداز منتخب کریں',
|
||||
selectCuttingHint: 'ہمارے قصاب آپ کے {category} کو بالکل آپ کی پسند کے مطابق تیار کریں گے',
|
||||
fishNote:
|
||||
'ہم صرف منجمد مچھلی فروخت کرتے ہیں۔ تمام مچھلی ماخذ پر منجمد، پیشہ ورانہ پیک اور منجمد حالت میں فروخت — پکانے تک فریزر میں رکھیں۔',
|
||||
},
|
||||
cutting: {
|
||||
nihari: 'نہاری کٹ',
|
||||
karahi: 'کڑاہی کٹ',
|
||||
qeema: 'قیمہ (کима)',
|
||||
boneless: 'بغیر ہڈی',
|
||||
steak: 'اسٹیک کٹ',
|
||||
},
|
||||
priceUnit: {
|
||||
perBird: 'فی مرغی',
|
||||
perPack: 'فی پیک',
|
||||
perKg: 'فی کلو',
|
||||
},
|
||||
badges: {
|
||||
bestseller: 'سب سے زیادہ فروخت',
|
||||
chefsPick: 'شیف کی پسند',
|
||||
premium: 'پریمیم',
|
||||
popular: 'مقبول',
|
||||
freshCatch: 'تازہ پکڑ',
|
||||
frozen: 'منجمد',
|
||||
},
|
||||
products: {
|
||||
'chicken-whole': {
|
||||
name: 'پوری مرغی',
|
||||
description: 'فارم سے تازہ پوری حلال مرغی، بھوننے یا کڑی کے لیے بہترین۔',
|
||||
longDescription:
|
||||
'ہماری پوری مرغیاں تصدیق شدہ حلال فارموں سے حاصل کی جاتی ہیں اور بہترین تازگی میں پہنچائی جاتی ہیں۔ ہر پرندہ معیار کے لیے ہاتھ سے منتخب کیا جاتا ہے، نرم گوشت اور صاف پروسیسنگ کے ساتھ۔ اپنی پسندیدہ ٹکڑوں کی تعداد منتخب کریں اور ہم اسے بالکل ویسے تیار کریں گے جیسا آپ چاہیں۔',
|
||||
},
|
||||
'chicken-breast': {
|
||||
name: 'مرغی کا سینہ',
|
||||
description: 'دبلا، بغیر ہڈی کا مرغی کا سینہ — گرل اور صحت مند کھانوں کے لیے بہترین۔',
|
||||
longDescription:
|
||||
'پریمیم بغیر ہڈی کا مرغی کا سینہ، تراشا ہوا اور پکانے کے لیے تیار۔ کباب، سٹیر فرائی اور صحت مند ہفتے کی رات کے کھانوں کے لیے بہترین۔',
|
||||
},
|
||||
'chicken-thighs': {
|
||||
name: 'مرغی کی ران',
|
||||
description: 'کڑی اور گرل کے لیے بھرپور ذائقے والی رس بھرے حلال مرغی کی ران۔',
|
||||
longDescription:
|
||||
'ہماری مرغی کی ران اپنی رس اور گہرے ذائقے کے لیے مشہور ہے۔ چاہے روایتی کڑاہی بنائیں یا ہفتے کے آخر میں BBQ، یہ ران ہر بار بہترین نتائج دیتی ہے۔',
|
||||
},
|
||||
'chicken-wings': {
|
||||
name: 'مرغی کے بازو',
|
||||
description: 'تلی، بیکنگ یا گرل کے لیے پارٹی کے لیے تیار حلال مرغی کے بازو۔',
|
||||
longDescription:
|
||||
'کرکرے، ذائقے دار مرغی کے بازو حلال طریقے سے تیار اور تازہ پہنچائے جاتے ہیں۔ میچ کی راتوں اور خاندانی محفلوں کی پسندیدہ۔',
|
||||
},
|
||||
'beef-nihari': {
|
||||
name: 'نہاری کے لیے گائے کا گوشت',
|
||||
description: 'آہستہ پکانے کے لیے تیار گائے کے گوشت کے کٹس، روایتی نہاری کے لیے بہترین۔',
|
||||
longDescription:
|
||||
'خاص طور پر منتخب گائے کے گوشت کے کٹس جو آہستہ پکی ہوئی نہاری کے لیے بہترین ہیں۔ کولیجن اور ذائقے سے بھرپور، یہ کٹس گھنٹوں کی دھیمی آنچ پر خوبصورتی سے گل جاتے ہیں۔',
|
||||
},
|
||||
'beef-steak': {
|
||||
name: 'پریمیم گائے کا اسٹیک',
|
||||
description: 'ریستوران معیار کے حلال اسٹیک کٹس، بہترین سیک کے لیے۔',
|
||||
longDescription:
|
||||
'بہترین حلال ذرائع سے ہاتھ سے کاٹے گئے پریمیم اسٹیک۔ چربیلے، نرم اور آپ کی گرل یا کاسٹ آئرن پین کے لیے تیار۔',
|
||||
},
|
||||
'beef-mince': {
|
||||
name: 'گائے کا قیمہ',
|
||||
description: 'کباب، برگر اور قیمہ کے لیے تازہ حلال گائے کا قیمہ۔',
|
||||
longDescription:
|
||||
'باریک پیسا ہوا حلال گائے کا قیمہ بہترین چربی کے تناسب کے ساتھ رسیلے کباب، ذائقے دار قیمہ اور گھریلو برگرز کے لیے۔ روزانہ تازہ پیسا جاتا ہے۔',
|
||||
},
|
||||
'beef-boneless': {
|
||||
name: 'بغیر ہڈی کے گائے کے مکعب',
|
||||
description: 'کڑاہی، بریانی اور اسٹیو کے لیے کثیر الاستعمال بغیر ہڈی کے مکعب۔',
|
||||
longDescription:
|
||||
'یکساں بغیر ہڈی کے گائے کے مکعب تیز پکوان کے لیے کامل سائز میں کاٹے گئے۔ کڑاہی، پلاؤ اور سٹیر فرائی کے لیے بہترین۔',
|
||||
},
|
||||
'lamb-shoulder': {
|
||||
name: 'بکرے کا کندھا',
|
||||
description: 'آہستہ بھوننے اور کڑی کے لیے بھرپور ذائقے والا بکرے کا کندھا۔',
|
||||
longDescription:
|
||||
'خوبصورت چربی کے نمونے والے پریمیم حلال بکرے کا کندھا۔ آہستہ بھونی ہوئی دعوتوں، بھرپور کڑیوں اور روایتی خاندانی کھانوں کے لیے بہترین۔',
|
||||
},
|
||||
'lamb-leg': {
|
||||
name: 'بکرے کی ٹانگ',
|
||||
description: 'بھوننے، گرل اور خاص مواقع کے لیے نرم بکرے کی ٹانگ۔',
|
||||
longDescription:
|
||||
'تصدیق شدہ حلال ذرائع سے پوری یا حصوں میں بکرے کی ٹانگ۔ عید کی تقریبات، ڈنر پارٹیوں اور اتوار کی بھوننے کے لیے مرکزی کٹ۔',
|
||||
},
|
||||
'lamb-chops': {
|
||||
name: 'بکرے کے کٹlets',
|
||||
description: 'گرل اور گھر میں فائن ڈائننگ کے لیے پریمیم بکرے کے کٹlets۔',
|
||||
longDescription:
|
||||
'موٹے کاٹے گئے حلال بکرے کے کٹlets گرل کے لیے بہترین چربی کی تہہ کے ساتھ۔ ریستوران معیار، آپ کے باورچی خانے تک پہنچایا گیا۔',
|
||||
},
|
||||
'lamb-mince': {
|
||||
name: 'بکرے کا قیمہ',
|
||||
description: 'کباب، سموسے اور قیمہ کے لیے تازہ حلال بکرے کا قیمہ۔',
|
||||
longDescription:
|
||||
'باریک پیسا ہوا بکرے کا قیمہ بھرپور ذائقے کے ساتھ۔ سیخ کباب، بکرے کا قیمہ اور بھرے ہوئے پراٹھوں کے لیے ضروری۔',
|
||||
},
|
||||
'fish-salmon': {
|
||||
name: 'منجمد اٹلانٹک سامن فلیٹ',
|
||||
description: 'منجمد سامن فلیٹس، ویکیوم پیک — گھر پر پگھلائیں اور پین میں سیکیں۔',
|
||||
longDescription:
|
||||
'پریمیم منجمد اٹلانٹک سامن فلیٹس، اومیگا 3 سے بھرپور۔ ماخذ پر منجمد، صاف، حصوں میں تقسیم اور ویکیوم سیلڈ۔ پکانے تک منجمد رکھیں۔',
|
||||
},
|
||||
'fish-rohu': {
|
||||
name: 'منجمد روہو مچھلی',
|
||||
description: 'منجمد پوری روہو — جنوبی ایشیائی پسندیدہ، صرف منجمد فروخت۔',
|
||||
longDescription:
|
||||
'منجمد روہو مچھلی، جنوبی ایشیائی کھانوں کا بنیادی جزو۔ ماخذ پر منجمد اور فریزر کے لیے پیک۔ مچھلی کی کڑی اور روایتی ترکیبوں سے پہلے پگھلائیں۔',
|
||||
},
|
||||
'fish-prawns': {
|
||||
name: 'منجمد بڑی جھینگے',
|
||||
description: 'منجمد بڑی جھینگے — پگھلانے کے بعد کڑی، بریانی اور گرل کے لیے۔',
|
||||
longDescription:
|
||||
'پریمیم منجمد بڑی جھینگے، صرف منجمد فروخت۔ میٹھا، مضبوط گوشت — کڑی اور تندوری سے پہلے مکمل پگھلائیں۔',
|
||||
},
|
||||
'fish-basa': {
|
||||
name: 'منجمد باسا فلیٹ',
|
||||
description: 'منجمد ہلکے باسا فلیٹس — پورے خاندان کے لیے آسان پگھلانا اور پکانا۔',
|
||||
longDescription:
|
||||
'منجمد بغیر ہڈی کے باسا فلیٹس، ہلکے ذائقے کے ساتھ۔ صرف منجمد فروخت — بیکنگ اور ہلکی کڑی سے پہلے پگھلائیں۔',
|
||||
},
|
||||
},
|
||||
cart: {
|
||||
title: 'آپ کی ٹوکری',
|
||||
itemsCount: 'آپ کی ٹوکری میں {count} آئٹم',
|
||||
empty: 'آپ کی ٹوکری خالی ہے',
|
||||
emptyHint: 'ہمارے پریمیم حلال مجموعے میں سے دیکھیں اور آئٹمز شامل کریں۔',
|
||||
startShopping: 'خریداری شروع کریں',
|
||||
customization: 'حسب ضرورت:',
|
||||
orderSummary: 'آرڈر کا خلاصہ',
|
||||
subtotal: 'ذیلی کل',
|
||||
delivery: 'ڈیلیوری',
|
||||
free: 'مفت',
|
||||
freeDeliveryHint: '500 کرون سے زیادہ کے آرڈرز پر مفت ڈیلیوری',
|
||||
total: 'کل',
|
||||
proceedCheckout: 'چیک آؤٹ پر جائیں',
|
||||
continueShopping: 'خریداری جاری رکھیں',
|
||||
removeItem: 'آئٹم ہٹائیں',
|
||||
},
|
||||
checkout: {
|
||||
title: 'محفوظ چیک آؤٹ',
|
||||
backToCart: 'ٹوکری پر واپس',
|
||||
noItems: 'چیک آؤٹ کے لیے کوئی آئٹم نہیں',
|
||||
goToShop: 'خریداری پر جائیں',
|
||||
orderConfirmed: 'آرڈر کی تصدیق ہو گئی!',
|
||||
thankYou: 'آپ کے آرڈر کا شکریہ۔ آپ کا پریمیم حلال گوشت تیار کیا جا رہا ہے۔',
|
||||
orderId: 'آرڈر آئی ڈی: {id}',
|
||||
viewOrders: 'آرڈرز دیکھیں',
|
||||
haveAccount: 'اکاؤنٹ ہے؟',
|
||||
signIn: 'سائن ان کریں',
|
||||
fasterCheckout: 'تیز چیک آؤٹ کے لیے۔',
|
||||
deliveryDetails: 'ڈیلیوری کی تفصیلات',
|
||||
fullName: 'پورا نام',
|
||||
email: 'ای میل',
|
||||
phone: 'فون',
|
||||
street: 'گلی کا پتہ',
|
||||
city: 'شہر',
|
||||
state: 'صوبہ',
|
||||
zip: 'پوسٹل کوڈ',
|
||||
payment: 'ادائیگی',
|
||||
creditCard: 'کریڈٹ کارڈ',
|
||||
cashOnDelivery: 'ڈیلیوری پر نقد',
|
||||
cardNumber: 'کارڈ نمبر',
|
||||
expiry: 'میعاد',
|
||||
cvv: 'CVV',
|
||||
qty: 'مقدار: {count}',
|
||||
processing: 'پروسیسنگ...',
|
||||
pay: '{amount} ادا کریں',
|
||||
secure: 'محفوظ 256 بٹ SSL انکرپشن',
|
||||
},
|
||||
auth: {
|
||||
welcomeBack: 'خوش آمدید',
|
||||
createAccount: 'اکاؤنٹ بنائیں',
|
||||
joinTagline: 'پریمیم شاپنگ کے تجربے کے لیے {name} میں شامل ہوں',
|
||||
signInTagline: 'اپنے {name} اکاؤنٹ میں سائن ان کریں',
|
||||
fullName: 'پورا نام',
|
||||
email: 'ای میل',
|
||||
password: 'پاس ورڈ',
|
||||
phone: 'فون',
|
||||
street: 'گلی کا پتہ',
|
||||
city: 'شہر',
|
||||
state: 'صوبہ',
|
||||
zip: 'پوسٹل کوڈ',
|
||||
signIn: 'سائن ان',
|
||||
register: 'اکاؤنٹ بنائیں',
|
||||
hasAccount: 'پہلے سے اکاؤنٹ ہے؟ سائن ان کریں',
|
||||
noAccount: 'اکاؤنٹ نہیں ہے؟ رجسٹر کریں',
|
||||
invalidCredentials: 'غلط ای میل یا پاس ورڈ۔ {email} / demo123 آزمائیں',
|
||||
demo: 'ڈیمو: {email} / demo123',
|
||||
},
|
||||
account: {
|
||||
title: 'میرا اکاؤنٹ',
|
||||
welcome: 'خوش آمدید، {name}',
|
||||
memberSince: '{date} سے رکن',
|
||||
myWishlist: 'میری پسندیدہ فہرست',
|
||||
signOut: 'سائن آؤٹ',
|
||||
orderHistory: 'آرڈر کی تاریخ',
|
||||
noOrders: 'ابھی تک کوئی آرڈر نہیں',
|
||||
startShopping: 'خریداری شروع کریں',
|
||||
},
|
||||
wishlist: {
|
||||
title: 'میری پسندیدہ فہرست',
|
||||
saved: '{count} محفوظ آئٹم',
|
||||
empty: 'آپ کی پسندیدہ فہرست خالی ہے',
|
||||
emptyHint: 'اپنی پسندیدہ مصنوعات محفوظ کریں تاکہ بعد میں خرید سکیں۔',
|
||||
browse: 'مصنوعات دیکھیں',
|
||||
},
|
||||
about: {
|
||||
title: '{name} کے بارے میں',
|
||||
subtitle:
|
||||
'پریمیم 100% حلال گوشت آپ کی میز تک — تازہ، حسب ضرورت اور احتیاط سے پہنچایا گیا۔',
|
||||
ourStory: 'ہماری کہانی',
|
||||
storyP1:
|
||||
'{name} ایک سادہ مشن کے ساتھ قائم ہوا: ہر خاندان کے لیے پریمیم حلال گوشت قابل رسائی بنانا، معیار، تازگی یا مذہبی تعمیل سے سمجھوتہ کیے بغیر۔ ہم سمجھتے ہیں کہ بہت سے گھروں کے لیے صحیح کٹ صحیح طریقے سے تیار کرنا عیش نہیں — یہ ضروری ہے۔',
|
||||
storyP2:
|
||||
'مرغی کے لیے ٹکڑوں کی تعداد منتخب کرنے سے لے کر گائے اور بکرے کے گوشت کے لیے نہاری یا کڑاہی کٹس کا انتخاب کرنے تک، ہم ہر آرڈر کے مرکز میں حسب ضرورت بناتے ہیں۔ ہمارے ماہر قصاب ہر آرڈر ہاتھ سے تیار کرتے ہیں، اور ہماری درجہ حرارت کنٹرولڈ ڈیلیوری یقینی بناتی ہے کہ آپ کا گوشت اسی دن کی طرح تازہ پہنچے جیسے کاٹا گیا تھا۔',
|
||||
halalTitle: 'حلال سرٹیفیکیشن',
|
||||
halalDesc:
|
||||
'{name} کی ہر مصنوع تصدیق شدہ حلال سپلائرز سے حاصل کی جاتی ہے۔ ہمارا سپلائی چین مکمل طور پر قابل سراغ ہے، اور ہم حلال ذبح اور پروسیسنگ کے معیارات کی سخت تعمیل برقرار رکھتے ہیں۔',
|
||||
freshDaily: 'روزانہ تازہ',
|
||||
freshDailyDesc: 'ہر صبح قابل اعتماد فارموں سے حاصل',
|
||||
premiumQuality: 'پریمیم معیار',
|
||||
premiumQualityDesc: 'ماہر قصابوں کے ذریعے منتخب کردہ کٹس',
|
||||
fastDelivery: 'تیز ڈیلیوری',
|
||||
fastDeliveryDesc: 'درجہ حرارت کنٹرولڈ اسی دن کی ڈیلیوری',
|
||||
deliveryTitle: 'ڈیلیوری کی معلومات',
|
||||
delivery1: 'ہم اپنی پروسیسنگ سہولت سے 40 کلومیٹر کے دائرے میں ڈیلیوری کرتے ہیں۔',
|
||||
delivery2: 'دوپہر 2 بجے سے پہلے کے آرڈرز اسی دن کی ڈیلیوری کے اہل ہیں۔',
|
||||
delivery3: '500 کرون سے زیادہ کے آرڈرز پر مفت ڈیلیوری۔ معیاری ڈیلیوری فیس: 49 کرون۔',
|
||||
delivery4: 'تمام مصنوعات ویکیوم سیلڈ اور انسولیٹڈ پیکجنگ میں منتقل کی جاتی ہیں۔',
|
||||
contactTitle: 'ہم سے رابطہ کریں',
|
||||
address: 'Tingvallavägen 11, 195 31 Märsta',
|
||||
privacyTitle: 'رازداری کی پالیسی',
|
||||
privacyText:
|
||||
'ہم صرف وہ معلومات جمع کرتے ہیں جو آپ کے آرڈرز پر کارروائی کے لیے ضروری ہیں — نام، رابطے کی تفصیلات اور ڈیلیوری کا پتہ۔ ہم آپ کا ڈیٹا تیسرے فریق کو نہیں بیچتے۔ ادائیگی کی تفصیلات محفوظ طریقے سے ہمارے پارٹنرز کے ذریعے سنبھالی جاتی ہیں۔',
|
||||
termsTitle: 'سروس کی شرائط',
|
||||
termsText:
|
||||
'تمام قیمتیں SEK میں دکھائی جاتی ہیں اور بغیر اطلاع کے تبدیل ہو سکتی ہیں۔ آرڈرز دستیابی پر منحصر ہیں۔ تمام گوشت کی مصنوعات پر حلال سرٹیفیکیشن لاگو ہوتا ہے۔ ڈیلیوری کے اوقات تخمینے ہیں اور مصروف اوقات میں مختلف ہو سکتے ہیں۔',
|
||||
},
|
||||
footer: {
|
||||
tagline:
|
||||
'پریمیم 100% حلال گوشت ڈیلیوری۔ تازہ، حسب ضرورت کٹس آپ کے دروازے تک بغیر کسی سمجھوتے کے معیار کے ساتھ پہنچائی جاتی ہیں۔',
|
||||
shop: 'خریداری',
|
||||
company: 'کمپنی',
|
||||
contact: 'رابطہ',
|
||||
rights: 'جملہ حقوق محفوظ ہیں۔',
|
||||
phone: '072-585 50 50',
|
||||
hours: 'ہر دن کھلا {hours}',
|
||||
},
|
||||
notFound: {
|
||||
title: '404',
|
||||
message: 'صفحہ نہیں ملا',
|
||||
goHome: 'ہوم پر جائیں',
|
||||
},
|
||||
orderStatus: {
|
||||
pending: 'زیر التوا',
|
||||
confirmed: 'تصدیق شدہ',
|
||||
preparing: 'تیاری میں',
|
||||
'out-for-delivery': 'ڈیلیوری کے لیے روانہ',
|
||||
delivered: 'پہنچا دیا گیا',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export type Locale = 'en' | 'sv' | 'ur' | 'ar' | 'fa' | 'tr';
|
||||
|
||||
export const LOCALES: { code: Locale; label: string; nativeLabel: string }[] = [
|
||||
{ code: 'sv', label: 'Swedish', nativeLabel: 'Svenska' },
|
||||
{ code: 'en', label: 'English', nativeLabel: 'English' },
|
||||
{ code: 'ur', label: 'Urdu', nativeLabel: 'اردو' },
|
||||
{ code: 'ar', label: 'Arabic', nativeLabel: 'العربية' },
|
||||
{ code: 'fa', label: 'Persian', nativeLabel: 'فارسی' },
|
||||
{ code: 'tr', label: 'Turkish', nativeLabel: 'Türkçe' },
|
||||
];
|
||||
|
||||
export const DEFAULT_LOCALE: Locale = 'sv';
|
||||
|
||||
export interface TranslationDict {
|
||||
[key: string]: string | TranslationDict;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Order } from '@/domain/entities';
|
||||
import { CustomizationDomainService } from '@/domain/services/CustomizationDomainService';
|
||||
import { PRODUCT_CATALOG } from './products.data';
|
||||
|
||||
function orderItem(
|
||||
productId: string,
|
||||
quantity: number,
|
||||
customizationLabel: string
|
||||
) {
|
||||
const product = PRODUCT_CATALOG.find((p) => p.id === productId)!;
|
||||
const customization = CustomizationDomainService.getDefaultCustomization(
|
||||
product.category
|
||||
);
|
||||
return {
|
||||
id: CustomizationDomainService.getCartItemKey(productId, customization),
|
||||
product,
|
||||
quantity,
|
||||
customization,
|
||||
customizationLabel,
|
||||
};
|
||||
}
|
||||
|
||||
const demoAddress = {
|
||||
street: 'Tingvallavägen 11',
|
||||
city: 'Märsta',
|
||||
state: 'Stockholm',
|
||||
zip: '195 31',
|
||||
};
|
||||
|
||||
export const DEMO_ORDERS: Order[] = [
|
||||
{
|
||||
id: 'KG-2026-0042',
|
||||
items: [
|
||||
orderItem('beef-steak', 2, '8 cuts · Karahi'),
|
||||
orderItem('lamb-shoulder', 1, '8 cuts · Nihari'),
|
||||
],
|
||||
total: 647,
|
||||
status: 'delivered',
|
||||
createdAt: '2026-06-10T14:30:00.000Z',
|
||||
deliveryAddress: demoAddress,
|
||||
paymentMethod: 'card',
|
||||
},
|
||||
{
|
||||
id: 'KG-2026-0051',
|
||||
items: [
|
||||
orderItem('chicken-whole', 2, '8 cuts · Karahi'),
|
||||
orderItem('beef-mince', 1, '8 cuts · Qeema'),
|
||||
orderItem('fish-prawns', 1, 'Standard cut'),
|
||||
],
|
||||
total: 556,
|
||||
status: 'out-for-delivery',
|
||||
createdAt: '2026-06-16T09:15:00.000Z',
|
||||
deliveryAddress: demoAddress,
|
||||
paymentMethod: 'swish',
|
||||
},
|
||||
{
|
||||
id: 'KG-2026-0058',
|
||||
items: [
|
||||
orderItem('lamb-leg', 1, '8 cuts · Steak'),
|
||||
orderItem('fish-salmon', 1, 'Standard cut'),
|
||||
],
|
||||
total: 418,
|
||||
status: 'preparing',
|
||||
createdAt: '2026-06-17T08:00:00.000Z',
|
||||
deliveryAddress: demoAddress,
|
||||
paymentMethod: 'card',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
import { getProductImages } from '@/infrastructure/images';
|
||||
|
||||
type ProductDefinition = Omit<Product, 'image' | 'images'>;
|
||||
|
||||
const PRODUCT_DEFINITIONS: ProductDefinition[] = [
|
||||
{
|
||||
id: 'chicken-whole',
|
||||
slug: 'whole-chicken',
|
||||
category: 'chicken',
|
||||
price: 129,
|
||||
priceUnitKey: 'perBird',
|
||||
badgeKey: 'bestseller',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '1.2–1.5 kg',
|
||||
tags: ['fresh', 'whole', 'popular'],
|
||||
},
|
||||
{
|
||||
id: 'chicken-breast',
|
||||
slug: 'chicken-breast',
|
||||
category: 'chicken',
|
||||
price: 99,
|
||||
priceUnitKey: 'perPack',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '500g–1kg',
|
||||
tags: ['fresh', 'boneless', 'lean'],
|
||||
},
|
||||
{
|
||||
id: 'chicken-thighs',
|
||||
slug: 'chicken-thighs',
|
||||
category: 'chicken',
|
||||
price: 85,
|
||||
priceUnitKey: 'perPack',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '500g–1kg',
|
||||
tags: ['fresh', 'juicy'],
|
||||
},
|
||||
{
|
||||
id: 'chicken-wings',
|
||||
slug: 'chicken-wings',
|
||||
category: 'chicken',
|
||||
price: 79,
|
||||
priceUnitKey: 'perPack',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
tags: ['fresh', 'party'],
|
||||
},
|
||||
{
|
||||
id: 'beef-nihari',
|
||||
slug: 'beef-for-nihari',
|
||||
category: 'beef',
|
||||
price: 149,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'chefsPick',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '1 kg',
|
||||
tags: ['fresh', 'traditional'],
|
||||
},
|
||||
{
|
||||
id: 'beef-steak',
|
||||
slug: 'premium-beef-steak',
|
||||
category: 'beef',
|
||||
price: 229,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'premium',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '1 kg',
|
||||
tags: ['premium', 'steak'],
|
||||
},
|
||||
{
|
||||
id: 'beef-mince',
|
||||
slug: 'beef-mince',
|
||||
category: 'beef',
|
||||
price: 119,
|
||||
priceUnitKey: 'perKg',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '500g–1kg',
|
||||
tags: ['fresh', 'mince'],
|
||||
},
|
||||
{
|
||||
id: 'beef-boneless',
|
||||
slug: 'boneless-beef-cubes',
|
||||
category: 'beef',
|
||||
price: 169,
|
||||
priceUnitKey: 'perKg',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '1 kg',
|
||||
tags: ['fresh', 'boneless'],
|
||||
},
|
||||
{
|
||||
id: 'lamb-shoulder',
|
||||
slug: 'lamb-shoulder',
|
||||
category: 'lamb',
|
||||
price: 189,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'popular',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '1 kg',
|
||||
tags: ['fresh', 'traditional'],
|
||||
},
|
||||
{
|
||||
id: 'lamb-leg',
|
||||
slug: 'lamb-leg',
|
||||
category: 'lamb',
|
||||
price: 219,
|
||||
priceUnitKey: 'perKg',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '1–2 kg',
|
||||
tags: ['premium', 'roast'],
|
||||
},
|
||||
{
|
||||
id: 'lamb-chops',
|
||||
slug: 'lamb-chops',
|
||||
category: 'lamb',
|
||||
price: 249,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'premium',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '500g',
|
||||
tags: ['premium', 'grill'],
|
||||
},
|
||||
{
|
||||
id: 'lamb-mince',
|
||||
slug: 'lamb-mince',
|
||||
category: 'lamb',
|
||||
price: 159,
|
||||
priceUnitKey: 'perKg',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '500g–1kg',
|
||||
tags: ['fresh', 'mince'],
|
||||
},
|
||||
{
|
||||
id: 'fish-salmon',
|
||||
slug: 'atlantic-salmon-fillet',
|
||||
category: 'fish',
|
||||
price: 199,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'frozen',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '500g',
|
||||
tags: ['frozen', 'fillet'],
|
||||
},
|
||||
{
|
||||
id: 'fish-rohu',
|
||||
slug: 'rohu-fish',
|
||||
category: 'fish',
|
||||
price: 119,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'frozen',
|
||||
inStock: true,
|
||||
featured: true,
|
||||
weight: '1–1.5 kg',
|
||||
tags: ['frozen', 'whole'],
|
||||
},
|
||||
{
|
||||
id: 'fish-prawns',
|
||||
slug: 'jumbo-prawns',
|
||||
category: 'fish',
|
||||
price: 179,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'frozen',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '500g',
|
||||
tags: ['frozen', 'seafood'],
|
||||
},
|
||||
{
|
||||
id: 'fish-basa',
|
||||
slug: 'basa-fillet',
|
||||
category: 'fish',
|
||||
price: 109,
|
||||
priceUnitKey: 'perKg',
|
||||
badgeKey: 'frozen',
|
||||
inStock: true,
|
||||
featured: false,
|
||||
weight: '500g',
|
||||
tags: ['frozen', 'fillet', 'mild'],
|
||||
},
|
||||
];
|
||||
|
||||
/** Images are attached from the central catalog — not duplicated here */
|
||||
export const PRODUCT_CATALOG: Product[] = PRODUCT_DEFINITIONS.map((product) => ({
|
||||
...product,
|
||||
...getProductImages(product.id),
|
||||
}));
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ITranslationService } from '@/application/ports/ITranslationService';
|
||||
import { createTranslator } from '@/i18n';
|
||||
import { Locale } from '@/i18n/types';
|
||||
|
||||
/**
|
||||
* INFRASTRUCTURE — Wraps existing i18n dictionaries behind a port
|
||||
*/
|
||||
export class I18nTranslationService implements ITranslationService {
|
||||
constructor(private readonly locale: Locale) {}
|
||||
|
||||
translate(
|
||||
path: string,
|
||||
params?: Record<string, string | number>
|
||||
): string {
|
||||
const t = createTranslator(this.locale);
|
||||
return t(path, params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
* SITE IMAGE CATALOG
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* ALL image files live in ONE folder on disk:
|
||||
*
|
||||
* public/images/site/
|
||||
*
|
||||
* To change an image: replace the file in that folder (same filename).
|
||||
* You do NOT need to edit this file unless you add a NEW product or page.
|
||||
*
|
||||
* Filename guide — see public/images/site/README.txt
|
||||
*/
|
||||
|
||||
import type { Category } from '@/domain/entities';
|
||||
|
||||
/** Single folder — every website image file is here */
|
||||
export const IMAGE_FOLDER = '/images/site';
|
||||
|
||||
/** Build path to a file in the site image folder */
|
||||
export function siteImage(filename: string): string {
|
||||
return `${IMAGE_FOLDER}/${filename}`;
|
||||
}
|
||||
|
||||
export type ProductImageId =
|
||||
| 'chicken-whole'
|
||||
| 'chicken-breast'
|
||||
| 'chicken-thighs'
|
||||
| 'chicken-wings'
|
||||
| 'beef-nihari'
|
||||
| 'beef-steak'
|
||||
| 'beef-mince'
|
||||
| 'beef-boneless'
|
||||
| 'lamb-shoulder'
|
||||
| 'lamb-leg'
|
||||
| 'lamb-chops'
|
||||
| 'lamb-mince'
|
||||
| 'fish-salmon'
|
||||
| 'fish-rohu'
|
||||
| 'fish-prawns'
|
||||
| 'fish-basa';
|
||||
|
||||
export type OfferImageId =
|
||||
| 'chicken-wings-pl'
|
||||
| 'lamb-steak-ireland'
|
||||
| 'beef-mince-irl';
|
||||
|
||||
type ImageRef =
|
||||
| { type: 'product'; id: ProductImageId }
|
||||
| { type: 'category'; id: Category };
|
||||
|
||||
interface ProductImageEntry {
|
||||
main: string;
|
||||
gallery?: string[];
|
||||
}
|
||||
|
||||
function product(id: ProductImageId, second?: string): ProductImageEntry {
|
||||
const main = siteImage(`${id}.jpg`);
|
||||
if (second) {
|
||||
return { main, gallery: [main, siteImage(second)] };
|
||||
}
|
||||
return { main };
|
||||
}
|
||||
|
||||
/** ─── Brand ─── */
|
||||
export const BRAND_IMAGES = {
|
||||
logo: siteImage('logo.jpeg'),
|
||||
} as const;
|
||||
|
||||
/** ─── Page backgrounds ─── */
|
||||
export const PAGE_IMAGES = {
|
||||
hero: siteImage('hero.jpg'),
|
||||
aboutMeat: siteImage('about.jpg'),
|
||||
weeklyOffersBanner: siteImage('weekly-offers.jpg'),
|
||||
} as const;
|
||||
|
||||
/** ─── Category tiles ─── */
|
||||
export const CATEGORY_IMAGES: Record<Category, string> = {
|
||||
chicken: siteImage('category-chicken.jpg'),
|
||||
beef: siteImage('category-beef.jpg'),
|
||||
lamb: siteImage('category-lamb.jpg'),
|
||||
fish: siteImage('category-fish.jpg'),
|
||||
};
|
||||
|
||||
/** ─── Product photos (filename = product id) ─── */
|
||||
export const PRODUCT_IMAGES: Record<ProductImageId, ProductImageEntry> = {
|
||||
'chicken-whole': product('chicken-whole', 'chicken-whole-2.jpg'),
|
||||
'chicken-breast': product('chicken-breast', 'chicken-breast-2.jpg'),
|
||||
'chicken-thighs': product('chicken-thighs'),
|
||||
'chicken-wings': product('chicken-wings'),
|
||||
'beef-nihari': product('beef-nihari', 'beef-nihari-2.jpg'),
|
||||
'beef-steak': product('beef-steak', 'beef-steak-2.jpg'),
|
||||
'beef-mince': product('beef-mince'),
|
||||
'beef-boneless': product('beef-boneless'),
|
||||
'lamb-shoulder': product('lamb-shoulder', 'lamb-shoulder-2.jpg'),
|
||||
'lamb-leg': product('lamb-leg'),
|
||||
'lamb-chops': product('lamb-chops'),
|
||||
'lamb-mince': product('lamb-mince'),
|
||||
'fish-salmon': product('fish-salmon', 'fish-salmon-2.jpg'),
|
||||
'fish-rohu': product('fish-rohu'),
|
||||
'fish-prawns': product('fish-prawns'),
|
||||
'fish-basa': product('fish-basa'),
|
||||
};
|
||||
|
||||
/** ─── Weekly offers (reuse product/category images) ─── */
|
||||
export const OFFER_IMAGE_REFS: Record<OfferImageId, ImageRef> = {
|
||||
'chicken-wings-pl': { type: 'product', id: 'chicken-wings' },
|
||||
'lamb-steak-ireland': { type: 'category', id: 'lamb' },
|
||||
'beef-mince-irl': { type: 'product', id: 'beef-mince' },
|
||||
};
|
||||
|
||||
export const IMAGE_QUALITY = 90;
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Public API for the site image catalog.
|
||||
* Import from here (or @/lib/images re-export) everywhere you need an image URL.
|
||||
*/
|
||||
import type { Category } from '@/domain/entities';
|
||||
import {
|
||||
IMAGE_FOLDER,
|
||||
siteImage,
|
||||
BRAND_IMAGES,
|
||||
PAGE_IMAGES,
|
||||
CATEGORY_IMAGES,
|
||||
PRODUCT_IMAGES,
|
||||
OFFER_IMAGE_REFS,
|
||||
IMAGE_QUALITY,
|
||||
type ProductImageId,
|
||||
type OfferImageId,
|
||||
} from './catalog';
|
||||
|
||||
export {
|
||||
IMAGE_FOLDER,
|
||||
siteImage,
|
||||
BRAND_IMAGES,
|
||||
PAGE_IMAGES,
|
||||
CATEGORY_IMAGES,
|
||||
PRODUCT_IMAGES,
|
||||
OFFER_IMAGE_REFS,
|
||||
IMAGE_QUALITY,
|
||||
type ProductImageId,
|
||||
type OfferImageId,
|
||||
};
|
||||
|
||||
/** Resolved product image fields for Product entity */
|
||||
export function getProductImages(productId: string): {
|
||||
image: string;
|
||||
images: string[];
|
||||
} {
|
||||
const entry = PRODUCT_IMAGES[productId as ProductImageId];
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`[site-images] Missing product image for "${productId}". Add it to infrastructure/images/catalog.ts`
|
||||
);
|
||||
}
|
||||
return {
|
||||
image: entry.main,
|
||||
images: entry.gallery ?? [entry.main],
|
||||
};
|
||||
}
|
||||
|
||||
/** Single URL for a weekly offer card */
|
||||
export function getOfferImage(offerId: string): string {
|
||||
const ref = OFFER_IMAGE_REFS[offerId as OfferImageId];
|
||||
if (!ref) {
|
||||
throw new Error(
|
||||
`[site-images] Missing offer image ref for "${offerId}". Add it to infrastructure/images/catalog.ts`
|
||||
);
|
||||
}
|
||||
if (ref.type === 'product') {
|
||||
return getProductImages(ref.id).image;
|
||||
}
|
||||
return CATEGORY_IMAGES[ref.id];
|
||||
}
|
||||
|
||||
/** Category tile image */
|
||||
export function getCategoryImage(category: Category): string {
|
||||
return CATEGORY_IMAGES[category];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy shape used across components — built from the catalog above.
|
||||
* Prefer getProductImages / PAGE_IMAGES / BRAND_IMAGES in new code.
|
||||
*/
|
||||
export const IMAGES = {
|
||||
logo: BRAND_IMAGES.logo,
|
||||
hero: PAGE_IMAGES.hero,
|
||||
aboutMeat: PAGE_IMAGES.aboutMeat,
|
||||
categories: CATEGORY_IMAGES,
|
||||
products: Object.fromEntries(
|
||||
Object.entries(PRODUCT_IMAGES).map(([id, entry]) => [id, entry.main])
|
||||
) as Record<ProductImageId, string>,
|
||||
} as const;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { Address, Order, User } from '@/domain/entities';
|
||||
import { AuthDomainService, RegisterInput } from '@/domain/services/AuthDomainService';
|
||||
import { IAuthRepository } from '@/application/ports/IAuthRepository';
|
||||
import { DEMO_EMAIL } from '@/lib/constants';
|
||||
import { DEMO_ORDERS } from '@/infrastructure/data/demo-orders.data';
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
orders: Order[];
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
interface AuthInternalActions {
|
||||
_patch: (patch: Partial<AuthState>) => void;
|
||||
}
|
||||
|
||||
const DEMO_USER: User = {
|
||||
id: 'demo-1',
|
||||
name: 'Ahmed Khan',
|
||||
email: DEMO_EMAIL,
|
||||
phone: '+46 72 585 50 50',
|
||||
address: {
|
||||
street: 'Tingvallavägen 11',
|
||||
city: 'Märsta',
|
||||
state: 'Stockholm',
|
||||
zip: '195 31',
|
||||
},
|
||||
createdAt: '2025-03-15T10:00:00.000Z',
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthState & AuthInternalActions>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: null,
|
||||
orders: [],
|
||||
isAuthenticated: false,
|
||||
_patch: (patch) => set(patch),
|
||||
}),
|
||||
{ name: 'kott-gard-auth' }
|
||||
)
|
||||
);
|
||||
|
||||
export class ZustandAuthRepository implements IAuthRepository {
|
||||
private getState() {
|
||||
return useAuthStore.getState();
|
||||
}
|
||||
|
||||
getUser(): User | null {
|
||||
return this.getState().user;
|
||||
}
|
||||
|
||||
getOrders(): Order[] {
|
||||
return this.getState().orders;
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.getState().isAuthenticated;
|
||||
}
|
||||
|
||||
login(email: string, password: string): boolean {
|
||||
const state = this.getState();
|
||||
|
||||
if (
|
||||
email === DEMO_EMAIL &&
|
||||
AuthDomainService.isDemoCredentials(email, password)
|
||||
) {
|
||||
state._patch({
|
||||
user: DEMO_USER,
|
||||
isAuthenticated: true,
|
||||
orders:
|
||||
state.orders.length > 0 ? state.orders : DEMO_ORDERS,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (AuthDomainService.emailMatchesStoredUser(state.user, email)) {
|
||||
state._patch({ isAuthenticated: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
register(data: RegisterInput): boolean {
|
||||
const user = AuthDomainService.createUser(data);
|
||||
this.getState()._patch({ user, isAuthenticated: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.getState()._patch({ isAuthenticated: false });
|
||||
}
|
||||
|
||||
updateProfile(data: Partial<User>): void {
|
||||
const current = this.getState().user;
|
||||
if (current) {
|
||||
this.getState()._patch({ user: { ...current, ...data } });
|
||||
}
|
||||
}
|
||||
|
||||
addOrder(order: Order): void {
|
||||
const orders = this.getState().orders;
|
||||
this.getState()._patch({ orders: [order, ...orders] });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { CartItem, Product, ProductCustomization } from '@/domain/entities';
|
||||
import { CartDomainService } from '@/domain/services/CartDomainService';
|
||||
import { ICartRepository } from '@/application/ports/ICartRepository';
|
||||
|
||||
interface CartState {
|
||||
items: CartItem[];
|
||||
}
|
||||
|
||||
interface CartActions {
|
||||
_setItems: (items: CartItem[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* INFRASTRUCTURE — Zustand store (state + persistence only)
|
||||
*/
|
||||
export const useCartStore = create<CartState & CartActions>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
items: [],
|
||||
_setItems: (items) => set({ items }),
|
||||
}),
|
||||
{ name: 'kott-gard-cart' }
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* INFRASTRUCTURE — Cart repository adapter (implements application port)
|
||||
*/
|
||||
export class ZustandCartRepository implements ICartRepository {
|
||||
getItems(): CartItem[] {
|
||||
return useCartStore.getState().items;
|
||||
}
|
||||
|
||||
setItems(items: CartItem[]): void {
|
||||
useCartStore.getState()._setItems(items);
|
||||
}
|
||||
|
||||
addItem(
|
||||
product: Product,
|
||||
customization: ProductCustomization,
|
||||
customizationLabel: string,
|
||||
quantity = 1
|
||||
): void {
|
||||
const items = this.getItems();
|
||||
const next = CartDomainService.addItem(
|
||||
items,
|
||||
product,
|
||||
customization,
|
||||
customizationLabel,
|
||||
quantity
|
||||
);
|
||||
this.setItems(next);
|
||||
}
|
||||
|
||||
removeItem(id: string): void {
|
||||
this.setItems(CartDomainService.removeItem(this.getItems(), id));
|
||||
}
|
||||
|
||||
updateQuantity(id: string, quantity: number): void {
|
||||
this.setItems(
|
||||
CartDomainService.updateQuantity(this.getItems(), id, quantity)
|
||||
);
|
||||
}
|
||||
|
||||
clearCart(): void {
|
||||
this.setItems([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { DEFAULT_LOCALE, Locale } from '@/i18n/types';
|
||||
|
||||
interface LocaleState {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
}
|
||||
|
||||
export const useLocaleStore = create<LocaleState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
locale: DEFAULT_LOCALE,
|
||||
setLocale: (locale) => set({ locale }),
|
||||
}),
|
||||
{ name: 'kott-gard-locale' }
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { Product } from '@/domain/entities';
|
||||
import { IWishlistRepository } from '@/application/ports/IWishlistRepository';
|
||||
|
||||
interface WishlistState {
|
||||
items: Product[];
|
||||
_setItems: (items: Product[]) => void;
|
||||
}
|
||||
|
||||
export const useWishlistStore = create<WishlistState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
items: [],
|
||||
_setItems: (items) => set({ items }),
|
||||
}),
|
||||
{ name: 'kott-gard-wishlist' }
|
||||
)
|
||||
);
|
||||
|
||||
export class ZustandWishlistRepository implements IWishlistRepository {
|
||||
getItems(): Product[] {
|
||||
return useWishlistStore.getState().items;
|
||||
}
|
||||
|
||||
private setItems(items: Product[]): void {
|
||||
useWishlistStore.getState()._setItems(items);
|
||||
}
|
||||
|
||||
addItem(product: Product): void {
|
||||
if (!this.isInWishlist(product.id)) {
|
||||
this.setItems([...this.getItems(), product]);
|
||||
}
|
||||
}
|
||||
|
||||
removeItem(productId: string): void {
|
||||
this.setItems(this.getItems().filter((p) => p.id !== productId));
|
||||
}
|
||||
|
||||
isInWishlist(productId: string): boolean {
|
||||
return this.getItems().some((p) => p.id === productId);
|
||||
}
|
||||
|
||||
toggleItem(product: Product): void {
|
||||
if (this.isInWishlist(product.id)) {
|
||||
this.removeItem(product.id);
|
||||
} else {
|
||||
this.addItem(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
import { IProductRepository } from '@/application/ports/IProductRepository';
|
||||
import { PRODUCT_CATALOG } from '@/infrastructure/data/products.data';
|
||||
|
||||
/**
|
||||
* INFRASTRUCTURE — In-memory product source (replace with API later)
|
||||
*/
|
||||
export class InMemoryProductRepository implements IProductRepository {
|
||||
constructor(private readonly catalog: Product[] = PRODUCT_CATALOG) {}
|
||||
|
||||
findAll(): Product[] {
|
||||
return [...this.catalog];
|
||||
}
|
||||
|
||||
findBySlug(slug: string): Product | undefined {
|
||||
return this.catalog.find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
findByCategory(category: string): Product[] {
|
||||
return this.catalog.filter((p) => p.category === category);
|
||||
}
|
||||
|
||||
findFeatured(): Product[] {
|
||||
return this.catalog.filter((p) => p.featured);
|
||||
}
|
||||
|
||||
findById(id: string): Product | undefined {
|
||||
return this.catalog.find((p) => p.id === id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { CutCount, CuttingStyleKey } from '@/types';
|
||||
import { CATEGORY_IMAGES } from '@/infrastructure/images';
|
||||
|
||||
export const CUT_COUNTS: CutCount[] = [4, 8, 10, 12];
|
||||
|
||||
export const CUTTING_STYLE_KEYS: CuttingStyleKey[] = [
|
||||
'nihari',
|
||||
'karahi',
|
||||
'qeema',
|
||||
'boneless',
|
||||
'steak',
|
||||
];
|
||||
|
||||
export const MEAT_CATEGORIES = ['chicken', 'beef', 'lamb'] as const;
|
||||
|
||||
export const CATEGORY_IDS = ['chicken', 'beef', 'lamb', 'fish'] as const;
|
||||
|
||||
export { CATEGORY_IMAGES };
|
||||
|
||||
export const SITE_NAME = 'Kött Gård';
|
||||
export const SITE_EMAIL = 'hello@kottgard.se';
|
||||
export const DEMO_EMAIL = 'demo@kottgard.se';
|
||||
export const SITE_PHONE = '+46 72 585 50 50';
|
||||
export const SITE_PHONE_DISPLAY = '072-585 50 50';
|
||||
export const SITE_ADDRESS = 'Tingvallavägen 11, 195 31 Märsta';
|
||||
export const SITE_LOCATION = 'Märsta · Sverige';
|
||||
export const SITE_HOURS = '10:00–19:00';
|
||||
export const FACEBOOK_URL = 'https://www.facebook.com/kottgard/';
|
||||
export const INSTAGRAM_URL = 'https://www.instagram.com/kottgard';
|
||||
export const MAPS_URL = 'https://share.google/fUkkhDNlhDTcImKo5';
|
||||
|
||||
const WHATSAPP_BASE = 'Hej Kött Gård! Jag vill beställa';
|
||||
|
||||
export function whatsappOrderUrl(product?: string): string {
|
||||
const text = product ? `${WHATSAPP_BASE} ${product}.` : `${WHATSAPP_BASE}.`;
|
||||
return `https://wa.me/46725855050?text=${encodeURIComponent(text)}`;
|
||||
}
|
||||
|
||||
export const WHATSAPP_URL = whatsappOrderUrl();
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Backward-compatible facade — logic lives in domain layer
|
||||
*/
|
||||
export {
|
||||
CustomizationDomainService,
|
||||
type Translator,
|
||||
} from '@/domain/services/CustomizationDomainService';
|
||||
|
||||
import { CustomizationDomainService } from '@/domain/services/CustomizationDomainService';
|
||||
|
||||
export const getDefaultCustomization =
|
||||
CustomizationDomainService.getDefaultCustomization;
|
||||
export const getCustomizationKey =
|
||||
CustomizationDomainService.getCustomizationKey;
|
||||
export const getCustomizationLabel =
|
||||
CustomizationDomainService.getCustomizationLabel;
|
||||
export const getCartItemKey = CustomizationDomainService.getCartItemKey;
|
||||
export const updateMeatCustomization =
|
||||
CustomizationDomainService.updateMeatCustomization;
|
||||
@@ -0,0 +1 @@
|
||||
export { DEMO_ORDERS } from '@/infrastructure/data/demo-orders.data';
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Re-export — all image FILES live in one folder:
|
||||
* public/images/site/
|
||||
* Paths are built in: src/infrastructure/images/catalog.ts
|
||||
*/
|
||||
export * from '@/infrastructure/images';
|
||||
@@ -0,0 +1,37 @@
|
||||
import { WeeklyOffer } from '@/types';
|
||||
import { getOfferImage } from '@/infrastructure/images';
|
||||
|
||||
export const weeklyOffers: WeeklyOffer[] = [
|
||||
{
|
||||
id: 'chicken-wings-pl',
|
||||
nameKey: 'offers.items.chickenWings',
|
||||
badgeKey: 'fresh',
|
||||
price: 49.99,
|
||||
originalPrice: 59.99,
|
||||
priceUnitKey: 'perKg',
|
||||
image: getOfferImage('chicken-wings-pl'),
|
||||
whatsappProduct: 'Kycklingvingar färsk PL',
|
||||
productSlug: 'chicken-wings',
|
||||
},
|
||||
{
|
||||
id: 'lamb-steak-ireland',
|
||||
nameKey: 'offers.items.lambSteak',
|
||||
badgeKey: 'halal',
|
||||
price: 204.99,
|
||||
priceUnitKey: 'perKg',
|
||||
image: getOfferImage('lamb-steak-ireland'),
|
||||
whatsappProduct: 'Lammstek färsk Ireland',
|
||||
productSlug: 'lamb-leg',
|
||||
},
|
||||
{
|
||||
id: 'beef-mince-irl',
|
||||
nameKey: 'offers.items.beefMince',
|
||||
badgeKey: 'fresh',
|
||||
price: 139.99,
|
||||
originalPrice: 159.99,
|
||||
priceUnitKey: 'perKg',
|
||||
image: getOfferImage('beef-mince-irl'),
|
||||
whatsappProduct: 'Nötfärs 5% fett IRL',
|
||||
productSlug: 'beef-mince',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Product } from '@/domain/entities';
|
||||
import { LocalizedProduct } from '@/application/dtos/LocalizedProduct';
|
||||
import { LocalizeProductUseCase } from '@/application/use-cases/catalog/LocalizeProduct';
|
||||
import { ITranslationService } from '@/application/ports/ITranslationService';
|
||||
|
||||
type Translator = (path: string, params?: Record<string, string | number>) => string;
|
||||
|
||||
export type { LocalizedProduct };
|
||||
|
||||
class TranslatorAdapter implements ITranslationService {
|
||||
constructor(private readonly t: Translator) {}
|
||||
|
||||
translate(
|
||||
path: string,
|
||||
params?: Record<string, string | number>
|
||||
): string {
|
||||
return this.t(path, params);
|
||||
}
|
||||
}
|
||||
|
||||
export function localizeProduct(product: Product, t: Translator): LocalizedProduct {
|
||||
return new LocalizeProductUseCase(new TranslatorAdapter(t)).execute(product);
|
||||
}
|
||||
|
||||
export function localizeCategory(
|
||||
categoryId: Product['category'],
|
||||
t: Translator
|
||||
): { name: string; description: string } {
|
||||
return {
|
||||
name: t(`categories.${categoryId}.name`),
|
||||
description: t(`categories.${categoryId}.description`),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { container } from '@/application/container';
|
||||
|
||||
/**
|
||||
* Backward-compatible facade — data lives in infrastructure layer
|
||||
*/
|
||||
export const products = container.productRepository.findAll();
|
||||
|
||||
export function getProductBySlug(slug: string) {
|
||||
return container.getProductBySlug.execute(slug);
|
||||
}
|
||||
|
||||
export function getProductsByCategory(category: string) {
|
||||
return container.productRepository.findByCategory(category);
|
||||
}
|
||||
|
||||
export function getFeaturedProducts() {
|
||||
return container.productRepository.findFeatured();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return clsx(inputs);
|
||||
}
|
||||
|
||||
export function formatPrice(price: number, locale = 'sv-SE'): string {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'SEK',
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
export function getFormatLocale(locale: string): string {
|
||||
const map: Record<string, string> = {
|
||||
en: 'en-US',
|
||||
sv: 'sv-SE',
|
||||
ur: 'ur-PK',
|
||||
ar: 'ar-SA',
|
||||
fa: 'fa-IR',
|
||||
tr: 'tr-TR',
|
||||
};
|
||||
return map[locale] ?? 'en-US';
|
||||
}
|
||||
|
||||
export function formatDate(date: string): string {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}).format(new Date(date));
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { Address, Order, User } from '@/domain/entities';
|
||||
import { RegisterInput } from '@/domain/services/AuthDomainService';
|
||||
import { container } from '@/application/container';
|
||||
import { useAuthStore } from '@/infrastructure/persistence/zustand/authStore';
|
||||
|
||||
export function useAuth() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const orders = useAuthStore((s) => s.orders);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
|
||||
return {
|
||||
user,
|
||||
orders,
|
||||
isAuthenticated,
|
||||
login: (email: string, password: string) =>
|
||||
container.login.execute(email, password),
|
||||
register: (data: RegisterInput) => container.register.execute(data),
|
||||
logout: () => container.authRepository.logout(),
|
||||
updateProfile: (data: Partial<User>) =>
|
||||
container.authRepository.updateProfile(data),
|
||||
addOrder: (order: Order) => container.authRepository.addOrder(order),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { Product, ProductCustomization } from '@/domain/entities';
|
||||
import { container } from '@/application/container';
|
||||
import { useCartStore } from '@/infrastructure/persistence/zustand/cartStore';
|
||||
|
||||
/**
|
||||
* PRESENTATION — Cart hook: React state + use cases (Clean Architecture boundary)
|
||||
*/
|
||||
export function useCart() {
|
||||
const items = useCartStore((s) => s.items);
|
||||
const summary = container.getCartSummary.execute();
|
||||
|
||||
return {
|
||||
items,
|
||||
summary,
|
||||
addItem: (
|
||||
product: Product,
|
||||
customization: ProductCustomization,
|
||||
customizationLabel: string,
|
||||
quantity?: number
|
||||
) => {
|
||||
container.addToCart.execute(
|
||||
product,
|
||||
customization,
|
||||
customizationLabel,
|
||||
quantity
|
||||
);
|
||||
},
|
||||
removeItem: (id: string) => container.removeFromCart.execute(id),
|
||||
updateQuantity: (id: string, quantity: number) =>
|
||||
container.updateCartQuantity.execute(id, quantity),
|
||||
clearCart: () => container.cartRepository.clearCart(),
|
||||
/** @deprecated Use summary.subtotal */
|
||||
getTotal: () => summary.subtotal,
|
||||
/** @deprecated Use summary.itemCount */
|
||||
getItemCount: () => summary.itemCount,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Category, SortOption } from '@/domain/entities';
|
||||
import { CategoryFilter } from '@/domain/services/CatalogDomainService';
|
||||
import { container } from '@/application/container';
|
||||
import { useLocaleStore } from '@/infrastructure/persistence/zustand/localeStore';
|
||||
import { LocalizedProduct } from '@/application/dtos/LocalizedProduct';
|
||||
|
||||
export function useCatalogFilter(input: {
|
||||
category: CategoryFilter;
|
||||
searchQuery: string;
|
||||
sortBy: SortOption;
|
||||
}): LocalizedProduct[] {
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
|
||||
return useMemo(() => {
|
||||
return container.createFilterProducts(locale).execute(input);
|
||||
}, [locale, input.category, input.searchQuery, input.sortBy]);
|
||||
}
|
||||
|
||||
export function useLocalizedProduct(
|
||||
productId: string
|
||||
): LocalizedProduct | undefined {
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
|
||||
return useMemo(() => {
|
||||
const product = container.productRepository.findById(productId);
|
||||
if (!product) return undefined;
|
||||
return container.createLocalizeProduct(locale).execute(product);
|
||||
}, [locale, productId]);
|
||||
}
|
||||
|
||||
export { FilterProductsUseCase as CatalogHelpers } from '@/application/use-cases/catalog/FilterProducts';
|
||||
|
||||
export type { CategoryFilter };
|
||||
export type { Category };
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { Address } from '@/domain/entities';
|
||||
import { container } from '@/application/container';
|
||||
|
||||
export function useCheckout() {
|
||||
return {
|
||||
placeOrder: (input: {
|
||||
deliveryAddress: Address;
|
||||
paymentMethod: string;
|
||||
}) => container.placeOrder.execute(input),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { Product } from '@/domain/entities';
|
||||
import { container } from '@/application/container';
|
||||
import { useWishlistStore } from '@/infrastructure/persistence/zustand/wishlistStore';
|
||||
|
||||
export function useWishlist() {
|
||||
const items = useWishlistStore((s) => s.items);
|
||||
|
||||
return {
|
||||
items,
|
||||
isInWishlist: (productId: string) =>
|
||||
container.wishlistRepository.isInWishlist(productId),
|
||||
toggleItem: (product: Product) =>
|
||||
container.toggleWishlist.execute(product),
|
||||
removeItem: (productId: string) =>
|
||||
container.wishlistRepository.removeItem(productId),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Backward-compatible re-export — prefer @/presentation/hooks/useAuth
|
||||
*/
|
||||
export { useAuthStore } from '@/infrastructure/persistence/zustand/authStore';
|
||||
export { useAuth } from '@/presentation/hooks/useAuth';
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Backward-compatible re-export — prefer @/presentation/hooks/useCart
|
||||
*/
|
||||
export { useCartStore } from '@/infrastructure/persistence/zustand/cartStore';
|
||||
export { useCart } from '@/presentation/hooks/useCart';
|
||||
@@ -0,0 +1 @@
|
||||
export { useLocaleStore } from '@/infrastructure/persistence/zustand/localeStore';
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Backward-compatible re-export — prefer @/presentation/hooks/useWishlist
|
||||
*/
|
||||
export { useWishlistStore } from '@/infrastructure/persistence/zustand/wishlistStore';
|
||||
export { useWishlist } from '@/presentation/hooks/useWishlist';
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Backward-compatible re-exports — prefer @/domain/entities in new code
|
||||
*/
|
||||
export * from '@/domain/entities';
|
||||
Reference in New Issue
Block a user