This commit is contained in:
2025-12-31 01:00:41 +01:00
parent 3110b48c33
commit cbcd09003e
5 changed files with 232 additions and 180 deletions

View File

@@ -6,7 +6,7 @@ $eventsForJs = [];
try {
$pdo = $app->pdo();
if ($pdo) {
$stmt = $pdo->prepare('SELECT id, title, teaser_public, description, city, region, zip, starts_at, allow_kids, visibility, location_label, lat, lng FROM events WHERE starts_at >= NOW() AND status != "cancelled" ORDER BY starts_at ASC LIMIT 50');
$stmt = $pdo->prepare('SELECT id, title, teaser_public, description, city, region, zip, starts_at, allow_kids, visibility, location_label, lat, lng, created_at FROM events WHERE starts_at >= NOW() AND status != "cancelled" ORDER BY created_at DESC, starts_at ASC LIMIT 10');
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
foreach ($rows as $r) {
@@ -58,63 +58,64 @@ try {
<div class="chip inline">Echt lokale Gruppen</div>
</div>
</div>
<div class="hero__card card">
<div class="badge">Schnellsuche</div>
<h3>Events in deiner Region</h3>
<div class="form-row">
<label class="label" for="locInput">PLZ oder Ort</label>
<input id="locInput" class="input" placeholder="z. B. 10437 oder Berlin" />
</div>
<div class="form-row">
<label class="label" for="topicSelect">Thema</label>
<select id="topicSelect" class="select">
<option value="">Alle</option>
<option value="outdoor">Outdoor / Spielplatz</option>
<option value="kaffee">Kaffee & Austausch</option>
<option value="sport">Sport</option>
<option value="workshop">Workshop</option>
</select>
</div>
<div class="form-row">
<label class="label" for="ageSelect">Alter der Kinder</label>
<select id="ageSelect" class="select">
<option value="">Alle</option>
<option value="baby">0-2 Jahre</option>
<option value="kids">3-6 Jahre</option>
<option value="school">7-12 Jahre</option>
</select>
</div>
<button class="btn block" id="btnSearch">Events anzeigen</button>
<button class="btn ghost block" id="btnGeo">Standort automatisch ermitteln</button>
<p class="muted small">Geodaten werden nur zur Anzeige verwendet.</p>
</div>
</div>
</section>
<style>
.slider {display:flex; align-items:center; gap:12px;}
.slider__viewport {overflow-x:hidden; flex:1; scroll-behavior:smooth;}
.slider__track {display:flex; gap:12px; min-height:100%;}
.slider__track .event-card-small {min-width:240px; max-width:260px;}
.slider__nav {min-width:44px;}
</style>
<section class="container section" id="events">
<div class="section__head">
<div>
<p class="eyebrow">Termine entdecken</p>
<h2>Die nächsten anstehenden Events</h2>
<p class="muted">Gäste sehen nur Basisinfos. Als Mitglied siehst du vollständige Details, kannst zusagen und neue Treffen anlegen.</p>
</div>
<div class="chips">
<button class="chip" data-filter="all">Alle</button>
<button class="chip" data-filter="outdoor">Outdoor</button>
<button class="chip" data-filter="kaffee">Kaffee</button>
<button class="chip" data-filter="sport">Sport</button>
<button class="chip" data-filter="workshop">Workshops</button>
<h2>Neueste Termine</h2>
<p class="muted">Die zehn neuesten veröffentlichten Events kompakt zum Durchscrollen. Gäste sehen Basisinfos, Mitglieder erhalten alle Details.</p>
</div>
</div>
<div class="results" id="eventList"></div>
<div class="surface border rounded p-4 mt-3">
<div class="flex between center-y gap-12">
<div>
<strong>Nur für eingeloggte Mitglieder</strong>
<p class="muted small">Volle Beschreibung, Kontakt, Kinder-Infos und Anmeldeoptionen.</p>
</div>
<button class="btn">Jetzt anmelden</button>
<div class="slider">
<button class="btn ghost slider__nav" type="button" data-slider-prev aria-label="Zurück"></button>
<div class="slider__viewport">
<div class="slider__track" id="eventSlider"></div>
</div>
<button class="btn ghost slider__nav" type="button" data-slider-next aria-label="Weiter"></button>
</div>
</section>
<section class="section alt" id="quicksearch">
<div class="container">
<p class="eyebrow">Schnellsuche</p>
<h3>Passende Events finden</h3>
<form id="quickSearchForm" class="grid grid-3" style="gap: 12px; align-items:flex-end;" action="/search" method="get">
<div class="stack gap-6">
<label class="label" for="qsQuery">Suchbegriff</label>
<input id="qsQuery" name="q" class="input" placeholder="Titel, Thema, Beschreibung">
</div>
<div class="stack gap-6">
<label class="label" for="qsLoc">Ort oder PLZ</label>
<div class="flex gap-8">
<input id="qsLoc" name="loc" class="input" placeholder="z. B. 10437 oder Berlin" style="flex:1;">
<button class="btn ghost" type="button" id="quickGeo">📍</button>
</div>
<input type="hidden" name="lat" id="qsLat">
<input type="hidden" name="lng" id="qsLng">
</div>
<div class="stack gap-6">
<label class="label" for="qsRadius">Umkreis (km)</label>
<select id="qsRadius" name="radius" class="select">
<?php foreach ([1,2,5,10,15,25] as $r): ?>
<option value="<?= $r ?>"><?= $r ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<button class="btn block" type="submit" style="width:100%;">Suchen</button>
</div>
</form>
<p class="muted small" style="margin-top:8px;">Optional Standort ermitteln oder Ort eingeben; Umkreis bestimmt die Treffer in der Suche.</p>
</div>
</section>

View File

@@ -4,29 +4,73 @@ declare(strict_types=1);
$app = app();
$pdo = $app->pdo();
$q = trim((string)($_GET['q'] ?? ''));
$loc = trim((string)($_GET['loc'] ?? ''));
$radius = isset($_GET['radius']) ? (float)$_GET['radius'] : 5.0;
$radius = ($radius > 0) ? $radius : 5.0;
$lat = isset($_GET['lat']) && $_GET['lat'] !== '' ? (float)$_GET['lat'] : null;
$lng = isset($_GET['lng']) && $_GET['lng'] !== '' ? (float)$_GET['lng'] : null;
$results = [];
if ($q !== '' && $pdo) {
function geocode_loc(string $loc): array
{
$url = 'https://nominatim.openstreetmap.org/search?' . http_build_query([
'format' => 'jsonv2',
'limit' => 1,
'q' => $loc,
]);
$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'], 6), round((float)$json[0]['lon'], 6)];
}
if ($pdo && ($q !== '' || $loc !== '' || ($lat !== null && $lng !== null))) {
if ($lat === null && $lng === null && $loc !== '') {
[$lat, $lng] = geocode_loc($loc);
}
$geo = ($lat !== null && $lng !== null) ? ['lat' => $lat, 'lng' => $lng, 'radius' => $radius] : null;
$search = new \App\Search($pdo);
$results = $search->searchEvents($q, 100);
$results = $search->searchEvents($q, 100, $geo);
}
?>
<main class="section">
<div class="container">
<p class="eyebrow">Suche</p>
<h1>Events finden</h1>
<form method="get" class="form-grid single" style="margin: 14px 0;">
<form method="get" class="form-grid single" style="margin: 14px 0; gap:12px; align-items:end;">
<div class="stack gap-6">
<label class="label" for="q">Suchbegriff (Titel, Ort, Beschreibung)</label>
<input id="q" name="q" class="input" value="<?= htmlspecialchars($q, ENT_QUOTES) ?>" placeholder="z. B. Berlin oder Spielplatz">
</div>
<div class="stack gap-6">
<label class="label" for="loc">Ort oder PLZ (optional)</label>
<input id="loc" name="loc" class="input" value="<?= htmlspecialchars($loc, ENT_QUOTES) ?>" placeholder="z. B. 10437 oder Berlin">
<input type="hidden" name="lat" value="<?= $lat !== null ? htmlspecialchars((string)$lat, ENT_QUOTES) : '' ?>">
<input type="hidden" name="lng" value="<?= $lng !== null ? htmlspecialchars((string)$lng, ENT_QUOTES) : '' ?>">
</div>
<div class="stack gap-6">
<label class="label" for="radius">Umkreis (km)</label>
<select id="radius" name="radius" class="select">
<?php foreach ([1,2,5,10,15,25] as $r): ?>
<option value="<?= $r ?>" <?= ((float)$radius === (float)$r) ? 'selected' : '' ?>><?= $r ?></option>
<?php endforeach; ?>
</select>
</div>
<button class="btn" type="submit">Suchen</button>
</form>
<?php if ($q === ''): ?>
<p class="muted">Bitte gib einen Suchbegriff ein.</p>
<?php if ($q === '' && $loc === '' && $lat === null && $lng === null): ?>
<p class="muted">Bitte gib einen Suchbegriff oder einen Ort ein.</p>
<?php else: ?>
<h3 style="margin-top: 16px;"><?= count($results) ?> Ergebnis(se) für „<?= htmlspecialchars($q, ENT_QUOTES) ?></h3>
<h3 style="margin-top: 16px;"><?= count($results) ?> Ergebnis(se)<?= $q !== '' ? ' für „' . htmlspecialchars($q, ENT_QUOTES) . '“' : '' ?></h3>
<?php if (!$results): ?>
<p class="muted">Keine passenden Events gefunden.</p>
<?php else: ?>
@@ -39,6 +83,9 @@ if ($q !== '' && $pdo) {
<span>📍 <?= htmlspecialchars($ev['region'] ?: $ev['city'], ENT_QUOTES) ?></span>
<span><?= $ev['visibility'] === 'public' ? 'Öffentlich' : 'Mitglieder' ?></span>
<span class="badge"><?= ((int)$ev['allow_kids'] === 1) ? 'Mit Kindern' : 'Ohne Kinder' ?></span>
<?php if (isset($ev['distance_km'])): ?>
<span class="badge"><?= number_format((float)$ev['distance_km'], 1) ?> km</span>
<?php endif; ?>
</div>
<h3><?= htmlspecialchars($ev['title'], ENT_QUOTES) ?></h3>
<p class="muted"><?= htmlspecialchars($ev['teaser_public'], ENT_QUOTES) ?></p>