/** * ============================================ * KREISTAGSWAHL 2026 - INTERACTIVE COMPONENTS * ============================================ * * NEUE STRUKTUR: * - Spitzenkandidaten: 3 Cards nebeneinander (kein Slider) mit Modal * - Alle Kandidaten: Multi-Card Slider mit 1/2/3/4 Cards pro Slide (responsive) * - Modal für alle Kandidaten-Details */ // ============================================ // KANDIDATEN-DATEN (werden aus JSON geladen) // ============================================ let kandidaten = []; let spitzenkandidaten = []; /** * Lädt Spitzenkandidaten-Daten aus der JSON-Datei */ async function loadSpitzenkandidaten() { try { const response = await fetch('../data/spitzenkandidaten.json'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); spitzenkandidaten = data; console.log(`✅ ${spitzenkandidaten.length} Spitzenkandidaten erfolgreich aus JSON geladen`); return spitzenkandidaten; } catch (error) { console.error('❌ Fehler beim Laden der Spitzenkandidaten:', error); return []; } } /** * Lädt Kandidaten-Daten aus der JSON-Datei (alle 60) */ async function loadKandidaten() { try { const response = await fetch('../data/kandidaten.json'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); kandidaten = data; // Fülle auf 60 Kandidaten auf (falls weniger in JSON vorhanden) while (kandidaten.length < 60) { const baseIndex = kandidaten.length % 10; const kandidat = { ...kandidaten[baseIndex] }; const num = kandidaten.length + 1; kandidat.name = `${kandidat.name.split(' ')[0]} ${kandidat.name.split(' ')[1]} ${num}`; kandidat.position = `Kreistagskandidat*in, Listenplatz ${num}`; kandidaten.push(kandidat); } console.log(`✅ ${kandidaten.length} Kandidaten erfolgreich aus JSON geladen`); return kandidaten; } catch (error) { console.error('❌ Fehler beim Laden der Kandidaten:', error); return []; } } // ============================================ // SPITZENKANDIDATEN GRID (3 Cards nebeneinander) // ============================================ class SpitzenkandidatenGrid { constructor(modal) { this.spitzenkandidaten = spitzenkandidaten; this.gridContainer = document.querySelector('.spitzenkandidaten-grid'); this.modal = modal; if (this.gridContainer) { this.init(); } } init() { console.log(`✅ Spitzenkandidaten-Grid wird initialisiert mit ${this.spitzenkandidaten.length} Kandidaten`); this.createCards(); } createCards() { this.spitzenkandidaten.forEach((kandidat, index) => { const card = document.createElement('div'); card.className = 'spitzenkandidat-card'; card.setAttribute('data-kandidat-id', index); // Topics HTML const topicsHTML = kandidat.topics .map(topic => `${topic}`) .join(''); card.innerHTML = `
${kandidat.name}

${kandidat.name}

${kandidat.position}

${kandidat.bio.split('\n')[0]}

