import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } /** Coerce API money fields (number | "199.00" | null) to a finite number. */ export function parseMoney(value: unknown): number { if (typeof value === 'number') return Number.isFinite(value) ? value : 0; if (typeof value === 'string') { const cleaned = value.trim().replace(/,/g, ''); if (!cleaned) return 0; const n = Number(cleaned); return Number.isFinite(n) ? n : 0; } return 0; } export type CatalogPriceInfo = { price: number; priceMax?: number; oldPrice?: number; discount?: number; }; /** * Resolve display price from either full ProductResponse (variants) * or list ProductCardResponse (top-level price / compare_price). */ export function getCatalogPriceInfo(product: { price?: unknown; compare_price?: unknown; discount_percent?: unknown; variants?: Array<{ price?: unknown; compare_price?: unknown }> | null; }): CatalogPriceInfo { const variants = product.variants || []; const prices = variants.map((v) => parseMoney(v.price)).filter((n) => n > 0); const compares = variants .map((v) => parseMoney(v.compare_price)) .filter((n) => n > 0); let price = prices.length ? Math.min(...prices) : 0; const priceMax = prices.length > 1 ? Math.max(...prices) : undefined; let oldPrice = compares.length ? Math.max(...compares) : undefined; // List/card endpoints expose price on the product, not variants if (!price) price = parseMoney(product.price); if (!oldPrice) { const compare = parseMoney(product.compare_price); if (compare > 0) oldPrice = compare; } let discount: number | undefined; if (price && oldPrice && oldPrice > price) { discount = Math.round(((oldPrice - price) / oldPrice) * 100); } else { const pct = parseMoney(product.discount_percent); if (pct > 0) discount = Math.round(pct); } return { price, priceMax: priceMax && priceMax > price ? priceMax : undefined, oldPrice: oldPrice && oldPrice > price ? oldPrice : undefined, discount, }; } export function formatCatalogPriceLabel(product: { price?: unknown; compare_price?: unknown; variants?: Array<{ price?: unknown; compare_price?: unknown }> | null; }): string { const { price, priceMax } = getCatalogPriceInfo(product); if (!price) return formatCurrency(0); if (priceMax && priceMax > price) { return `${formatCurrency(price)} – ${formatCurrency(priceMax)}`; } return formatCurrency(price); } export function formatCurrency(amount: number | string, currency = 'INR'): string { return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 2, }).format(parseMoney(amount)); } export function getImageUrl(path?: string | null | unknown): string { let resolvedPath: string; if (typeof path === 'string') { resolvedPath = path; } else if (path && typeof path === 'object') { const obj = path as Record; resolvedPath = String(obj.image_url ?? obj.url ?? obj.src ?? ''); } else { return '/images/placeholder-product.webp'; } if (!resolvedPath || resolvedPath === 'null' || resolvedPath === 'undefined') { return '/images/placeholder-product.webp'; } const trimmed = resolvedPath.trim(); if (!trimmed) return '/images/placeholder-product.webp'; if ( trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('data:') || trimmed.startsWith('blob:') ) { return trimmed; } let cleanPath = trimmed.startsWith('/') ? trimmed : `/${trimmed}`; // Local storefront static assets (e.g. /images/placeholder-product.webp, /icons/...) if ( cleanPath.startsWith('/images/') || cleanPath.startsWith('/icons/') || cleanPath.startsWith('/assets/') || cleanPath.startsWith('/_next/') ) { return cleanPath; } // Normalize relative upload paths (/uploads/product/..., product/..., uploads/product/...) if (cleanPath.startsWith('/product/')) { cleanPath = `/uploads${cleanPath}`; } else if (!cleanPath.startsWith('/uploads/')) { const withoutLeadingSlash = cleanPath.replace(/^\//, ''); if (withoutLeadingSlash.startsWith('uploads/')) { cleanPath = `/${withoutLeadingSlash}`; } else if (withoutLeadingSlash.startsWith('product/')) { cleanPath = `/uploads/${withoutLeadingSlash}`; } } const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'https://ifixkartbe.trionixsolution.com'; return `${backendUrl.replace(/\/$/, '')}${cleanPath}`; }