PageSourceSearch

https://www.ciudaddelaluz.com/app.js

js ciudaddelaluz.com collected 2026-10-02 12:56:19 UTC 28,098 bytes, 724 lines download raw bytes

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.