fix(admin): resolve node:net error by decoupling date utils & brand detection from server libraries and add client webpack fallbacks
This commit is contained in:
parent
1bba620689
commit
7028a5b397
@ -2,6 +2,23 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
serverExternalPackages: ["node-appwrite", "@google/genai"],
|
||||
turbopack: {},
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = {
|
||||
...(config.resolve.fallback || {}),
|
||||
net: false,
|
||||
tls: false,
|
||||
fs: false,
|
||||
child_process: false,
|
||||
dns: false,
|
||||
http2: false,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ protocol: "https", hostname: "**" },
|
||||
|
||||
@ -4,23 +4,52 @@ import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Package, Plus, Eye, EyeOff, Check, X, Sparkles, Filter, Phone, MessageCircle, Truck, Trash2, Edit, Wand2, ArrowRight, Layers, Palette, CheckSquare, Square, MinusSquare, Split, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Tag, AlertCircle, CheckCircle2, Clock, RefreshCw, Zap, ShieldCheck, ArrowUpDown, History } from 'lucide-react';
|
||||
import { Product, Supplier } from '@/types';
|
||||
import { INITIAL_PRODUCTS, INITIAL_SUPPLIERS } from '@/lib/data-service';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
import { GroupProductsModal } from '@/components/admin/GroupProductsModal';
|
||||
import { ManageVariantsModal } from '@/components/admin/ManageVariantsModal';
|
||||
import { renderClientTemplate, initClientTemplates } from '@/lib/templates-client';
|
||||
import { OptimizedImage } from '@/components/ui/OptimizedImage';
|
||||
import { sanitizePublicationTitle, sanitizePublicationText } from '@/lib/sanitizer';
|
||||
import { cleanProductName, detectBrandFromText } from '@/lib/gemini-parser';
|
||||
import { cleanProductName, detectBrandFromText } from '@/lib/brand-detection';
|
||||
import { generateAttractiveProductCopy } from '@/lib/attractive-copy';
|
||||
import { WhatsAppOrderLinkModal } from '@/components/admin/WhatsAppOrderLinkModal';
|
||||
import { getProductPublicationDate, getProductAgeInDays, isProductOlderThanDays } from '@/lib/expiration-service';
|
||||
import { getProductPublicationDate, getProductAgeInDays, isProductOlderThanDays } from '@/lib/product-date-utils';
|
||||
import { FormattedDescription } from '@/components/store/FormattedDescription';
|
||||
import { getCategoryIcon, normalizeCategoryName, matchCategory } from '@/lib/category-helper';
|
||||
|
||||
const CLIENT_INITIAL_SUPPLIERS: Supplier[] = [
|
||||
{
|
||||
id: "sup-jovic",
|
||||
name: "💎 2️⃣ DIFUSIÓN JOVIC STORE COLOMBIA🍀",
|
||||
phone: "573167455877",
|
||||
contactPerson: "Asesor Jovic Store",
|
||||
address: "Centro / Bodega Cali",
|
||||
notes: "Proveedor oficial Nike, Jordan, Nocta y Vomero calidades Top.",
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
id: "sup-ccmio",
|
||||
name: "EXCLUSIVE CC MÍO 3B",
|
||||
phone: "573159876543",
|
||||
contactPerson: "Despachos CC MÍO 3B",
|
||||
address: "CC MÍO Local 3B, Cali",
|
||||
notes: "Especialista en Adidas, Coach, Timberland y Skatewear.",
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
id: "sup-central",
|
||||
name: "BODEGA CENTRAL MAYORISTA",
|
||||
phone: "573101234567",
|
||||
contactPerson: "Logística Despachos",
|
||||
address: "San Andresito / Cali",
|
||||
notes: "Despacho rápido en 24 horas para separación inmediata.",
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
export default function AdminProductsPage() {
|
||||
const [products, setProducts] = useState<Product[]>(INITIAL_PRODUCTS);
|
||||
const [suppliers, setSuppliers] = useState<Supplier[]>(INITIAL_SUPPLIERS);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [suppliers, setSuppliers] = useState<Supplier[]>(CLIENT_INITIAL_SUPPLIERS);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
// Filtros Avanzados para Administradores
|
||||
|
||||
255
dripzone-app/src/lib/brand-detection.ts
Normal file
255
dripzone-app/src/lib/brand-detection.ts
Normal file
@ -0,0 +1,255 @@
|
||||
import { sanitizePublicationTitle } from './sanitizer';
|
||||
|
||||
export interface BrandSignature {
|
||||
brand: string;
|
||||
aliases: RegExp[];
|
||||
models: { name: string; pattern: RegExp }[];
|
||||
}
|
||||
|
||||
export const BRAND_SIGNATURES: BrandSignature[] = [
|
||||
{
|
||||
brand: 'Adidas',
|
||||
aliases: [/\badidas\b/i, /\btrefoil\b/i, /\btr[eé]bol\b/i, /3\s*rayas|tres\s*rayas/i],
|
||||
models: [
|
||||
{ name: 'Adidas Samba OG', pattern: /\bsamba\b/i },
|
||||
{ name: 'Adidas Gazelle Indoor', pattern: /\bgazelle\b/i },
|
||||
{ name: 'Adidas Campus 00s', pattern: /\bcampus\b/i },
|
||||
{ name: 'Adidas Handball Spezial', pattern: /\bspezial\b/i },
|
||||
{ name: 'Adidas Forum Low', pattern: /\bforum\b/i },
|
||||
{ name: 'Adidas Bad Bunny', pattern: /\bbad\s*bunny\b/i },
|
||||
{ name: 'Adidas Stan Smith', pattern: /\bstan\s*smith\b/i },
|
||||
{ name: 'Adidas Superstar', pattern: /\bsuperstar\b/i },
|
||||
{ name: 'Adidas SL 72', pattern: /\bsl\s*72\b|\bsl72\b/i },
|
||||
{ name: 'Adidas Response CL', pattern: /\bresponse\s*cl\b/i },
|
||||
{ name: 'Adidas Yeezy Boost 350', pattern: /\byeezy\b|\b350\s*v2\b|\bfoam\s*runner\b/i },
|
||||
{ name: 'Adidas Astir', pattern: /\bastir\b/i },
|
||||
{ name: 'Adidas Ozelia', pattern: /\bozelia\b/i },
|
||||
{ name: 'Adidas Adizero', pattern: /\badizero\b/i },
|
||||
{ name: 'Adidas Country OG', pattern: /\bcountry\s*og\b/i },
|
||||
{ name: 'Adidas NMD R1', pattern: /\bnmd\b/i },
|
||||
{ name: 'Adidas Ultraboost', pattern: /\bultra\s*boost\b|\bultraboost\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Jordan',
|
||||
aliases: [/\bjordan\b/i, /\bjumpman\b/i, /\bair\s*jordan\b/i, /\baj\d+\b/i],
|
||||
models: [
|
||||
{ name: 'Jordan 4 Retro', pattern: /\b(?:retro\s*4|aj4|jordan\s*4|retro\s*iv)\b/i },
|
||||
{ name: 'Jordan 1 Retro High OG', pattern: /\b(?:retro\s*1|aj1|jordan\s*1|retro\s*i)\b/i },
|
||||
{ name: 'Jordan 3 Retro', pattern: /\b(?:retro\s*3|aj3|jordan\s*3|retro\s*iii)\b/i },
|
||||
{ name: 'Jordan 11 Retro', pattern: /\b(?:retro\s*11|aj11|jordan\s*11|retro\s*xi)\b/i },
|
||||
{ name: 'Jordan 1 Low Travis Scott', pattern: /\btravis\s*scott\b|\bcactus\s*jack\b/i },
|
||||
{ name: 'Jordan Spizike Low', pattern: /\bspizike\b/i },
|
||||
{ name: 'Jordan 5 Retro', pattern: /\b(?:retro\s*5|aj5|jordan\s*5)\b/i },
|
||||
{ name: 'Jordan 6 Retro', pattern: /\b(?:retro\s*6|aj6|jordan\s*6)\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'New Balance',
|
||||
aliases: [/\bnew\s*balance\b/i, /\bnb\b/i],
|
||||
models: [
|
||||
{ name: 'New Balance 9060', pattern: /\b9060\b/i },
|
||||
{ name: 'New Balance 550', pattern: /\b550\b/i },
|
||||
{ name: 'New Balance 530', pattern: /\b530\b/i },
|
||||
{ name: 'New Balance 1906R', pattern: /\b1906\b|\b1906r\b|\b1906d\b/i },
|
||||
{ name: 'New Balance 2002R', pattern: /\b2002\b|\b2002r\b|\bprotection\s*pack\b/i },
|
||||
{ name: 'New Balance 327', pattern: /\b327\b/i },
|
||||
{ name: 'New Balance 574', pattern: /\b574\b/i },
|
||||
{ name: 'New Balance 990v6', pattern: /\b990\b|\b990v\d\b/i },
|
||||
{ name: 'New Balance 991', pattern: /\b991\b/i },
|
||||
{ name: 'New Balance 992', pattern: /\b992\b/i },
|
||||
{ name: 'New Balance 993', pattern: /\b993\b/i },
|
||||
{ name: 'New Balance 610', pattern: /\b610\b/i },
|
||||
{ name: 'New Balance CT302', pattern: /\bct302\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'On Running',
|
||||
aliases: [/\bon\s*running\b/i, /\bon\s*cloud\b/i, /\bcloud\b/i, /\bclould\b/i],
|
||||
models: [
|
||||
{ name: 'On Cloudmonster', pattern: /\bcloudmonster\b|\bcloud\s*monster\b/i },
|
||||
{ name: 'On Cloudtilt Loewe', pattern: /\bcloudtilt\b|\bcloud\s*tilt\b|\bloewe\b/i },
|
||||
{ name: 'On Cloud 5', pattern: /\bcloud\s*5\b/i },
|
||||
{ name: 'On Cloud X3', pattern: /\bcloud\s*x\b|\bcloud\s*x3\b/i },
|
||||
{ name: 'On Cloudsurfer', pattern: /\bcloudsurfer\b|\bcloud\s*surfer\b/i },
|
||||
{ name: 'On Cloudstratus', pattern: /\bcloudstratus\b|\bcloud\s*stratus\b/i },
|
||||
{ name: 'On Cloudrunner', pattern: /\bcloudrunner\b|\bcloud\s*runner\b/i },
|
||||
{ name: 'On The Roger Centre Court', pattern: /\broger\b|\bthe\s*roger\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Puma',
|
||||
aliases: [/\bpuma\b/i],
|
||||
models: [
|
||||
{ name: 'Puma Palermo Special', pattern: /\bpalermo\b/i },
|
||||
{ name: 'Puma Speedcat OG', pattern: /\bspeedcat\b|\bspeed\s*cat\b/i },
|
||||
{ name: 'Puma Suede Classic', pattern: /\bsuede\b/i },
|
||||
{ name: 'Puma Caven 2.0', pattern: /\bcaven\b/i },
|
||||
{ name: 'Puma Slipstream', pattern: /\bslipstream\b/i },
|
||||
{ name: 'Puma Mayze Wedge', pattern: /\bmayze\b/i },
|
||||
{ name: 'Puma Easy Rider', pattern: /\beasy\s*rider\b/i },
|
||||
{ name: 'Puma RS-X Efekt', pattern: /\brs-x\b|\brsx\b/i },
|
||||
{ name: 'Puma CA Pro Classic', pattern: /\bca\s*pro\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Asics',
|
||||
aliases: [/\basics\b/i],
|
||||
models: [
|
||||
{ name: 'Asics Gel-Kayano 14', pattern: /\bgel[\s-]*kayano\b|\bkayano\b/i },
|
||||
{ name: 'Asics Gel-NYC', pattern: /\bgel[\s-]*nyc\b|\bnyc\b/i },
|
||||
{ name: 'Asics Gel-1130', pattern: /\bgel[\s-]*1130\b|\b1130\b/i },
|
||||
{ name: 'Asics GT-2160', pattern: /\bgt[\s-]*2160\b|\b2160\b/i },
|
||||
{ name: 'Asics Gel-Quantum 360', pattern: /\bgel[\s-]*quantum\b|\bquantum\b/i },
|
||||
{ name: 'Asics Gel-Nimbus 9', pattern: /\bgel[\s-]*nimbus\b|\bnimbus\b/i },
|
||||
{ name: 'Asics Gel-Lyte III', pattern: /\bgel[\s-]*lyte\b/i },
|
||||
{ name: 'Asics Novablast 4', pattern: /\bnovablast\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Nike',
|
||||
aliases: [/\bnike\b/i, /\bswoosh\b/i],
|
||||
models: [
|
||||
{ name: 'Nike Air Force 1 07', pattern: /\bair\s*force\b|\baf1\b|\baf-1\b|\bforce\s*1\b/i },
|
||||
{ name: 'Nike Dunk Low Retro Panda', pattern: /\bdunk\b|\bpanda\b|\bsb\s*dunk\b/i },
|
||||
{ name: 'Nike Air Max 90', pattern: /\bair\s*max\s*90\b|\bam90\b|\bmax\s*90\b/i },
|
||||
{ name: 'Nike Air Max 97', pattern: /\bair\s*max\s*97\b|\bam97\b|\bmax\s*97\b/i },
|
||||
{ name: 'Nike Air Max 270', pattern: /\bair\s*max\s*270\b|\bmax\s*270\b/i },
|
||||
{ name: 'Nike Air Max Plus TN', pattern: /\btn\b|\bair\s*max\s*plus\b|\btuned\b/i },
|
||||
{ name: 'Nike Vomero 5', pattern: /\bvomero\b|\bvomero\s*5\b/i },
|
||||
{ name: 'Nike P-6000 Retro', pattern: /\bp-6000\b|\bp6000\b/i },
|
||||
{ name: 'Nike Shox TL', pattern: /\bshox\b|\bshox\s*tl\b|\br4\b/i },
|
||||
{ name: 'Nike Cortez Classic', pattern: /\bcortez\b/i },
|
||||
{ name: 'Nike Blazer Mid 77', pattern: /\bblazer\b/i },
|
||||
{ name: 'Nike TC 7900 Chunky', pattern: /\btc\s*7900\b|\btc7900\b/i },
|
||||
{ name: 'Nike Gamma Force', pattern: /\bgamma\s*force\b/i },
|
||||
{ name: 'Nike Air VaporMax Plus', pattern: /\bvapormax\b|\bvapor\s*max\b/i },
|
||||
{ name: 'Nike Pegasus 40', pattern: /\bpegasus\b/i },
|
||||
{ name: 'Nike Killshot 2', pattern: /\bkillshot\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Vans',
|
||||
aliases: [/\bvans\b/i, /\boff\s*the\s*wall\b/i],
|
||||
models: [
|
||||
{ name: 'Vans Knu Skool Chunky', pattern: /\bknu\s*skool\b|\bknuskool\b|\bknu\b/i },
|
||||
{ name: 'Vans Old Skool Classic', pattern: /\bold\s*skool\b|\boldskool\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Converse',
|
||||
aliases: [/\bconverse\b/i, /\ball\s*star\b/i],
|
||||
models: [
|
||||
{ name: 'Converse Chuck 70 Vintage', pattern: /\bchuck\s*70\b|\bchuck70\b/i },
|
||||
{ name: 'Converse Chuck Taylor All Star', pattern: /\bchuck\s*taylor\b|\ball\s*star\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Salomon',
|
||||
aliases: [/\bsalomon\b/i],
|
||||
models: [
|
||||
{ name: 'Salomon XT-6 Gore-Tex', pattern: /\bxt-6\b|\bxt6\b/i },
|
||||
{ name: 'Salomon XT-4 OG', pattern: /\bxt-4\b|\bxt4\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Crocs',
|
||||
aliases: [/\bcrocs\b/i],
|
||||
models: [
|
||||
{ name: 'Crocs Pollex Clog Salehe Bembury', pattern: /\bpollex\b|\bsalehe\b/i },
|
||||
{ name: 'Crocs Echo Clog', pattern: /\becho\s*clog\b|\becho\b/i },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Detector determinístico y de alta precisión de marca y modelo desde texto (100% seguro para cliente y servidor).
|
||||
*/
|
||||
export function detectBrandFromText(text: string): {
|
||||
brand: string;
|
||||
modelName?: string;
|
||||
confidence: number;
|
||||
} {
|
||||
const normalized = (text || '').toLowerCase();
|
||||
|
||||
for (const item of BRAND_SIGNATURES) {
|
||||
for (const model of item.models) {
|
||||
if (model.pattern.test(normalized)) {
|
||||
return {
|
||||
brand: item.brand,
|
||||
modelName: model.name,
|
||||
confidence: 0.96,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of BRAND_SIGNATURES) {
|
||||
for (const alias of item.aliases) {
|
||||
if (alias.test(normalized)) {
|
||||
return {
|
||||
brand: item.brand,
|
||||
confidence: 0.88,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
brand: 'Urbano / Variado',
|
||||
confidence: 0.4,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia y formatea el nombre del producto garantizando que NUNCA se incluyan nombres
|
||||
* de proveedores, bodegas, canales o grupos de WhatsApp.
|
||||
*/
|
||||
export function cleanProductName(
|
||||
text: string,
|
||||
detectedBrand: string,
|
||||
extraForbidden: (string | undefined | null)[] = []
|
||||
): string {
|
||||
const lines = (text || '').split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
||||
for (const line of lines) {
|
||||
const lower = line.toLowerCase();
|
||||
if (
|
||||
lower.includes('difusion jovic') ||
|
||||
lower.includes('jovic') ||
|
||||
lower.includes('cc mío') ||
|
||||
lower.includes('cc mio') ||
|
||||
lower.includes('bodega central') ||
|
||||
lower.includes('bodega mayorista') ||
|
||||
lower.includes('proveedor') ||
|
||||
lower.includes('canal mayorista') ||
|
||||
lower.includes('canal de difusión') ||
|
||||
lower.includes('canal de difusion') ||
|
||||
lower.includes('grupo mayorista') ||
|
||||
lower.includes('grupo whatsapp') ||
|
||||
lower.includes('team emporio') ||
|
||||
lower.includes('notificaciones') ||
|
||||
lower.includes('medios y prensa') ||
|
||||
lower.includes('calzado mayorista') ||
|
||||
lower.includes('san andresito')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clean = line
|
||||
.replace(/🔥|🔴|‼️|✨|🌸|👑|💎|🛍️|🌟|💸|💰|💵|💲|\*/g, '')
|
||||
.replace(/dama\s*y\s*caballero|caballero|dama|unisex|recarga|nueva\s*mercanc[ií]a|[uú]ltima\s*unidad/gi, '')
|
||||
.replace(/c[oó]digo\s*:\s*[\d-]+/gi, '')
|
||||
.replace(/\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\$\s*\d+/gi, '')
|
||||
.replace(/tallas?:.*|precio:.*|eur\s*[\d\/-]+/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
const sanitized = sanitizePublicationTitle(clean, extraForbidden, detectedBrand);
|
||||
if (sanitized.length >= 3 && !/^\d+$/.test(sanitized) && !/^\d+[\.,]\d+$/.test(sanitized)) {
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultGeneric = `${detectedBrand} Urban Edition`;
|
||||
return sanitizePublicationTitle(defaultGeneric, extraForbidden, detectedBrand);
|
||||
}
|
||||
1
dripzone-app/src/lib/empty-module.js
Normal file
1
dripzone-app/src/lib/empty-module.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@ -3,53 +3,17 @@ import { dataService } from './data-service';
|
||||
import { saveProductToAppwrite } from './server-appwrite';
|
||||
import { registerProductInWhatsAppCatalog } from './whatsapp-catalog';
|
||||
|
||||
/**
|
||||
* Determina la fecha exacta de publicación de una zapatilla.
|
||||
* Prioriza:
|
||||
* 1. publishedAt
|
||||
* 2. createdAt
|
||||
* 3. Timestamp extraído del ID 'snk-<timestamp>-...'
|
||||
* 4. Fallback al momento actual
|
||||
*/
|
||||
export function getProductPublicationDate(product: Product): Date {
|
||||
if (!product) return new Date();
|
||||
if (product.publishedAt) {
|
||||
const d = new Date(product.publishedAt);
|
||||
if (!isNaN(d.getTime())) return d;
|
||||
}
|
||||
if (product.createdAt) {
|
||||
const d = new Date(product.createdAt);
|
||||
if (!isNaN(d.getTime())) return d;
|
||||
}
|
||||
if (product.id && typeof product.id === 'string' && product.id.startsWith('snk-')) {
|
||||
const parts = product.id.split('-');
|
||||
if (parts[1]) {
|
||||
const ts = parseInt(parts[1], 10);
|
||||
if (!isNaN(ts) && ts > 1000000000000 && ts < 2500000000000) {
|
||||
return new Date(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
import {
|
||||
getProductPublicationDate,
|
||||
getProductAgeInDays,
|
||||
isProductOlderThanDays,
|
||||
} from './product-date-utils';
|
||||
|
||||
/**
|
||||
* Calcula la antigüedad en días de una publicación de calzado
|
||||
*/
|
||||
export function getProductAgeInDays(product: Product): number {
|
||||
if (!product) return 0;
|
||||
const pubDate = getProductPublicationDate(product);
|
||||
const diffMs = Date.now() - pubDate.getTime();
|
||||
return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si una publicación supera el umbral de días establecido (por defecto 7 días)
|
||||
*/
|
||||
export function isProductOlderThanDays(product: Product, daysThreshold: number = 7): boolean {
|
||||
if (!product) return false;
|
||||
return getProductAgeInDays(product) >= daysThreshold;
|
||||
}
|
||||
export {
|
||||
getProductPublicationDate,
|
||||
getProductAgeInDays,
|
||||
isProductOlderThanDays,
|
||||
};
|
||||
|
||||
export interface ExpirationExecutionResult {
|
||||
success: boolean;
|
||||
|
||||
45
dripzone-app/src/lib/product-date-utils.ts
Normal file
45
dripzone-app/src/lib/product-date-utils.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Product } from '@/types';
|
||||
|
||||
/**
|
||||
* Determina la fecha exacta de publicación de una zapatilla de forma 100% segura para el cliente (navegador).
|
||||
* No importa módulos de Node.js ni bases de datos de servidor.
|
||||
*/
|
||||
export function getProductPublicationDate(product: Product): Date {
|
||||
if (!product) return new Date();
|
||||
if (product.publishedAt) {
|
||||
const d = new Date(product.publishedAt);
|
||||
if (!isNaN(d.getTime())) return d;
|
||||
}
|
||||
if (product.createdAt) {
|
||||
const d = new Date(product.createdAt);
|
||||
if (!isNaN(d.getTime())) return d;
|
||||
}
|
||||
if (product.id && typeof product.id === 'string' && product.id.startsWith('snk-')) {
|
||||
const parts = product.id.split('-');
|
||||
if (parts[1]) {
|
||||
const ts = parseInt(parts[1], 10);
|
||||
if (!isNaN(ts) && ts > 1000000000000 && ts < 2500000000000) {
|
||||
return new Date(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula la antigüedad en días de una publicación de calzado
|
||||
*/
|
||||
export function getProductAgeInDays(product: Product): number {
|
||||
if (!product) return 0;
|
||||
const pubDate = getProductPublicationDate(product);
|
||||
const diffMs = Date.now() - pubDate.getTime();
|
||||
return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si una publicación supera el umbral de días establecido (por defecto 7 días)
|
||||
*/
|
||||
export function isProductOlderThanDays(product: Product, daysThreshold: number = 7): boolean {
|
||||
if (!product) return false;
|
||||
return getProductAgeInDays(product) >= daysThreshold;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user