From 3a2ea7a8c7d177718f332f02a5d2eac5c99c8b80 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Mon, 16 Mar 2026 16:09:23 +0100 Subject: [PATCH] Tutorial: Karte im Vollbild mit Markern, echte Drag/Resize-Animationen Karte (Step 17): - Oeffnet jetzt die Karten-Vollbild-Ansicht mit eigenem Leaflet-Map - Zoomt auf Hamburg (Zoom 13) mit 3 farbcodierten Markern - Cursor faehrt zu Markern, oeffnet Popups (Burchardkai, Innenstadt) - Legende erklaert Kategorien (Hauptereignisort/Erwaehnt/Kontext) - Funktionen Orte einlesen + Vollbild werden in der Bubble erklaert - Map wird beim Step-Exit sauber aus dem Fullscreen entfernt Drag-Demo (Step 18): - Kachel bewegt sich jetzt visuell per CSS transform mit dem Cursor - 150px nach rechts, dann zurueck - echte Verschiebe-Animation - Kachel erhaelt erhoehten z-index waehrend der Animation Resize-Demo (Step 19): - Kachel aendert visuell Breite/Hoehe mit dem Cursor - 80px breiter + 50px hoeher, dann zurueck - Echte Groessenaenderung sichtbar statt nur Cursor-Bewegung Co-Authored-By: Claude Opus 4.6 (1M context) --- src/static/dashboard.html | 4 +- src/static/js/tutorial.js | 333 +++++++++++++++++++++++++++++++++----- 2 files changed, 299 insertions(+), 38 deletions(-) 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 @@ - + @@ -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 = '
' + + '
' + loc.name + '
' + + '
' + catLabels[loc.cat] + '
' + + '
' + loc.articles + ' Artikel
' + + '
'; + 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 // -----------------------------------------------------------------------