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,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Website\Controller;
|
||||
|
||||
use App\Tech\Entity\User;
|
||||
use App\Website\Entity\EscapeRoom;
|
||||
use App\Website\Entity\EscapeRoomReview;
|
||||
use App\Website\Enum\EscapeRoomSource;
|
||||
use App\Website\Enum\EscapeRoomStatus;
|
||||
use App\Website\Form\EscapeRoomReviewType;
|
||||
use App\Website\Form\SuggestEscapeRoomType;
|
||||
use App\Website\Repository\EscapeRoomRepository;
|
||||
use App\Website\Repository\EscapeRoomReviewRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
final class EscapeRoomController extends AbstractController
|
||||
{
|
||||
#[Route(path: '/escape-rooms', name: 'website_escaperooms', methods: ['GET'])]
|
||||
public function index(EscapeRoomRepository $rooms): Response
|
||||
{
|
||||
return $this->render('website/escape_room/index.html.twig', [
|
||||
'publishedCount' => $rooms->countByStatus(EscapeRoomStatus::PUBLISHED),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slim feed the map and the list are both built from. Cached at the edge for
|
||||
* a few minutes; the dataset only changes when the importer runs or an admin
|
||||
* edits a room.
|
||||
*/
|
||||
#[Route(path: '/escape-rooms/data.json', name: 'website_escaperooms_data', methods: ['GET'])]
|
||||
public function data(EscapeRoomRepository $rooms): JsonResponse
|
||||
{
|
||||
$response = new JsonResponse([
|
||||
'generatedAt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
|
||||
'rooms' => $rooms->findMapData(),
|
||||
]);
|
||||
$response->setPublic();
|
||||
$response->setMaxAge(600);
|
||||
$response->setSharedMaxAge(600);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route(path: '/escape-rooms/suggest', name: 'website_escaperoom_suggest', methods: ['GET', 'POST'])]
|
||||
public function suggest(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$room = new EscapeRoom();
|
||||
$form = $this->createForm(SuggestEscapeRoomType::class, $room);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$room->setSource(EscapeRoomSource::USER)
|
||||
->setStatus(EscapeRoomStatus::PENDING);
|
||||
|
||||
if ($this->getUser() instanceof User) {
|
||||
$room->setSubmittedBy($this->getUser());
|
||||
}
|
||||
|
||||
$em->persist($room);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Thanks! Your suggestion has been sent for review and will appear on the map once approved.');
|
||||
|
||||
return $this->redirectToRoute('website_escaperooms');
|
||||
}
|
||||
|
||||
return $this->render('website/escape_room/suggest.html.twig', [
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/escape-rooms/{id}-{slug}', name: 'website_escaperoom_show', methods: ['GET'], requirements: ['id' => '\d+', 'slug' => '[a-z0-9-]*'])]
|
||||
public function show(EscapeRoom $room, EscapeRoomReviewRepository $reviews): Response
|
||||
{
|
||||
$this->guardVisible($room);
|
||||
|
||||
return $this->renderRoom($room, $reviews, $this->createForm(EscapeRoomReviewType::class));
|
||||
}
|
||||
|
||||
#[Route(path: '/escape-rooms/{id}/reviews', name: 'website_escaperoom_review', methods: ['POST'], requirements: ['id' => '\d+'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function review(
|
||||
EscapeRoom $room,
|
||||
Request $request,
|
||||
EntityManagerInterface $em,
|
||||
EscapeRoomReviewRepository $reviews,
|
||||
): Response {
|
||||
$this->guardVisible($room);
|
||||
|
||||
$review = new EscapeRoomReview();
|
||||
$form = $this->createForm(EscapeRoomReviewType::class, $review);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$review->setEscapeRoom($room)->setAuthor($user);
|
||||
|
||||
$em->persist($review);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Thanks for your review!');
|
||||
|
||||
return $this->redirectToRoute('website_escaperoom_show', [
|
||||
'id' => $room->getId(),
|
||||
'slug' => $room->getSlug(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->renderRoom($room, $reviews, $form);
|
||||
}
|
||||
|
||||
private function guardVisible(EscapeRoom $room): void
|
||||
{
|
||||
if ($room->isDeleted() || !$room->getStatus()->isPubliclyVisible()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
private function renderRoom(EscapeRoom $room, EscapeRoomReviewRepository $reviews, FormInterface $reviewForm): Response
|
||||
{
|
||||
return $this->render('website/escape_room/show.html.twig', [
|
||||
'room' => $room,
|
||||
'reviews' => $reviews->findVisibleForRoom($room),
|
||||
'rating' => $reviews->ratingSummary($room),
|
||||
'reviewForm' => $reviewForm,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user