358 lines
13 KiB
TypeScript
358 lines
13 KiB
TypeScript
export interface ProductImage {
|
|
imageId: string;
|
|
imageLabel: string;
|
|
imageTag: string;
|
|
imageUrl: string;
|
|
imageText: string;
|
|
imageLastModified: string;
|
|
}
|
|
|
|
export interface CommercialOffer {
|
|
Price: number;
|
|
ListPrice: number;
|
|
PriceWithoutDiscount: number;
|
|
FullSellingPrice: number;
|
|
RewardValue: number;
|
|
PriceValidUntil: string;
|
|
AvailableQuantity: number;
|
|
IsAvailable: boolean;
|
|
GiftSkuIds: string[];
|
|
Installments: {
|
|
Value: number;
|
|
InterestRate: number;
|
|
TotalValuePlusInterestRate: number;
|
|
NumberOfInstallments: number;
|
|
}[];
|
|
discountHighLight: unknown[];
|
|
teasers: unknown[];
|
|
}
|
|
|
|
export interface ProductSeller {
|
|
sellerId: string;
|
|
sellerName: string;
|
|
addToCartLink: string;
|
|
sellerDefault: boolean;
|
|
commertialOffer: CommercialOffer;
|
|
}
|
|
|
|
export interface ProductItem {
|
|
itemId: string;
|
|
name: string;
|
|
nameComplete: string;
|
|
complementName: string;
|
|
ean: string;
|
|
referenceId: { Key: string; Value: string }[];
|
|
measurementUnit: string;
|
|
unitMultiplier: number;
|
|
modalType: null;
|
|
isKit: boolean;
|
|
images: ProductImage[];
|
|
Talla: string[];
|
|
variations: string[];
|
|
sellers: ProductSeller[];
|
|
Videos: unknown[];
|
|
estimatedDateArrival: null;
|
|
Tipo?: string[];
|
|
Ocasión?: string[];
|
|
Género?: string[];
|
|
Acabado?: string[];
|
|
Color?: string[];
|
|
Estilo?: string[];
|
|
Disciplina?: string[];
|
|
}
|
|
|
|
export interface Product {
|
|
productId: string;
|
|
productName: string;
|
|
brand: string;
|
|
brandId: number;
|
|
brandImageUrl: null | string;
|
|
linkText: string;
|
|
productReference: string;
|
|
productReferenceCode: string;
|
|
categoryId: string;
|
|
productTitle: string;
|
|
metaTagDescription: string;
|
|
releaseDate: string;
|
|
clusterHighlights: Record<string, unknown>;
|
|
productClusters: Record<string, string>;
|
|
searchableClusters: Record<string, unknown>;
|
|
categories: string[];
|
|
categoriesIds: string[];
|
|
link: string;
|
|
description: string;
|
|
items: ProductItem[];
|
|
// Additional fields that appear at product level
|
|
Color?: string[];
|
|
Género?: string[];
|
|
Ocasión?: string[];
|
|
Tipo?: string[];
|
|
Acabado?: string[];
|
|
Estilo?: string[];
|
|
Disciplina?: string[];
|
|
}
|
|
|
|
// Fallback product ID used only when no productId is provided
|
|
// This should rarely happen - normally productId comes from SKU detection or history
|
|
const FIXED_PRODUCT_ID = '180474';
|
|
|
|
export async function fetchProduct(productId: string = FIXED_PRODUCT_ID): Promise<Product | null> {
|
|
try {
|
|
// Use our Next.js API route as a proxy to avoid CORS issues
|
|
const response = await fetch(`/api/products/${productId}`);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
console.error('Failed to fetch product:', response.status, errorData);
|
|
return null;
|
|
}
|
|
|
|
const product = await response.json();
|
|
return product;
|
|
} catch (error) {
|
|
console.error('Error fetching product:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getProductImages(product: Product): string[] {
|
|
const allImages: string[] = [];
|
|
|
|
product.items.forEach(item => {
|
|
item.images.forEach(image => {
|
|
if (image.imageUrl && !allImages.includes(image.imageUrl)) {
|
|
allImages.push(image.imageUrl);
|
|
}
|
|
});
|
|
});
|
|
|
|
return allImages;
|
|
}
|
|
|
|
export function getProductPricing(product: Product) {
|
|
const firstAvailableItem = product.items.find(item =>
|
|
item.sellers.some(seller => seller.commertialOffer.IsAvailable)
|
|
);
|
|
|
|
if (!firstAvailableItem) {
|
|
return { price: 0, listPrice: 0, discount: 0, isAvailable: false };
|
|
}
|
|
|
|
const seller = firstAvailableItem.sellers.find(s => s.commertialOffer.IsAvailable);
|
|
if (!seller) {
|
|
return { price: 0, listPrice: 0, discount: 0, isAvailable: false };
|
|
}
|
|
|
|
const { Price, ListPrice } = seller.commertialOffer;
|
|
const discount = ListPrice > Price ? Math.round(((ListPrice - Price) / ListPrice) * 100) : 0;
|
|
|
|
return {
|
|
price: Price,
|
|
listPrice: ListPrice,
|
|
discount,
|
|
isAvailable: true
|
|
};
|
|
}
|
|
|
|
export function getProductVariants(product: Product) {
|
|
return product.items.map(item => {
|
|
const availableSeller = item.sellers.find(s => s.commertialOffer.IsAvailable);
|
|
return {
|
|
itemId: item.itemId,
|
|
name: item.name,
|
|
sizes: item.Talla,
|
|
images: item.images.map(img => img.imageUrl),
|
|
isAvailable: item.sellers.some(seller => seller.commertialOffer.IsAvailable),
|
|
price: availableSeller?.commertialOffer.Price || 0,
|
|
availableQuantity: availableSeller?.commertialOffer.AvailableQuantity || 0,
|
|
seller: availableSeller || item.sellers[0] // Fallback to first seller
|
|
};
|
|
});
|
|
}
|
|
|
|
export function getProductCategories(product: Product) {
|
|
return product.categories.map(category => {
|
|
// Remove leading/trailing slashes and split by '/'
|
|
const parts = category.replace(/^\/|\/$/g, '').split('/');
|
|
return parts.filter(part => part.length > 0);
|
|
}).flat();
|
|
}
|
|
|
|
export function getProductClusters(product: Product) {
|
|
return Object.entries(product.productClusters).map(([id, name]) => ({
|
|
id,
|
|
name,
|
|
displayName: name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
|
|
}));
|
|
}
|
|
|
|
export function getStockStatus(availableQuantity: number) {
|
|
if (availableQuantity === 0) return { status: 'out_of_stock', label: 'Agotado', color: 'text-red-400' };
|
|
if (availableQuantity === 1) return { status: 'low_stock', label: 'Última pieza', color: 'text-yellow-400' };
|
|
if (availableQuantity <= 3) return { status: 'limited', label: `Solo ${availableQuantity}`, color: 'text-orange-400' };
|
|
return { status: 'in_stock', label: 'Disponible', color: 'text-green-400' };
|
|
}
|
|
|
|
export function getProductGender(product: Product) {
|
|
// Get gender from product level first, then fallback to items
|
|
const gender = product.Género?.[0] || product.items[0]?.Género?.[0];
|
|
|
|
if (!gender) return null;
|
|
|
|
// Map gender to display info
|
|
const genderMap: Record<string, { label: string; icon: string; color: string }> = {
|
|
'Hombre': { label: 'Hombre', icon: '♂', color: 'bg-blue-500/20 text-blue-300 border-blue-500/30' },
|
|
'Mujer': { label: 'Mujer', icon: '♀', color: 'bg-pink-500/20 text-pink-300 border-pink-500/30' },
|
|
'Niño': { label: 'Niño', icon: '👶', color: 'bg-green-500/20 text-green-300 border-green-500/30' },
|
|
'Unisex': { label: 'Unisex', icon: '⚧', color: 'bg-purple-500/20 text-purple-300 border-purple-500/30' }
|
|
};
|
|
|
|
return genderMap[gender] || { label: gender, icon: '👤', color: 'bg-gray-500/20 text-gray-300 border-gray-500/30' };
|
|
}
|
|
|
|
export function getProductSeason(product: Product) {
|
|
// Check product clusters for season info
|
|
const seasonCluster = Object.values(product.productClusters).find(cluster => {
|
|
const clusterLower = cluster.toLowerCase();
|
|
return clusterLower.includes('oi') || clusterLower.includes('pv') ||
|
|
clusterLower.includes('fw') || clusterLower.includes('ss') ||
|
|
clusterLower.includes('2024') || clusterLower.includes('2025');
|
|
});
|
|
|
|
if (!seasonCluster) return null;
|
|
|
|
// Map season patterns
|
|
if (seasonCluster.toLowerCase().includes('oi') || seasonCluster.toLowerCase().includes('fw')) {
|
|
return { label: 'Otoño-Invierno', season: seasonCluster, color: 'bg-orange-500/20 text-orange-300 border-orange-500/30', icon: '🍂' };
|
|
}
|
|
|
|
if (seasonCluster.toLowerCase().includes('pv') || seasonCluster.toLowerCase().includes('ss')) {
|
|
return { label: 'Primavera-Verano', season: seasonCluster, color: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30', icon: '🌸' };
|
|
}
|
|
|
|
// Return generic season info
|
|
return { label: 'Temporada', season: seasonCluster, color: 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30', icon: '📅' };
|
|
}
|
|
|
|
export function getProductOccasion(product: Product) {
|
|
// Get occasion from product level first, then fallback to items
|
|
const occasion = product.Ocasión?.[0] || product.items[0]?.Ocasión?.[0];
|
|
|
|
if (!occasion) return null;
|
|
|
|
const occasionMap: Record<string, { label: string; color: string; icon: string }> = {
|
|
'Casual': { label: 'Casual', color: 'bg-teal-500/20 text-teal-300 border-teal-500/30', icon: '👟' },
|
|
'Deportivo': { label: 'Deportivo', color: 'bg-red-500/20 text-red-300 border-red-500/30', icon: '🏃' },
|
|
'Formal': { label: 'Formal', color: 'bg-slate-500/20 text-slate-300 border-slate-500/30', icon: '👔' },
|
|
'Trabajo': { label: 'Trabajo', color: 'bg-amber-500/20 text-amber-300 border-amber-500/30', icon: '💼' }
|
|
};
|
|
|
|
return occasionMap[occasion] || { label: occasion, color: 'bg-gray-500/20 text-gray-300 border-gray-500/30', icon: '🏷️' };
|
|
}
|
|
|
|
export function getProductColors(product: Product) {
|
|
// Get colors from product level first (main approach for this API)
|
|
const allColors = new Set<string>();
|
|
|
|
// Check product-level color field (this is where the color is stored in this API)
|
|
if (product.Color && Array.isArray(product.Color)) {
|
|
product.Color.forEach(color => allColors.add(color));
|
|
}
|
|
|
|
// Fallback: check items level
|
|
product.items.forEach(item => {
|
|
if (item.Color && Array.isArray(item.Color)) {
|
|
item.Color.forEach(color => allColors.add(color));
|
|
}
|
|
});
|
|
|
|
const colors = Array.from(allColors);
|
|
|
|
if (colors.length === 0) return [];
|
|
|
|
// Map colors to display info with hex approximations
|
|
return colors.map(color => {
|
|
const colorLower = color.toLowerCase();
|
|
const colorMap: Record<string, { label: string; hex: string; badge: string }> = {
|
|
'negro': { label: 'Negro', hex: '#1f2937', badge: 'bg-gray-800 text-white border-gray-700' },
|
|
'blanco': { label: 'Blanco', hex: '#f9fafb', badge: 'bg-gray-100 text-gray-900 border-gray-300' },
|
|
'rojo': { label: 'Rojo', hex: '#dc2626', badge: 'bg-red-500/20 text-red-300 border-red-500/30' },
|
|
'azul': { label: 'Azul', hex: '#2563eb', badge: 'bg-blue-500/20 text-blue-300 border-blue-500/30' },
|
|
'verde': { label: 'Verde', hex: '#16a34a', badge: 'bg-green-500/20 text-green-300 border-green-500/30' },
|
|
'amarillo': { label: 'Amarillo', hex: '#ca8a04', badge: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' },
|
|
'rosa': { label: 'Rosa', hex: '#ec4899', badge: 'bg-pink-500/20 text-pink-300 border-pink-500/30' },
|
|
'morado': { label: 'Morado', hex: '#9333ea', badge: 'bg-purple-500/20 text-purple-300 border-purple-500/30' },
|
|
'naranja': { label: 'Naranja', hex: '#ea580c', badge: 'bg-orange-500/20 text-orange-300 border-orange-500/30' },
|
|
'gris': { label: 'Gris', hex: '#6b7280', badge: 'bg-gray-500/20 text-gray-300 border-gray-500/30' },
|
|
'marrón': { label: 'Marrón', hex: '#92400e', badge: 'bg-amber-700/20 text-amber-300 border-amber-700/30' },
|
|
'café': { label: 'Café', hex: '#92400e', badge: 'bg-amber-700/20 text-amber-300 border-amber-700/30' },
|
|
'beige': { label: 'Beige', hex: '#d6d3d1', badge: 'bg-stone-300/20 text-stone-300 border-stone-300/30' },
|
|
'dorado': { label: 'Dorado', hex: '#f59e0b', badge: 'bg-yellow-600/20 text-yellow-300 border-yellow-600/30' },
|
|
'plateado': { label: 'Plateado', hex: '#9ca3af', badge: 'bg-gray-400/20 text-gray-300 border-gray-400/30' }
|
|
};
|
|
|
|
// Try exact match first
|
|
const exactMatch = colorMap[colorLower];
|
|
if (exactMatch) {
|
|
return { ...exactMatch, original: color };
|
|
}
|
|
|
|
// Try partial matches for compound colors
|
|
const partialMatch = Object.entries(colorMap).find(([key]) =>
|
|
colorLower.includes(key) || key.includes(colorLower)
|
|
);
|
|
|
|
if (partialMatch) {
|
|
return { ...partialMatch[1], original: color };
|
|
}
|
|
|
|
// Default fallback
|
|
return {
|
|
label: color,
|
|
hex: '#64748b',
|
|
badge: 'bg-slate-500/20 text-slate-300 border-slate-500/30',
|
|
original: color
|
|
};
|
|
});
|
|
}
|
|
|
|
export function getProductHighlight(product: Product) {
|
|
// Only show badge if clusterHighlights has actual content
|
|
if (!product.clusterHighlights || Object.keys(product.clusterHighlights).length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Show the highlight from clusterHighlights
|
|
const highlight = Object.values(product.clusterHighlights)[0];
|
|
return {
|
|
label: String(highlight),
|
|
style: 'bg-gradient-to-br from-yellow-500 to-orange-600 text-white shadow-lg shadow-yellow-500/50',
|
|
icon: '⭐',
|
|
type: 'promotion'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Loads test product data from JSON file
|
|
* Used when SKU detection returns "test_product"
|
|
*
|
|
* The test product data is stored in /public/test-product.json
|
|
* and can be edited manually without changing code
|
|
*/
|
|
export async function fetchTestProduct(): Promise<Product | null> {
|
|
try {
|
|
const response = await fetch('/test-product.json');
|
|
|
|
if (!response.ok) {
|
|
console.error('Failed to load test product JSON:', response.status);
|
|
return null;
|
|
}
|
|
|
|
const testProduct: Product = await response.json();
|
|
console.log('✅ Test product loaded from JSON successfully');
|
|
return testProduct;
|
|
} catch (error) {
|
|
console.error('Error loading test product:', error);
|
|
return null;
|
|
}
|
|
} |