diff --git a/.env.example b/.env.example index 92a85cb..6daedb4 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,5 @@ POSTGRES_HOSTNAME=postgres_host POSTGRES_PORT=postgres_port POSTGRES_DB=postgres_database POSTGRES_USER=postgres_user -POSTGRES_PASSWORD= \ No newline at end of file +POSTGRES_PASSWORD= +ADMIN_PASSWORD= \ No newline at end of file diff --git a/public/admin.css b/public/admin.css new file mode 100644 index 0000000..b851ead --- /dev/null +++ b/public/admin.css @@ -0,0 +1,281 @@ +/* ===================================================================== + Moderation Page Styles + ===================================================================== */ + +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Segoe UI', system-ui, sans-serif; + background: #f4f5f7; + color: #1a1a2e; + font-size: 15px; +} + +/* ----------------------------------------------------------------- + Header + ----------------------------------------------------------------- */ +.admin-header { + background: var(--color-primary); + color: white; + padding: 16px 24px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.admin-header h1 { font-size: 1.2rem; } + +.admin-header a { + color: white; + text-decoration: none; + opacity: 0.8; + font-size: 0.85rem; +} + +.admin-header a:hover { opacity: 1; } + +.admin-nav { + display: flex; + gap: 16px; + align-items: center; +} + +/* ----------------------------------------------------------------- + Container + ----------------------------------------------------------------- */ +.admin-container { + max-width: 900px; + margin: 24px auto; + padding: 0 16px; +} + +/* ----------------------------------------------------------------- + Statistics Cards + ----------------------------------------------------------------- */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + margin-bottom: 32px; +} + +.stat-card { + background: white; + border-radius: 8px; + padding: 16px; + text-align: center; + border: 1px solid #e0e0e0; +} + +.stat-card .stat-number { + font-size: 1.8rem; + font-weight: 700; + color: var(--color-primary); +} + +.stat-card .stat-label { + font-size: 0.8rem; + color: #5a5a7a; + margin-top: 4px; +} + +/* ----------------------------------------------------------------- + Section Headers + ----------------------------------------------------------------- */ +h2 { + font-size: 1.1rem; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px solid var(--color-primary); +} + +.section { margin-bottom: 40px; } + +/* ----------------------------------------------------------------- + Contribution Rows + ----------------------------------------------------------------- */ +.contribution-row { + background: white; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; +} + +.contribution-info { flex: 1; } + +.contribution-info .title { + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 4px; +} + +.contribution-info .meta { + font-size: 0.8rem; + color: #5a5a7a; + margin-bottom: 4px; +} + +.contribution-info .description { + font-size: 0.85rem; + color: #5a5a7a; + line-height: 1.4; +} + +/* ----------------------------------------------------------------- + Badges + ----------------------------------------------------------------- */ +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.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 + ----------------------------------------------------------------- */ +.empty-state { + text-align: center; + padding: 40px; + color: #999; + font-size: 0.9rem; +} + +/* ----------------------------------------------------------------- + Login Page + ----------------------------------------------------------------- */ +.login-wrapper { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +.login-box { + background: white; + border-radius: 12px; + padding: 32px; + max-width: 380px; + width: 90%; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.login-box h1 { + font-size: 1.3rem; + margin-bottom: 8px; +} + +.login-box p { + font-size: 0.85rem; + color: #5a5a7a; + margin-bottom: 20px; +} + +.login-box input[type="password"] { + width: 100%; + padding: 10px 12px; + border: 1px solid #e0e0e0; + border-radius: 6px; + font-size: 0.9rem; + margin-bottom: 12px; + font-family: 'Segoe UI', system-ui, sans-serif; +} + +.login-box input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(0, 55, 109, 0.1); +} + +.login-box button { + width: 100%; + padding: 10px; + background: var(--color-primary); + color: white; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + font-family: 'Segoe UI', system-ui, sans-serif; +} + +.login-box button:hover { filter: brightness(1.15); } + +.error { + color: #c62828; + font-size: 0.85rem; + margin-bottom: 12px; +} + +.back-link { + margin-top: 16px; + font-size: 0.8rem; +} + +.back-link a { color: #5a5a7a; } + +/* ----------------------------------------------------------------- + Mobile Responsive + ----------------------------------------------------------------- */ +@media (max-width: 768px) { + .contribution-row { + flex-direction: column; + } + + .action-buttons { + width: 100%; + } + + .action-buttons form { + flex: 1; + } + + .action-buttons .btn { + width: 100%; + justify-content: center; + } +} \ No newline at end of file diff --git a/public/admin.php b/public/admin.php new file mode 100644 index 0000000..002447c --- /dev/null +++ b/public/admin.php @@ -0,0 +1,254 @@ +prepare("SELECT * FROM municipalities WHERE slug = :slug"); +$stmt->execute([':slug' => 'lohne']); +$municipality = $stmt->fetch(); + +// Show Login Page if not authenticated +if ($page === 'login' || !is_admin()) { + show_login_page($municipality, $login_error ?? null); + exit; +} + +// ----------------------------------------------------------------- +// Handle Moderation Actions (Approve / Reject) +// ----------------------------------------------------------------- +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; +} + +// ----------------------------------------------------------------- +// Load Contributions Data +// ----------------------------------------------------------------- + +// Pending Contributions +$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' + ORDER BY created_at DESC +"); +$stmt->execute([':mid' => $municipality['municipality_id']]); +$pending = $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']; +} + +// ----------------------------------------------------------------- +// Render Main Page +// ----------------------------------------------------------------- +?> + + +
+ + +Bitte geben Sie das Moderationspasswort ein.
+ +