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,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Website\Repository;
|
||||
|
||||
use App\Website\Entity\EscapeRoom;
|
||||
use App\Website\Entity\EscapeRoomReview;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<EscapeRoomReview>
|
||||
*/
|
||||
class EscapeRoomReviewRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, EscapeRoomReview::class);
|
||||
}
|
||||
|
||||
public function save(EscapeRoomReview $review, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($review);
|
||||
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EscapeRoomReview[]
|
||||
*/
|
||||
public function findVisibleForRoom(EscapeRoom $room): array
|
||||
{
|
||||
return $this->createQueryBuilder('rev')
|
||||
->where('rev.escapeRoom = :room')
|
||||
->andWhere('rev.deletedAt IS NULL')
|
||||
->setParameter('room', $room)
|
||||
->orderBy('rev.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{avg: ?float, count: int}
|
||||
*/
|
||||
public function ratingSummary(EscapeRoom $room): array
|
||||
{
|
||||
$row = $this->createQueryBuilder('rev')
|
||||
->select('AVG(rev.rating) AS avg, COUNT(rev.id) AS count')
|
||||
->where('rev.escapeRoom = :room')
|
||||
->andWhere('rev.deletedAt IS NULL')
|
||||
->setParameter('room', $room)
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
|
||||
$count = (int) $row['count'];
|
||||
|
||||
return [
|
||||
'avg' => $count > 0 ? round((float) $row['avg'], 1) : null,
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent reviews across all rooms, for admin moderation.
|
||||
*
|
||||
* @return EscapeRoomReview[]
|
||||
*/
|
||||
public function findRecent(int $limit = 200): array
|
||||
{
|
||||
return $this->createQueryBuilder('rev')
|
||||
->addSelect('room')
|
||||
->join('rev.escapeRoom', 'room')
|
||||
->where('rev.deletedAt IS NULL')
|
||||
->orderBy('rev.createdAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user