Add physical escape room map, reviews, and website admin
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>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user