addes app.js with map initialization, CRUD workflow, sidebar and modal logic

This commit is contained in:
2026-04-17 20:45:03 +02:00
parent 65ef7f07c9
commit 77df35926d

890
public/js/app.js Normal file
View File

@@ -0,0 +1,890 @@
// =====================================================================
// WebGIS Citizen Participation Portal — Application Logic
// Initializes the Leaflet Map, loads Contributions from the API,
// handles the CRUD Workflow, and manages all UI Interactions.
//
// Depends on: MUNICIPALITY Object (set in index.php), Leaflet, Geoman,
// Sidebar v2, Geocoder, PolylineMeasure, Fullscreen, SweetAlert2
// =====================================================================
// =====================================================================
// Block 1: Configuration and Application State
// =====================================================================
// API Endpoint — relative Path from public/ to api/
var API_URL = '../api/contributions.php';
// Current User Name — set via Login Modal, stored in sessionStorage
var currentUser = sessionStorage.getItem('webgis_user') || '';
// Category Definitions with Labels, Icons, and Colors
var CATEGORIES = {
mobility: { label: 'Mobilität', icon: '🚲', color: '#1565C0', faIcon: 'fa-bicycle' },
building: { label: 'Bauen', icon: '🏗️', color: '#E65100', faIcon: 'fa-helmet-safety' },
energy: { label: 'Energie', icon: '⚡', color: '#F9A825', faIcon: 'fa-bolt' },
environment: { label: 'Umwelt', icon: '🌳', color: '#2E7D32', faIcon: 'fa-tree' },
industry: { label: 'Industrie', icon: '🏭', color: '#6A1B9A', faIcon: 'fa-industry' },
consumption: { label: 'Konsum', icon: '🛒', color: '#AD1457', faIcon: 'fa-cart-shopping' },
other: { label: 'Sonstiges', icon: '📌', color: '#546E7A', faIcon: 'fa-map-pin' }
};
// Application State
var map; // Leaflet Map Instance
var sidebar; // Sidebar Instance
var contributionsLayer; // GeoJSON Layer holding all Contributions
var contributionsData = []; // Raw Contribution Data Array
var activeFilters = Object.keys(CATEGORIES); // Active Category Filters (all enabled by Default)
var drawnGeometry = null; // Temporary Storage for Geometry drawn with Geoman
var drawnGeomType = null; // Temporary Storage for Geometry Type
// =====================================================================
// Block 2: Map Initialization
// =====================================================================
map = L.map('map', {
center: MUNICIPALITY.center,
zoom: MUNICIPALITY.zoom,
zoomControl: false, // Added manually in Block 3 for Position Control
attributionControl: true
});
// =====================================================================
// Block 3: Basemaps and Layer Control
// =====================================================================
// Basemap Tile Layers
var basemapOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 20
});
var basemapCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© <a href="https://carto.com/">CARTO</a>',
maxZoom: 20
});
var basemapSatellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© <a href="https://www.esri.com/">Esri</a>',
maxZoom: 20
});
var basemapTopo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://opentopomap.org">OpenTopoMap</a>',
maxZoom: 18
});
// Set Default Basemap
basemapCartoDB.addTo(map);
// Layer Control
var basemaps = {
'OpenStreetMap': basemapOSM,
'CartoDB (hell)': basemapCartoDB,
'Satellit (Esri)': basemapSatellite,
'Topographisch': basemapTopo
};
var overlays = {}; // Populated later with Contribution Layer
var layerControl = L.control.layers(basemaps, overlays, {
position: 'topright',
collapsed: true
}).addTo(map);
// =====================================================================
// Block 4: Map Controls
// =====================================================================
// Zoom Control (top right)
L.control.zoom({
position: 'topright'
}).addTo(map);
// Scale Bar (bottom right)
L.control.scale({
position: 'bottomright',
maxWidth: 200,
imperial: false
}).addTo(map);
// Fullscreen Button
L.control.fullscreen({
position: 'topright',
title: 'Vollbild',
titleCancel: 'Vollbild beenden'
}).addTo(map);
// Address Search (Geocoder with Nominatim)
L.Control.geocoder({
position: 'topright',
placeholder: 'Adresse suchen...',
defaultMarkGeocode: true,
geocoder: L.Control.Geocoder.nominatim({
geocodingQueryParams: {
countrycodes: 'de',
viewbox: '8.0,52.5,8.5,52.8',
bounded: 1
}
})
}).addTo(map);
// Polyline Measure Tool
L.control.polylineMeasure({
position: 'topright',
unit: 'metres',
showBearings: false,
clearMeasurementsOnStop: false,
showClearControl: true
}).addTo(map);
// Mouse Position Display
var MousePositionControl = L.Control.extend({
options: { position: 'bottomright' },
onAdd: function () {
var container = L.DomUtil.create('div', 'mouse-position-display');
container.style.background = 'rgba(255,255,255,0.85)';
container.style.padding = '2px 8px';
container.style.fontSize = '12px';
container.style.borderRadius = '4px';
container.style.fontFamily = 'monospace';
container.innerHTML = 'Lat: — | Lng: —';
map.on('mousemove', function (e) {
container.innerHTML = 'Lat: ' + e.latlng.lat.toFixed(5) + ' | Lng: ' + e.latlng.lng.toFixed(5);
});
return container;
}
});
new MousePositionControl().addTo(map);
// GPS Location Button
var GpsControl = L.Control.extend({
options: { position: 'topright' },
onAdd: function () {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
var button = L.DomUtil.create('a', '', container);
button.href = '#';
button.title = 'Mein Standort';
button.innerHTML = '<i class="fa-solid fa-location-crosshairs"></i>';
button.style.fontSize = '16px';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.width = '30px';
button.style.height = '30px';
L.DomEvent.on(button, 'click', function (e) {
L.DomEvent.preventDefault(e);
map.locate({ setView: true, maxZoom: 17 });
});
return container;
}
});
new GpsControl().addTo(map);
// GPS Location Found Handler
var gpsMarker = null;
map.on('locationfound', function (e) {
if (gpsMarker) {
map.removeLayer(gpsMarker);
}
gpsMarker = L.circleMarker(e.latlng, {
radius: 8,
color: '#1565C0',
fillColor: '#42A5F5',
fillOpacity: 0.8,
weight: 2
}).addTo(map).bindPopup('Ihr Standort').openPopup();
});
map.on('locationerror', function () {
Swal.fire('Standort nicht gefunden', 'Bitte erlauben Sie den Standortzugriff in Ihrem Browser.', 'warning');
});
// =====================================================================
// Block 5: Sidebar Initialization
// =====================================================================
sidebar = L.control.sidebar({
autopan: true,
closeButton: true,
container: 'sidebar',
position: 'left'
}).addTo(map);
// =====================================================================
// Block 6: Geoman Drawing Tools and CRUD Trigger
// =====================================================================
map.pm.addControls({
position: 'topright',
drawMarker: true,
drawPolyline: true,
drawPolygon: true,
drawCircleMarker: false,
drawCircle: false,
drawText: false,
drawRectangle: false,
editMode: false,
dragMode: false,
cutPolygon: false,
removalMode: false,
rotateMode: false
});
// When a Shape is drawn, capture the Geometry and open the Create Modal
map.on('pm:create', function (e) {
var geojson = e.layer.toGeoJSON().geometry;
// Determine Geometry Type and normalize to simple Types
if (e.shape === 'Marker') {
drawnGeometry = { type: 'Point', coordinates: geojson.coordinates };
drawnGeomType = 'point';
} else if (e.shape === 'Line') {
drawnGeometry = { type: 'LineString', coordinates: geojson.coordinates };
drawnGeomType = 'line';
} else if (e.shape === 'Polygon') {
drawnGeometry = { type: 'Polygon', coordinates: geojson.coordinates };
drawnGeomType = 'polygon';
} else {
// Unsupported Shape — remove from Map and exit
map.removeLayer(e.layer);
return;
}
// Remove the drawn Layer — it will be re-added after API Confirmation
map.removeLayer(e.layer);
// Check if User is logged in
if (!currentUser) {
Swal.fire('Bitte anmelden', 'Sie müssen sich anmelden, um Beiträge zu erstellen.', 'info');
showLoginModal();
return;
}
// Populate hidden Fields and open Create Modal
document.getElementById('create-geom').value = JSON.stringify(drawnGeometry);
document.getElementById('create-geom-type').value = drawnGeomType;
document.getElementById('create-modal').style.display = 'flex';
});
// =====================================================================
// Block 7: API Communication
// =====================================================================
// Generic API Call Function
function apiCall(data, callback) {
var formData = new FormData();
for (var key in data) {
formData.append(key, data[key]);
}
fetch(API_URL, { method: 'POST', body: formData })
.then(function (response) {
return response.json().then(function (json) {
json._status = response.status;
return json;
});
})
.then(function (json) {
callback(json);
})
.catch(function (error) {
console.error('API Error:', error);
Swal.fire('Verbindungsfehler', 'Die Verbindung zum Server ist fehlgeschlagen.', 'error');
});
}
// Load all Contributions from API and display on Map
function loadContributions() {
apiCall({ action: 'read', municipality_id: MUNICIPALITY.id }, function (data) {
if (data.error) {
console.error('Load Error:', data.error);
return;
}
contributionsData = data.features || [];
// Remove existing Layer if present
if (contributionsLayer) {
map.removeLayer(contributionsLayer);
layerControl.removeLayer(contributionsLayer);
}
// Create new GeoJSON Layer
contributionsLayer = L.geoJSON(data, {
pointToLayer: stylePoint,
style: styleLinePolygon,
onEachFeature: bindFeaturePopup
}).addTo(map);
layerControl.addOverlay(contributionsLayer, 'Beiträge');
// Update Sidebar List and Statistics
updateContributionsList();
updateStatistics();
});
}
// =====================================================================
// Block 8: Feature Styling by Category
// =====================================================================
// Style for Point Features (CircleMarkers)
function stylePoint(feature, latlng) {
var cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
return L.circleMarker(latlng, {
radius: 8,
color: '#ffffff',
weight: 2,
fillColor: cat.color,
fillOpacity: 0.9
});
}
// Style for Line and Polygon Features
function styleLinePolygon(feature) {
var cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
return {
color: cat.color,
weight: 3,
opacity: 0.8,
fillColor: cat.color,
fillOpacity: 0.25
};
}
// =====================================================================
// Block 9: Feature Popups (Read, Vote, Edit, Delete)
// =====================================================================
function bindFeaturePopup(feature, layer) {
var props = feature.properties;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
// Format Date
var date = new Date(props.created_at);
var dateStr = date.toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric'
});
// Build Popup HTML
var html = '' +
'<div class="popup-detail">' +
'<span class="popup-detail-category">' + cat.icon + ' ' + cat.label + '</span>' +
'<div class="popup-detail-title">' + escapeHtml(props.title) + '</div>' +
(props.description ? '<div class="popup-detail-description">' + escapeHtml(props.description) + '</div>' : '') +
'<div class="popup-detail-meta">' +
'<i class="fa-solid fa-user"></i> ' + escapeHtml(props.author_name) +
' &middot; <i class="fa-solid fa-calendar"></i> ' + dateStr +
'</div>' +
'<div class="popup-detail-votes">' +
'<button class="popup-vote-btn" onclick="voteContribution(' + props.contribution_id + ', \'like\')" title="Gefällt mir">' +
'<i class="fa-solid fa-thumbs-up"></i> <span id="likes-' + props.contribution_id + '">' + props.likes_count + '</span>' +
'</button>' +
'<button class="popup-vote-btn" onclick="voteContribution(' + props.contribution_id + ', \'dislike\')" title="Gefällt mir nicht">' +
'<i class="fa-solid fa-thumbs-down"></i> <span id="dislikes-' + props.contribution_id + '">' + props.dislikes_count + '</span>' +
'</button>' +
'</div>' +
'<div class="popup-detail-actions">' +
'<button class="btn btn-primary" onclick="editContribution(' + props.contribution_id + ')"><i class="fa-solid fa-pen"></i> Bearbeiten</button>' +
'<button class="btn btn-danger" onclick="deleteContribution(' + props.contribution_id + ')"><i class="fa-solid fa-trash"></i> Löschen</button>' +
'</div>' +
'</div>';
layer.bindPopup(html, { maxWidth: 320, minWidth: 240 });
// Tooltip on Hover
layer.bindTooltip(cat.icon + ' ' + escapeHtml(props.title), {
direction: 'top',
offset: [0, -10]
});
}
// =====================================================================
// Block 10: CRUD Operations
// =====================================================================
// CREATE — Submit new Contribution from Modal
function submitCreate() {
var category = document.getElementById('create-category').value;
var title = document.getElementById('create-title').value.trim();
var description = document.getElementById('create-description').value.trim();
var geom = document.getElementById('create-geom').value;
var geomType = document.getElementById('create-geom-type').value;
// Validate
if (!category) {
Swal.fire('Kategorie fehlt', 'Bitte wählen Sie eine Kategorie aus.', 'warning');
return;
}
if (!title) {
Swal.fire('Titel fehlt', 'Bitte geben Sie einen Titel ein.', 'warning');
return;
}
if (!geom) {
Swal.fire('Geometrie fehlt', 'Bitte zeichnen Sie zuerst ein Objekt auf der Karte.', 'warning');
return;
}
apiCall({
action: 'create',
municipality_id: MUNICIPALITY.id,
category: category,
title: title,
description: description,
geom: geom,
geom_type: geomType,
author_name: currentUser
}, function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Eingereicht!', 'Ihr Beitrag wurde erfolgreich eingereicht und wird nach Prüfung veröffentlicht.', 'success');
closeCreateModal();
loadContributions();
});
}
// Cancel Create — close Modal and clear Form
function cancelCreate() {
closeCreateModal();
}
function closeCreateModal() {
document.getElementById('create-modal').style.display = 'none';
document.getElementById('create-category').value = '';
document.getElementById('create-title').value = '';
document.getElementById('create-description').value = '';
document.getElementById('create-geom').value = '';
document.getElementById('create-geom-type').value = '';
drawnGeometry = null;
drawnGeomType = null;
}
// UPDATE — Edit an existing Contribution
function editContribution(contributionId) {
// Find Contribution in local Data
var contribution = contributionsData.find(function (f) {
return f.properties.contribution_id === contributionId;
});
if (!contribution) return;
var props = contribution.properties;
Swal.fire({
title: 'Beitrag bearbeiten',
html:
'<div style="text-align:left;">' +
'<label style="font-weight:600;font-size:0.85rem;">Titel</label>' +
'<input id="swal-title" class="swal2-input" value="' + escapeHtml(props.title) + '">' +
'<label style="font-weight:600;font-size:0.85rem;">Beschreibung</label>' +
'<textarea id="swal-description" class="swal2-textarea">' + escapeHtml(props.description || '') + '</textarea>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Speichern',
cancelButtonText: 'Abbrechen',
confirmButtonColor: MUNICIPALITY.primaryColor,
preConfirm: function () {
return {
title: document.getElementById('swal-title').value.trim(),
description: document.getElementById('swal-description').value.trim()
};
}
}).then(function (result) {
if (!result.isConfirmed) return;
apiCall({
action: 'update',
contribution_id: contributionId,
title: result.value.title,
description: result.value.description
}, function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Gespeichert!', 'Der Beitrag wurde aktualisiert.', 'success');
loadContributions();
});
});
}
// DELETE — Delete a Contribution
function deleteContribution(contributionId) {
Swal.fire({
title: 'Beitrag löschen?',
text: 'Diese Aktion kann nicht rückgängig gemacht werden.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Löschen',
cancelButtonText: 'Abbrechen',
confirmButtonColor: '#c62828'
}).then(function (result) {
if (!result.isConfirmed) return;
apiCall({
action: 'delete',
contribution_id: contributionId
}, function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Gelöscht!', 'Der Beitrag wurde entfernt.', 'success');
map.closePopup();
loadContributions();
});
});
}
// VOTE — Like or Dislike a Contribution
function voteContribution(contributionId, voteType) {
if (!currentUser) {
Swal.fire('Bitte anmelden', 'Sie müssen sich anmelden, um abzustimmen.', 'info');
showLoginModal();
return;
}
apiCall({
action: 'vote',
contribution_id: contributionId,
voter_name: currentUser,
vote_type: voteType
}, function (response) {
if (response.error) {
Swal.fire('Hinweis', response.error, 'info');
return;
}
// Update Vote Counts in the Popup without reloading everything
loadContributions();
});
}
// =====================================================================
// Block 11: Sidebar — Contributions List
// =====================================================================
function updateContributionsList() {
var container = document.getElementById('contributions-list');
var searchInput = document.getElementById('list-search-input');
var searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
// Filter by active Categories and Search Term
var filtered = contributionsData.filter(function (f) {
var props = f.properties;
var matchesCategory = activeFilters.indexOf(props.category) !== -1;
var matchesSearch = !searchTerm ||
props.title.toLowerCase().indexOf(searchTerm) !== -1 ||
(props.description && props.description.toLowerCase().indexOf(searchTerm) !== -1) ||
props.author_name.toLowerCase().indexOf(searchTerm) !== -1;
return matchesCategory && matchesSearch;
});
// Sort by Date (newest first)
filtered.sort(function (a, b) {
return new Date(b.properties.created_at) - new Date(a.properties.created_at);
});
// Build HTML
if (filtered.length === 0) {
container.innerHTML = '<p style="text-align:center;color:#999;padding:20px;">Keine Beiträge gefunden.</p>';
return;
}
var html = '';
filtered.forEach(function (f) {
var props = f.properties;
var cat = CATEGORIES[props.category] || CATEGORIES.other;
var date = new Date(props.created_at).toLocaleDateString('de-DE');
html += '' +
'<div class="contribution-card" onclick="flyToContribution(' + props.contribution_id + ')">' +
'<div class="contribution-card-header">' +
'<span class="contribution-card-category">' + cat.icon + ' ' + cat.label + '</span>' +
'</div>' +
'<div class="contribution-card-title">' + escapeHtml(props.title) + '</div>' +
'<div class="contribution-card-meta">' +
'<span>' + escapeHtml(props.author_name) + ' · ' + date + '</span>' +
'<span class="contribution-card-votes">' +
'<span title="Likes"><i class="fa-solid fa-thumbs-up"></i> ' + props.likes_count + '</span>' +
'<span title="Dislikes"><i class="fa-solid fa-thumbs-down"></i> ' + props.dislikes_count + '</span>' +
'</span>' +
'</div>' +
'</div>';
});
container.innerHTML = html;
}
// Fly to a Contribution on the Map and open its Popup
function flyToContribution(contributionId) {
if (!contributionsLayer) return;
contributionsLayer.eachLayer(function (layer) {
if (layer.feature && layer.feature.properties.contribution_id === contributionId) {
// Zoom to Feature
if (layer.getLatLng) {
// Point Feature
map.flyTo(layer.getLatLng(), 17);
} else if (layer.getBounds) {
// Line or Polygon Feature
map.flyToBounds(layer.getBounds(), { maxZoom: 17 });
}
// Open Popup
layer.openPopup();
// Close Sidebar on Mobile
if (window.innerWidth < 769) {
sidebar.close();
}
}
});
}
// Search Input Event Listener
document.getElementById('list-search-input').addEventListener('input', function () {
updateContributionsList();
});
// =====================================================================
// Block 12: Sidebar — Category Filter and Statistics
// =====================================================================
// Build Category Filter Checkboxes
function buildCategoryFilter() {
var container = document.getElementById('category-filter');
var html = '';
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var checked = activeFilters.indexOf(key) !== -1 ? 'checked' : '';
html += '' +
'<label style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;">' +
'<input type="checkbox" value="' + key + '" ' + checked + ' onchange="toggleCategoryFilter(this)">' +
'<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:' + cat.color + ';"></span>' +
'<span>' + cat.icon + ' ' + cat.label + '</span>' +
'</label>';
}
container.innerHTML = html;
}
// Toggle a Category Filter on/off
function toggleCategoryFilter(checkbox) {
var category = checkbox.value;
if (checkbox.checked) {
if (activeFilters.indexOf(category) === -1) {
activeFilters.push(category);
}
} else {
activeFilters = activeFilters.filter(function (c) { return c !== category; });
}
// Re-filter the Map Layer
if (contributionsLayer) {
contributionsLayer.eachLayer(function (layer) {
if (layer.feature) {
var cat = layer.feature.properties.category;
if (activeFilters.indexOf(cat) !== -1) {
layer.setStyle({ opacity: 1, fillOpacity: layer.feature.geometry.type === 'Point' ? 0.9 : 0.25 });
if (layer.setRadius) layer.setRadius(8);
} else {
layer.setStyle({ opacity: 0, fillOpacity: 0 });
if (layer.setRadius) layer.setRadius(0);
}
}
});
}
// Update List
updateContributionsList();
}
// Update Statistics in Home Tab
function updateStatistics() {
var container = document.getElementById('stats-container');
var total = contributionsData.length;
// Count per Category
var counts = {};
contributionsData.forEach(function (f) {
var cat = f.properties.category;
counts[cat] = (counts[cat] || 0) + 1;
});
var html = '<p style="font-size:0.9rem;"><strong>' + total + '</strong> Beiträge insgesamt</p>';
for (var key in CATEGORIES) {
var cat = CATEGORIES[key];
var count = counts[key] || 0;
if (count > 0) {
html += '<div style="display:flex;align-items:center;gap:8px;margin:4px 0;font-size:0.85rem;">' +
'<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + cat.color + ';"></span>' +
cat.label + ': ' + count +
'</div>';
}
}
container.innerHTML = html;
}
// =====================================================================
// Block 13: Modals — Welcome, Login, Info, Privacy, Imprint
// =====================================================================
// Welcome Modal — show on first Visit
function checkWelcomeModal() {
var hasVisited = localStorage.getItem('webgis_welcomed');
if (!hasVisited) {
document.getElementById('welcome-modal').style.display = 'flex';
}
}
function closeWelcomeAndShowLogin() {
localStorage.setItem('webgis_welcomed', 'true');
document.getElementById('welcome-modal').style.display = 'none';
showLoginModal();
}
// Login Modal
function showLoginModal() {
document.getElementById('login-modal').style.display = 'flex';
document.getElementById('user-name-input').value = currentUser;
document.getElementById('user-name-input').focus();
}
function submitLogin() {
var name = document.getElementById('user-name-input').value.trim();
if (!name) {
Swal.fire('Name eingeben', 'Bitte geben Sie Ihren Namen ein.', 'warning');
return;
}
currentUser = name;
sessionStorage.setItem('webgis_user', currentUser);
document.getElementById('login-modal').style.display = 'none';
}
function skipLogin() {
document.getElementById('login-modal').style.display = 'none';
}
// Info Modal
function showInfoModal() {
Swal.fire({
title: 'Über das Portal',
html: '<p style="text-align:left;line-height:1.6;">Das Bürgerbeteiligungsportal ermöglicht es ' +
'Bürgerinnen und Bürgern sowie der Stadtverwaltung, gemeinsam an der Gestaltung von ' +
'<strong>' + MUNICIPALITY.name + '</strong> mitzuwirken.</p>' +
'<p style="text-align:left;line-height:1.6;">Tragen Sie Hinweise, Ideen und Verbesserungsvorschläge ' +
'direkt auf der Karte ein.</p>',
confirmButtonColor: MUNICIPALITY.primaryColor
});
}
// Privacy Modal
function showPrivacyModal() {
Swal.fire({
title: 'Datenschutz',
html: '<p style="text-align:left;line-height:1.6;">Dieses Portal speichert die von Ihnen ' +
'eingegebenen Daten (Name, Beiträge, Bewertungen) zur Durchführung der Bürgerbeteiligung.</p>' +
'<p style="text-align:left;line-height:1.6;">Ihre Daten werden nicht an Dritte weitergegeben. ' +
'Details entnehmen Sie bitte der vollständigen Datenschutzerklärung der Stadt ' +
MUNICIPALITY.name + '.</p>',
confirmButtonColor: MUNICIPALITY.primaryColor
});
}
// Imprint Modal
function showImprintModal() {
Swal.fire({
title: 'Impressum',
html: '<p style="text-align:left;line-height:1.6;">Stadt ' + MUNICIPALITY.name + '</p>' +
'<p style="text-align:left;line-height:1.6;color:#777;">Die vollständigen Angaben gemäß § 5 TMG ' +
'werden hier ergänzt, sobald das Portal in den Produktivbetrieb geht.</p>',
confirmButtonColor: MUNICIPALITY.primaryColor
});
}
// =====================================================================
// Block 14: Mobile Navigation
// =====================================================================
function toggleMobileNav() {
var nav = document.querySelector('.header-nav');
nav.classList.toggle('open');
}
// Close Mobile Nav when clicking outside
document.addEventListener('click', function (e) {
var nav = document.querySelector('.header-nav');
var toggle = document.querySelector('.header-menu-toggle');
if (nav.classList.contains('open') && !nav.contains(e.target) && !toggle.contains(e.target)) {
nav.classList.remove('open');
}
});
// Close Modals on Escape Key
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
document.getElementById('welcome-modal').style.display = 'none';
document.getElementById('login-modal').style.display = 'none';
document.getElementById('create-modal').style.display = 'none';
}
});
// =====================================================================
// Block 15: Utility Functions
// =====================================================================
// Escape HTML to prevent XSS in Popups and Lists
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
// =====================================================================
// Block 16: Application Startup
// =====================================================================
// Initialize Category Filter in Sidebar
buildCategoryFilter();
// Load Contributions from API
loadContributions();
// Show Welcome Modal on first Visit
checkWelcomeModal();