diff --git a/src/static/dashboard.html b/src/static/dashboard.html
index ffd9d97..6f19152 100644
--- a/src/static/dashboard.html
+++ b/src/static/dashboard.html
@@ -17,7 +17,7 @@
-
+
Zum Hauptinhalt springen
@@ -764,7 +764,7 @@
-
+
diff --git a/src/static/js/tutorial.js b/src/static/js/tutorial.js
index 80510dc..98e4e2e 100644
--- a/src/static/js/tutorial.js
+++ b/src/static/js/tutorial.js
@@ -188,12 +188,11 @@ const Tutorial = {
timeline.innerHTML = tlHtml;
}
- // Karte: Leaflet-Map mit Demo-Markern initialisieren
+ // Karte: Stats setzen, Map wird erst im Vollbild-Step initialisiert
var mapEmpty = document.getElementById('map-empty');
if (mapEmpty) mapEmpty.style.display = 'none';
var mapStats = document.getElementById('map-stats');
if (mapStats) mapStats.textContent = '3 Orte / 9 Artikel';
- this._initDemoMap();
// Meta
var metaUpdated = document.getElementById('meta-updated');
@@ -841,30 +840,26 @@ const Tutorial = {
Tutorial._clearSubHighlights();
},
},
- // 17 - Karte
+ // 17 - Karte (Vollbild)
{
id: 'karte',
- target: '[gs-id="karte"]',
+ target: '#map-fullscreen-overlay',
title: 'Geografische Verteilung',
text: 'Die Karte zeigt per Geoparsing automatisch erkannte Orte aus den Quellen. '
+ '● Hauptereignisort - Zentraler Ort des Geschehens '
- + '● Erwähnt - In Artikeln genannte Orte '
+ + '● Erw\u00e4hnt - In Artikeln genannte Orte '
+ '● Kontext - Relevante Umgebung '
- + 'Klicken Sie auf Marker für Details und verknüpfte Artikel. '
- + 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert.',
- position: 'top',
+ + 'Klicken Sie auf Marker f\u00fcr Details und verkn\u00fcpfte Artikel. '
+ + 'Bei vielen Markern werden nahe Orte zu Clustern gruppiert. '
+ + 'Orte einlesen startet das Geoparsing manuell neu. '
+ + 'Vollbild zeigt die Karte in dieser gro\u00dfen Ansicht.',
+ position: 'left',
disableNav: true,
onEnter: function() {
- // Demo-Map Popup öffnen + invalidateSize
- if (Tutorial._demoMap) {
- Tutorial._demoMap.invalidateSize();
- setTimeout(function() {
- if (Tutorial._demoMap) Tutorial._demoMap.setView([53.545, 9.98], 13);
- }, 300);
- }
- Tutorial._simulateMapDemo();
+ Tutorial._openDemoMapFullscreen();
},
onExit: function() {
+ Tutorial._closeDemoMapFullscreen();
Tutorial._clearSubHighlights();
},
},
@@ -1346,43 +1341,153 @@ const Tutorial = {
},
// -----------------------------------------------------------------------
- // Karten-Demo (Step 17): Marker klicken, Geoparse + Vollbild highlighten
+ // Karten-Demo: Vollbild mit Markern und Cursor-Demo
// -----------------------------------------------------------------------
+ _openDemoMapFullscreen() {
+ var overlay = document.getElementById('map-fullscreen-overlay');
+ var fsContainer = document.getElementById('map-fullscreen-container');
+ var fsStats = document.getElementById('map-fullscreen-stats');
+ if (!overlay || !fsContainer) return;
+
+ // Overlay anzeigen (\u00fcber Tutorial-Z-Index)
+ overlay.classList.add('active');
+ overlay.style.zIndex = '9002';
+
+ if (fsStats) fsStats.textContent = '3 Orte / 9 Artikel';
+
+ // Alte Demo-Map entfernen falls vorhanden
+ this._destroyDemoMap();
+
+ // Neue Map im Fullscreen-Container erstellen
+ fsContainer.innerHTML = '';
+ var mapDiv = document.createElement('div');
+ mapDiv.id = 'tutorial-fs-map';
+ mapDiv.style.cssText = 'width:100%;height:100%;';
+ fsContainer.appendChild(mapDiv);
+
+ var isDark = !document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-theme') !== 'light';
+ this._demoMap = L.map(mapDiv, {
+ zoomControl: true,
+ attributionControl: true,
+ }).setView([53.545, 9.98], 13);
+
+ var tileUrl = isDark
+ ? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
+ : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
+ L.tileLayer(tileUrl, {
+ attribution: '\u00a9 OpenStreetMap, \u00a9 CARTO',
+ maxZoom: 19,
+ }).addTo(this._demoMap);
+
+ // Marker
+ var locations = [
+ { 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.5380, lon: 9.9400, name: 'Elbe / Hafengebiet', articles: 1, cat: 'tertiary' },
+ ];
+ var catColors = { primary: '#EF4444', secondary: '#F59E0B', tertiary: '#3B82F6' };
+ var catLabels = { primary: 'Hauptereignisort', secondary: 'Erw\u00e4hnt', tertiary: 'Kontext' };
+ var self = this;
+ this._demoMapMarkers = [];
+
+ locations.forEach(function(loc) {
+ var color = catColors[loc.cat];
+ var icon = L.divIcon({
+ className: 'tutorial-map-marker',
+ html: '
',
+ iconSize: [16, 16],
+ iconAnchor: [8, 8],
+ });
+ var marker = L.marker([loc.lat, loc.lon], { icon: icon });
+ var popupHtml = '';
+ marker.bindPopup(popupHtml, { maxWidth: 250, className: 'map-popup-container' });
+ marker.addTo(self._demoMap);
+ self._demoMapMarkers.push(marker);
+ });
+
+ // Legende
+ var legend = L.control({ position: 'bottomright' });
+ legend.onAdd = function() {
+ var div = L.DomUtil.create('div', 'map-legend-ctrl');
+ L.DomEvent.disableClickPropagation(div);
+ var html = 'Legende ';
+ ['primary', 'secondary', 'tertiary'].forEach(function(cat) {
+ html += ''
+ + ' '
+ + '' + catLabels[cat] + '
';
+ });
+ div.innerHTML = html;
+ return div;
+ };
+ legend.addTo(this._demoMap);
+ this._demoMapLegend = legend;
+
+ // Resize + Start Demo
+ var map = this._demoMap;
+ setTimeout(function() {
+ if (map) map.invalidateSize();
+ setTimeout(function() {
+ if (map) map.setView([53.545, 9.98], 13);
+ self._simulateMapDemo();
+ }, 300);
+ }, 200);
+ },
+
+ _closeDemoMapFullscreen() {
+ var overlay = document.getElementById('map-fullscreen-overlay');
+ if (overlay) {
+ overlay.classList.remove('active');
+ overlay.style.zIndex = '';
+ }
+ var fsContainer = document.getElementById('map-fullscreen-container');
+ if (fsContainer) fsContainer.innerHTML = '';
+ this._destroyDemoMap();
+ },
+
async _simulateMapDemo() {
this._demoRunning = true;
- // 1. Auf Marker zeigen und klicken
+ await this._wait(500);
+
+ // 1. Cursor zu Hauptmarker und klicken
if (this._demoMapMarkers.length > 0 && this._demoMap) {
var mainMarker = this._demoMapMarkers[0];
var latLng = mainMarker.getLatLng();
var point = this._demoMap.latLngToContainerPoint(latLng);
- var mapContainer = document.getElementById('map-container');
- if (mapContainer) {
- var mapRect = mapContainer.getBoundingClientRect();
+ var mapEl = document.getElementById('tutorial-fs-map');
+ if (mapEl) {
+ var mapRect = mapEl.getBoundingClientRect();
var markerX = mapRect.left + point.x;
var markerY = mapRect.top + point.y;
- this._showCursor(markerX - 40, markerY - 40, 'default');
- await this._wait(400);
- await this._animateCursor(markerX - 40, markerY - 40, markerX, markerY, 600);
+ this._showCursor(markerX - 60, markerY - 60, 'default');
await this._wait(300);
+ await this._animateCursor(markerX - 60, markerY - 60, markerX, markerY, 700);
+ await this._wait(200);
mainMarker.openPopup();
- await this._wait(2000);
+ await this._wait(2500);
mainMarker.closePopup();
+
+ // 2. Zum zweiten Marker
+ var secondMarker = this._demoMapMarkers[1];
+ if (secondMarker) {
+ var p2 = this._demoMap.latLngToContainerPoint(secondMarker.getLatLng());
+ var m2x = mapRect.left + p2.x;
+ var m2y = mapRect.top + p2.y;
+ await this._animateCursor(markerX, markerY, m2x, m2y, 600);
+ await this._wait(200);
+ secondMarker.openPopup();
+ await this._wait(2000);
+ secondMarker.closePopup();
+ }
}
}
- // 2. Geoparse-Button highlighten
- this._hideCursor();
- this._highlightSub('#geoparse-btn');
- await this._wait(2000);
- this._clearSubHighlights();
-
- // 3. Vollbild-Button highlighten
- this._highlightSub('#map-expand-btn');
- await this._wait(2000);
- this._clearSubHighlights();
-
this._hideCursor();
this._demoRunning = false;
this._enableNavAfterDemo();
@@ -1642,6 +1747,162 @@ const Tutorial = {
this._enableNavAfterDemo();
},
+ // -----------------------------------------------------------------------
+ // Drag-Demo: Kachel visuell verschieben
+ // -----------------------------------------------------------------------
+ async _simulateDrag() {
+ this._demoRunning = true;
+ var tile = document.querySelector('[gs-id="lagebild"]');
+ var header = tile ? tile.querySelector('.card-header') : null;
+ if (!header || !tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
+
+ var rect = header.getBoundingClientRect();
+ var startX = rect.left + rect.width / 2;
+ var startY = rect.top + rect.height / 2;
+ var moveX = 150;
+
+ // Cursor erscheint
+ this._showCursor(startX - 50, startY - 30, 'default');
+ await this._wait(300);
+ await this._animateCursor(startX - 50, startY - 30, startX, startY, 500);
+ await this._wait(300);
+
+ // Grabbing
+ this._els.cursor.classList.remove('tutorial-cursor-default');
+ this._els.cursor.classList.add('tutorial-cursor-grabbing');
+ await this._wait(200);
+
+ // Kachel visuell verschieben (CSS transform)
+ tile.style.transition = 'none';
+ tile.style.zIndex = '9002';
+ var self = this;
+ var start = null;
+ await new Promise(function(resolve) {
+ function frame(ts) {
+ if (!self._isActive) { resolve(); return; }
+ if (!start) start = ts;
+ var progress = Math.min((ts - start) / 1200, 1);
+ var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
+ var dx = moveX * eased;
+ tile.style.transform = 'translateX(' + dx + 'px)';
+ self._els.cursor.style.left = (startX + dx) + 'px';
+ if (progress < 1) requestAnimationFrame(frame); else resolve();
+ }
+ requestAnimationFrame(frame);
+ });
+ await this._wait(300);
+
+ // Zurück
+ this._els.cursor.classList.remove('tutorial-cursor-grabbing');
+ this._els.cursor.classList.add('tutorial-cursor-default');
+ start = null;
+ await new Promise(function(resolve) {
+ function frame(ts) {
+ if (!self._isActive) { resolve(); return; }
+ if (!start) start = ts;
+ var progress = Math.min((ts - start) / 800, 1);
+ var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
+ var dx = moveX * (1 - eased);
+ tile.style.transform = 'translateX(' + dx + 'px)';
+ self._els.cursor.style.left = (startX + dx) + 'px';
+ if (progress < 1) requestAnimationFrame(frame); else resolve();
+ }
+ requestAnimationFrame(frame);
+ });
+
+ // Aufräumen
+ tile.style.transform = '';
+ tile.style.transition = '';
+ tile.style.zIndex = '';
+ this._hideCursor();
+ await this._wait(200);
+
+ this._demoRunning = false;
+ this._enableNavAfterDemo();
+ },
+
+ // -----------------------------------------------------------------------
+ // Resize-Demo: Kachel visuell vergrößern
+ // -----------------------------------------------------------------------
+ async _simulateResize() {
+ this._demoRunning = true;
+ var tile = document.querySelector('[gs-id="faktencheck"]');
+ if (!tile) { this._demoRunning = false; this._enableNavAfterDemo(); return; }
+
+ var rect = tile.getBoundingClientRect();
+ var startX = rect.right - 6;
+ var startY = rect.bottom - 6;
+ var expandX = 80;
+ var expandY = 50;
+
+ // Cursor am Rand
+ this._showCursor(startX - 40, startY - 40, 'default');
+ await this._wait(300);
+ await this._animateCursor(startX - 40, startY - 40, startX, startY, 400);
+ await this._wait(200);
+
+ // Resize-Cursor
+ this._els.cursor.classList.remove('tutorial-cursor-default');
+ this._els.cursor.classList.add('tutorial-cursor-resize');
+ await this._wait(200);
+
+ // Kachel vergrößern (visuell)
+ tile.style.transition = 'none';
+ tile.style.zIndex = '9002';
+ var origW = tile.offsetWidth;
+ var origH = tile.offsetHeight;
+ var self = this;
+ var start = null;
+
+ await new Promise(function(resolve) {
+ function frame(ts) {
+ if (!self._isActive) { resolve(); return; }
+ if (!start) start = ts;
+ var progress = Math.min((ts - start) / 1000, 1);
+ var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
+ var dx = expandX * eased;
+ var dy = expandY * eased;
+ tile.style.width = (origW + dx) + 'px';
+ tile.style.height = (origH + dy) + 'px';
+ self._els.cursor.style.left = (startX + dx) + 'px';
+ self._els.cursor.style.top = (startY + dy) + 'px';
+ if (progress < 1) requestAnimationFrame(frame); else resolve();
+ }
+ requestAnimationFrame(frame);
+ });
+ await this._wait(500);
+
+ // Zurück
+ start = null;
+ await new Promise(function(resolve) {
+ function frame(ts) {
+ if (!self._isActive) { resolve(); return; }
+ if (!start) start = ts;
+ var progress = Math.min((ts - start) / 700, 1);
+ var eased = progress < 0.5 ? 4*progress*progress*progress : 1 - Math.pow(-2*progress+2,3)/2;
+ var dx = expandX * (1 - eased);
+ var dy = expandY * (1 - eased);
+ tile.style.width = (origW + dx) + 'px';
+ tile.style.height = (origH + dy) + 'px';
+ self._els.cursor.style.left = (startX + dx) + 'px';
+ self._els.cursor.style.top = (startY + dy) + 'px';
+ if (progress < 1) requestAnimationFrame(frame); else resolve();
+ }
+ requestAnimationFrame(frame);
+ });
+
+ // Aufräumen
+ tile.style.width = '';
+ tile.style.height = '';
+ tile.style.transition = '';
+ tile.style.zIndex = '';
+ this._hideCursor();
+ await this._wait(200);
+
+ this._demoRunning = false;
+ this._enableNavAfterDemo();
+ },
+
// -----------------------------------------------------------------------
// Keyboard
// -----------------------------------------------------------------------