'use client'; 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 { 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 { useTranslations } from '@/core/lib/translations'; 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 switch for * back-in-stock alerts on this storefront. 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 (
{/* DESIGN ME — product page (PDP): gallery, buy box (price/stock/variants/customization/modifiers/quantity/add-to-cart), description, specs, cross-sells; everything comes from useProductPage(initialProduct). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
{/* Image Gallery */}
{/* `relative` + `aspect-square` are layout-critical for next/image fill */}
{mainImageUrl ? ( {product.name} ) : ( {product.name} )}
{/* Thumbnails */} {images.length > 1 && (
{images.map((img, idx) => ( ))}
)}
{product.name}
{/* Product Info / buy box */}
{/* 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 ? (

{t('instantDownload')}

) : ( )} {/* Downloadable files info */} {product.isDownloadable && product.downloads && product.downloads.length > 0 && (

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

    {product.downloads.map((file: DownloadFile) => (
  • {file.name} {file.size && ( {' '} ( {file.size < 1024 * 1024 ? `${(file.size / 1024).toFixed(0)} KB` : `${(file.size / (1024 * 1024)).toFixed(1)} MB`} ) )}
  • ))}
)} {/* 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. Pass the selected variant: without it a shopper who wanted the medium is 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 && ( )}
); }