'use client'; import React, { useState, useEffect } from 'react'; import { useParams } from 'next/navigation'; import { useProduct, type Product } from '@/hooks/use-shopify-products'; import { useShopifyCart, redirectToCheckout } from '@/hooks/use-shopify-cart'; import ProductDetailGallery from './product-detail-gallery'; import ProductDetailInfo from './product-detail-info'; import { Button } from '@/components/ui/button'; import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, } from '@/components/ui/empty'; interface ProductVariant { id: string; title: string; price: { amount: string; currencyCode: string; }; availableForSale: boolean; selectedOptions: Array<{ name: string; value: string; }>; image?: { url: string; altText?: string | null; } | null; } export type { Product }; interface ProductDetailProps { handle?: string; addToCartLabel?: string; } const ProductDetail: React.FC = ({ handle: handleProp, addToCartLabel = 'Add to Cart', }) => { const params = useParams(); const handle = handleProp || (params?.handle as string); const { addItem, openCart, checkoutUrl } = useShopifyCart(); const { product, loading, error } = useProduct(handle); const [selectedVariant, setSelectedVariant] = useState( null ); const [selectedOptions, setSelectedOptions] = useState< Record >({}); const [quantity, setQuantity] = useState(1); const [addingToCart, setAddingToCart] = useState(false); const [buyingNow, setBuyingNow] = useState(false); // Initialize variant when product loads useEffect(() => { if (product) { const firstVariant = product.variants.edges[0]?.node; if (firstVariant) { setSelectedVariant(firstVariant); const initialOptions: Record = {}; firstVariant.selectedOptions.forEach( (option: { name: string; value: string }) => { initialOptions[option.name] = option.value; } ); setSelectedOptions(initialOptions); } } }, [product]); // A value is available if some in-stock variant carries it alongside the // other currently-selected options. Options the shopper hasn't chosen yet // act as wildcards, so nothing is struck through before a full selection. const isOptionValueAvailable = (optionName: string, value: string) => { const variants = product?.variants.edges ?? []; if (variants.length === 0) return true; return variants.some(({ node }) => { if (!node.availableForSale) return false; return node.selectedOptions.every((option) => { if (option.name === optionName) return option.value === value; const selected = selectedOptions[option.name]; return selected === undefined || selected === option.value; }); }); }; const handleOptionChange = (optionName: string, value: string) => { const newOptions = { ...selectedOptions, [optionName]: value }; setSelectedOptions(newOptions); // Find matching variant const matchingVariant = product?.variants.edges.find(({ node }) => { return node.selectedOptions.every( (option) => newOptions[option.name] === option.value ); }); if (matchingVariant) { setSelectedVariant(matchingVariant.node); } }; const handleAddToCart = async () => { if (!selectedVariant || !product) return; try { setAddingToCart(true); await addItem(selectedVariant.id, quantity); openCart(); } catch (err) { console.error('Failed to add item to cart:', err); } finally { setAddingToCart(false); } }; // Adds the item, then sends the shopper straight to the Shopify checkout // (where Shop Pay is offered) rather than opening the cart drawer. const handleBuyNow = async () => { if (!selectedVariant || !product) return; try { setBuyingNow(true); const updatedCart = await addItem(selectedVariant.id, quantity); const url = updatedCart?.checkoutUrl ?? checkoutUrl; if (url) { redirectToCheckout(url); } else { openCart(); } } catch (err) { console.error('Failed to start checkout:', err); } finally { setBuyingNow(false); } }; if (loading) { return (
{/* Image Gallery Skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
))}
{/* Product Info Skeleton */}
); } if (error || !product) { return (
Product Not Found {error || 'The requested product could not be found.'}
); } return (
edge.node)} />
); }; export default ProductDetail;