Dateien
Website/vorschau/js/app.js
Claude Code 6e7f5b5c5f feat: Karte reagiert auf Karussell-Wechsel
- Map-Instanz wird einmalig erstellt, Marker dynamisch gewechselt
- data-lage Attribute auf Carousel-Cards fuer Lagen-Zuordnung
- Bei Lage mit Daten: Marker + Legende angezeigt
- Bei Platzhalter: Karte ausgeblendet, 'Kartendaten folgen'
- Zukunftssicher: Neue Lagen brauchen nur data-lage + summary.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:03:59 +02:00

325 Zeilen
13 KiB
JavaScript

/* AegisSight Monitor - Product Page v2 */
(function () {
'use strict';
/* ==================== NAVBAR ==================== */
var navbar = document.getElementById('navbar');
window.addEventListener('scroll', function () {
navbar.classList.toggle('scrolled', window.scrollY > 10);
});
/* ==================== MOBILE MENU ==================== */
var toggle = document.querySelector('.mobile-menu-toggle');
var menu = document.getElementById('mobile-menu');
var overlay = document.getElementById('mobile-overlay');
function closeMenu() {
toggle.classList.remove('active');
menu.classList.remove('open');
overlay.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
}
toggle.addEventListener('click', function () {
var isOpen = menu.classList.contains('open');
if (isOpen) { closeMenu(); } else {
toggle.classList.add('active');
menu.classList.add('open');
overlay.classList.add('open');
toggle.setAttribute('aria-expanded', 'true');
}
});
overlay.addEventListener('click', closeMenu);
menu.querySelectorAll('a').forEach(function (l) { l.addEventListener('click', closeMenu); });
/* ==================== SMOOTH SCROLL ==================== */
document.querySelectorAll('a[href^="#"]').forEach(function (link) {
link.addEventListener('click', function (e) {
var t = document.querySelector(this.getAttribute('href'));
if (t) { e.preventDefault(); t.scrollIntoView({ behavior: 'smooth' }); }
});
});
/* ==================== HERO VIDEOS ==================== */
var videos = document.querySelectorAll('.hero-video');
var currentVideo = 0;
var rotationTimer;
function switchVideo() {
videos[currentVideo].classList.remove('active');
currentVideo = (currentVideo + 1) % videos.length;
var next = videos[currentVideo];
next.currentTime = 0;
next.play().catch(function () {});
next.classList.add('active');
}
function startRotation() {
rotationTimer = setInterval(switchVideo, 12000);
}
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
clearInterval(rotationTimer);
videos.forEach(function (v) { v.pause(); });
} else {
videos.forEach(function (v) { if (v.classList.contains('active')) v.play().catch(function () {}); });
startRotation();
}
});
if (videos.length > 1) startRotation();
/* ==================== MAP STATE ==================== */
var mapInstance = null;
var markerLayer = null;
var legendControl = null;
var lageData = {};
/* ==================== 3D CAROUSEL ==================== */
var cards = document.querySelectorAll('.carousel-card');
var dots = document.querySelectorAll('.carousel-dot');
var activeIndex = 0;
function positionCards(idx) {
activeIndex = idx;
cards.forEach(function (card, i) {
card.classList.remove('active', 'left', 'right', 'hidden');
if (i === idx) card.classList.add('active');
else if (i === (idx - 1 + cards.length) % cards.length) card.classList.add('left');
else if (i === (idx + 1) % cards.length) card.classList.add('right');
else card.classList.add('hidden');
});
dots.forEach(function (dot, i) {
dot.classList.toggle('active', i === idx);
});
// Update map based on active Lage
var lage = cards[idx].getAttribute('data-lage');
var mapSection = document.getElementById('map-section');
if (lage && lageData[lage]) {
mapSection.classList.remove('map-hidden');
showMarkers(lageData[lage].locations, lageData[lage].category_labels);
} else {
if (mapSection) mapSection.classList.add('map-hidden');
clearMarkers();
}
}
cards.forEach(function (card, i) {
card.addEventListener('click', function () {
if (!card.classList.contains('active')) positionCards(i);
});
});
dots.forEach(function (dot, i) {
dot.addEventListener('click', function () { positionCards(i); });
});
positionCards(0);
// Arrow navigation
var prevBtn = document.querySelector('.carousel-prev');
var nextBtn = document.querySelector('.carousel-next');
if (prevBtn) prevBtn.addEventListener('click', function () {
positionCards((activeIndex - 1 + cards.length) % cards.length);
});
if (nextBtn) nextBtn.addEventListener('click', function () {
positionCards((activeIndex + 1) % cards.length);
});
/* ==================== SIMPLE MARKDOWN ==================== */
function mdToHtml(md) {
if (!md) return '';
var lines = md.split('\n');
var html = '';
var inList = false;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) {
if (inList) { html += '</ul>'; inList = false; }
continue;
}
line = line.replace(/\[(\d+[a-z]?)\]/g, '');
line = line.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
if (/^## /.test(line)) {
if (inList) { html += '</ul>'; inList = false; }
html += '<h2>' + line.replace(/^## /, '') + '</h2>';
} else if (/^### /.test(line)) {
if (inList) { html += '</ul>'; inList = false; }
html += '<h3>' + line.replace(/^### /, '') + '</h3>';
} else if (/^- /.test(line)) {
if (!inList) { html += '<ul>'; inList = true; }
html += '<li>' + line.replace(/^- /, '') + '</li>';
} else {
if (inList) { html += '</ul>'; inList = false; }
html += '<p>' + line + '</p>';
}
}
if (inList) html += '</ul>';
return html;
}
/* ==================== LIVE DATA ==================== */
function timeAgo(dateStr) {
var diffMin = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000);
if (diffMin < 1) return 'Gerade eben aktualisiert';
if (diffMin < 60) return 'Aktualisiert vor ' + diffMin + ' Min.';
var diffH = Math.floor(diffMin / 60);
if (diffH < 24) return 'Aktualisiert vor ' + diffH + ' Std.';
var diffD = Math.floor(diffH / 24);
return 'Aktualisiert vor ' + diffD + (diffD === 1 ? ' Tag' : ' Tagen');
}
function loadLiveData() {
fetch('/lagen/iran-konflikt/data/summary.json?t=' + Date.now())
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(function (data) {
var inc = data.incident || {};
// summary.json has flat structure
var ea = document.getElementById('stat-articles');
var es = document.getElementById('stat-sources');
var ef = document.getElementById('stat-factchecks');
function countUp(el, target) {
if (!el || !target) return;
var start = 0;
var duration = 1200;
var startTime = null;
function step(ts) {
if (!startTime) startTime = ts;
var progress = Math.min((ts - startTime) / duration, 1);
var ease = 1 - Math.pow(1 - progress, 3);
el.textContent = Math.floor(ease * target).toLocaleString('de-DE');
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
countUp(ea, inc.article_count);
countUp(es, inc.source_count);
countUp(ef, inc.factcheck_count);
// Excerpt: pre-extracted in summary.json
var excerptEl = document.getElementById('excerpt-text');
if (excerptEl && data.zusammenfassung) {
excerptEl.innerHTML = mdToHtml(data.zusammenfassung);
}
// Store data and init map
lageData['iran-konflikt'] = {
locations: data.locations || [],
category_labels: data.category_labels || {}
};
createMap();
showMarkers(data.locations || [], data.category_labels || {});
})
.catch(function () {
});
}
/* ==================== LEAFLET MAP ==================== */
function clearMarkers() {
if (markerLayer) markerLayer.clearLayers();
if (legendControl && mapInstance) { mapInstance.removeControl(legendControl); legendControl = null; }
}
function createMap() {
if (mapInstance) return;
var mapEl = document.getElementById('map-container');
if (!mapEl || typeof L === 'undefined') return;
mapInstance = L.map(mapEl, {
center: [33.0, 48.0], zoom: 5, zoomControl: true, scrollWheelZoom: true,
minZoom: 2, maxBounds: [[-85, -180], [85, 180]], maxBoundsViscosity: 1.0
});
L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19, noWrap: true
}).addTo(mapInstance);
markerLayer = L.layerGroup().addTo(mapInstance);
setTimeout(function () { mapInstance.invalidateSize(); }, 500);
}
function pulseIcon(color) {
return L.divIcon({
className: '',
html: '<div class="pulse-marker-wrapper">'
+ '<div class="pulse-marker-ring" style="border-color:' + color + '"></div>'
+ '<div class="pulse-marker-ring" style="border-color:' + color + '"></div>'
+ '<div class="pulse-marker-dot" style="background:' + color + ';box-shadow:0 0 10px ' + color + '"></div>'
+ '</div>',
iconSize: [20, 20], iconAnchor: [10, 10], popupAnchor: [0, -12]
});
}
function showMarkers(locations, apiLabels) {
if (!mapInstance) createMap();
clearMarkers();
var categoryColors = {
primary: '#ef4444',
secondary: '#f59e0b',
tertiary: '#3b82f6',
mentioned: '#7b7b7b'
};
var defaultLabels = {
primary: 'Hauptgeschehen',
secondary: 'Reaktionen',
tertiary: 'Beteiligte',
mentioned: 'Erwähnt'
};
var categoryLabels = {};
['primary', 'secondary', 'tertiary', 'mentioned'].forEach(function (k) {
categoryLabels[k] = (apiLabels && apiLabels[k]) || defaultLabels[k];
});
var usedCategories = {};
var bounds = [];
locations.forEach(function (loc) {
if (!loc.lat || !loc.lon) return;
var cat = loc.category || 'mentioned';
var color = categoryColors[cat] || '#7b7b7b';
usedCategories[cat] = true;
var popup = '<strong style="color:#E8ECF4;">' + (loc.name || '') + '</strong>';
if (loc.country_code) popup += ' <span style="color:#8896AB;font-size:0.8rem;">(' + loc.country_code + ')</span>';
popup += '<br><span style="font-size:0.85rem;color:#8896AB;">' + (loc.article_count || 0) + ' Artikel</span>';
L.marker([loc.lat, loc.lon], { icon: pulseIcon(color) })
.addTo(markerLayer)
.bindPopup(popup);
bounds.push([loc.lat, loc.lon]);
});
// Dark legend (exact lagebild style)
var legend = L.control({ position: 'bottomright' });
legend.onAdd = function () {
var div = L.DomUtil.create('div');
div.style.cssText = 'background:#151D2E;padding:10px 14px;border-radius:4px;border:1px solid #1E2D45;box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:0.8rem;line-height:1.8;color:#E8ECF4;';
var html = '<strong style="color:#C8A851;">Legende</strong><br>';
['primary', 'secondary', 'tertiary', 'mentioned'].forEach(function (cat) {
if (usedCategories[cat]) {
html += '<span style="color:' + categoryColors[cat] + ';">&#9679;</span> ' + categoryLabels[cat] + '<br>';
}
});
div.innerHTML = html;
return div;
};
legend.addTo(mapInstance);
if (bounds.length > 0) {
mapInstance.fitBounds(bounds, { padding: [30, 30], maxZoom: 7 });
}
setTimeout(function () { mapInstance.invalidateSize(); }, 300);
}
/* ==================== INIT ==================== */
loadLiveData();
})();