Naive Timestamps aus der DB (ohne Timezone-Info) wurden faelschlich als UTC interpretiert (+Z), dann nochmal +1h fuer Europe/Berlin angezeigt. Jetzt wird der korrekte Berlin-Offset (CET/CEST) dynamisch ermittelt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1109 Zeilen
48 KiB
JavaScript
1109 Zeilen
48 KiB
JavaScript
/**
|
|
* AegisSight Lagebild Page - Dark Theme Design Refresh
|
|
* Count-Up, Timeline, Scroll-Reveal, Particles, Live-Feed, Pulse-Markers
|
|
*/
|
|
|
|
/** Feste Zeitzone fuer alle Anzeigen - NIEMALS aendern. */
|
|
var TIMEZONE = 'Europe/Berlin';
|
|
|
|
var Lagebild = {
|
|
data: null,
|
|
allSnapshots: {},
|
|
currentView: null,
|
|
map: null,
|
|
timelineGroups: null,
|
|
|
|
/* ===== Inline SVG Icons ===== */
|
|
icons: {
|
|
clock: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
|
|
fileText: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
|
|
globe: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
|
shieldCheck: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 11 14 15 10"/></svg>',
|
|
externalLink: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>'
|
|
},
|
|
|
|
async init() {
|
|
if (typeof initTranslations === 'function') {
|
|
try { initTranslations(); } catch(e) {}
|
|
}
|
|
this.initScrollProgress();
|
|
this.initParticles();
|
|
try {
|
|
var resp = await fetch('data/current.json?t=' + Date.now());
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
this.data = await resp.json();
|
|
this.currentView = {
|
|
summary: this.data.current_lagebild.summary_markdown,
|
|
sources_json: this.data.current_lagebild.sources_json,
|
|
updated_at: this.data.current_lagebild.updated_at || this.data.generated_at,
|
|
articles: this.data.articles,
|
|
fact_checks: this.data.fact_checks
|
|
};
|
|
this.render();
|
|
this.initTabs();
|
|
this.initLangToggle();
|
|
this.initScrollReveal();
|
|
this.initFloatingCta();
|
|
this.initLiveFeed();
|
|
} catch (e) {
|
|
console.error('Lagebild laden fehlgeschlagen:', e);
|
|
this.showError();
|
|
}
|
|
},
|
|
|
|
render: function() {
|
|
this.renderHero();
|
|
this.renderTimeline();
|
|
this.renderTabBadges();
|
|
this.renderCurrentView();
|
|
},
|
|
|
|
/* ===== SCROLL PROGRESS BAR ===== */
|
|
initScrollProgress: function() {
|
|
var bar = document.getElementById('scroll-progress');
|
|
if (!bar) return;
|
|
window.addEventListener('scroll', function() {
|
|
var scrollTop = window.scrollY;
|
|
var docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
if (docHeight <= 0) return;
|
|
bar.style.width = ((scrollTop / docHeight) * 100) + '%';
|
|
});
|
|
},
|
|
|
|
/* ===== HERO PARTICLES ===== */
|
|
initParticles: function() {
|
|
var canvas = document.getElementById('hero-particles');
|
|
if (!canvas) return;
|
|
var ctx = canvas.getContext('2d');
|
|
var particles = [];
|
|
var count = 35;
|
|
var connectDist = 120;
|
|
|
|
function resize() {
|
|
var hero = canvas.parentElement;
|
|
canvas.width = hero.offsetWidth;
|
|
canvas.height = hero.offsetHeight;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
particles.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * 0.4,
|
|
vy: (Math.random() - 0.5) * 0.4,
|
|
r: Math.random() * 1.5 + 0.5
|
|
});
|
|
}
|
|
|
|
function draw() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw connections
|
|
for (var i = 0; i < particles.length; i++) {
|
|
for (var j = i + 1; j < particles.length; j++) {
|
|
var dx = particles[i].x - particles[j].x;
|
|
var dy = particles[i].y - particles[j].y;
|
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < connectDist) {
|
|
var alpha = (1 - dist / connectDist) * 0.15;
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = 'rgba(200, 168, 81, ' + alpha + ')';
|
|
ctx.lineWidth = 0.5;
|
|
ctx.moveTo(particles[i].x, particles[i].y);
|
|
ctx.lineTo(particles[j].x, particles[j].y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw & move particles
|
|
for (var k = 0; k < particles.length; k++) {
|
|
var p = particles[k];
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
ctx.fillStyle = 'rgba(200, 168, 81, 0.4)';
|
|
ctx.fill();
|
|
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
|
|
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
|
|
}
|
|
|
|
requestAnimationFrame(draw);
|
|
}
|
|
draw();
|
|
},
|
|
|
|
/* ===== LIVE FEED TICKER ===== */
|
|
initLiveFeed: function() {
|
|
var container = document.getElementById('live-feed');
|
|
if (!container) return;
|
|
|
|
var d = this.data;
|
|
var genDate = new Date(this.toUTC(d.generated_at));
|
|
var diffMin = Math.max(1, Math.round((Date.now() - genDate.getTime()) / 60000));
|
|
var diffText = diffMin < 60 ? ('vor ' + diffMin + ' Min') : ('vor ' + Math.round(diffMin / 60) + ' Std');
|
|
|
|
container.innerHTML = '<div class="live-feed-item active">'
|
|
+ '<span class="live-feed-dot"></span>'
|
|
+ '<span>Letzte Aktualisierung: ' + diffText + '</span>'
|
|
+ '</div>';
|
|
},
|
|
|
|
/* ===== HERO ===== */
|
|
renderHero: function() {
|
|
var d = this.data;
|
|
document.getElementById('incident-title').innerHTML =
|
|
this.esc(this.fixUmlauts(d.incident.title)) +
|
|
' <span class="hero-date-info">\u2013 Stand: ' + this.fmtDateOnly(d.generated_at) + ', ' + this.fmtTimeOnly(d.generated_at) + ' Uhr</span>';
|
|
|
|
// Stat Cards (3: Artikel, Quellen, Faktenchecks)
|
|
var statsHtml = '';
|
|
statsHtml += this.statCard(this.icons.fileText, '<span class="count-up" data-target="' + d.incident.article_count + '">0</span>', 'Artikel');
|
|
statsHtml += this.statCard(this.icons.globe, '<span class="count-up" data-target="' + d.incident.source_count + '">0</span>', 'Quellen');
|
|
statsHtml += this.statCard(this.icons.shieldCheck, '<span class="count-up" id="hero-fc-count" data-target="' + d.incident.factcheck_count + '">0</span>', 'Faktenchecks');
|
|
document.getElementById('hero-stats').innerHTML = statsHtml;
|
|
|
|
// Start count-up animations
|
|
var self = this;
|
|
requestAnimationFrame(function() {
|
|
var els = document.querySelectorAll('.count-up');
|
|
for (var i = 0; i < els.length; i++) {
|
|
self.animateCount(els[i], parseInt(els[i].getAttribute('data-target')), 800);
|
|
}
|
|
});
|
|
},
|
|
|
|
statCard: function(icon, value, label) {
|
|
return '<div class="stat-card">' +
|
|
'<div class="stat-card-icon">' + icon + '</div>' +
|
|
'<div class="stat-card-content">' +
|
|
'<span class="stat-card-value">' + value + '</span>' +
|
|
'<span class="stat-card-label">' + label + '</span>' +
|
|
'</div></div>';
|
|
},
|
|
|
|
/* ===== COUNT-UP ANIMATION ===== */
|
|
animateCount: function(element, target, duration) {
|
|
var start = performance.now();
|
|
function update(now) {
|
|
var elapsed = now - start;
|
|
var progress = Math.min(elapsed / duration, 1);
|
|
var eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic
|
|
var current = Math.round(target * eased);
|
|
element.textContent = current.toLocaleString('de-DE');
|
|
if (progress < 1) {
|
|
requestAnimationFrame(update);
|
|
}
|
|
}
|
|
requestAnimationFrame(update);
|
|
},
|
|
|
|
/* ===== TIMELINE STRIP ===== */
|
|
renderTimeline: function() {
|
|
var snaps = this.data.available_snapshots || [];
|
|
var current = {
|
|
id: 'current',
|
|
article_count: this.data.incident.article_count,
|
|
fact_check_count: this.data.incident.factcheck_count,
|
|
created_at: this.data.generated_at
|
|
};
|
|
var all = [current].concat(snaps);
|
|
|
|
// Group by date
|
|
var groups = {};
|
|
for (var i = 0; i < all.length; i++) {
|
|
var s = all[i];
|
|
var dateKey = this.toDateKey(s.created_at);
|
|
if (!groups[dateKey]) groups[dateKey] = [];
|
|
groups[dateKey].push(s);
|
|
}
|
|
|
|
// Sort each group descending (newest first)
|
|
for (var dk in groups) {
|
|
groups[dk].sort(function(a, b) {
|
|
return new Date(Lagebild.toUTC(b.created_at)) - new Date(Lagebild.toUTC(a.created_at));
|
|
});
|
|
}
|
|
|
|
this.timelineGroups = groups;
|
|
var dates = Object.keys(groups).sort();
|
|
var strip = document.getElementById('timeline-strip');
|
|
var h = '';
|
|
|
|
for (var j = 0; j < dates.length; j++) {
|
|
var date = dates[j];
|
|
var daySnaps = groups[date];
|
|
var isActive = (j === dates.length - 1);
|
|
var defaultSnap = isActive ? daySnaps[0] : daySnaps[daySnaps.length - 1];
|
|
var d = new Date(date + 'T12:00:00Z');
|
|
|
|
h += '<button class="timeline-day' + (isActive ? ' active' : '') + '"';
|
|
h += ' data-date="' + date + '"';
|
|
h += ' data-snapshot-id="' + defaultSnap.id + '"';
|
|
if (daySnaps.length > 1) {
|
|
h += ' title="' + daySnaps.length + ' Updates an diesem Tag"';
|
|
}
|
|
h += '>';
|
|
if (isActive) h += '<span class="timeline-dot"></span>';
|
|
h += '<span class="timeline-day-num">' + d.getUTCDate() + '</span>';
|
|
h += '<span class="timeline-day-month">' + d.toLocaleDateString('de-DE', { month: 'short', timeZone: 'UTC' }) + '</span>';
|
|
h += '<span class="timeline-day-count">' + defaultSnap.article_count + '</span>';
|
|
if (daySnaps.length > 1) {
|
|
h += '<span class="timeline-day-updates">' + daySnaps.length + 'x</span>';
|
|
}
|
|
if (isActive) {
|
|
h += '<span class="timeline-day-label">Aktuell</span>';
|
|
}
|
|
h += '</button>';
|
|
}
|
|
|
|
strip.innerHTML = h;
|
|
|
|
// Scroll to active day
|
|
var active = strip.querySelector('.timeline-day.active');
|
|
if (active) {
|
|
setTimeout(function() {
|
|
active.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
}, 150);
|
|
}
|
|
|
|
// Click handler for day buttons
|
|
var self = this;
|
|
strip.addEventListener('click', function(e) {
|
|
var btn = e.target.closest('.timeline-day');
|
|
if (!btn) return;
|
|
|
|
var allDays = strip.querySelectorAll('.timeline-day');
|
|
for (var k = 0; k < allDays.length; k++) allDays[k].classList.remove('active');
|
|
btn.classList.add('active');
|
|
|
|
var dateKey = btn.getAttribute('data-date');
|
|
var snapId = btn.getAttribute('data-snapshot-id');
|
|
|
|
self.showTimelineDropdown(dateKey, snapId);
|
|
|
|
if (snapId === 'current') {
|
|
self.currentView = {
|
|
summary: self.data.current_lagebild.summary_markdown,
|
|
sources_json: self.data.current_lagebild.sources_json,
|
|
updated_at: self.data.current_lagebild.updated_at || self.data.generated_at,
|
|
articles: self.data.articles,
|
|
fact_checks: self.data.fact_checks
|
|
};
|
|
self.renderCurrentView();
|
|
} else {
|
|
self.loadSnapshot(parseInt(snapId));
|
|
}
|
|
});
|
|
|
|
// Click handler for dropdown snapshot items (delegated, set up once)
|
|
var dropdown = document.getElementById('timeline-dropdown');
|
|
dropdown.addEventListener('click', function(e) {
|
|
var item = e.target.closest('.h-timeline-point');
|
|
if (!item) return;
|
|
|
|
var items = dropdown.querySelectorAll('.h-timeline-point');
|
|
for (var k = 0; k < items.length; k++) items[k].classList.remove('active');
|
|
item.classList.add('active');
|
|
|
|
var snapId = item.getAttribute('data-snapshot-id');
|
|
if (snapId === 'current') {
|
|
self.currentView = {
|
|
summary: self.data.current_lagebild.summary_markdown,
|
|
sources_json: self.data.current_lagebild.sources_json,
|
|
updated_at: self.data.current_lagebild.updated_at || self.data.generated_at,
|
|
articles: self.data.articles,
|
|
fact_checks: self.data.fact_checks
|
|
};
|
|
self.renderCurrentView();
|
|
} else {
|
|
self.loadSnapshot(parseInt(snapId));
|
|
}
|
|
});
|
|
|
|
// Show dropdown for newest day by default
|
|
var newestDate = dates[dates.length - 1];
|
|
if (newestDate) {
|
|
this.showTimelineDropdown(newestDate, groups[newestDate][0].id);
|
|
}
|
|
},
|
|
|
|
showTimelineDropdown: function(dateKey, activeSnapId) {
|
|
var dropdown = document.getElementById('timeline-dropdown');
|
|
var snaps = this.timelineGroups[dateKey];
|
|
|
|
if (!snaps || snaps.length === 0) {
|
|
dropdown.classList.remove('open');
|
|
dropdown.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
// Oldest left, newest right
|
|
var ordered = snaps.slice().reverse();
|
|
|
|
var h = '<div class="h-timeline-track">';
|
|
for (var i = 0; i < ordered.length; i++) {
|
|
var snap = ordered[i];
|
|
var isActive = (String(snap.id) === String(activeSnapId));
|
|
h += '<button class="h-timeline-point' + (isActive ? ' active' : '') + '"';
|
|
h += ' data-snapshot-id="' + snap.id + '">';
|
|
h += '<span class="h-timeline-time">' + this.fmtTimeOnly(snap.created_at) + '</span>';
|
|
h += '<span class="h-timeline-dot"></span>';
|
|
h += '<span class="h-timeline-meta">' + snap.article_count + ' Artikel</span>';
|
|
h += '<span class="h-timeline-meta">' + this.getFactChecksAtTime(snap.created_at).length + ' Faktenchecks</span>';
|
|
h += '</button>';
|
|
}
|
|
h += '</div>';
|
|
|
|
dropdown.innerHTML = h;
|
|
dropdown.classList.add('open');
|
|
|
|
// Scroll to active point
|
|
var activePoint = dropdown.querySelector('.h-timeline-point.active');
|
|
if (activePoint) {
|
|
setTimeout(function() {
|
|
activePoint.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
}, 50);
|
|
}
|
|
},
|
|
|
|
toDateKey: function(iso) {
|
|
if (!iso) return '';
|
|
var d = new Date(this.toUTC(iso));
|
|
return d.toLocaleDateString('en-CA', { timeZone: TIMEZONE });
|
|
},
|
|
|
|
/* ===== TAB BADGES ===== */
|
|
renderTabBadges: function() {
|
|
var quellenBadge = document.getElementById('tab-badge-quellen');
|
|
var fcBadge = document.getElementById('tab-badge-faktenchecks');
|
|
if (quellenBadge) quellenBadge.textContent = this.data.incident.source_count;
|
|
if (fcBadge) fcBadge.textContent = this.data.incident.factcheck_count;
|
|
},
|
|
|
|
/* ===== SNAPSHOT LOADING ===== */
|
|
loadSnapshot: async function(id) {
|
|
if (this.allSnapshots[id]) {
|
|
this.currentView = this.allSnapshots[id];
|
|
this.renderCurrentView();
|
|
return;
|
|
}
|
|
try {
|
|
var resp = await fetch('data/snapshot-' + id + '.json');
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
var sd = await resp.json();
|
|
var sj = sd.sources_json;
|
|
if (typeof sj === 'string') { try { sj = JSON.parse(sj); } catch(e) { sj = []; } }
|
|
this.currentView = {
|
|
summary: sd.summary,
|
|
sources_json: sj || [],
|
|
updated_at: sd.created_at,
|
|
articles: this.data.articles,
|
|
fact_checks: this.getFactChecksAtTime(sd.created_at)
|
|
};
|
|
this.allSnapshots[id] = this.currentView;
|
|
this.renderCurrentView();
|
|
} catch (e) { console.error('Snapshot Fehler:', e); }
|
|
},
|
|
|
|
renderCurrentView: function() {
|
|
this.renderSummary();
|
|
this.renderInlineSources();
|
|
this.renderSourcesTab();
|
|
this.renderArticlesTab();
|
|
this.renderFactChecksTab();
|
|
if (document.getElementById('panel-karte').classList.contains('active')) {
|
|
this.renderMap();
|
|
}
|
|
// Faktencheck-Zaehler aktualisieren (Badge + Hero)
|
|
var fcCount = (this.currentView.fact_checks || []).length;
|
|
var fcBadge = document.getElementById('tab-badge-faktenchecks');
|
|
if (fcBadge) fcBadge.textContent = fcCount;
|
|
var heroFc = document.getElementById('hero-fc-count');
|
|
if (heroFc) heroFc.textContent = fcCount.toLocaleString('de-DE');
|
|
},
|
|
|
|
/* ===== TAB: LAGEBILD ===== */
|
|
renderSummary: function() {
|
|
var v = this.currentView;
|
|
document.getElementById('lagebild-timestamp').textContent = this.fmtDT(v.updated_at);
|
|
var md = this.fixUmlauts(v.summary || '');
|
|
var html = this.mdToHtml(md);
|
|
|
|
// Build source lookup for citation links
|
|
var srcMap = {};
|
|
var sources = v.sources_json || [];
|
|
for (var i = 0; i < sources.length; i++) {
|
|
srcMap[sources[i].nr] = sources[i];
|
|
}
|
|
var self = this;
|
|
html = html.replace(/\[(\d+)\]/g, function(match, nr) {
|
|
var src = srcMap[nr];
|
|
if (src && src.url) {
|
|
return '<a class="citation-ref" href="' + self.esc(src.url) + '" target="_blank" rel="noopener" title="' + self.esc(src.name || '') + '">[' + nr + ']</a>';
|
|
}
|
|
return '<a class="citation-ref" title="Quelle ' + nr + '">[' + nr + ']</a>';
|
|
});
|
|
|
|
document.getElementById('summary-content').innerHTML = html;
|
|
},
|
|
|
|
renderInlineSources: function() {
|
|
document.getElementById('inline-sources').innerHTML = '';
|
|
},
|
|
|
|
/* ===== TAB: QUELLEN (Tile Grid) ===== */
|
|
renderSourcesTab: function() {
|
|
var articles = this.currentView.articles || [];
|
|
var container = document.getElementById('sources-grid-container');
|
|
if (!container) return;
|
|
|
|
// Aggregate by source
|
|
var sourceMap = {};
|
|
for (var i = 0; i < articles.length; i++) {
|
|
var a = articles[i];
|
|
var name = a.source || 'Unbekannt';
|
|
if (!sourceMap[name]) sourceMap[name] = { count: 0, articles: [], languages: {}, domain: null };
|
|
sourceMap[name].count++;
|
|
sourceMap[name].articles.push(a);
|
|
var lang = (a.language || '').toUpperCase();
|
|
if (lang) sourceMap[name].languages[lang] = (sourceMap[name].languages[lang] || 0) + 1;
|
|
if (!sourceMap[name].domain && a.source_url) sourceMap[name].domain = this.extractDomain(a.source_url);
|
|
}
|
|
|
|
// Sort by count desc
|
|
var sources = [];
|
|
for (var name in sourceMap) {
|
|
sources.push({ name: name, data: sourceMap[name] });
|
|
}
|
|
sources.sort(function(a, b) { return b.data.count - a.data.count; });
|
|
|
|
// Language totals
|
|
var langTotals = {};
|
|
for (var i = 0; i < articles.length; i++) {
|
|
var lang = (articles[i].language || '').toUpperCase();
|
|
if (lang) langTotals[lang] = (langTotals[lang] || 0) + 1;
|
|
}
|
|
|
|
var h = '';
|
|
|
|
// Header
|
|
h += '<div class="sources-overview-header">';
|
|
h += '<span class="sources-overview-title">' + articles.length + ' Artikel aus ' + sources.length + ' Quellen</span>';
|
|
h += '<div class="sources-lang-chips">';
|
|
var langKeys = Object.keys(langTotals).sort(function(a, b) { return langTotals[b] - langTotals[a]; });
|
|
for (var i = 0; i < langKeys.length; i++) {
|
|
h += '<span class="sources-lang-chip">' + langKeys[i] + ' ' + langTotals[langKeys[i]] + '</span>';
|
|
}
|
|
h += '</div></div>';
|
|
|
|
// Grid
|
|
h += '<div class="sources-grid" id="sources-grid">';
|
|
for (var i = 0; i < sources.length; i++) {
|
|
var s = sources[i];
|
|
var langBadge = Object.keys(s.data.languages).join('/');
|
|
h += '<div class="source-tile" data-source-index="' + i + '">';
|
|
h += '<div class="source-tile-top">';
|
|
if (s.data.domain) {
|
|
h += '<img class="source-tile-favicon" src="https://www.google.com/s2/favicons?domain=' + encodeURIComponent(s.data.domain) + '&sz=16" width="16" height="16" alt="" loading="lazy">';
|
|
}
|
|
h += '<span class="source-tile-name">' + this.esc(s.name) + '</span>';
|
|
h += '</div>';
|
|
h += '<div class="source-tile-bottom">';
|
|
h += '<span class="source-tile-lang">' + langBadge + '</span>';
|
|
h += '<span class="source-tile-count">' + s.data.count + '</span>';
|
|
h += '</div>';
|
|
h += '</div>';
|
|
}
|
|
h += '</div>';
|
|
|
|
container.innerHTML = h;
|
|
this._sourceTiles = sources;
|
|
|
|
// Click handler
|
|
var self = this;
|
|
document.getElementById('sources-grid').addEventListener('click', function(e) {
|
|
var tile = e.target.closest('.source-tile');
|
|
if (!tile) return;
|
|
self.toggleSourceDetail(tile);
|
|
});
|
|
},
|
|
|
|
toggleSourceDetail: function(tile) {
|
|
var grid = document.getElementById('sources-grid');
|
|
var idx = parseInt(tile.getAttribute('data-source-index'));
|
|
var existingPanel = grid.querySelector('.source-detail-panel');
|
|
var wasActive = tile.classList.contains('active');
|
|
|
|
// Remove active from all tiles
|
|
var allTiles = grid.querySelectorAll('.source-tile');
|
|
for (var k = 0; k < allTiles.length; k++) allTiles[k].classList.remove('active');
|
|
|
|
// Remove existing panel
|
|
if (existingPanel) existingPanel.remove();
|
|
|
|
// If same tile was active, just close
|
|
if (wasActive) return;
|
|
|
|
tile.classList.add('active');
|
|
|
|
// Find last tile in same visual row (by offsetTop)
|
|
var clickedTop = tile.offsetTop;
|
|
var lastInRow = tile;
|
|
for (var k = 0; k < allTiles.length; k++) {
|
|
if (allTiles[k].offsetTop === clickedTop) {
|
|
lastInRow = allTiles[k];
|
|
}
|
|
}
|
|
|
|
// Build detail panel
|
|
var src = this._sourceTiles[idx];
|
|
var arts = src.data.articles.slice().sort(function(a, b) {
|
|
var da = new Date(Lagebild.toUTC(a.published_at || a.collected_at || ''));
|
|
var db = new Date(Lagebild.toUTC(b.published_at || b.collected_at || ''));
|
|
return db - da;
|
|
});
|
|
|
|
var h = '<div class="source-detail-panel">';
|
|
h += '<div class="source-detail-header">';
|
|
h += '<span class="source-detail-name">';
|
|
if (src.data.domain) {
|
|
h += '<img class="source-tile-favicon" src="https://www.google.com/s2/favicons?domain=' + encodeURIComponent(src.data.domain) + '&sz=16" width="16" height="16" alt="" loading="lazy"> ';
|
|
}
|
|
h += this.esc(src.name) + '</span>';
|
|
h += '<span class="source-detail-count">' + src.data.count + ' Artikel</span>';
|
|
h += '<button class="source-detail-close" aria-label="Schließen">×</button>';
|
|
h += '</div>';
|
|
h += '<div class="source-detail-articles">';
|
|
for (var j = 0; j < arts.length; j++) {
|
|
var a = arts[j];
|
|
var dt = a.published_at || a.collected_at || '';
|
|
var dObj = dt ? new Date(this.toUTC(dt)) : null;
|
|
var hl = this.fixUmlauts(a.headline_de || a.headline || '');
|
|
h += '<div class="source-detail-article">';
|
|
if (a.source_url) {
|
|
h += '<a href="' + this.esc(a.source_url) + '" target="_blank" rel="noopener" class="source-detail-article-title">' + this.esc(hl) + ' ' + this.icons.externalLink + '</a>';
|
|
} else {
|
|
h += '<span class="source-detail-article-title">' + this.esc(hl) + '</span>';
|
|
}
|
|
if (dObj && !isNaN(dObj.getTime())) {
|
|
h += '<span class="source-detail-article-date">' + dObj.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE }) + '</span>';
|
|
}
|
|
h += '</div>';
|
|
}
|
|
h += '</div></div>';
|
|
|
|
// Insert after last tile in row
|
|
lastInRow.insertAdjacentHTML('afterend', h);
|
|
|
|
// Close button handler
|
|
var panel = grid.querySelector('.source-detail-panel');
|
|
var self = this;
|
|
panel.querySelector('.source-detail-close').addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
panel.remove();
|
|
tile.classList.remove('active');
|
|
});
|
|
|
|
// Scroll into view
|
|
setTimeout(function() {
|
|
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}, 50);
|
|
},
|
|
|
|
renderArticlesTab: function() {},
|
|
|
|
/* ===== TAB: KARTE (Pulse Markers) ===== */
|
|
renderMap: function() {
|
|
if (this.map) { this.map.remove(); this.map = null; }
|
|
this.map = L.map('map-container').setView([33.0, 48.0], 5);
|
|
|
|
// Light map tiles (CartoDB Voyager)
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>',
|
|
maxZoom: 19,
|
|
subdomains: 'abcd'
|
|
}).addTo(this.map);
|
|
|
|
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]
|
|
});
|
|
}
|
|
|
|
// Kategorie-Farben
|
|
var categoryColors = {
|
|
target: '#ef4444',
|
|
retaliation: '#f59e0b',
|
|
response: '#f59e0b',
|
|
actor: '#3b82f6',
|
|
mentioned: '#7b7b7b'
|
|
};
|
|
var categoryLabels = {
|
|
target: 'Angegriffene Ziele',
|
|
retaliation: 'Vergeltung / Eskalation',
|
|
response: 'Reaktion / Gegenmassnahmen',
|
|
actor: 'Strategische Akteure',
|
|
mentioned: 'Erwaehnt'
|
|
};
|
|
|
|
// Locations aus API-Daten laden
|
|
var locs = (this.data && this.data.locations) ? this.data.locations : [];
|
|
|
|
if (locs.length === 0) {
|
|
var emptyDiv = document.createElement('div');
|
|
emptyDiv.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1000;background:#151D2E;padding:20px 30px;border-radius:8px;border:1px solid #1E2D45;color:#8896AB;text-align:center;';
|
|
emptyDiv.innerHTML = 'Keine Standortdaten verfuegbar';
|
|
document.getElementById('map-container').appendChild(emptyDiv);
|
|
}
|
|
|
|
var usedCategories = {};
|
|
for (var i = 0; i < locs.length; i++) {
|
|
var l = locs[i];
|
|
if (!l.lat || !l.lon) continue;
|
|
var cat = l.category || 'mentioned';
|
|
var color = categoryColors[cat] || '#7b7b7b';
|
|
usedCategories[cat] = true;
|
|
var popupText = '<strong style="color:#E8ECF4;">' + (l.name || '') + '</strong>';
|
|
if (l.country_code) popupText += ' <span style="color:#8896AB;font-size:0.8rem;">(' + l.country_code + ')</span>';
|
|
popupText += '<br><span style="font-size:0.85rem;color:#8896AB;">' + (l.article_count || 0) + ' Artikel</span>';
|
|
L.marker([l.lat, l.lon], { icon: pulseIcon(color) })
|
|
.addTo(this.map)
|
|
.bindPopup(popupText);
|
|
}
|
|
|
|
// Dark legend (dynamisch)
|
|
var legend = L.control({ position: 'bottomright' });
|
|
legend.onAdd = function() {
|
|
var div = L.DomUtil.create('div', 'map-legend');
|
|
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>';
|
|
['target', 'response', 'retaliation', 'actor', 'mentioned'].forEach(function(cat) {
|
|
if (usedCategories[cat]) {
|
|
html += '<span style="color:' + categoryColors[cat] + ';">●</span> ' + categoryLabels[cat] + '<br>';
|
|
}
|
|
});
|
|
div.innerHTML = html;
|
|
return div;
|
|
};
|
|
legend.addTo(this.map);
|
|
|
|
// Dark popup styling
|
|
if (!document.getElementById('leaflet-dark-style')) {
|
|
var style = document.createElement('style');
|
|
style.id = 'leaflet-dark-style';
|
|
style.textContent = '.lagebild-page .leaflet-popup-content-wrapper{background:#151D2E;color:#E8ECF4;border:1px solid #1E2D45;border-radius:4px;box-shadow:0 4px 16px rgba(0,0,0,0.4);}.lagebild-page .leaflet-popup-tip{background:#151D2E;}';
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
setTimeout(function() { if (Lagebild.map) Lagebild.map.invalidateSize(); }, 300);
|
|
},
|
|
|
|
/* ===== TAB: FAKTENCHECKS ===== */
|
|
/* ===== Factcheck Icons (from real Monitor) ===== */
|
|
fcIcons: {
|
|
confirmed: '✓',
|
|
unconfirmed: '?',
|
|
contradicted: '✗',
|
|
developing: '↻',
|
|
established: '✓',
|
|
disputed: '⚠',
|
|
'false': '✗',
|
|
unverified: '?'
|
|
},
|
|
|
|
fcLabels: {
|
|
confirmed: 'Bestätigt',
|
|
unconfirmed: 'Unbestätigt',
|
|
contradicted: 'Widerlegt',
|
|
developing: 'Unklar',
|
|
established: 'Gesichert',
|
|
disputed: 'Umstritten',
|
|
'false': 'Falsch',
|
|
unverified: 'Nicht verifiziert'
|
|
},
|
|
|
|
renderFactChecksTab: function() {
|
|
var checks = this.currentView.fact_checks || [];
|
|
if (!checks.length) {
|
|
document.getElementById('factchecks-content').innerHTML = '<p style="color:#8896AB">Keine Faktenchecks verfügbar.</p>';
|
|
return;
|
|
}
|
|
|
|
// Count stats
|
|
var stats = { confirmed: 0, unconfirmed: 0, contradicted: 0, developing: 0, established: 0, disputed: 0 };
|
|
for (var k = 0; k < checks.length; k++) {
|
|
var st = checks[k].status || 'developing';
|
|
if (stats[st] !== undefined) stats[st]++;
|
|
}
|
|
|
|
var confirmedTotal = stats.confirmed + stats.established;
|
|
var openTotal = stats.unconfirmed + stats.developing;
|
|
var contradictedTotal = stats.contradicted + stats.disputed;
|
|
|
|
// Stat cards (clickable filters)
|
|
var h = '<div class="fc-stats">';
|
|
h += '<button class="fc-stat active" data-filter="all"><span class="fc-stat-num">' + checks.length + '</span><span class="fc-stat-label">Gesamt</span></button>';
|
|
h += '<button class="fc-stat confirmed" data-filter="confirmed"><span class="fc-stat-num">' + confirmedTotal + '</span><span class="fc-stat-label">Bestätigt</span></button>';
|
|
h += '<button class="fc-stat unconfirmed" data-filter="unconfirmed"><span class="fc-stat-num">' + openTotal + '</span><span class="fc-stat-label">Offen</span></button>';
|
|
if (contradictedTotal > 0)
|
|
h += '<button class="fc-stat contradicted" data-filter="contradicted"><span class="fc-stat-num">' + contradictedTotal + '</span><span class="fc-stat-label">Widerlegt</span></button>';
|
|
h += '</div>';
|
|
|
|
// Sort: status_history first, then by sources_count
|
|
checks = checks.slice().sort(function(a, b) {
|
|
var aH = (a.status_history || []).length;
|
|
var bH = (b.status_history || []).length;
|
|
if (bH !== aH) return bH - aH;
|
|
return (b.sources_count || 0) - (a.sources_count || 0);
|
|
});
|
|
|
|
// Compact accordion list
|
|
h += '<div class="fc-list" id="fc-list">';
|
|
for (var i = 0; i < checks.length; i++) {
|
|
var fc = checks[i];
|
|
var status = fc.status || 'developing';
|
|
var filterGroup = 'all';
|
|
if (status === 'confirmed' || status === 'established') filterGroup = 'confirmed';
|
|
else if (status === 'unconfirmed' || status === 'developing') filterGroup = 'unconfirmed';
|
|
else if (status === 'contradicted' || status === 'disputed') filterGroup = 'contradicted';
|
|
|
|
var hasProg = fc.status_history && fc.status_history.length > 1;
|
|
var icon = this.fcIcons[status] || '?';
|
|
var label = this.fcLabels[status] || status;
|
|
|
|
h += '<div class="fc-row" data-status-group="' + filterGroup + '">';
|
|
h += '<div class="fc-row-header" data-fc-index="' + i + '">';
|
|
h += '<span class="fc-icon ' + status + '" title="' + this.esc(label) + '">' + icon + '</span>';
|
|
h += '<span class="fc-row-claim">' + this.esc(this.fixUmlauts(fc.claim || '')) + '</span>';
|
|
h += '<span class="fc-row-sources">' + (fc.sources_count || 0) + '</span>';
|
|
if (hasProg) h += '<span class="fc-row-history-dot" title="Statusverlauf vorhanden"></span>';
|
|
h += '<span class="fc-row-chevron">▸</span>';
|
|
h += '</div>';
|
|
|
|
// Expandable detail
|
|
h += '<div class="fc-row-detail">';
|
|
h += '<div class="fc-row-detail-inner">';
|
|
h += '<div class="fc-detail-status"><span class="fc-icon ' + status + '">' + icon + '</span> <strong>' + label + '</strong> – ' + (fc.sources_count || 0) + ' unabhängige Quellen</div>';
|
|
|
|
if (fc.evidence) {
|
|
var ev = this.fixUmlauts(fc.evidence);
|
|
ev = this.esc(ev).replace(/(https?:\/\/[^\s,)]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>');
|
|
h += '<div class="fc-detail-evidence"><strong>Evidenz:</strong> ' + ev + '</div>';
|
|
}
|
|
|
|
if (hasProg) {
|
|
h += '<div class="fc-detail-progression">';
|
|
h += '<span class="fc-detail-prog-label">Verlauf:</span>';
|
|
for (var j = 0; j < fc.status_history.length; j++) {
|
|
var step = fc.status_history[j];
|
|
if (j > 0) h += '<span class="progression-arrow">→</span>';
|
|
h += '<span class="progression-step">';
|
|
h += '<span class="fc-icon small ' + step.status + '">' + (this.fcIcons[step.status] || '?') + '</span>';
|
|
if (step.at) h += '<span class="progression-time">' + this.fmtShort(step.at) + '</span>';
|
|
h += '</span>';
|
|
}
|
|
h += '</div>';
|
|
}
|
|
h += '</div></div>';
|
|
h += '</div>';
|
|
}
|
|
h += '</div>';
|
|
|
|
document.getElementById('factchecks-content').innerHTML = h;
|
|
|
|
// Filter click handler
|
|
var statBtns = document.querySelectorAll('.fc-stat');
|
|
statBtns.forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
for (var k = 0; k < statBtns.length; k++) statBtns[k].classList.remove('active');
|
|
btn.classList.add('active');
|
|
var filter = btn.getAttribute('data-filter');
|
|
var rows = document.querySelectorAll('.fc-row');
|
|
for (var k = 0; k < rows.length; k++) {
|
|
if (filter === 'all' || rows[k].getAttribute('data-status-group') === filter) {
|
|
rows[k].style.display = '';
|
|
} else {
|
|
rows[k].style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Accordion click handler
|
|
document.getElementById('fc-list').addEventListener('click', function(e) {
|
|
var header = e.target.closest('.fc-row-header');
|
|
if (!header) return;
|
|
var row = header.closest('.fc-row');
|
|
var wasOpen = row.classList.contains('open');
|
|
|
|
// Close all open rows
|
|
var allRows = document.querySelectorAll('.fc-row.open');
|
|
for (var k = 0; k < allRows.length; k++) allRows[k].classList.remove('open');
|
|
|
|
// Toggle clicked row
|
|
if (!wasOpen) {
|
|
row.classList.add('open');
|
|
var detail = row.querySelector('.fc-row-detail');
|
|
if (detail) {
|
|
setTimeout(function() {
|
|
detail.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}, 50);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/* ===== TABS ===== */
|
|
initTabs: function() {
|
|
var btns = document.querySelectorAll('.tab-btn');
|
|
var self = this;
|
|
for (var i = 0; i < btns.length; i++) {
|
|
btns[i].addEventListener('click', function() {
|
|
var tab = this.getAttribute('data-tab');
|
|
for (var j = 0; j < btns.length; j++) btns[j].classList.remove('active');
|
|
this.classList.add('active');
|
|
var panels = document.querySelectorAll('.tab-panel');
|
|
for (var j = 0; j < panels.length; j++) panels[j].classList.remove('active');
|
|
var activePanel = document.getElementById('panel-' + tab);
|
|
activePanel.classList.add('active');
|
|
|
|
// Trigger reveal for cards in newly active panel
|
|
var revealCards = activePanel.querySelectorAll('.reveal:not(.revealed)');
|
|
for (var k = 0; k < revealCards.length; k++) {
|
|
revealCards[k].classList.add('revealed');
|
|
}
|
|
|
|
if (tab === 'karte') self.renderMap();
|
|
});
|
|
}
|
|
},
|
|
|
|
initLangToggle: function() {
|
|
var btn = document.querySelector('.lang-toggle');
|
|
if (!btn) return;
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
if (typeof switchLanguage === 'function') {
|
|
var cur = (typeof getCurrentLanguage === 'function') ? getCurrentLanguage() : 'de';
|
|
switchLanguage(cur === 'de' ? 'en' : 'de');
|
|
}
|
|
});
|
|
},
|
|
|
|
/* ===== FLOATING CTA ===== */
|
|
initFloatingCta: function() {
|
|
var cta = document.createElement('div');
|
|
cta.className = 'floating-cta';
|
|
cta.innerHTML = '<span class="floating-cta-text">AegisSight Monitor f\u00fcr Ihre Organisation</span>'
|
|
+ '<a href="mailto:info@aegis-sight.de" class="floating-cta-btn">Kontakt aufnehmen \u2192</a>'
|
|
+ '<button class="floating-cta-close" aria-label="Schlie\u00dfen">×</button>';
|
|
document.body.appendChild(cta);
|
|
|
|
// Show after scrolling past hero
|
|
var shown = false;
|
|
window.addEventListener('scroll', function() {
|
|
if (shown) return;
|
|
if (window.scrollY > 400) {
|
|
cta.classList.add('visible');
|
|
shown = true;
|
|
}
|
|
});
|
|
|
|
// Close button
|
|
cta.querySelector('.floating-cta-close').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
cta.classList.add('dismissed');
|
|
setTimeout(function() { cta.classList.remove('dismissed'); }, 60000);
|
|
});
|
|
},
|
|
|
|
/* ===== SCROLL REVEAL ===== */
|
|
initScrollReveal: function() {
|
|
var cards = document.querySelectorAll('.content-card, .lagebild-cta');
|
|
if (!('IntersectionObserver' in window)) {
|
|
for (var i = 0; i < cards.length; i++) cards[i].classList.add('revealed');
|
|
return;
|
|
}
|
|
var observer = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(entry) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('revealed');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.1 });
|
|
|
|
for (var i = 0; i < cards.length; i++) {
|
|
cards[i].classList.add('reveal');
|
|
// Immediately reveal cards in the active (visible) tab panel
|
|
var panel = cards[i].closest('.tab-panel');
|
|
if (!panel || panel.classList.contains('active')) {
|
|
cards[i].classList.add('revealed');
|
|
} else {
|
|
observer.observe(cards[i]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/* ===== FAKTENCHECK-FILTER NACH ZEITRAUM ===== */
|
|
getFactChecksAtTime: function(cutoff) {
|
|
var allFCs = this.data.fact_checks || [];
|
|
if (!cutoff) return allFCs;
|
|
var cutoffTime = new Date(this.toUTC(cutoff)).getTime();
|
|
var filtered = [];
|
|
for (var i = 0; i < allFCs.length; i++) {
|
|
var fc = allFCs[i];
|
|
var hist = fc.status_history || [];
|
|
if (!hist.length) continue;
|
|
// Erster Eintrag = Erstellungszeitpunkt des Faktenchecks
|
|
var firstAt = new Date(this.toUTC(hist[0].at)).getTime();
|
|
if (firstAt > cutoffTime) continue;
|
|
// Status zum gewaehlten Zeitpunkt ermitteln
|
|
var statusAtTime = hist[0].status;
|
|
for (var j = 0; j < hist.length; j++) {
|
|
var stepTime = new Date(this.toUTC(hist[j].at)).getTime();
|
|
if (stepTime <= cutoffTime) {
|
|
statusAtTime = hist[j].status;
|
|
}
|
|
}
|
|
// Kopie mit angepasstem Status und getrimmter History
|
|
var copy = {};
|
|
for (var key in fc) { if (fc.hasOwnProperty(key)) copy[key] = fc[key]; }
|
|
copy.status = statusAtTime;
|
|
copy.status_history = [];
|
|
for (var j = 0; j < hist.length; j++) {
|
|
if (new Date(this.toUTC(hist[j].at)).getTime() <= cutoffTime) {
|
|
copy.status_history.push(hist[j]);
|
|
}
|
|
}
|
|
filtered.push(copy);
|
|
}
|
|
return filtered;
|
|
},
|
|
|
|
/* ===== HILFSFUNKTIONEN ===== */
|
|
extractDomain: function(url) {
|
|
if (!url) return null;
|
|
try { return new URL(url).hostname; } catch(e) { return null; }
|
|
},
|
|
|
|
fixUmlauts: function(text) {
|
|
if (!text) return text;
|
|
var skip = ['Israel','Israelis','Jazeera','Euronews','Reuters','Februar',
|
|
'Juffair','abgefeuert','Feindseligkeiten','Gegenschlag','neuesten',
|
|
'auszuweiten','befeuert','feuerte','Feuer','feuer','neue','neuen',
|
|
'neuer','neues','Neue','Aero','aero','Manoeuvre','Dauerfeuer'];
|
|
var ph = []; var c = 0;
|
|
for (var i = 0; i < skip.length; i++) {
|
|
var re = new RegExp('\\b' + skip[i] + '\\b', 'g');
|
|
text = text.replace(re, function(m) { ph.push(m); return '##S' + (c++) + '##'; });
|
|
}
|
|
text = text.replace(/ae/g, '\u00e4').replace(/Ae/g, '\u00c4');
|
|
text = text.replace(/oe/g, '\u00f6').replace(/Oe/g, '\u00d6');
|
|
text = text.replace(/ue/g, '\u00fc').replace(/Ue/g, '\u00dc');
|
|
text = text.replace(/##S(\d+)##/g, function(m, idx) { return ph[parseInt(idx)]; });
|
|
return text;
|
|
},
|
|
|
|
stLabel: function(s) {
|
|
return { confirmed: 'Best\u00e4tigt', unconfirmed: 'Unbest\u00e4tigt', established: 'Gesichert',
|
|
unverified: 'Nicht verifiziert', contradicted: 'Widerlegt', disputed: 'Umstritten',
|
|
developing: 'In Entwicklung', 'false': 'Falsch' }[s] || s;
|
|
},
|
|
|
|
mdToHtml: function(md) {
|
|
if (!md) return '';
|
|
var lines = md.split('\n'), html = '', inList = false;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var l = lines[i];
|
|
if (/^### (.+)$/.test(l)) { if (inList) { html += '</ul>'; inList = false; } html += '<h3>' + l.replace(/^### /, '') + '</h3>'; continue; }
|
|
if (/^## (.+)$/.test(l)) { if (inList) { html += '</ul>'; inList = false; } html += '<h2>' + l.replace(/^## /, '') + '</h2>'; continue; }
|
|
if (/^[-*] (.+)$/.test(l)) { if (!inList) { html += '<ul>'; inList = true; } html += '<li>' + l.replace(/^[-*] /, '') + '</li>'; continue; }
|
|
if (inList) { html += '</ul>'; inList = false; }
|
|
if (l.trim() === '') continue;
|
|
html += '<p>' + l + '</p>';
|
|
}
|
|
if (inList) html += '</ul>';
|
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
return html;
|
|
},
|
|
|
|
esc: function(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; },
|
|
|
|
toUTC: function(s) {
|
|
if (!s) return s;
|
|
s = String(s).trim();
|
|
if (/[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return s;
|
|
// Naive Timestamps aus der DB sind Europe/Berlin Lokalzeit.
|
|
// Korrekten UTC-Offset ermitteln (CET +01:00 / CEST +02:00).
|
|
var iso = s.replace(' ', 'T');
|
|
var temp = new Date(iso + 'Z');
|
|
var utc = new Date(temp.toLocaleString('en-US', { timeZone: 'UTC' }));
|
|
var local = new Date(temp.toLocaleString('en-US', { timeZone: TIMEZONE }));
|
|
var offMin = (local - utc) / 60000;
|
|
var sign = offMin >= 0 ? '+' : '-';
|
|
var h = String(Math.floor(Math.abs(offMin) / 60)).padStart(2, '0');
|
|
var m = String(Math.abs(offMin) % 60).padStart(2, '0');
|
|
return iso + sign + h + ':' + m;
|
|
},
|
|
|
|
fmtDT: function(iso) {
|
|
if (!iso) return '';
|
|
try {
|
|
var d = new Date(this.toUTC(iso));
|
|
if (isNaN(d.getTime())) return iso;
|
|
var opts = { timeZone: TIMEZONE, weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false };
|
|
var parts = new Intl.DateTimeFormat('de-DE', opts).formatToParts(d);
|
|
var p = {};
|
|
parts.forEach(function(x) { p[x.type] = x.value; });
|
|
return p.weekday + ', ' + p.day + '. ' + p.month + ' ' + p.year
|
|
+ ' um ' + p.hour + ':' + p.minute + ' Uhr';
|
|
} catch(e) { return iso; }
|
|
},
|
|
|
|
fmtDateOnly: function(iso) {
|
|
if (!iso) return '';
|
|
try {
|
|
var d = new Date(this.toUTC(iso));
|
|
if (isNaN(d.getTime())) return iso;
|
|
return d.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric', timeZone: TIMEZONE });
|
|
} catch(e) { return iso; }
|
|
},
|
|
|
|
fmtTimeOnly: function(iso) {
|
|
if (!iso) return '';
|
|
try {
|
|
var d = new Date(this.toUTC(iso));
|
|
if (isNaN(d.getTime())) return iso;
|
|
return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
|
|
} catch(e) { return iso; }
|
|
},
|
|
|
|
fmtShort: function(iso) {
|
|
if (!iso) return '';
|
|
try { return new Date(this.toUTC(iso)).toLocaleDateString('de-DE', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE }); }
|
|
catch(e) { return iso; }
|
|
},
|
|
|
|
showError: function() {
|
|
document.getElementById('summary-content').innerHTML =
|
|
'<div class="lagebild-error"><p>Das Lagebild konnte nicht geladen werden. Bitte versuchen Sie es sp\u00e4ter erneut.</p></div>';
|
|
}
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { Lagebild.init(); });
|