- Create Saleor checkout store (Zustand + persist) - Update CartDrawer to use Saleor checkout - Update Header to use Saleor checkout store - Update ProductDetail with Add to Cart functionality - Add checkout initialization on app load - Handle checkout line add/update/delete operations - Add error handling and loading states
198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Image from "next/image";
|
|
import { motion } from "framer-motion";
|
|
import type { Product } from "@/types/saleor";
|
|
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
|
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
|
import ProductCard from "@/components/product/ProductCard";
|
|
|
|
interface ProductDetailProps {
|
|
product: Product;
|
|
relatedProducts: Product[];
|
|
locale?: string;
|
|
}
|
|
|
|
export default function ProductDetail({ product, relatedProducts, locale = "SR" }: ProductDetailProps) {
|
|
const [selectedImage, setSelectedImage] = useState(0);
|
|
const [quantity, setQuantity] = useState(1);
|
|
const [isAdding, setIsAdding] = useState(false);
|
|
const { addLine, openCart } = useSaleorCheckoutStore();
|
|
|
|
const localized = getLocalizedProduct(product, locale);
|
|
const variant = product.variants?.[0];
|
|
const images = product.media?.length > 0
|
|
? product.media
|
|
: [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" }];
|
|
|
|
const handleAddToCart = async () => {
|
|
if (!variant?.id) return;
|
|
|
|
setIsAdding(true);
|
|
try {
|
|
await addLine(variant.id, quantity);
|
|
openCart();
|
|
} finally {
|
|
setIsAdding(false);
|
|
}
|
|
};
|
|
|
|
const stripHtml = (html: string) => {
|
|
if (!html) return "";
|
|
return html.replace(/<[^>]*>/g, "");
|
|
};
|
|
|
|
const isAvailable = variant?.quantityAvailable > 0;
|
|
const price = getProductPrice(product);
|
|
|
|
return (
|
|
<>
|
|
<section className="py-12 md:py-20 px-4">
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
|
{/* Product Images */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.6 }}
|
|
>
|
|
<div className="relative aspect-square bg-background-ice mb-4">
|
|
{images[selectedImage] && (
|
|
<Image
|
|
src={images[selectedImage].url}
|
|
alt={images[selectedImage].alt || localized.name}
|
|
fill
|
|
className="object-cover"
|
|
priority
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{images.length > 1 && (
|
|
<div className="flex gap-2 overflow-x-auto">
|
|
{images.map((image, index) => (
|
|
<button
|
|
key={image.id}
|
|
onClick={() => setSelectedImage(index)}
|
|
className={`relative w-20 h-20 flex-shrink-0 ${
|
|
selectedImage === index ? "ring-2 ring-foreground" : ""
|
|
}`}
|
|
>
|
|
<Image
|
|
src={image.url}
|
|
alt={image.alt || localized.name}
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
|
|
{/* Product Info */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.6, delay: 0.2 }}
|
|
>
|
|
<h1 className="text-3xl md:text-4xl font-serif mb-4">
|
|
{localized.name}
|
|
</h1>
|
|
|
|
<p className="text-2xl text-foreground-muted mb-6">
|
|
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
|
</p>
|
|
|
|
{/* Short Description */}
|
|
<div className="prose prose-sm max-w-none mb-8 text-foreground-muted">
|
|
<p>{stripHtml(localized.description).slice(0, 200)}...</p>
|
|
</div>
|
|
|
|
{/* Add to Cart */}
|
|
{isAvailable ? (
|
|
<div className="flex items-center gap-4 mb-8">
|
|
{/* Quantity Selector */}
|
|
<div className="flex items-center border border-border">
|
|
<button
|
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
|
className="px-4 py-3 hover:bg-gray-50"
|
|
>
|
|
-
|
|
</button>
|
|
<span className="px-4 py-3 min-w-[3rem] text-center">{quantity}</span>
|
|
<button
|
|
onClick={() => setQuantity(quantity + 1)}
|
|
className="px-4 py-3 hover:bg-gray-50"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
|
|
{/* Add to Cart Button */}
|
|
<button
|
|
onClick={handleAddToCart}
|
|
disabled={isAdding}
|
|
className="flex-1 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors disabled:opacity-50"
|
|
>
|
|
{isAdding
|
|
? (locale === "EN" ? "Adding..." : "Dodavanje...")
|
|
: (locale === "EN" ? "Add to Cart" : "Dodaj u korpu")
|
|
}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="py-3 bg-red-50 text-red-600 text-center mb-8">
|
|
{locale === "EN" ? "Out of Stock" : "Nema na stanju"}
|
|
</div>
|
|
)}
|
|
|
|
{/* SKU */}
|
|
{variant?.sku && (
|
|
<p className="text-sm text-foreground-muted mb-4">
|
|
SKU: {variant.sku}
|
|
</p>
|
|
)}
|
|
|
|
{/* Full Description */}
|
|
{localized.description && (
|
|
<div className="border-t border-border/30 pt-6">
|
|
<h3 className="font-serif text-lg mb-4">
|
|
{locale === "EN" ? "Description" : "Opis"}
|
|
</h3>
|
|
<div
|
|
className="prose max-w-none text-foreground-muted"
|
|
dangerouslySetInnerHTML={{ __html: localized.description }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Related Products */}
|
|
{relatedProducts.length > 0 && (
|
|
<section className="py-12 px-4 bg-background-ice">
|
|
<div className="max-w-7xl mx-auto">
|
|
<h2 className="text-2xl font-serif text-center mb-8">
|
|
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
|
|
</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
|
{relatedProducts.map((relatedProduct, index) => (
|
|
<ProductCard
|
|
key={relatedProduct.id}
|
|
product={relatedProduct}
|
|
index={index}
|
|
locale={locale}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</>
|
|
);
|
|
}
|