fix: 3D-Karussell + exakte Leaflet-Karte wie /lagebild

- 3D-Perspektiv-Karussell: Zentrale Card gross, seitliche klein/gekippt
- Klick auf seitliche Cards wechselt Ansicht, Dot-Navigation
- Karte mit exakten Pulse-Markern (Ring + Dot Animation)
- Dark Popups und Dark Legende wie bei /lagen/iran-konflikt/
- Kategorie-Farben und Labels aus der API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Code
2026-04-06 18:15:41 +02:00
Ursprung d4d54a59b8
Commit 9c5ce933fb
3 geänderte Dateien mit 136 neuen und 59 gelöschten Zeilen

Datei anzeigen

@@ -130,11 +130,17 @@ a { color:inherit; text-decoration:none; }
.feature-card h3 { font-size:1rem; font-weight:700; color:var(--navy); margin-bottom:8px; } .feature-card h3 { font-size:1rem; font-weight:700; color:var(--navy); margin-bottom:8px; }
.feature-card p { font-size:0.88rem; color:var(--text-light); line-height:1.6; } .feature-card p { font-size:0.88rem; color:var(--text-light); line-height:1.6; }
/* ==================== CAROUSEL ==================== */ /* ==================== 3D CAROUSEL ==================== */
.carousel-wrapper { overflow-x:auto; -webkit-overflow-scrolling:touch; scrollbar-width:none; margin:0 -24px; padding:0 24px; } .carousel-viewport { perspective:1200px; overflow:visible; padding:40px 0 20px; }
.carousel-wrapper::-webkit-scrollbar { display:none; } .carousel-track { display:flex; justify-content:center; align-items:center; position:relative; min-height:440px; }
.carousel { display:flex; gap:24px; padding:8px 0 16px; } .carousel-card { width:380px; flex-shrink:0; background:var(--white); border-radius:var(--radius-lg); padding:28px 24px; box-shadow:var(--shadow); position:absolute; display:flex; flex-direction:column; transition:all 0.6s cubic-bezier(0.4,0,0.2,1); cursor:pointer; transform-style:preserve-3d; }
.carousel-card { min-width:340px; max-width:380px; flex-shrink:0; background:var(--white); border-radius:var(--radius-lg); padding:28px 24px; box-shadow:var(--shadow); position:relative; display:flex; flex-direction:column; } .carousel-card.active { transform:scale(1) translateX(0) rotateY(0); z-index:3; opacity:1; pointer-events:all; }
.carousel-card.left { transform:scale(0.78) translateX(-110%) rotateY(12deg); z-index:1; opacity:0.5; pointer-events:all; }
.carousel-card.right { transform:scale(0.78) translateX(110%) rotateY(-12deg); z-index:1; opacity:0.5; pointer-events:all; }
.carousel-card.hidden { transform:scale(0.6) translateX(0); z-index:0; opacity:0; pointer-events:none; }
.carousel-nav { display:flex; justify-content:center; gap:10px; margin-top:24px; }
.carousel-dot { width:10px; height:10px; border-radius:50%; border:2px solid var(--gold); background:transparent; cursor:pointer; transition:all 0.3s; padding:0; }
.carousel-dot.active { background:var(--gold); }
.card-live { border:2px solid var(--gold); box-shadow:0 4px 24px rgba(200,168,81,0.15); } .card-live { border:2px solid var(--gold); box-shadow:0 4px 24px rgba(200,168,81,0.15); }
.card-placeholder { border:2px dashed var(--gray-200); opacity:0.55; } .card-placeholder { border:2px dashed var(--gray-200); opacity:0.55; }
.demo-badge { display:inline-block; padding:4px 14px; border-radius:20px; font-size:0.72rem; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:14px; width:fit-content; background:var(--gold); color:var(--navy); } .demo-badge { display:inline-block; padding:4px 14px; border-radius:20px; font-size:0.72rem; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; margin-bottom:14px; width:fit-content; background:var(--gold); color:var(--navy); }
@@ -162,18 +168,17 @@ a { color:inherit; text-decoration:none; }
.map-title { font-size:1.1rem; font-weight:600; color:var(--navy); margin-bottom:16px; text-align:center; } .map-title { font-size:1.1rem; font-weight:600; color:var(--navy); margin-bottom:16px; text-align:center; }
#map-container { height:420px; border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow); border:1px solid var(--gray-100); } #map-container { height:420px; border-radius:var(--radius-lg); overflow:hidden; box-shadow:var(--shadow); border:1px solid var(--gray-100); }
/* Map pulse markers */ /* Map pulse markers (exact lagebild style) */
.pulse-marker { width:12px; height:12px; border-radius:50%; position:relative; } .pulse-marker-wrapper { position:relative; width:20px; height:20px; }
.pulse-marker::after { content:''; position:absolute; inset:-4px; border-radius:50%; border:2px solid; opacity:0.5; animation:mapPulse 2s infinite; } .pulse-marker-ring { position:absolute; inset:0; border-radius:50%; border:2px solid; animation:mapPulseRing 2s infinite; opacity:0; }
.pulse-marker.cat-primary { background:#E74C3C; } .pulse-marker-ring:nth-child(2) { animation-delay:1s; }
.pulse-marker.cat-primary::after { border-color:#E74C3C; } @keyframes mapPulseRing { 0%{transform:scale(0.5);opacity:0} 30%{opacity:0.6} 100%{transform:scale(2.5);opacity:0} }
.pulse-marker.cat-secondary { background:#F39C12; } .pulse-marker-dot { position:absolute; top:50%; left:50%; width:8px; height:8px; margin:-4px 0 0 -4px; border-radius:50%; animation:pulseDot 2s infinite; }
.pulse-marker.cat-secondary::after { border-color:#F39C12; } @keyframes pulseDot { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.7)} }
.pulse-marker.cat-tertiary { background:#3498DB; } /* Dark popup style */
.pulse-marker.cat-tertiary::after { border-color:#3498DB; } .leaflet-popup-content-wrapper { background:#151D2E!important; color:#E8ECF4!important; border:1px solid #1E2D45!important; border-radius:4px!important; box-shadow:0 4px 16px rgba(0,0,0,0.4)!important; }
.pulse-marker.cat-mentioned { background:#95A5A6; } .leaflet-popup-tip { background:#151D2E!important; }
.pulse-marker.cat-mentioned::after { border-color:#95A5A6; } .leaflet-popup-content { margin:10px 14px!important; font-size:0.85rem!important; }
@keyframes mapPulse { 0%{transform:scale(1);opacity:0.5} 100%{transform:scale(2.5);opacity:0} }
/* ==================== TRUST ==================== */ /* ==================== TRUST ==================== */
.trust-grid { margin-top:48px; } .trust-grid { margin-top:48px; }
@@ -205,7 +210,7 @@ a { color:inherit; text-decoration:none; }
.hero-title { font-size:2.8rem; } .hero-title { font-size:2.8rem; }
.section { padding:64px 0; } .section { padding:64px 0; }
.workflow-connector { width:40px; } .workflow-connector { width:40px; }
.carousel-card { min-width:300px; }
} }
@media(max-width:768px) { @media(max-width:768px) {
@@ -224,12 +229,12 @@ a { color:inherit; text-decoration:none; }
.hero-cta { flex-direction:column; } .hero-cta { flex-direction:column; }
.hero-cta .btn { width:100%; } .hero-cta .btn { width:100%; }
.footer-content { flex-direction:column; text-align:center; gap:16px; } .footer-content { flex-direction:column; text-align:center; gap:16px; }
.carousel-card { min-width:280px; }
#map-container { height:300px; } #map-container { height:300px; }
} }
@media(max-width:480px) { @media(max-width:480px) {
.hero-title { font-size:1.8rem; } .hero-title { font-size:1.8rem; }
.container { padding:0 16px; } .container { padding:0 16px; }
.carousel-card { min-width:260px; }
} }

Datei anzeigen

@@ -112,11 +112,11 @@
<h2 class="section-title">Sehen Sie den Monitor in Aktion</h2> <h2 class="section-title">Sehen Sie den Monitor in Aktion</h2>
<p class="section-subtitle">Echte Lagebilder, erstellt vom AegisSight Monitor. Live und ohne Bearbeitung.</p> <p class="section-subtitle">Echte Lagebilder, erstellt vom AegisSight Monitor. Live und ohne Bearbeitung.</p>
<!-- Carousel --> <!-- 3D Carousel -->
<div class="carousel-wrapper"> <div class="carousel-viewport">
<div class="carousel" id="carousel"> <div class="carousel-track" id="carousel">
<!-- Iran Card --> <!-- Iran Card -->
<div class="carousel-card card-live"> <div class="carousel-card card-live active" data-index="0">
<div class="demo-badge">LIVE</div> <div class="demo-badge">LIVE</div>
<h3 class="demo-title">Iran-Konflikt</h3> <h3 class="demo-title">Iran-Konflikt</h3>
<div class="demo-stats" id="demo-stats-iran"> <div class="demo-stats" id="demo-stats-iran">
@@ -141,18 +141,23 @@
<a href="/lagen/iran-konflikt/" class="btn btn-primary btn-block">Vollständiges Lagebild öffnen</a> <a href="/lagen/iran-konflikt/" class="btn btn-primary btn-block">Vollständiges Lagebild öffnen</a>
</div> </div>
<!-- Placeholder 2 --> <!-- Placeholder 2 -->
<div class="carousel-card card-placeholder"> <div class="carousel-card card-placeholder" data-index="1">
<div class="demo-badge badge-soon">Demnächst</div> <div class="demo-badge badge-soon">Demnächst</div>
<h3 class="demo-title placeholder-title">Weitere Lage</h3> <h3 class="demo-title placeholder-title">Weitere Lage</h3>
<p class="placeholder-text">In Vorbereitung</p> <p class="placeholder-text">In Vorbereitung</p>
</div> </div>
<!-- Placeholder 3 --> <!-- Placeholder 3 -->
<div class="carousel-card card-placeholder"> <div class="carousel-card card-placeholder" data-index="2"data-index="1">
<div class="demo-badge badge-soon">Demnächst</div> <div class="demo-badge badge-soon">Demnächst</div>
<h3 class="demo-title placeholder-title">Weitere Lage</h3> <h3 class="demo-title placeholder-title">Weitere Lage</h3>
<p class="placeholder-text">In Vorbereitung</p> <p class="placeholder-text">In Vorbereitung</p>
</div> </div>
</div> </div>
<div class="carousel-nav">
<button class="carousel-dot active" data-index="0"></button>
<button class="carousel-dot" data-index="1"></button>
<button class="carousel-dot" data-index="2"></button>
</div>
</div> </div>
<!-- Map --> <!-- Map -->

Datei anzeigen

@@ -44,7 +44,6 @@
/* ==================== HERO VIDEOS ==================== */ /* ==================== HERO VIDEOS ==================== */
var videos = document.querySelectorAll('.hero-video'); var videos = document.querySelectorAll('.hero-video');
var currentVideo = 0; var currentVideo = 0;
var ROTATION_INTERVAL = 12000;
var rotationTimer; var rotationTimer;
function switchVideo() { function switchVideo() {
@@ -57,10 +56,9 @@
} }
function startRotation() { function startRotation() {
rotationTimer = setInterval(switchVideo, ROTATION_INTERVAL); rotationTimer = setInterval(switchVideo, 12000);
} }
// Pause when tab hidden
document.addEventListener('visibilitychange', function () { document.addEventListener('visibilitychange', function () {
if (document.hidden) { if (document.hidden) {
clearInterval(rotationTimer); clearInterval(rotationTimer);
@@ -73,10 +71,40 @@
if (videos.length > 1) startRotation(); if (videos.length > 1) startRotation();
/* ==================== 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);
});
}
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);
/* ==================== SIMPLE MARKDOWN ==================== */ /* ==================== SIMPLE MARKDOWN ==================== */
function mdToText(md) { function mdToHtml(md) {
if (!md) return ''; if (!md) return '';
// Remove ## headers and bold markers, keep text
return md return md
.replace(/^## .+$/gm, '') .replace(/^## .+$/gm, '')
.replace(/^### .+$/gm, '') .replace(/^### .+$/gm, '')
@@ -101,17 +129,13 @@
return 'Aktualisiert vor ' + diffD + (diffD === 1 ? ' Tag' : ' Tagen'); return 'Aktualisiert vor ' + diffD + (diffD === 1 ? ' Tag' : ' Tagen');
} }
var liveData = null;
function loadLiveData() { function loadLiveData() {
fetch('/lagen/iran-konflikt/data/current.json?t=' + Date.now()) fetch('/lagen/iran-konflikt/data/current.json?t=' + Date.now())
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }) .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })
.then(function (data) { .then(function (data) {
liveData = data;
var inc = data.incident || {}; var inc = data.incident || {};
var lag = data.current_lagebild || {}; var lag = data.current_lagebild || {};
// Stats
var ea = document.getElementById('stat-articles'); var ea = document.getElementById('stat-articles');
var es = document.getElementById('stat-sources'); var es = document.getElementById('stat-sources');
var ef = document.getElementById('stat-factchecks'); var ef = document.getElementById('stat-factchecks');
@@ -125,10 +149,8 @@
var excerptEl = document.getElementById('excerpt-text'); var excerptEl = document.getElementById('excerpt-text');
var toggleBtn = document.getElementById('excerpt-toggle'); var toggleBtn = document.getElementById('excerpt-toggle');
if (excerptEl && lag.summary_markdown) { if (excerptEl && lag.summary_markdown) {
var html = mdToText(lag.summary_markdown); excerptEl.innerHTML = mdToHtml(lag.summary_markdown);
excerptEl.innerHTML = html;
toggleBtn.style.display = 'inline-block'; toggleBtn.style.display = 'inline-block';
toggleBtn.addEventListener('click', function () { toggleBtn.addEventListener('click', function () {
var expanded = excerptEl.classList.toggle('expanded'); var expanded = excerptEl.classList.toggle('expanded');
this.textContent = expanded ? 'Weniger anzeigen' : 'Weiterlesen'; this.textContent = expanded ? 'Weniger anzeigen' : 'Weiterlesen';
@@ -146,8 +168,8 @@
}); });
} }
/* ==================== LEAFLET MAP ==================== */ /* ==================== LEAFLET MAP (exact lagebild style) ==================== */
function initMap(locations, categoryLabels) { function initMap(locations, apiLabels) {
var mapEl = document.getElementById('map-container'); var mapEl = document.getElementById('map-container');
if (!mapEl || typeof L === 'undefined') return; if (!mapEl || typeof L === 'undefined') return;
@@ -155,46 +177,91 @@
center: [33.0, 48.0], center: [33.0, 48.0],
zoom: 5, zoom: 5,
zoomControl: true, zoomControl: true,
scrollWheelZoom: false scrollWheelZoom: false,
minZoom: 2,
maxBounds: [[-85, -180], [85, 180]],
maxBoundsViscosity: 1.0
}); });
L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap', attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 18 maxZoom: 19,
noWrap: true
}).addTo(map); }).addTo(map);
var catColors = { // Exact same pulse icon as lagebild
primary: '#E74C3C', function pulseIcon(color) {
secondary: '#F39C12', return L.divIcon({
tertiary: '#3498DB', className: '',
mentioned: '#95A5A6' 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]
});
}
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 = []; var bounds = [];
locations.forEach(function (loc) { locations.forEach(function (loc) {
if (!loc.lat || !loc.lon) return; if (!loc.lat || !loc.lon) return;
var cat = loc.category || 'mentioned'; var cat = loc.category || 'mentioned';
var color = catColors[cat] || catColors.mentioned; var color = categoryColors[cat] || '#7b7b7b';
var catClass = 'cat-' + cat; usedCategories[cat] = true;
var icon = L.divIcon({ var popup = '<strong style="color:#E8ECF4;">' + (loc.name || '') + '</strong>';
className: 'pulse-marker ' + catClass, if (loc.country_code) popup += ' <span style="color:#8896AB;font-size:0.8rem;">(' + loc.country_code + ')</span>';
iconSize: [12, 12], popup += '<br><span style="font-size:0.85rem;color:#8896AB;">' + (loc.article_count || 0) + ' Artikel</span>';
iconAnchor: [6, 6]
});
var marker = L.marker([loc.lat, loc.lon], { icon: icon }).addTo(map); L.marker([loc.lat, loc.lon], { icon: pulseIcon(color) })
var label = categoryLabels[cat] || cat; .addTo(map)
marker.bindPopup('<strong>' + loc.name + '</strong><br>' + label + ' (' + (loc.article_count || 0) + ' Artikel)'); .bindPopup(popup);
bounds.push([loc.lat, loc.lon]); 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(map);
if (bounds.length > 0) { if (bounds.length > 0) {
map.fitBounds(bounds, { padding: [30, 30], maxZoom: 7 }); map.fitBounds(bounds, { padding: [30, 30], maxZoom: 7 });
} }
// Fix tile rendering on hidden tab / late load
setTimeout(function () { map.invalidateSize(); }, 500); setTimeout(function () { map.invalidateSize(); }, 500);
} }