1// galeria/galeria.js 2// Objetivo actual: aislar todas las imágenes antiguas de images/index.json en el baúl "Histórico". 3// Preparado para futuro: nuevas colecciones por carpeta dentro de /images. 4 5document.addEventListener("DOMContentLoaded", iniciarGaleria); 6 7const RUTA_INDEX = "images/index.json"; 8const RUTA_IMAGES = "images"; 9const RUTA_COLECCIONES = "images/colecciones.json"; 10const EXTENSIONES_IMAGEN = /\.(jpe?g|png|gif|webp|bmp|svg)$/i; 11 12async function iniciarGaleria() { 13 const contenedor = document.getElementById("galeria"); 14 15 if (!contenedor) { 16 console.error('No se encontró el contenedor con id="galeria"'); 17 return; 18 } 19 20 contenedor.innerHTML = ` 21 <div class="estado-galeria"> 22 <span>ð¨ Cargando recuerdos...</span> 23 </div> 24 `; 25 26 try { 27 const [historico, configuracion] = await Promise.all([ 28 cargarJson(RUTA_INDEX), 29 cargarJsonOpcional(RUTA_COLECCIONES) 30 ]); 31 32 const colecciones = await normalizarColecciones({ 33 historico, 34 colecciones: configuracion.colecciones || [] 35 }); 36 37 if (!colecciones.length) { 38 mostrarMensaje(contenedor, "No se encontraron imágenes registradas en el Ãndice."); 39 return; 40 } 41 42 renderizarColecciones(contenedor, colecciones); 43 } catch (error) { 44 console.error("Error cargando la galerÃa:", error); 45 mostrarMensaje(contenedor, "No se pudo cargar la galerÃa. Revisa que exista images/index.json."); 46 } 47} 48 49async function cargarJson(ruta) { 50 const respuesta = await fetch(ruta, { cache: "no-cache" }); 51 52 if (!respuesta.ok) { 53 throw new Error(`No se pudo cargar ${ruta}`); 54 } 55 56 return respuesta.json(); 57} 58 59async function cargarJsonOpcional(ruta) { 60 const respuesta = await fetch(ruta, { cache: "no-cache" }); 61 62 // Si todavÃa no hay colecciones, la galerÃa sigue mostrando Histórico. 63 if (respuesta.status === 404) { 64 return { colecciones: [] }; 65 } 66 67 if (!respuesta.ok) { 68 throw new Error(`No se pudo cargar ${ruta}`); 69 } 70 71 return respuesta.json(); 72} 73 74async function normalizarColecciones(datos) { 75 // Compatibilidad con el formato antiguo: 76 // ["1.jpg", "2.jpg", ...] 77 if (Array.isArray(datos)) { 78 return [crearColeccionHistorica(datos)]; 79 } 80 81 if (!datos || typeof datos !== "object") { 82 return []; 83 } 84 85 const colecciones = []; 86 const historico = datos.historico || datos.historical || datos.imagenes || datos.images; 87 88 // Mantiene el Baúl de recuerdos: Histórico. 89 if (Array.isArray(historico) && historico.length) { 90 colecciones.push(crearColeccionHistorica(historico)); 91 } 92 93 const nuevasColecciones = datos.colecciones || datos.collections || []; 94 95 for (let indice = 0; indice < nuevasColecciones.length; indice++) { 96 const item = nuevasColecciones[indice]; 97 98 if (!item || typeof item !== "object") { 99 continue; 100 } 101 102 const nombre = item.nombre || item.name || item.titulo || `Colección ${indice + 1}`; 103 const carpeta = item.carpeta || item.folder || slugify(nombre); 104 const rutaBase = normalizarRutaBase(carpeta); 105 106 // Permite dos formas: 107 // 1. imágenes escritas directamente en el Ãndice general. 108 // 2. imágenes leÃdas desde images/NOMBRE-CARPETA/index.json. 109 let imagenes = item.imagenes || item.images || item.fotos || item.files || []; 110 111 if (!Array.isArray(imagenes) || !imagenes.length) { 112 const archivoIndice = item.indice || item.index || "index.json"; 113 114 try { 115 const datosIndice = await cargarJson( 116 construirRutaImagen(rutaBase, archivoIndice) 117 ); 118 119 imagenes = Array.isArray(datosIndice) 120 ? datosIndice 121 : ( 122 datosIndice.imagenes || 123 datosIndice.images || 124 datosIndice.fotos || 125 datosIndice.files || 126 [] 127 ); 128 } catch (error) { 129 console.warn(`No se pudo cargar el Ãndice de ${nombre}.`, error); 130 continue; 131 } 132 } 133 134 const archivos = limpiarListaImagenes(imagenes); 135 136 if (!archivos.length) { 137 continue; 138 } 139 140 colecciones.push({ 141 id: item.id || slugify(nombre), 142 nombre, 143 kicker: "Nueva colección", 144 descripcion: item.descripcion || item.description || "Fotos organizadas por colección.", 145 rutaBase, 146 archivos, 147 portada: item.portada || item.cover || null, 148 esHistorico: false 149 }); 150 } 151 152 return colecciones; 153} 154 155function crearColeccionHistorica(archivos) { 156 return { 157 id: "historico", 158 nombre: "Baúl de recuerdos: Histórico", 159 kicker: "Material antiguo conservado", 160 descripcion: "Aquà se guardan los momentos más icónicos de nuestros niños que perduran en el tiempo.", 161 rutaBase: RUTA_IMAGES, 162 archivos: limpiarListaImagenes(archivos), 163 portada: null, 164 esHistorico: true 165 }; 166} 167 168function limpiarListaImagenes(archivos) { 169 return Array.from(new Set( 170 archivos 171 .map(archivo => String(archivo || "").trim()) 172 .filter(archivo => archivo && EXTENSIONES_IMAGEN.test(archivo)) 173 .map(archivo => archivo.replace(/^\/+/, "")) 174 )).sort(ordenNatural); 175} 176 177function renderizarColecciones(contenedor, colecciones) { 178 const totalFotos = colecciones.reduce((total, coleccion) => total + coleccion.archivos.length, 0); 179 180 contenedor.innerHTML = ` 181 <div class="resumen-galeria mb-4">
182 <span class="pildora-resumen">ð ${colecciones.length} colección${colecciones.length === 1 ? "" : "es"}</span> 183 <span class="pildora-resumen">ð¼ï¸ ${totalFotos} foto${totalFotos === 1 ? "" : "s"}</span> 184 </div> 185 <div class="colecciones-grid" id="coleccionesGrid"></div> 186 `; 187 188 const grid = contenedor.querySelector("#coleccionesGrid"); 189 190 colecciones.forEach(coleccion => { 191 grid.appendChild(crearTarjetaColeccion(coleccion)); 192 }); 193 194 contenedor.addEventListener("click", function (evento) { 195 const botonColeccion = evento.target.closest("[data-abrir-coleccion]"); 196 const botonFoto = evento.target.closest("[data-ver-foto]"); 197 198 if (botonColeccion) { 199 alternarColeccion(botonColeccion, colecciones); 200 } 201 202 if (botonFoto) { 203 abrirModalFoto(botonFoto); 204 } 205 }); 206} 207 208function crearTarjetaColeccion(coleccion) { 209 const tarjeta = document.createElement("article"); 210 tarjeta.className = `coleccion-card${coleccion.esHistorico ? " es-historico" : ""}`; 211 212 const panelId = `panel-${coleccion.id}`; 213 const cantidad = coleccion.archivos.length; 214 const previews = obtenerPreviews(coleccion); 215 216 tarjeta.innerHTML = ` 217 <div class="coleccion-superior"> 218 <div class="baul-preview" aria-hidden="true"> 219 ${previews.map(src => `<img class="mini-foto" src="${escapeAttr(src)}" alt="" loading="lazy">`).join("")} 220 <span class="baul-caja"></span> 221 </div> 222 223 <div> 224 <p class="coleccion-kicker">${escapeHtml(coleccion.kicker)}</p> 225 <h2 class="coleccion-titulo">${escapeHtml(coleccion.nombre)}</h2> 226 <p class="coleccion-descripcion">${escapeHtml(coleccion.descripcion)}</p> 227 228 <div class="coleccion-meta"> 229 <span class="chip-meta">${cantidad} foto${cantidad === 1 ? "" : "s"}</span> 230 <span class="chip-meta">${coleccion.esHistorico ? "Histórico" : "Actualizado"}</span> 231 </div> 232 233 <button class="btn btn-baul" type="button" data-abrir-coleccion="${escapeAttr(coleccion.id)}" aria-expanded="false" aria-controls="${escapeAttr(panelId)}"> 234 ${coleccion.esHistorico ? "Abrir baúl histórico" : "Ver colección"} 235 </button> 236 </div> 237 </div> 238 239 <section class="panel-coleccion" id="${escapeAttr(panelId)}" hidden> 240 <div class="panel-cabecera"> 241 <h3>${escapeHtml(coleccion.esHistorico ? "Fotos guardadas en histórico" : coleccion.nombre)}</h3> 242 <!--<span class="chip-meta">Carga diferida para no hacer pesada la página</span>--> 243 </div> 244 <div class="grid-fotos" data-grid-coleccion="${escapeAttr(coleccion.id)}"></div> 245 </section> 246 `; 247 248 return tarjeta; 249} 250 251function obtenerPreviews(coleccion) { 252 const archivos = coleccion.portada 253 ? [coleccion.portada, ...coleccion.archivos.filter(archivo => archivo !== coleccion.portada)] 254 : coleccion.archivos; 255 256 return archivos.slice(0, 3).map(archivo => construirRutaImagen(coleccion.rutaBase, archivo)); 257} 258 259function alternarColeccion(boton, colecciones) { 260 const id = boton.dataset.abrirColeccion; 261 const coleccion = colecciones.find(item => item.id === id); 262 263 if (!coleccion) { 264 return; 265 } 266 267 const panel = document.getElementById(`panel-${id}`); 268 const grid = panel 269 ? panel.querySelector(`[data-grid-coleccion="${id}"]`) 270 : null; 271 272 if (!panel || !grid) { 273 return; 274 } 275 276 // Determina si la colección seleccionada se abrirá o cerrará. 277 const seAbrira = panel.hidden; 278 279 // Cierra todas las demás colecciones antes de abrir la seleccionada. 280 colecciones.forEach(otraColeccion => { 281 if (otraColeccion.id === id) { 282 return; 283 } 284 285 const otroPanel = document.getElementById( 286 `panel-${otraColeccion.id}` 287 ); 288 289 const otroBoton = Array.from( 290 document.querySelectorAll("[data-abrir-coleccion]") 291 ).find( 292 elemento => elemento.dataset.abrirColeccion === otraColeccion.id 293 ); 294 295 if (otroPanel) { 296 otroPanel.hidden = true; 297 } 298 299 if (otroBoton) { 300 otroBoton.setAttribute("aria-expanded", "false"); 301 302 otroBoton.textContent = otraColeccion.esHistorico 303 ? "Abrir baúl histórico" 304 : "Ver colección"; 305 } 306 }); 307
308 // Abre o cierra la colección seleccionada. 309 panel.hidden = !seAbrira; 310 boton.setAttribute("aria-expanded", String(seAbrira)); 311 312 boton.textContent = seAbrira 313 ? "Cerrar colección" 314 : (coleccion.esHistorico 315 ? "Abrir baúl histórico" 316 : "Ver colección"); 317 318 // Solo renderiza las imágenes una vez. 319 if (seAbrira && !grid.dataset.renderizado) { 320 renderizarFotos(grid, coleccion); 321 grid.dataset.renderizado = "true"; 322 } 323} 324 325function renderizarFotos(grid, coleccion) { 326 const fragmento = document.createDocumentFragment(); 327 328 coleccion.archivos.forEach((archivo, indice) => { 329 const figura = document.createElement("figure"); 330 figura.className = "foto-recuerdo"; 331 figura.style.setProperty("--rotacion", `${obtenerRotacion(indice)}deg`); 332 333 const src = construirRutaImagen(coleccion.rutaBase, archivo); 334 const alt = `${coleccion.nombre} - ${archivo}`; 335 336 figura.innerHTML = ` 337 <button class="foto-boton" type="button" data-ver-foto data-src="${escapeAttr(src)}" data-alt="${escapeAttr(alt)}"> 338 <img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}" loading="lazy" decoding="async" onerror="this.closest('figure').classList.add('is-missing')"> 339 </button> 340 `; 341 342 fragmento.appendChild(figura); 343 }); 344 345 grid.appendChild(fragmento); 346} 347 348function abrirModalFoto(boton) { 349 const imagen = document.getElementById("fotoModalImagen"); 350 const titulo = document.getElementById("fotoModalTitulo"); 351 352 if (!imagen || !titulo || typeof bootstrap === "undefined") { 353 return; 354 } 355 356 imagen.src = boton.dataset.src; 357 imagen.alt = boton.dataset.alt; 358 titulo.textContent = boton.dataset.alt; 359 360 const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById("fotoModal")); 361 modal.show(); 362} 363 364function construirRutaImagen(rutaBase, archivo) { 365 const base = String(rutaBase || RUTA_IMAGES) 366 .replace(/^\/+|\/+$/g, "") 367 .split("/") 368 .map(encodeURIComponent) 369 .join("/"); 370 371 const nombreArchivo = String(archivo || "") 372 .replace(/^\/+/, "") 373 .split("/") 374 .map(encodeURIComponent) 375 .join("/"); 376 377 return `${base}/${nombreArchivo}`; 378} 379 380function normalizarRutaBase(carpeta) { 381 const limpia = String(carpeta || "") 382 .trim() 383 .replace(/^\/+|\/+$/g, ""); 384 385 if (!limpia) { 386 return RUTA_IMAGES; 387 } 388 389 return limpia.startsWith(`${RUTA_IMAGES}/`) ? limpia : `${RUTA_IMAGES}/${limpia}`; 390} 391 392function mostrarMensaje(contenedor, mensaje) { 393 contenedor.innerHTML = ` 394 <div class="estado-galeria"> 395 <span>𧩠${escapeHtml(mensaje)}</span> 396 </div> 397 `; 398} 399 400function obtenerRotacion(indice) { 401 const rotaciones = [-2, 1.5, -1, 2, -.5, 1]; 402 return rotaciones[indice % rotaciones.length]; 403} 404 405function ordenNatural(a, b) { 406 return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); 407} 408 409function slugify(texto) { 410 return String(texto || "coleccion") 411 .normalize("NFD") 412 .replace(/[\u0300-\u036f]/g, "") 413 .toLowerCase() 414 .replace(/[^a-z0-9]+/g, "-") 415 .replace(/(^-|-$)/g, "") || "coleccion"; 416} 417 418function escapeHtml(valor) { 419 return String(valor).replace(/[&<>"]/g, caracter => ({ 420 "&": "&", 421 "<": "<", 422 ">": ">", 423 '"': """ 424 }[caracter])); 425} 426 427function escapeAttr(valor) { 428 return escapeHtml(valor).replace(/'/g, "'"); 429}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.