From be7bbfc28ba2327a0e70245521425bdb1c1e762e Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Mon, 27 Apr 2026 15:17:17 +0200 Subject: [PATCH 1/9] comment section in moderation portal --- public/admin.php | 141 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/public/admin.php b/public/admin.php index ce77702..7a17530 100644 --- a/public/admin.php +++ b/public/admin.php @@ -57,6 +57,7 @@ $stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug"); $stmt->execute([':slug' => getenv('MUNICIPALITY_SLUG')]); $municipality = $stmt->fetch(); + // Loads News for Moderation $stmt = $pdo->prepare(" SELECT news_id, title, content, author_name, published_at, created_at @@ -67,6 +68,20 @@ $stmt = $pdo->prepare(" $stmt->execute([':mid' => $municipality['municipality_id']]); $news_items = $stmt->fetchAll(); + +// Loads Comments with Contribution for Moderation +$stmt = $pdo->prepare(" + SELECT cm.comment_id, cm.contribution_id, cm.author_name, cm.browser_id, cm.content, cm.created_at, + co.title AS contribution_title + FROM comments cm + JOIN contributions co ON cm.contribution_id = co.contribution_id + WHERE co.municipality_id = :mid + ORDER BY cm.created_at DESC +"); +$stmt->execute([':mid' => $municipality['municipality_id']]); +$all_comments = $stmt->fetchAll(); + + // Shows Login Page if not authenticated if ($page === 'login' || !is_admin()) { show_login_page($municipality, $login_error ?? null); @@ -159,6 +174,9 @@ $counts['total'] = count($all_contributions); + @@ -322,6 +340,73 @@ $counts['total'] = count($all_contributions); + + + + + + @@ -621,7 +706,7 @@ $counts['total'] = count($all_contributions); // ============================================================= - // Edit Contribution (Title and Description) + // Edit Contribution // ============================================================= function editContribution(contributionId, currentTitle, currentDescription) { @@ -847,6 +932,60 @@ $counts['total'] = count($all_contributions); }); } + + // ============================================================= + // Sort Comments + // ============================================================= + + function sortCommentRows(sortBy) { + const container = document.getElementById('comments-mod-container'); + const rows = Array.from(container.querySelectorAll('.comment-mod-row')); + + rows.sort(function (a, b) { + if (sortBy === 'date-desc') { + return new Date(b.dataset.date) - new Date(a.dataset.date); + } else if (sortBy === 'date-asc') { + return new Date(a.dataset.date) - new Date(b.dataset.date); + } else if (sortBy === 'contribution') { + return a.dataset.contribution.localeCompare(b.dataset.contribution); + } + return 0; + }); + + rows.forEach(function (row) { container.appendChild(row); }); + } + + + // ============================================================= + // Delete Comments + // ============================================================= + + function deleteModComment(commentId) { + Swal.fire({ + title: 'Kommentar 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_comment', + comment_id: commentId + }).then(function (response) { + if (response.error) { + Swal.fire('Fehler', response.error, 'error'); + return; + } + Swal.fire('Gelöscht!', 'Kommentar wurde entfernt.', 'success') + .then(function () { location.reload(); }); + }); + }); + } + -- 2.49.1 From 879d7c585851cb3d07d4014da5712821f16c6122 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Mon, 27 Apr 2026 15:30:33 +0200 Subject: [PATCH 2/9] photos section in moderation portal with slider --- public/admin.php | 64 ++++++++++++++++++++++++++++++++++++++++++++--- public/styles.css | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/public/admin.php b/public/admin.php index 7a17530..c599583 100644 --- a/public/admin.php +++ b/public/admin.php @@ -99,7 +99,7 @@ $categories = get_categories(); // Loads all Contributions for Municipality $stmt = $pdo->prepare(" - SELECT contribution_id, title, category, description, author_name, + SELECT contribution_id, title, category, description, author_name, photo_path, geom_type, status, likes_count, dislikes_count, created_at, updated_at FROM contributions WHERE municipality_id = :mid @@ -275,9 +275,28 @@ $counts['total'] = count($all_contributions);
- -
+ +
+ +
+
+
+
+ + + + + + +
@@ -295,6 +314,10 @@ $counts['total'] = count($all_contributions); · + · + + +
@@ -537,6 +560,39 @@ $counts['total'] = count($all_contributions); } + // ============================================================= + // Detail Slider for Maps and Photos + // ============================================================= + + function slideDetail(contributionId, direction) { + const slider = document.getElementById('slider-' + contributionId); + if (!slider) return; + + const slides = slider.querySelectorAll('.detail-slide'); + let activeIndex = -1; + + // Finds currently active Slide + slides.forEach(function (slide, i) { + if (slide.style.display !== 'none') activeIndex = i; + }); + + // Calculates next Slide Index (wraps around) + const nextIndex = (activeIndex + direction + slides.length) % slides.length; + + // Switches Slides + slides.forEach(function (slide) { slide.style.display = 'none'; }); + slides[nextIndex].style.display = 'block'; + + // Loads Map if switching to Map Slide and not yet loaded + if (slides[nextIndex].dataset.slide === 'map') { + const mapDiv = slides[nextIndex].querySelector('.detail-map'); + if (mapDiv && !mapDiv.dataset.loaded) { + loadMapPreview(mapDiv); + } + } + } + + // ============================================================= // Map Preview (Leaflet Mini Map per Contribution) // ============================================================= diff --git a/public/styles.css b/public/styles.css index c65704b..4ec85bc 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1036,6 +1036,60 @@ select.form-input { cursor: pointer; } .back-link a { color: var(--color-text-secondary); } +/* ----------------------------------------------------------------- + 5.8 Detail Slider (Map/Photo in Admin) + ----------------------------------------------------------------- */ +.detail-slider { + width: 220px; + height: 170px; + flex-shrink: 0; + position: relative; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--color-border); + background: #f0f0f0; +} + +.detail-slide { width: 100%; height: 100%; } + +.detail-slide-photo { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; +} + +.detail-slider .detail-map { + width: 100%; + height: 100%; + border: none; + border-radius: 0; +} + +.slider-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + width: 28px; + height: 28px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + z-index: 1000; + transition: background var(--transition-fast); +} + +.slider-arrow:hover { background: rgba(0, 0, 0, 0.7); } +.slider-arrow-left { left: 4px; } +.slider-arrow-right { right: 4px; } + + /* ================================================================= SECTION 6: Responsive Overrides ================================================================= */ @@ -1075,6 +1129,8 @@ select.form-input { cursor: pointer; } .action-buttons .btn { justify-content: center; } .filter-tabs { overflow-x: auto; } .page-tabs { overflow-x: auto; } + .detail-slider { width: 100%; height: 200px; } + /* Legal */ .page-content-box { padding: 20px; } -- 2.49.1 From b18811c453cacd565bf189555db666c19660511a Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Tue, 28 Apr 2026 15:15:33 +0200 Subject: [PATCH 3/9] fixed comments count in citizen portal --- migrations/007_comment_moderation.sql | 14 +++++ migrations/008_comment_count_trigger.sql | 65 ++++++++++++++++++++++++ public/admin.php | 12 ++--- public/api/contributions.php | 4 +- 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 migrations/007_comment_moderation.sql create mode 100644 migrations/008_comment_count_trigger.sql diff --git a/migrations/007_comment_moderation.sql b/migrations/007_comment_moderation.sql new file mode 100644 index 0000000..78356a1 --- /dev/null +++ b/migrations/007_comment_moderation.sql @@ -0,0 +1,14 @@ +-- ===================================================================== +-- Migration 007: Adds Status Column to Comments for Moderation +-- ===================================================================== + +-- Adds Status Column with Default 'pending' +ALTER TABLE comments + ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'approved', 'rejected')); + +-- Index for fast Status Filtering +CREATE INDEX idx_comments_status ON comments(status); + +-- Sets all existing Comments to 'approved' (retroactive) +UPDATE comments SET status = 'approved'; \ No newline at end of file diff --git a/migrations/008_comment_count_trigger.sql b/migrations/008_comment_count_trigger.sql new file mode 100644 index 0000000..04dba3b --- /dev/null +++ b/migrations/008_comment_count_trigger.sql @@ -0,0 +1,65 @@ +-- ===================================================================== +-- Migration 008: Adds comment_count Column with automatic Trigger +-- Mirrors Pattern from likes_count and dislikes_count. +-- ===================================================================== + + +-- --------------------------------------------------------------------- +-- Block 1: Adds comment_count Column to Contributions +-- --------------------------------------------------------------------- +ALTER TABLE contributions + ADD COLUMN comment_count INTEGER NOT NULL DEFAULT 0; + + +-- --------------------------------------------------------------------- +-- Block 2: Backfills existing Comment Counts +-- --------------------------------------------------------------------- +UPDATE contributions c +SET comment_count = ( + SELECT COUNT(*) + FROM comments cm + WHERE cm.contribution_id = c.contribution_id + AND cm.status = 'approved' +); + + +-- --------------------------------------------------------------------- +-- Block 3: Trigger Function to update comment_count +-- Fires on Status Change on comments. Only counts approved Comments +-- --------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION update_comment_count() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN + UPDATE contributions + SET comment_count = ( + SELECT COUNT(*) FROM comments + WHERE contribution_id = NEW.contribution_id + AND status = 'approved' + ) + WHERE contribution_id = NEW.contribution_id; + END IF; + + IF TG_OP = 'DELETE' OR (TG_OP = 'UPDATE' AND OLD.contribution_id != NEW.contribution_id) THEN + UPDATE contributions + SET comment_count = ( + SELECT COUNT(*) FROM comments + WHERE contribution_id = OLD.contribution_id + AND status = 'approved' + ) + WHERE contribution_id = OLD.contribution_id; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + + +-- --------------------------------------------------------------------- +-- Block 4: Attaches Trigger to comments Table +-- --------------------------------------------------------------------- +CREATE TRIGGER trigger_update_comment_count + AFTER INSERT OR DELETE OR UPDATE OF status + ON comments + FOR EACH ROW + EXECUTE FUNCTION update_comment_count(); \ No newline at end of file diff --git a/public/admin.php b/public/admin.php index c599583..8da4f3c 100644 --- a/public/admin.php +++ b/public/admin.php @@ -71,12 +71,12 @@ $news_items = $stmt->fetchAll(); // Loads Comments with Contribution for Moderation $stmt = $pdo->prepare(" - SELECT cm.comment_id, cm.contribution_id, cm.author_name, cm.browser_id, cm.content, cm.created_at, - co.title AS contribution_title - FROM comments cm - JOIN contributions co ON cm.contribution_id = co.contribution_id - WHERE co.municipality_id = :mid - ORDER BY cm.created_at DESC + SELECT contribution_id, title, category, description, author_name, + geom_type, status, likes_count, dislikes_count, comment_count, + photo_path, created_at, updated_at + FROM contributions + WHERE municipality_id = :mid + ORDER BY created_at DESC "); $stmt->execute([':mid' => $municipality['municipality_id']]); $all_comments = $stmt->fetchAll(); diff --git a/public/api/contributions.php b/public/api/contributions.php index 2c1620f..8a4f662 100644 --- a/public/api/contributions.php +++ b/public/api/contributions.php @@ -83,8 +83,8 @@ function handle_read($input) { $municipality_id = $input['municipality_id']; // Builds SQL Query with Placeholders for prepared Statement - $sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson - FROM contributions + $sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson + FROM contributions WHERE municipality_id = :mid"; $params = [':mid' => $municipality_id]; -- 2.49.1 From e68ddd0ccf8ec451cabadf165bfd3a9727d65204 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Tue, 28 Apr 2026 15:21:41 +0200 Subject: [PATCH 4/9] changed position of photo toggle button --- public/js/app.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 5b2c2e4..2d55fa9 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -409,11 +409,6 @@ function buildPopupHtml(feature) { if (props.photo_path) { html += '' + - ''; } @@ -423,15 +418,23 @@ function buildPopupHtml(feature) { ' · ' + dateStr + '
'; - // Vote Buttons + // Vote Buttons and Photo Toggle html += ''; + ''; + + // Photo Toggle Button + if (props.photo_path) { + html += ''; + } + + html += ''; // Edit and Delete Buttons for Author or Admin if (props.browser_id === browserId || (typeof IS_ADMIN !== 'undefined' && IS_ADMIN)) { -- 2.49.1 From 9463530ee5af129999cfd06bc762580fb0db03e7 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Tue, 28 Apr 2026 15:22:33 +0200 Subject: [PATCH 5/9] changed position of photo toggle button --- public/styles.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/styles.css b/public/styles.css index 4ec85bc..a967582 100644 --- a/public/styles.css +++ b/public/styles.css @@ -634,10 +634,6 @@ select.form-input { cursor: pointer; } ----------------------------------------------------------------- */ /* Photo Toggle Button */ -.popup-photo-toggle { - margin: var(--space-sm) 0; -} - .popup-photo-btn { display: inline-flex; align-items: center; -- 2.49.1 From bc37051619bf700874951c05cec5a66c17a67135 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Tue, 28 Apr 2026 15:25:27 +0200 Subject: [PATCH 6/9] username now saved in cookie --- public/js/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/js/app.js b/public/js/app.js index 2d55fa9..96e2db6 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -17,7 +17,8 @@ const API_URL = 'api/contributions.php'; // Username set via Login Modal stored in sessionStorage -let currentUser = sessionStorage.getItem('webgis_user') || ''; +let currentUser = sessionStorage.getItem('webgis_user') || + decodeURIComponent(document.cookie.replace(/(?:(?:^|.*;\s*)webgis_user\s*=\s*([^;]*).*$)|^.*$/, '$1')) || ''; // Browser Identification Number for anonymous User Identification stored as Cookie let browserId = getBrowserId(); @@ -955,6 +956,7 @@ function submitLogin() { } currentUser = name; sessionStorage.setItem('webgis_user', currentUser); + document.cookie = 'webgis_user=' + encodeURIComponent(name) + ';path=/;max-age=31536000;SameSite=Lax'; document.getElementById('login-modal').style.display = 'none'; // Open Create Modal if Geometry is pending -- 2.49.1 From 5b77b0b524a04c41bb764e5436fecbd4599f7219 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Tue, 28 Apr 2026 15:32:24 +0200 Subject: [PATCH 7/9] page tab in moderation portal saved for persistence after reload --- public/admin.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/admin.php b/public/admin.php index 8da4f3c..728d780 100644 --- a/public/admin.php +++ b/public/admin.php @@ -513,13 +513,25 @@ $counts['total'] = count($all_contributions); // Current Status Filter let currentFilter = 'all'; + // Restores active Tab after Page Reload + const savedTab = sessionStorage.getItem('admin_active_tab'); + if (savedTab) { + // Delays to ensure DOM is ready + setTimeout(function () { + const tabBtn = document.querySelector('.page-tab[onclick*="' + savedTab + '"]'); + if (tabBtn) tabBtn.click(); + }, 100); + } + // ============================================================= // Page Tab Navigation // ============================================================= function showPageTab(tabName) { - // Hides all Tab Contents + // 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'; }); -- 2.49.1 From 950ac25828de0d7cff25ef3f63c9e9ca93fce471 Mon Sep 17 00:00:00 2001 From: patrickzerhusen Date: Tue, 28 Apr 2026 15:50:58 +0200 Subject: [PATCH 8/9] fixed comment count bug in moderation portal --- migrations/007_comment_moderation.sql | 2 +- public/admin.php | 210 ++++++++++++++++++++------ public/api/contributions.php | 52 ++++++- public/js/app.js | 9 +- 4 files changed, 226 insertions(+), 47 deletions(-) diff --git a/migrations/007_comment_moderation.sql b/migrations/007_comment_moderation.sql index 78356a1..97af7d4 100644 --- a/migrations/007_comment_moderation.sql +++ b/migrations/007_comment_moderation.sql @@ -10,5 +10,5 @@ ALTER TABLE comments -- Index for fast Status Filtering CREATE INDEX idx_comments_status ON comments(status); --- Sets all existing Comments to 'approved' (retroactive) +-- Approves existing Comments UPDATE comments SET status = 'approved'; \ No newline at end of file diff --git a/public/admin.php b/public/admin.php index 728d780..8c5e615 100644 --- a/public/admin.php +++ b/public/admin.php @@ -69,18 +69,28 @@ $stmt->execute([':mid' => $municipality['municipality_id']]); $news_items = $stmt->fetchAll(); -// Loads Comments with Contribution for Moderation +// Loads all Comments with Contribution Titles for Moderation $stmt = $pdo->prepare(" - SELECT contribution_id, title, category, description, author_name, - geom_type, status, likes_count, dislikes_count, comment_count, - photo_path, created_at, updated_at - FROM contributions - WHERE municipality_id = :mid - ORDER BY created_at DESC + SELECT cm.comment_id, cm.contribution_id, cm.author_name, cm.browser_id, + cm.content, cm.status, cm.created_at, + co.title AS contribution_title, co.category AS contribution_category + FROM comments cm + JOIN contributions co ON cm.contribution_id = co.contribution_id + WHERE co.municipality_id = :mid + ORDER BY cm.created_at DESC "); $stmt->execute([':mid' => $municipality['municipality_id']]); $all_comments = $stmt->fetchAll(); +// Counts Comments per Status +$comment_counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0]; +foreach ($all_comments as $c) { + if (isset($comment_counts[$c['status']])) { + $comment_counts[$c['status']]++; + } +} +$comment_counts['total'] = count($all_comments); + // Shows Login Page if not authenticated if ($page === 'login' || !is_admin()) { @@ -100,7 +110,7 @@ $categories = get_categories(); // Loads all Contributions for Municipality $stmt = $pdo->prepare(" SELECT contribution_id, title, category, description, author_name, photo_path, - geom_type, status, likes_count, dislikes_count, created_at, updated_at + geom_type, status, likes_count, dislikes_count, comment_count, created_at, updated_at FROM contributions WHERE municipality_id = :mid ORDER BY created_at DESC @@ -194,27 +204,6 @@ $counts['total'] = count($all_contributions);
- -
-
-
-
Alle
-
-
-
-
Ausstehend
-
-
-
-
Akzeptiert
-
-
-
-
Abgelehnt
-
- -
-
@@ -363,14 +352,33 @@ $counts['total'] = count($all_contributions);
+ + + - - - -- 2.49.1