// Wander — Map (discovery) screen function MapScreen({ onOpenTour, onTab, dense, brand }) { const { TOURS, NEARBY } = window.WANDER_DATA; const [filter, setFilter] = React.useState('all'); const [selected, setSelected] = React.useState(null); const sel = NEARBY.find(n => n.id === selected); const visible = filter==='all' ? NEARBY : NEARBY.filter(n => n.kind===filter); // ── Live location (real GPS, or a simulated marina stroll off-site) ────────── const geoApi = window.WanderGeo; const [geo, setGeo] = React.useState(() => (geoApi ? geoApi.get() : null)); const marinaRoute = React.useMemo( () => NEARBY.filter(p => p.lat != null).map(p => ({ lat: p.lat, lng: p.lng })), []); React.useEffect(() => { if (!geoApi) return; geoApi.ensure({ route: marinaRoute, loop: true, speed: 16 }); return geoApi.subscribe(setGeo); }, []); const project = geo ? geo.project : null; const userCoords = geo && geo.coords ? geo.coords : null; const userPos = userCoords && project ? project(userCoords.lat, userCoords.lng) : null; const located = !!userCoords; // ── Pan / zoom: the map follows the user's live position when zoomed in ────── const [zoom, setZoom] = React.useState(1); const ZMIN = 1, ZMAX = 3.5; const setZ = (z) => setZoom(Math.max(ZMIN, Math.min(ZMAX, +z.toFixed(2)))); // Snap to a comfortable, user-centred zoom the first time we get a fix. const didCenter = React.useRef(false); React.useEffect(() => { if (located && !didCenter.current) { didCenter.current = true; setZoom(1.9); } }, [located]); const recenter = () => { if (geoApi) geoApi.ensure({ route: marinaRoute, loop: true, speed: 16 }); setZ(Math.max(zoom, 1.9)); }; // ── Filtering: applies to BOTH the map pins and the tour list below ────────── const matchesTour = (t) => { if (filter === 'free') return t.tier === 'free'; if (filter === 'pro') return t.tier === 'pro'; return true; // 'all' and 'current' (Active) show every tour }; let shownTours = TOURS.filter(matchesTour); if (located) { shownTours = [...shownTours].sort((a, b) => (tourDistance(geo, userCoords, a.id) ?? 1e9) - (tourDistance(geo, userCoords, b.id) ?? 1e9)); } return (
Now exploring
Marina Bay, Singapore
{[{id:'all',label:'All'},{id:'free',label:'Free'},{id:'pro',label:'Pro'},{id:'current',label:'Active'}].map(f => ( ))}
{/* Zoom controls */}
{geo && geo.mode==='sim' && (
🧭 Demo location
)} {sel ? setSelected(null)} onOpen={onOpenTour} geo={geo} userCoords={userCoords}/> : }
); } function IllustratedMap({ points, selected, onSelect, project, userPos, mode, zoom = 1 }) { const pinPos = (p) => (p.lat != null && project) ? project(p.lat, p.lng) : { x: p.x, y: p.y }; // Pan so the user's position stays centred as you zoom in, clamped so the // map art always fills the viewport (no empty gaps at the edges). const cx = userPos ? userPos.x : 50; const cy = userPos ? userPos.y : 50; const clamp = (v, a, b) => Math.max(a, Math.min(b, v)); const span = (zoom - 1) * 100; const tx = clamp(50 - cx * zoom, -span, 0); const ty = clamp(50 - cy * zoom, -span, 0); return (
Downtown Core
Marina South
Singapore Strait
{/* Live "you are here" dot — projected from real or simulated coordinates */} {userPos && (
)} {points.map(p => { const pos = pinPos(p); return ( ); })}
); } function PinShape({ kind, emoji }) { const bg = kind==='free'?'var(--w-mint)':kind==='pro'?'var(--w-ink)':kind==='current'?'var(--w-coral)':'var(--w-paper)'; const ring = kind==='current'?'0 0 0 3px rgba(255,107,90,0.3),0 6px 14px rgba(0,0,0,0.18)':'0 6px 14px rgba(0,0,0,0.18)'; return (
{emoji}
{kind==='current' &&
}
); } // Distance (m) from the user to a tour, measured to its first stop. function tourDistance(geo, userCoords, tourId) { if (!geo || !userCoords) return null; const stops = window.WANDER_DATA.stopsForTour(tourId) || []; const first = stops.find(s => s.lat != null); if (!first) return null; return geo.distance(userCoords, { lat: first.lat, lng: first.lng }); } function NearbySheet({ tours, onOpen, dense, geo, userCoords, filter }) { const located = !!userCoords; const heading = { all:'Nearby tours', free:'Free tours', pro:'Pro tours', current:'Active near you' }[filter] || 'Nearby tours'; return (
{located?'From you':(filter&&filter!=='all'?filter:'Nearby')} {heading}
{tours.length === 0 && (
No {filter} tours in this area.
)}
{tours.map(t => { const d = tourDistance(geo, userCoords, t.id); return ( ); })}
); } function SelectedCard({ point, tours, onClose, onOpen, geo, userCoords }) { const matchTour = tours[0]; const d = (geo && userCoords && point.lat != null) ? geo.distance(userCoords, { lat: point.lat, lng: point.lng }) : null; const distLabel = d != null ? `${geo.formatDistance(d)} away · ${geo.walkMins(d)} min walk` : '180 m away · 3 min walk'; return (
{point.emoji}
{point.name}
{distLabel}
); } Object.assign(window, { MapScreen });