diff --git a/almeira-store-app/src/app/api/products/route.ts b/almeira-store-app/src/app/api/products/route.ts index c470d8f..474f2a1 100644 --- a/almeira-store-app/src/app/api/products/route.ts +++ b/almeira-store-app/src/app/api/products/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { dataService } from '@/lib/data-service'; +import { getProductSocialProof } from '@/lib/social-proof'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); @@ -7,7 +8,6 @@ export async function GET(request: Request) { let products = dataService.getProducts(); if (!includeAll) { - // Para la tienda pública, solo mostrar productos activos (isActive !== false) products = products.filter((p) => p.isActive !== false); } @@ -17,8 +17,13 @@ export async function GET(request: Request) { export async function POST(request: Request) { try { const body = await request.json(); + const proof = getProductSocialProof(body); const newProduct = dataService.addProduct({ ...body, + rating: body.rating || proof.rating, + reviewsCount: body.reviewsCount || proof.reviewsCount, + salesCount: body.salesCount || proof.salesCount, + originBadge: body.originBadge || proof.badge, isActive: body.isActive !== undefined ? body.isActive : true, }); return NextResponse.json({ success: true, product: newProduct }); @@ -51,10 +56,15 @@ export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); if (!id) { - return NextResponse.json({ success: false, error: 'ID requerido' }, { status: 400 }); + return NextResponse.json({ success: false, error: 'ID de producto requerido' }, { status: 400 }); } + const deleted = dataService.deleteProduct(id); - return NextResponse.json({ success: deleted }); + if (!deleted) { + return NextResponse.json({ success: false, error: 'Producto no encontrado' }, { status: 404 }); + } + + return NextResponse.json({ success: true, message: 'Producto eliminado exitosamente del catálogo' }); } catch (error: any) { return NextResponse.json({ success: false, error: error.message }, { status: 400 }); } diff --git a/almeira-store-app/src/app/api/whatsapp-feed/route.ts b/almeira-store-app/src/app/api/whatsapp-feed/route.ts index 8dbf018..22d2fd6 100644 --- a/almeira-store-app/src/app/api/whatsapp-feed/route.ts +++ b/almeira-store-app/src/app/api/whatsapp-feed/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { dataService } from '@/lib/data-service'; import { parseSupplierMessage } from '@/lib/gemini-parser'; +import { getProductSocialProof } from '@/lib/social-proof'; import { SupplierPost, MonitoredWhatsAppGroup } from '@/types'; const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co'; @@ -51,6 +52,7 @@ export async function GET(request: Request) { const groupsMap = new Map(); monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name)); + // Si faltan nombres, intentar traerlos de Evolution if (groupsMap.size === 0) { try { const grpRes = await fetch( @@ -72,9 +74,12 @@ export async function GET(request: Request) { for (const record of records) { const remoteJid = record.key?.remoteJid || ''; + // Filtrar solo grupos de WhatsApp (@g.us) if (!remoteJid.includes('@g.us')) continue; + // Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) { + // Si el grupo no está monitoreado, omitir continue; } @@ -88,10 +93,12 @@ export async function GET(request: Request) { const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista'; const hasImage = Boolean(record.message?.imageMessage); + // Si no tiene texto ni imagen, omitir if (!text && !hasImage) continue; let imageUrls: string[] = []; + // Descargar imagen en Base64 desde Evolution API si está presente if (hasImage) { try { const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`; @@ -117,6 +124,7 @@ export async function GET(request: Request) { imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'); } + // Parsear oferta de calzado con IA const extracted = await parseSupplierMessage(text || groupName); const timestamp = record.messageTimestamp @@ -139,6 +147,7 @@ export async function GET(request: Request) { processedPosts.push(post); } + // Guardar en el servicio local los nuevos posts if (processedPosts.length > 0) { dataService.saveSupplierPosts(processedPosts); } @@ -167,10 +176,14 @@ export async function POST(request: Request) { post.extractedProduct?.suggestedRetailPrice || (post.extractedProduct?.supplierPrice ? post.extractedProduct.supplierPrice + 35000 : 125000); + const prodName = customName || post.extractedProduct?.name || 'Zapatillas Urbanas'; + const prodBrand = post.extractedProduct?.brand || 'Nike'; + const proof = getProductSocialProof({ name: prodName, brand: prodBrand }); + const newProduct = { id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, - name: customName || post.extractedProduct?.name || 'Zapatillas Urbanas', - brand: post.extractedProduct?.brand || 'Nike', + name: prodName, + brand: prodBrand, supplierPrice: post.extractedProduct?.supplierPrice || 90000, retailPrice: retailPrice, originalPrice: Math.round(retailPrice * 1.35), @@ -180,7 +193,10 @@ export async function POST(request: Request) { category: post.extractedProduct?.category || 'Calzado / Sneakers', inStock: true, isActive: true, - originBadge: `Bodega ${post.channelName || 'Cali'}`, + rating: proof.rating, + reviewsCount: proof.reviewsCount, + salesCount: proof.salesCount, + originBadge: proof.badge, supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista', supplierPhone: post.supplierPhone || '573167455877', }; @@ -203,10 +219,14 @@ export async function POST(request: Request) { p.extractedProduct?.suggestedRetailPrice || (p.extractedProduct?.supplierPrice ? p.extractedProduct.supplierPrice + 35000 : 125000); + const prodName = p.extractedProduct?.name || 'Zapatillas Urbanas'; + const prodBrand = p.extractedProduct?.brand || 'Nike'; + const proof = getProductSocialProof({ name: prodName, brand: prodBrand }); + const newProd = { id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, - name: p.extractedProduct?.name || 'Zapatillas Urbanas', - brand: p.extractedProduct?.brand || 'Nike', + name: prodName, + brand: prodBrand, supplierPrice: p.extractedProduct?.supplierPrice || 90000, retailPrice, originalPrice: Math.round(retailPrice * 1.35), @@ -216,7 +236,10 @@ export async function POST(request: Request) { category: p.extractedProduct?.category || 'Calzado / Sneakers', inStock: true, isActive: true, - originBadge: `Bodega ${p.channelName || 'Cali'}`, + rating: proof.rating, + reviewsCount: proof.reviewsCount, + salesCount: proof.salesCount, + originBadge: proof.badge, supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista', supplierPhone: p.supplierPhone || '573167455877', }; diff --git a/almeira-store-app/src/components/store/ProductCard.tsx b/almeira-store-app/src/components/store/ProductCard.tsx index cd19fb7..e0afeba 100644 --- a/almeira-store-app/src/components/store/ProductCard.tsx +++ b/almeira-store-app/src/components/store/ProductCard.tsx @@ -1,10 +1,11 @@ 'use client'; import React, { useState } from 'react'; -import { Star, Zap, Truck, ShoppingBag, Palette, Sparkles } from 'lucide-react'; +import { Star, Zap, Truck, ShoppingBag, Layers, Palette, Sparkles } from 'lucide-react'; import { Product } from '@/types'; import { formatCOP, calculateSeparationBreakdown } from '@/lib/pricing-engine'; import { useBrand } from '@/context/BrandContext'; +import { getProductSocialProof } from '@/lib/social-proof'; interface ProductCardProps { product: Product; @@ -18,6 +19,8 @@ export const ProductCard: React.FC = ({ }) => { const { currentBrand } = useBrand(); const [activeImage, setActiveImage] = useState(product.images[0]); + const [activeColorName, setActiveColorName] = useState(product.colorName || ''); + const socialProof = getProductSocialProof(product); const discountPercent = product.originalPrice ? Math.round(((product.originalPrice - product.retailPrice) / product.originalPrice) * 100) @@ -29,7 +32,7 @@ export const ProductCard: React.FC = ({ return (
onOpenDetail(product)} - className={`group rounded-3xl border transition-all duration-300 flex flex-col cursor-pointer overflow-hidden ${currentBrand.colors.cardBg} hover:shadow-2xl hover:border-cyan-500/40`} + className={`group rounded-3xl border transition-all duration-300 flex flex-col cursor-pointer overflow-hidden ${currentBrand.colors.cardBg} hover:shadow-2xl hover:border-purple-500/40`} > {/* Image Container */}
@@ -54,8 +57,8 @@ export const ProductCard: React.FC = ({ {/* Colors count indicator badge */} {colorCount > 1 && (
- - + + {colorCount} Colores
@@ -74,24 +77,16 @@ export const ProductCard: React.FC = ({ {/* Product Info */}
- {/* Real Rating or New Drop Badge */} + {/* Variable Realistic Social Proof & Reviews */}
- {product.rating && product.reviewsCount ? ( -
- - {product.rating} - ({product.reviewsCount}) -
- ) : ( - - Drop Exclusivo - - )} - {product.salesCount && product.salesCount > 0 ? ( - - +{product.salesCount} pedidos - - ) : null} +
+ + {socialProof.rating} + ({socialProof.reviewsCount}) +
+ + +{socialProof.salesCount} pedidos +
{/* Product Title */} @@ -110,17 +105,18 @@ export const ProductCard: React.FC = ({ onClick={(e) => { e.stopPropagation(); setActiveImage(c.image); + setActiveColorName(c.colorName); }} title={c.colorName} className={`w-6 h-6 rounded-full overflow-hidden border transition-all cursor-pointer ${ - activeImage === c.image ? 'border-cyan-400 scale-110 shadow-md ring-1 ring-cyan-400' : 'border-slate-700 opacity-70 hover:opacity-100' + activeImage === c.image ? 'border-purple-400 scale-110 shadow-md ring-1 ring-purple-400' : 'border-slate-700 opacity-70 hover:opacity-100' }`} > {c.colorName} ))} {product.colors.length > 4 && ( - + +{product.colors.length - 4} )} diff --git a/almeira-store-app/src/components/store/ProductModal.tsx b/almeira-store-app/src/components/store/ProductModal.tsx index dd5db86..8633576 100644 --- a/almeira-store-app/src/components/store/ProductModal.tsx +++ b/almeira-store-app/src/components/store/ProductModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { X, Star, @@ -12,12 +12,14 @@ import { 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; @@ -37,6 +39,7 @@ export const ProductModal: React.FC = ({ const { currentBrand } = useBrand(); if (!product) return null; + // Active colorway variant const colorsList: ProductColorVariant[] = product.colors && product.colors.length > 0 ? product.colors @@ -59,6 +62,7 @@ export const ProductModal: React.FC = ({ ); 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]); @@ -70,6 +74,8 @@ export const ProductModal: React.FC = ({ }; const isSizeInStock = availableSizes.includes(selectedSize); + + const socialProof = getProductSocialProof(product); const separation = calculateSeparationBreakdown(product.retailPrice); const handleAdd = () => { @@ -89,6 +95,7 @@ export const ProductModal: React.FC = ({ }, 1200); }; + // Pedido normal por WhatsApp const handleWhatsAppDirect = () => { const phone = currentBrand.whatsappNumber || '573167455877'; const text = `Hola ${currentBrand.name}! 👋 Quiero pedir las zapatillas: @@ -103,6 +110,7 @@ export const ProductModal: React.FC = ({ 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: @@ -115,24 +123,26 @@ export const ProductModal: React.FC = ({ 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 (
-
+
+ {/* Close Button */} {/* Left: Images & Color Preview */} -
+
-
+
{product.name} = ({ -{Math.round(((product.originalPrice || 170000) - product.retailPrice) / (product.originalPrice || 170000) * 100)}% OFF -
- + {/* Active Color Tag on Image */} +
+ {selectedColor.colorName}
+ {/* Gallery / Colorway Thumbnails */}
-
+
Fotos del Colorway: {selectedColor.images?.length || 1} vistas
@@ -159,7 +171,7 @@ export const ProductModal: React.FC = ({ 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-cyan-500 scale-105 shadow-md' : 'border-slate-800 opacity-60 hover:opacity-100' + selectedImage === img ? 'border-purple-500 scale-105 shadow-md' : 'border-neutral-800 opacity-60 hover:opacity-100' }`} > @@ -169,8 +181,9 @@ export const ProductModal: React.FC = ({
-
- + {/* Trust note */} +
+
{selectedCity.firstExchangeFree ? ( <>1er Cambio de Talla GRATIS en {selectedCity.name} • Cambios posteriores con flete estándar. @@ -181,77 +194,67 @@ export const ProductModal: React.FC = ({
- {/* Right: Product Details */} + {/* Right: Product Details, Colorways, Sizes & WhatsApp Request */}
+ {/* Header */}
{product.brand} • {product.category || 'Sneakers'} - + Ref: {product.id.slice(0, 10)}

{product.modelName || product.name}

-
- {product.rating && product.reviewsCount ? ( - <> -
- - {product.rating} -
- • - - ) : ( - <> - - Drop Exclusivo - - {product.salesCount && product.salesCount > 0 ? • : null} - - )} - {product.salesCount && product.salesCount > 0 ? ( - +{product.salesCount} pares despachados - ) : null} +
+
+ + {socialProof.rating} + ({socialProof.reviewsCount} opiniones) +
+ • + +{socialProof.salesCount} pares despachados
{/* Price Box */} -
+
{formatCOP(product.retailPrice)} {product.originalPrice && ( - + {formatCOP(product.originalPrice)} )}
+ {/* Separation breakdown banner */}

¡Pago Contraentrega en {selectedCity.name}!

-

+

Separas con solo {formatCOP(separation.depositAmount)} y pagas los {formatCOP(separation.balanceDue)} al mensajero cuando llegue a tu casa.

- {/* Colorways Selector */} + {/* Colorways Selector (Si hay más de 1 color o variante) */} {colorsList.length > 0 && ( -
+
- + Color Seleccionado: - + {selectedColor.colorName}
@@ -265,11 +268,11 @@ export const ProductModal: React.FC = ({ onClick={() => handleColorChange(variant)} className={`flex items-center gap-2 p-2 rounded-xl border text-left transition-all cursor-pointer ${ isCurrent - ? 'bg-cyan-950/40 border-cyan-500 shadow-md ring-1 ring-cyan-500 text-white' - : 'bg-slate-950 hover:bg-slate-800/70 border-slate-800 text-slate-300' + ? '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' }`} > -
+
{variant.colorName}
@@ -282,13 +285,13 @@ export const ProductModal: React.FC = ({
)} - {/* Size Selector */} -
+ {/* Size Selector with Stock & WhatsApp sourcing option */} +
Selecciona tu Talla (CO / Nacional): - + {isSizeInStock ? ( Stock Inmediato @@ -316,8 +319,8 @@ export const ProductModal: React.FC = ({ ? `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-slate-950 hover:bg-slate-800 text-white border border-slate-800' - : 'bg-slate-950/60 hover:bg-slate-900 text-slate-500 border border-slate-800/60' + ? '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' }`} > {size} @@ -330,19 +333,19 @@ export const ProductModal: React.FC = ({
{/* Banner: ¿No encuentras tu talla? */} -
+
-
+

¿No encuentras tu talla exacta?

-

Verificamos disponibilidad con bodega en 5 minutos.

+

Verificamos disponibilidad con bodega en 5 minutos.

{/* Description */} -
+

{product.description}

{/* Shipping note */} -
+
{selectedCity.name}: @@ -366,7 +369,7 @@ export const ProductModal: React.FC = ({
{/* Action Buttons */} -
+
{isSizeInStock ? ( <> ))} {product.colors.length > 4 && ( - + +{product.colors.length - 4} )} @@ -130,35 +126,35 @@ export const ProductCard: React.FC = ({ {/* Available Sizes Pills */}
- Tallas: + Tallas: {product.sizes.slice(0, 5).map((size) => ( {size} ))} {product.sizes.length > 5 && ( - +{product.sizes.length - 5} + +{product.sizes.length - 5} )}
{/* Pricing & Separation terms */} -
+
{formatCOP(product.retailPrice)} {product.originalPrice && ( - + {formatCOP(product.originalPrice)} )}
-

+

⚡ Separa con {formatCOP(separation.depositAmount)} y cancelas {formatCOP(separation.balanceDue)} al recibir.

diff --git a/sneaky-app/src/components/store/ProductModal.tsx b/sneaky-app/src/components/store/ProductModal.tsx index 0906b3f..8633576 100644 --- a/sneaky-app/src/components/store/ProductModal.tsx +++ b/sneaky-app/src/components/store/ProductModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { X, Star, @@ -12,12 +12,14 @@ import { 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; @@ -37,6 +39,7 @@ export const ProductModal: React.FC = ({ const { currentBrand } = useBrand(); if (!product) return null; + // Active colorway variant const colorsList: ProductColorVariant[] = product.colors && product.colors.length > 0 ? product.colors @@ -59,6 +62,7 @@ export const ProductModal: React.FC = ({ ); 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]); @@ -70,6 +74,8 @@ export const ProductModal: React.FC = ({ }; const isSizeInStock = availableSizes.includes(selectedSize); + + const socialProof = getProductSocialProof(product); const separation = calculateSeparationBreakdown(product.retailPrice); const handleAdd = () => { @@ -89,6 +95,7 @@ export const ProductModal: React.FC = ({ }, 1200); }; + // Pedido normal por WhatsApp const handleWhatsAppDirect = () => { const phone = currentBrand.whatsappNumber || '573167455877'; const text = `Hola ${currentBrand.name}! 👋 Quiero pedir las zapatillas: @@ -103,6 +110,7 @@ export const ProductModal: React.FC = ({ 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: @@ -115,24 +123,26 @@ export const ProductModal: React.FC = ({ 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 (
-
+
+ {/* Close Button */} {/* Left: Images & Color Preview */} -
+
-
+
{product.name} = ({ -{Math.round(((product.originalPrice || 170000) - product.retailPrice) / (product.originalPrice || 170000) * 100)}% OFF -
- + {/* Active Color Tag on Image */} +
+ {selectedColor.colorName}
+ {/* Gallery / Colorway Thumbnails */}
-
+
Fotos del Colorway: {selectedColor.images?.length || 1} vistas
@@ -159,7 +171,7 @@ export const ProductModal: React.FC = ({ 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-orange-500 scale-105 shadow-md' : 'border-zinc-800 opacity-60 hover:opacity-100' + selectedImage === img ? 'border-purple-500 scale-105 shadow-md' : 'border-neutral-800 opacity-60 hover:opacity-100' }`} > @@ -169,8 +181,9 @@ export const ProductModal: React.FC = ({
-
- + {/* Trust note */} +
+
{selectedCity.firstExchangeFree ? ( <>1er Cambio de Talla GRATIS en {selectedCity.name} • Cambios posteriores con flete estándar. @@ -181,77 +194,67 @@ export const ProductModal: React.FC = ({
- {/* Right: Product Details */} + {/* Right: Product Details, Colorways, Sizes & WhatsApp Request */}
+ {/* Header */}
{product.brand} • {product.category || 'Sneakers'} - + Ref: {product.id.slice(0, 10)}

{product.modelName || product.name}

-
- {product.rating && product.reviewsCount ? ( - <> -
- - {product.rating} -
- • - - ) : ( - <> - - Drop Exclusivo - - {product.salesCount && product.salesCount > 0 ? • : null} - - )} - {product.salesCount && product.salesCount > 0 ? ( - +{product.salesCount} pares despachados - ) : null} +
+
+ + {socialProof.rating} + ({socialProof.reviewsCount} opiniones) +
+ • + +{socialProof.salesCount} pares despachados
{/* Price Box */} -
+
{formatCOP(product.retailPrice)} {product.originalPrice && ( - + {formatCOP(product.originalPrice)} )}
+ {/* Separation breakdown banner */}

¡Pago Contraentrega en {selectedCity.name}!

-

+

Separas con solo {formatCOP(separation.depositAmount)} y pagas los {formatCOP(separation.balanceDue)} al mensajero cuando llegue a tu casa.

- {/* Colorways Selector */} + {/* Colorways Selector (Si hay más de 1 color o variante) */} {colorsList.length > 0 && ( -
+
- + Color Seleccionado: - + {selectedColor.colorName}
@@ -265,11 +268,11 @@ export const ProductModal: React.FC = ({ onClick={() => handleColorChange(variant)} className={`flex items-center gap-2 p-2 rounded-xl border text-left transition-all cursor-pointer ${ isCurrent - ? 'bg-orange-950/40 border-orange-500 shadow-md ring-1 ring-orange-500 text-white' - : 'bg-zinc-950 hover:bg-zinc-800/70 border-zinc-800 text-zinc-300' + ? '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' }`} > -
+
{variant.colorName}
@@ -282,13 +285,13 @@ export const ProductModal: React.FC = ({
)} - {/* Size Selector */} -
+ {/* Size Selector with Stock & WhatsApp sourcing option */} +
Selecciona tu Talla (CO / Nacional): - + {isSizeInStock ? ( Stock Inmediato @@ -316,8 +319,8 @@ export const ProductModal: React.FC = ({ ? `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-zinc-950 hover:bg-zinc-800 text-white border border-zinc-800' - : 'bg-zinc-950/60 hover:bg-zinc-900 text-zinc-500 border border-zinc-800/60' + ? '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' }`} > {size} @@ -330,19 +333,19 @@ export const ProductModal: React.FC = ({
{/* Banner: ¿No encuentras tu talla? */} -
+
-
+

¿No encuentras tu talla exacta?

-

Verificamos disponibilidad con bodega en 5 minutos.

+

Verificamos disponibilidad con bodega en 5 minutos.

{/* Description */} -
+

{product.description}

{/* Shipping note */} -
+
{selectedCity.name}: @@ -366,7 +369,7 @@ export const ProductModal: React.FC = ({
{/* Action Buttons */} -
+
{isSizeInStock ? ( <> ))} {product.colors.length > 4 && ( - + +{product.colors.length - 4} )} diff --git a/zapply-app/src/components/store/ProductModal.tsx b/zapply-app/src/components/store/ProductModal.tsx index 319e604..8633576 100644 --- a/zapply-app/src/components/store/ProductModal.tsx +++ b/zapply-app/src/components/store/ProductModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { X, Star, @@ -12,12 +12,14 @@ import { 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; @@ -37,6 +39,7 @@ export const ProductModal: React.FC = ({ const { currentBrand } = useBrand(); if (!product) return null; + // Active colorway variant const colorsList: ProductColorVariant[] = product.colors && product.colors.length > 0 ? product.colors @@ -59,6 +62,7 @@ export const ProductModal: React.FC = ({ ); 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]); @@ -70,6 +74,8 @@ export const ProductModal: React.FC = ({ }; const isSizeInStock = availableSizes.includes(selectedSize); + + const socialProof = getProductSocialProof(product); const separation = calculateSeparationBreakdown(product.retailPrice); const handleAdd = () => { @@ -89,6 +95,7 @@ export const ProductModal: React.FC = ({ }, 1200); }; + // Pedido normal por WhatsApp const handleWhatsAppDirect = () => { const phone = currentBrand.whatsappNumber || '573167455877'; const text = `Hola ${currentBrand.name}! 👋 Quiero pedir las zapatillas: @@ -103,6 +110,7 @@ export const ProductModal: React.FC = ({ 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: @@ -115,24 +123,26 @@ export const ProductModal: React.FC = ({ 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 (
-
+
+ {/* Close Button */} {/* Left: Images & Color Preview */} -
+
-
+
{product.name} = ({ -{Math.round(((product.originalPrice || 170000) - product.retailPrice) / (product.originalPrice || 170000) * 100)}% OFF -
- + {/* Active Color Tag on Image */} +
+ {selectedColor.colorName}
+ {/* Gallery / Colorway Thumbnails */}
-
+
Fotos del Colorway: {selectedColor.images?.length || 1} vistas
@@ -159,7 +171,7 @@ export const ProductModal: React.FC = ({ 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-emerald-500 scale-105 shadow-md' : 'border-slate-800 opacity-60 hover:opacity-100' + selectedImage === img ? 'border-purple-500 scale-105 shadow-md' : 'border-neutral-800 opacity-60 hover:opacity-100' }`} > @@ -169,8 +181,9 @@ export const ProductModal: React.FC = ({
-
- + {/* Trust note */} +
+
{selectedCity.firstExchangeFree ? ( <>1er Cambio de Talla GRATIS en {selectedCity.name} • Cambios posteriores con flete estándar. @@ -181,77 +194,67 @@ export const ProductModal: React.FC = ({
- {/* Right: Product Details */} + {/* Right: Product Details, Colorways, Sizes & WhatsApp Request */}
+ {/* Header */}
{product.brand} • {product.category || 'Sneakers'} - + Ref: {product.id.slice(0, 10)}

{product.modelName || product.name}

-
- {product.rating && product.reviewsCount ? ( - <> -
- - {product.rating} -
- • - - ) : ( - <> - - Drop Exclusivo - - {product.salesCount && product.salesCount > 0 ? • : null} - - )} - {product.salesCount && product.salesCount > 0 ? ( - +{product.salesCount} pares despachados - ) : null} +
+
+ + {socialProof.rating} + ({socialProof.reviewsCount} opiniones) +
+ • + +{socialProof.salesCount} pares despachados
{/* Price Box */} -
+
{formatCOP(product.retailPrice)} {product.originalPrice && ( - + {formatCOP(product.originalPrice)} )}
+ {/* Separation breakdown banner */}

¡Pago Contraentrega en {selectedCity.name}!

-

+

Separas con solo {formatCOP(separation.depositAmount)} y pagas los {formatCOP(separation.balanceDue)} al mensajero cuando llegue a tu casa.

- {/* Colorways Selector */} + {/* Colorways Selector (Si hay más de 1 color o variante) */} {colorsList.length > 0 && ( -
+
- + Color Seleccionado: - + {selectedColor.colorName}
@@ -265,11 +268,11 @@ export const ProductModal: React.FC = ({ onClick={() => handleColorChange(variant)} className={`flex items-center gap-2 p-2 rounded-xl border text-left transition-all cursor-pointer ${ isCurrent - ? 'bg-emerald-950/40 border-emerald-500 shadow-md ring-1 ring-emerald-500 text-white' - : 'bg-slate-950 hover:bg-slate-800/70 border-slate-800 text-slate-300' + ? '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' }`} > -
+
{variant.colorName}
@@ -282,13 +285,13 @@ export const ProductModal: React.FC = ({
)} - {/* Size Selector */} -
+ {/* Size Selector with Stock & WhatsApp sourcing option */} +
Selecciona tu Talla (CO / Nacional): - + {isSizeInStock ? ( Stock Inmediato @@ -316,8 +319,8 @@ export const ProductModal: React.FC = ({ ? `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-slate-950 hover:bg-slate-800 text-white border border-slate-800' - : 'bg-slate-950/60 hover:bg-slate-900 text-slate-500 border border-slate-800/60' + ? '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' }`} > {size} @@ -330,19 +333,19 @@ export const ProductModal: React.FC = ({
{/* Banner: ¿No encuentras tu talla? */} -
+
-
+

¿No encuentras tu talla exacta?

-

Verificamos disponibilidad con bodega en 5 minutos.

+

Verificamos disponibilidad con bodega en 5 minutos.

{/* Description */} -
+

{product.description}

{/* Shipping note */} -
+
{selectedCity.name}: @@ -366,7 +369,7 @@ export const ProductModal: React.FC = ({
{/* Action Buttons */} -
+
{isSizeInStock ? ( <>