/*! * Nuva Product Detail UI — Phase 6B * --------------------------------------------------------------------------- * Reusable product detail template. Loads product data dynamically from * NuvaProductCatalogAdmin — nothing about any specific product is hard-coded * into this file (Part 15). */ (function (root) { 'use strict'; var UI_VERSION = '1.0'; function str(v) { return typeof v === 'string' ? v : ''; } function arr(v) { return Array.isArray(v) ? v : []; } /** * Pure-data product detail builder — no DOM required. * `matchContext` is optional: pass a match object (from a recommendation * result) to also surface "why this fits" / relevant concerns; omit it for * a plain catalog view with no personalization context. */ function buildProductDetailData(productId, matchContext) { var admin = root.NuvaProductCatalogAdmin; if (!admin) throw new Error('NuvaProductDetailUI requires window.NuvaProductCatalogAdmin (Phase 6B) to be loaded first.'); if (!admin.isProductPubliclyVisible(productId)) { return { found: false, productId: productId }; } var product = admin.getProduct(productId); if (!product) return { found: false, productId: productId }; var ui = root.NuvaProductRecommendationUI; var priceLabel = ui ? ui.formatPrice(product.price, product.currency) : (product.price !== null ? String(product.price) : ''); var available = admin.isProductPurchasable(productId); return { found: true, productId: product.productId, name: product.name, category: product.category, subcategory: product.subcategory, imageUrl: product.imageUrl || '', description: str(product.description), keyIngredients: arr(product.keyIngredients), suitableSkinTypes: arr(product.suitableSkinTypes), usage: product.usage || {}, relevantConcerns: matchContext ? arr(matchContext.supportedConcerns) : arr(product.targetConcerns), compatibilityNotes: matchContext ? arr(matchContext.compatibilityNotes).concat(arr(matchContext.cautionNotes)) : [], priceLabel: priceLabel, available: available, addToCartEnabled: available, buyNowEnabled: available, productUrl: admin.getProductUrl(product), isPlaceholder: !!product.isPlaceholder }; } function el(tag, attrs, children) { var node = document.createElement(tag); attrs = attrs || {}; Object.keys(attrs).forEach(function (k) { if (k === 'class') node.className = attrs[k]; else if (k === 'text') node.textContent = attrs[k]; else node.setAttribute(k, attrs[k]); }); (children || []).forEach(function (c) { if (c) node.appendChild(c); }); return node; } function renderProductDetail(container, productId, matchContext) { if (!container) return null; container.innerHTML = ''; var data = buildProductDetailData(productId, matchContext); if (!data.found) { container.appendChild(el('p', { class: 'nuva-product-detail__not-found', text: 'This product is not currently available.' })); return data; } container.appendChild(el('img', { class: 'nuva-product-detail__image', src: data.imageUrl, alt: data.name })); container.appendChild(el('h1', { class: 'nuva-product-detail__name', text: data.name })); container.appendChild(el('p', { class: 'nuva-product-detail__price', text: data.priceLabel || 'Price unavailable' })); container.appendChild(el('p', { class: 'nuva-product-detail__availability', text: data.available ? 'Available' : 'Currently unavailable' })); container.appendChild(el('p', { class: 'nuva-product-detail__description', text: data.description })); if (data.keyIngredients.length) { container.appendChild(el('h2', { text: 'Ingredients' })); container.appendChild(el('p', { class: 'nuva-product-detail__ingredients', text: data.keyIngredients.join(', ') })); } if (data.suitableSkinTypes.length) { container.appendChild(el('h2', { text: 'Compatibility' })); container.appendChild(el('p', { class: 'nuva-product-detail__skin-types', text: 'Suitable skin types: ' + data.suitableSkinTypes.join(', ') })); } var usageParts = []; if (data.usage.morning) usageParts.push('Morning'); if (data.usage.evening) usageParts.push('Evening'); if (data.usage.frequency) usageParts.push(data.usage.frequency); if (usageParts.length || data.usage.instructions) { container.appendChild(el('h2', { text: 'How To Use' })); if (usageParts.length) container.appendChild(el('p', { class: 'nuva-product-detail__usage', text: usageParts.join(' • ') })); if (data.usage.instructions) container.appendChild(el('p', { class: 'nuva-product-detail__instructions', text: data.usage.instructions })); } if (data.compatibilityNotes.length) { container.appendChild(el('h2', { text: 'Compatibility Notes' })); data.compatibilityNotes.forEach(function (note) { container.appendChild(el('p', { class: 'nuva-product-detail__note', text: note })); }); } var actions = el('div', { class: 'nuva-product-detail__actions' }, [ el('button', { class: 'nuva-product-detail__add-to-cart', type: 'button', 'data-product-id': data.productId, 'aria-disabled': data.addToCartEnabled ? 'false' : 'true', text: data.addToCartEnabled ? 'Add to Cart' : 'Unavailable' }), el('button', { class: 'nuva-product-detail__buy-now', type: 'button', 'data-product-id': data.productId, 'aria-disabled': data.buyNowEnabled ? 'false' : 'true', text: 'Buy Now' }) ]); container.appendChild(actions); return data; } var api = { version: UI_VERSION, buildProductDetailData: buildProductDetailData, renderProductDetail: renderProductDetail }; root.NuvaProductDetailUI = api; if (typeof module !== 'undefined' && module.exports) { module.exports = api; } })(typeof window !== 'undefined' ? window : this);