- Add /solutions hub page with 10 category cards - Add /solutions/by-concern directory page - Add /solutions/by-oil directory page - Add Solutions section to Footer with navigation links - Add Breadcrumb component for solution pages - Add translations for all solution pages (sr, en, de, fr) - Fix ExitIntentDetector JSON parsing error - Update sitemap with solution pages - Create 3 sample solution pages with data files
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import Image from "next/image";
|
|
import { useState } from "react";
|
|
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
|
import { useAnalytics } from "@/lib/analytics";
|
|
import type { Product } from "@/types/saleor";
|
|
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
|
import { isValidLocale, getSaleorLocale } from "@/lib/i18n/locales";
|
|
import { useTranslations } from "next-intl";
|
|
import Link from "next/link";
|
|
|
|
interface ProductsGridProps {
|
|
products: Product[];
|
|
locale: string;
|
|
}
|
|
|
|
function ProductCardWithAddToCart({ product, index, locale }: { product: Product; index: number; locale: string }) {
|
|
const t = useTranslations("ProductCard");
|
|
const tProduct = useTranslations("Product");
|
|
const [isAdding, setIsAdding] = useState(false);
|
|
const { addLine, openCart, setLanguageCode } = useSaleorCheckoutStore();
|
|
const { trackAddToCart } = useAnalytics();
|
|
|
|
const image = getProductImage(product);
|
|
const price = getProductPrice(product);
|
|
const saleorLocale = isValidLocale(locale) ? getSaleorLocale(locale) : "SR";
|
|
const localized = getLocalizedProduct(product, saleorLocale);
|
|
const variant = product.variants?.[0];
|
|
const isAvailable = (variant?.quantityAvailable || 0) > 0;
|
|
const productHref = locale === "sr" ? `/products/${localized.slug}` : `/${locale}/products/${localized.slug}`;
|
|
|
|
const handleAddToCart = async (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (!variant?.id) return;
|
|
|
|
if (isValidLocale(locale)) {
|
|
setLanguageCode(locale);
|
|
}
|
|
|
|
setIsAdding(true);
|
|
try {
|
|
await addLine(variant.id, 1);
|
|
|
|
const priceAmount = variant?.pricing?.price?.gross?.amount || 0;
|
|
const currency = variant?.pricing?.price?.gross?.currency || "RSD";
|
|
|
|
trackAddToCart({
|
|
id: product.id,
|
|
name: localized.name,
|
|
price: priceAmount,
|
|
currency,
|
|
quantity: 1,
|
|
variant: variant.name,
|
|
});
|
|
|
|
openCart();
|
|
} finally {
|
|
setIsAdding(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
className="group"
|
|
>
|
|
<Link href={productHref} className="block">
|
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
|
{image ? (
|
|
<Image
|
|
src={image}
|
|
alt={localized.name}
|
|
fill
|
|
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
|
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
|
loading={index < 4 ? "eager" : "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>
|
|
</Link>
|
|
|
|
<div className="text-center">
|
|
<Link href={productHref}>
|
|
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
|
|
{localized.name}
|
|
</h3>
|
|
</Link>
|
|
|
|
<p className="text-[14px] text-[#666666] mb-3">
|
|
{price || t("contactForPrice")}
|
|
</p>
|
|
|
|
{isAvailable ? (
|
|
<button
|
|
onClick={handleAddToCart}
|
|
disabled={isAdding}
|
|
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isAdding ? tProduct("adding") : tProduct("addToCart")}
|
|
</button>
|
|
) : (
|
|
<div className="w-full py-3 bg-[#f8f9fa] text-[#666666] text-xs uppercase tracking-[0.1em]">
|
|
{t("outOfStock")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export default function ProductsGrid({ products, locale }: ProductsGridProps) {
|
|
const t = useTranslations("Solutions");
|
|
const validProducts = products.filter(p => p && p.id);
|
|
|
|
return (
|
|
<section className="py-16 lg:py-24 bg-[#1A1A1A]">
|
|
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="text-center mb-12">
|
|
<h2 className="text-3xl md:text-4xl font-light text-white mb-4">
|
|
{t("completeYourRoutine")}
|
|
</h2>
|
|
<p className="text-[#999999] max-w-2xl mx-auto">
|
|
{t("discoverProducts")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
|
{validProducts.map((product, index) => (
|
|
<ProductCardWithAddToCart
|
|
key={product.id}
|
|
product={product}
|
|
index={index}
|
|
locale={locale}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|