Tutorial: Umfangreiche visuelle Korrekturen

Step 17 (Lagebild): Quellenverweise [1]-[5] jetzt in Gold (var(--accent))
wie echte Links im Original.

Step 20 (Faktencheck-Detail): Scrollt beim Betreten ans Ende der
Faktencheck-Liste um auch unbestaetigte/widerlegte Eintraege zu zeigen,
scrollt dann zurueck nach oben.

Step 22 (Timeline): Komplett ueberarbeitete Demo-Timeline mit echtem
Achsen-basiertem Layout (ht-axis, ht-points, ht-day-markers, ht-detail-panel)
statt einfacher Listenansicht. Entspricht dem Original-Rendering.

Step 23 (Karte): Startet jetzt bei Europa-Zoom (Zoom 5), dann animierter
flyTo auf Hamburg (Zoom 13, 2.5s Dauer). Marker und Legende wie bisher.

Step 25 (Resize): Stellt exakte Originalgroesse nach Demo wieder her,
entfernt CSS-Werte erst nach 100ms damit GridStack uebernehmen kann.

Step 27+30 (Bubble-Position): Post-Render-Check verhindert dass Bubbles
unter den Viewport-Rand rutschen, verschiebt sie automatisch nach oben.

Layout: Tutorial erzwingt Standard-Layout beim Start (DEFAULT_LAYOUT),
stellt das vom Nutzer angepasste Layout nach Tutorial-Ende wieder her.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-16 16:37:57 +01:00
Ursprung 4aaf0c1d5e
Commit 4a1ab67703
2 geänderte Dateien mit 109 neuen und 33 gelöschten Zeilen

Datei anzeigen

@@ -764,7 +764,7 @@
<script src="/static/js/api_network.js?v=20260316a"></script> <script src="/static/js/api_network.js?v=20260316a"></script>
<script src="/static/js/network-graph.js?v=20260316a"></script> <script src="/static/js/network-graph.js?v=20260316a"></script>
<script src="/static/js/app_network.js?v=20260316a"></script> <script src="/static/js/app_network.js?v=20260316a"></script>
<script src="/static/js/tutorial.js?v=20260316n"></script> <script src="/static/js/tutorial.js?v=20260316o"></script>
<script src="/static/js/chat.js?v=20260316f"></script> <script src="/static/js/chat.js?v=20260316f"></script>
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script> <script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>

Datei anzeigen

