import { useState, useEffect } from 'react' import { useParams, Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' import Navbar from '../components/Navbar' import Footer from '../components/Footer' import ProductCard from '../components/ProductCard' import ProductReviews from '../components/ProductReviews' import ProductImageGallery from '../components/ProductImageGallery' import DynamicPageRenderer from '../components/DynamicPageRenderer' import { API_BASE, getCategoryImage, formatPrice, type Category, type Product, type PageSection, type ProductAttribute, type TranslatableString } from '../types' import { useCartStore } from '../store/cartStore' import { marked } from 'marked' import DOMPurify from 'dompurify' /** แปลง TranslatableString (string | Record) เป็น string ตาม locale */ function resolveTranslatable(value: TranslatableString | null | undefined, lang: string): string { if (!value) return '' if (typeof value === 'string') return value // Record: ลอง lang เต็ม → ลอง prefix 2 double-char → fallback 'th'/'en' return value[lang] ?? value[lang.slice(0, 2)] ?? value['th'] ?? value['en'] ?? Object.values(value)[0] ?? '' } function resolveOptionValue(val: string, options: any[] | null | undefined, lang: string): string { if (!val) return ''; if (!options || !Array.isArray(options)) return val; for (const option of options) { if (!option.values || !Array.isArray(option.values)) continue; for (const v of option.values) { const baseStr = typeof v === 'string' ? v : (v['th'] || v['en'] || Object.values(v)[0] || ''); if (baseStr === val) { if (typeof v === 'string') return v; return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? baseStr; } } } return val; } function formatAttributeValue(attribute: ProductAttribute): string { const value = attribute.value if (value === null || value === undefined || value === '') return '' if (typeof value === 'boolean') return value ? 'Yes' : 'No' if (typeof value === 'number') { const unit = attribute.attributeDefinition.unit ? ` ${attribute.attributeDefinition.unit}` : '' return `${value}${unit}` } if (typeof value === 'string') return value return JSON.stringify(value) } export default function ProductDetailPage() { const { id } = useParams<{ id: string }>() const [categories, setCategories] = useState([]) const [product, setProduct] = useState(null) const [related, setRelated] = useState([]) const [selectedVariant, setSelectedVariant] = useState(0) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [cmsSections, setCmsSections] = useState([]) const { addToCart } = useCartStore() const { i18n, t } = useTranslation() function resolveStr(v: TranslatableString | null | undefined, lang: string): string { if (!v) return '' if (typeof v === 'string') return v return v[lang] ?? v[lang.slice(0, 2)] ?? v['th'] ?? v['en'] ?? Object.values(v)[0] ?? '' } const displayName = resolveStr(product?.name, i18n.language) || product?.brand?.name || '' useEffect(() => { fetch(`${API_BASE}/categories?lang=${i18n.language}`) .then((r) => r.json()) .then(setCategories) .catch(console.error) }, [i18n.language]) useEffect(() => { fetch(`${API_BASE}/pages/product-detail-layout?lang=${i18n.language}`) .then((r) => { if (!r.ok) throw new Error('Page not found'); return r.json(); }) .then((data) => { if (data && data.sections) { setCmsSections(data.sections); } }) .catch((err) => { console.log('Product detail dynamic layout sections not loaded:', err.message); }); }, [i18n.language]) useEffect(() => { if (!id) return setLoading(true) setError(null) setSelectedVariant(0) fetch(`${API_BASE}/products/${id}?lang=${i18n.language}`) .then(async (r) => { if (!r.ok) throw new Error('Product not found') return r.json() }) .then(async (data: Product) => { setProduct(data) // Fetch related (same category, different product) const relRes = await fetch(`${API_BASE}/products?categoryId=${data.category.id}&lang=${i18n.language}`) const relJson = await relRes.json() const relData: Product[] = relJson.data || [] setRelated(relData.filter((p) => p.id !== data.id).slice(0, 6)) }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) }, [id, i18n.language]) if (loading) { return (
) } if (error || !product) { return (

⚠️

{error ?? t('product.notFound')}

{t('common.backToShop')}
) } const currentVariant = product.variants[selectedVariant] const visibleAttributes = (product.attributes || []).filter((attribute) => formatAttributeValue(attribute)) const handleAddToCart = () => { if (!product || !currentVariant) return addToCart({ productId: product.id, variantId: currentVariant.id, quantity: 1, price: currentVariant.price || 0, productName: displayName, variantName: [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / '), productImage: product.images?.[0]?.url || getCategoryImage(typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.en || '') }) } const announcementSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() === 'ANNOUNCEMENT_BAR'); const bottomSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR'); return (
{announcementSections.length > 0 && ( )}
{/* Breadcrumb */}
{t('common.home')} / {t('common.shop')} / {typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''} / {displayName}
{/* Product Detail */}
{/* Image Gallery */}
{/* Info */}

{typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}

{displayName}

{t('product.premiumCollection')}

{resolveTranslatable(product.description, i18n.language) && (

{resolveTranslatable(product.description, i18n.language)}

)} {/* Price */}
{formatPrice(currentVariant?.price)} {currentVariant && ( / {[currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).map(opt => resolveOptionValue(opt, product.options, i18n.language)).join(' / ')} )}
{visibleAttributes.length > 0 && (

Product Specs

Specifications

{visibleAttributes.map((attribute) => { const label = typeof attribute.attributeDefinition.label === 'string' ? attribute.attributeDefinition.label : resolveTranslatable(attribute.attributeDefinition.label, i18n.language) return (
{label} {formatAttributeValue(attribute)}
) })}
)} {/* Variant Selector */}

{t('product.selectSize')}

{product.variants.map((v, i) => ( ))}
{/* Actions */}
{/* Meta */}
{t('product.brand', 'Brand')} {product.brand?.name || ''}
{t('product.category')} {typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.en || ''}
{/* Full Details Section */} {resolveTranslatable(product.details, i18n.language) && (

{t('product.details')}

)} {/* Reviews Section */} {/* Related Products */} {related.length > 0 && (

{t('product.moreRelated', { category: typeof product.category.name === 'string' ? product.category.name : (product.category.name as any)?.[i18n.language] || (product.category.name as any)?.['en'] })}

{t('product.relatedProducts')}

{t('common.viewAll')}
{related.map((p) => ( ))}
)}
{bottomSections.length > 0 && ( )}
) }