import { parseMetadata, resolveIsActive } from '@/lib/layoutConfig'; import { detectProductKind, type CompareKind } from '@/lib/compareSpecs'; export type PdpFact = { label: string; value: string }; export type PdpTrustBadge = { title: string; icon?: string }; const FACT_PRIORITY = [ 'ram', 'storage', 'memory', 'color', 'colour', 'condition', 'size', 'warranty', 'warranty_months', ]; function normalizeKey(value?: string | null): string { return String(value || '') .trim() .toLowerCase() .replace(/[\s-]+/g, '_'); } const MISSING_SPEC_VALUES = new Set([ 'n/a', 'na', 'n.a', 'n.a.', '-', '--', '—', '–', 'none', 'null', 'undefined', 'not available', 'not applicable', 'tbd', 'unknown', ]); export function isMissingSpecValue(value?: string | null): boolean { const trimmed = String(value ?? '').trim(); if (!trimmed) return true; const compact = trimmed.toLowerCase().replace(/[_/]+/g, ' ').replace(/\s+/g, ' ').trim(); return MISSING_SPEC_VALUES.has(compact) || MISSING_SPEC_VALUES.has(compact.replace(/\s/g, '')); } function prettyValue(label: string, value: string): string { const trimmed = value.trim(); if (isMissingSpecValue(trimmed)) return ''; if (normalizeKey(label) === 'color' || normalizeKey(label) === 'colour') { return trimmed.replace(/\b\w/g, (c) => c.toUpperCase()); } return trimmed; } function factLabel(code: string, name?: string): string { const key = normalizeKey(name || code); const labels: Record = { quality: 'Quality', qul: 'Quality', qual: 'Quality', memory: 'Memory', ram: 'RAM', storage: 'Storage', color: 'Color', colour: 'Color', clr: 'Color', condition: 'Condition', size: 'Size', warranty: 'Warranty', warranty_months: 'Warranty', compatible_with: 'Compatible with', type: 'Type', connector: 'Connector', output: 'Output', material: 'Material', processor: 'Processor', display: 'Display', connectivity: 'Connectivity', series: 'Series', }; if (labels[key]) return labels[key]; if (name && name.trim()) return name.trim(); return code.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); } export function extractPdpFacts( product: { variants?: any[]; specifications?: { label?: string; value?: string }[]; }, selectedVariant?: any | null, extraSpecs: { label: string; value: string }[] = [] ): PdpFact[] { const variant = selectedVariant || product.variants?.[0] || null; const facts: PdpFact[] = []; const seen = new Set(); const push = (label: string, value: string) => { const pretty = prettyValue(label, value); const key = normalizeKey(label); if (!pretty || seen.has(key)) return; seen.add(key); facts.push({ label, value: pretty }); }; (variant?.attributes || []).forEach((attr: any) => { const key = normalizeKey(attr.attribute_code || attr.attribute_name); if (!FACT_PRIORITY.includes(key)) return; push(factLabel(attr.attribute_code || '', attr.attribute_name), String(attr.attribute_value || '')); }); extraSpecs.forEach((spec) => { const key = normalizeKey(spec.label); if (!['series', 'model', 'model_reference', 'condition'].includes(key)) return; push(spec.label, spec.value); }); (product.specifications || []).forEach((spec) => { const key = normalizeKey(spec.label); if (!['series', 'model', 'model_reference', 'condition'].includes(key)) return; push(String(spec.label || ''), String(spec.value || '')); }); return facts.slice(0, 4); } const QUICK_VIEW_KIND_KEYS: Record = { phone: ['ram', 'storage', 'color', 'condition', 'series'], laptop: ['processor', 'ram', 'storage', 'display'], watch: ['size', 'color', 'compatible_with', 'condition'], case: ['compatible_with', 'color', 'material', 'condition'], camera: ['compatible_with', 'type', 'color', 'condition'], charger: ['connector', 'output', 'compatible_with', 'color'], protector: ['compatible_with', 'material', 'type'], audio: ['compatible_with', 'connectivity', 'color'], speaker: ['compatible_with', 'connectivity', 'color'], general: ['compatible_with', 'type', 'color', 'condition', 'size'], }; const QUICK_VIEW_TYPE_LABEL: Record = { audio: 'Headphones', speaker: 'Speaker', case: 'Phone case', camera: 'Mobile camera', charger: 'Charger / cable', phone: 'Smartphone', laptop: 'Laptop', watch: 'Smartwatch', protector: 'Screen protector', general: 'Accessory', }; function resolveQuickViewKind(product: { device_type?: string | null; name?: string; slug?: string; full_path?: string }): CompareKind { const deviceType = normalizeKey(product.device_type); if (['mobile', 'phone', 'smartphone', 'feature_phone', 'tablet', 'ipad'].includes(deviceType)) { return 'phone'; } if (['laptop', 'notebook', 'ultrabook'].includes(deviceType)) return 'laptop'; if (['watch', 'smartwatch', 'wearable'].includes(deviceType)) return 'watch'; return detectProductKind(product as any); } export function extractQuickViewSpecs( product: { name?: string; device_type?: string | null; variants?: any[]; specifications?: { label?: string; value?: string }[]; compatible_devices?: { name?: string }[]; }, selectedVariant?: any | null, extraSpecs: { label?: string; value?: string }[] = [] ): PdpFact[] { const kind = resolveQuickViewKind(product); const wanted = QUICK_VIEW_KIND_KEYS[kind] || QUICK_VIEW_KIND_KEYS.general; const bag = new Map(); const add = (rawLabel: string, rawValue: string) => { const label = factLabel(rawLabel, rawLabel); const pretty = prettyValue(label, rawValue); const key = normalizeKey(label); if (!pretty || key === 'brand' || key === 'manufacturer') return; if (!bag.has(key)) bag.set(key, { label, values: [] }); const entry = bag.get(key)!; if (!entry.values.some((value) => value.toLowerCase() === pretty.toLowerCase())) { entry.values.push(pretty); } }; const variant = selectedVariant || product.variants?.[0] || null; (variant?.attributes || []).forEach((attr: any) => { add(attr.attribute_name || attr.attribute_code || '', String(attr.attribute_value || '')); }); (product.variants || []).forEach((item: any) => { (item?.attributes || []).forEach((attr: any) => { add(attr.attribute_name || attr.attribute_code || '', String(attr.attribute_value || '')); }); }); [...extraSpecs, ...(product.specifications || [])].forEach((spec) => { add(String(spec.label || ''), String(spec.value || '')); }); const deviceNames = (product.compatible_devices || []) .map((device) => String(device?.name || '').trim()) .filter((name) => name && !isMissingSpecValue(name) && name.length > 1); if (deviceNames.length) { add('Compatible with', deviceNames.slice(0, 2).join(', ')); } const forMatch = String(product.name || '').match(/\bfor\s+(.+)$/i); if (forMatch?.[1]) add('Compatible with', forMatch[1].replace(/\s+/g, ' ').trim()); if (kind !== 'phone' && kind !== 'laptop' && kind !== 'general') { add('Type', QUICK_VIEW_TYPE_LABEL[kind]); } const facts: PdpFact[] = []; const used = new Set(); wanted.forEach((key) => { const entry = bag.get(key); if (!entry || used.has(key)) return; used.add(key); facts.push({ label: entry.label, value: entry.values.slice(0, 3).join(', '), }); }); return facts.slice(0, 4); } const TRUST_DEVICE_TYPES = new Set([ 'mobile', 'phone', 'smartphone', 'feature_phone', 'laptop', 'notebook', 'ultrabook', 'tablet', 'ipad', 'watch', 'smartwatch', 'wearable', 'desktop', 'pc', 'computer', 'tv', 'television', 'console', 'gaming', ]); const TRUST_CATEGORY_KEYS = new Set([ 'phones', 'phone', 'mobiles', 'mobile', 'smartphones', 'laptops', 'laptop', 'notebooks', 'tablets', 'tablet', 'computers', 'wearables', 'smart_watches', 'smartwatches', 'spare_parts', 'mobile_display', 'mobile_battery', 'mother_board', 'motherboard', 'mobile_back_panel', 'mobile_camera', ]); const ACCESSORY_CATEGORY_KEYS = new Set([ 'accessories', 'accessory', 'back_covers', 'back_cover', 'mobile_cases', 'mobile_case', 'cases', 'screen_guards', 'screen_guard', 'camera_lens_guards', 'usb_c_cables', 'charging_adapters', 'mobile_charger', 'charging', 'cables', 'earbuds', 'headphones', 'audio', 'bluetooth_speaker', ]); function categoryChainKeys( product: { category_id?: string; full_path?: string; category?: { slug?: string; name?: string } }, categories: { category_id: string; parent_category_id?: string | null; slug?: string; name?: string }[] = [] ): string[] { const keys: string[] = []; const byId = new Map(categories.map((cat) => [cat.category_id, cat])); let current = product.category_id ? byId.get(product.category_id) : undefined; while (current) { keys.push(normalizeKey(current.slug), normalizeKey(current.name)); current = current.parent_category_id ? byId.get(current.parent_category_id) : undefined; } const firstPath = String(product.full_path || '') .split('/') .map((part) => part.trim()) .filter(Boolean)[0]; if (firstPath) keys.push(normalizeKey(firstPath)); if (product.category?.slug) keys.push(normalizeKey(product.category.slug)); if (product.category?.name) keys.push(normalizeKey(product.category.name)); return keys.filter(Boolean); } export function productShowsTrustBadges( product: { device_type?: string | null; category_id?: string; full_path?: string; category?: { slug?: string; name?: string }; }, categories: { category_id: string; parent_category_id?: string | null; slug?: string; name?: string }[] = [] ): boolean { return true; } const DEFAULT_HOMEPAGE_TRUST_BADGES: PdpTrustBadge[] = [ { title: '6-Month Spare Parts Warranty', icon: 'ShieldCheck' }, { title: 'Skilled Tech Diagnostics', icon: 'Wrench' }, { title: 'Fast Doorstep Repairs', icon: 'Clock' }, ]; export function parseTrustBadgesFromLayout(layoutData: unknown): PdpTrustBadge[] { const rows = Array.isArray(layoutData) ? layoutData : []; const section = rows.find((item: any) => { const region = String(item?.region || item?.metadata?.section_key || item?.type || '').toLowerCase(); return region === 'trust_badges' || region === 'badges' || region === 'sec_trust'; }); if (section && resolveIsActive(section) === false) return []; if (section) { const meta = parseMetadata(section); const features = Array.isArray(meta.features) ? meta.features : []; const parsed = features .map((item: any) => ({ title: String(item?.title || '').trim(), icon: String(item?.icon || '').trim() || undefined, })) .filter((item: PdpTrustBadge) => item.title) .slice(0, 3); if (parsed.length > 0) return parsed; } return DEFAULT_HOMEPAGE_TRUST_BADGES; }