adasd
All checks were successful
Deploy / deploy (push) Successful in 52s

This commit is contained in:
2026-07-22 22:19:31 +02:00
parent 2e800d4839
commit cbecbef830
8 changed files with 450 additions and 194 deletions

View File

@@ -61,17 +61,18 @@ Stand: 2026-07-22
6. Aktueller Funktionsstand
- Startseite mit Hero, Suche, neueste Treffen, Community-Vorschau, Mitgliederbereich.
- Hauptnavigation aktuell reduziert auf `Home`, `Suche`, `Community`.
- Für eingeloggte Nutzer gibt es rechts ein Profil-Dropdown mit `Übersicht`, `Mitgliederbereich`, `Abmelden`.
- Für eingeloggte Nutzer gibt es rechts ein Profil-Dropdown mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`.
- Kein eigener Top-Level-Punkt `Termine`; Event-Fokus liegt derzeit auf Startseite und Suche.
- Community mit Kategorien, Boards, Thread-Liste, Thread-Ansicht, Rollen-/Moderationslogik.
- Dashboard mit Profil, Kinder, eigene Events, Community-Status.
- Mitgliederbereich als Bereichslayout mit linker Navigation und separaten Seiten für Profil, Kinder, Termine, Community und Einstellungen.
- Community-Admin-Bereich für Bewerbungen, Meldungen, Rollen und Migration.
- Impressum, Datenschutz-&-Cookies-Seite und Über-uns vorhanden.
- Standortsortierung auf der Startseite für Treffen in der Nähe.
- Standortpräferenz im Profil:
- Standortpräferenz im Einstellungsbereich:
- `disabled`
- `prompt`
- `enabled`
- Im Einstellungsbereich stehen zusätzlich Browser-Hinweise zur Standortfreigabe sowie der Einstieg in die Consent-Verwaltung.
- Consent-Manager:
- notwendige Cookies immer aktiv
- `analytics` für Matomo

View File

@@ -22,7 +22,8 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
## Navigation / Konto
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.
- Das Profil-Menü enthält aktuell `Übersicht`, `Mitgliederbereich`, `Abmelden`.
- Das Profil-Menü enthält aktuell `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`, `Abmelden`.
- Der Mitgliederbereich selbst ist als Layout mit linker Bereichsnavigation aufgebaut.
- Debug ist kein Menüpunkt und erscheint nur als Floating-Käfer für `site_admin` bei aktivem Debug-Modus.
## Community und Rechte
@@ -40,10 +41,11 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
- `external_services`
- Matomo wird nur nach Analyse-Einwilligung geladen.
- Externe Karten-/Standortdienste und lokale Standortspeicherung hängen an `external_services`.
- Zusätzlich gibt es eine Standortpräferenz im Profil:
- Zusätzlich gibt es eine Standortpräferenz im Bereich `Einstellungen`:
- `disabled`
- `prompt`
- `enabled`
- Im Einstellungsbereich werden außerdem Browser-Grenzen erklärt: Die Browser-Freigabe selbst kann nicht von der Website gesetzt oder aufgehoben werden.
- Es gibt eine eigene Seite `/datenschutz` für Datenschutz- und Cookie-Hinweise sowie einen Footer-Link auf die Consent-Einstellungen.
## Doku-Regel

View File

@@ -16,10 +16,11 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
- Community-Admin-Bereich für Bewerbungen, Meldungen, Rollen und Migration
- Impressum, Datenschutz-&-Cookies-Seite und Über-uns
- standortbasierte Sortierung für die neuesten Treffen
- Standortpräferenz im Profil
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
- Consent-Manager für Analyse und externe Dienste
- Navigation aktuell mit `Home`, `Suche`, `Community`
- eingeloggte Nutzer sehen rechts ein Profil-Menü statt separater `Dashboard`-/`Logout`-Buttons
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`
- der Mitgliederbereich ist als Seitenlayout mit linker Bereichsnavigation aufgebaut
- Debug-Floating-Button nur für `site_admin` im Debug-Modus
## Technik

View File

