9 Commits

6 changed files with 567 additions and 54 deletions

View File

@@ -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);
-- Approves existing Comments
UPDATE comments SET status = 'approved';

View File

@@ -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();

View File

@@ -3,12 +3,6 @@
// Moderation Page
// Lists Contributions for Review. Moderators can approve, reject,
// edit and delete Contributions. Includes Map Preview and Filtering.
//
// ToDo's:
// - Comment Moderation Tab
// - News Management Tab
// - User Management Tab
// - Analytics Tab
// =====================================================================
// Reads Environment Configfile
@@ -57,6 +51,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 +62,30 @@ $stmt = $pdo->prepare("
$stmt->execute([':mid' => $municipality['municipality_id']]);
$news_items = $stmt->fetchAll();
// Loads all Comments with Contribution Titles for Moderation
$stmt = $pdo->prepare("
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()) {
show_login_page($municipality, $login_error ?? null);
@@ -84,8 +103,8 @@ $categories = get_categories();
// Loads all Contributions for Municipality
$stmt = $pdo->prepare("
SELECT contribution_id, title, category, description, author_name,
geom_type, status, likes_count, dislikes_count, created_at, updated_at
SELECT contribution_id, title, category, description, author_name, photo_path,
geom_type, status, likes_count, dislikes_count, comment_count, created_at, updated_at
FROM contributions
WHERE municipality_id = :mid
ORDER BY created_at DESC
@@ -159,6 +178,9 @@ $counts['total'] = count($all_contributions);
<button class="page-tab active" onclick="showPageTab('contributions')">
<i class="fa-solid fa-list-check"></i> Beiträge
</button>
<button class="page-tab" onclick="showPageTab('comments')">
<i class="fa-solid fa-comments"></i> Kommentare
</button>
<button class="page-tab" onclick="showPageTab('news')">
<i class="fa-solid fa-newspaper"></i> Neuigkeiten
</button>
@@ -176,27 +198,6 @@ $counts['total'] = count($all_contributions);
<!-- ========================================================= -->
<div id="tab-contributions" class="page-tab-content">
<!-- Statistics Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number"><?= $counts['total'] ?></div>
<div class="stat-label">Alle</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $counts['pending'] ?></div>
<div class="stat-label">Ausstehend</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $counts['approved'] ?></div>
<div class="stat-label">Akzeptiert</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $counts['rejected'] ?></div>
<div class="stat-label">Abgelehnt</div>
</div>
</div>
<!-- Status Filter Tabs -->
<div class="filter-tabs">
<button class="filter-tab active" onclick="filterByStatus('all', this)">
@@ -257,9 +258,28 @@ $counts['total'] = count($all_contributions);
<!-- Expanded Detail -->
<div class="contribution-row-detail">
<div class="detail-layout">
<!-- Map Preview -->
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
data-contribution-id="<?= $item['contribution_id'] ?>">
<!-- Map and Photo Slider -->
<div class="detail-slider" id="slider-<?= $item['contribution_id'] ?>">
<!-- Slide 1: Map -->
<div class="detail-slide active" data-slide="map">
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
data-contribution-id="<?= $item['contribution_id'] ?>">
</div>
</div>
<?php if (!empty($item['photo_path'])): ?>
<!-- Slide 2: Photo -->
<div class="detail-slide" data-slide="photo" style="display:none;">
<img src="<?= htmlspecialchars($item['photo_path']) ?>" alt="Foto"
class="detail-slide-photo" onclick="window.open('<?= htmlspecialchars($item['photo_path']) ?>', '_blank')">
</div>
<!-- Slider Arrows -->
<button class="slider-arrow slider-arrow-left" onclick="slideDetail(<?= $item['contribution_id'] ?>, -1)">
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="slider-arrow slider-arrow-right" onclick="slideDetail(<?= $item['contribution_id'] ?>, 1)">
<i class="fa-solid fa-chevron-right"></i>
</button>
<?php endif; ?>
</div>
<!-- Content -->
@@ -277,6 +297,10 @@ $counts['total'] = count($all_contributions);
<i class="fa-solid fa-thumbs-up"></i> <?= $item['likes_count'] ?>
&middot;
<i class="fa-solid fa-thumbs-down"></i> <?= $item['dislikes_count'] ?>
&middot;
<i class="fa-solid fa-comment"></i> <?= $item['comment_count'] ?? 0 ?>
</span>
</div>
</div>
@@ -297,7 +321,7 @@ $counts['total'] = count($all_contributions);
<?php endif; ?>
<?php if ($item['status'] !== 'pending'): ?>
<button class="btn btn-reset" onclick="changeStatus(..., 'pending')">
<button class="btn btn-reset" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'pending')">
<i class="fa-solid fa-rotate-left"></i> Zurücksetzen
</button>
<?php endif; ?>
@@ -322,6 +346,113 @@ $counts['total'] = count($all_contributions);
</div>
<!-- ========================================================= -->
<!-- Comments Moderation Tab -->
<!-- ========================================================= -->
<div id="tab-comments" class="page-tab-content" style="display:none;">
<!-- Status Filter Tabs for Comments -->
<div class="filter-tabs" id="comment-filter-tabs">
<button class="filter-tab active" onclick="filterCommentsByStatus('all', this)">
Alle <span class="tab-count"><?= $comment_counts['total'] ?></span>
</button>
<button class="filter-tab" onclick="filterCommentsByStatus('pending', this)">
Ausstehend <span class="tab-count"><?= $comment_counts['pending'] ?></span>
</button>
<button class="filter-tab" onclick="filterCommentsByStatus('approved', this)">
Akzeptiert <span class="tab-count"><?= $comment_counts['approved'] ?></span>
</button>
<button class="filter-tab" onclick="filterCommentsByStatus('rejected', this)">
Abgelehnt <span class="tab-count"><?= $comment_counts['rejected'] ?></span>
</button>
</div>
<!-- Sort Controls -->
<div class="sort-controls">
<span id="comment-visible-count"><?= $comment_counts['total'] ?> Kommentare</span>
<select onchange="sortCommentRows(this.value)">
<option value="date-desc">Neueste zuerst</option>
<option value="date-asc">Älteste zuerst</option>
<option value="contribution">Nach Beitrag</option>
</select>
</div>
<!-- Comments List -->
<div id="comments-mod-container">
<?php if (empty($all_comments)): ?>
<div class="empty-state">
<i class="fa-solid fa-comments" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
Noch keine Kommentare vorhanden.
</div>
<?php else: ?>
<?php foreach ($all_comments as $comment):
$comment_cat = $categories[$comment['contribution_category'] ?? ''] ?? ['label' => 'Unbekannt', 'faIcon' => 'fa-question', 'color' => '#999'];
$comment_status_label = ['pending' => 'Ausstehend', 'approved' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
?>
<div class="contribution-row comment-mod-row"
data-status="<?= $comment['status'] ?>"
data-date="<?= $comment['created_at'] ?>"
data-contribution="<?= htmlspecialchars($comment['contribution_title']) ?>">
<!-- Collapsed: Contribution Title + Comment Status + Category -->
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
<div class="contribution-row-summary">
<span class="title"><?= htmlspecialchars($comment['contribution_title']) ?></span>
<span class="badge badge-<?= $comment['status'] ?>"><?= $comment_status_label[$comment['status']] ?? $comment['status'] ?></span>
<span class="badge badge-category">
<i class="fa-solid <?= $comment_cat['faIcon'] ?>"></i>
<?= $comment_cat['label'] ?>
</span>
</div>
<i class="fa-solid fa-chevron-down collapse-icon"></i>
</div>
<!-- Expanded Detail -->
<div class="contribution-row-detail">
<div style="padding:12px 0;">
<!-- Comment Content -->
<div style="font-size:0.9rem;line-height:1.6;color:var(--color-text);margin-bottom:12px;">
<?= nl2br(htmlspecialchars($comment['content'])) ?>
</div>
<!-- Meta -->
<div class="detail-meta">
<span><i class="fa-solid fa-user"></i> <?= htmlspecialchars($comment['author_name']) ?></span>
<span><i class="fa-solid fa-calendar"></i> <?= date('d.m.Y, H:i', strtotime($comment['created_at'])) ?> Uhr</span>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<?php if ($comment['status'] !== 'approved'): ?>
<button class="btn btn-approve" onclick="changeCommentStatus(<?= $comment['comment_id'] ?>, 'approved')">
<i class="fa-solid fa-check"></i> Akzeptieren
</button>
<?php endif; ?>
<?php if ($comment['status'] !== 'rejected'): ?>
<button class="btn btn-reject" onclick="changeCommentStatus(<?= $comment['comment_id'] ?>, 'rejected')">
<i class="fa-solid fa-xmark"></i> Ablehnen
</button>
<?php endif; ?>
<?php if ($comment['status'] !== 'pending'): ?>
<button class="btn btn-reset" onclick="changeCommentStatus(<?= $comment['comment_id'] ?>, 'pending')">
<i class="fa-solid fa-rotate-left"></i> Zurücksetzen
</button>
<?php endif; ?>
<button class="btn btn-edit" onclick="editModComment(<?= $comment['comment_id'] ?>, '<?= htmlspecialchars(addslashes($comment['content']), ENT_QUOTES) ?>')">
<i class="fa-solid fa-pen"></i> Bearbeiten
</button>
<button class="btn btn-delete" onclick="deleteModComment(<?= $comment['comment_id'] ?>)">
<i class="fa-solid fa-trash"></i> Löschen
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- ========================================================= -->
<!-- News Article Tab -->
<!-- ========================================================= -->
@@ -405,13 +536,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';
});
@@ -452,6 +595,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)
// =============================================================
@@ -621,7 +797,7 @@ $counts['total'] = count($all_contributions);
// =============================================================
// Edit Contribution (Title and Description)
// Edit Contribution
// =============================================================
function editContribution(contributionId, currentTitle, currentDescription) {
@@ -847,6 +1023,154 @@ $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(); });
});
});
}
// =============================================================
// Filter Comments by Status
// =============================================================
function filterCommentsByStatus(status, tabButton) {
// Updates active Filter Tab (only within Comments Tab)
document.querySelectorAll('#comment-filter-tabs .filter-tab').forEach(function (el) {
el.classList.remove('active');
});
tabButton.classList.add('active');
// Shows/Hides Comment Rows
let visibleCount = 0;
document.querySelectorAll('.comment-mod-row').forEach(function (row) {
if (status === 'all' || row.dataset.status === status) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
document.getElementById('comment-visible-count').textContent = visibleCount + ' Kommentare';
}
// =============================================================
// Change Comment Status (approve, reject, reset)
// =============================================================
function changeCommentStatus(commentId, newStatus) {
const labels = { approved: 'akzeptieren', rejected: 'ablehnen', pending: 'zurücksetzen' };
Swal.fire({
title: 'Kommentar ' + labels[newStatus] + '?',
showCancelButton: true,
confirmButtonText: 'Ja',
cancelButtonText: 'Abbrechen',
confirmButtonColor: PRIMARY_COLOR
}).then(function (result) {
if (!result.isConfirmed) return;
apiCall({
action: 'update_comment',
comment_id: commentId,
status: newStatus
}).then(function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
location.reload();
});
});
}
// =============================================================
// Edit Comment Content
// =============================================================
function editModComment(commentId, currentContent) {
Swal.fire({
title: 'Kommentar bearbeiten',
html:
'<div style="text-align:left;">' +
'<label style="display:block;font-weight:600;font-size:1.15rem;margin-bottom:4px;">Inhalt</label>' +
'<textarea id="swal-comment-content" class="swal2-textarea" style="margin:0;width:100%;">' + currentContent + '</textarea>' +
'</div>',
showCancelButton: true,
confirmButtonText: 'Speichern',
cancelButtonText: 'Abbrechen',
confirmButtonColor: PRIMARY_COLOR,
preConfirm: function () {
return { content: document.getElementById('swal-comment-content').value.trim() };
}
}).then(function (result) {
if (!result.isConfirmed) return;
apiCall({
action: 'update_comment',
comment_id: commentId,
content: result.value.content
}).then(function (response) {
if (response.error) {
Swal.fire('Fehler', response.error, 'error');
return;
}
Swal.fire('Gespeichert!', 'Kommentar wurde aktualisiert.', 'success')
.then(function () { location.reload(); });
});
});
}
</script>
</body>

View File

@@ -56,6 +56,9 @@ switch ($action) {
case 'delete_comment':
handle_delete_comment($input);
break;
case 'update_comment':
handle_update_comment($input);
break;
default:
error_response('Unknown Action. Supported Actions are read, create, update, delete, vote.');
}
@@ -83,8 +86,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];
@@ -576,9 +579,9 @@ function handle_read_comments($input) {
try {
$stmt = $pdo->prepare("
SELECT comment_id, contribution_id, author_name, browser_id, content, created_at
SELECT comment_id, contribution_id, author_name, browser_id, content, status, created_at
FROM comments
WHERE contribution_id = :cid
WHERE contribution_id = :cid AND status = 'approved'
ORDER BY created_at ASC
");
$stmt->execute([':cid' => $input['contribution_id']]);
@@ -661,4 +664,49 @@ function handle_delete_comment($input) {
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
}
// ---------------------------------------------------------------------
// UPDATE COMMENT: Changes Comment Status or Content
// Required: comment_id
// Optional: status, content
// ---------------------------------------------------------------------
function handle_update_comment($input) {
$pdo = get_db();
$missing = validate_required($input, ['comment_id']);
if (!empty($missing)) {
error_response('Missing Fields: ' . implode(', ', $missing));
}
$set = [];
$params = [':id' => $input['comment_id']];
// Updates Status if provided
if (isset($input['status']) && $input['status'] !== '') {
$valid = ['pending', 'approved', 'rejected'];
if (!in_array($input['status'], $valid)) {
error_response('Invalid Status.');
}
$set[] = "status = :status";
$params[':status'] = $input['status'];
}
// Updates Content if provided
if (isset($input['content']) && $input['content'] !== '') {
$set[] = "content = :content";
$params[':content'] = $input['content'];
}
if (empty($set)) {
error_response('No Fields to update.');
}
try {
$stmt = $pdo->prepare("UPDATE comments SET " . implode(', ', $set) . " WHERE comment_id = :id");
$stmt->execute($params);
json_response(['message' => 'Comment updated successfully.']);
} catch (PDOException $e) {
error_response('Database Error: ' . $e->getMessage(), 500);
}
}

View File

@@ -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();
@@ -409,11 +410,6 @@ function buildPopupHtml(feature) {
if (props.photo_path) {
html += '<div class="popup-photo-container" id="photo-container-' + props.contribution_id + '" style="display:none;">' +
'<img src="' + escapeHtml(props.photo_path) + '" alt="Foto" class="popup-photo-img" onclick="window.open(\'' + escapeHtml(props.photo_path) + '\', \'_blank\')">' +
'</div>' +
'<div class="popup-photo-toggle">' +
'<button class="popup-photo-btn" onclick="togglePhoto(' + props.contribution_id + ')">' +
'<i class="fa-solid fa-camera"></i> <span id="photo-label-' + props.contribution_id + '">Foto anzeigen</span>' +
'</button>' +
'</div>';
}
@@ -423,15 +419,23 @@ function buildPopupHtml(feature) {
' &middot; <i class="fa-solid fa-calendar"></i> ' + dateStr +
'</div>';
// Vote Buttons
// Vote Buttons and Photo Toggle
html += '<div class="popup-detail-votes">' +
'<button class="popup-vote-btn' + (userVotes[props.contribution_id] === 'like' ? ' liked' : '') + '" id="vote-like-' + props.contribution_id + '" 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' + (userVotes[props.contribution_id] === 'dislike' ? ' disliked' : '') + '" id="vote-dislike-' + props.contribution_id + '" 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>';
'</button>';
// Photo Toggle Button
if (props.photo_path) {
html += '<button class="popup-vote-btn" onclick="togglePhoto(' + props.contribution_id + ')" title="Foto">' +
'<i class="fa-solid fa-camera"></i> <span id="photo-label-' + props.contribution_id + '">Foto anzeigen</span>' +
'</button>';
}
html += '</div>';
// Edit and Delete Buttons for Author or Admin
if (props.browser_id === browserId || (typeof IS_ADMIN !== 'undefined' && IS_ADMIN)) {
@@ -952,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
@@ -1126,9 +1131,14 @@ function submitComment(contributionId) {
Swal.fire('Fehler', response.error, 'error');
return;
}
// Clears Input and reloads Comments
if (input) input.value = '';
loadComments(contributionId);
Swal.fire({
title: 'Eingereicht!',
text: 'Ihr Kommentar wurde erfolgreich eingereicht und wird nach Prüfung durch das Moderationsteam veröffentlicht.',
icon: 'success',
timer: 3000,
showConfirmButton: true
});
});
}

View File

@@ -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;
@@ -1036,6 +1032,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 +1125,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; }