From 5706792980318ef9bbf85515f948e5ff8fa50627 Mon Sep 17 00:00:00 2001 From: Unchained Date: Sat, 21 Mar 2026 12:38:24 +0200 Subject: [PATCH] feat(saleor): Phase 2 - Product Migration - Update ProductCard to use Saleor Product type - Update products listing page to fetch from Saleor - Update product detail page with Saleor integration - Add language switching support (SR/EN) - Add SEO metadata generation - Implement static params generation for all product slugs - Add availability checking based on variant quantity --- src/app/products/[slug]/page.tsx | 148 +++++++++++++++++++++---- src/app/products/page.tsx | 35 +++--- src/components/product/ProductCard.tsx | 25 +++-- 3 files changed, 160 insertions(+), 48 deletions(-) diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index be5f9ea..f314130 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -1,37 +1,93 @@ -import { getProducts } from "@/lib/woocommerce"; +import Image from "next/image"; +import { getProductBySlug, getProducts, getProductPrice, getProductImage, getLocalizedProduct, formatPrice } from "@/lib/saleor"; import Header from "@/components/layout/Header"; import Footer from "@/components/layout/Footer"; +import type { Product } from "@/types/saleor"; -export const dynamic = 'force-dynamic'; +interface ProductPageProps { + params: Promise<{ slug: string; locale?: string }>; +} -// Disable static generation - this page will be server-rendered -export const generateStaticParams = undefined; - -export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params; - let product = null; - +// Generate static params for all products +export async function generateStaticParams() { try { - const products = await getProducts(); - product = products.find((p) => (p.slug || p.id.toString()) === slug); + const products = await getProducts("SR", 100); + const params: Array<{ slug: string; locale: string }> = []; + + products.forEach((product: Product) => { + // Serbian slug + params.push({ slug: product.slug, locale: "sr" }); + + // English slug (if translation exists) + if (product.translation?.slug) { + params.push({ slug: product.translation.slug, locale: "en" }); + } + }); + + return params; } catch (e) { - // Fallback + return []; } +} +export async function generateMetadata({ params }: ProductPageProps) { + const { slug, locale = "sr" } = await params; + const product = await getProductBySlug(slug, locale.toUpperCase()); + + if (!product) { + return { + title: locale === "en" ? "Product Not Found" : "Proizvod nije pronađen", + }; + } + + const localized = getLocalizedProduct(product, locale.toUpperCase()); + + return { + title: localized.name, + description: localized.seoDescription || localized.description?.slice(0, 160), + alternates: { + canonical: `/products/${product.slug}`, + languages: { + "sr": `/products/${product.slug}`, + "en": product.translation?.slug ? `/products/${product.translation.slug}` : `/products/${product.slug}`, + }, + }, + }; +} + +export default async function ProductPage({ params }: ProductPageProps) { + const { slug, locale = "sr" } = await params; + const product = await getProductBySlug(slug, locale.toUpperCase()); + if (!product) { return (
-

Product not found

+

+ {locale === "en" ? "Product not found" : "Proizvod nije pronađen"} +

); } - const image = product.images?.[0]?.src || '/placeholder.jpg'; - const price = product.sale_price || product.price; + const localized = getLocalizedProduct(product, locale.toUpperCase()); + const image = getProductImage(product); + const price = getProductPrice(product); + const variant = product.variants?.[0]; + const isAvailable = variant?.quantityAvailable > 0; + + // Determine language based on which slug matched + const isEnglishSlug = slug === product.translation?.slug; + const currentLocale = isEnglishSlug ? "en" : "sr"; + + // URLs for language switcher + const serbianUrl = `/products/${product.slug}`; + const englishUrl = product.translation?.slug + ? `/products/${product.translation.slug}` + : serbianUrl; return (
@@ -40,26 +96,70 @@ export default async function ProductPage({ params }: { params: Promise<{ slug:
+ {/* Product Image */}
- {product.name}
+ {/* Product Info */}
-

{product.name}

+

{localized.name}

-
{price} RSD
+ {price && ( +
{price}
+ )} -
+ {localized.description && ( +
+ )} + {/* Add to Cart Button */} + + {/* SKU */} + {variant?.sku && ( +

+ SKU: {variant.sku} +

+ )} + + {/* Language Switcher */} +
+

+ {currentLocale === "en" ? "Language:" : "Jezik:"} +

+ +
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx index ebbd4d6..79fd8d6 100644 --- a/src/app/products/page.tsx +++ b/src/app/products/page.tsx @@ -1,4 +1,4 @@ -import { getProducts } from "@/lib/woocommerce"; +import { getProducts } from "@/lib/saleor"; import Header from "@/components/layout/Header"; import Footer from "@/components/layout/Footer"; import ProductCard from "@/components/product/ProductCard"; @@ -8,15 +8,13 @@ export const metadata = { description: "Browse our collection of premium natural oils for hair and skin care.", }; -export default async function ProductsPage() { - let products: any[] = []; - try { - products = await getProducts(); - } catch (e) { - console.log('Failed to fetch products during build'); - } - - const publishedProducts = products.filter((p) => p.status === "publish"); +interface ProductsPageProps { + params: Promise<{ locale: string }>; +} + +export default async function ProductsPage({ params }: ProductsPageProps) { + const { locale = "sr" } = await params; + const products = await getProducts(locale.toUpperCase()); return (
@@ -25,15 +23,22 @@ export default async function ProductsPage() {

- All Products + {locale === "en" ? "All Products" : "Svi Proizvodi"}

- {publishedProducts.length === 0 ? ( -

No products available

+ {products.length === 0 ? ( +

+ {locale === "en" ? "No products available" : "Nema dostupnih proizvoda"} +

) : (
- {publishedProducts.map((product, index) => ( - + {products.map((product, index) => ( + ))}
)} diff --git a/src/components/product/ProductCard.tsx b/src/components/product/ProductCard.tsx index 8242160..f404732 100644 --- a/src/components/product/ProductCard.tsx +++ b/src/components/product/ProductCard.tsx @@ -3,15 +3,20 @@ import { motion } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; -import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce"; +import type { Product } from "@/types/saleor"; +import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor"; interface ProductCardProps { - product: WooProduct; + product: Product; index?: number; + locale?: string; } -export default function ProductCard({ product, index = 0 }: ProductCardProps) { +export default function ProductCard({ product, index = 0, locale = "SR" }: ProductCardProps) { const image = getProductImage(product); + const price = getProductPrice(product); + const localized = getLocalizedProduct(product, locale); + const isAvailable = product.variants?.[0]?.quantityAvailable > 0; return ( - +
{image && ( {product.name} )} - {product.stock_status === "outofstock" && ( + {!isAvailable && (
- Out of Stock + + {locale === "en" ? "Out of Stock" : "Nema na stanju"} +
)}

- {product.name} + {localized.name}

- {product.price ? formatPrice(product.price) : "Contact for price"} + {price || (locale === "en" ? "Contact for price" : "Kontaktirajte za cenu")}