added frontend and made it usable

This commit is contained in:
Hendrik Behme committed 2026-08-15 14:50:42 +02:00
1 parent 96408f00d8
commit b9b15152bc
50 files changed
+7974 -1

No files matched your search

@@ -0,0 +1,52 @@
<?php
namespace App\Controller;
use App\Repository\CategoryRepository;
use App\Repository\EventRepository;
use App\Service\EventMapSerializer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class EventController extends AbstractController
{
private const PER_PAGE = 24;
#[Route('/events', name: 'event_list')]
public function list(Request $request, EventRepository $events, CategoryRepository $categories): Response
{
$page = max(1, $request->query->getInt('page', 1));
$category = $request->query->get('category') ?: null;
$offset = (($page ?: 1) - 1) * self::PER_PAGE;
$items = $events->findPage(self::PER_PAGE, $offset, $category);
$total = $events->countPage($category);
return $this->render('events/list.html.twig', [
'activeNav' => 'events',
'events' => $items,
'categories' => $categories->findUsedNames(),
'activeCategory' => $category,
'page' => $page,
'totalPages' => max(1, (int) ceil($total / self::PER_PAGE)),
'total' => $total,
]);
}
#[Route('/events/{id}', name: 'event_show', requirements: ['id' => '\d+'])]
public function show(int $id, EventRepository $events, EventMapSerializer $mapSerializer): Response
{
$event = $events->findOneNotDeleted($id);
if (!$event) {
throw $this->createNotFoundException('Event nicht gefunden.');
}
return $this->render('events/show.html.twig', [
'activeNav' => 'events',
'event' => $event,
'mapEventJson' => $event->hasCoordinates() ? $mapSerializer->toJson([$event]) : '[]',
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Controller;
use App\Repository\CategoryRepository;
use App\Repository\EventRepository;
use App\Service\DefaultLocation;
use App\Service\EventMapSerializer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class HomeController extends AbstractController
{
// Matches /map's MAX_RANGE_KM/POOL_LIMIT: the light map only shows the
// nearest 20, but the candidate pool it picks from needs to actually
// cover that area, not just be an arbitrary unordered chunk of events.
private const POOL_RADIUS_KM = 100;
private const POOL_LIMIT = 3000;
#[Route('/', name: 'home')]
public function index(
EventRepository $events,
CategoryRepository $categories,
EventMapSerializer $mapSerializer
): Response {
// Same fallback as /map: default to Köln until real geolocation
// (checked client-side in map.js) says otherwise.
$lat = DefaultLocation::LAT;
$lon = DefaultLocation::LON;
$pool = $events->findWithinRadius($lat, $lon, self::POOL_RADIUS_KM, self::POOL_LIMIT);
return $this->render('home.html.twig', [
'activeNav' => 'home',
'upcomingEvents' => $events->findRecent(6),
'mapEventsJson' => $mapSerializer->toJson($pool),
'popularCategories' => $categories->findMostUsedNames(12),
'centerLat' => $lat,
'centerLon' => $lon,
]);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Controller;
use App\Repository\CategoryRepository;
use App\Repository\EventRepository;
use App\Service\DefaultLocation;
use App\Service\EventMapSerializer;
use App\Service\RangeDefaults;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class MapController extends AbstractController
{
// Safety cap on the pool shipped to the client, not a practical limit:
// even the densest 100km radius seen (NRW around Köln) comes in under
// 3000, so this should never actually truncate in normal use.
private const POOL_LIMIT = 3000;
#[Route('/map', name: 'map')]
public function index(
Request $request,
EventRepository $events,
CategoryRepository $categories,
EventMapSerializer $mapSerializer
): Response {
$lat = (float) ($request->query->get('lat') ?: DefaultLocation::LAT);
$lon = (float) ($request->query->get('lon') ?: DefaultLocation::LON);
$rangeKm = (int) $request->query->get('range', RangeDefaults::DEFAULT_KM);
if ($rangeKm <= 0 || $rangeKm > RangeDefaults::MAX_KM) {
$rangeKm = RangeDefaults::DEFAULT_KM;
}
// Ship the pool sized to the *max* selectable range so the range
// dropdown and small map clicks/drags can be handled entirely
// client-side, without another page load.
$pool = $events->findWithinRadius($lat, $lon, RangeDefaults::MAX_KM, self::POOL_LIMIT);
return $this->render('map.html.twig', [
'activeNav' => 'map',
'mapEventsJson' => $mapSerializer->toJson($pool),
'categories' => $categories->findUsedNames(),
'centerLat' => $lat,
'centerLon' => $lon,
'rangeKm' => $rangeKm,
'rangeOptions' => RangeDefaults::OPTIONS_KM,
'maxRangeKm' => RangeDefaults::MAX_KM,
]);
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Controller;
use App\Repository\CategoryRepository;
use App\Repository\EventRepository;
use App\Service\RangeDefaults;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SearchController extends AbstractController
{
private const PER_PAGE = 24;
#[Route('/search', name: 'search')]
public function index(Request $request, EventRepository $events, CategoryRepository $categories): Response
{
$q = trim((string) $request->query->get('q', ''));
$category = $request->query->get('category') ?: null;
$when = $request->query->get('when') ?: 'any';
$page = max(1, $request->query->getInt('page', 1));
// Location filtering is opt-in — plain text/category search behaves
// exactly as before unless a place/"use my location" actually set
// lat/lon, unlike /map which always has *some* center.
$latParam = $request->query->get('lat');
$lonParam = $request->query->get('lon');
$hasLocation = null !== $latParam && null !== $lonParam && '' !== $latParam && '' !== $lonParam;
$lat = $hasLocation ? (float) $latParam : null;
$lon = $hasLocation ? (float) $lonParam : null;
$rangeKm = (int) $request->query->get('range', RangeDefaults::DEFAULT_KM);
if ($rangeKm <= 0 || $rangeKm > RangeDefaults::MAX_KM) {
$rangeKm = RangeDefaults::DEFAULT_KM;
}
$distances = [];
$locationIds = null;
if ($hasLocation) {
$distances = $events->distancesWithinRadius($lat, $lon, $rangeKm, 3000);
$locationIds = array_keys($distances);
}
$offset = ($page - 1) * self::PER_PAGE;
$results = $events->search($q ?: null, $category, $when, self::PER_PAGE, $offset, $locationIds);
$total = $events->countSearch($q ?: null, $category, $when, $locationIds);
return $this->render('search.html.twig', [
'activeNav' => 'search',
'q' => $q,
'events' => $results,
'categories' => $categories->findUsedNames(),
'activeCategory' => $category,
'when' => $when,
'hasLocation' => $hasLocation,
'lat' => $lat,
'lon' => $lon,
'rangeKm' => $rangeKm,
'rangeOptions' => RangeDefaults::OPTIONS_KM,
'distances' => $distances,
'page' => $page,
'totalPages' => max(1, (int) ceil($total / self::PER_PAGE)),
'total' => $total,
]);
}
}