feat: add product detail page with add-to-cart functionality
- Create dynamic product page at /products/[slug] - Add variant selection buttons - Add quantity controls - Implement cart management with localStorage cart_id - Update cart page with product images, quantity controls, and remove functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ca2d13d9a1
commit
286eb2f06c
|
|
@ -1,12 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
|
||||
|
||||
interface CartItem {
|
||||
id: string;
|
||||
product_slug: string;
|
||||
product_name: string;
|
||||
variant: string | null;
|
||||
variant_sku: string;
|
||||
variant_name: string | null;
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
subtotal: number;
|
||||
|
|
@ -23,45 +27,99 @@ export default function CartPage() {
|
|||
const [cart, setCart] = useState<Cart | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [checkingOut, setCheckingOut] = useState(false);
|
||||
const [updating, setUpdating] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCart = async () => {
|
||||
const cartId = localStorage.getItem("cart_id");
|
||||
if (cartId) {
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"}/cart/${cartId}`
|
||||
)
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then(setCart)
|
||||
.finally(() => setLoading(false));
|
||||
} else {
|
||||
setLoading(false);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/cart/${cartId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setCart(data);
|
||||
} else {
|
||||
// Cart expired or deleted
|
||||
localStorage.removeItem("cart_id");
|
||||
setCart(null);
|
||||
}
|
||||
} catch {
|
||||
setCart(null);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCart();
|
||||
}, []);
|
||||
|
||||
const updateQuantity = async (itemId: string, newQuantity: number) => {
|
||||
if (!cart || newQuantity < 1) return;
|
||||
|
||||
setUpdating(itemId);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/cart/${cart.id}/items/${itemId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ quantity: newQuantity }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const updatedCart = await res.json();
|
||||
setCart(updatedCart);
|
||||
}
|
||||
} catch {
|
||||
console.error("Failed to update quantity");
|
||||
} finally {
|
||||
setUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const removeItem = async (itemId: string) => {
|
||||
if (!cart) return;
|
||||
|
||||
setUpdating(itemId);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/cart/${cart.id}/items/${itemId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const updatedCart = await res.json();
|
||||
setCart(updatedCart);
|
||||
}
|
||||
} catch {
|
||||
console.error("Failed to remove item");
|
||||
} finally {
|
||||
setUpdating(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckout = async () => {
|
||||
if (!cart) return;
|
||||
setCheckingOut(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api"}/checkout/session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
cart_id: cart.id,
|
||||
success_url: `${window.location.origin}/checkout/success`,
|
||||
cancel_url: `${window.location.origin}/cart`,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const res = await fetch(`${API_URL}/checkout/session`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
cart_id: cart.id,
|
||||
success_url: `${window.location.origin}/checkout/success`,
|
||||
cancel_url: `${window.location.origin}/cart`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const { checkout_url } = await res.json();
|
||||
window.location.href = checkout_url;
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.detail || "Failed to start checkout");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Checkout error:", error);
|
||||
alert("Failed to start checkout");
|
||||
} finally {
|
||||
setCheckingOut(false);
|
||||
}
|
||||
|
|
@ -69,8 +127,10 @@ export default function CartPage() {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16 text-center">
|
||||
<p>Loading cart...</p>
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -80,9 +140,12 @@ export default function CartPage() {
|
|||
<div className="container mx-auto px-4 py-16 text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Your Cart</h1>
|
||||
<p className="text-muted-foreground mb-8">Your cart is empty.</p>
|
||||
<a href="/products" className="text-primary hover:underline">
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -91,23 +154,71 @@ export default function CartPage() {
|
|||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-8">Your Cart</h1>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{cart.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-4 p-4 border rounded-lg"
|
||||
className={`flex items-center gap-4 p-4 border rounded-lg ${
|
||||
updating === item.id ? "opacity-50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="w-20 h-20 bg-muted rounded" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold">{item.product_name}</h3>
|
||||
{item.variant && (
|
||||
{/* Product Image */}
|
||||
<Link href={`/products/${item.product_slug}`}>
|
||||
<div className="w-24 h-24 bg-muted rounded-lg overflow-hidden flex-shrink-0">
|
||||
<img
|
||||
src={`${API_URL}/designs/${item.product_slug}/image`}
|
||||
alt={item.product_name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link
|
||||
href={`/products/${item.product_slug}`}
|
||||
className="font-semibold hover:text-primary transition-colors"
|
||||
>
|
||||
{item.product_name}
|
||||
</Link>
|
||||
{item.variant_name && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Variant: {item.variant}
|
||||
{item.variant_name}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm">Qty: {item.quantity}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
${item.unit_price.toFixed(2)} each
|
||||
</p>
|
||||
|
||||
{/* Quantity Controls */}
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||||
disabled={updating === item.id || item.quantity <= 1}
|
||||
className="w-8 h-8 rounded border flex items-center justify-center hover:bg-muted transition-colors disabled:opacity-50"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="w-8 text-center">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||
disabled={updating === item.id}
|
||||
className="w-8 h-8 rounded border flex items-center justify-center hover:bg-muted transition-colors disabled:opacity-50"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
disabled={updating === item.id}
|
||||
className="ml-4 text-sm text-red-600 hover:text-red-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtotal */}
|
||||
<div className="text-right">
|
||||
<p className="font-bold">${item.subtotal.toFixed(2)}</p>
|
||||
</div>
|
||||
|
|
@ -115,31 +226,67 @@ export default function CartPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-6 h-fit">
|
||||
<h2 className="font-bold mb-4">Order Summary</h2>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>${cart.subtotal.toFixed(2)}</span>
|
||||
{/* Order Summary */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="border rounded-lg p-6 sticky top-4">
|
||||
<h2 className="font-bold text-lg mb-4">Order Summary</h2>
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
Subtotal ({cart.item_count} item{cart.item_count !== 1 ? "s" : ""})
|
||||
</span>
|
||||
<span>${cart.subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Shipping</span>
|
||||
<span>Calculated at checkout</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Shipping</span>
|
||||
<span>Calculated at checkout</span>
|
||||
<div className="border-t pt-4 mb-6">
|
||||
<div className="flex justify-between font-bold text-lg">
|
||||
<span>Total</span>
|
||||
<span>${cart.subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
disabled={checkingOut}
|
||||
className="w-full bg-primary text-primary-foreground py-3 rounded-md font-medium hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{checkingOut ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
Processing...
|
||||
</span>
|
||||
) : (
|
||||
"Proceed to Checkout"
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
href="/products"
|
||||
className="block text-center mt-4 text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
<div className="border-t pt-4 mb-4">
|
||||
<div className="flex justify-between font-bold">
|
||||
<span>Total</span>
|
||||
<span>${cart.subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
disabled={checkingOut}
|
||||
className="w-full bg-primary text-primary-foreground py-3 rounded-md font-medium hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{checkingOut ? "Redirecting..." : "Checkout"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,329 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
|
||||
|
||||
interface ProductVariant {
|
||||
name: string;
|
||||
sku: string;
|
||||
provider: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface Product {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string;
|
||||
product_type: string;
|
||||
tags: string[];
|
||||
image_url: string;
|
||||
base_price: number;
|
||||
variants: ProductVariant[];
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export default function ProductPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const slug = params.slug as string;
|
||||
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [addingToCart, setAddingToCart] = useState(false);
|
||||
const [addedToCart, setAddedToCart] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchProduct() {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/products/${slug}`);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) {
|
||||
setError("Product not found");
|
||||
} else {
|
||||
setError("Failed to load product");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
setProduct(data);
|
||||
if (data.variants && data.variants.length > 0) {
|
||||
setSelectedVariant(data.variants[0]);
|
||||
}
|
||||
} catch {
|
||||
setError("Failed to load product");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (slug) {
|
||||
fetchProduct();
|
||||
}
|
||||
}, [slug]);
|
||||
|
||||
const getOrCreateCart = async (): Promise<string | null> => {
|
||||
// Check for existing cart in localStorage
|
||||
let cartId = localStorage.getItem("cart_id");
|
||||
|
||||
if (cartId) {
|
||||
// Verify cart still exists
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/cart/${cartId}`);
|
||||
if (res.ok) {
|
||||
return cartId;
|
||||
}
|
||||
} catch {
|
||||
// Cart doesn't exist, create new one
|
||||
}
|
||||
}
|
||||
|
||||
// Create new cart
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/cart`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
cartId = data.id;
|
||||
localStorage.setItem("cart_id", cartId!);
|
||||
return cartId;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleAddToCart = async () => {
|
||||
if (!product || !selectedVariant) return;
|
||||
|
||||
setAddingToCart(true);
|
||||
try {
|
||||
const cartId = await getOrCreateCart();
|
||||
if (!cartId) {
|
||||
alert("Failed to create cart");
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_URL}/cart/${cartId}/items`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_slug: product.slug,
|
||||
variant_sku: selectedVariant.sku,
|
||||
quantity: quantity,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setAddedToCart(true);
|
||||
setTimeout(() => setAddedToCart(false), 3000);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.detail || "Failed to add to cart");
|
||||
}
|
||||
} catch {
|
||||
alert("Failed to add to cart");
|
||||
} finally {
|
||||
setAddingToCart(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !product) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">{error || "Product not found"}</h1>
|
||||
<Link
|
||||
href="/products"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Back to Products
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-8 text-sm">
|
||||
<Link href="/" className="text-muted-foreground hover:text-primary">
|
||||
Home
|
||||
</Link>
|
||||
<span className="mx-2 text-muted-foreground">/</span>
|
||||
<Link href="/products" className="text-muted-foreground hover:text-primary">
|
||||
Products
|
||||
</Link>
|
||||
<span className="mx-2 text-muted-foreground">/</span>
|
||||
<span className="text-foreground">{product.name}</span>
|
||||
</nav>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-12">
|
||||
{/* Product Image */}
|
||||
<div className="aspect-square bg-muted rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={`${API_URL}/designs/${product.slug}/image`}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Product Details */}
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<span className="text-sm text-muted-foreground capitalize">
|
||||
{product.category} / {product.product_type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold mb-4">{product.name}</h1>
|
||||
|
||||
<p className="text-muted-foreground mb-6">{product.description}</p>
|
||||
|
||||
<div className="text-3xl font-bold mb-6">
|
||||
${selectedVariant?.price.toFixed(2) || product.base_price.toFixed(2)}
|
||||
</div>
|
||||
|
||||
{/* Variant Selection */}
|
||||
{product.variants && product.variants.length > 1 && (
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Select Option
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{product.variants.map((variant) => (
|
||||
<button
|
||||
key={variant.sku}
|
||||
onClick={() => setSelectedVariant(variant)}
|
||||
className={`px-4 py-2 rounded-md border transition-colors ${
|
||||
selectedVariant?.sku === variant.sku
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-muted-foreground/30 hover:border-primary"
|
||||
}`}
|
||||
>
|
||||
{variant.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quantity */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium mb-2">Quantity</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="w-10 h-10 rounded-md border flex items-center justify-center hover:bg-muted transition-colors"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="w-12 text-center font-medium">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="w-10 h-10 rounded-md border flex items-center justify-center hover:bg-muted transition-colors"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={addingToCart || !selectedVariant}
|
||||
className={`w-full py-4 rounded-md font-medium transition-colors ${
|
||||
addedToCart
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{addingToCart ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<svg
|
||||
className="animate-spin h-5 w-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
Adding...
|
||||
</span>
|
||||
) : addedToCart ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Added to Cart!
|
||||
</span>
|
||||
) : (
|
||||
"Add to Cart"
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* View Cart Link */}
|
||||
{addedToCart && (
|
||||
<Link
|
||||
href="/cart"
|
||||
className="block text-center mt-4 text-primary hover:underline"
|
||||
>
|
||||
View Cart
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{product.tags && product.tags.length > 0 && (
|
||||
<div className="mt-8 pt-6 border-t">
|
||||
<span className="text-sm text-muted-foreground">Tags: </span>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{product.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-1 text-xs bg-muted rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue