/* 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 (
{generarDesc(p, patron)}—
) : (
{p.nombre}
{p.variante ?
Mismo nombre, distinto dibujo. Toca cualquiera para ver su ficha, o entra al modelo completo.
Piezas distintas dentro de la misma categoría.