460 lines
22 KiB
TypeScript
460 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
X,
|
|
Star,
|
|
Truck,
|
|
ShieldCheck,
|
|
Zap,
|
|
MessageCircle,
|
|
ShoppingBag,
|
|
Check,
|
|
Palette,
|
|
AlertCircle,
|
|
HelpCircle,
|
|
Sparkles,
|
|
ArrowRight,
|
|
} from 'lucide-react';
|
|
import { Product, City, ProductColorVariant } from '@/types';
|
|
import { formatCOP, calculateSeparationBreakdown, getProductEffectivePrice } from '@/lib/pricing-engine';
|
|
import { useBrand } from '@/context/BrandContext';
|
|
import { getProductSocialProof } from '@/lib/social-proof';
|
|
import { renderClientTemplate } from '@/lib/templates-client';
|
|
import { OptimizedImage } from '@/components/ui/OptimizedImage';
|
|
|
|
|
|
interface ProductModalProps {
|
|
product: Product | null;
|
|
onClose: () => void;
|
|
onAddToCart: (product: Product, size: string) => void;
|
|
selectedCity: City;
|
|
}
|
|
|
|
const ALL_STANDARD_SIZES = ['36', '37', '38', '39', '40', '41', '42', '43', '44'];
|
|
|
|
export const ProductModal: React.FC<ProductModalProps> = ({
|
|
product,
|
|
onClose,
|
|
onAddToCart,
|
|
selectedCity,
|
|
}) => {
|
|
const { currentBrand } = useBrand();
|
|
if (!product) return null;
|
|
|
|
const displaySizes = Array.from(
|
|
new Set([...ALL_STANDARD_SIZES, ...(product.sizes || [])])
|
|
).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
|
|
|
// Active colorway variant
|
|
const colorsList: ProductColorVariant[] =
|
|
product.colors && product.colors.length > 0
|
|
? product.colors
|
|
: [
|
|
{
|
|
id: 'col-default',
|
|
colorName: product.colorName || 'Color Principal',
|
|
image: product.images[0],
|
|
images: product.images,
|
|
sizes: product.sizes,
|
|
inStock: product.inStock,
|
|
},
|
|
];
|
|
|
|
const [selectedColor, setSelectedColor] = useState<ProductColorVariant>(colorsList[0]);
|
|
const [selectedImage, setSelectedImage] = useState<string>(colorsList[0]?.image || product.images[0]);
|
|
const [availableSizes, setAvailableSizes] = useState<string[]>(colorsList[0]?.sizes || product.sizes);
|
|
const [selectedSize, setSelectedSize] = useState<string>(
|
|
(colorsList[0]?.sizes && colorsList[0].sizes[0]) || product.sizes[0] || '39'
|
|
);
|
|
const [added, setAdded] = useState(false);
|
|
|
|
// When color variant changes, update images & available sizes
|
|
const handleColorChange = (variant: ProductColorVariant) => {
|
|
setSelectedColor(variant);
|
|
setSelectedImage(variant.image || (variant.images && variant.images[0]) || product.images[0]);
|
|
const nextSizes = variant.sizes && variant.sizes.length > 0 ? variant.sizes : product.sizes;
|
|
setAvailableSizes(nextSizes);
|
|
if (!nextSizes.includes(selectedSize)) {
|
|
setSelectedSize(nextSizes[0] || selectedSize);
|
|
}
|
|
};
|
|
|
|
const isSizeInStock = availableSizes.includes(selectedSize);
|
|
|
|
// Precio dinámico efectivo según colorway y talla seleccionada
|
|
const currentPrice = getProductEffectivePrice(product, selectedColor, selectedSize);
|
|
const socialProof = getProductSocialProof(product);
|
|
const separation = calculateSeparationBreakdown(currentPrice);
|
|
|
|
const handleAdd = () => {
|
|
if (!isSizeInStock) return;
|
|
onAddToCart(
|
|
{
|
|
...product,
|
|
name: `${product.modelName || product.name} (${selectedColor.colorName})`,
|
|
retailPrice: currentPrice,
|
|
images: selectedColor.images || [selectedImage],
|
|
},
|
|
selectedSize
|
|
);
|
|
setAdded(true);
|
|
setTimeout(() => {
|
|
setAdded(false);
|
|
onClose();
|
|
}, 1200);
|
|
};
|
|
|
|
// Pedido normal por WhatsApp
|
|
const handleWhatsAppDirect = () => {
|
|
const phone = currentBrand.whatsappNumber || '573167455877';
|
|
const rawText = renderClientTemplate('product_direct_inquiry', {
|
|
tienda_nombre: currentBrand.name,
|
|
zapatilla_nombre: product.modelName || product.name,
|
|
color_nombre: selectedColor.colorName,
|
|
talla: selectedSize,
|
|
precio: formatCOP(currentPrice),
|
|
ciudad: selectedCity.name,
|
|
marca: product.brand || 'Sneakers',
|
|
});
|
|
window.open(`https://wa.me/${phone}?text=${encodeURIComponent(rawText)}`, '_blank');
|
|
};
|
|
|
|
// Solicitud especial de talla no disponible
|
|
const handleWhatsAppRequestSize = (sizeToRequest: string) => {
|
|
const phone = currentBrand.whatsappNumber || '573167455877';
|
|
const rawText = renderClientTemplate('product_request_size', {
|
|
tienda_nombre: currentBrand.name,
|
|
zapatilla_nombre: product.modelName || product.name,
|
|
color_nombre: selectedColor.colorName,
|
|
talla: sizeToRequest,
|
|
ciudad: selectedCity.name,
|
|
marca: product.brand || 'Sneakers',
|
|
});
|
|
window.open(`https://wa.me/${phone}?text=${encodeURIComponent(rawText)}`, '_blank');
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-3 sm:p-4 overflow-y-auto animate-in fade-in">
|
|
<div className={`bg-neutral-900 border ${currentBrand.colors.border} text-white rounded-3xl max-w-4xl w-full overflow-hidden shadow-2xl relative my-auto max-h-[94vh] flex flex-col md:flex-row`}>
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 z-20 w-9 h-9 rounded-full bg-neutral-800/90 hover:bg-neutral-700 text-neutral-300 hover:text-white flex items-center justify-center transition-all cursor-pointer border border-neutral-700 shadow-md"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
|
|
{/* Left: Images & Color Preview */}
|
|
<div className="md:w-1/2 bg-neutral-950 p-4 sm:p-6 flex flex-col justify-between border-b md:border-b-0 md:border-r border-neutral-800">
|
|
<div>
|
|
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-900 shadow-inner mb-3 border border-neutral-800">
|
|
<OptimizedImage
|
|
src={selectedImage}
|
|
alt={product.modelName || product.name}
|
|
width={700}
|
|
height={700}
|
|
quality={85}
|
|
aspectRatio="square"
|
|
priority={true}
|
|
className="w-full h-full object-cover transition-all duration-300"
|
|
containerClassName="w-full h-full"
|
|
/>
|
|
<span className={`absolute top-3 left-3 bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white text-xs font-black px-2.5 py-1 rounded-lg uppercase shadow`}>
|
|
-{Math.round(((product.originalPrice || 170000) - product.retailPrice) / (product.originalPrice || 170000) * 100)}% OFF
|
|
</span>
|
|
|
|
{/* Active Color Tag on Image */}
|
|
<div className="absolute bottom-3 left-3 bg-neutral-950/90 backdrop-blur-md px-3 py-1 rounded-full border border-white/20 text-xs font-bold text-white flex items-center gap-1.5 shadow-lg">
|
|
<Palette className="w-3.5 h-3.5 text-purple-400" />
|
|
<span>{selectedColor.colorName}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Gallery / Colorway Thumbnails */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-[11px] text-neutral-400">
|
|
<span className="font-bold uppercase tracking-wider">Fotos del Colorway:</span>
|
|
<span>{(selectedColor.images || product.images).length} vistas</span>
|
|
</div>
|
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
{(selectedColor.images && selectedColor.images.length > 0 ? selectedColor.images : product.images).map((img, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => setSelectedImage(img)}
|
|
className={`w-16 h-16 rounded-xl overflow-hidden border-2 transition-all cursor-pointer shrink-0 ${
|
|
selectedImage === img ? 'border-purple-500 scale-105 shadow-md' : 'border-neutral-800 opacity-60 hover:opacity-100'
|
|
}`}
|
|
>
|
|
<OptimizedImage
|
|
src={img}
|
|
alt=""
|
|
width={120}
|
|
height={120}
|
|
quality={75}
|
|
className="w-full h-full object-cover"
|
|
showSkeleton={false}
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Trust note */}
|
|
<div className="mt-4 bg-white/5 border border-white/10 rounded-2xl p-3 text-xs text-neutral-300 flex items-center gap-2.5">
|
|
<ShieldCheck className={`w-5 h-5 shrink-0 ${selectedCity.firstExchangeFree ? 'text-emerald-400' : 'text-purple-400'}`} />
|
|
<div className="leading-tight">
|
|
{selectedCity.firstExchangeFree ? (
|
|
<><strong>1er Cambio de Talla GRATIS en {selectedCity.name}</strong> • Cambios posteriores con flete estándar.</>
|
|
) : (
|
|
<><strong>Garantía de Cambio en {selectedCity.name}</strong> • Envío asegurado contraentrega.</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Product Details, Colorways, Sizes & WhatsApp Request */}
|
|
<div className="md:w-1/2 p-5 sm:p-6 flex flex-col justify-between overflow-y-auto max-h-[60vh] md:max-h-[88vh] space-y-4">
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<div>
|
|
<div className="flex items-center justify-between">
|
|
<span className={`text-xs font-black uppercase tracking-wider text-transparent bg-clip-text bg-gradient-to-r ${currentBrand.colors.primaryGradient}`}>
|
|
{product.brand} • {product.category || 'Sneakers'}
|
|
</span>
|
|
<span className="text-[10px] text-neutral-400 bg-neutral-800 px-2 py-0.5 rounded-full font-mono">
|
|
Ref: {product.id.slice(0, 10)}
|
|
</span>
|
|
</div>
|
|
<h2 className="text-xl sm:text-2xl font-black text-white leading-snug mt-0.5">
|
|
{product.modelName || product.name}
|
|
</h2>
|
|
<div className="flex items-center space-x-2 text-xs text-neutral-400 mt-1">
|
|
<div className="flex items-center text-amber-400 font-bold">
|
|
<Star className="w-4 h-4 fill-amber-400 mr-1" />
|
|
<span>{socialProof.rating}</span>
|
|
<span className="text-neutral-500 font-normal ml-1">({socialProof.reviewsCount} opiniones)</span>
|
|
</div>
|
|
<span>•</span>
|
|
<span className="text-neutral-300 font-medium">+{socialProof.salesCount} pares despachados</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Price Box */}
|
|
<div className="bg-neutral-950/80 border border-neutral-800 rounded-2xl p-3.5 space-y-2">
|
|
<div className="flex items-baseline space-x-3">
|
|
<span className={`text-2xl sm:text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r ${currentBrand.colors.primaryGradient}`}>
|
|
{formatCOP(currentPrice)}
|
|
</span>
|
|
{product.originalPrice && (
|
|
<span className="text-sm text-neutral-500 line-through font-bold">
|
|
{formatCOP(product.originalPrice)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Separation breakdown banner */}
|
|
<div className="bg-white/5 rounded-xl p-2.5 border border-white/10 text-xs space-y-1">
|
|
<p className="font-bold text-white flex items-center gap-1">
|
|
<Zap className="w-3.5 h-3.5 fill-white" />
|
|
¡Pago Contraentrega en {selectedCity.name}!
|
|
</p>
|
|
<p className="text-neutral-300 text-[11px] leading-relaxed">
|
|
Separas con solo <strong className="text-white">{formatCOP(separation.depositAmount)}</strong> y pagas los <strong className="text-white">{formatCOP(separation.balanceDue)}</strong> al mensajero cuando llegue a tu casa.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Colorways Selector (Si hay más de 1 color o variante) */}
|
|
{colorsList.length > 0 && (
|
|
<div className="space-y-2 pt-1 border-t border-neutral-800/80">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-black text-white uppercase flex items-center gap-1.5">
|
|
<Palette className="w-3.5 h-3.5 text-purple-400" />
|
|
<span>ESTILO/COLOR SELECCIONADO:</span>
|
|
</span>
|
|
<span className="text-xs font-bold text-purple-300">
|
|
{selectedColor.colorName}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2.5 overflow-x-auto pb-1.5 pt-0.5 scrollbar-none">
|
|
{colorsList.map((variant) => {
|
|
const isCurrent = selectedColor.id === variant.id;
|
|
return (
|
|
<button
|
|
key={variant.id}
|
|
onClick={() => handleColorChange(variant)}
|
|
title={variant.colorName}
|
|
className={`relative w-14 h-14 sm:w-16 sm:h-16 rounded-2xl overflow-hidden shrink-0 border-2 transition-all cursor-pointer p-0.5 ${
|
|
isCurrent
|
|
? 'border-purple-500 shadow-[0_0_15px_rgba(168,85,247,0.4)] scale-105 ring-2 ring-purple-400/50 bg-purple-950/50'
|
|
: 'border-neutral-800 hover:border-neutral-600 bg-neutral-900 opacity-75 hover:opacity-100'
|
|
}`}
|
|
>
|
|
<OptimizedImage
|
|
src={variant.image}
|
|
alt={variant.colorName}
|
|
width={90}
|
|
height={90}
|
|
quality={75}
|
|
className="w-full h-full object-cover rounded-xl"
|
|
showSkeleton={false}
|
|
/>
|
|
{isCurrent && (
|
|
<div className="absolute top-1 right-1 w-3.5 h-3.5 bg-purple-600 rounded-full flex items-center justify-center border border-white">
|
|
<Check className="w-2.5 h-2.5 text-white" />
|
|
</div>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Size Selector with Stock & WhatsApp sourcing option */}
|
|
<div className="space-y-2 pt-1 border-t border-neutral-800/80">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-black text-white uppercase">
|
|
Selecciona tu Talla (CO / Nacional):
|
|
</span>
|
|
<span className="text-[11px] text-neutral-400 font-medium">
|
|
{isSizeInStock ? (
|
|
<span className="text-emerald-400 font-bold flex items-center gap-1">
|
|
<Check className="w-3 h-3" /> Stock Inmediato
|
|
</span>
|
|
) : (
|
|
<span className="text-amber-400 font-bold flex items-center gap-1">
|
|
<Sparkles className="w-3 h-3" /> Solicitud Bajo Pedido
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-5 sm:grid-cols-5 gap-2">
|
|
{displaySizes.map((size) => {
|
|
const inStock = availableSizes.includes(size);
|
|
const isSelected = selectedSize === size;
|
|
const sizePrice = getProductEffectivePrice(product, selectedColor, size);
|
|
const hasCustomSizePrice = sizePrice !== product.retailPrice;
|
|
|
|
return (
|
|
<button
|
|
key={size}
|
|
onClick={() => setSelectedSize(size)}
|
|
className={`relative py-2 rounded-xl text-xs font-black transition-all cursor-pointer flex flex-col items-center justify-center ${
|
|
isSelected
|
|
? inStock
|
|
? `bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white shadow-lg scale-105 border border-white/30`
|
|
: 'bg-amber-600 text-white shadow-lg scale-105 border border-amber-300'
|
|
: inStock
|
|
? 'bg-neutral-950 hover:bg-neutral-800 text-white border border-neutral-800'
|
|
: 'bg-neutral-950/60 hover:bg-neutral-900 text-neutral-500 border border-neutral-800/60'
|
|
}`}
|
|
>
|
|
<span>{size}</span>
|
|
<span className="text-[8px] font-mono leading-none mt-0.5 opacity-80">
|
|
{hasCustomSizePrice ? `$${Math.round(sizePrice / 1000)}k` : inStock ? 'Listo' : 'Pedir'}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Banner: ¿No encuentras tu talla? */}
|
|
<div className="bg-gradient-to-r from-purple-950/40 to-neutral-950 border border-purple-500/30 rounded-2xl p-3 flex items-center justify-between gap-2.5">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-7 h-7 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center shrink-0">
|
|
<MessageCircle className="w-4 h-4" />
|
|
</div>
|
|
<div>
|
|
<p className="text-[11px] font-bold text-white">¿No encuentras tu talla exacta?</p>
|
|
<p className="text-[10px] text-neutral-400">Verificamos disponibilidad con bodega.</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => handleWhatsAppRequestSize(selectedSize)}
|
|
className="bg-purple-600 hover:bg-purple-500 text-white font-black text-[10px] px-2.5 py-1.5 rounded-xl shrink-0 cursor-pointer shadow flex items-center gap-1"
|
|
>
|
|
<span>Pedir Talla</span>
|
|
<ArrowRight className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div className="text-xs text-neutral-400 leading-relaxed border-t border-neutral-800 pt-3">
|
|
<p>{product.description}</p>
|
|
</div>
|
|
|
|
{/* Shipping note */}
|
|
<div className="flex items-center gap-2 text-xs text-neutral-300 bg-neutral-950 p-2.5 rounded-xl border border-neutral-800">
|
|
<Truck className="w-4 h-4 text-white shrink-0" />
|
|
<div>
|
|
<strong className="text-white">{selectedCity.name}: </strong>
|
|
<span>{selectedCity.deliveryTime} ({selectedCity.shippingCost === 0 ? 'Envío Gratis' : formatCOP(selectedCity.shippingCost)})</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="pt-4 border-t border-neutral-800 flex flex-col gap-2.5 mt-4">
|
|
{isSizeInStock ? (
|
|
<>
|
|
<button
|
|
onClick={handleWhatsAppDirect}
|
|
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-sm py-3.5 px-4 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-emerald-600/20 active:scale-95 transition-all cursor-pointer"
|
|
>
|
|
<MessageCircle className="w-4 h-4 fill-white" />
|
|
<span>Pedir por WhatsApp (Contraentrega Talla {selectedSize} • {selectedColor.colorName})</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleAdd}
|
|
disabled={added}
|
|
className={`w-full font-bold text-sm py-3.5 px-4 rounded-2xl flex items-center justify-center space-x-2 transition-all cursor-pointer ${
|
|
added
|
|
? 'bg-neutral-800 text-white border border-white/20'
|
|
: `bg-gradient-to-r ${currentBrand.colors.primaryGradient} hover:opacity-95 text-white shadow-lg active:scale-95`
|
|
}`}
|
|
>
|
|
{added ? (
|
|
<>
|
|
<Check className="w-4 h-4 text-white" />
|
|
<span>¡Añadido al Carrito!</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<ShoppingBag className="w-4 h-4" />
|
|
<span>Añadir al Carrito (Talla {selectedSize})</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<div className="space-y-2">
|
|
<div className="bg-amber-500/10 border border-amber-500/30 rounded-2xl p-3 text-xs text-amber-200 flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4 shrink-0 text-amber-400" />
|
|
<span>
|
|
La talla <strong>{selectedSize}</strong> en <strong>{selectedColor.colorName}</strong> no tiene despacho inmediato en 24h, pero podemos pedirla a la bodega central para ti.
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleWhatsAppRequestSize(selectedSize)}
|
|
className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 hover:opacity-95 text-white font-black text-sm py-3.5 px-4 rounded-2xl flex items-center justify-center space-x-2 shadow-xl shadow-emerald-600/30 active:scale-95 transition-all cursor-pointer"
|
|
>
|
|
<MessageCircle className="w-4 h-4 fill-white" />
|
|
<span>Solicitar Talla {selectedSize} Bajo Pedido por WhatsApp</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|