restructured admin.js
This commit is contained in:
@@ -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?',
|
||||
|
||||
Reference in New Issue
Block a user