1// scripts.js 2document.addEventListener("DOMContentLoaded", function() { 3 4 // --- SECCIÃN 1: Carga de Componentes (Header/Footer) --- 5 6 /** 7 * Carga un componente HTML (como el header o footer) desde una URL 8 * y lo inserta en un elemento contenedor en la página principal. 9 */ 10 const loadComponent = (url, placeholderId) => { 11 fetch(url) 12 .then(response => { 13 if (!response.ok) { 14 throw new Error(`Error al cargar el componente: ${response.statusText}`); 15 } 16 return response.text(); 17 }) 18 .then(data => { 19 const placeholder = document.getElementById(placeholderId); 20 if (placeholder) { 21 placeholder.innerHTML = data; 22 } 23 24 // Configurar interactividad post-carga 25 if (placeholderId === 'header-placeholder') { 26 setupHeader(); 27 } 28 if (placeholderId === 'footer-placeholder') { 29 setupFooter(); 30 } 31 }) 32 .catch(error => console.error(`Error al cargar ${url}:`, error)); 33 }; 34 35 function setupHeader() { 36 const mobileMenuButton = document.getElementById('mobile-menu-button'); 37 const mobileMenu = document.getElementById('mobile-menu'); 38 if (mobileMenuButton && mobileMenu) { 39 mobileMenuButton.addEventListener('click', () => { 40 mobileMenu.classList.toggle('hidden'); 41 }); 42 } 43 44 const currentPage = window.location.pathname.split("/").pop() || "index.html"; 45 const navLinksDesktop = document.querySelectorAll('header .hidden.lg\\:flex nav a'); 46 const navLinksMobile = document.querySelectorAll('#mobile-menu a'); 47 48 const highlightLink = (links) => { 49 links.forEach(link => { 50 const linkPage = (link.getAttribute('href') || "").split("/").pop(); 51 if (linkPage === currentPage) { 52 link.classList.add('text-teal-600', 'font-bold'); 53 if (link.closest('#mobile-menu')) { 54 link.classList.add('bg-teal-50'); 55 } 56 } 57 }); 58 }; 59 60 highlightLink(navLinksDesktop); 61 highlightLink(navLinksMobile); 62 } 63 64 function setupFooter() { 65 const yearEl = document.getElementById('year'); 66 if (yearEl) { 67 yearEl.textContent = new Date().getFullYear(); 68 } 69 } 70 71 // Iniciar carga de componentes 72 // Siempre usar rutas absolutas desde la raÃz 73 const basePath = '/'; 74 75 loadComponent(basePath + 'header.html', 'header-placeholder'); 76 loadComponent(basePath + 'footer.html', 'footer-placeholder'); 77 78 79 // --- SECCIÃN 2: Lógica de Noticias (JSON) --- 80 81 /** 82 * Carga noticias desde un archivo JSON. 83 * Si estamos en noticias.html, maneja filtros y carga dinámica (paginación). 84 * Si estamos en index.html, carga el carrusel. 85 */ 86 function loadNews() { 87 const newsSectionIndex = document.getElementById('noticias-carrusel'); 88 const newsGridPage = document.getElementById('news-grid'); 89 90 // Si no estamos en la página de noticias ni en la home, no hacemos nada 91 if (!newsSectionIndex && !newsGridPage) return; 92 93 fetch(basePath + 'noticias.json') 94 .then(response => { 95 if (!response.ok) throw new Error('No se pudo cargar noticias.json'); 96 return response.json(); 97 }) 98 .then(noticias => { 99 if (noticias.length === 0) return; 100 101 // Lógica para la Home (Carrusel) 102 if (newsSectionIndex) { 103 renderHomeCarousel(noticias, newsSectionIndex); 104 } 105 106 // Lógica para la Página de Noticias (Grid + Filtros) 107 if (newsGridPage) { 108 renderNewsPage(noticias, newsGridPage); 109 } 110 }) 111 .catch(error => console.error('Error cargando noticias:', error)); 112 } 113 114 /**
115 * Renderiza el carrusel en la página principal 116 */ 117 function renderHomeCarousel(noticias, section) { 118 const slider = document.getElementById('news-slider'); 119 if (!slider) return; 120 121 section.classList.remove('hidden'); 122 123 // Mostrar las 5 más recientes 124 noticias.slice(0, 5).forEach(noticia => { 125 const slide = document.createElement('div'); 126 slide.className = 'news-slide flex-shrink-0 w-full flex flex-col md:flex-row'; 127 slide.innerHTML = ` 128 <div class="md:w-1/2 h-64 md:h-[450px] overflow-hidden"> 129 <img src="${noticia.imagen}" alt="${noticia.titulo}" class="h-full w-full object-cover transition hover:scale-105 duration-700"> 130 </div> 131 <div class="md:w-1/2 p-8 md:p-12 flex flex-col justify-center bg-white"> 132 <span class="text-teal-600 font-bold uppercase tracking-wider text-sm mb-2">${noticia.categoria || 'Actualidad'}</span> 133 <h3 class="text-2xl md:text-3xl font-bold mb-4 text-gray-800 line-clamp-3">${noticia.titulo}</h3> 134 <p class="text-gray-600 mb-6 line-clamp-3 text-lg">${noticia.descripcion || ''}</p> 135 <a href="${noticia.enlace}" class="inline-block bg-teal-600 text-white font-bold py-3 px-6 rounded-lg hover:bg-teal-700 transition self-start shadow-md hover:shadow-lg">${noticia.texto_enlace || 'Leer más'}</a> 136 </div> 137 `; 138 slider.appendChild(slide); 139 }); 140 141 // 3. Añadir tarjeta "Ver Todas" al final del carrusel 142 const allNewsSlide = document.createElement('div'); 143 allNewsSlide.className = 'news-slide flex-shrink-0 w-full flex flex-col md:flex-row bg-teal-800 text-white'; 144 allNewsSlide.innerHTML = ` 145 <div class="md:w-1/2 h-64 md:h-[450px] flex items-center justify-center bg-teal-900"> 146 <i class="fas fa-newspaper text-9xl text-teal-700 opacity-50"></i> 147 </div> 148 <div class="md:w-1/2 p-8 md:p-12 flex flex-col justify-center"> 149 <span class="text-teal-300 font-bold uppercase tracking-wider text-sm mb-2">Archivo de Noticias</span> 150 <h3 class="text-3xl md:text-4xl font-bold mb-6">¿Te perdiste alguna noticia?</h3> 151 <p class="text-teal-100 mb-8 text-lg">Consulta nuestro archivo completo de noticias, comunicados y eventos pasados.</p> 152 <a href="/noticias/news.html" class="inline-block bg-white text-teal-800 font-bold py-3 px-8 rounded-lg hover:bg-teal-50 transition self-start shadow-md hover:shadow-lg"> 153 Ver todas las noticias <i class="fas fa-arrow-right ml-2"></i> 154 </a> 155 </div> 156 `; 157 slider.appendChild(allNewsSlide); 158 159 setupNewsCarousel(); 160 } 161 162 function setupNewsCarousel() { 163 const slider = document.getElementById('news-slider'); 164 const prev = document.getElementById('news-prev'); 165 const next = document.getElementById('news-next'); 166 let currentSlide = 0; 167 168 // Esperar un momento para asegurar que el DOM se actualizó 169 setTimeout(() => { 170 const slides = document.querySelectorAll('.news-slide'); 171 const totalSlides = slides.length; 172 173 if (totalSlides === 0) return; 174 175 const moveSlider = () => { 176 slider.style.transform = `translateX(-${currentSlide * 100}%)`; 177 }; 178 179 if (next) { 180 next.addEventListener('click', () => { 181 currentSlide = (currentSlide + 1) % totalSlides; 182 moveSlider(); 183 }); 184 } 185 186 if (prev) { 187 prev.addEventListener('click', () => { 188 currentSlide = (currentSlide - 1 + totalSlides) % totalSlides; 189 moveSlider(); 190 }); 191 } 192 193 // Auto-play opcional 194 setInterval(() => { 195 currentSlide = (currentSlide + 1) % totalSlides; 196 moveSlider(); 197 }, 7000); 198 }, 100); 199 } 200 201 /**
202 * Renderiza la página completa de noticias con filtros y paginación 203 */ 204 function renderNewsPage(allNews, gridContainer) { 205 const filtersContainer = document.getElementById('category-filters'); 206 const loadMoreContainer = document.getElementById('load-more-container'); 207 const loadMoreBtn = document.getElementById('load-more-btn'); 208 const noNewsMsg = document.getElementById('no-news-message'); 209 210 let currentFilter = 'all'; 211 let itemsToShow = 6; 212 let visibleItems = 0; 213 let filteredNews = [...allNews]; 214 215 // 1. Generar Filtros 216 const categories = [...new Set(allNews.map(n => n.categoria).filter(c => c))]; 217 categories.forEach(cat => { 218 const btn = document.createElement('button'); 219 btn.className = 'filter-btn border border-teal-600 text-teal-700 px-5 py-2 rounded-full hover:bg-teal-600 hover:text-white transition uppercase text-sm font-semibold tracking-wide'; 220 btn.textContent = cat; 221 btn.dataset.category = cat; 222 btn.addEventListener('click', (e) => filterNews(cat, e.target)); 223 filtersContainer.appendChild(btn); 224 }); 225 226 // 2. Renderizado Grid 227 const renderGrid = (append = false) => { 228 if (!append) { 229 gridContainer.innerHTML = ''; 230 visibleItems = 0; 231 } 232 233 const nextBatch = filteredNews.slice(visibleItems, visibleItems + itemsToShow); 234 visibleItems += nextBatch.length; 235 236 if (filteredNews.length === 0) { 237 noNewsMsg.classList.remove('hidden'); 238 loadMoreContainer.classList.add('hidden'); 239 return; 240 } else { 241 noNewsMsg.classList.add('hidden'); 242 } 243 244 nextBatch.forEach(noticia => { 245 const card = document.createElement('div'); 246 card.className = 'news-card bg-white rounded-xl overflow-hidden shadow-sm border border-gray-100 hover:shadow-xl transition flex flex-col h-full'; 247 card.innerHTML = ` 248 <div class="h-48 overflow-hidden relative group"> 249 <img src="${noticia.imagen}" alt="${noticia.titulo}" class="w-full h-full object-cover transition duration-500 group-hover:scale-110"> 250 <span class="absolute top-4 left-4 bg-teal-600 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wider shadow-md"> 251 ${noticia.categoria || 'General'} 252 </span> 253 </div> 254 <div class="p-6 flex flex-col flex-grow"> 255 <div class="text-xs text-gray-400 mb-2 flex items-center gap-2"> 256 <i class="far fa-calendar-alt"></i> ${noticia.fecha} 257 </div> 258 <h3 class="font-bold text-lg text-gray-800 mb-3 hover:text-teal-600 transition line-clamp-3 leading-tight"> 259 <a href="${noticia.enlace}">${noticia.titulo}</a> 260 </h3> 261 <p class="text-gray-600 text-sm mb-4 line-clamp-3 flex-grow">${noticia.descripcion || ''}</p> 262 <a href="${noticia.enlace}" class="text-teal-600 font-bold text-sm hover:underline mt-auto flex items-center gap-1 group-hover:gap-2 transition-all"> 263 Leer nota completa <i class="fas fa-arrow-right text-xs"></i> 264 </a> 265 </div> 266 `; 267 gridContainer.appendChild(card); 268 }); 269 270 if (visibleItems >= filteredNews.length) { 271 loadMoreContainer.classList.add('hidden'); 272 } else { 273 loadMoreContainer.classList.remove('hidden'); 274 } 275 }; 276 277 // 3. Filtrado 278 const filterNews = (category, btnClicked) => { 279 currentFilter = category; 280 281 const allButtons = document.querySelectorAll('.filter-btn'); 282 allButtons.forEach(btn => { 283 btn.classList.remove('active', 'bg-teal-600', 'text-white'); 284 btn.classList.add('text-teal-700', 'bg-transparent'); 285 }); 286 287 btnClicked.classList.add('active', 'bg-teal-600', 'text-white'); 288 btnClicked.classList.remove('text-teal-700', 'bg-transparent'); 289 290 if (category === 'all') { 291 filteredNews = [...allNews]; 292 } else { 293 filteredNews = allNews.filter(n => n.categoria === category); 294 } 295 296 renderGrid(false); 297 }; 298 299 if (loadMoreBtn) {
300 loadMoreBtn.addEventListener('click', () => renderGrid(true)); 301 } 302 303 const allBtn = document.querySelector('.filter-btn[data-category="all"]'); 304 if(allBtn) { 305 allBtn.addEventListener('click', (e) => filterNews('all', e.target)); 306 // Estilo activo por defecto 307 allBtn.classList.add('active', 'bg-teal-600', 'text-white'); 308 allBtn.classList.remove('text-teal-700', 'bg-transparent'); 309 } 310 311 renderGrid(); 312 } 313 314 // Ejecutar carga de noticias 315 loadNews(); 316});
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.