@@ -6,6 +6,16 @@ $editing = isset($editEvent) && $editEvent !== null;
$actionEvent = $editing ? 'event_update' : 'event_add';
$startVal = $editEvent ? date('Y-m-d\TH:i', strtotime((string)$editEvent['starts_at'])) : '';
$allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false;
$sectionLinks = [
'profile' => 'Profil',
'children' => 'Kinder',
'events' => 'Termine',
'community' => 'Community',
'settings' => 'Einstellungen',
];
if (in_array('admin', $allowedSections ?? [], true)) {
$sectionLinks['admin'] = 'Admin';
}
?>
<main class="section">
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
@@ -29,177 +39,272 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
</div>
<div class="container dash-section">
<div class="dash-grid">
<?php if (in_array('forum_admin', $communityRoles ?? [], true) || !empty($communityIsSiteAdmin)): ?>
<div class="card dash-card">
<div class="badge">Admin</div>
<h3>Community-Verwaltung</h3>
<div class="flex gap-12" style="margin:12px 0; flex-wrap:wrap;">
<a class="btn" href="/community-admin">Zum Admin-Bereich</a>
<div class="account-layout">
<aside class="account-sidebar">
<div class="account-sidebar__inner">
<div class="account-sidebar__head">
<span class="badge">Mitgliederbereich</span>
<h2>Navigation</h2>
</div>
<?php if ($communityMigrationStatus): ?>
<ul class="dash-list">
<li>Status: <?= !empty($communityMigrationStatus['complete']) ? 'Vollständig eingerichtet' : 'Migration noch offen' ?></li>
<li>Fehlende Bausteine: <?= !empty($communityMigrationStatus['missing']) ? htmlspecialchars(implode(', ', $communityMigrationStatus['missing']), ENT_QUOTES) : 'Keine' ?></li>
</ul>
<?php else: ?>
<p class="muted small">Migrationsstatus konnte nicht ermittelt werden.</p>
<?php endif; ?>
<?php if (!empty($communityIsSiteAdmin)): ?>
<form method="post" style="margin-top:12px;">
<input type="hidden" name="action" value="community_migrate">
<button class="btn" type="submit">Community-Migration ausführen</button>
</form>
<?php endif; ?>
<p class="muted small" style="margin-top:10px;">Bündelt Moderation, Bewerbungen, Rollen und bei Bedarf auch die Community-Migration.</p>
</div>
<?php endif; ?>
<div class="card dash-card">
<div class="badge">Community</div>
<h3>Dein Community-Status</h3>
<ul class="dash-list">
<li>Rang: <?= htmlspecialchars(trim(($communityLevel['icon'] ?? '') . ' ' . ($communityLevel['label'] ?? '')), ENT_QUOTES) ?></li>
<li>Punkte: <?= number_format((float)$communityPoints, 1, ',', '.') ?></li>
<li>Rollen: <?= htmlspecialchars($communityRoles ? implode(', ', $communityRoles) : 'Keine besonderen Rollen', ENT_QUOTES) ?></li>
<li>Themen erstellen: <?= $communityRestrictions['thread_create_blocked'] ? 'Gesperrt' : 'Erlaubt' ?></li>
<li>Antworten schreiben: <?= $communityRestrictions['reply_blocked'] ? 'Gesperrt' : 'Erlaubt' ?></li>
</ul>
<?php if (!empty($communityRestrictions['reason'])): ?>
<p class="muted small" style="margin-top:10px;">Hinweis zur Community-Sperre: <?= htmlspecialchars((string)$communityRestrictions['reason'], ENT_QUOTES) ?></p>
<?php endif; ?>
<?php if ($communityCanApply): ?>
<form method="post" class="stack gap-6" style="margin-top:12px;">
<input type="hidden" name="action" value="community_admin_apply">
<label class="label" for="communityMotivation">Bewerbung als Forum-Admin</label>
<textarea id="communityMotivation" name="motivation" class="textarea" rows="4" placeholder="Warum möchtest du die Community als Forum-Admin unterstützen?" required></textarea>
<button class="btn" type="submit">Bewerbung absenden</button>
</form>
<?php elseif ($communityApplication): ?>
<div style="margin-top:12px;">
<p class="muted small" style="margin:0;">Letzte Bewerbung: <strong><?= htmlspecialchars((string)$communityApplication['status'], ENT_QUOTES) ?></strong></p>
<?php if (!empty($communityApplication['decision_reason'])): ?>
<p class="muted small" style="margin-top:6px;"><?= htmlspecialchars((string)$communityApplication['decision_reason'], ENT_QUOTES) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="card dash-card">
<div class="badge">Profil</div>
<h3>Deine Angaben</h3>
<ul class="dash-list">
<li>Name: <?= htmlspecialchars(trim($profile['first_name'] . ' ' . $profile['last_name']), ENT_QUOTES) ?></li>
<li>Anzeigename: <?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?></li>
<li>Ort: <?= htmlspecialchars($profile['city'], ENT_QUOTES) ?> <?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?></li>
<li>E-Mail: <?= htmlspecialchars($profile['email'], ENT_QUOTES) ?></li>
<li>Telefon: <?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?></li>
<li>Standortfreigabe: <?=
htmlspecialchars(match ($profile['location_tracking_preference'] ?? 'prompt') {
'enabled' => 'Immer verwenden',
'disabled' => 'Deaktiviert',
default => 'Beim nächsten Mal fragen',
}, ENT_QUOTES)
?></li>
<li>Beruf: <?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?></li>
<li>Sprachen: <?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?></li>
<li>About: <?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></li>
</ul>
<div class="flex gap-12" style="margin-top:12px;">
<button class="btn" type="button" data-modal-open="modalProfile">Bearbeiten</button>
</div>
</div>
<div class="card dash-card">
<div class="badge">Kinder</div>
<h3>Deine Kids</h3>
<?php if (!$children): ?>
<p class="muted small">Noch keine Kinder eingetragen.</p>
<?php else: ?>
<ul class="dash-list">
<?php foreach ($children as $c): ?>
<li><?= htmlspecialchars($c['first_name'], ENT_QUOTES) ?>, <?= htmlspecialchars($c['gender'], ENT_QUOTES) ?> <?= $c['age_years'] ? '(' . (int)$c['age_years'] . ' Jahre)' : '' ?></li>
<nav class="account-sidebar__nav">
<?php foreach ($sectionLinks as $sectionKey => $sectionLabel): ?>
<a href="/dashboard?section=<?= rawurlencode($sectionKey) ?>" class="<?= $section === $sectionKey ? 'is-active' : '' ?>">
<span><?= htmlspecialchars($sectionLabel, ENT_QUOTES) ?></span>
</a>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<div class="flex gap-12" style="margin-top:12px;">
<button class="btn" type="button" data-modal-open="modalChild">Kind hinzufügen</button>
</nav>
</div>
</div>
</div>
</div>
</aside>
<div class="container dash-section" id="events">
<div class="card dash-card">
<div class="badge">Deine Events</div>
<div class="flex gap-12" style="margin:10px 0 16px 0; flex-wrap: wrap;">
<button class="btn" type="button" data-modal-open="modalEvent">Event anlegen</button>
</div>
<?php if (!$eventsUpcoming): ?>
<p class="muted small">Keine zukünftigen Events angelegt.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($eventsUpcoming as $e): ?>
<li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
<div>
<strong><?= htmlspecialchars($e['title'], ENT_QUOTES) ?></strong>
<?php if ($e['status'] === 'cancelled'): ?>
<span class="badge" style="background:#fee2e2; color:#991b1b;">Abgesagt</span>
<?php endif; ?>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars($e['city'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>
· Anmeldungen: <?= (int)$e['participant_count'] ?>
</div>
</div>
<div class="flex gap-8" style="flex-wrap: wrap;">
<a class="btn ghost" href="/dashboard?edit_event=<?= (int)$e['id'] ?>#events">Bearbeiten</a>
<?php if ((int)$e['participant_count'] === 0): ?>
<form method="post" action="/dashboard#events" onsubmit="return confirm('Event wirklich löschen?');">
<input type="hidden" name="action" value="event_delete">
<input type="hidden" name="event_id" value="<?= (int)$e['id'] ?>">
<button class="btn ghost" type="submit">Löschen</button>
</form>
<?php else: ?>
<?php if ($e['status'] !== 'cancelled'): ?>
<form method="post" action="/dashboard#events" onsubmit="return confirm('Event für alle absagen?');">
<input type="hidden" name="action" value="event_cancel">
<input type="hidden" name="event_id" value="<?= (int)$e['id'] ?>">
<button class="btn ghost" type="submit">Absagen</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="account-main">
<?php if ($section === 'profile'): ?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Dein Profil</h2>
<p class="muted">Hier pflegst du deine persönlichen Angaben für den Mitgliederbereich.</p>
</div>
<div class="account-panel__body">
<dl class="account-kv">
<div><dt>Name</dt><dd><?= htmlspecialchars(trim($profile['first_name'] . ' ' . $profile['last_name']), ENT_QUOTES) ?></dd></div>
<div><dt>Anzeigename</dt><dd><?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?></dd></div>
<div><dt>Ort</dt><dd><?= htmlspecialchars(trim($profile['city'] . ' ' . $profile['zip']), ENT_QUOTES) ?></dd></div>
<div><dt>E-Mail</dt><dd><?= htmlspecialchars($profile['email'], ENT_QUOTES) ?></dd></div>
<div><dt>Telefon</dt><dd><?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?></dd></div>
<div><dt>Beruf</dt><dd><?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?></dd></div>
<div><dt>Sprachen</dt><dd><?= htmlspecialchars($profile['languages'], ENT_QUOTES) ?></dd></div>
<div><dt>Über dich</dt><dd><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></dd></div>
</dl>
<div class="flex gap-12" style="margin-top:18px;">
<button class="btn" type="button" data-modal-open="modalProfile">Profil bearbeiten</button>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<details style="margin-top:12px;">
<summary style="cursor:pointer;">Vergangene Events anzeigen</summary>
<?php if (!$eventsPast): ?>
<p class="muted small">Keine vergangenen Events.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($eventsPast as $e): ?>
<li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
<div>
<strong><?= htmlspecialchars($e['title'], ENT_QUOTES) ?></strong>
<?php if ($e['status'] === 'cancelled'): ?>
<span class="badge" style="background:#fee2e2; color:#991b1b;">Abgesagt</span>
<?php endif; ?>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars($e['city'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>
· Anmeldungen: <?= (int)$e['participant_count'] ?>
</div>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</section>
<?php endif; ?>
</details>
<?php if ($section === 'children'): ?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Deine Kinder</h2>
<p class="muted">Optionale Angaben, die dir bei Planung und passenden Treffen helfen können.</p>
</div>
<div class="account-panel__body">
<?php if (!$children): ?>
<p class="muted small">Noch keine Kinder eingetragen.</p>
<?php else: ?>
<ul class="dash-list">
<?php foreach ($children as $c): ?>
<li><?= htmlspecialchars($c['first_name'], ENT_QUOTES) ?>, <?= htmlspecialchars($c['gender'], ENT_QUOTES) ?> <?= $c['age_years'] ? '(' . (int)$c['age_years'] . ' Jahre)' : '' ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<div class="flex gap-12" style="margin-top:18px;">
<button class="btn" type="button" data-modal-open="modalChild">Kind hinzufügen</button>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'community'): ?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Dein Community-Status</h2>
<p class="muted">Rang, Punkte, Rollen und dein aktueller Stand in der Community.</p>
</div>
<div class="account-panel__body">
<dl class="account-kv">
<div><dt>Rang</dt><dd><?= htmlspecialchars(trim(($communityLevel['icon'] ?? '') . ' ' . ($communityLevel['label'] ?? '')), ENT_QUOTES) ?></dd></div>
<div><dt>Punkte</dt><dd><?= number_format((float)$communityPoints, 1, ',', '.') ?></dd></div>
<div><dt>Rollen</dt><dd><?= htmlspecialchars($communityRoles ? implode(', ', $communityRoles) : 'Keine besonderen Rollen', ENT_QUOTES) ?></dd></div>
<div><dt>Themen erstellen</dt><dd><?= $communityRestrictions['thread_create_blocked'] ? 'Gesperrt' : 'Erlaubt' ?></dd></div>
<div><dt>Antworten schreiben</dt><dd><?= $communityRestrictions['reply_blocked'] ? 'Gesperrt' : 'Erlaubt' ?></dd></div>
</dl>
<?php if (!empty($communityRestrictions['reason'])): ?>
<p class="muted small" style="margin-top:14px;">Hinweis zur Community-Sperre: <?= htmlspecialchars((string)$communityRestrictions['reason'], ENT_QUOTES) ?></p>
<?php endif; ?>
<?php if ($communityCanApply): ?>
<form method="post" class="stack gap-6" style="margin-top:18px;">
<input type="hidden" name="action" value="community_admin_apply">
<label class="label" for="communityMotivation">Bewerbung als Forum-Admin</label>
<textarea id="communityMotivation" name="motivation" class="textarea" rows="4" placeholder="Warum möchtest du die Community als Forum-Admin unterstützen?" required></textarea>
<button class="btn" type="submit">Bewerbung absenden</button>
</form>
<?php elseif ($communityApplication): ?>
<div style="margin-top:18px;">
<p class="muted small" style="margin:0;">Letzte Bewerbung: <strong><?= htmlspecialchars((string)$communityApplication['status'], ENT_QUOTES) ?></strong></p>
<?php if (!empty($communityApplication['decision_reason'])): ?>
<p class="muted small" style="margin-top:6px;"><?= htmlspecialchars((string)$communityApplication['decision_reason'], ENT_QUOTES) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'events'): ?>
<section class="account-panel" id="events">
<div class="account-panel__head">
<h2>Deine Termine</h2>
<p class="muted">Erstelle, bearbeite und verwalte deine eigenen Treffen.</p>
</div>
<div class="account-panel__body">
<div class="flex gap-12" style="margin:0 0 16px 0; flex-wrap: wrap;">
<button class="btn" type="button" data-modal-open="modalEvent">Event anlegen</button>
</div>
<?php if (!$eventsUpcoming): ?>
<p class="muted small">Keine zukünftigen Events angelegt.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($eventsUpcoming as $e): ?>
<li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
<div>
<strong><?= htmlspecialchars($e['title'], ENT_QUOTES) ?></strong>
<?php if ($e['status'] === 'cancelled'): ?>
<span class="badge" style="background:#fee2e2; color:#991b1b;">Abgesagt</span>
<?php endif; ?>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars($e['city'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>
· Anmeldungen: <?= (int)$e['participant_count'] ?>
</div>
</div>
<div class="flex gap-8" style="flex-wrap: wrap;">
<a class="btn ghost" href="/dashboard?section=events&edit_event=<?= (int)$e['id'] ?>#events">Bearbeiten</a>
<?php if ((int)$e['participant_count'] === 0): ?>
<form method="post" action="/dashboard?section=events#events" onsubmit="return confirm('Event wirklich löschen?');">
<input type="hidden" name="action" value="event_delete">
<input type="hidden" name="event_id" value="<?= (int)$e['id'] ?>">
<button class="btn ghost" type="submit">Löschen</button>
</form>
<?php else: ?>
<?php if ($e['status'] !== 'cancelled'): ?>
<form method="post" action="/dashboard?section=events#events" onsubmit="return confirm('Event für alle absagen?');">
<input type="hidden" name="action" value="event_cancel">
<input type="hidden" name="event_id" value="<?= (int)$e['id'] ?>">
<button class="btn ghost" type="submit">Absagen</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<details style="margin-top:12px;">
<summary style="cursor:pointer;">Vergangene Events anzeigen</summary>
<?php if (!$eventsPast): ?>
<p class="muted small">Keine vergangenen Events.</p>
<?php else: ?>
<ul class="dash-list" style="margin-top:10px;">
<?php foreach ($eventsPast as $e): ?>
<li>
<div style="display:flex; justify-content:space-between; gap:12px; align-items:center; flex-wrap: wrap;">
<div>
<strong><?= htmlspecialchars($e['title'], ENT_QUOTES) ?></strong>
<?php if ($e['status'] === 'cancelled'): ?>
<span class="badge" style="background:#fee2e2; color:#991b1b;">Abgesagt</span>
<?php endif; ?>
<div class="muted small" style="margin-top:4px;">
<?= htmlspecialchars($e['city'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['starts_at'], ENT_QUOTES) ?> · <?= htmlspecialchars($e['visibility'], ENT_QUOTES) ?>
· Anmeldungen: <?= (int)$e['participant_count'] ?>
</div>
</div>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</details>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'settings'): ?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Einstellungen</h2>
<p class="muted">Standort, Browserfreigabe und Cookie-Einstellungen an einem Ort.</p>
</div>
<div class="account-panel__body">
<form method="post" class="stack gap-12">
<input type="hidden" name="action" value="settings_location">
<div class="stack gap-6">
<label class="label" for="pLocationPref">Standort auf Papa-Kind-Treff verwenden</label>
<select id="pLocationPref" name="location_tracking_preference" class="select">
<option value="disabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'disabled') ? 'selected' : '' ?>>Nie verwenden</option>
<option value="prompt" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'prompt') ? 'selected' : '' ?>>Bei Bedarf nachfragen</option>
<option value="enabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'enabled') ? 'selected' : '' ?>>Wenn im Browser erlaubt, automatisch verwenden</option>
</select>
<p class="muted small">Diese Einstellung steuert nur, ob Papa-Kind-Treff Standortdaten nutzen darf. Die eigentliche Browserfreigabe wird separat im Browser verwaltet.</p>
</div>
<div class="flex gap-12">
<button class="btn" type="submit">Einstellungen speichern</button>
<button class="btn ghost" type="button" data-consent-open>Cookie-Einstellungen öffnen</button>
</div>
</form>
<div class="card" style="margin-top:18px;" id="locationBrowserStatusCard">
<strong>Browser-Status für Standort</strong>
<p class="muted small" id="locationBrowserStatusText" style="margin:8px 0 0;">Wird geprüft …</p>
<p class="muted small" id="locationBrowserStatusHint" style="margin:8px 0 0;">
Deine Website-Einstellung und die Browserfreigabe sind getrennte Ebenen. Wenn der Browser blockiert, kann Papa-Kind-Treff den Standort auch bei aktiver Einstellung nicht nutzen.
</p>
</div>
<div class="settings-help" style="margin-top:18px;">
<details>
<summary>Warum gibt es zwei Einstellungen?</summary>
<p class="muted small" style="margin:10px 0 0;">Der Browser entscheidet technisch, ob Standortzugriffe erlaubt sind. Papa-Kind-Treff entscheidet mit deiner Website-Einstellung, ob diese Möglichkeit auf der Plattform überhaupt verwendet werden soll.</p>
</details>
<details>
<summary>Chrome und Edge</summary>
<p class="muted small" style="margin:10px 0 0;">Auf das Schloss-Symbol links neben der Adresszeile klicken, dann `Website-Einstellungen` oder `Standort` öffnen und die Freigabe dort anpassen.</p>
</details>
<details>
<summary>Firefox</summary>
<p class="muted small" style="margin:10px 0 0;">Auf das Schloss-Symbol links neben der Adresse klicken und unter Berechtigungen den Standort prüfen oder zurücksetzen. Alternativ über Einstellungen → Datenschutz & Sicherheit → Berechtigungen.</p>
</details>
<details>
<summary>Safari</summary>
<p class="muted small" style="margin:10px 0 0;">In Safari die Website-Einstellungen für den aktuellen Tab oder die Datenschutz-/Standort-Einstellungen des Browsers beziehungsweise des Geräts prüfen.</p>
</details>
<details>
<summary>Direkt in die Browser-Einstellungen springen?</summary>
<p class="muted small" style="margin:10px 0 0;">Nein, das kann eine Website nicht zuverlässig und browserübergreifend auslösen. Deshalb zeigt Papa-Kind-Treff nur den Status an und erklärt, wo du die Freigabe findest.</p>
</details>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($section === 'admin' && in_array('admin', $allowedSections ?? [], true)): ?>
<section class="account-panel">
<div class="account-panel__head">
<h2>Community-Verwaltung</h2>
<p class="muted">Moderation, Bewerbungen, Rollen und Migration.</p>
</div>
<div class="account-panel__body">
<div class="flex gap-12" style="margin:0 0 16px 0; flex-wrap:wrap;">
<a class="btn" href="/community-admin">Zum Admin-Bereich</a>
</div>
<?php if ($communityMigrationStatus): ?>
<ul class="dash-list">
<li>Status: <?= !empty($communityMigrationStatus['complete']) ? 'Vollständig eingerichtet' : 'Migration noch offen' ?></li>
<li>Fehlende Bausteine: <?= !empty($communityMigrationStatus['missing']) ? htmlspecialchars(implode(', ', $communityMigrationStatus['missing']), ENT_QUOTES) : 'Keine' ?></li>
</ul>
<?php else: ?>
<p class="muted small">Migrationsstatus konnte nicht ermittelt werden.</p>
<?php endif; ?>
<?php if (!empty($communityIsSiteAdmin)): ?>
<form method="post" style="margin-top:12px;">
<input type="hidden" name="action" value="community_migrate">
<button class="btn" type="submit">Community-Migration ausführen</button>
</form>
<?php endif; ?>
</div>
</section>
<?php endif; ?>
</div>
</div>
</div>
@@ -263,15 +368,6 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
<label class="label" for="pProf">Beruf</label>
<input id="pProf" name="profession" class="input" value="<?= htmlspecialchars($profile['profession'], ENT_QUOTES) ?>">
</div>
<div class="stack gap-6">
<label class="label" for="pLocationPref">Standortfreigabe</label>
<select id="pLocationPref" name="location_tracking_preference" class="select">
<option value="disabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'disabled') ? 'selected' : '' ?>>Deaktiviert</option>
<option value="prompt" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'prompt') ? 'selected' : '' ?>>Beim nächsten Mal fragen</option>
<option value="enabled" <?= (($profile['location_tracking_preference'] ?? 'prompt') === 'enabled') ? 'selected' : '' ?>>Immer verwenden</option>
</select>
<p class="muted small">Bei aktiver Freigabe wird dein aktueller Standort für passende Treffen in deiner Nähe verwendet. Du kannst die Abfrage hier jederzeit wieder auf Nachfrage oder komplett aus stellen.</p>
</div>
<div class="stack gap-6">
<label class="label" for="pAbout">Kurzvorstellung</label>
<textarea id="pAbout" name="about" class="textarea" rows="3"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
@@ -335,7 +431,7 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
<h3><?= $editing ? 'Event bearbeiten' : 'Neues Event' ?></h3>
<button class="btn ghost" type="button" data-modal-close>✕</button>
</div>
<form class="stack gap-12" style="margin-top: 10px;" method="post" action="/dashboard#events">
<form class="stack gap-12" style="margin-top: 10px;" method="post" action="/dashboard?section=events#events">
<input type="hidden" name="action" value="<?= htmlspecialchars($actionEvent, ENT_QUOTES) ?>">
<?php if ($editing && $editEvent): ?>
<input type="hidden" name="event_id" value="<?= (int)$editEvent['id'] ?>">
@@ -600,4 +696,45 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
if (modal) { modal.classList.add('open'); }
})();
<?php endif; ?>
(function(){
const statusText = document.getElementById('locationBrowserStatusText');
const hintText = document.getElementById('locationBrowserStatusHint');
const profilePreference = document.body?.dataset.locationPreference || 'prompt';
if (!statusText || !hintText) return;
const profileLabels = {
disabled: 'Deaktiviert',
prompt: 'Beim nächsten Mal fragen',
enabled: 'Immer verwenden',
};
const render = (browserState) => {
if (browserState === 'granted') {
statusText.textContent = 'Der Browser erlaubt Standortzugriffe derzeit.';
} else if (browserState === 'denied') {
statusText.textContent = 'Der Browser blockiert Standortzugriffe derzeit.';
} else if (browserState === 'prompt') {
statusText.textContent = 'Der Browser fragt bei Standortzugriffen erneut nach.';
} else {
statusText.textContent = 'Der Browserstatus konnte nicht eindeutig ermittelt werden.';
}
const profileState = profileLabels[profilePreference] || profileLabels.prompt;
hintText.textContent = `Website-Einstellung: ${profileState}. Wenn Browser und Website-Einstellung sich unterscheiden, gilt technisch zuerst die Browserfreigabe.`;
};
if (!('permissions' in navigator) || typeof navigator.permissions.query !== 'function') {
render('unknown');
return;
}
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
render(result.state);
if (typeof result.onchange !== 'undefined') {
result.onchange = () => render(result.state);
}
}).catch(() => render('unknown'));
})();
</script>