${topicsHTML}
`; // Click-Event für Modal card.style.cursor = 'pointer'; card.addEventListener('click', () => { if (this.modal) { this.modal.open(kandidat); } }); this.gridContainer.appendChild(card); }); console.log(`📊 ${this.spitzenkandidaten.length} Spitzenkandidaten-Cards erstellt`); } } // ============================================ // ALLE KANDIDATEN MULTI-CARD SLIDER // Shows 1/2/3/4 cards per slide (responsive) // ============================================ class AlleKandidatenSlider { constructor(modal) { this.kandidaten = kandidaten; this.modal = modal; this.currentSlide = 0; this.totalSlides = 0; this.cardsPerSlide = 1; // Wird responsiv angepasst // DOM Elemente this.sliderContent = document.querySelector('.alle-slider-content'); this.prevBtn = document.querySelector('.alle-slider-btn-prev'); this.nextBtn = document.querySelector('.alle-slider-btn-next'); this.dotsContainer = document.querySelector('.alle-slider-dots'); this.currentSlideSpan = document.querySelector('.alle-current-slide'); this.totalSlidesSpan = document.querySelector('.alle-total-slides'); if (this.sliderContent) { this.init(); } } init() { console.log(`✅ Alle-Kandidaten-Slider wird initialisiert mit ${this.kandidaten.length} Kandidaten`); // Bestimme Cards pro Slide basierend auf Viewport this.updateCardsPerSlide(); this.createSlides(); this.createDots(); this.attachEventListeners(); this.showSlide(0); // Re-render bei Resize window.addEventListener('resize', () => this.handleResize()); } /** * Bestimmt wie viele Cards pro Slide basierend auf Viewport-Breite */ updateCardsPerSlide() { const width = window.innerWidth; if (width >= 1024) { this.cardsPerSlide = 4; // Desktop: 4 Cards } else if (width >= 768) { this.cardsPerSlide = 3; // Tablet: 3 Cards } else if (width >= 640) { this.cardsPerSlide = 2; // Small Tablet: 2 Cards } else { this.cardsPerSlide = 1; // Mobile: 1 Card } this.totalSlides = Math.ceil(this.kandidaten.length / this.cardsPerSlide); console.log(`📱 Viewport: ${width}px → ${this.cardsPerSlide} Cards/Slide → ${this.totalSlides} Slides`); } /** * Erstellt Slides mit mehreren Cards */ createSlides() { // Lösche vorhandene Slides this.sliderContent.innerHTML = ''; for (let i = 0; i < this.totalSlides; i++) { const slide = document.createElement('div'); slide.className = 'alle-slide'; slide.setAttribute('data-slide', i); // Hole Kandidaten für diesen Slide const startIndex = i * this.cardsPerSlide; const endIndex = Math.min(startIndex + this.cardsPerSlide, this.kandidaten.length); const slideKandidaten = this.kandidaten.slice(startIndex, endIndex); // Erstelle Cards für diesen Slide slideKandidaten.forEach((kandidat, cardIndex) => { const card = this.createCard(kandidat, startIndex + cardIndex); slide.appendChild(card); }); this.sliderContent.appendChild(slide); } console.log(`📊 ${this.totalSlides} Multi-Card Slides erstellt`); } /** * Erstellt eine einzelne Kandidaten-Card */ createCard(kandidat, index) { const card = document.createElement('div'); card.className = 'kandidat-card'; card.setAttribute('data-kandidat-id', index); // Topics HTML (max 3) const topicsHTML = kandidat.topics .slice(0, 3) .map(topic => `${topic}`) .join(''); card.innerHTML = `
${kandidat.name}

${kandidat.name}

${kandidat.position}

${kandidat.bio}

