'use client'; import { Download, FileText, Image as ImageIcon } from 'lucide-react'; import { CdnImage as Image } from '@/ui/shared/cdn-image'; import type { Product, ProductMetafield, DownloadFile } from 'brainerce'; import { useProductPage } from '@/core/hooks/use-product-page'; import { PriceDisplay } from '@/ui/product/price-display'; import { LoadingSpinner } from '@/ui/shared/loading-spinner'; import { VariantSelector } from '@/ui/product/variant-selector'; import { StockBadge } from '@/ui/product/stock-badge'; import { DiscountBadge } from '@/ui/product/discount-badge'; import { BackInStockForm, canOfferStockAlert } from '@/ui/product/back-in-stock-form'; import { RecommendationSection } from '@/ui/product/recommendation-section'; import { FrequentlyBoughtTogether } from '@/ui/product/frequently-bought-together'; import { CustomizationFields } from '@/ui/product/customization-fields'; import { ModifierGroupSelector } from '@/ui/product/modifier-group-selector'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import { useTranslations } from '@/core/lib/translations'; import { cn } from '@/core/lib/utils'; import { sanitizeProductHtml } from '@/core/lib/sanitize-html'; /** Render a metafield value based on its type */ function MetafieldValue({ field }: { field: ProductMetafield }) { const tc = useTranslations('common'); switch (field.type) { case 'IMAGE': { if (!field.value) return -; return ( {field.definitionName} ); } case 'GALLERY': { let urls: string[] = []; try { const parsed = JSON.parse(field.value); urls = Array.isArray(parsed) ? parsed.filter((u: unknown) => typeof u === 'string' && u) : []; } catch { urls = field.value ? [field.value] : []; } if (urls.length === 0) return -; return (
{urls.map((url, i) => ( {`${field.definitionName} ))}
); } case 'URL': return field.value ? ( {field.value} ) : ( - ); case 'COLOR': return field.value ? ( {field.value} ) : ( - ); case 'BOOLEAN': return {field.value === 'true' ? tc('yes') : tc('no')}; case 'DATE': case 'DATETIME': { if (!field.value) return -; try { const date = new Date(field.value); return ( {field.type === 'DATETIME' ? date.toLocaleString() : date.toLocaleDateString()} ); } catch { return {field.value}; } } default: return {field.value || '-'}; } } interface ProductClientSectionProps { product: Product; /** * From `getStoreInfo().stockAlertsEnabled` — the merchant's switch for * back-in-stock alerts on this storefront. Passed down rather than fetched * here because the page already holds storeInfo and a second call would be a * waterfall. Undefined on a public-storefront (storeId) client, where the * setting does not apply and the feature is on. */ stockAlertsEnabled?: boolean; } export function ProductClientSection({ product: initialProduct, stockAlertsEnabled, }: ProductClientSectionProps) { const t = useTranslations('productDetail'); const { product, recommendations, images, selectedImageIndex, setSelectedImageIndex, mainImageUrl, selectedVariant, setSelectedVariant, priceInfo, displayPrice, inventory, canPurchase, description, quantity, setQuantity, addingToCart, addedMessage, addToCartError, handleAddToCart, customizationFields, customizationValues, setCustomizationValues, customizationErrors, modifierGroups, modifierSelections, setModifierSelection, modifierError, } = useProductPage(initialProduct); return (
{/* Image Gallery */}
{/* Main Image */}
{mainImageUrl ? ( {product.name} ) : (
)}
{/* Thumbnails */} {images.length > 1 && (
{images.map((img, idx) => ( ))}
)}
{/* Product Info */}
{/* Categories */} {product.categories && product.categories.length > 0 && (
{product.categories.map((cat) => ( {cat.name} ))}
)} {/* Brand */} {(product as { brands?: Array<{ id: string; name: string }> }).brands && (product as { brands: Array<{ id: string; name: string }> }).brands.length > 0 && (
{t('by')}{' '} {(product as { brands: Array<{ id: string; name: string }> }).brands .map((b) => b.name) .join(', ')}
)} {/* Title */}

{product.name}

{/* Tags */} {(product as unknown as { tags?: Array<{ id: string; name: string }> }).tags && (product as unknown as { tags: Array<{ id: string; name: string }> }).tags.length > 0 && (
{(product as unknown as { tags: Array<{ id: string; name: string }> }).tags.map( (tag) => ( #{tag.name} ) )}
)} {/* Active discount rule on this product, as a badge. `product.discount` is returned by getProductBySlug and is null/absent when no rule applies, and DiscountBadge returns null on a falsy `discount`, so this renders nothing on a store with no discounts configured. */} {/* Price */} {/* Stock / Digital badge */} {product.isDownloadable ? ( ) : ( )} {/* Downloadable files info */} {product.isDownloadable && product.downloads && product.downloads.length > 0 && (

{t('filesIncluded')} ({product.downloads.length})

    {product.downloads.map((file: DownloadFile) => (
  • ))}
)} {/* Variant Selector */} {/* A KIT is bought as ONE line, but the shopper needs to see what is in the box before deciding. These rows are display only — never add them to the cart individually; the kit reserves its components on its own. */} {product.type === 'KIT' && product.kitComponents?.length ? (

{t('whatsInTheBox')}

    {product.kitComponents.map((c) => (
  • {c.image ? ( {c.name} ) : (
    )} {c.name} x{c.quantity}
  • ))}
) : null} {product.type === 'VARIABLE' && product.variants && product.variants.length > 0 && ( )} {/* Customization Fields (buyer input) */} {customizationFields.length > 0 && ( )} {/* Modifier groups (toppings, sauce, bread type, …) */} {modifierGroups.length > 0 && (
{modifierGroups.map((group) => ( setModifierSelection(group.id, next)} disabled={addingToCart} /> ))} {modifierError && (

{modifierError}

)}
)} {/* Quantity + Add to Cart */}
{quantity}
{/* Why the add was refused. Deliberately OUTSIDE the modifier block above: that block only renders when the product HAS modifier groups, so a refusal shown there would be invisible on almost every product and the shopper would just watch the button reset. */} {addToCartError && (

{addToCartError === 'CART_FULL' ? t('cartFull') : t('addToCartFailed')}

)} {/* Sold out is not the end of the page. `canOfferStockAlert` is the whole gate — it also covers the merchant's switch and backorderable items, where an alert would tell someone to come back and do what they can already do. The selected variant is passed on purpose: without it a shopper who wanted the medium gets mailed when the small returns. */} {canOfferStockAlert(inventory, stockAlertsEnabled) && ( )} {/* Download after purchase note */} {product.isDownloadable && (

{t('downloadAfterPurchase')}

)} {/* Description */} {description && (

{t('description')}

{'html' in description ? ( // Security: merchant-authored product description HTML, // sanitized via `sanitizeProductHtml` (isomorphic-dompurify, // allowlisted tags) before injecting.
) : (

{description.text}

)}
)} {/* Metafields / Specifications */} {product.metafields && product.metafields.length > 0 && (

{t('specifications')}

{product.metafields.map((field) => ( ))}
{field.definitionName}
)}
{/* Frequently Bought Together (cross-sells) */} {recommendations?.crossSells && recommendations.crossSells.length > 0 && ( )} {/* Upsells */} {recommendations?.upsells && recommendations.upsells.length > 0 && ( )} {/* Related products */} {recommendations?.related && recommendations.related.length > 0 && ( )}
); }