/* global React, CATALOGO, CAT_LABELS, detectarPatron, Placeholder, Eyebrow, Arr, HeartIcon, Reveal, useWishlist, useUI, waLink, CatProd */ const { useState, useMemo, useEffect, useCallback } = React; /* Construye el array de media de un producto: - Fotos: slot 1 (p.foto) + secundarias _2…_7 descubiertas con HEAD - Video: {id}_video.mp4 o .mov si existe → item { type:'video', url } - Fotos son { type:'img', url } El video se inserta al final, después de todas las fotos. */ function useMedia(p) { const [items, setItems] = useState(p?.foto ? [{ type: "img", url: p.foto }] : []); useEffect(() => { if (!p) { setItems([]); return; } const base = p.foto ? p.foto.replace(/\.[^.]+$/, "").replace(/_\d+$/, "") : null; const ext = p.foto ? (p.foto.match(/\.([^.?]+)(\?|$)/)?.[1] || "jpg") : "jpg"; // Muestra principal inmediato if (p.foto) setItems([{ type: "img", url: p.foto }]); else { setItems([]); return; } // Verifica imagen real cargando el elemento — más confiable que HEAD en Hostinger const checkImg = (url) => new Promise((resolve) => { const img = new window.Image(); img.onload = () => resolve({ type: "img", url }); img.onerror = () => resolve(null); img.src = url + "?t=" + Date.now(); // evita caché de errores }); // Video: igual pero con fetch HEAD (los videos no se "cargan" como imagen) const checkVideo = (url) => fetch(url, { method: "HEAD" }) .then(r => (r.ok && r.headers.get("content-type")?.includes("video")) ? { type: "video", url } : null) .catch(() => null); // Fotos secundarias 2-7 const fotoChecks = base ? Array.from({ length: 6 }, (_, i) => checkImg(`${base}_${i + 2}.${ext}`)) : []; // Video const videoCheck = ["mp4", "mov"].reduce((acc, e) => { return acc.then(found => found || checkVideo(`/assets/productos/${p.id}_video.${e}`)); }, Promise.resolve(null)); Promise.all([Promise.all(fotoChecks), videoCheck]).then(([fotosExtra, video]) => { const fotos = [{ type: "img", url: p.foto }, ...fotosExtra.filter(Boolean)]; setItems(video ? [...fotos, video] : fotos); }); }, [p?.id]); return items; } /* ===== DESCRIPCIÓN DINÁMICA DE VENTA ===== Genera un texto de venta único por producto combinando modelo, patrón, categoría, material y precio. */ function generarDesc(p, patron) { const cat = (window.CAT_LABELS && window.CAT_LABELS[p.categoria]) || p.categoria || ""; const nombre = p.nombre || ""; const mat = p.material || "Plástico Gold"; const precio = p.precio ? `$${p.precio.toLocaleString("es-CO")} COP` : ""; // Textos por patrón const patronTexts = { "Precolombino especial": "El patrón Precolombino Especial es el más elaborado del taller — requiere doble tensión y el doble de tiempo que un tejido estándar. Cada rombo es producto de un conteo exacto hilo a hilo.", "Precolombino": "El patrón Precolombino es nuestra firma más reconocida. La geometría en rombos se teje contando hilo a hilo, sin plantilla — solo la memoria de las manos del artesano.", "Lineal con refuerzo": "El tejido Lineal con refuerzo intercala bandas de doble hilo para darle mayor resistencia y un acabado visual limpio que no pasa desapercibido.", "Lineal": "El patrón Lineal es el más puro del catálogo — líneas paralelas tensadas con precisión milimétrica que crean un efecto óptico de movimiento.", "Diamante": "El patrón Diamante es geometría en estado puro. Cada figura requiere cambiar la dirección del hilo en ángulos exactos — un trabajo que solo sale bien a mano.", "Pirámide": "El patrón Pirámide crea volumen visual con una progresión de líneas que se estrechan hacia el centro. Cada pieza es diferente dependiendo del ángulo de lectura.", "Espiga": "El tejido Espiga toma su nombre del grano de trigo — líneas diagonales que se encuentran en un eje central y crean un efecto de movimiento continuo.", "Rombo": "El patrón Rombo es una variación geométrica que enmarca el asiento con figuras de cuatro puntas. Cada vértice tiene que cerrarse perfectamente.", "Cruz": "El patrón Cruz alterna bloques horizontales y verticales que se cruzan en puntos exactos. La dificultad está en mantener la tensión pareja en los dos ejes.", "Square": "El patrón Square crea una cuadrícula perfecta con cruces de hilo que definen cada celda. Limpio, moderno, preciso.", "Tapizado": "El tapizado es la técnica más densa del taller — el hilo cubre toda la superficie sin dejar espacio entre pasadas, creando un asiento firme y cálido al tacto.", }; // Textos por categoría const catTexts = { "mecedoras": "La mecedora es la pieza con más historia en el taller — el movimiento de vaivén exige que la estructura y el tejido trabajen juntos sin ceder.", "sofas": "El sofá es la pieza más exigente del catálogo en términos de tensión — el tejido debe soportar el peso distribuido de más de una persona sin deformarse.", "sillas-bar": "La silla de bar exige que el tejido soporte el peso concentrado en un área menor — la tensión de cada hilo es crítica para que dure décadas.", "comedores": "La silla de comedor convive con el uso diario más intenso — lavados, roces, peso constante. El tejido en Plástico Gold está hecho exactamente para eso.", "mesas": "La mesa combina estructura de madera curada con acabados tejidos que le dan carácter propio. Cada una se produce a pedido según las medidas confirmadas.", "percheros": "El perchero lleva el tejido a una función distinta — no es un asiento sino una estructura vertical donde el patrón se lee de otra manera.", "sillas": "La silla de sala equilibra comodidad y carácter visual. El tejido define el nivel de firmeza del asiento — más cerrado es más firme.", "combos": "El combo está pensado para quienes quieren coherencia visual en un espacio completo — mismo patrón, mismo color, producido en el mismo ciclo de tejido.", }; // Textos por modelo (los más icónicos) const modeloTexts = { "Pavo Real": "El Pavo Real es el modelo que le dio identidad al taller — el respaldo en abanico es su firma, copiada muchas veces pero nunca igualada en tensión y acabado.", "Pavo Real Plus": "La versión Plus del Pavo Real amplía el respaldo y agrega brazos tejidos — más presencia, mismo principio.", "Colibrí": "El Colibrí es la pieza más ligera del catálogo — estructura espigada y tejido liviano que la hacen perfecta para terrazas y espacios al aire libre.", "Sirena": "La Sirena tiene la forma más orgánica del catálogo — asiento profundo y respaldo curvado que envuelve al que se sienta.", "Acapulco": "El Acapulco es una relectura del clásico mexicano con tejido más cerrado y estructura de hierro forjado que le da más décadas de vida.", "Diamante": "El modelo Diamante lleva su patrón en el nombre — la geometría del tejido define la identidad de cada pieza.", "España": "La España es la silla de bar más clásica del catálogo — patas esbeltas, asiento tenso, sin adornos.", "Madrid": "El Madrid tiene el respaldo más generoso de las sillas de bar — pensado para espacios donde el cliente se sienta a quedarse.", "Tradicional": "La Tradicional es el punto de origen — la mecedora que le dio forma al taller antes de que existieran los demás modelos.", }; const patronDesc = patron && patronTexts[patron] ? patronTexts[patron] : ""; const catDesc = catTexts[p.categoria] || ""; const modeloDesc = modeloTexts[nombre] || ""; // Construir descripción compuesta const partes = []; if (modeloDesc) partes.push(modeloDesc); else if (catDesc) partes.push(catDesc); if (patronDesc && patronDesc !== modeloDesc) partes.push(patronDesc); partes.push(`Tejida en ${mat}. Tu pieza se empieza a producir cuando la pides — entrega en ~15 días directamente desde el taller en Bogotá.`); return partes.join(" "); } function Ficha({ productId, setPage, openFicha, openModelo, goCatalog }) { const wl = useWishlist(); const pid = (productId || "").toString().toUpperCase(); const p = CATALOGO.find((x) => (x.id || "").toUpperCase() === pid); const [img, setImg] = useState(0); const [lightbox, setLightbox] = useState(false); const media = useMedia(p); const current = media[img] || null; useEffect(() => { window.scrollTo({ top: 0, behavior: "instant" }); setImg(0); }, [productId]); // Navegación teclado en lightbox useEffect(() => { if (!lightbox) return; const onKey = (e) => { if (e.key === "Escape") setLightbox(false); if (e.key === "ArrowRight") setImg(i => Math.min(i + 1, media.length - 1)); if (e.key === "ArrowLeft") setImg(i => Math.max(i - 1, 0)); }; window.addEventListener("keydown", onKey); document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; }; }, [lightbox, media.length]); // ── Schema markup (JSON-LD) ──────────────────────────────────────────────── // Inyecta datos estructurados de producto para Google Rich Results. // Se actualiza cada vez que cambia el producto activo. useEffect(() => { if (!p) return; const siteUrl = 'https://artedavas.com'; const canonicalUrl = p.catSlug && p.slug ? `${siteUrl}/${p.catSlug}/${p.slug}` : `${siteUrl}/producto/${(p.id || '').toLowerCase()}`; const imageUrl = p.foto ? (p.foto.startsWith('http') ? p.foto : `${siteUrl}${p.foto}`) : `${siteUrl}/assets/og-default.jpg`; const nombreCompleto = p.nombre + (p.variante ? ' — ' + p.variante : ''); const precioNum = p.precio || 0; const schema = { "@context": "https://schema.org", "@type": "Product", "name": nombreCompleto, "description": `${nombreCompleto} tejida a mano${p.material ? ' en ' + p.material : ''}. Hecha a pedido en Colombia. Entrega ~15 días desde el taller en Bogotá.`, "image": [imageUrl], "sku": p.id, "brand": { "@type": "Brand", "name": "DAVA'S Arte & Diseño" }, "category": (window.CAT_LABELS && window.CAT_LABELS[p.categoria]) || p.categoria || '', "offers": { "@type": "Offer", "url": canonicalUrl, "priceCurrency": "COP", "price": precioNum, "priceValidUntil": new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString().split('T')[0], "availability": "https://schema.org/InStock", "itemCondition": "https://schema.org/NewCondition", "seller": { "@type": "Organization", "name": "DAVA'S Arte & Diseño", "url": siteUrl } }, "manufacturer": { "@type": "Organization", "name": "DAVA'S Arte & Diseño", "url": siteUrl } }; const existing = document.getElementById('schema-product'); if (existing) existing.remove(); const script = document.createElement('script'); script.id = 'schema-product'; script.type = 'application/ld+json'; script.textContent = JSON.stringify(schema); document.head.appendChild(script); return () => { const s = document.getElementById('schema-product'); if (s) s.remove(); }; }, [p?.id]); if (!p) { return (
Producto no encontrado

); } const patron = detectarPatron(p.variante); const isOn = wl.has(p.id); const others = CATALOGO.filter((x) => x.nombre === p.nombre && x.id !== p.id && x.foto).slice(0, 4); const sameFamily = CATALOGO.filter((x) => x.categoria === p.categoria && x.id !== p.id && x.foto).slice(0, 4); const msg = `Hola DAVA'S, me interesa "${p.nombre}${p.variante ? " — " + p.variante : ""}" (${p.id}) por $${p.precio.toLocaleString("es-CO")}. ¿Confirman disponibilidad?`; const shareUrl = p.catSlug && p.slug ? `artedavas.com/${p.catSlug}/${p.slug}` : `artedavas.com/producto/${p.id.toLowerCase()}`; return ( <>
Catálogo {" · "} {CAT_LABELS[p.categoria]} {" · "} {others.length ? openModelo(p.nombre)} style={{ cursor: "pointer", textDecoration: "underline" }}>{p.nombre} : {p.nombre}}
{/* Item principal — imagen o video */}
current?.type === "img" && media.length && setLightbox(true)} style={{ cursor: current?.type === "img" ? "zoom-in" : "default" }}> {current?.type === "video" ? (
{/* Thumbnails */} {media.length > 1 && (
{media.map((item, i) => ( ))}
)}
{p.id} · {CAT_LABELS[p.categoria]} {patron ? · {patron} : null}

{p.nombre}

{p.variante ?
{p.variante}
: null}
$ {p.precio.toLocaleString("es-CO")}
Producción a pedido · ~15 días
Material tejido
{p.material || "—"}
Patrón
{patron || "—"}
Familia
{CAT_LABELS[p.categoria]}
Código
{p.id}

{generarDesc(p, patron)}

{ window.dataLayer = window.dataLayer || []; window.dataLayer.push({ ecommerce: null }); window.dataLayer.push({ event: "generate_lead", lead_source: "whatsapp_ficha", ecommerce: { currency: "COP", value: p.precio, items: [{ item_id: p.id, item_name: p.nombre, item_variant: p.variante || "", item_category: CAT_LABELS[p.categoria] || p.categoria, price: p.precio, }], }, }); }}> Pedir por WhatsApp
URL compartible: {shareUrl}
{/* LIGHTBOX — solo para fotos */} {lightbox && current?.type === "img" && (
setLightbox(false)} style={{ position: "fixed", inset: 0, background: "rgba(18,10,1,.95)", zIndex: 9000, display: "flex", alignItems: "center", justifyContent: "center" }}> {img > 0 && ( )} {`${p.nombre} e.stopPropagation()} style={{ maxWidth: "90vw", maxHeight: "90vh", objectFit: "contain", borderRadius: 8 }} /> {img < media.length - 1 && ( )}
{img + 1} / {media.length}
{media.length > 1 && (
{media.map((item, i) => ( ))}
)}
)} {others.length > 0 ? (
Otras versiones del modelo

Más {p.nombre}.

Mismo nombre, distinto dibujo. Toca cualquiera para ver su ficha, o entra al modelo completo.

{others.filter(o => o.foto).map((o) => ( ))}
) : (
También en {CAT_LABELS[p.categoria]}

Otras piezas de la familia.

Piezas distintas dentro de la misma categoría.

{sameFamily.filter(o => o.foto).map((o) => ( ))}
)} ); } window.Ficha = Ficha;