// 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 (
);
}
Object.assign(window, { MapScreen });