- zoesch.de - blitzkiste.net - gruene-hassberge (norbert.zoesch.de) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
701 lines
22 KiB
JavaScript
701 lines
22 KiB
JavaScript
/**
|
|
* ============================================
|
|
* 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 => `<span class="spitzenkandidat-topic-tag">${topic}</span>`)
|
|
.join('');
|
|
|
|
card.innerHTML = `
|
|
<div class="spitzenkandidat-card-image">
|
|
<img src="${kandidat.image}"
|
|
alt="${kandidat.name}"
|
|
loading="lazy">
|
|
</div>
|
|
<div class="spitzenkandidat-card-content">
|
|
<h3 class="spitzenkandidat-card-name">${kandidat.name}</h3>
|
|
<p class="spitzenkandidat-card-position">${kandidat.position}</p>
|
|
<p class="spitzenkandidat-card-bio">${kandidat.bio.split('\n')[0]}</p>
|
|
<div class="spitzenkandidat-card-topics">
|
|
${topicsHTML}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 => `<span class="kandidat-topic-tag">${topic}</span>`)
|
|
.join('');
|
|
|
|
card.innerHTML = `
|
|
<div class="kandidat-card-image">
|
|
<img src="${kandidat.image}"
|
|
alt="${kandidat.name}"
|
|
loading="lazy">
|
|
</div>
|
|
<div class="kandidat-card-content">
|
|
<h3 class="kandidat-card-name">${kandidat.name}</h3>
|
|
<p class="kandidat-card-position">${kandidat.position}</p>
|
|
<p class="kandidat-card-bio">${kandidat.bio}</p>
|
|
<div class="kandidat-card-topics">
|
|
${topicsHTML}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 => `<span class="modal-topic-tag">${topic}</span>`)
|
|
.join('');
|
|
|
|
// Highlights HTML (falls vorhanden, für Spitzenkandidaten)
|
|
const highlightsHTML = kandidat.highlights ? `
|
|
<div class="modal-highlights">
|
|
<h3>Meine Ziele</h3>
|
|
<ul>
|
|
${kandidat.highlights.map(highlight => `<li>${highlight}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
` : '';
|
|
|
|
// Bio mit Zeilenumbrüchen
|
|
const bioHTML = kandidat.bio.replace(/\n/g, '<br><br>');
|
|
|
|
this.modalContent.innerHTML = `
|
|
<div class="modal-header">
|
|
<div class="modal-image">
|
|
<img src="${kandidat.image}" alt="${kandidat.name}">
|
|
</div>
|
|
<div class="modal-info">
|
|
<h2>${kandidat.name}</h2>
|
|
<p class="position">${kandidat.position}</p>
|
|
</div>
|
|
</div>
|
|
<div class="modal-bio">
|
|
${bioHTML}
|
|
</div>
|
|
${highlightsHTML}
|
|
<div class="modal-topics">
|
|
<h3>Schwerpunkte</h3>
|
|
<div class="modal-topics-list">
|
|
${topicsHTML}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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);
|
|
});
|