Files
Felix Zösch 07c290a453 Initial commit: Backup der Webseiten
- 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>
2025-12-13 01:17:15 +01:00

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);
});