feat(product): add reusable Product Experience 2.0
Unify product details modules behind config-driven contracts so teams can extend UX without changing runtime architecture or bootstrap flow. Keep backward compatibility with existing product payloads by treating new media/specification/variant/related structures as optional extensions. Improve conversion and content discoverability with reusable actions, typed media rendering, grouped specifications, dynamic variants, and multi-collection related products.
This commit is contained in:
@@ -98,6 +98,15 @@ export class ApiService {
|
||||
return c.startsWith('0x') ? '#' + c.slice(2) : c;
|
||||
}
|
||||
|
||||
private normalizeMediaType(type: unknown): 'image' | 'video' | 'pdf' | 'manual' | 'warranty' {
|
||||
const value = String(type ?? '').toLowerCase();
|
||||
if (value === 'video') return 'video';
|
||||
if (value === 'pdf') return 'pdf';
|
||||
if (value === 'manual') return 'manual';
|
||||
if (value === 'warranty') return 'warranty';
|
||||
return 'image';
|
||||
}
|
||||
|
||||
private normalizeDeliveryData(
|
||||
raw: any,
|
||||
legacyDeliveryPrice?: number
|
||||
@@ -256,6 +265,29 @@ export class ApiService {
|
||||
item.imgs = raw.imgs?.map((u: string) => this.resolveImageUrl(u))
|
||||
|| item.photos?.map((p: any) => p.url) || [];
|
||||
|
||||
const rawMedia = Array.isArray(raw.media) ? raw.media : [];
|
||||
const mediaFromPhotos = (item.photos ?? []).map((photo: any, index: number) => ({
|
||||
id: photo.id ?? `photo-${item.itemID}-${index}`,
|
||||
type: this.normalizeMediaType(photo.type ?? (photo.video ? 'video' : 'image')),
|
||||
url: this.resolveImageUrl(photo.url),
|
||||
thumbnailUrl: photo.thumbnailUrl ? this.resolveImageUrl(photo.thumbnailUrl) : undefined,
|
||||
alt: photo.alt,
|
||||
title: photo.title,
|
||||
labels: photo.labels
|
||||
}));
|
||||
|
||||
item.media = (rawMedia.length > 0 ? rawMedia : mediaFromPhotos)
|
||||
.map((entry: any, index: number) => ({
|
||||
id: entry.id ?? `media-${item.itemID}-${index}`,
|
||||
type: this.normalizeMediaType(entry.type),
|
||||
url: this.resolveImageUrl(entry.url),
|
||||
thumbnailUrl: entry.thumbnailUrl ? this.resolveImageUrl(entry.thumbnailUrl) : undefined,
|
||||
alt: entry.alt,
|
||||
title: entry.title,
|
||||
labels: entry.labels,
|
||||
}))
|
||||
.filter((entry: any) => !!entry.url);
|
||||
|
||||
// Map backOffice description (key-value array) → legacy description string
|
||||
if (Array.isArray(raw.description)) {
|
||||
item.descriptionFields = raw.description;
|
||||
@@ -287,6 +319,51 @@ export class ApiService {
|
||||
// Preserve attributes from backend
|
||||
item.attributes = raw.attributes || [];
|
||||
|
||||
item.specificationGroups = Array.isArray(raw.specificationGroups)
|
||||
? raw.specificationGroups.map((group: any, groupIndex: number) => ({
|
||||
id: group.id ?? `group-${groupIndex}`,
|
||||
key: group.key ?? `group-${groupIndex}`,
|
||||
label: group.label,
|
||||
labels: group.labels,
|
||||
attributes: Array.isArray(group.attributes)
|
||||
? group.attributes.map((attribute: any, attributeIndex: number) => ({
|
||||
key: attribute.key ?? `attribute-${attributeIndex}`,
|
||||
value: String(attribute.value ?? ''),
|
||||
label: attribute.label,
|
||||
labels: attribute.labels,
|
||||
unit: attribute.unit,
|
||||
}))
|
||||
: []
|
||||
}))
|
||||
: [];
|
||||
|
||||
item.variantOptions = Array.isArray(raw.variantOptions)
|
||||
? raw.variantOptions.map((group: any, groupIndex: number) => ({
|
||||
key: group.key ?? `variant-${groupIndex}`,
|
||||
label: group.label,
|
||||
labels: group.labels,
|
||||
options: Array.isArray(group.options)
|
||||
? group.options.map((option: any) => ({
|
||||
value: String(option.value ?? ''),
|
||||
label: option.label,
|
||||
labels: option.labels,
|
||||
available: option.available !== false,
|
||||
})).filter((option: any) => option.value.length > 0)
|
||||
: []
|
||||
})).filter((group: any) => group.options.length > 0)
|
||||
: [];
|
||||
|
||||
item.relatedCollections = Array.isArray(raw.relatedCollections)
|
||||
? raw.relatedCollections.map((collection: any, collectionIndex: number) => ({
|
||||
id: String(collection.id ?? `related-${collectionIndex}`),
|
||||
title: String(collection.title ?? ''),
|
||||
titles: collection.titles,
|
||||
products: Array.isArray(collection.products)
|
||||
? collection.products.map((productId: any) => Number(productId)).filter((productId: number) => Number.isFinite(productId))
|
||||
: []
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Preserve colour & size (only if not already set from itemDetails)
|
||||
if (!item.colour) item.colour = this.normalizeColor(raw.colour || '');
|
||||
if (!item.size) item.size = raw.size || '';
|
||||
|
||||
Reference in New Issue
Block a user