${topicsHTML}
`; // Click-Event für Modal card.style.cursor = 'pointer'; card.addEventListener('click', () => { if (this.modal) { this.modal.open(kandidat); } }); return card; } /** * Erstellt Dot-Navigation */ createDots() { // Lösche vorhandene Dots this.dotsContainer.innerHTML = ''; // Zeige nur Dots wenn es mehr als 1 Slide gibt if (this.totalSlides <= 1) { this.dotsContainer.style.display = 'none'; return; } this.dotsContainer.style.display = 'flex'; // Bei vielen Slides nur jeden 2. oder 3. Dot zeigen const dotStep = this.totalSlides > 20 ? 3 : (this.totalSlides > 10 ? 2 : 1); for (let i = 0; i < this.totalSlides; i += dotStep) { const dot = document.createElement('button'); dot.className = 'dot'; dot.setAttribute('role', 'tab'); dot.setAttribute('aria-label', `Zu Seite ${i + 1} springen`); dot.setAttribute('data-slide', i); dot.addEventListener('click', () => this.showSlide(i)); this.dotsContainer.appendChild(dot); } this.updateDotsDisplay(); } /** * Aktualisiert Dot-Anzeige */ updateDotsDisplay() { const dots = this.dotsContainer.querySelectorAll('.dot'); dots.forEach((dot) => { const slideIndex = parseInt(dot.getAttribute('data-slide')); dot.classList.toggle('active', slideIndex === this.currentSlide); }); } /** * Event Listeners */ attachEventListeners() { // Button Navigation this.prevBtn?.addEventListener('click', () => this.previousSlide()); this.nextBtn?.addEventListener('click', () => this.nextSlide()); // Keyboard Navigation document.addEventListener('keydown', (e) => { if (this.isInViewport()) { if (e.key === 'ArrowLeft') this.previousSlide(); if (e.key === 'ArrowRight') this.nextSlide(); } }); } /** * Zeigt einen bestimmten Slide */ showSlide(index) { // Loop: Wenn Ende erreicht, springe zum Anfang if (index >= this.totalSlides) index = 0; if (index < 0) index = this.totalSlides - 1; this.currentSlide = index; // Update Slides const slides = this.sliderContent.querySelectorAll('.alle-slide'); slides.forEach((slide, i) => { const isActive = i === index; slide.classList.toggle('active', isActive); slide.setAttribute('aria-hidden', !isActive); }); // Update Dots this.updateDotsDisplay(); // Update Counter this.updateCounter(); // Update Navigation Buttons this.updateButtons(); console.log(`📍 Slide ${index + 1} / ${this.totalSlides} wird angezeigt`); } /** * Navigation: Vorheriger Slide */ previousSlide() { this.showSlide(this.currentSlide - 1); } /** * Navigation: Nächster Slide */ nextSlide() { this.showSlide(this.currentSlide + 1); } /** * Aktualisiert Counter */ updateCounter() { if (this.currentSlideSpan) { this.currentSlideSpan.textContent = this.currentSlide + 1; } if (this.totalSlidesSpan) { this.totalSlidesSpan.textContent = this.totalSlides; } } /** * Aktualisiert Button-Status */ updateButtons() { // Bei Loop sind Buttons immer aktiviert if (this.prevBtn) this.prevBtn.disabled = false; if (this.nextBtn) this.nextBtn.disabled = false; } /** * Behandelt Resize-Events */ handleResize() { const oldCardsPerSlide = this.cardsPerSlide; this.updateCardsPerSlide(); // Nur neu rendern wenn sich cardsPerSlide geändert hat if (oldCardsPerSlide !== this.cardsPerSlide) { console.log(`🔄 Resize: ${oldCardsPerSlide} → ${this.cardsPerSlide} Cards/Slide`); // Berechne neue Slide-Position basierend auf erstem sichtbaren Kandidaten const firstVisibleKandidatIndex = this.currentSlide * oldCardsPerSlide; const newSlideIndex = Math.floor(firstVisibleKandidatIndex / this.cardsPerSlide); this.createSlides(); this.createDots(); this.showSlide(Math.min(newSlideIndex, this.totalSlides - 1)); } } /** * Prüft ob Slider im Viewport ist */ isInViewport() { if (!this.sliderContent) return false; const rect = this.sliderContent.getBoundingClientRect(); return rect.top < window.innerHeight && rect.bottom > 0; } } // ============================================ // MOBILE MENU // ============================================ class MobileMenu { constructor() { this.toggle = document.querySelector('.mobile-menu-toggle'); this.navLinks = document.querySelector('.nav-links'); this.isOpen = false; this.init(); } init() { if (!this.toggle) return; this.toggle.addEventListener('click', () => this.toggleMenu()); // Schließe Menü bei Klick auf Links const links = this.navLinks.querySelectorAll('a'); links.forEach(link => { link.addEventListener('click', () => { if (this.isOpen) this.toggleMenu(); }); }); // Schließe bei ESC-Taste document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen) { this.toggleMenu(); } }); } toggleMenu() { this.isOpen = !this.isOpen; // Mobile: Flexbox-Layout für vertikales Menü if (this.isOpen) { this.navLinks.style.display = 'flex'; this.navLinks.style.flexDirection = 'column'; this.navLinks.style.position = 'absolute'; this.navLinks.style.top = '100%'; this.navLinks.style.left = '0'; this.navLinks.style.right = '0'; this.navLinks.style.background = 'rgb(245, 241, 233)'; this.navLinks.style.padding = '1rem'; this.navLinks.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)'; } else { this.navLinks.style.display = 'none'; } this.toggle.setAttribute('aria-expanded', this.isOpen); // Animiere Hamburger Icon const spans = this.toggle.querySelectorAll('span'); if (this.isOpen) { spans[0].style.transform = 'rotate(45deg) translate(5px, 5px)'; spans[1].style.opacity = '0'; spans[2].style.transform = 'rotate(-45deg) translate(7px, -6px)'; } else { spans.forEach(span => { span.style.transform = ''; span.style.opacity = ''; }); } } } // ============================================ // KANDIDATEN MODAL // ============================================ class KandidatenModal { constructor() { this.modal = document.getElementById('kandidat-modal'); this.modalContent = this.modal?.querySelector('.modal-content'); this.closeBtn = this.modal?.querySelector('.modal-close'); this.overlay = this.modal?.querySelector('.modal-overlay'); if (this.modal) { this.init(); } } init() { // Close-Button this.closeBtn?.addEventListener('click', () => this.close()); // Overlay-Klick this.overlay?.addEventListener('click', () => this.close()); // ESC-Taste document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.modal.classList.contains('active')) { this.close(); } }); } open(kandidat) { if (!this.modal || !kandidat) return; // Modal-Inhalt erstellen const topicsHTML = kandidat.topics .map(topic => `${topic}`) .join(''); // Highlights HTML (falls vorhanden, für Spitzenkandidaten) const highlightsHTML = kandidat.highlights ? ` ` : ''; // Bio mit Zeilenumbrüchen const bioHTML = kandidat.bio.replace(/\n/g, '

'); this.modalContent.innerHTML = ` ${highlightsHTML} `; // Modal öffnen this.modal.classList.add('active'); this.modal.setAttribute('aria-hidden', 'false'); document.body.classList.add('modal-open'); } close() { if (!this.modal) return; this.modal.classList.remove('active'); this.modal.setAttribute('aria-hidden', 'true'); document.body.classList.remove('modal-open'); } } // ============================================ // SMOOTH SCROLL FÜR ANKER-LINKS // ============================================ function initSmoothScroll() { document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { const href = this.getAttribute('href'); // Ignoriere leere Anker und Platzhalter if (href === '#' || href === '#kontakt' || href === '#programm' || href === '#impressum' || href === '#datenschutz') { e.preventDefault(); return; } const target = document.querySelector(href); if (target) { e.preventDefault(); const navHeight = document.querySelector('.navbar')?.offsetHeight || 0; const targetPosition = target.offsetTop - navHeight - 20; window.scrollTo({ top: targetPosition, behavior: 'smooth' }); } }); }); } // ============================================ // INTERSECTION OBSERVER für Fade-In Animationen // ============================================ function initScrollAnimations() { const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }, observerOptions); // Beobachte Sections für Fade-In Effekt const sections = document.querySelectorAll('.landrat-section, .spitzenkandidaten-section, .alle-kandidaten-section, .cta-section'); sections.forEach(section => { section.classList.add('fade-in'); observer.observe(section); }); } // ============================================ // PERFORMANCE: Lazy Loading für Bilder // ============================================ function initLazyLoading() { if ('loading' in HTMLImageElement.prototype) { console.log('✅ Native lazy loading unterstützt'); return; } // Fallback für ältere Browser const images = document.querySelectorAll('img[loading="lazy"]'); const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src || img.src; imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img)); } // ============================================ // INITIALISIERUNG // ============================================ document.addEventListener('DOMContentLoaded', async () => { console.log('🚀 Kreistagswahl 2026 - Landingpage wird initialisiert...'); // 1. Lade Spitzenkandidaten-Daten aus JSON await loadSpitzenkandidaten(); // 2. Lade alle Kandidaten-Daten aus JSON await loadKandidaten(); // 3. Initialisiere Modal const modal = new KandidatenModal(); // 4. Initialisiere Spitzenkandidaten-Grid (3 Cards nebeneinander) const spitzenGrid = new SpitzenkandidatenGrid(modal); // 5. Initialisiere Alle-Kandidaten-Slider (Multi-Card Slider) const alleSlider = new AlleKandidatenSlider(modal); // 6. Initialisiere Mobile Menu const mobileMenu = new MobileMenu(); // 7. Initialisiere Smooth Scroll initSmoothScroll(); // 8. Initialisiere Scroll-Animationen initScrollAnimations(); // 9. Initialisiere Lazy Loading initLazyLoading(); console.log('✅ Alle Komponenten erfolgreich geladen!'); console.log(`📊 Spitzen-Grid: ${spitzenkandidaten.length} Kandidaten | Alle-Slider: ${kandidaten.length} Kandidaten (${alleSlider.totalSlides} Slides)`); }); // ============================================ // ERROR HANDLING // ============================================ window.addEventListener('error', (e) => { console.error('❌ JavaScript Fehler:', e.message); });