Created src/lib/i18n/locales.ts as single source of truth for: - SUPPORTED_LOCALES array - LOCALE_COOKIE name - DEFAULT_LOCALE - LOCALE_CONFIG (labels, flags, Saleor locale mapping) - Helper functions (isValidLocale, getSaleorLocale, getLocaleFromPath) Updated all files to use centralized constants: - middleware.ts - Header.tsx - ProductCard.tsx - sitemap.ts - root layout and locale layout - routing.ts Benefits: - Adding new locale only requires updating ONE file (locales.ts) - No more hardcoded locale lists scattered across codebase - Cookie name defined in one place - Type-safe locale validation
80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { useTranslations } from "next-intl";
|
|
import type { Product } from "@/types/saleor";
|
|
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
|
import { isValidLocale, getSaleorLocale } from "@/lib/i18n/locales";
|
|
|
|
interface ProductCardProps {
|
|
product: Product;
|
|
index?: number;
|
|
locale?: string;
|
|
}
|
|
|
|
export default function ProductCard({ product, index = 0, locale = "sr" }: ProductCardProps) {
|
|
const t = useTranslations("ProductCard");
|
|
const image = getProductImage(product);
|
|
const price = getProductPrice(product);
|
|
const saleorLocale = isValidLocale(locale) ? getSaleorLocale(locale) : "SR";
|
|
const localized = getLocalizedProduct(product, saleorLocale);
|
|
const isAvailable = product.variants?.[0]?.quantityAvailable > 0;
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
>
|
|
<Link href={`/${locale}/products/${localized.slug}`} className="group block">
|
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
|
{image ? (
|
|
<img
|
|
src={image}
|
|
alt={localized.name}
|
|
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
|
loading="lazy"
|
|
/>
|
|
) : (
|
|
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
|
<span className="text-sm">{t("noImage")}</span>
|
|
</div>
|
|
)}
|
|
|
|
{!isAvailable && (
|
|
<div className="absolute inset-0 bg-white/80 flex items-center justify-center">
|
|
<span className="text-sm uppercase tracking-[0.1em] text-[#666666]">
|
|
{t("outOfStock")}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
|
<button
|
|
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
}}
|
|
>
|
|
{t("quickAdd")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-center">
|
|
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
|
|
{localized.name}
|
|
</h3>
|
|
|
|
<p className="text-[14px] text-[#666666]">
|
|
{price || t("contactForPrice")}
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
</motion.div>
|
|
);
|
|
}
|