Public:
- /escape-rooms: Leaflet + OpenStreetMap map with marker clustering and a
searchable list, both built from a cached slim JSON feed
- /escape-rooms/{id}-{slug}: room detail with a mini-map and user reviews
(rating + title + body, login required)
- /escape-rooms/suggest: visitor submission form with click-to-drop-pin map
and reCAPTCHA v3; new rooms land as pending
Data:
- EscapeRoom / EscapeRoomReview entities (soft-delete), migration
- app:escaperooms:import-osm console command pulls leisure=escape_game from
the Overpass API, upserts by (osmType, osmId), skips locked rows;
--tiled / --area / --bbox options
Admin (new "Website Admin" area, separate from Game Admin):
- src/Website/Controller/AdminEscapeRoomController.php at /admin/escape-rooms:
status-filtered list, new/edit, publish/hide/reject, delete, review removal
- game admin sidebar extracted into a block and cross-linked
Assets: leaflet + leaflet.markercluster; escaperoom-map and escaperoom-detail
Encore entries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
153 lines
6.0 KiB
PHP
153 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Website\Controller;
|
|
|
|
use App\Website\Entity\EscapeRoom;
|
|
use App\Website\Entity\EscapeRoomReview;
|
|
use App\Website\Enum\EscapeRoomStatus;
|
|
use App\Website\Form\AdminEscapeRoomType;
|
|
use App\Website\Repository\EscapeRoomRepository;
|
|
use App\Website\Repository\EscapeRoomReviewRepository;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Annotation\Route;
|
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|
|
|
#[Route('/admin/escape-rooms')]
|
|
#[IsGranted('ROLE_ADMIN')]
|
|
final class AdminEscapeRoomController extends AbstractController
|
|
{
|
|
#[Route('', name: 'website_admin_escaperooms', methods: ['GET'])]
|
|
public function index(Request $request, EscapeRoomRepository $rooms): Response
|
|
{
|
|
$filter = EscapeRoomStatus::tryFrom((string) $request->query->get('status'));
|
|
|
|
return $this->render('website/admin/escape_room/index.html.twig', [
|
|
'rooms' => $rooms->findForAdmin($filter),
|
|
'reviewCounts' => $rooms->reviewCounts(),
|
|
'filter' => $filter,
|
|
'counts' => [
|
|
'all' => \count($rooms->findForAdmin()),
|
|
EscapeRoomStatus::PENDING->value => $rooms->countByStatus(EscapeRoomStatus::PENDING),
|
|
EscapeRoomStatus::PUBLISHED->value => $rooms->countByStatus(EscapeRoomStatus::PUBLISHED),
|
|
EscapeRoomStatus::HIDDEN->value => $rooms->countByStatus(EscapeRoomStatus::HIDDEN),
|
|
EscapeRoomStatus::REJECTED->value => $rooms->countByStatus(EscapeRoomStatus::REJECTED),
|
|
],
|
|
]);
|
|
}
|
|
|
|
#[Route('/new', name: 'website_admin_escaperoom_new', methods: ['GET', 'POST'])]
|
|
public function new(Request $request, EntityManagerInterface $em): Response
|
|
{
|
|
$room = new EscapeRoom();
|
|
$form = $this->createForm(AdminEscapeRoomType::class, $room);
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$em->persist($room);
|
|
$em->flush();
|
|
$this->addFlash('success', sprintf('"%s" added.', $room->getName()));
|
|
|
|
return $this->redirectToRoute('website_admin_escaperooms');
|
|
}
|
|
|
|
return $this->render('website/admin/escape_room/form.html.twig', [
|
|
'form' => $form,
|
|
'room' => $room,
|
|
'heading' => 'New escape room',
|
|
]);
|
|
}
|
|
|
|
#[Route('/{id}/edit', name: 'website_admin_escaperoom_edit', methods: ['GET', 'POST'], requirements: ['id' => '\d+'])]
|
|
public function edit(EscapeRoom $room, Request $request, EntityManagerInterface $em): Response
|
|
{
|
|
$form = $this->createForm(AdminEscapeRoomType::class, $room);
|
|
$form->handleRequest($request);
|
|
|
|
if ($form->isSubmitted() && $form->isValid()) {
|
|
$room->touch();
|
|
$em->flush();
|
|
$this->addFlash('success', 'Changes saved.');
|
|
|
|
return $this->redirectToRoute('website_admin_escaperooms');
|
|
}
|
|
|
|
return $this->render('website/admin/escape_room/form.html.twig', [
|
|
'form' => $form,
|
|
'room' => $room,
|
|
'heading' => 'Edit: '.$room->getName(),
|
|
]);
|
|
}
|
|
|
|
#[Route('/{id}/status/{status}', name: 'website_admin_escaperoom_moderate', methods: ['POST'], requirements: ['id' => '\d+'])]
|
|
public function moderate(EscapeRoom $room, string $status, Request $request, EntityManagerInterface $em): Response
|
|
{
|
|
if (!$this->isCsrfTokenValid('moderate_room_'.$room->getId(), (string) $request->request->get('_token'))) {
|
|
$this->addFlash('danger', 'Invalid CSRF token.');
|
|
|
|
return $this->redirectToRoute('website_admin_escaperooms');
|
|
}
|
|
|
|
$target = EscapeRoomStatus::tryFrom($status);
|
|
if ($target === null) {
|
|
throw $this->createNotFoundException();
|
|
}
|
|
|
|
$room->setStatus($target)->touch();
|
|
$em->flush();
|
|
$this->addFlash('success', sprintf('"%s" is now %s.', $room->getName(), $target->label()));
|
|
|
|
return $this->redirectToRoute('website_admin_escaperooms', ['status' => $request->request->get('return_to')]);
|
|
}
|
|
|
|
#[Route('/{id}/delete', name: 'website_admin_escaperoom_delete', methods: ['POST'], requirements: ['id' => '\d+'])]
|
|
public function delete(EscapeRoom $room, Request $request, EntityManagerInterface $em): Response
|
|
{
|
|
if (!$this->isCsrfTokenValid('delete_room_'.$room->getId(), (string) $request->request->get('_token'))) {
|
|
$this->addFlash('danger', 'Invalid CSRF token.');
|
|
|
|
return $this->redirectToRoute('website_admin_escaperooms');
|
|
}
|
|
|
|
if (!$room->isDeleted()) {
|
|
$room->setDeletedAt(new \DateTimeImmutable());
|
|
$em->flush();
|
|
}
|
|
|
|
$this->addFlash('success', sprintf('"%s" deleted.', $room->getName()));
|
|
|
|
return $this->redirectToRoute('website_admin_escaperooms');
|
|
}
|
|
|
|
#[Route('/reviews', name: 'website_admin_escaperoom_reviews', methods: ['GET'])]
|
|
public function reviews(EscapeRoomReviewRepository $reviews): Response
|
|
{
|
|
return $this->render('website/admin/review/index.html.twig', [
|
|
'reviews' => $reviews->findRecent(),
|
|
]);
|
|
}
|
|
|
|
#[Route('/reviews/{id}/delete', name: 'website_admin_escaperoom_review_delete', methods: ['POST'], requirements: ['id' => '\d+'])]
|
|
public function deleteReview(EscapeRoomReview $review, Request $request, EntityManagerInterface $em): Response
|
|
{
|
|
if (!$this->isCsrfTokenValid('delete_review_'.$review->getId(), (string) $request->request->get('_token'))) {
|
|
$this->addFlash('danger', 'Invalid CSRF token.');
|
|
|
|
return $this->redirectToRoute('website_admin_escaperoom_reviews');
|
|
}
|
|
|
|
if (!$review->isDeleted()) {
|
|
$review->setDeletedAt(new \DateTimeImmutable());
|
|
$em->flush();
|
|
}
|
|
|
|
$this->addFlash('success', 'Review removed.');
|
|
|
|
return $this->redirectToRoute('website_admin_escaperoom_reviews');
|
|
}
|
|
}
|