new moderation portal with CRUD operations and map preview function, bugfixes, comments and improved textblocks #7
512
public/admin.css
512
public/admin.css
@@ -1,7 +1,12 @@
|
||||
/* =====================================================================
|
||||
Moderation Page Styles
|
||||
Moderation Page — Styles
|
||||
Separate Stylesheet for the Admin Moderation Interface.
|
||||
===================================================================== */
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Base
|
||||
----------------------------------------------------------------- */
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
@@ -11,44 +16,52 @@ body {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Header
|
||||
----------------------------------------------------------------- */
|
||||
.admin-header {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
padding: 14px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.admin-header h1 { font-size: 1.2rem; }
|
||||
|
||||
.admin-header a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
font-size: 0.85rem;
|
||||
.admin-header h1 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-header a:hover { opacity: 1; }
|
||||
|
||||
.admin-nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.admin-nav a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
font-size: 0.85rem;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.admin-nav a:hover { opacity: 1; }
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Container
|
||||
----------------------------------------------------------------- */
|
||||
.admin-container {
|
||||
max-width: 900px;
|
||||
max-width: 960px;
|
||||
margin: 24px auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Statistics Cards
|
||||
----------------------------------------------------------------- */
|
||||
@@ -56,7 +69,7 @@ body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -79,17 +92,230 @@ body {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Section Headers
|
||||
Filter Tabs
|
||||
----------------------------------------------------------------- */
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section { margin-bottom: 40px; }
|
||||
.filter-tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #5a5a7a;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: color 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.filter-tab .tab-count {
|
||||
background: #e0e0e0;
|
||||
color: #5a5a7a;
|
||||
font-size: 0.7rem;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.filter-tab.active .tab-count {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Sort Controls
|
||||
----------------------------------------------------------------- */
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.85rem;
|
||||
color: #5a5a7a;
|
||||
}
|
||||
|
||||
.sort-controls select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Collapsible Contribution Rows
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.contribution-row:hover {
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.contribution-row-header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.contribution-row-header:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.contribution-row-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.contribution-row-summary .title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
color: #999;
|
||||
font-size: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
.contribution-row.open .collapse-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Contribution Detail View (expanded)
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row-detail {
|
||||
padding: 0 16px 16px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.contribution-row.open .contribution-row-detail {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-map {
|
||||
width: 220px;
|
||||
height: 170px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-content .description {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: #5a5a7a;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.detail-content .description.empty {
|
||||
color: #bbb;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Action Buttons
|
||||
----------------------------------------------------------------- */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 7px 14px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: inherit;
|
||||
transition: filter 150ms ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover { filter: brightness(1.1); }
|
||||
|
||||
.btn-approve { background: #2e7d32; color: white; }
|
||||
.btn-reject { background: #c62828; color: white; }
|
||||
.btn-edit { background: #1565C0; color: white; }
|
||||
.btn-delete { background: #424242; color: white; }
|
||||
.btn-map { background: #546E7A; color: white; }
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Badges
|
||||
@@ -102,41 +328,13 @@ h2 {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-pending { background: #fff3cd; color: #856404; }
|
||||
.badge-approved { background: #d4edda; color: #155724; }
|
||||
.badge-rejected { background: #f8d7da; color: #721c24; }
|
||||
.badge-point { background: #e3f2fd; color: #1565c0; }
|
||||
.badge-line { background: #f3e5f5; color: #6a1b9a; }
|
||||
.badge-polygon { background: #e8f5e9; color: #2e7d32; }
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Action Buttons
|
||||
----------------------------------------------------------------- */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.btn-approve { background: #2e7d32; color: white; }
|
||||
.btn-approve:hover { background: #1b5e20; }
|
||||
|
||||
.btn-reject { background: #c62828; color: white; }
|
||||
.btn-reject:hover { background: #b71c1c; }
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Empty State
|
||||
@@ -148,6 +346,70 @@ h2 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Section Spacing
|
||||
----------------------------------------------------------------- */
|
||||
.section { margin-bottom: 32px; }
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Placeholder Tabs (future Features)
|
||||
----------------------------------------------------------------- */
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.placeholder-content i {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Navigation Tabs (Page Sections)
|
||||
----------------------------------------------------------------- */
|
||||
.page-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
background: white;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.page-tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #5a5a7a;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 150ms ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.page-tab:hover { background: #f0f0f0; }
|
||||
|
||||
.page-tab.active {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Login Page
|
||||
----------------------------------------------------------------- */
|
||||
@@ -186,7 +448,7 @@ h2 {
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 12px;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.login-box input:focus {
|
||||
@@ -205,12 +467,12 @@ h2 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.login-box button:hover { filter: brightness(1.15); }
|
||||
|
||||
.error {
|
||||
.login-error {
|
||||
color: #c62828;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 12px;
|
||||
@@ -223,127 +485,19 @@ h2 {
|
||||
|
||||
.back-link a { color: #5a5a7a; }
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Mobile Responsive
|
||||
----------------------------------------------------------------- */
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-buttons form {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Collapsible Contribution Rows
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row-collapsible {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contribution-row-header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.contribution-row-header:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.contribution-row-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contribution-row-summary .title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.meta-inline {
|
||||
font-size: 0.8rem;
|
||||
color: #5a5a7a;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
color: #999;
|
||||
font-size: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Contribution Detail View (expanded)
|
||||
----------------------------------------------------------------- */
|
||||
.contribution-row-detail {
|
||||
padding: 0 16px 16px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.detail-map {
|
||||
width: 200px;
|
||||
height: 160px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-content .description {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: #5a5a7a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-content .meta {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-map {
|
||||
background: #1565C0;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-map:hover {
|
||||
background: #0d47a1;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Mobile: Detail Layout stacked
|
||||
----------------------------------------------------------------- */
|
||||
@media (max-width: 768px) {
|
||||
.admin-header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.admin-header h1 { font-size: 1rem; }
|
||||
|
||||
.detail-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -353,9 +507,23 @@ h2 {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.contribution-row-summary {
|
||||
.contribution-row-summary .title {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.page-tabs {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
710
public/admin.php
710
public/admin.php
@@ -1,20 +1,26 @@
|
||||
<?php
|
||||
// =====================================================================
|
||||
// Moderation Page
|
||||
// Lists pending Contributions for Review. Moderators can approve
|
||||
// or reject Contributions.
|
||||
// ToDo: Extend with News Management, User Management, Analytics.
|
||||
// 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
|
||||
// =====================================================================
|
||||
|
||||
require_once __DIR__ . '/api/db.php';
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Routing: Login, Logout, or Main Page
|
||||
// -----------------------------------------------------------------
|
||||
$page = $_GET['page'] ?? 'main';
|
||||
|
||||
// Handle Login Form Submission
|
||||
// Handles Login
|
||||
if ($page === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$password = $_POST['password'] ?? '';
|
||||
if (admin_login($password)) {
|
||||
@@ -25,7 +31,7 @@ if ($page === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Logout
|
||||
// Handles Logout
|
||||
if ($page === 'logout') {
|
||||
admin_logout();
|
||||
header('Location: admin.php?page=login');
|
||||
@@ -33,88 +39,50 @@ if ($page === 'logout') {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Load Municipality for Theming (needed for both Login and Main Page)
|
||||
// Loads Municipality Configuration for Theming
|
||||
// -----------------------------------------------------------------
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
|
||||
$stmt->execute([':slug' => 'lohne']);
|
||||
$municipality = $stmt->fetch();
|
||||
|
||||
// Show Login Page if not authenticated
|
||||
// Shows Login Page if not authenticated
|
||||
if ($page === 'login' || !is_admin()) {
|
||||
show_login_page($municipality, $login_error ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Handle Moderation Actions (Approve / Reject)
|
||||
// Loads shared Category Definitions
|
||||
// -----------------------------------------------------------------
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mod_action'])) {
|
||||
$contribution_id = $_POST['contribution_id'] ?? '';
|
||||
$mod_action = $_POST['mod_action'];
|
||||
|
||||
if ($contribution_id && in_array($mod_action, ['approved', 'rejected'])) {
|
||||
$stmt = $pdo->prepare("UPDATE contributions SET status = :status WHERE contribution_id = :id");
|
||||
$stmt->execute([':status' => $mod_action, ':id' => $contribution_id]);
|
||||
}
|
||||
|
||||
// Redirects to prevent Form Resubmission on Refresh
|
||||
header('Location: admin.php');
|
||||
exit;
|
||||
}
|
||||
$categories = get_categories();
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Load Contributions Data
|
||||
// Loads Contributions and Statistics
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// Pending Contributions
|
||||
// Loads all Contributions for Municipality
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at
|
||||
FROM contributions
|
||||
WHERE municipality_id = :mid AND status = 'pending'
|
||||
SELECT contribution_id, title, category, description, author_name,
|
||||
geom_type, status, likes_count, dislikes_count, created_at, updated_at
|
||||
FROM contributions
|
||||
WHERE municipality_id = :mid
|
||||
ORDER BY created_at DESC
|
||||
");
|
||||
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||
$pending = $stmt->fetchAll();
|
||||
$all_contributions = $stmt->fetchAll();
|
||||
|
||||
// Recently moderated Contributions
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at, updated_at
|
||||
FROM contributions
|
||||
WHERE municipality_id = :mid AND status IN ('approved', 'rejected')
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 20
|
||||
");
|
||||
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||
$moderated = $stmt->fetchAll();
|
||||
|
||||
// Statistics
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM contributions
|
||||
WHERE municipality_id = :mid
|
||||
GROUP BY status
|
||||
");
|
||||
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||
$stats_rows = $stmt->fetchAll();
|
||||
$stats = [];
|
||||
foreach ($stats_rows as $row) {
|
||||
$stats[$row['status']] = $row['count'];
|
||||
// Counts per Status
|
||||
$counts = ['pending' => 0, 'approved' => 0, 'rejected' => 0];
|
||||
foreach ($all_contributions as $item) {
|
||||
if (isset($counts[$item['status']])) {
|
||||
$counts[$item['status']]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Category Labels for German Display
|
||||
$category_labels = [
|
||||
'mobility' => '🚲 Mobilität',
|
||||
'building' => '🏗️ Bauen',
|
||||
'energy' => '⚡ Energie',
|
||||
'environment' => '🌳 Umwelt',
|
||||
'industry' => '🏭 Industrie',
|
||||
'consumption' => '🛒 Konsum',
|
||||
'other' => '📌 Sonstiges'
|
||||
];
|
||||
$counts['total'] = count($all_contributions);
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Render Main Page
|
||||
// Renders Main Page
|
||||
// -----------------------------------------------------------------
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
@@ -123,192 +91,315 @@ $category_labels = [
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Moderation — <?= htmlspecialchars($municipality['name']) ?></title>
|
||||
<link rel="icon" href="assets/icon-municipality.png" type="image/png">
|
||||
|
||||
|
||||
<!-- Loads CSS Dependencies -->
|
||||
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
|
||||
|
||||
<!-- Application Styles -->
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
|
||||
|
||||
<!-- Loads JavaScript Dependencies -->
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.14.0/dist/sweetalert2.all.min.js"></script>
|
||||
|
||||
|
||||
<!-- Loads Municipality Theme from Database -->
|
||||
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- Header -->
|
||||
<!-- ============================================================= -->
|
||||
<div class="admin-header">
|
||||
<h1><i class="fa-solid fa-shield-halved"></i> Moderation — <?= htmlspecialchars($municipality['name']) ?></h1>
|
||||
<h1><i class="fa-solid fa-shield-halved"></i> Moderationsportal <?= htmlspecialchars($municipality['name']) ?></h1>
|
||||
<div class="admin-nav">
|
||||
<a href="index.php"><i class="fa-solid fa-map"></i> Zur Karte</a>
|
||||
<a href="index.php"><i class="fa-solid fa-map"></i> Bürgerportal</a>
|
||||
<a href="admin.php?page=logout"><i class="fa-solid fa-right-from-bracket"></i> Abmelden</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-container">
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><?= ($stats['pending'] ?? 0) ?></div>
|
||||
<div class="stat-label">Ausstehend</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><?= ($stats['approved'] ?? 0) ?></div>
|
||||
<div class="stat-label">Freigegeben</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><?= ($stats['rejected'] ?? 0) ?></div>
|
||||
<div class="stat-label">Abgelehnt</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number"><?= array_sum(array_column($stats_rows, 'count')) ?></div>
|
||||
<div class="stat-label">Gesamt</div>
|
||||
</div>
|
||||
<!-- ========================================================= -->
|
||||
<!-- Page Navigation Tabs -->
|
||||
<!-- ========================================================= -->
|
||||
<div class="page-tabs">
|
||||
<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('news')">
|
||||
<i class="fa-solid fa-newspaper"></i> Neuigkeiten
|
||||
</button>
|
||||
<button class="page-tab" onclick="showPageTab('stats')">
|
||||
<i class="fa-solid fa-chart-bar"></i> Statistik
|
||||
</button>
|
||||
<button class="page-tab" onclick="showPageTab('users')">
|
||||
<i class="fa-solid fa-users"></i> Benutzer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pending Contributions -->
|
||||
<div class="section">
|
||||
<h2><i class="fa-solid fa-clock"></i> Ausstehende Beiträge (<?= count($pending) ?>)</h2>
|
||||
|
||||
<?php if (empty($pending)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-check-circle" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
||||
Keine ausstehenden Beiträge.
|
||||
<!-- ========================================================= -->
|
||||
<!-- Contributions Tab -->
|
||||
<!-- ========================================================= -->
|
||||
<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>
|
||||
<?php else: ?>
|
||||
<?php foreach ($pending as $item): ?>
|
||||
<div class="contribution-row-collapsible">
|
||||
<div class="contribution-row-header" onclick="toggleContribution(this)">
|
||||
<div class="contribution-row-summary">
|
||||
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
||||
<span class="badge badge-pending">ausstehend</span>
|
||||
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
||||
<span class="meta-inline">
|
||||
<?= $category_labels[$item['category']] ?? $item['category'] ?>
|
||||
· <?= htmlspecialchars($item['author_name']) ?>
|
||||
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||
</span>
|
||||
</div>
|
||||
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
||||
</div>
|
||||
<div class="contribution-row-detail" style="display:none;">
|
||||
<div class="detail-layout">
|
||||
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
|
||||
data-contribution-id="<?= $item['contribution_id'] ?>">
|
||||
<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)">
|
||||
Alle <span class="tab-count"><?= $counts['total'] ?></span>
|
||||
</button>
|
||||
<button class="filter-tab" onclick="filterByStatus('pending', this)">
|
||||
Ausstehend <span class="tab-count"><?= $counts['pending'] ?></span>
|
||||
</button>
|
||||
<button class="filter-tab" onclick="filterByStatus('approved', this)">
|
||||
Akzeptiert <span class="tab-count"><?= $counts['approved'] ?></span>
|
||||
</button>
|
||||
<button class="filter-tab" onclick="filterByStatus('rejected', this)">
|
||||
Abgelehnt <span class="tab-count"><?= $counts['rejected'] ?></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sort Controls -->
|
||||
<div class="sort-controls">
|
||||
<span id="visible-count"><?= $counts['total'] ?> Beiträge</span>
|
||||
<select onchange="sortContributions(this.value)">
|
||||
<option value="date-desc">Neueste zuerst</option>
|
||||
<option value="date-asc">Älteste zuerst</option>
|
||||
<option value="category">Nach Kategorie</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Contribution List -->
|
||||
<div id="contributions-container">
|
||||
<?php if (empty($all_contributions)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-inbox" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
||||
Noch keine Beiträge vorhanden.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($all_contributions as $item):
|
||||
$cat = $categories[$item['category']] ?? ['label' => $item['category'], 'faIcon' => 'fa-question', 'color' => '#999'];
|
||||
$status_label = ['pending' => 'Ausstehend', 'approved' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
?>
|
||||
<div class="contribution-row"
|
||||
data-status="<?= $item['status'] ?>"
|
||||
data-category="<?= htmlspecialchars($item['category']) ?>"
|
||||
data-date="<?= $item['created_at'] ?>"
|
||||
data-id="<?= $item['contribution_id'] ?>">
|
||||
|
||||
<!-- Collapsed Header: Title + Status -->
|
||||
<div class="contribution-row-header" onclick="toggleRow(this.parentElement)">
|
||||
<div class="contribution-row-summary">
|
||||
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
||||
<span class="badge badge-<?= $item['status'] ?>"><?= $status_label[$item['status']] ?? $item['status'] ?></span>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<?php if ($item['description']): ?>
|
||||
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="description" style="color:#bbb;font-style:italic;">Keine Beschreibung.</div>
|
||||
<?php endif; ?>
|
||||
<div class="meta">
|
||||
<i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?>
|
||||
· <i class="fa-solid fa-calendar"></i> <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
||||
</div>
|
||||
|
||||
<!-- 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'] ?>">
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="detail-content">
|
||||
<?php if ($item['description']): ?>
|
||||
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="description empty">Keine Beschreibung vorhanden.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="detail-meta">
|
||||
<span>
|
||||
<i class="fa-solid <?= $cat['faIcon'] ?>" style="color:<?= $cat['color'] ?>;"></i>
|
||||
<?= $cat['label'] ?>
|
||||
</span>
|
||||
<span><i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?></span>
|
||||
<span><i class="fa-solid fa-calendar"></i> <?= date('d.m.Y, H:i', strtotime($item['created_at'])) ?> Uhr</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-thumbs-up"></i> <?= $item['likes_count'] ?>
|
||||
·
|
||||
<i class="fa-solid fa-thumbs-down"></i> <?= $item['dislikes_count'] ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
|
||||
<input type="hidden" name="mod_action" value="approved">
|
||||
<button type="submit" class="btn btn-approve"><i class="fa-solid fa-check"></i> Freigeben</button>
|
||||
</form>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
|
||||
<input type="hidden" name="mod_action" value="rejected">
|
||||
<button type="submit" class="btn btn-reject"><i class="fa-solid fa-xmark"></i> Ablehnen</button>
|
||||
</form>
|
||||
<a href="index.php#contribution-<?= $item['contribution_id'] ?>" class="btn btn-map" target="_blank">
|
||||
<i class="fa-solid fa-map-location-dot"></i> Auf Karte
|
||||
</a>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<?php if ($item['status'] !== 'approved'): ?>
|
||||
<button class="btn btn-approve" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'approved')">
|
||||
<i class="fa-solid fa-check"></i> Akzeptieren
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item['status'] !== 'rejected'): ?>
|
||||
<button class="btn btn-reject" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'rejected')">
|
||||
<i class="fa-solid fa-xmark"></i> Ablehnen
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($item['status'] !== 'pending'): ?>
|
||||
<button class="btn btn-edit" onclick="changeStatus(<?= $item['contribution_id'] ?>, 'pending')" style="background:#f57f17;">
|
||||
<i class="fa-solid fa-rotate-left"></i> Zurücksetzen
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<button class="btn btn-edit" onclick="editContribution(<?= $item['contribution_id'] ?>, '<?= htmlspecialchars(addslashes($item['title']), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($item['description'] ?? ''), ENT_QUOTES) ?>')">
|
||||
<i class="fa-solid fa-pen"></i> Bearbeiten
|
||||
</button>
|
||||
|
||||
<button class="btn btn-delete" onclick="deleteContribution(<?= $item['contribution_id'] ?>)">
|
||||
<i class="fa-solid fa-trash"></i> Löschen
|
||||
</button>
|
||||
|
||||
<a class="btn btn-map" href="index.php" target="_blank">
|
||||
<i class="fa-solid fa-map-location-dot"></i> Karte
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recently Moderated -->
|
||||
<div class="section">
|
||||
<h2><i class="fa-solid fa-history"></i> Kürzlich moderiert</h2>
|
||||
|
||||
<?php if (empty($moderated)): ?>
|
||||
<div class="empty-state">Noch keine moderierten Beiträge.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($moderated as $item): ?>
|
||||
<div class="contribution-row-collapsible">
|
||||
<div class="contribution-row-header" onclick="toggleContribution(this)">
|
||||
<div class="contribution-row-summary">
|
||||
<span class="title"><?= htmlspecialchars($item['title']) ?></span>
|
||||
<span class="badge badge-<?= $item['status'] ?>"><?= $item['status'] === 'approved' ? 'freigegeben' : 'abgelehnt' ?></span>
|
||||
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
||||
<span class="meta-inline">
|
||||
<?= $category_labels[$item['category']] ?? $item['category'] ?>
|
||||
· <?= htmlspecialchars($item['author_name']) ?>
|
||||
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||
</span>
|
||||
</div>
|
||||
<i class="fa-solid fa-chevron-down collapse-icon"></i>
|
||||
</div>
|
||||
<div class="contribution-row-detail" style="display:none;">
|
||||
<div class="detail-layout">
|
||||
<div class="detail-map" id="map-<?= $item['contribution_id'] ?>"
|
||||
data-contribution-id="<?= $item['contribution_id'] ?>">
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<?php if ($item['description'] ?? false): ?>
|
||||
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
||||
<?php else: ?>
|
||||
<div class="description" style="color:#bbb;font-style:italic;">Keine Beschreibung.</div>
|
||||
<?php endif; ?>
|
||||
<div class="meta">
|
||||
<i class="fa-solid fa-user"></i> <?= htmlspecialchars($item['author_name']) ?>
|
||||
· <i class="fa-solid fa-calendar"></i> <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<!-- ========================================================= -->
|
||||
<!-- Placeholder Tabs for future Features -->
|
||||
<!-- ========================================================= -->
|
||||
<div id="tab-news" class="page-tab-content" style="display:none;">
|
||||
<div class="placeholder-content">
|
||||
<i class="fa-solid fa-newspaper"></i>
|
||||
<p>Neuigkeiten verwalten - geplant in zukünftiger Version.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-stats" class="page-tab-content" style="display:none;">
|
||||
<div class="placeholder-content">
|
||||
<i class="fa-solid fa-chart-bar"></i>
|
||||
<p>Statistiken und Analysen - geplant in zukünftiger Version.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-users" class="page-tab-content" style="display:none;">
|
||||
<div class="placeholder-content">
|
||||
<i class="fa-solid fa-users"></i>
|
||||
<p>Benutzerverwaltung - geplant in zukünftiger Version.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Leaflet for Map Previews -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- JavaScript: Leaflet, Interactions, API Calls -->
|
||||
<!-- ============================================================= -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Municipality Configuration for Map Previews
|
||||
var MUNICIPALITY_CENTER = [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>];
|
||||
var API_URL = 'api/contributions.php';
|
||||
const MUNICIPALITY_CENTER = [<?= $municipality['center_lat'] ?>, <?= $municipality['center_lng'] ?>];
|
||||
const MUNICIPALITY_ID = <?= $municipality['municipality_id'] ?>;
|
||||
const API_URL = 'api/contributions.php';
|
||||
const PRIMARY_COLOR = '<?= htmlspecialchars($municipality['primary_color']) ?>';
|
||||
|
||||
// Toggle Contribution Detail View
|
||||
function toggleContribution(header) {
|
||||
var detail = header.nextElementSibling;
|
||||
var icon = header.querySelector('.collapse-icon');
|
||||
var isOpen = detail.style.display !== 'none';
|
||||
// Current Status Filter
|
||||
let currentFilter = 'all';
|
||||
|
||||
if (isOpen) {
|
||||
detail.style.display = 'none';
|
||||
icon.classList.remove('fa-chevron-up');
|
||||
icon.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
detail.style.display = 'block';
|
||||
icon.classList.remove('fa-chevron-down');
|
||||
icon.classList.add('fa-chevron-up');
|
||||
|
||||
// Load Map Preview if not already loaded
|
||||
var mapDiv = detail.querySelector('.detail-map');
|
||||
// =============================================================
|
||||
// Page Tab Navigation
|
||||
// =============================================================
|
||||
|
||||
function showPageTab(tabName) {
|
||||
// Hides all Tab Contents
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Collapsible Rows
|
||||
// =============================================================
|
||||
|
||||
function toggleRow(row) {
|
||||
const wasOpen = row.classList.contains('open');
|
||||
|
||||
// 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');
|
||||
|
||||
// Loads Map Preview if not already loaded
|
||||
const mapDiv = row.querySelector('.detail-map');
|
||||
if (mapDiv && !mapDiv.dataset.loaded) {
|
||||
loadMapPreview(mapDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load a small Leaflet Map Preview for a Contribution
|
||||
function loadMapPreview(mapDiv) {
|
||||
var contributionId = mapDiv.dataset.contributionId;
|
||||
|
||||
// Fetch Contribution Geometry from API
|
||||
var formData = new FormData();
|
||||
// =============================================================
|
||||
// Map Preview (Leaflet Mini Map per Contribution)
|
||||
// =============================================================
|
||||
|
||||
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', '<?= $municipality['municipality_id'] ?>');
|
||||
formData.append('municipality_id', MUNICIPALITY_ID);
|
||||
formData.append('status', 'all');
|
||||
|
||||
fetch(API_URL, { method: 'POST', body: formData })
|
||||
@@ -316,18 +407,18 @@ $category_labels = [
|
||||
.then(function (data) {
|
||||
if (!data.features) return;
|
||||
|
||||
// Find the specific Contribution
|
||||
var feature = data.features.find(function (f) {
|
||||
// Finds specific Contribution
|
||||
const feature = data.features.find(function (f) {
|
||||
return f.properties.contribution_id == contributionId;
|
||||
});
|
||||
|
||||
if (!feature) {
|
||||
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;">Keine Geometrie gefunden.</div>';
|
||||
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Geometrie nicht gefunden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create small Leaflet Map
|
||||
var miniMap = L.map(mapDiv, {
|
||||
// Creates Leaflet Mini Map
|
||||
const miniMap = L.map(mapDiv, {
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
dragging: true,
|
||||
@@ -338,20 +429,21 @@ $category_labels = [
|
||||
maxZoom: 20
|
||||
}).addTo(miniMap);
|
||||
|
||||
// Add Geometry to Map
|
||||
var geojsonLayer = L.geoJSON(feature, {
|
||||
style: { color: '#c62828', weight: 3, fillOpacity: 0.3 },
|
||||
// Adds Geometry to Mini Map
|
||||
const geojsonLayer = L.geoJSON(feature, {
|
||||
style: { color: PRIMARY_COLOR, weight: 3, fillOpacity: 0.2 },
|
||||
pointToLayer: function (f, latlng) {
|
||||
return L.circleMarker(latlng, {
|
||||
radius: 8, color: '#c62828', fillColor: '#ef5350', fillOpacity: 0.8, weight: 2
|
||||
radius: 8, color: '#ffffff', weight: 2,
|
||||
fillColor: PRIMARY_COLOR, fillOpacity: 0.9
|
||||
});
|
||||
}
|
||||
}).addTo(miniMap);
|
||||
|
||||
// Fit Map to Geometry Bounds
|
||||
var bounds = geojsonLayer.getBounds();
|
||||
// Fits Map to Geometry Bounds
|
||||
const bounds = geojsonLayer.getBounds();
|
||||
if (bounds.isValid()) {
|
||||
miniMap.fitBounds(bounds, { padding: [20, 20], maxZoom: 17 });
|
||||
miniMap.fitBounds(bounds, { padding: [25, 25], maxZoom: 17 });
|
||||
} else {
|
||||
miniMap.setView(MUNICIPALITY_CENTER, 15);
|
||||
}
|
||||
@@ -359,14 +451,191 @@ $category_labels = [
|
||||
mapDiv.dataset.loaded = 'true';
|
||||
})
|
||||
.catch(function () {
|
||||
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;">Karte nicht verfügbar.</div>';
|
||||
mapDiv.innerHTML = '<div style="padding:20px;color:#999;text-align:center;font-size:0.8rem;">Karte nicht verfügbar.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Status Filter
|
||||
// =============================================================
|
||||
|
||||
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/Hides Contribution Rows
|
||||
let visibleCount = 0;
|
||||
document.querySelectorAll('.contribution-row').forEach(function (row) {
|
||||
if (status === 'all' || row.dataset.status === status) {
|
||||
row.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Updates Count Display
|
||||
document.getElementById('visible-count').textContent = visibleCount + ' Beiträge';
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Sort Contributions
|
||||
// =============================================================
|
||||
|
||||
function sortContributions(sortBy) {
|
||||
const container = document.getElementById('contributions-container');
|
||||
const rows = Array.from(container.querySelectorAll('.contribution-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 === 'category') {
|
||||
return a.dataset.category.localeCompare(b.dataset.category);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Reappends sorted Rows
|
||||
rows.forEach(function (row) {
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// API Helper
|
||||
// =============================================================
|
||||
|
||||
function apiCall(data) {
|
||||
const formData = new FormData();
|
||||
for (const key in data) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
return fetch(API_URL, { method: 'POST', body: formData })
|
||||
.then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Change Contribution Status
|
||||
// =============================================================
|
||||
|
||||
function changeStatus(contributionId, newStatus) {
|
||||
const labels = { approved: 'freigeben', rejected: 'ablehnen', pending: 'zurücksetzen' };
|
||||
|
||||
Swal.fire({
|
||||
title: 'Beitrag ' + labels[newStatus] + '?',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ja',
|
||||
cancelButtonText: 'Abbrechen',
|
||||
confirmButtonColor: PRIMARY_COLOR
|
||||
}).then(function (result) {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
apiCall({
|
||||
action: 'update',
|
||||
contribution_id: contributionId,
|
||||
status: newStatus
|
||||
}).then(function (response) {
|
||||
if (response.error) {
|
||||
Swal.fire('Fehler', response.error, 'error');
|
||||
return;
|
||||
}
|
||||
// Reloads Page to reflect Changes
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Edit Contribution (Title and Description)
|
||||
// =============================================================
|
||||
|
||||
function editContribution(contributionId, currentTitle, currentDescription) {
|
||||
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="' + currentTitle + '">' +
|
||||
'<label style="font-weight:600;font-size:0.85rem;">Beschreibung</label>' +
|
||||
'<textarea id="swal-description" class="swal2-textarea">' + currentDescription + '</textarea>' +
|
||||
'</div>',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Speichern',
|
||||
cancelButtonText: 'Abbrechen',
|
||||
confirmButtonColor: PRIMARY_COLOR,
|
||||
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
|
||||
}).then(function (response) {
|
||||
if (response.error) {
|
||||
Swal.fire('Fehler', response.error, 'error');
|
||||
return;
|
||||
}
|
||||
Swal.fire('Gespeichert!', 'Beitrag wurde aktualisiert.', 'success')
|
||||
.then(function () { location.reload(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// =============================================================
|
||||
// Delete 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: 'Beitrag löschen',
|
||||
cancelButtonText: 'Abbrechen',
|
||||
confirmButtonColor: '#c62828'
|
||||
}).then(function (result) {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
apiCall({
|
||||
action: 'delete',
|
||||
contribution_id: contributionId
|
||||
}).then(function (response) {
|
||||
if (response.error) {
|
||||
Swal.fire('Fehler', response.error, 'error');
|
||||
return;
|
||||
}
|
||||
Swal.fire('Gelöscht!', 'Beitrag wurde gelöscht.', 'success')
|
||||
.then(function () { location.reload(); });
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
<?php
|
||||
// -----------------------------------------------------------------
|
||||
// Login Page
|
||||
@@ -378,7 +647,8 @@ function show_login_page($municipality, $error = null) {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Moderation — Anmeldung</title>
|
||||
<title>Moderation - Anmeldung</title>
|
||||
<link rel="icon" href="assets/icon-municipality.png" type="image/png">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||
@@ -386,16 +656,16 @@ function show_login_page($municipality, $error = null) {
|
||||
<body>
|
||||
<div class="login-wrapper">
|
||||
<div class="login-box">
|
||||
<h1><i class="fa-solid fa-shield-halved"></i> Moderation</h1>
|
||||
<h1><i class="fa-solid fa-shield-halved"></i> Moderationsportal</h1>
|
||||
<p>Bitte geben Sie das Moderationspasswort ein.</p>
|
||||
<?php if ($error): ?>
|
||||
<div class="error"><?= htmlspecialchars($error) ?></div>
|
||||
<div class="login-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="admin.php?page=login">
|
||||
<input type="password" name="password" placeholder="Passwort" autofocus>
|
||||
<button type="submit"><i class="fa-solid fa-right-to-bracket"></i> Anmelden</button>
|
||||
</form>
|
||||
<div class="back-link"><a href="index.php">← Zurück zur Karte</a></div>
|
||||
<div class="back-link"><i class="fa fa-arrow-left"></i></i> <a href="index.php">Zurück zum Bürgerportal</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -2,15 +2,10 @@
|
||||
// =====================================================================
|
||||
// Admin Authentication Helper
|
||||
// Provides simple Password-based Session Authentication for the
|
||||
// Moderation Page. Uses ADMIN_PASSWORD from .env File.
|
||||
// Moderation Page. Reads Password from .env File.
|
||||
// ToDo: Replace with full User Authentication in Phase 3-3.
|
||||
// =====================================================================
|
||||
|
||||
// Reads Admin Password from Environment
|
||||
function get_admin_password() {
|
||||
return getenv('ADMIN_PASSWORD');
|
||||
}
|
||||
|
||||
// Checks if current Session is authenticated as Admin
|
||||
function is_admin() {
|
||||
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||
@@ -18,7 +13,7 @@ function is_admin() {
|
||||
|
||||
// Authenticates with Password, returns true on Success
|
||||
function admin_login($password) {
|
||||
$correct = get_admin_password();
|
||||
$correct = getenv('ADMIN_PASSWORD');
|
||||
if ($correct && $password === $correct) {
|
||||
$_SESSION['is_admin'] = true;
|
||||
return true;
|
||||
@@ -30,12 +25,4 @@ function admin_login($password) {
|
||||
function admin_logout() {
|
||||
$_SESSION['is_admin'] = false;
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
// Redirects to Login if not authenticated
|
||||
function require_admin() {
|
||||
if (!is_admin()) {
|
||||
header('Location: admin.php?page=login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
// =====================================================================
|
||||
// Database Helper
|
||||
// Provides PDO Connection to Database and shared miscellaneous
|
||||
// Functions for all API Endpoints.
|
||||
// Database Helper Functions
|
||||
// Provides PDO Connection, JSON Response Helpers, Category Definitions
|
||||
// and shared miscellaneous Functions for all API Endpoints.
|
||||
// =====================================================================
|
||||
|
||||
require_once __DIR__ . '/init.php';
|
||||
@@ -91,4 +91,23 @@ function get_db() {
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Category Definitions
|
||||
// Returns associative Array of Category Keys to Labels, Icons,
|
||||
// and Colors. Shared between Citizen Participation Portal and
|
||||
// Moderation Page.
|
||||
// ToDo: Move to Database Table.
|
||||
// ---------------------------------------------------------------------
|
||||
function get_categories() {
|
||||
return [
|
||||
'consumption' => ['label' => 'Geschäfte', 'faIcon' => 'fa-cart-shopping', 'color' => '#C00000'],
|
||||
'building' => ['label' => 'Bauen', 'faIcon' => 'fa-building', 'color' => '#E65100'],
|
||||
'energy' => ['label' => 'Energie', 'faIcon' => 'fa-bolt', 'color' => '#FFC000'],
|
||||
'environment' => ['label' => 'Umwelt', 'faIcon' => 'fa-seedling', 'color' => '#92D050'],
|
||||
'mobility' => ['label' => 'Mobilität', 'faIcon' => 'fa-bus', 'color' => '#0070C0'],
|
||||
'industry' => ['label' => 'Industrie', 'faIcon' => 'fa-industry', 'color' => '#7030A0'],
|
||||
'other' => ['label' => 'Sonstiges', 'faIcon' => 'fa-thumbtack', 'color' => '#7F7F7F'],
|
||||
];
|
||||
}
|
||||
@@ -56,7 +56,7 @@ if (!$municipality) {
|
||||
<!-- Leaflet Polyline Measurement Tool -->
|
||||
<!-- <link rel="stylesheet" href="https://ppete2.github.io/Leaflet.PolylineMeasure/Leaflet.PolylineMeasure.css"> -->
|
||||
|
||||
<!-- Font Awesome 6 for Icons -->
|
||||
<!-- Font Awesome for Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
<!-- Application Styles -->
|
||||
@@ -97,7 +97,7 @@ if (!$municipality) {
|
||||
<i class="fa-solid fa-scale-balanced"></i>
|
||||
<span class="nav-label">Impressum</span>
|
||||
</button>
|
||||
<a href="admin.php" class="nav-btn nav-btn-admin" title="Moderationsbereich">
|
||||
<a href="admin.php" class="nav-btn nav-btn-admin" title="Moderationsbereich" target="_blank">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -216,8 +216,10 @@ if (!$municipality) {
|
||||
<!-- Footer -->
|
||||
<!-- ============================================================= -->
|
||||
<footer id="app-footer">
|
||||
<span class="dev-warning">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i> Pilotprojekt - nicht offiziell von der Stadt Lohne (Oldenburg) beauftragt
|
||||
</span>
|
||||
<div class="footer-content">
|
||||
<!-- <img src="assets/logo-company.png" alt="Company Logo" class="footer-logo" onerror="this.style.display='none'"> -->
|
||||
<span class="footer-text">© <a href="https://endex-geodaten.de" target="_blank" style="color:inherit;">endex GmbH</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -327,11 +329,11 @@ if (!$municipality) {
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.14.0/dist/sweetalert2.all.min.js"></script>
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- Municipality Configuration (passed to JavaScript) -->
|
||||
<!-- Municipality Configuration passed to JavaScript -->
|
||||
<!-- ============================================================= -->
|
||||
<script>
|
||||
// Municipality Configuration from Database — used by app.js
|
||||
var MUNICIPALITY = {
|
||||
const MUNICIPALITY = {
|
||||
id: <?= $municipality['municipality_id'] ?>,
|
||||
name: "<?= htmlspecialchars($municipality['name'], ENT_QUOTES) ?>",
|
||||
slug: "<?= htmlspecialchars($municipality['slug'], ENT_QUOTES) ?>",
|
||||
@@ -339,6 +341,9 @@ if (!$municipality) {
|
||||
zoom: <?= $municipality['default_zoom'] ?>,
|
||||
primaryColor: "<?= htmlspecialchars($municipality['primary_color'], ENT_QUOTES) ?>"
|
||||
};
|
||||
|
||||
// Category Definitions from Database
|
||||
const CATEGORIES = <?= json_encode(get_categories(), JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
|
||||
<!-- Application Logic -->
|
||||
|
||||
@@ -19,17 +19,6 @@ const API_URL = 'api/contributions.php';
|
||||
// Current User Name, set via Login Modal, stored in sessionStorage
|
||||
let currentUser = sessionStorage.getItem('webgis_user') || '';
|
||||
|
||||
// Category Definitions with Labels, Icons, and Colors
|
||||
const CATEGORIES = {
|
||||
consumption: { label: 'Geschäfte', faIcon: 'fa-cart-shopping', color: '#C00000' },
|
||||
building: { label: 'Bauen', faIcon: 'fa-building', color: '#E65100' },
|
||||
energy: { label: 'Energie', faIcon: 'fa-bolt', color: '#FFC000' },
|
||||
environment: { label: 'Umwelt', faIcon: 'fa-seedling', color: '#92D050' },
|
||||
mobility: { label: 'Mobilität', faIcon: 'fa-bus', color: '#0070C0' },
|
||||
industry: { label: 'Industrie', faIcon: 'fa-industry', color: '#7030A0' },
|
||||
other: { label: 'Sonstiges', faIcon: 'fa-thumbtack', color: '#7F7F7F' }
|
||||
};
|
||||
|
||||
// Application State
|
||||
let map; // Leaflet Map Instance
|
||||
let sidebar; // Sidebar Instance
|
||||
@@ -367,7 +356,7 @@ function styleLinePolygon(feature) {
|
||||
// Block 9: Feature Popups for Read, Edit, Delete and Vote
|
||||
// =====================================================================
|
||||
|
||||
function bindFeaturePopup(feature, layer) {
|
||||
function buildPopupHtml(feature) {
|
||||
const props = feature.properties;
|
||||
const cat = CATEGORIES[props.category] || CATEGORIES.other;
|
||||
|
||||
@@ -377,8 +366,7 @@ function bindFeaturePopup(feature, layer) {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
||||
});
|
||||
|
||||
// Builds Popup on Click
|
||||
const html = '' +
|
||||
return '' +
|
||||
'<div class="popup-detail">' +
|
||||
'<span class="popup-detail-category">' + categoryIcon(cat) + ' ' + cat.label + '</span>' +
|
||||
'<div class="popup-detail-title">' + escapeHtml(props.title) + '</div>' +
|
||||
@@ -401,11 +389,17 @@ function bindFeaturePopup(feature, layer) {
|
||||
'<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 });
|
||||
// Binds Popup and Tooltip to Feature Layer
|
||||
function bindFeaturePopup(feature, layer) {
|
||||
const cat = CATEGORIES[feature.properties.category] || CATEGORIES.other;
|
||||
|
||||
// Builds Tooltip on Hover
|
||||
layer.bindTooltip(categoryIcon(cat) + ' ' + escapeHtml(props.title), {
|
||||
// Rebuilts if Popup opens
|
||||
layer.bindPopup(function () { return buildPopupHtml(feature); }, { maxWidth: 320, minWidth: 240 });
|
||||
|
||||
// Tooltip on Hover
|
||||
layer.bindTooltip(categoryIcon(cat) + ' ' + escapeHtml(feature.properties.title), {
|
||||
direction: 'top',
|
||||
offset: [0, -10]
|
||||
});
|
||||
@@ -575,39 +569,53 @@ function voteContribution(contributionId, voteType) {
|
||||
const likesSpan = document.getElementById('likes-' + contributionId);
|
||||
const dislikesSpan = document.getElementById('dislikes-' + contributionId);
|
||||
|
||||
// Finds Feature in Contributions to update Properties
|
||||
const feature = contributionsData.find(function (f) {
|
||||
return f.properties.contribution_id === contributionId;
|
||||
});
|
||||
|
||||
if (response.action === 'created') {
|
||||
// New Vote — Highlights Button and updates Count
|
||||
userVotes[contributionId] = voteType;
|
||||
if (voteType === 'like') {
|
||||
likeBtn.classList.add('liked');
|
||||
likesSpan.textContent = parseInt(likesSpan.textContent) + 1;
|
||||
if (feature) feature.properties.likes_count++;
|
||||
} else {
|
||||
dislikeBtn.classList.add('disliked');
|
||||
dislikesSpan.textContent = parseInt(dislikesSpan.textContent) + 1;
|
||||
if (feature) feature.properties.dislikes_count++;
|
||||
}
|
||||
} else if (response.action === 'removed') {
|
||||
// Vote removed — Removes Button Highlight and updates Count
|
||||
delete userVotes[contributionId];
|
||||
if (voteType === 'like') {
|
||||
likeBtn.classList.remove('liked');
|
||||
likesSpan.textContent = Math.max(0, parseInt(likesSpan.textContent) - 1);
|
||||
if (feature) feature.properties.likes_count = Math.max(0, feature.properties.likes_count - 1);
|
||||
} else {
|
||||
dislikeBtn.classList.remove('disliked');
|
||||
dislikesSpan.textContent = Math.max(0, parseInt(dislikesSpan.textContent) - 1);
|
||||
if (feature) feature.properties.dislikes_count = Math.max(0, feature.properties.dislikes_count - 1);
|
||||
}
|
||||
} else if (response.action === 'changed') {
|
||||
// Vote changed — Switches Highlights and updates both Counts
|
||||
userVotes[contributionId] = voteType;
|
||||
if (voteType === 'like') {
|
||||
likeBtn.classList.add('liked');
|
||||
dislikeBtn.classList.remove('disliked');
|
||||
likesSpan.textContent = parseInt(likesSpan.textContent) + 1;
|
||||
dislikesSpan.textContent = Math.max(0, parseInt(dislikesSpan.textContent) - 1);
|
||||
if (feature) {
|
||||
feature.properties.likes_count++;
|
||||
feature.properties.dislikes_count = Math.max(0, feature.properties.dislikes_count - 1);
|
||||
}
|
||||
} else {
|
||||
dislikeBtn.classList.add('disliked');
|
||||
likeBtn.classList.remove('liked');
|
||||
dislikesSpan.textContent = parseInt(dislikesSpan.textContent) + 1;
|
||||
likesSpan.textContent = Math.max(0, parseInt(likesSpan.textContent) - 1);
|
||||
if (feature) {
|
||||
feature.properties.dislikes_count++;
|
||||
feature.properties.likes_count = Math.max(0, feature.properties.likes_count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -743,7 +751,7 @@ function toggleCategoryFilter(checkbox) {
|
||||
if (layer.feature) {
|
||||
const cat = layer.feature.properties.category;
|
||||
if (activeFilters.indexOf(cat) !== -1) {
|
||||
layer.setStyle({ opacity: 1, fillOpacity: layer.feature.geometry.type === 'Point' ? 0.9 : 0.25 });
|
||||
layer.setStyle({ opacity: 0.8, fillOpacity: layer.feature.geometry.type === 'Point' ? 0.25 : 0.25 });
|
||||
if (layer.setRadius) layer.setRadius(8);
|
||||
layer.options.interactive = true;
|
||||
} else {
|
||||
|
||||
@@ -205,6 +205,28 @@ html, body {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dev-warning {
|
||||
position: absolute;
|
||||
left: var(--space-md);
|
||||
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #856404;
|
||||
white-space: nowrap;
|
||||
|
||||
background: rgba(255, 243, 205, 0.8); /* leicht transparent */
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
|
||||
border: 1px solid #856404; /* NICHT transparent */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dev-warning {
|
||||
font-size: 0.6rem;
|
||||
left: var(--space-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Mouse Position Display
|
||||
|
||||
Reference in New Issue
Block a user