147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
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<string, unknown>;
|
||
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}`;
|
||
}
|
||
|