PageSourceSearch

https://ieinjp.edu.pe/galeria/galeria.js

js ieinjp.edu.pe collected 2026-10-04 01:44:31 UTC 12,690 bytes, 429 lines download raw bytes

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    "&": "&amp;",
421    "<": "&lt;",
422    ">": "&gt;",
423    '"': "&quot;"
424  }[caracter]));
425}
426
427function escapeAttr(valor) {
428  return escapeHtml(valor).replace(/'/g, "&#39;");
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.