ifixkart-storefront/lib/pdpContent.ts

370 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string> = {
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<string>();
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<CompareKind | 'phone', string[]> = {
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<CompareKind, string> = {
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<string, { label: string; values: string[] }>();
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<string>();
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;
}