From 5bfdda2340133c1138bde9f5de8c1b294a586105 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Thu, 30 Apr 2026 16:36:03 +0200 Subject: [PATCH] restructured admin.js --- public/js/admin.js | 156 ++++++++++++++++++--------------------------- 1 file changed, 61 insertions(+), 95 deletions(-) diff --git a/public/js/admin.js b/public/js/admin.js index 33f9b89..0cdf875 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -9,14 +9,10 @@ // ===================================================================== -// Block 1: Status Filter for Contributions +// Block 1: Page Tab Navigation // ===================================================================== -let currentFilter = 'all'; - -// ===================================================================== // Restores active Tab after Page Reload -// ===================================================================== const savedTab = sessionStorage.getItem('admin_active_tab'); if (savedTab) { // Delays to ensure DOM is ready @@ -27,48 +23,42 @@ if (savedTab) { } -// ===================================================================== -// Block 3: Page Tab Navigation -// ===================================================================== - -// Wechselt den sichtbaren Haupt-Tab (Beiträge, Kommentare, Neuigkeiten usw.). -// Speichert den aktiven Tab in sessionStorage für Persistenz nach Reload. +// Page Tab Navigation function showPageTab(tabName) { - + // Saves active Tab for Persistence after Reload sessionStorage.setItem('admin_active_tab', tabName); document.querySelectorAll('.page-tab-content').forEach(function (el) { el.style.display = 'none'; }); + // Deactivates all Tab Buttons document.querySelectorAll('.page-tab').forEach(function (el) { el.classList.remove('active'); }); + // Shows selected Tab and activates Button document.getElementById('tab-' + tabName).style.display = 'block'; event.currentTarget.classList.add('active'); } // ===================================================================== -// Block 4: Aufklappbare Zeilen (Collapsible Rows) +// Block 2: Collapsible Rows for Contributions and Comments // ===================================================================== - -// Öffnet oder schließt eine Beitrags-/Kommentar-Zeile. -// Jeweils nur eine Zeile gleichzeitig geöffnet. -// Lädt bei Bedarf die Karten-Vorschau (Map Preview). function toggleRow(row) { const wasOpen = row.classList.contains('open'); - // Alle offenen Zeilen schließen + // Closes all open Rows document.querySelectorAll('.contribution-row.open').forEach(function (el) { el.classList.remove('open'); }); + // Toggles clicked Row if (!wasOpen) { row.classList.add('open'); - // Karten-Vorschau laden, falls noch nicht geschehen + // Loads Map Preview if not already loaded const mapDiv = row.querySelector('.detail-map'); if (mapDiv && !mapDiv.dataset.loaded) { loadMapPreview(mapDiv); @@ -78,12 +68,8 @@ function toggleRow(row) { // ===================================================================== -// Block 5: Detail-Slider (Karte / Foto) +// Block 3: Details Slider for Maps and Photos // ===================================================================== - -// Wechselt zwischen den Slides (Karte und Foto) in einer Beitrags-Zeile. -// direction: -1 für zurück, +1 für vor. -// Wrapped circular: nach dem letzten Slide kommt der erste. function slideDetail(contributionId, direction) { const slider = document.getElementById('slider-' + contributionId); if (!slider) return; @@ -91,16 +77,19 @@ function slideDetail(contributionId, direction) { const slides = slider.querySelectorAll('.detail-slide'); let activeIndex = -1; + // Finds active Slide slides.forEach(function (slide, i) { if (slide.style.display !== 'none') activeIndex = i; }); + // Calculates next Slide Index const nextIndex = (activeIndex + direction + slides.length) % slides.length; + // Switches Slides slides.forEach(function (slide) { slide.style.display = 'none'; }); slides[nextIndex].style.display = 'block'; - // Karte beim Zurückwechseln zum Map-Slide laden, falls noch nicht geschehen + // Loads Map if switching to Map Slide if (slides[nextIndex].dataset.slide === 'map') { const mapDiv = slides[nextIndex].querySelector('.detail-map'); if (mapDiv && !mapDiv.dataset.loaded) { @@ -111,7 +100,7 @@ function slideDetail(contributionId, direction) { // ===================================================================== -// Block 6: Leaflet Karten-Vorschau (Mini-Map pro Beitrag) +// Block 4: Map Preview (Leaflet Mini Map per Contribution) // ===================================================================== // Erstellt eine Leaflet-Mini-Map in einem Beitrags-Detail-Container. @@ -119,7 +108,8 @@ function slideDetail(contributionId, direction) { // Markiert die Map als geladen (data-loaded="true"), um doppeltes Laden zu verhindern. function loadMapPreview(mapDiv) { const contributionId = mapDiv.dataset.contributionId; - + + // Fetches all Contributions to find the Geometry const formData = new FormData(); formData.append('action', 'read'); formData.append('municipality_id', ADMIN_CONFIG.municipalityId); @@ -129,7 +119,8 @@ function loadMapPreview(mapDiv) { .then(function (r) { return r.json(); }) .then(function (data) { if (!data.features) return; - + + // Finds specific Contribution const feature = data.features.find(function (f) { return f.properties.contribution_id == contributionId; }); @@ -139,6 +130,7 @@ function loadMapPreview(mapDiv) { return; } + // Creates Leaflet Mini Map const miniMap = L.map(mapDiv, { zoomControl: false, attributionControl: false, @@ -150,6 +142,7 @@ function loadMapPreview(mapDiv) { maxZoom: 20 }).addTo(miniMap); + // Adds Geometry to Mini Map const geojsonLayer = L.geoJSON(feature, { style: { color: ADMIN_CONFIG.primaryColor, @@ -167,6 +160,7 @@ function loadMapPreview(mapDiv) { } }).addTo(miniMap); + // Fits Map to Geometry Bounds const bounds = geojsonLayer.getBounds(); if (bounds.isValid()) { miniMap.fitBounds(bounds, { padding: [25, 25], maxZoom: 17 }); @@ -183,19 +177,21 @@ function loadMapPreview(mapDiv) { // ===================================================================== -// Block 7: Beitrags-Filter und Sortierung +// Block 5: Contributions Filter and Sorting // ===================================================================== -// Filtert Beitrags-Zeilen nach Status (pending, approved, rejected, all). -// Aktualisiert die sichtbare Anzahl im Sort-Control. +// Filters Contributions +let currentFilter = 'all'; function filterByStatus(status, tabButton) { currentFilter = status; + // Updates active Tab document.querySelectorAll('.filter-tab').forEach(function (el) { el.classList.remove('active'); }); tabButton.classList.add('active'); + // Shows or Hides Contribution Rows let visibleCount = 0; document.querySelectorAll('#contributions-container .contribution-row').forEach(function (row) { if (status === 'all' || row.dataset.status === status) { @@ -205,11 +201,13 @@ function filterByStatus(status, tabButton) { row.style.display = 'none'; } }); - + + // Updates Count Display document.getElementById('visible-count').textContent = visibleCount + ' Beiträge'; } -// Sortiert Beitrags-Zeilen im DOM nach Datum (auf-/absteigend) oder Kategorie. + +// Sorts Contributions function sortContributions(sortBy) { const container = document.getElementById('contributions-container'); const rows = Array.from(container.querySelectorAll('.contribution-row')); @@ -221,23 +219,25 @@ function sortContributions(sortBy) { return 0; }); + // Reappends sorted Rows rows.forEach(function (row) { container.appendChild(row); }); } // ===================================================================== -// Block 8: Kommentar-Filter und Sortierung +// Block 6: Comments Filter and Sorting // ===================================================================== -// Filtert Kommentar-Zeilen nach Status (pending, approved, rejected, all). -// Arbeitet ausschließlich auf dem #comment-filter-tabs Container, -// damit keine Kollision mit dem Beitrags-Filter entsteht. +// Filters Comments function filterCommentsByStatus(status, tabButton) { + + // Updates active Tab document.querySelectorAll('#comment-filter-tabs .filter-tab').forEach(function (el) { el.classList.remove('active'); }); tabButton.classList.add('active'); - + + // Shows or Hides Comments Rows let visibleCount = 0; document.querySelectorAll('.comment-mod-row').forEach(function (row) { if (status === 'all' || row.dataset.status === status) { @@ -247,11 +247,12 @@ function filterCommentsByStatus(status, tabButton) { row.style.display = 'none'; } }); - + // Updates Count Display document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare'; } -// Sortiert Kommentar-Zeilen nach Datum oder Beitrags-Titel. + +// Sorts Comments function sortCommentRows(sortBy) { const container = document.getElementById('comments-mod-container'); const rows = Array.from(container.querySelectorAll('.comment-mod-row')); @@ -262,49 +263,42 @@ function sortCommentRows(sortBy) { if (sortBy === 'contribution') return a.dataset.contribution.localeCompare(b.dataset.contribution); return 0; }); - + // Reappends sorted Rows rows.forEach(function (row) { container.appendChild(row); }); } // ===================================================================== -// Block 9: API-Hilfsfunktion +// Block 7: Helper Functions // ===================================================================== -// Sendet einen POST-Request an die API und gibt ein Promise zurück. -// Analog zu apiCall() in app.js, jedoch Promise-basiert statt Callback-basiert, -// da admin.js keine asynchrone Verkettung mit Callback-Chains benötigt. +// Sends a POST request to API +// promise-based instead of callback-based function apiCall(data) { const formData = new FormData(); for (const key in data) { formData.append(key, data[key]); } + return fetch(ADMIN_CONFIG.apiUrl, { method: 'POST', body: formData }) .then(function (r) { return r.json(); }); } -// ===================================================================== -// Block 10: Hilfsfunktion HTML-Escaping -// ===================================================================== - -// Verhindert XSS in dynamisch erzeugten SweetAlert-Dialogen. -// Identisch zu escapeHtml() in app.js — beide Dateien arbeiten unabhängig, -// daher keine gemeinsame Abhängigkeit eingeführt. +// Escapes HTML to prevent Cross-Site Scripting (XSS) in Popups and Lists function escapeHtml(text) { + if (!text) return ''; const div = document.createElement('div'); div.appendChild(document.createTextNode(text)); return div.innerHTML; } - // ===================================================================== -// Block 11: Beitrags-Status ändern +// Block 8: CRUD Operations for Contributions // ===================================================================== -// Ändert den Status eines Beitrags (approved / rejected / pending). -// Zeigt eine SweetAlert-Bestätigung und lädt die Seite nach Erfolg neu. +// STATUS: Changes Contribution Status function changeStatus(contributionId, newStatus) { const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' }; @@ -326,18 +320,14 @@ function changeStatus(contributionId, newStatus) { Swal.fire('Fehler', response.error, 'error'); return; } + // Reloads Page to reflect Changes location.reload(); }); }); } -// ===================================================================== -// Block 12: Beitrag bearbeiten -// ===================================================================== - -// Öffnet einen SweetAlert-Dialog zum Bearbeiten von Titel und Beschreibung. -// Werte werden mit escapeHtml() gesichert, bevor sie ins HTML eingefügt werden. +// UPDATE: Edits existing Contributions function editContribution(contributionId, currentTitle, currentDescription) { Swal.fire({ title: 'Beitrag bearbeiten', @@ -382,11 +372,7 @@ function editContribution(contributionId, currentTitle, currentDescription) { } -// ===================================================================== -// Block 13: Beitrag löschen -// ===================================================================== - -// Zeigt eine Lösch-Bestätigung und entfernt den Beitrag dauerhaft. +// DELETE: Deletes existing Contributions function deleteContribution(contributionId) { Swal.fire({ title: 'Beitrag löschen?', @@ -413,12 +399,11 @@ function deleteContribution(contributionId) { }); } - // ===================================================================== -// Block 14: Kommentar-Status ändern +// Block 9: CRUD Operations for Comments // ===================================================================== -// Ändert den Status eines Kommentars (approved / rejected / pending). +// STATUS: Changes Comment Status function changeCommentStatus(commentId, newStatus) { const labels = { approved: 'akzeptieren', rejected: 'ablehnen', pending: 'zurücksetzen' }; @@ -446,11 +431,7 @@ function changeCommentStatus(commentId, newStatus) { } -// ===================================================================== -// Block 15: Kommentar bearbeiten -// ===================================================================== - -// Öffnet einen Dialog zum Bearbeiten des Kommentar-Inhalts. +// UPDATE: Edits existing Comments function editModComment(commentId, currentContent) { Swal.fire({ title: 'Kommentar bearbeiten', @@ -485,11 +466,7 @@ function editModComment(commentId, currentContent) { } -// ===================================================================== -// Block 16: Kommentar löschen -// ===================================================================== - -// Zeigt eine Lösch-Bestätigung und entfernt den Kommentar dauerhaft. +// DELETE: Deletes existing Comments function deleteModComment(commentId) { Swal.fire({ title: 'Kommentar löschen?', @@ -518,11 +495,10 @@ function deleteModComment(commentId) { // ===================================================================== -// Block 17: Neuigkeit erstellen +// Block 10: CRUD Operations for News // ===================================================================== -// Öffnet einen Dialog zum Erstellen einer neuen Neuigkeit. -// Titel und Inhalt sind Pflichtfelder, Autor hat einen Standardwert. +// CREATE: Submits new News Article function createNews() { Swal.fire({ title: 'Neuigkeit hinzufügen', @@ -576,11 +552,7 @@ function createNews() { } -// ===================================================================== -// Block 18: Neuigkeit bearbeiten -// ===================================================================== - -// Öffnet einen Dialog zum Bearbeiten einer bestehenden Neuigkeit. +// UPDATE: Edits existing News function editNews(newsId, currentTitle, currentContent, currentAuthor) { Swal.fire({ title: 'Neuigkeit bearbeiten', @@ -631,13 +603,7 @@ function editNews(newsId, currentTitle, currentContent, currentAuthor) { } -// ===================================================================== -// Block 19: Neuigkeit löschen -// ===================================================================== - -// Zeigt eine Lösch-Bestätigung und entfernt die Neuigkeit dauerhaft. -// Hinweis: Im Original-Code war dieser Block fälschlicherweise mit -// "// Create News Article" kommentiert — hier korrigiert. +// DELETE: Deletes existing Comments function deleteNews(newsId) { Swal.fire({ title: 'Neuigkeit löschen?',