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,85 @@
|
||||
/*
|
||||
* Small single-purpose Leaflet maps:
|
||||
* - #escaperoom-detail-map : read-only pin on a room's detail page
|
||||
* - #escaperoom-suggest-map : click to drop / move a pin on the "suggest a room"
|
||||
* form, writing the coordinates into the hidden latitude/longitude fields.
|
||||
*/
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
import iconUrl from 'leaflet/dist/images/marker-icon.png';
|
||||
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
|
||||
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
|
||||
|
||||
delete L.Icon.Default.prototype._getIconUrl;
|
||||
L.Icon.Default.mergeOptions({ iconUrl, iconRetinaUrl, shadowUrl });
|
||||
|
||||
const OSM = {
|
||||
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
opts: {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
},
|
||||
};
|
||||
|
||||
function initDetailMap(el) {
|
||||
const lat = parseFloat(el.dataset.lat);
|
||||
const lng = parseFloat(el.dataset.lng);
|
||||
if (Number.isNaN(lat) || Number.isNaN(lng)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const map = L.map(el, { scrollWheelZoom: false }).setView([lat, lng], 15);
|
||||
L.tileLayer(OSM.url, OSM.opts).addTo(map);
|
||||
L.marker([lat, lng]).addTo(map);
|
||||
}
|
||||
|
||||
function initSuggestMap(el) {
|
||||
const form = el.closest('form');
|
||||
const latField = form && form.querySelector('input[name$="[latitude]"]');
|
||||
const lngField = form && form.querySelector('input[name$="[longitude]"]');
|
||||
if (!latField || !lngField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startLat = parseFloat(latField.value);
|
||||
const startLng = parseFloat(lngField.value);
|
||||
const hasStart = !Number.isNaN(startLat) && !Number.isNaN(startLng);
|
||||
|
||||
const map = L.map(el).setView(hasStart ? [startLat, startLng] : [20, 0], hasStart ? 15 : 2);
|
||||
L.tileLayer(OSM.url, OSM.opts).addTo(map);
|
||||
|
||||
let marker = hasStart ? L.marker([startLat, startLng], { draggable: true }).addTo(map) : null;
|
||||
|
||||
const write = (latlng) => {
|
||||
latField.value = latlng.lat.toFixed(7);
|
||||
lngField.value = latlng.lng.toFixed(7);
|
||||
latField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
};
|
||||
|
||||
if (marker) {
|
||||
marker.on('dragend', () => write(marker.getLatLng()));
|
||||
}
|
||||
|
||||
map.on('click', (e) => {
|
||||
if (marker) {
|
||||
marker.setLatLng(e.latlng);
|
||||
} else {
|
||||
marker = L.marker(e.latlng, { draggable: true }).addTo(map);
|
||||
marker.on('dragend', () => write(marker.getLatLng()));
|
||||
}
|
||||
write(e.latlng);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const detail = document.getElementById('escaperoom-detail-map');
|
||||
if (detail) {
|
||||
initDetailMap(detail);
|
||||
}
|
||||
|
||||
const suggest = document.getElementById('escaperoom-suggest-map');
|
||||
if (suggest) {
|
||||
initSuggestMap(suggest);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user