@@ -52,16 +52,16 @@ const Tutorial = {
_DEMO_SUMMARY: '<p>Am 16. März 2026 kam es im Hamburger Hafen zu einer Explosion ' _DEMO_SUMMARY: '<p>Am 16. März 2026 kam es im Hamburger Hafen zu einer Explosion '
+ 'im Containerterminal Burchardkai ' + 'im Containerterminal Burchardkai '
+ '<span class="source-ref" data-index="1" title="dpa">[1]</span> ' + '<span class="source-ref" data-index="1" title="dpa" style="color:var(--accent);cursor:pointer;">[1]</span> '
+ '<span class="source-ref" data-index="2" title="Reuters">[2]</span>. ' + '<span class="source-ref" data-index="2" title="Reuters" style="color:var(--accent);cursor:pointer;">[2]</span>. '
+ 'Die Detonation ereignete sich gegen 06:45 Uhr in einem Gefahrgutbereich. ' + 'Die Detonation ereignete sich gegen 06:45 Uhr in einem Gefahrgutbereich. '
+ 'Mindestens 12 Personen wurden verletzt ' + 'Mindestens 12 Personen wurden verletzt '
+ '<span class="source-ref" data-index="3" title="NDR">[3]</span>.</p>' + '<span class="source-ref" data-index="3" title="NDR" style="color:var(--accent);cursor:pointer;">[3]</span>.</p>'
+ '<p>Die Feuerwehr ist mit einem Großaufgebot vor Ort. Der Hafenbetrieb im betroffenen Terminal wurde ' + '<p>Die Feuerwehr ist mit einem Großaufgebot vor Ort. Der Hafenbetrieb im betroffenen Terminal wurde '
+ 'vorübergehend eingestellt ' + 'vorübergehend eingestellt '
+ '<span class="source-ref" data-index="4" title="HPA">[4]</span>. ' + '<span class="source-ref" data-index="4" title="HPA" style="color:var(--accent);cursor:pointer;">[4]</span>. '
+ 'Die Ursache ist noch unklar ' + 'Die Ursache ist noch unklar '
+ '<span class="source-ref" data-index="5" title="Hamburger Abendblatt">[5]</span>.</p>', + '<span class="source-ref" data-index="5" title="Hamburger Abendblatt" style="color:var(--accent);cursor:pointer;">[5]</span>.</p>',
_DEMO_FACTCHECKS: [ _DEMO_FACTCHECKS: [
{ status: 'confirmed', icon: '&#10003;', claim: 'Eine Explosion ereignete sich am 16.03.2026 gegen 06:45 Uhr im Hamburger Hafen.', sources: 5 }, { status: 'confirmed', icon: '&#10003;', claim: 'Eine Explosion ereignete sich am 16.03.2026 gegen 06:45 Uhr im Hamburger Hafen.', sources: 5 },
@@ -84,6 +84,55 @@ const Tutorial = {
{ time: '09:10', title: 'Gefahrgut-Spezialisten am Einsatzort eingetroffen', source: 'tagesschau.de', type: 'article' }, { time: '09:10', title: 'Gefahrgut-Spezialisten am Einsatzort eingetroffen', source: 'tagesschau.de', type: 'article' },
], ],
_buildDemoTimelineHTML() {
// Achsen-basierte Timeline wie im Original
var entries = this._DEMO_TIMELINE;
var times = ['06:45','07:02','07:15','07:30','08:00','08:22','09:10'];
var startMin = 6*60+45;
var endMin = 9*60+10;
var range = endMin - startMin;
var html = '<div class="ht-axis">';
// Datums-Marker
html += '<div class="ht-day-markers">';
html += '<div class="ht-day-marker" style="left:0%;"><div class="ht-day-marker-label">16. Mär.</div><div class="ht-day-marker-line"></div></div>';
html += '</div>';
// Punkte
html += '<div class="ht-points">';
entries.forEach(function(e, i) {
var parts = e.time.split(':');
var min = parseInt(parts[0])*60 + parseInt(parts[1]);
var pos = ((min - startMin) / range) * 92 + 4;
var isSnapshot = e.type === 'snapshot';
var cls = 'ht-point' + (isSnapshot ? ' ht-snapshot-point' : '') + (i === 0 ? ' active' : '');
var size = isSnapshot ? 14 : 10;
html += '<div class="' + cls + '" style="left:' + pos + '%;width:' + size + 'px;height:' + size + 'px;" data-idx="' + i + '">';
html += '<div class="ht-tooltip">' + e.time + ': ' + e.title + '</div>';
html += '</div>';
});
html += '</div>';
// Achsenlinie
html += '<div class="ht-axis-line"></div>';
// Labels
html += '<div class="ht-axis-labels">';
['07:00','08:00','09:00'].forEach(function(t) {
var parts = t.split(':');
var min = parseInt(parts[0])*60;
var pos = ((min - startMin) / range) * 92 + 4;
html += '<div class="ht-axis-label" style="left:' + pos + '%;">' + t + '</div>';
});
html += '</div>';
html += '</div>';
// Detail-Panel fuer aktiven Punkt
html += '<div class="ht-detail-panel">';
var first = entries[0];
html += '<div class="ht-detail-header"><span class="ht-detail-time">16.03.2026, ' + first.time + '</span><span class="ht-detail-count">1 Eintrag</span></div>';
html += '<div class="ht-detail-entries">';
html += '<div class="ht-detail-entry"><div class="ht-detail-entry-source">' + first.source + '</div><div class="ht-detail-entry-title">' + first.title + '</div></div>';
html += '</div></div>';
return html;
},
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Demo-View injizieren / entfernen // Demo-View injizieren / entfernen
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -141,6 +190,11 @@ const Tutorial = {
if (typeof LayoutManager !== 'undefined' && !LayoutManager._initialized) { if (typeof LayoutManager !== 'undefined' && !LayoutManager._initialized) {
LayoutManager.init(); LayoutManager.init();
} }
// Aktuelles Layout sichern und Standard-Layout erzwingen
if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) {
this._savedState.savedLayout = LayoutManager._grid.save(false);
LayoutManager._applyLayout(LayoutManager.DEFAULT_LAYOUT);
}
// GridStack Resize triggern damit Kacheln korrekt positioniert werden // GridStack Resize triggern damit Kacheln korrekt positioniert werden
if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) { if (typeof LayoutManager !== 'undefined' && LayoutManager._grid) {
LayoutManager._grid.engine.nodes.forEach(function(n) { LayoutManager._grid.engine.nodes.forEach(function(n) {
@@ -191,23 +245,13 @@ const Tutorial = {
var sourceStats = document.getElementById('source-overview-header-stats'); var sourceStats = document.getElementById('source-overview-header-stats');
if (sourceStats) sourceStats.innerHTML = this._DEMO_SOURCES_STATS; if (sourceStats) sourceStats.innerHTML = this._DEMO_SOURCES_STATS;
// Timeline // Timeline (Achsen-basiert wie im Original)
var timeline = document.getElementById('timeline'); var timeline = document.getElementById('timeline');
if (timeline) { if (timeline) {
var tlHtml = '<div class="ht-timeline">'; timeline.innerHTML = this._buildDemoTimelineHTML();
this._DEMO_TIMELINE.forEach(function(ev) {
var cls = ev.type === 'snapshot' ? 'ht-entry ht-snapshot' : 'ht-entry';
tlHtml += '<div class="' + cls + '">'
+ '<div class="ht-time">' + ev.time + '</div>'
+ '<div class="ht-dot"></div>'
+ '<div class="ht-content">'
+ '<div class="ht-title">' + ev.title + '</div>'
+ '<div class="ht-source">' + ev.source + '</div>'
+ '</div></div>';
});
tlHtml += '</div>';
timeline.innerHTML = tlHtml;
} }
var articleCount = document.getElementById('article-count');
if (articleCount) articleCount.textContent = '7 Einträge';
// Karte: Stats setzen + Platzhalter anzeigen, echte Map erst im Vollbild-Step // Karte: Stats setzen + Platzhalter anzeigen, echte Map erst im Vollbild-Step
var mapEmpty = document.getElementById('map-empty'); var mapEmpty = document.getElementById('map-empty');
@@ -288,6 +332,11 @@ const Tutorial = {
var ts = document.getElementById('lagebild-timestamp'); var ts = document.getElementById('lagebild-timestamp');
if (ts) ts.textContent = ''; if (ts) ts.textContent = '';
// Gespeichertes Layout wiederherstellen
if (typeof LayoutManager !== 'undefined' && LayoutManager._grid && s.savedLayout) {
LayoutManager._applyLayout(s.savedLayout);
}
this._savedState = null; this._savedState = null;
}, },
@@ -825,6 +874,12 @@ const Tutorial = {
+ 'z.B. nur unbestätigte Meldungen anzeigen.', + 'z.B. nur unbestätigte Meldungen anzeigen.',
position: 'left', position: 'left',
onEnter: function() { onEnter: function() {
// Zum nicht-verifizierten Eintrag scrollen
var fcList = document.getElementById('factcheck-list');
if (fcList) fcList.scrollTo({ top: fcList.scrollHeight, behavior: 'smooth' });
Tutorial._stepTimeout(function() {
if (fcList) fcList.scrollTo({ top: 0, behavior: 'smooth' });
}, 2000);
var item = document.querySelector('.factcheck-item[data-fc-status="confirmed"]'); var item = document.querySelector('.factcheck-item[data-fc-status="confirmed"]');
if (item) item.classList.add('tutorial-sub-highlight'); if (item) item.classList.add('tutorial-sub-highlight');
Tutorial._cleanupFns.push(function() { Tutorial._cleanupFns.push(function() {
@@ -1276,6 +1331,18 @@ const Tutorial = {
// Sichtbar machen // Sichtbar machen
bubble.classList.add('visible'); bubble.classList.add('visible');
// Sicherheitscheck: Bubble nicht unter Viewport-Rand
var self = this;
requestAnimationFrame(function() {
var bRect = bubble.getBoundingClientRect();
var vHeight = window.innerHeight;
if (bRect.bottom > vHeight - 8) {
var shift = bRect.bottom - vHeight + 16;
var currentTop = parseFloat(bubble.style.top) || 0;
bubble.style.top = (currentTop - shift) + 'px';
}
});
}, },
_positionBubble(step) { _positionBubble(step) {
@@ -1418,16 +1485,15 @@ const Tutorial = {
var fsStats = document.getElementById('map-fullscreen-stats'); var fsStats = document.getElementById('map-fullscreen-stats');
if (!overlay || !fsContainer) return; if (!overlay || !fsContainer) return;
// Overlay anzeigen (\u00fcber Tutorial-Z-Index) // Overlay anzeigen
overlay.classList.add('active'); overlay.classList.add('active');
overlay.style.zIndex = '9002'; overlay.style.zIndex = '9002';
if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel'; if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel';
// Alte Demo-Map entfernen falls vorhanden // Alte Demo-Map entfernen
this._destroyDemoMap(); this._destroyDemoMap();
// Neue Map im Fullscreen-Container erstellen // Map im Fullscreen-Container
fsContainer.innerHTML = ''; fsContainer.innerHTML = '';
var mapDiv = document.createElement('div'); var mapDiv = document.createElement('div');
mapDiv.id = 'tutorial-fs-map'; mapDiv.id = 'tutorial-fs-map';
@@ -1435,10 +1501,11 @@ const Tutorial = {
fsContainer.appendChild(mapDiv); fsContainer.appendChild(mapDiv);
var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light'; var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light';
// Start weit herausgezoomt (Europa)
this._demoMap = L.map(mapDiv, { this._demoMap = L.map(mapDiv, {
zoomControl: true, zoomControl: true,
attributionControl: true, attributionControl: true,
}).setView([53.545, 9.98], 13); }).setView([51.0, 10.0], 5);
var tileUrl = isDark var tileUrl = isDark
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' ? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
@@ -1448,7 +1515,7 @@ const Tutorial = {
maxZoom: 19, maxZoom: 19,
}).addTo(this._demoMap); }).addTo(this._demoMap);
// Marker // Marker (aber noch nicht sichtbar bei Zoom 5)
var locations = [ var locations = [
{ lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' }, { lat: 53.5325, lon: 9.9275, name: 'Burchardkai Terminal', articles: 6, cat: 'primary' },
{ lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' }, { lat: 53.5460, lon: 9.9690, name: 'Hamburg Innenstadt', articles: 2, cat: 'secondary' },
@@ -1496,14 +1563,18 @@ const Tutorial = {
legend.addTo(this._demoMap); legend.addTo(this._demoMap);
this._demoMapLegend = legend; this._demoMapLegend = legend;
// Resize + Start Demo // Resize + animierter Zoom auf Hamburg
var map = this._demoMap; var map = this._demoMap;
setTimeout(function() { setTimeout(function() {
if (map) map.invalidateSize(); if (map) map.invalidateSize();
// Sanft auf Hamburg zoomen
setTimeout(function() {
if (map) map.flyTo([53.54, 9.97], 13, { duration: 2.5 });
// Nach Zoom: Demo starten
setTimeout(function() { setTimeout(function() {
if (map) map.setView([53.545, 9.98], 13);
self._simulateMapDemo(); self._simulateMapDemo();
}, 300); }, 3000);
}, 500);
}, 200); }, 200);
}, },
@@ -1960,11 +2031,16 @@ const Tutorial = {
requestAnimationFrame(frame); requestAnimationFrame(frame);
}); });
// Aufräumen // Aufräumen - exakte Originalgröße wiederherstellen
tile.style.width = ''; tile.style.width = origW + 'px';
tile.style.height = ''; tile.style.height = origH + 'px';
tile.style.transition = ''; tile.style.transition = '';
tile.style.zIndex = ''; tile.style.zIndex = '';
// Nach kurzer Verzögerung CSS-Werte entfernen damit GridStack wieder übernimmt
setTimeout(function() {
tile.style.width = '';
tile.style.height = '';
}, 100);
this._hideCursor(); this._hideCursor();
await this._wait(200); await this._wait(200);