Files
papa-kind-treff.info/partials/landing/account/dashboard.php
2025-12-29 00:37:29 +01:00

662 lines
28 KiB
PHP

<?php
if (!isset($_SESSION['user_id'])) {
redirect('/login');
}
$app = app();
$pdo = $app->pdo();
$flash = $app->flash()->get();
$userId = (int)$_SESSION['user_id'];
$error = '';
$info = '';
$crypto = null;
try { $crypto = new \App\Crypto($app->config()); } catch (\Throwable) {}
function geocode_address(?string $street, ?string $zip, ?string $city, ?string $region): array
{
$parts = array_filter([
$street ?: null,
$zip ?: null,
$city ?: null,
$region ?: null,
]);
if (!$parts) {
return [null, null];
}
$query = implode(', ', $parts);
$url = 'https://nominatim.openstreetmap.org/search?' . http_build_query([
'format' => 'jsonv2',
'limit' => 1,
'q' => $query,
]);
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'header' => "User-Agent: papa-kind-treff/1.0\r\nAccept-Language: de\r\n",
'timeout' => 6,
],
]);
$resp = @file_get_contents($url, false, $ctx);
if ($resp === false) {
return [null, null];
}
$json = json_decode($resp, true);
if (!is_array($json) || empty($json[0]['lat']) || empty($json[0]['lon'])) {
return [null, null];
}
return [round((float)$json[0]['lat'], 7), round((float)$json[0]['lon'], 7)];
}
// POST Aktionen
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
try {
if ($action === 'profile') {
$languages = $_POST['languages'] ?? '';
if (is_array($languages)) {
$languages = implode(', ', array_map('trim', $languages));
}
$phoneEnc = $crypto ? $crypto->encrypt(trim((string)$_POST['contact_phone'])) : trim((string)$_POST['contact_phone']);
$stmt = $pdo->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, updated_at=NOW() WHERE user_id=:id');
$stmt->execute([
'name' => trim((string)$_POST['display_name']),
'fname' => trim((string)$_POST['first_name']),
'lname' => trim((string)$_POST['last_name']),
'zip' => trim((string)$_POST['zip']),
'city' => trim((string)$_POST['city']),
'prof' => trim((string)$_POST['profession']),
'langs' => trim((string)$languages),
'about' => trim((string)$_POST['about']),
'phone' => $phoneEnc,
'id' => $userId,
]);
$info = 'Profil 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']);
$stmt = $pdo->prepare('INSERT INTO children (user_id, gender, birthdate, age_years, encrypted_first_name, note, created_at, updated_at) VALUES (:uid, :gender, :birthdate, :age, :name, :note, NOW(), NOW())');
$stmt->execute([
'uid' => $userId,
'gender' => $_POST['gender'] ?? 'unknown',
'birthdate' => $_POST['birthdate'] ?: null,
'age' => $_POST['age_years'] ?: null,
'name' => $firstNameEnc,
'note' => $noteEnc,
]);
$info = 'Kind hinzugefügt.';
} elseif ($action === 'event_add') {
$street = trim((string)($_POST['street'] ?? ''));
$zip = trim((string)($_POST['zip'] ?? ''));
$city = trim((string)($_POST['city'] ?? ''));
$region = trim((string)($_POST['region'] ?? ''));
$lat = isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null;
$lng = isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null;
if ($lat === 0.0 && $lng === 0.0 && ($_POST['lat'] === '0' || $_POST['lng'] === '0')) {
// keep zero if explicitly set (unlikely)
} elseif ($lat === null || $lng === null) {
[$lat, $lng] = geocode_address($street, $zip, $city, $region);
}
$stmt = $pdo->prepare('INSERT INTO events (created_by, title, teaser_public, description, location_label, street, zip, city, region, lat, lng, starts_at, allow_kids, visibility, status, created_at, updated_at) VALUES (:uid, :title, :teaser, :descr, :loc, :street, :zip, :city, :region, :lat, :lng, :start, :allow, :vis, :status, NOW(), NOW())');
$stmt->execute([
'uid' => $userId,
'title' => trim((string)$_POST['title']),
'teaser' => trim((string)$_POST['teaser']),
'descr' => trim((string)$_POST['description']),
'loc' => trim((string)$_POST['location_label']),
'street' => $street ?: null,
'zip' => $zip,
'city' => $city,
'region' => $region,
'lat' => $lat,
'lng' => $lng,
'start' => $_POST['starts_at'] ?? null,
'allow' => isset($_POST['allow_kids']) ? 1 : 0,
'vis' => $_POST['visibility'] ?? 'public',
'status' => 'published',
]);
$info = 'Event gespeichert.';
} elseif ($action === 'event_delete') {
$eventId = (int)($_POST['event_id'] ?? 0);
$stmt = $pdo->prepare('SELECT id, created_by, status, (SELECT COUNT(*) FROM event_participants ep WHERE ep.event_id = events.id) AS participant_count FROM events WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $eventId]);
$ev = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ev || (int)$ev['created_by'] !== $userId) {
throw new RuntimeException('Event nicht gefunden.');
}
if ((int)$ev['participant_count'] > 0) {
throw new RuntimeException('Event hat Anmeldungen und kann nicht gelöscht werden.');
}
$pdo->prepare('DELETE FROM events WHERE id = :id')->execute(['id' => $eventId]);
$info = 'Event gelöscht.';
} elseif ($action === 'event_cancel') {
$eventId = (int)($_POST['event_id'] ?? 0);
$stmt = $pdo->prepare('SELECT id, created_by FROM events WHERE id = :id LIMIT 1');
$stmt->execute(['id' => $eventId]);
$ev = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ev || (int)$ev['created_by'] !== $userId) {
throw new RuntimeException('Event nicht gefunden.');
}
$pdo->prepare('UPDATE events SET status = :st, updated_at = NOW() WHERE id = :id')->execute([
'st' => 'cancelled',
'id' => $eventId,
]);
$info = 'Event wurde abgesagt.';
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
// Daten laden
$profile = [
'display_name' => '',
'first_name' => '',
'last_name' => '',
'zip' => '',
'city' => '',
'profession' => '',
'languages' => '',
'about' => '',
'email' => '',
'contact_phone' => '',
];
$stmt = $pdo->prepare('SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
$stmt->execute(['id' => $userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$profile = array_merge($profile, array_filter($row, fn($v) => $v !== null));
if ($crypto && !empty($profile['contact_phone'])) {
$profile['contact_phone'] = $crypto->decrypt((string)$profile['contact_phone']) ?: '';
}
}
$children = [];
$stmt = $pdo->prepare('SELECT id, encrypted_first_name AS first_name, note, gender, birthdate, age_years FROM children WHERE user_id = :id ORDER BY id DESC');
$stmt->execute(['id' => $userId]);
$childrenRaw = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
foreach ($childrenRaw as $c) {
if ($crypto) {
$c['first_name'] = $crypto->decrypt((string)$c['first_name']) ?: '';
$c['note'] = $crypto->decrypt((string)($c['note'] ?? '')) ?: '';
}
$children[] = $c;
}
$eventsUpcoming = [];
$eventsPast = [];
$stmt = $pdo->prepare(
'SELECT e.id, e.title, e.teaser_public, e.starts_at, e.city, e.visibility, e.status,
(SELECT COUNT(*) FROM event_participants ep WHERE ep.event_id = e.id) AS participant_count
FROM events e
WHERE e.created_by = :id AND e.starts_at >= NOW()
ORDER BY e.starts_at ASC'
);
$stmt->execute(['id' => $userId]);
$eventsUpcoming = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
$stmt = $pdo->prepare(
'SELECT e.id, e.title, e.teaser_public, e.starts_at, e.city, e.visibility, e.status,
(SELECT COUNT(*) FROM event_participants ep WHERE ep.event_id = e.id) AS participant_count
FROM events e
WHERE e.created_by = :id AND e.starts_at < NOW()
ORDER BY e.starts_at DESC'
);
$stmt->execute(['id' => $userId]);
$eventsPast = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
?>
<main class="section">
<div class="container" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;">
<div>
<p class="eyebrow">Mitgliederbereich</p>
<h1>Hallo, <span style="color: var(--color-primary);"><?= htmlspecialchars($profile['display_name'] ?: 'Papa', ENT_QUOTES) ?></span>!</h1>
<p class="muted">Verwalte dein Profil, Kinder, Events und Teilnahmen.</p>
</div>
</div>
<div class="container dash-section">
<?php if ($flash): ?>
<div class="toast-bar"><?= htmlspecialchars($flash['message'], ENT_QUOTES) ?></div>
<?php endif; ?>
<?php if ($info): ?>
<div class="toast-bar" style="margin-top:10px;"><?= htmlspecialchars($info, ENT_QUOTES) ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="toast-bar" style="margin-top:10px; border-color:#f87171; color:#991b1b;">Fehler: <?= htmlspecialchars($error, ENT_QUOTES) ?></div>
<?php endif; ?>
</div>
<div class="container dash-section">
<div class="dash-grid-2">
<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>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>
<?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>
</div>
</div>
</div>
</div>
<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;">
<?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>
</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>
</div>
<!-- Modals -->
<div class="modal" id="modalProfile">
<div class="panel">
<div class="head flex between center-y">
<h3>Profil bearbeiten</h3>
<button class="btn ghost" type="button" data-modal-close>✕</button>
</div>
<form method="post" class="stack gap-12">
<input type="hidden" name="action" value="profile">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pName">Anzeigename</label>
<input id="pName" name="display_name" class="input" value="<?= htmlspecialchars($profile['display_name'], ENT_QUOTES) ?>">
</div>
<div class="stack gap-6">
<label class="label" for="pFirst">Vorname</label>
<input id="pFirst" name="first_name" class="input" value="<?= htmlspecialchars($profile['first_name'], ENT_QUOTES) ?>">
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pLast">Nachname</label>
<input id="pLast" name="last_name" class="input" value="<?= htmlspecialchars($profile['last_name'], ENT_QUOTES) ?>">
</div>
<div class="stack gap-6">
<label class="label" for="pCity">Ort</label>
<input id="pCity" name="city" class="input" value="<?= htmlspecialchars($profile['city'], ENT_QUOTES) ?>">
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="pZip">PLZ</label>
<input id="pZip" name="zip" class="input" value="<?= htmlspecialchars($profile['zip'], ENT_QUOTES) ?>">
</div>
<div class="stack gap-6">
<label class="label" for="pPhone">Telefon (mobil)</label>
<input id="pPhone" name="contact_phone" class="input" value="<?= htmlspecialchars($profile['contact_phone'], ENT_QUOTES) ?>">
</div>
</div>
<div class="stack gap-6">
<label class="label">Sprachen (Mehrfachauswahl)</label>
<div class="chips" style="flex-wrap: wrap;">
<?php
$langOptions = ['Deutsch','Englisch','Französisch','Spanisch','Türkisch','Arabisch','Polnisch'];
$currentLangs = array_filter(array_map('trim', explode(',', (string)$profile['languages'])));
?>
<?php foreach ($langOptions as $opt): ?>
<label class="chip" style="cursor:pointer;">
<input type="checkbox" name="languages[]" value="<?= htmlspecialchars($opt, ENT_QUOTES) ?>" <?= in_array($opt, $currentLangs, true) ? 'checked' : '' ?> style="margin-right:6px;">
<?= htmlspecialchars($opt, ENT_QUOTES) ?>
</label>
<?php endforeach; ?>
</div>
<label class="label" for="pLangCustom">Weitere Sprachen (Kommagetrennt)</label>
<input id="pLangCustom" name="languages[]" class="input" placeholder="z. B. Italienisch, Niederländisch">
</div>
<div class="stack gap-6">
<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="pAbout">Kurzvorstellung</label>
<textarea id="pAbout" name="about" class="textarea" rows="3"><?= htmlspecialchars($profile['about'], ENT_QUOTES) ?></textarea>
</div>
<div class="flex gap-12">
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
<button class="btn" type="submit">Speichern</button>
</div>
</form>
</div>
</div>
<div class="modal" id="modalChild">
<div class="panel">
<div class="head flex between center-y">
<h3>Kind hinzufügen</h3>
<button class="btn ghost" type="button" data-modal-close>✕</button>
</div>
<form method="post" class="stack gap-12">
<input type="hidden" name="action" value="child_add">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="cName">Vorname</label>
<input id="cName" name="first_name" class="input" required>
</div>
<div class="stack gap-6">
<label class="label" for="cGender">Geschlecht</label>
<select id="cGender" name="gender" class="select">
<option value="male">Männlich</option>
<option value="female">Weiblich</option>
<option value="diverse">Divers</option>
<option value="unknown">Unbekannt</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="cBirth">Geburtsdatum</label>
<input id="cBirth" name="birthdate" class="input" type="date">
</div>
<div class="stack gap-6">
<label class="label" for="cAge">Alter (Jahre)</label>
<input id="cAge" name="age_years" class="input" type="number" min="0" max="18">
</div>
</div>
<div class="stack gap-6">
<label class="label" for="cNote">Notiz</label>
<input id="cNote" name="note" class="input">
</div>
<div class="flex gap-12">
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
<button class="btn" type="submit">Speichern</button>
</div>
</form>
</div>
</div>
<div class="modal" id="modalEvent">
<div class="panel">
<div class="head flex between center-y">
<h3>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">
<input type="hidden" name="action" value="event_add">
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="evTitle">Titel</label>
<input id="evTitle" name="title" class="input" placeholder="z. B. Väter-Kaffee im Park" required>
</div>
<div class="stack gap-6">
<label class="label" for="evTeaser">Kurzbeschreibung</label>
<input id="evTeaser" name="teaser" class="input" placeholder="Kurztext für Gäste" required>
</div>
</div>
<div class="stack gap-6">
<label class="label" for="evDesc">Beschreibung (voll)</label>
<textarea id="evDesc" name="description" class="textarea" rows="3" placeholder="Was soll passieren, was mitbringen?" required></textarea>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="evDate">Datum & Uhrzeit</label>
<input id="evDate" name="starts_at" class="input" type="datetime-local" required>
</div>
<div class="stack gap-6">
<label class="label" for "evLoc">Ort/Label</label>
<input id="evLoc" name="location_label" class="input" placeholder="Park / Café">
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="evZip">PLZ</label>
<input id="evZip" name="zip" class="input" maxlength="5">
</div>
<div class="stack gap-6">
<label class="label" for="evCity">Stadt</label>
<input id="evCity" name="city" class="input">
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="evStreet">Straße / Adresse</label>
<input id="evStreet" name="street" class="input" placeholder="z. B. Musterstraße 12">
<p class="muted small">Wird zur Karten-/Umkreissuche genutzt.</p>
</div>
<div class="stack gap-6">
<label class="label" for="evRegion">Region/Bezirk</label>
<input id="evRegion" name="region" class="input">
</div>
</div>
<input type="hidden" id="evLat" name="lat">
<input type="hidden" id="evLng" name="lng">
<div class="stack gap-6">
<button class="btn ghost" type="button" id="btnMap">Auf Karte suchen</button>
<div id="mapWrapper" class="map-wrapper" hidden>
<div class="form-row">
<label class="label" for="mapSearch">Adresse suchen (optional)</label>
<div class="flex gap-8">
<input id="mapSearch" class="input" placeholder="Straße, PLZ, Ort">
<button class="btn ghost" type="button" id="btnMapSearch">Suchen</button>
</div>
<p class="muted small">Klick auf die Karte setzt den Treffpunkt. Zoom/Scroll möglich.</p>
</div>
<div id="mapContainer" class="map-container"></div>
</div>
</div>
<div class="form-grid">
<div class="stack gap-6">
<label class="label" for="evVis">Sichtbarkeit</label>
<select id="evVis" name="visibility" class="select">
<option value="public">Öffentlich</option>
<option value="members">Nur Mitglieder</option>
</select>
</div>
</div>
<label class="label" style="display:flex; align-items:center; gap:8px;">
<input type="checkbox" name="allow_kids" checked> Kinder erlaubt
</label>
<div class="flex gap-12">
<button class="btn ghost" type="button" data-modal-close>Abbrechen</button>
<button class="btn" type="submit">Event anlegen</button>
</div>
</form>
</div>
</div>
</main>
<script>
document.querySelectorAll('[data-modal-open]').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-modal-open');
const modal = document.getElementById(id);
if (modal) modal.classList.add('open');
});
});
document.querySelectorAll('[data-modal-close]').forEach(btn => {
btn.addEventListener('click', () => {
const modal = btn.closest('.modal');
if (modal) modal.classList.remove('open');
});
});
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.classList.remove('open');
});
});
// Map picker for events (Leaflet)
const btnMap = document.getElementById('btnMap');
const mapWrapper = document.getElementById('mapWrapper');
const mapContainer = document.getElementById('mapContainer');
const btnMapSearch = document.getElementById('btnMapSearch');
const mapSearch = document.getElementById('mapSearch');
const latInput = document.getElementById('evLat');
const lngInput = document.getElementById('evLng');
let map, marker;
function ensureLeaflet(callback) {
if (window.L) { callback(); return; }
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(css);
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = callback;
document.body.appendChild(script);
}
function initMap() {
if (map) { map.invalidateSize(); return; }
map = L.map(mapContainer).setView([51.1657, 10.4515], 6);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap',
}).addTo(map);
const setMarker = (latlng) => {
if (marker) {
marker.setLatLng(latlng);
} else {
marker = L.marker(latlng, { draggable: true }).addTo(map);
marker.on('dragend', () => {
const ll = marker.getLatLng();
latInput.value = ll.lat.toFixed(7);
lngInput.value = ll.lng.toFixed(7);
});
}
latInput.value = latlng.lat.toFixed(7);
lngInput.value = latlng.lng.toFixed(7);
};
map.on('click', (e) => setMarker(e.latlng));
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
setMarker({ lat, lng });
map.setView({ lat, lng }, 14);
}
}
btnMap?.addEventListener('click', () => {
mapWrapper.hidden = false;
ensureLeaflet(() => {
setTimeout(() => initMap(), 50);
});
});
btnMapSearch?.addEventListener('click', () => {
const q = (mapSearch?.value || '').trim();
if (!q) return;
fetch('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q=' + encodeURIComponent(q), {
headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' },
})
.then(r => r.json())
.then(data => {
if (Array.isArray(data) && data[0]) {
const lat = parseFloat(data[0].lat);
const lng = parseFloat(data[0].lon);
ensureLeaflet(() => {
mapWrapper.hidden = false;
initMap();
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
map.setView([lat, lng], 15);
if (window.L) {
if (!marker) marker = L.marker([lat, lng], { draggable: true }).addTo(map);
marker.setLatLng([lat, lng]);
latInput.value = lat.toFixed(7);
lngInput.value = lng.toFixed(7);
}
}
});
}
})
.catch(() => {});
});
</script>