ifixkart-storefront/app/products/[slug]/page.tsx

426 lines
14 KiB
TypeScript

import React from 'react';
import { notFound } from 'next/navigation';
import { AnnouncementBar } from '@/layout/AnnouncementBar';
import { Header } from '@/layout/Header';
import { Navbar } from '@/layout/Navbar';
import { Footer } from '@/layout/Footer';
import { FloatingButtons } from '@/components/FloatingButtons';
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
import { catalogServerService } from '@/services/api/catalogServerService';
import { ProductInteractiveGrid } from '@/components/pdp/ProductInteractiveGrid';
import { ProductTabsWrapper } from '@/components/pdp/ProductTabsWrapper';
import { QuickComparisonTable } from '@/components/pdp/QuickComparisonTable';
import { YouMayAlsoLike } from '@/components/pdp/YouMayAlsoLike';
interface ProductDetailPageProps {
params: Promise<{ slug: string }>;
}
const HIDDEN_SPEC_KEYS = [
'cost_price',
'costprice',
'parent_media_key',
'parent_category',
'parent_id',
'media_key',
'sku',
'barcode',
'compare_price',
];
function normalizeSpecKey(code: string): string {
return String(code || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
}
function humanizeSpecLabel(code: string): string {
const key = normalizeSpecKey(code);
const labels: Record<string, string> = {
quality: 'Quality',
qul: 'Quality',
qual: 'Quality',
ram: 'RAM',
storage: 'Storage',
color: 'Color',
colour: 'Color',
clr: 'Color',
size: 'Size',
condition: 'Condition',
in_box_contents: 'In the box',
warranty_months: 'Warranty',
country_of_origin: 'Country of origin',
device_model: 'Device model',
device_series: 'Device series',
};
if (labels[key]) return labels[key];
return code
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.replace(/\b\w/g, (c) => c.toUpperCase());
}
function looksLikeId(value?: string | null): boolean {
if (!value) return true;
if (value.length > 18 && /[0-9]/.test(value)) return true;
if (/^[0-9A-Z]{16,}$/i.test(value.replace(/[_-]/g, ''))) return true;
return false;
}
function extractDeviceModel(product: any, specifications: any[] = []): string {
if (product.device_model_name && !looksLikeId(product.device_model_name)) {
return product.device_model_name;
}
if (typeof product.device_model === 'object' && product.device_model?.name) {
return product.device_model.name;
}
if (typeof product.device_model === 'string' && product.device_model && !looksLikeId(product.device_model)) {
return product.device_model;
}
if (Array.isArray(specifications)) {
const modelRef = specifications.find((s: any) =>
['model reference', 'device model', 'model', 'device_model', 'model_reference'].includes(
String(s.label || s.code || s.name || '').toLowerCase()
)
);
if (modelRef && modelRef.value && modelRef.value !== 'N/A' && !looksLikeId(modelRef.value)) {
return modelRef.value;
}
const seriesRef = specifications.find((s: any) =>
['series', 'device series', 'device_series'].includes(
String(s.label || s.code || s.name || '').toLowerCase()
)
);
if (seriesRef && seriesRef.value && seriesRef.value !== 'N/A' && !looksLikeId(seriesRef.value)) {
return seriesRef.value;
}
}
if (product.device_series_name && !looksLikeId(product.device_series_name)) {
return product.device_series_name;
}
if (typeof product.device_series === 'object' && product.device_series?.name) {
return product.device_series.name;
}
if (typeof product.device_series === 'string' && product.device_series && !looksLikeId(product.device_series)) {
return product.device_series;
}
const attrRows = variantAttributeRows(product);
const devAttr = attrRows.find((a) =>
['device_model', 'device_series', 'compatible_device', 'supported_device', 'model_reference', 'model'].includes(a.key)
);
if (devAttr && devAttr.value && !looksLikeId(devAttr.value)) {
return devAttr.value;
}
const title = String(product.name || product.title || '').trim();
if (title) {
const cleaned = title
.replace(/\s*(?:with\s+touch\s+screen|with\s+touch|display|screen|touch\s+glass|combo|battery|panel|housing|back\s+glass|charging\s+port|flex\s+cable|spare\s+parts|replacement)\b.*/i, '')
.replace(/^buy\s+/i, '')
.trim();
if (cleaned) return cleaned;
return title;
}
return '';
}
function variantAttributeRows(product: any): { key: string; label: string; value: string }[] {
const collected = new Map<string, Set<string>>();
(product.variants || []).forEach((variant: any) => {
(variant.attributes || []).forEach((attr: any) => {
const code = String(attr.attribute_code || '');
const key = normalizeSpecKey(code);
if (!code || HIDDEN_SPEC_KEYS.some((h) => key === h || key.includes(h))) return;
if (key.startsWith('parent_') || key.includes('cost')) return;
const value = String(attr.attribute_value || '').trim();
if (!value) return;
if (!collected.has(key)) collected.set(key, new Set());
collected.get(key)!.add(value);
});
});
return Array.from(collected.entries()).map(([key, values]) => ({
key,
label: humanizeSpecLabel(key),
value: Array.from(values).join(', '),
}));
}
function groupSpecs(specifications: any[], product: any) {
const groups: { title: string; rows: { label: string; value: string }[] }[] = [];
const attrRows = variantAttributeRows(product);
const mapped = (specifications || []).map((spec: any) => ({
group: String(spec.group || spec.section || spec.category || '').trim(),
label: String(spec.label || spec.name || spec.key || ''),
value: String(spec.value ?? spec.val ?? ''),
})).filter((s) => s.label);
const brandValue = !looksLikeId(product.brand_name)
? product.brand_name
: !looksLikeId(product.brand_id)
? product.brand_id
: 'Gallery';
const used = new Set<string>();
const take = (keys: string[]) =>
attrRows
.filter((row) => keys.includes(row.key))
.map((row) => {
used.add(row.key);
return { label: row.label, value: row.value };
});
const general = [
{ label: 'Brand', value: brandValue },
{ label: 'Model', value: product.name },
...take(['device_series', 'device_model', 'condition', 'country_of_origin']),
];
const memory = take(['ram', 'storage', 'memory']);
const appearance = take(['color', 'colour']);
const pack = [
...(product.warranty_type ? [{ label: 'Warranty Type', value: product.warranty_type }] : []),
...(product.warranty_summary ? [{ label: 'Warranty Details', value: product.warranty_summary }] : []),
...take(['in_box_contents', 'warranty_months', 'warranty']),
];
const leftover = attrRows
.filter((row) => !used.has(row.key))
.map((row) => ({ label: row.label, value: row.value }));
groups.push({ title: 'GENERAL INFORMATION', rows: general });
if (memory.length) groups.push({ title: 'MEMORY & STORAGE', rows: memory });
if (appearance.length) groups.push({ title: 'APPEARANCE', rows: appearance });
if (pack.length) groups.push({ title: 'PACKAGE & WARRANTY', rows: pack });
if (leftover.length) groups.push({ title: 'ADDITIONAL DETAILS', rows: leftover });
if (mapped.some((s) => s.group)) {
const bucket: Record<string, { label: string; value: string }[]> = {};
mapped.forEach((s) => {
const key = (s.group || 'PRODUCT DETAILS').toUpperCase();
if (!bucket[key]) bucket[key] = [];
bucket[key].push({ label: s.label, value: s.value });
});
Object.entries(bucket).forEach(([title, rows]) => {
if (rows.length) groups.push({ title, rows });
});
return groups;
}
if (mapped.length) {
groups.push({
title: 'PRODUCT DETAILS',
rows: mapped.map((s) => ({ label: s.label, value: s.value })),
});
}
return groups;
}
export default async function ProductDetailPage({ params }: ProductDetailPageProps) {
const { slug } = await params;
if (!slug) {
return notFound();
}
let data: any = null;
try {
data = await catalogServerService.getProductDetail(slug);
} catch (err) {
console.error('Failed to load product details on server:', err);
return notFound();
}
if (!data || !data.product) {
return notFound();
}
const { product, reviews = [], related_products = [], specifications = [] } = data;
const related = related_products.slice(0, 5);
const comparisonProducts = [product, ...related_products.filter((p: any) => p.product_id !== product.product_id)].slice(0, 5);
const specGroups = groupSpecs(specifications, product);
const additionalInfoRows: { label: string; value: string }[] = [];
// Supported Brand (auto-fetched)
let brandVal = product.brand_name || product.brand?.name || '';
if (looksLikeId(brandVal)) {
brandVal = '';
}
if (!brandVal) {
const brandAttr = variantAttributeRows(product).find((a) => a.key === 'brand' || a.key === 'brand_name');
if (brandAttr && !looksLikeId(brandAttr.value)) {
brandVal = brandAttr.value;
}
}
if (brandVal) {
additionalInfoRows.push({ label: 'Supported Brand', value: brandVal });
}
// Supported Device (auto-fetched exact device model)
let deviceVal = extractDeviceModel(product, specifications);
if (deviceVal) {
additionalInfoRows.push({ label: 'Supported Device', value: deviceVal });
}
const wType = product.warranty_type || 'Brand Warranty';
additionalInfoRows.push({ label: 'Warranty Type', value: wType });
if (product.warranty_summary || product.warranty_details) {
additionalInfoRows.push({
label: 'Warranty Details',
value: product.warranty_summary || product.warranty_details,
});
}
const attrRowsForPack = variantAttributeRows(product);
attrRowsForPack.forEach((attr) => {
if (['in_box_contents', 'warranty_months', 'warranty'].includes(attr.key)) {
if (!additionalInfoRows.some((p) => p.label.toLowerCase() === attr.label.toLowerCase())) {
additionalInfoRows.push({ label: attr.label, value: attr.value });
}
}
});
const descriptionHtml = (
<div>
{product.description ? (
<div className="text-[13px] text-[#555] leading-relaxed px-5 py-4">
<div
className="product-description-html prose prose-sm max-w-none text-[13px] text-[#555] leading-relaxed"
dangerouslySetInnerHTML={{ __html: product.description }}
/>
</div>
) : (
<div className="text-[13px] text-gray-400 px-5 py-6">
No description available for this product.
</div>
)}
{additionalInfoRows.length > 0 && (
<div className="border-t border-[#e0e0e0]">
<table className="w-full border-collapse text-[13px]">
<tbody>
<tr>
<th
colSpan={2}
className="text-left text-primary font-bold uppercase tracking-wide text-[13px] bg-[#f6f7f9] px-5 py-3 border-b border-[#e0e0e0]"
>
ADDITIONAL INFORMATION
</th>
</tr>
{additionalInfoRows.map((row, idx) => {
const isLast = idx === additionalInfoRows.length - 1;
return (
<tr key={`pw-${row.label}`}>
<td
className={`w-[28%] align-top font-semibold text-[#444] px-5 py-3.5 ${
isLast ? '' : 'border-b border-[#e0e0e0]'
}`}
>
{row.label}
</td>
<td
className={`align-top text-[#555] px-5 py-3.5 ${
isLast ? '' : 'border-b border-[#e0e0e0]'
}`}
>
{row.value}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
const specsHtml = (
<table className="w-full border-collapse text-[13px]">
<tbody>
{specGroups.map((group, groupIdx) => (
<React.Fragment key={group.title}>
<tr>
<th
colSpan={2}
className={`text-left text-primary font-bold uppercase tracking-wide text-[13px] bg-[#f6f7f9] px-5 py-3 border-b border-[#e0e0e0] ${
groupIdx > 0 ? 'border-t border-[#e0e0e0]' : ''
}`}
>
{group.title}
</th>
</tr>
{group.rows.map((row, rowIdx) => {
const isLast =
groupIdx === specGroups.length - 1 && rowIdx === group.rows.length - 1;
return (
<tr key={`${group.title}-${row.label}`}>
<td
className={`w-[28%] align-top font-semibold text-[#444] px-5 py-3.5 ${
isLast ? '' : 'border-b border-[#e0e0e0]'
}`}
>
{row.label}
</td>
<td
className={`align-top text-[#555] px-5 py-3.5 ${
isLast ? '' : 'border-b border-[#e0e0e0]'
}`}
>
{row.value}
</td>
</tr>
);
})}
</React.Fragment>
))}
</tbody>
</table>
);
return (
<div className="min-h-screen bg-white text-[#1E293B] flex flex-col font-sans">
<AnnouncementBar />
<Header />
<StickyHeaderSpacer />
<Navbar />
<main className="flex-grow pb-24 text-gray-800 antialiased">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
<ProductInteractiveGrid
product={product}
initialReviewsCount={reviews.length}
relatedProducts={related}
/>
<div className="mt-10">
<ProductTabsWrapper
descriptionHtml={descriptionHtml}
specsHtml={specsHtml}
showSpecifications={product.show_specifications ?? true}
/>
</div>
{related.length > 0 && <YouMayAlsoLike products={related} />}
{comparisonProducts.length > 0 && (
<QuickComparisonTable products={comparisonProducts} />
)}
</div>
</main>
<Footer />
<FloatingButtons />
</div>
);
}