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
This commit is contained in:
@@ -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';
|
||||
|
||||
// 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;
|
||||
|
||||
try {
|
||||
const products = await getProducts();
|
||||
product = products.find((p) => (p.slug || p.id.toString()) === slug);
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
interface ProductPageProps {
|
||||
params: Promise<{ slug: string; locale?: string }>;
|
||||
}
|
||||
|
||||
// Generate static params for all products
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
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) {
|
||||
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 (
|
||||
<main className="min-h-screen">
|
||||
<Header />
|
||||
<div className="pt-24 text-center">
|
||||
<h1 className="text-2xl">Product not found</h1>
|
||||
<h1 className="text-2xl">
|
||||
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
|
||||
</h1>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<main className="min-h-screen">
|
||||
@@ -40,26 +96,70 @@ export default async function ProductPage({ params }: { params: Promise<{ slug:
|
||||
<section className="pt-24 pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
{/* Product Image */}
|
||||
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden">
|
||||
<img
|
||||
<Image
|
||||
src={image}
|
||||
alt={product.name}
|
||||
className="object-cover w-full h-full"
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-4xl font-serif mb-4">{product.name}</h1>
|
||||
<h1 className="text-4xl font-serif mb-4">{localized.name}</h1>
|
||||
|
||||
<div className="text-2xl mb-6">{price} RSD</div>
|
||||
{price && (
|
||||
<div className="text-2xl mb-6">{price}</div>
|
||||
)}
|
||||
|
||||
<div className="prose max-w-none mb-8" dangerouslySetInnerHTML={{ __html: product.description || '' }} />
|
||||
{localized.description && (
|
||||
<div
|
||||
className="prose max-w-none mb-8"
|
||||
dangerouslySetInnerHTML={{ __html: localized.description }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<button
|
||||
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all"
|
||||
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!isAvailable}
|
||||
>
|
||||
Add to Cart
|
||||
{isAvailable
|
||||
? (currentLocale === "en" ? "Add to Cart" : "Dodaj u korpu")
|
||||
: (currentLocale === "en" ? "Out of Stock" : "Nema na stanju")
|
||||
}
|
||||
</button>
|
||||
|
||||
{/* SKU */}
|
||||
{variant?.sku && (
|
||||
<p className="mt-4 text-sm text-foreground-muted">
|
||||
SKU: {variant.sku}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Language Switcher */}
|
||||
<div className="mt-8 pt-8 border-t">
|
||||
<p className="text-sm text-foreground-muted mb-2">
|
||||
{currentLocale === "en" ? "Language:" : "Jezik:"}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href={serbianUrl}
|
||||
className={`text-sm font-medium ${currentLocale === "sr" ? "text-foreground" : "text-foreground-muted hover:text-foreground"}`}
|
||||
>
|
||||
🇷🇸 Srpski
|
||||
</a>
|
||||
<a
|
||||
href={englishUrl}
|
||||
className={`text-sm font-medium ${currentLocale === "en" ? "text-foreground" : "text-foreground-muted hover:text-foreground"}`}
|
||||
>
|
||||
🇬🇧 English
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
interface ProductsPageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
const publishedProducts = products.filter((p) => p.status === "publish");
|
||||
export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
const { locale = "sr" } = await params;
|
||||
const products = await getProducts(locale.toUpperCase());
|
||||
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
@@ -25,15 +23,22 @@ export default async function ProductsPage() {
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16">
|
||||
All Products
|
||||
{locale === "en" ? "All Products" : "Svi Proizvodi"}
|
||||
</h1>
|
||||
|
||||
{publishedProducts.length === 0 ? (
|
||||
<p className="text-center text-foreground-muted">No products available</p>
|
||||
{products.length === 0 ? (
|
||||
<p className="text-center text-foreground-muted">
|
||||
{locale === "en" ? "No products available" : "Nema dostupnih proizvoda"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{publishedProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
{products.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
index={index}
|
||||
locale={locale.toUpperCase()}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
@@ -20,29 +25,31 @@ export default function ProductCard({ product, index = 0 }: ProductCardProps) {
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Link href={`/products/${product.slug}`} className="group block">
|
||||
<Link href={`/products/${localized.slug}`} className="group block">
|
||||
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden mb-4">
|
||||
{image && (
|
||||
<Image
|
||||
src={image}
|
||||
alt={product.name}
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
{product.stock_status === "outofstock" && (
|
||||
{!isAvailable && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||
<span className="text-white font-medium">Out of Stock</span>
|
||||
<span className="text-white font-medium">
|
||||
{locale === "en" ? "Out of Stock" : "Nema na stanju"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="font-serif text-lg mb-1 group-hover:text-accent-dark transition-colors">
|
||||
{product.name}
|
||||
{localized.name}
|
||||
</h3>
|
||||
|
||||
<p className="text-foreground-muted">
|
||||
{product.price ? formatPrice(product.price) : "Contact for price"}
|
||||
{price || (locale === "en" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
</p>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user