1// app.js 2 3// --- 1. MÃDULOS PRINCIPALES --- 4import { initRouter, navigateTo } from './router.js'; 5import * as ShortlistManager from './components/ShortlistManager.js'; 6import * as ComparatorManager from './components/ComparatorManager.js'; 7 8// --- 2. COMPONENTES DE LAYOUT --- 9import { renderHeader, setupHeaderEvents } from './components/Header.js'; 10import { renderFooter } from './components/Footer.js'; 11 12import { renderComparatorBar } from './components/ComparatorBar.js'; 13 14// --- 3. PÃGINAS --- 15import { renderHomepage } from './pages/Homepage.js'; 16import { renderStudiosOverview, renderPlatosPage, renderBacklotsPage } from './pages/StudiosOverviewPage.js'; 17import { renderPlatoDetail } from './pages/PlatoDetailPage.js'; 18import { renderBacklotDetail } from './pages/BacklotDetailPage.js'; 19import { renderEventsPage, renderWorkshopPage } from './pages/ServiciosAuxiliaresPage.js'; 20import { renderShortlistPage } from './pages/ShortlistPage.js'; 21import { renderContactPage } from './pages/ContactPage.js'; 22import { renderChangePasswordPage } from './pages/ChangePasswordPage.js'; 23import { renderForgotPasswordPage } from './pages/ForgotPasswordPage.js'; 24import { renderResetPasswordPage } from './pages/ResetPasswordPage.js'; 25import { renderProfessionalsDirectory } from './pages/HubProfessionalsPage.js'; 26import { renderProfessionalDetailPage } from './pages/ProfessionalDetailPage.js'; 27import { renderProfessionalsComparatorPage, renderProfessionalRegistrationForm, renderEditProfilePage } from './pages/ProfessionalFormsPage.js'; 28import { renderTestimonialsPage } from './pages/TestimonialsPage.js'; 29// 30// NOTA: NewsAgendaPage eliminado del router (Decisión D-002.11) 31 32import { renderWhyCVOverview, renderDataPage, renderSuccessStoriesPage, renderConnectivityPage, renderInvestorsContactPage, renderProductionGuidePage, renderFilmographyDetailPage, renderIncentivesPage } from './pages/WhyCVPage.js'; 33import { i18n } from './services/I18nService.js'; 34 35import { renderLocationsFinder } from './pages/LocationsPage.js'; 36import { renderLocationDetailPage } from './pages/LocationDetailPage.js'; 37import { renderLocationSubmissionPage, renderLocationEditPage, renderMyLocationsPage } from './pages/LocationFormsPage.js'; 38 39import { renderHubOverview, renderFilmOfficesDirectory } from './pages/HubOverviewPage.js'; 40 41import { renderCompaniesDirectory } from './pages/HubCompaniesDirectory.js'; 42import { renderCompanyDetailPage } from './pages/CompanyDetailPage.js'; 43import { renderCompanyRegistrationForm, renderCompanyEditPage } from './pages/CompanyFormsPage.js'; 44 45 46import { renderLoginPage } from './pages/LoginPage.js'; 47 48import { initDropzone } from './dropzone/dropzone_config.js'; 49 50import { 51 renderWaterTankPage // Nueva página del Tanque de Agua 52} from './pages/WaterTankPage.js'; 53 54 55import { renderFAQPage, renderTransparencyPage, renderLegalNoticePage, renderPrivacyPolicyPage } from './pages/LegalPages.js'; 56import { renderPressPage, renderPressReleaseDetailPage } from './pages/PressPage.js'; 57 58// --- 4. SERVICIOS --- 59import { initCookieConsent } from './components/CookieConsent.js'; 60import { authService } from './services/AuthService.js'; 61import { globalSettingsService } from './services/GlobalSettingsService.js'; 62import { seoService } from './services/SeoService.js'; 63import { contentService } from './services/ContentService.js'; 64 65import { renderJoinHubLanding } from './pages/JoinHubPage.js'; 66import { renderHubComparatorPage } from './pages/HubComparatorPage.js'; 67import { renderComingSoonPage } from './pages/ComingSoonPage.js'; 68import { AppConfig } from './config/AppConfig.js'; 69 70import { renderProductionBuildingsPage } from './pages/ProductionBuildingsPage.js'; 71import { renderFilmSchool } from './pages/FilmSchoolPage.js'; 72import { initImageOrientationAutoFit } from './services/ImageOrientationService.js'; 73import { renderDashboardPage } from './pages/DashboardPage.js'; 74 75const appContainer = document.getElementById('app'); 76 77// --- CONFIGURACIÃN DE INTERFAZ --- 78function setupTabs() { 79 const tabContainers = document.querySelectorAll('[data-tabs-container]');
80 tabContainers.forEach(container => { 81 const tabButtons = container.querySelectorAll('[role="tab"]'); 82 const tabPanels = container.querySelectorAll('[role="tabpanel"]'); 83 tabButtons.forEach(button => { 84 button.addEventListener('click', (event) => { 85 event.preventDefault(); 86 tabButtons.forEach(btn => { 87 btn.classList.remove('active'); 88 btn.setAttribute('aria-selected', 'false'); 89 }); 90 tabPanels.forEach(panel => { 91 panel.classList.add('hidden'); 92 }); 93 const targetPanelId = button.getAttribute('aria-controls'); 94 const targetPanel = document.getElementById(targetPanelId); 95 button.classList.add('active'); 96 button.setAttribute('aria-selected', 'true'); 97 if (targetPanel) { 98 targetPanel.classList.remove('hidden'); 99 } 100 }); 101 }); 102 }); 103} 104 105function setupCarousels() { 106 document.querySelectorAll('.carousel-container').forEach(container => { 107 const track = container.querySelector('.carousel-track'); 108 if (!track) return; 109 const slides = Array.from(track.children); 110 const nextButton = container.querySelector('.carousel-next'); 111 const prevButton = container.querySelector('.carousel-prev'); 112 113 if (slides.length === 0 || !nextButton || !prevButton) return; 114 115 let currentIndex = 0; 116 const slideWidth = slides[0].getBoundingClientRect().width || container.offsetWidth; 117 118 const goToSlide = (index) => { 119 // Usamos porcentajes para que sea 100% responsivo sin recalcular 120 track.style.transform = `translateX(-${index * 100}%)`; 121 currentIndex = index; 122 }; 123 nextButton.addEventListener('click', () => { 124 const newIndex = (currentIndex + 1) % slides.length; 125 goToSlide(newIndex); 126 }); 127 prevButton.addEventListener('click', () => { 128 const newIndex = (currentIndex - 1 + slides.length) % slides.length; 129 goToSlide(newIndex); 130 }); 131 }); 132} 133 134// Hacemos la función disponible para las páginas dinámicas 135window.setupCarousels = setupCarousels; 136 137function setupMultiStepForms() { 138 document.querySelectorAll('.multi-step-form').forEach(form => { 139 const steps = form.querySelectorAll('.form-step'); 140 const progressSteps = form.querySelectorAll('.progress-step'); 141 if (steps.length === 0) return; 142 let currentStep = 1; 143 const showStep = (stepNumber) => { 144 steps.forEach(step => { 145 step.classList.toggle('active', parseInt(step.dataset.step) === stepNumber); 146 step.classList.toggle('hidden', parseInt(step.dataset.step) !== stepNumber); 147 }); 148 if (progressSteps.length > 0) { 149 progressSteps.forEach(pStep => { 150 pStep.classList.toggle('active', parseInt(pStep.dataset.step) <= stepNumber); 151 }); 152 } 153 currentStep = stepNumber; 154 }; 155 form.addEventListener('click', (e) => { 156 const button = e.target.closest('button'); 157 if (!button || !button.dataset.action) return; 158 e.preventDefault(); 159 if (button.dataset.action === 'next-step') { 160 if (currentStep < steps.length) showStep(currentStep + 1); 161 } 162 if (button.dataset.action === 'prev-step') { 163 if (currentStep > 1) showStep(currentStep - 1); 164 } 165 }); 166 }); 167} 168 169function setupModals() { 170 document.querySelectorAll('[data-modal-target]').forEach(button => { 171 button.addEventListener('click', (e) => { 172 e.preventDefault(); 173 const modalId = button.dataset.modalTarget; 174 const modal = document.getElementById(modalId); 175 if (modal) { 176 modal.classList.remove('hidden'); 177 } 178 }); 179 }); 180 document.querySelectorAll('.modal-overlay').forEach(modal => { 181 modal.addEventListener('click', (event) => { 182 if (event.target === modal || event.target.closest('[data-modal-close]')) { 183 modal.classList.add('hidden'); 184 } 185 }); 186 }); 187} 188 189function updateShortlistCounter() { 190 const count = ShortlistManager.getShortlistCount(); 191 // Seleccionamos por clase para actualizar tanto escritorio como móvil 192 const counters = document.querySelectorAll('.js-shortlist-badge'); 193
194 counters.forEach(counter => { 195 counter.innerText = count; 196 // Nos aseguramos de que sea visible (por si acaso estaba oculto) 197 counter.classList.remove('hidden'); 198 }); 199} 200 201function ensureActionButtonsAreNonSubmit() { 202 const actionButtons = document.querySelectorAll('button[data-action]'); 203 actionButtons.forEach((button) => { 204 if (!button.getAttribute('type')) { 205 button.setAttribute('type', 'button'); 206 } 207 }); 208} 209 210function highlightShortlistAccessPoint() { 211 const shortlistLinks = document.querySelectorAll('a[href="/shortlist"]'); 212 213 shortlistLinks.forEach((link) => { 214 link.classList.add('ring-2', 'ring-cdl-gold', 'ring-offset-2', 'rounded-xl'); 215 216 setTimeout(() => { 217 link.classList.remove('ring-2', 'ring-cdl-gold', 'ring-offset-2', 'rounded-xl'); 218 }, 2400); 219 }); 220} 221 222function showShortlistFeedbackToast({ added, itemTitle } = {}) { 223 const t = (key) => i18n.t(key); 224 const existingToast = document.getElementById('shortlist-feedback-toast'); 225 if (existingToast) { 226 existingToast.remove(); 227 } 228 229 const toast = document.createElement('div'); 230 toast.id = 'shortlist-feedback-toast'; 231 toast.className = 'fixed top-24 right-4 z-[120] max-w-xs bg-white border border-slate-200 rounded-2xl shadow-2xl p-4 text-slate-700 animate-fade-in-up'; 232 const message = added ? (t('shortlist.added_toast') || 'Añadido al Plan de Rodaje') : (t('shortlist.removed_toast') || 'Sacado del Plan de Rodaje'); 233 const safeTitle = itemTitle || ''; 234 235 toast.innerHTML = ` 236 <div class="flex items-start gap-3"> 237 <div class="mt-0.5 w-7 h-7 rounded-full ${added ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'} flex items-center justify-center"> 238 <i data-lucide="${added ? 'check' : 'undo-2'}" class="w-4 h-4"></i> 239 </div> 240 <div class="min-w-0 flex-1"> 241 <p class="text-[11px] font-black uppercase tracking-widest">${message}</p> 242 ${safeTitle ? `<p class="text-xs mt-1 text-slate-500 truncate">${safeTitle}</p>` : ''} 243 <a href="/shortlist" class="inline-flex mt-2 text-[10px] font-black uppercase tracking-widest text-cdl-blue hover:text-slate-900 transition">${t('actions.shortlist_tooltip') || 'Ver Plan de Rodaje'}</a> 244 </div> 245 </div> 246 `; 247 248 document.body.appendChild(toast); 249 250 const shortlistBadges = document.querySelectorAll('.js-shortlist-badge'); 251 shortlistBadges.forEach((badge) => { 252 badge.animate( 253 [ 254 { transform: 'scale(1)' }, 255 { transform: 'scale(1.35)' }, 256 { transform: 'scale(1)' } 257 ], 258 { duration: 320, easing: 'ease-out' } 259 ); 260 }); 261 262 const shortlistLinks = document.querySelectorAll('a[href="/shortlist"]'); 263 shortlistLinks.forEach((link) => { 264 link.animate( 265 [ 266 { transform: 'translateY(0)' }, 267 { transform: 'translateY(-2px)' }, 268 { transform: 'translateY(0)' } 269 ], 270 { duration: 300, easing: 'ease-out' } 271 ); 272 }); 273 274 if (added) { 275 highlightShortlistAccessPoint(); 276 } 277 278 if (window.lucide) { 279 window.lucide.createIcons(); 280 } 281 282 setTimeout(() => { 283 toast.remove(); 284 }, 2800); 285} 286 287function applyShortlistButtonState(button, isAdded) { 288 if (!button) return; 289 290 const t = (key) => i18n.t(key); 291 const defaultAdd = t('actions.shortlist') || 'Añadir al Plan de Rodaje'; 292 const defaultRemove = t('actions.shortlist_remove') || 'Sacar del Plan de Rodaje'; 293 const addLabel = button.dataset.shortlistAddLabel || defaultAdd; 294 const removeLabel = button.dataset.shortlistRemoveLabel || defaultRemove; 295 const addIcon = button.dataset.shortlistAddIcon || 'plus'; 296 const removeIcon = button.dataset.shortlistRemoveIcon || 'check'; 297 const iconClass = button.dataset.shortlistIconClass || 'w-5 h-5'; 298 const baseClass = button.dataset.shortlistBaseClass; 299 const addedClass = button.dataset.shortlistClassAdded; 300 const removedClass = button.dataset.shortlistClassRemoved; 301 const hasVariantClassConfig = Boolean(baseClass && addedClass && removedClass); 302 const hasLabelConfig = Boolean(button.dataset.shortlistAddLabel || button.dataset.shortlistRemoveLabel); 303 304 if (hasVariantClassConfig) { 305 button.className = `${baseClass} ${isAdded ? addedClass : removedClass}`; 306 } else { 307 button.classList.toggle('bg-green-500', isAdded); 308 button.classList.toggle('text-white', isAdded); 309 button.classList.toggle('border-green-600', isAdded); 310 if (!isAdded) { 311 button.classList.remove('bg-green-500', 'border-green-600'); 312 } 313 } 314 315 button.setAttribute('aria-pressed', isAdded ? 'true' : 'false'); 316 button.title = isAdded ? removeLabel : addLabel; 317 318 if (hasLabelConfig || hasVariantClassConfig) { 319 button.innerHTML = `<i data-lucide="${isAdded ? removeIcon : addIcon}" class="${iconClass}"></i> ${isAdded ? removeLabel : addLabel}`; 320 } else { 321 button.innerHTML = `<i data-lucide="${isAdded ? 'check' : 'plus'}" class="w-4 h-4"></i>`; 322 } 323 324 if (window.lucide) { 325 window.lucide.createIcons(); 326 } 327} 328 329function setupPageActionListeners() { 330 document.body.addEventListener('click', (event) => { 331 const actionButton = event.target.closest('button[data-action]'); 332 if (!actionButton) return; 333 334 event.preventDefault(); 335 event.stopPropagation(); 336 event.stopImmediatePropagation(); 337 const action = actionButton.dataset.action; 338 const itemId = actionButton.dataset.itemId; 339 340 if (action === 'toggle-shortlist' || action === 'remove-from-shortlist') { 341 const item = { id: itemId, title: actionButton.dataset.itemTitle, type: actionButton.dataset.itemType, url: actionButton.dataset.itemUrl }; 342 const wasInShortlist = ShortlistManager.isInShortlist(itemId); 343 if (wasInShortlist) { 344 ShortlistManager.removeFromShortlist(itemId); 345 } else { 346 ShortlistManager.addToShortlist(item); 347 } 348 const isNowAdded = !wasInShortlist; 349 applyShortlistButtonState(actionButton, isNowAdded); 350 document.dispatchEvent(new CustomEvent('shortlistAction', { detail: { added: isNowAdded, itemTitle: item.title } })); 351 } 352 353 if (action === 'toggle-comparator') { 354 const item = { id: actionButton.dataset.itemId, name: actionButton.dataset.itemName, image: actionButton.dataset.itemImage }; 355 ComparatorManager.toggleComparator(item); 356 } 357 358 if (action === 'clear-comparator') { 359 ComparatorManager.clearComparator(); 360 } 361 }); 362} 363 364let lastRenderedHash = null; 365
366const PAGE_TITLES = { 367 home: "Ciudad de la Luz | Europe's Premier Film Destination", 368 studios: "Estudios de Cine | Ciudad de la Luz", 369 locations: "Localizaciones | Ciudad de la Luz", 370 hub: "Hub Audiovisual | Ciudad de la Luz", 371 why: "¿Por qué Comunitat Valenciana? | Ciudad de la Luz", 372 press: "Prensa | Ciudad de la Luz", 373 school: "Escuela de Cine | Ciudad de la Luz", 374 contact: "Contacto | Ciudad de la Luz", 375 legal: "Legal | Ciudad de la Luz", 376}; 377 378// --- FUNCIÃN DE RENDERIZADO PRINCIPAL --- 379function renderLayout(pageContent, pageId) { 380 if (!appContainer) { 381 console.error('Error: Contenedor #app no encontrado.'); 382 return; 383 } 384 const headerHTML = renderHeader(pageId, ShortlistManager.getShortlistCount()); 385 const footerHTML = renderFooter(); 386 const comparatorBarHTML = renderComparatorBar(); 387 388 appContainer.innerHTML = ` 389 ${headerHTML} 390 <main id="main-content" class="min-h-screen bg-gray-50 flex flex-col"> 391 ${pageContent} 392 </main> 393 ${footerHTML} 394 ${comparatorBarHTML} 395 `; 396 397 seoService.reset(pageId); 398 399 setupHeaderEvents(); 400 401 setupTabs(); 402 setupModals(); 403 setupCarousels(); 404 setupMultiStepForms(); 405 ensureActionButtonsAreNonSubmit(); 406 initImageOrientationAutoFit(document.getElementById('main-content') || appContainer); 407 if (window.lucide) { 408 window.lucide.createIcons(); 409 } 410 411 const currentPath = globalThis.location.pathname || '/'; 412 if (lastRenderedHash !== currentPath) { 413 globalThis.scrollTo(0, 0); 414 } 415 lastRenderedHash = currentPath; 416} 417 418// --- DEFINICIÃN DE RUTAS --- 419const routes = { 420 // 1. HOME 421 '/': () => { 422 const content = renderHomepage(); 423 renderLayout(content, 'home'); 424 }, 425 426 // 2. STUDIOS (Reestructurado) 427 '/studios-overview': async () => { 428 const content = await renderStudiosOverview(); 429 renderLayout(content, 'studios'); 430 }, 431 '/platos': async () => { 432 const content = await renderPlatosPage(); 433 renderLayout(content, 'studios'); 434 }, 435 '/backlots': async () => { 436 const content = await renderBacklotsPage(); 437 renderLayout(content, 'studios'); 438 }, 439 '/plato/:id': async (params) => { 440 const content = await renderPlatoDetail(params.id); 441 renderLayout(content, 'studios'); 442 }, 443 '/backlot/:id': (params) => { 444 const content = renderBacklotDetail(params.id); 445 renderLayout(content, 'studios'); 446 }, 447 '/water-tank': async () => { 448 const content = await renderWaterTankPage(); 449 renderLayout(content, 'studios'); 450 }, 451 '/servicios-auxiliares': async () => { 452 const content = await renderServiciosAuxuliares(); 453 renderLayout(content, 'studios'); 454 }, 455 '/production-buildings': async () => { 456 const content = await renderProductionBuildingsPage(); 457 renderLayout(content, 'studios'); 458 }, 459 '/workshop': async () => { 460 const content = await renderWorkshopPage(); 461 renderLayout(content, 'studios'); 462 }, 463 '/post-pro': async () => { 464 const content = await renderEventsPage(); 465 renderLayout(content, 'studios'); 466 }, 467 468 // 3. LOCATIONS 469 '/locations': () => { 470 const content = renderLocationsFinder(); 471 renderLayout(content, 'locations'); 472 }, 473 '/location/:id': async (params) => { 474 const content = await renderLocationDetailPage(params.id); 475 renderLayout(content, 'locations'); 476 }, 477 // Ruta con número de página 478 '/locations/:page': (params) => { 479 const content = renderLocationsFinder(parseInt(params.page)); 480 renderLayout(content, 'locations'); 481 }, 482 '/location/:id/edit': async (params) => { 483 const content = await renderLocationEditPage(params.id); 484 renderLayout(content, 'locations'); 485 }, 486 '/locations/submit': () => { 487 const content = renderLocationSubmissionPage(); 488 renderLayout(content, 'locations'); 489 initDropzone("#location-photos-dropzone", '/locations/submit'); 490 }, 491 '/my-locations': async () => { 492 const content = await renderMyLocationsPage(); 493 renderLayout(content, 'locations'); 494 }, 495 496 // 4. PRODUCTION HUB 497 '/hub': () => { 498 const content = renderHubOverview(); 499 renderLayout(content, 'hub'); 500 }, 501 '/hub/professionals': () => { 502 const content = renderProfessionalsDirectory(); 503 renderLayout(content, 'hub'); 504 }, 505 '/hub/professional/:id': (params) => { 506 const content = renderProfessionalDetailPage(params.id); 507 renderLayout(content, 'hub'); 508 }, 509 '/hub/companies': () => { 510 const content = renderCompaniesDirectory(); 511 renderLayout(content, 'hub'); 512 }, 513 '/hub/company/:id': (params) => { 514 const content = renderCompanyDetailPage(params.id); 515 renderLayout(content, 'hub'); 516 }, 517 '/hub/production-guide': () => { 518 const content = renderProductionGuidePage(); // Llamamos a la función de WhyCVPage 519 renderLayout(content, 'hub'); // Pero aplicamos el layout del Hub 520 }, 521 // NUEVO D-002.5 522 '/hub/film-offices': () => { 523 const content = renderFilmOfficesDirectory(); 524 renderLayout(content, 'hub'); 525 }, 526 '/hub/compare': () => { 527 const content = renderHubComparatorPage(); 528 renderLayout(content, 'hub'); 529 }, 530 531 // 5. WHY FILM IN CV 532 '/why-cv': () => { 533 const content = renderWhyCVOverview(); 534 renderLayout(content, 'why'); 535 }, 536 // '/why-cv/data': () => { 537 // const content = renderDataPage(); 538 // renderLayout(content, 'why'); 539 // }, 540 // CAMBIO D-002.6: Renombrado a FilmografÃa 541 '/filmography': () => { 542 const content = renderSuccessStoriesPage(); 543 renderLayout(content, 'why'); 544 }, 545 // Ruta con número de página 546 '/filmography/p/:page': (params) => { 547 const content = renderSuccessStoriesPage(params.page); 548 renderLayout(content, 'why'); 549 }, 550 // '/why-cv/connectivity': () => { 551 // const content = renderConnectivityPage(); 552 // renderLayout(content, 'why'); 553 // }, 554 '/testimonials': () => { 555 const content = renderTestimonialsPage(); 556 renderLayout(content, 'why'); 557 }, 558 '/why-cv/incentives': () => { 559 const content = renderIncentivesPage(); 560 renderLayout(content, 'why'); 561 }, 562 '/why-cv/investors': () => { 563 const content = renderInvestorsContactPage(); 564 renderLayout(content, 'why'); 565 }, 566 '/filmography/:id': async (params) => { 567 const content = await renderFilmographyDetailPage(params.id); 568 renderLayout(content, 'why'); 569 }, 570 // CAMBIO D-002.8: Producción Movida a Why CV 571 '/why-cv/production-guide': () => { 572 const content = renderProductionGuidePage(); 573 renderLayout(content, 'why'); 574 }, 575 576 // 7. PRENSA (Antes News & Agenda) 577 '/press': () => { 578 const content = renderPressPage(); 579 renderLayout(content, 'press'); 580 }, 581 '/press/:id': async (params) => { 582 const content = await renderPressReleaseDetailPage(params.id); 583 renderLayout(content, 'press'); 584 }, 585 586 // UTILS & LEGAL 587 '/shortlist': () => { 588 const content = renderShortlistPage(); 589 renderLayout(content, 'shortlist'); 590 }, 591 '/film-school': () => { 592 const content = renderFilmSchool(); 593 renderLayout(content, 'school'); 594 }, 595 '/contact': () => { 596 const content = renderContactPage(); 597 renderLayout(content, 'contact'); 598 }, 599 '/join-hub': () => { 600 if (authService.isLoggedIn()) { 601 authService.redirectToPostLogin(); 602 return; 603 } 604 const content = renderJoinHubLanding(); 605 renderLayout(content, 'hub'); 606 authService.loginEvent(); 607 }, 608 '/join-hub/professional': () => { 609 const content = renderProfessionalRegistrationForm(); 610 renderLayout(content, 'hub'); 611 }, 612 '/join-hub/company': () => { 613 const content = renderCompanyRegistrationForm(); 614 renderLayout(content, 'hub'); 615 }, 616 '/profile/edit': async () => { // Añade async 617 const content = await renderEditProfilePage(); // Añade await 618 renderLayout(content, 'hub'); 619 }, 620 '/company/edit': async () => { // Añade async 621 const content = await renderCompanyEditPage(); // Añade await 622 renderLayout(content, 'hub'); 623 }, 624 '/dashboard': () => { 625 const content = renderDashboardPage(); 626 renderLayout(content, 'hub'); 627 }, 628 '/faq': async () => { 629 const content = await renderFAQPage(); 630 renderLayout(content, 'legal'); 631 }, 632 '/transparency': () => { 633 const content = renderTransparencyPage(); 634 renderLayout(content, 'legal'); 635 }, 636 '/legal-notice': () => { 637 const content = renderLegalNoticePage(); 638 renderLayout(content, 'legal'); 639 }, 640 '/privacy-policy': () => { 641 const content = renderPrivacyPolicyPage(); 642 renderLayout(content, 'legal'); 643 }, 644 '/change-password': () => { 645 const content = renderChangePasswordPage(); 646 renderLayout(content, 'hub'); // Usamos el grupo 'hub' para mantener la coherencia con el perfil 647 }, 648 '/forgot-password': () => { 649 const content = renderForgotPasswordPage(); 650 renderLayout(content, 'hub'); 651 }, 652 '/reset-password': () => { 653 const content = renderResetPasswordPage(); 654 renderLayout(content, 'hub'); 655 }, 656}; 657 658// --- INICIALIZACIÃN --- 659document.addEventListener('DOMContentLoaded', async () => { 660 // Detectar parámetro bypass_coming_soon en la URL y añadir a sessionStorage
661 const urlParams = new URLSearchParams(window.location.search); 662 if (urlParams.get('bypass_coming_soon') === 'true') { 663 sessionStorage.setItem('cdl_bypass_coming_soon', 'true'); 664 // Limpiar URL para que no sea visible el parámetro 665 globalThis.history.replaceState({}, document.title, globalThis.location.pathname); 666 } 667 668 // 0. Modo próximamente: si está activo y no hay bypass en sessionStorage, mostrar la página y detener la inicialización 669 const comingSoonBypassed = sessionStorage.getItem('cdl_bypass_coming_soon') === 'true'; 670 if (AppConfig.COMING_SOON && !comingSoonBypassed) { 671 renderComingSoonPage(); 672 return; 673 } 674 675 // 1. Inicializar sistema de idiomas 676 await i18n.init(); 677 678 // 2. Suscribirse a cambios de idioma para re-renderizar 679 i18n.subscribe(async () => { 680 const [, homeData] = await Promise.all([ 681 globalSettingsService.init(), 682 contentService.getContent('home', { skipAutoSeoUpdate: true }).catch(() => null), 683 ]); 684 seoService.setGlobalDefaults(globalSettingsService.getSettings()?.seo); 685 seoService.setHomeSeoTitle(homeData?.seo?.metaTitle); 686 // Recargar la ruta actual para aplicar traducciones 687 window.dispatchEvent(new PopStateEvent('popstate')); 688 }); 689 690 // 3. Inicializar GlobalSettings y SEO defaults (en paralelo con pre-carga del tÃtulo home) 691 const [, homeData] = await Promise.all([ 692 globalSettingsService.init(), 693 contentService.getContent('home', { skipAutoSeoUpdate: true }).catch(() => null), 694 ]); 695 seoService.setGlobalDefaults(globalSettingsService.getSettings()?.seo); 696 seoService.setHomeSeoTitle(homeData?.seo?.metaTitle); 697 698 // 4. Iniciar Router y App 699 initRouter(routes, appContainer, renderLayout); 700 701 // 5. Cookie consent + Google Analytics 702 initCookieConsent(); 703 704 setupPageActionListeners(); 705 706 document.addEventListener('shortlistUpdated', updateShortlistCounter); 707 document.addEventListener('shortlistAction', (event) => { 708 showShortlistFeedbackToast(event.detail || {}); 709 }); 710 document.addEventListener('comparatorUpdated', () => { 711 const oldBar = document.getElementById('comparator-bar'); 712 if (oldBar) oldBar.remove(); 713 document.body.insertAdjacentHTML('beforeend', renderComparatorBar()); 714 }); 715 716 document.body.addEventListener('click', (event) => { 717 const navLink = event.target.closest('a[href^="#"]'); 718 if (navLink) { 719 const href = navLink.getAttribute('href'); 720 // Prevenir el comportamiento nativo de los anchors vacÃos 721 if (href === '#') event.preventDefault(); 722 } 723 }); 724});
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.