almeira-web/dripzone-app/src/components/store/ProductModal.tsx

429 lines
20 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 } from '@/lib/pricing-engine';
import { useBrand } from '@/context/BrandContext';
import { getProductSocialProof } from '@/lib/social-proof';
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;
// 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);
const socialProof = getProductSocialProof(product);
const separation = calculateSeparationBreakdown(product.retailPrice);
const handleAdd = () => {
if (!isSizeInStock) return;
onAddToCart(
{
...product,
name: `${product.modelName || product.name} (${selectedColor.colorName})`,
images: selectedColor.images || [selectedImage],
},
selectedSize
);
setAdded(true);
setTimeout(() => {
setAdded(false);
onClose();
}, 1200);
};
// Pedido normal por WhatsApp
const handleWhatsAppDirect = () => {
const phone = currentBrand.whatsappNumber || '573167455877';
const text = `Hola ${currentBrand.name}! 👋 Quiero pedir las zapatillas:
👟 *${product.modelName || product.name}*
🎨 Color: *${selectedColor.colorName}*
📏 Talla: *${selectedSize}* (Horma Nacional)
💎 Precio: *${formatCOP(product.retailPrice)} COP*
🚚 Entrega en: *${selectedCity.name}* (Pago Contraentrega)
⚡ Deseo separar con $10.000 COP y cancelar el saldo en mi casa.
¿Me confirman disponibilidad para despacho por favor?`;
window.open(`https://wa.me/${phone}?text=${encodeURIComponent(text)}`, '_blank');
};
// Solicitud especial de talla no disponible
const handleWhatsAppRequestSize = (sizeToRequest: string) => {
const phone = currentBrand.whatsappNumber || '573167455877';
const text = `Hola ${currentBrand.name}! 👋 Estaba revisando la tienda y me gustaron mucho estas zapatillas:
👟 *${product.modelName || product.name}*
🎨 Color: *${selectedColor.colorName}*
📏 Talla que necesito: *${sizeToRequest}* (Actualmente no aparece disponible en stock inmediato)
📍 Ciudad de entrega: *${selectedCity.name}*
¿Es posible conseguir esta talla bajo pedido con sus bodegas aliadas para despacho contraentrega? Quedo muy atento, ¡muchas gracias!`;
window.open(`https://wa.me/${phone}?text=${encodeURIComponent(text)}`, '_blank');
};
// All sizes to show (combination of standard sizes and any custom product sizes)
const displaySizes = Array.from(new Set([...ALL_STANDARD_SIZES, ...product.sizes])).sort(
(a, b) => parseInt(a, 10) - parseInt(b, 10)
);
return (
<div className="fixed inset-0 z-50 bg-black/85 backdrop-blur-md flex items-center justify-center p-3 sm:p-4 overflow-y-auto animate-in fade-in duration-200">
<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">
<img
src={selectedImage}
alt={product.name}
className="w-full h-full object-cover transition-all duration-300"
/>
<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?.length || 1} 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'
}`}
>
<img src={img} alt="" className="w-full h-full object-cover" />
</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(product.retailPrice)}
</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>Color Seleccionado:</span>
</span>
<span className="text-xs font-bold text-purple-300">
{selectedColor.colorName}
</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{colorsList.map((variant) => {
const isCurrent = selectedColor.id === variant.id;
return (
<button
key={variant.id}
onClick={() => handleColorChange(variant)}
className={`flex items-center gap-2 p-2 rounded-xl border text-left transition-all cursor-pointer ${
isCurrent
? 'bg-purple-950/40 border-purple-500 shadow-md ring-1 ring-purple-500 text-white'
: 'bg-neutral-950 hover:bg-neutral-800/70 border-neutral-800 text-neutral-300'
}`}
>
<div className="w-9 h-9 rounded-lg overflow-hidden shrink-0 border border-neutral-700 bg-neutral-900">
<img src={variant.image} alt={variant.colorName} className="w-full h-full object-cover" />
</div>
<span className="text-[11px] font-bold leading-tight line-clamp-2">
{variant.colorName}
</span>
</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;
return (
<button
key={size}
onClick={() => setSelectedSize(size)}
className={`relative py-2.5 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">
{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 en 5 minutos.</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>
);
};