Files
Escapepage/assets/escaperoom-detail.js
T
FrankandClaude Sonnet 5 008887ab60 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>
2026-08-31 00:24:03 +02:00

86 lines
2.8 KiB
JavaScript

/*
* 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: '&copy; <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);
}
});