View File

@@ -116,6 +116,7 @@ if ($pdo && ($q !== '' || $loc !== '' || ($lat !== null && $lng !== null))) {
</main>
<script>
(function(){
const body = document.body;
const locInput = document.getElementById('loc');
const latInput = document.getElementById('lat');
const lngInput = document.getElementById('lng');
@@ -125,8 +126,64 @@ if ($pdo && ($q !== '' || $loc !== '' || ($lat !== null && $lng !== null))) {
const btnMap = document.getElementById('btnMap');
const mapWrapper = document.getElementById('mapWrapper');
const mapContainer = document.getElementById('mapContainer');
const isLoggedIn = body?.dataset.auth === '1';
const profileLocationPreference = body?.dataset.locationPreference || 'prompt';
const locationStorageKey = 'pkt_user_location';
const locationSessionStorageKey = 'pkt_user_location_session';
let map, marker;
function effectiveLocationPreference() {
if (!isLoggedIn) {
try {
const guestPreference = window.localStorage.getItem('pkt_location_preference_guest');
if (guestPreference && ['disabled', 'prompt', 'enabled'].includes(guestPreference)) {
return guestPreference;
}
} catch (err) {
// ignore
}
}
return profileLocationPreference;
}
function getStoredLocation() {
const candidates = [];
try {
const sessionValue = window.sessionStorage.getItem(locationSessionStorageKey);
if (sessionValue) candidates.push(sessionValue);
} catch (err) {
// ignore
}
try {
const localValue = window.localStorage.getItem(locationStorageKey);
if (localValue) candidates.push(localValue);
} catch (err) {
// ignore
}
for (const value of candidates) {
try {
const parsed = JSON.parse(value);
if (parsed && Number.isFinite(parsed.lat) && Number.isFinite(parsed.lng)) {
return parsed;
}
} catch (err) {
// ignore
}
}
return null;
}
function applyLocation(lat, lng, label = 'Mein Standort') {
latInput.value = Number(lat).toFixed(6);
lngInput.value = Number(lng).toFixed(6);
if (locInput && !locInput.value.trim()) {
locInput.value = label;
}
toggleRadius(true);
}
function toggleRadius(show) {
if (!radiusWrap) return;
radiusWrap.hidden = !show;
@@ -198,17 +255,25 @@ if ($pdo && ($q !== '' || $loc !== '' || ($lat !== null && $lng !== null))) {
window.PKTConsent?.openPreferences?.();
return;
}
const preference = effectiveLocationPreference();
if (preference === 'disabled') {
alert('Die Standortverwendung ist in deinem Profil deaktiviert.');
return;
}
const storedLocation = getStoredLocation();
if (preference === 'enabled' && storedLocation) {
applyLocation(storedLocation.lat, storedLocation.lng, storedLocation.label || 'Mein Standort');
return;
}
if (!navigator.geolocation) {
alert('Geolocation wird nicht unterstützt.');
return;
}
navigator.geolocation.getCurrentPosition((pos) => {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
latInput.value = lat.toFixed(6);
lngInput.value = lng.toFixed(6);
if (locInput && !locInput.value.trim()) locInput.value = 'Mein Standort';
toggleRadius(true);
applyLocation(pos.coords.latitude, pos.coords.longitude, 'Mein Standort');
}, () => alert('Standort konnte nicht ermittelt werden.'));
});

View File

@@ -45,8 +45,11 @@ if ($isLoggedIn) {
<span class="profile-menu__name"><?= htmlspecialchars($displayName, ENT_QUOTES) ?></span>
</button>
<div class="profile-menu__dropdown" role="menu">
<a href="/dashboard" role="menuitem">Übersicht</a>
<a href="/dashboard" role="menuitem">Mitgliederbereich</a>
<a href="/dashboard?section=profile" role="menuitem">Profil</a>
<a href="/dashboard?section=children" role="menuitem">Kinder</a>
<a href="/dashboard?section=events" role="menuitem">Termine</a>
<a href="/dashboard?section=community" role="menuitem">Community</a>
<a href="/dashboard?section=settings" role="menuitem">Einstellungen</a>
<a href="/logout" role="menuitem">Abmelden</a>
</div>
</div>
@@ -62,8 +65,11 @@ if ($isLoggedIn) {
<a href="/search">Suche</a>
<a href="/community">Community</a>
<?php if ($isLoggedIn): ?>
<a class="btn ghost" href="/dashboard">Übersicht</a>
<a class="btn ghost" href="/dashboard">Mitgliederbereich</a>
<a class="btn ghost" href="/dashboard?section=profile">Profil</a>
<a class="btn ghost" href="/dashboard?section=children">Kinder</a>
<a class="btn ghost" href="/dashboard?section=events">Termine</a>
<a class="btn ghost" href="/dashboard?section=community">Community</a>
<a class="btn ghost" href="/dashboard?section=settings">Einstellungen</a>
<a class="btn block" href="/logout">Abmelden</a>
<?php else: ?>
<a class="btn ghost" href="/login">Anmelden</a>

View File

@@ -207,6 +207,27 @@ body {
.forum-sidebar__group a:hover { background: #f8f4ec; color: var(--color-primary); }
.forum-sidebar__group a.is-active { background: var(--color-primary); color: var(--color-primary-contrast); }
.forum-sidebar__group small { font-size: 12px; opacity: .8; }
.account-layout { display: grid; grid-template-columns: 290px minmax(0, 1fr); gap: 24px; align-items: start; }
.account-sidebar { position: sticky; top: 104px; }
.account-sidebar__inner { background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); overflow: hidden; }
.account-sidebar__head { padding: 20px 20px 16px; border-bottom: 1px solid var(--color-border); background: linear-gradient(180deg, #fffdfa, #f8f4ec); }
.account-sidebar__head h2 { margin: 10px 0 0; font-size: 22px; }
.account-sidebar__nav { padding: 14px; display: grid; gap: 6px; }
.account-sidebar__nav a { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-radius: 12px; color: var(--color-text); font-weight: 600; }
.account-sidebar__nav a:hover { background: #f8f4ec; color: var(--color-primary); }
.account-sidebar__nav a.is-active { background: var(--color-primary); color: var(--color-primary-contrast); }
.account-main { min-width: 0; display: grid; gap: 20px; }
.account-panel { background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); overflow: hidden; }
.account-panel__head { padding: 22px 24px; border-bottom: 1px solid var(--color-border); background: #fffdfa; }
.account-panel__head h2 { margin: 0 0 6px; }
.account-panel__body { padding: 24px; }
.account-kv { display: grid; gap: 10px; margin: 0; }
.account-kv div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 14px; }
.account-kv dt { font-weight: 700; color: var(--color-muted); }
.account-kv dd { margin: 0; }
.settings-help { display: grid; gap: 14px; }
.settings-help details { border: 1px solid var(--color-border); border-radius: 12px; padding: 12px 14px; background: #fff; }
.settings-help summary { cursor: pointer; font-weight: 700; }
.forum-search { display: grid; gap: 8px; min-width: min(100%, 360px); }
.forum-search--hero { width: 100%; }
.forum-search__row { display: flex; gap: 10px; }
@@ -271,7 +292,9 @@ body {
.forum-list__header,
.forum-row { grid-template-columns: minmax(0, 1fr); }
.forum-shell { grid-template-columns: 1fr; }
.account-layout { grid-template-columns: 1fr; }
.forum-sidebar { position: static; }
.account-sidebar { position: static; }
.forum-row__count,
.forum-row__activity { grid-auto-flow: column; justify-content: start; gap: 10px; align-items: baseline; }
.forum-post { grid-template-columns: 1fr; }
@@ -295,9 +318,11 @@ body {
.forum-board-row,
.forum-admin-item { padding-left: 16px; padding-right: 16px; }
.forum-post__body,
.forum-reply-form { padding: 18px 16px; }
.forum-reply-form,
.account-panel__body { padding: 18px 16px; }
.cookie-consent__inner { grid-template-columns: 1fr; }
.cookie-consent__actions { justify-content: stretch; }
.account-kv div { grid-template-columns: 1fr; gap: 4px; }
}
/* Auth & Dashboard */

View File

@@ -159,6 +159,8 @@ final class AccountPages
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
$communityMigration = $pdo ? new CommunityMigration($pdo) : null;
$profileSettings = $pdo ? new ProfileSettings($pdo) : null;
$section = (string)($_GET['section'] ?? 'profile');
$allowedSections = ['profile', 'children', 'events', 'community', 'settings'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
@@ -188,6 +190,15 @@ final class AccountPages
'id' => $userId,
]);
$info = 'Profil gespeichert.';
} elseif ($action === 'settings_location') {
$locationPreference = (string)($_POST['location_tracking_preference'] ?? 'prompt');
if ($profileSettings) {
$profileSettings->updateLocationTrackingPreference(
$userId,
in_array($locationPreference, ['disabled', 'prompt', 'enabled'], true) ? $locationPreference : 'prompt'
);
}
$info = 'Einstellungen gespeichert.';
} elseif ($action === 'child_add') {
$firstNameEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['first_name'])) : trim((string)$_POST['first_name']);
$noteEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['note'])) : trim((string)$_POST['note']);
@@ -396,6 +407,12 @@ final class AccountPages
];
$communityIsSiteAdmin = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
$communityMigrationStatus = ($communityMigration && $communityIsSiteAdmin) ? $communityMigration->status() : null;
if (in_array('forum_admin', $communityRoles, true) || $communityIsSiteAdmin) {
$allowedSections[] = 'admin';
}
if (!in_array($section, $allowedSections, true)) {
$section = 'profile';
}
return compact(
'flash',
@@ -413,7 +430,9 @@ final class AccountPages
'communityCanApply',
'communityRestrictions',
'communityIsSiteAdmin',
'communityMigrationStatus'
'communityMigrationStatus',
'section',
'allowedSections'
);
}