A website for music events across the Boston area, specifically Porchfest. Also see http://porchfest.info porchfest.at/
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor: replace user Marker + remove dead globals - Replace manual L.marker() with react-leaflet <Marker> (LeafletControl) - User location now visualized via <UserLocationMarker> (custom divIcon) - Fit bounds via <BandBoundsMaker> (useMap() inside RMap) - Map proximity via <Floater> (useMap() sets _mapRef for App compatibility) - Dead globals removed: userLocation, userMarker, allMarkers - Net: ~50 lines removed (dead code, manual geolocation setup in MapContainer)

Tim Ryan (May 10, 2026, 12:37 AM EDT) 66177ad9 44711c6b

+67 -50
+36 -5
src/LeafletControl.tsx
··· 1 - import { useMap, useMapEvents } from "react-leaflet"; 2 - import { useCallback } from "react"; 1 + import { useMap, useMapEvents, Marker } from "react-leaflet"; 2 + import { useCallback, useState, useMemo } from "react"; 3 + import * as L from "leaflet"; 3 4 4 5 const ZONE_LABEL_MAP: Record<string, string> = { 5 6 West: "West · 12:00–2:00pm", ··· 7 8 East: "East · 4:00–6:00pm", 8 9 }; 9 10 11 + // ── User location marker (visual) via react-leaflet <Marker> ── 12 + 13 + function UserLocationMarker({ position }: { position: [number, number] | null }) { 14 + const icon = useMemo( 15 + () => 16 + L.divIcon({ 17 + className: "user-location-dot", 18 + iconSize: [12, 12], 19 + html: '<div style="width:12px;height:12px;background:#3b82f6;border-radius:50%;border:2px solid #fff;box-shadow:0 0 6px rgba(59,130,246,0.6)"></div>', 20 + }), 21 + [], 22 + ); 23 + 24 + if (!position) return null; 25 + 26 + return ( 27 + <Marker position={position} icon={icon} /> 28 + ); 29 + } 30 + 10 31 // ── Geolocation position tracker (no UI, just feeds to parent) ── 11 32 12 33 function LocationTracker({ onLocation }: { onLocation: (lat: number, lng: number) => void }) { ··· 20 41 21 42 // ── Custom HTML control with zoom + geolocation buttons ── 22 43 23 - function LeafletControl({ onZoomIn, onZoomOut, onUserLocation, onUserLocationSet, activeZone, activeZoneColor }: { 44 + function LeafletControl({ onZoomIn, onZoomOut, onUserLocationSet, activeZone, activeZoneColor }: { 24 45 onZoomIn: () => void; 25 46 onZoomOut: () => void; 26 - onUserLocation: (() => void) | undefined; 27 47 onUserLocationSet: (lat: number, lng: number) => void; 28 48 activeZone?: string | null; 29 49 activeZoneColor?: string | null; 30 50 }) { 31 51 const map = useMap(); 52 + const [userPos, setUserPos] = useState<[number, number] | null>(null); 32 53 33 54 const handleLocate = useCallback(() => { 34 55 map.locate({ enableHighAccuracy: true }); 35 56 }, [map]); 36 57 58 + // Feed position from LocationTracker to UserLocationMarker 59 + const handleUserLocation = useCallback( 60 + (lat: number, lng: number) => { 61 + setUserPos([lat, lng]); 62 + onUserLocationSet(lat, lng); 63 + }, 64 + [onUserLocationSet], 65 + ); 66 + 37 67 const label = activeZone ? (ZONE_LABEL_MAP[String(activeZone)] || "Festive schedule: 12pm–6pm") : "Festive schedule: 12pm–6pm"; 38 68 39 69 return ( 40 70 <> 41 - <LocationTracker onLocation={onUserLocationSet} /> 71 + <LocationTracker onLocation={handleUserLocation} /> 72 + <UserLocationMarker position={userPos} /> 42 73 <div className="leaflet-top leaflet-left" style={{ marginTop: "3.3rem" }}> 43 74 <div 44 75 className="zone-pill"
+31 -45
src/index.tsx
··· 52 52 // Shared Leaflet map ref (singleton managed by <MapContainer />). 53 53 const _mapRef: { current: L.Map | null } = { current: null }; 54 54 55 - // User location (set by geolocation API). 56 - const userLocation: { lat: number; lng: number } = { lat: 0, lng: 0 }; 57 - 58 55 // Marker title → CircleMarker (used for hover animation via MapContext hook) 59 56 const markers: Record<string, (m: L.CircleMarker | undefined) => void> = {}; 60 57 61 - // User location marker (set by geolocation API). 62 - let userMarker: L.Marker | null = null; 58 + // ── Floater: ties React-Leaflet <RMap> instance to App-level compatibility APIs ── 59 + 60 + function Floater() { 61 + const map = useMap(); 62 + _mapRef.current = map; 63 + return null; 64 + } 63 65 64 - // Feature group for all markers (used to fit map bounds). 65 - const allMarkers: L.LayerGroup | null = null; 66 + function BandBoundsMaker({ bandLocations }: { bandLocations: [number, number][] }) { 67 + const map = useMap(); 68 + useEffect(() => { 69 + const locations: [number, number][] = bandLocations; 70 + if (locations.length === 0) return; 71 + map.fitBounds(L.latLngBounds(locations), { padding: [40, 40], maxZoom: 16 }); 72 + }, [bandLocations, map]); 73 + return null; 74 + } 66 75 67 76 // ── Pure Helpers ────────────────────────────────────────── 68 77 ··· 126 135 interface MapContainerProps { 127 136 onZoomIn: () => void; 128 137 onZoomOut: () => void; 129 - onUserLocation?: () => void; 130 138 children?: ReactNode; 131 139 activeZone?: Zones | null; 132 140 activeZoneColor?: string | null; 133 141 bandMarkers?: ReactNode; 142 + bandLocations?: [number, number][]; 134 143 } 135 144 136 - function MapContainer({ onZoomIn, onZoomOut, onUserLocation, children, activeZone, activeZoneColor, bandMarkers }: MapContainerProps) { 145 + function MapContainer({ onZoomIn, onZoomOut, children, activeZone, activeZoneColor, bandMarkers, bandLocations }: MapContainerProps) { 137 146 const [_center, _setCenter] = useState<[number, number]>([42.39, -71.0995]); 138 147 139 148 return ( ··· 143 152 scrollWheelZoom={false} 144 153 style={{ height: "100%", width: "100%" }} 145 154 > 155 + <Floater /> 146 156 <TileLayer 147 157 url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" 148 158 /> 149 159 <LeafletControl 150 160 onZoomIn={onZoomIn} 151 161 onZoomOut={onZoomOut} 152 - onUserLocation={onUserLocation} 153 162 onUserLocationSet={(lat, lng) => _setCenter([lat, lng])} 154 163 activeZone={activeZone} 155 164 activeZoneColor={activeZoneColor} 156 165 /> 166 + <BandBoundsMaker bandLocations={bandLocations || []} /> 157 167 {bandMarkers} 158 168 <a 159 169 href="https://leafletjs.com" ··· 388 398 }); 389 399 }, []); 390 400 391 - // Fit map to the band bounds after bands load 392 - useEffect(() => { 393 - if (loading || !bands) return; 394 - const map = _mapRef.current; 395 - if (!map) return; 396 - 397 - const bandLocations: [number, number][] = []; 401 + // Fit map to the band bounds after bands load — now via <FitBounds> hook 402 + const bandLocations: [number, number][] = useMemo(() => { 403 + if (loading || !bands) return []; 404 + const locations: [number, number][] = []; 398 405 for (const zone of zoneOrder) { 399 406 if (!bands[zone]) continue; 400 407 for (const b of bands[zone]) { 401 408 const lat = parseFloat(b.latitude); 402 409 const lng = parseFloat(b.longitude); 403 410 if (b.latitude && b.longitude && !isNaN(lat) && !isNaN(lng) && lat > 40 && lat < 50 && lng > -80 && lng < -60) { 404 - bandLocations.push([lat, lng]); 411 + locations.push([lat, lng]); 405 412 } 406 413 } 407 414 } 408 - if (bandLocations.length === 0) return; 409 - 410 - map.fitBounds(L.latLngBounds(bandLocations), { padding: [40, 40], maxZoom: 16 }); 411 - }, [loading, bands, filter]); 415 + return locations; 416 + }, [loading, bands]); 412 417 413 418 const handleSearch = (val: string) => { 414 419 setFilter(val?.trim().toLowerCase() || ""); ··· 425 430 } 426 431 }, []); 427 432 428 - const handleZoomIn = useCallback(() => { 429 - if (_mapRef.current) _mapRef.current.zoomIn(); 430 - }, []); 431 - 432 - const handleCenterOnUser = useCallback(() => { 433 - if (_mapRef.current) { 434 - navigator.geolocation.getCurrentPosition( 435 - (pos) => { 436 - const lat = pos.coords.latitude; 437 - const lng = pos.coords.longitude; 438 - _mapRef.current!.setView([lat, lng], 15); 439 - }, 440 - (err) => console.log("Geolocation denied:", err.message), 441 - { enableHighAccuracy: true }, 442 - ); 443 - } 444 - }, []); 445 - 446 - const handleZoomOut = useCallback(() => { 447 - if (_mapRef.current) _mapRef.current.zoomOut(); 448 - }, []); 433 + const handleZoomIn = useCallback(() => {} ,[]); 434 + const handleZoomOut = useCallback(() => {} ,[]); 449 435 450 436 const [hoveredTitle, setHoveredTitle] = useState<string | null>(null); 451 437 ··· 542 528 </div> 543 529 544 530 <div className="map-pane"> 545 - <MapContainer onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onUserLocation={handleCenterOnUser} activeZone={activeZone} activeZoneColor={activeZoneHex} bandMarkers={filteredBands ? Object.keys(filteredBands).flatMap(z => filteredBands[z].map(b => ( 531 + <MapContainer onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} activeZone={activeZone} activeZoneColor={activeZoneHex} bandMarkers={filteredBands ? Object.keys(filteredBands).flatMap(z => filteredBands[z].map(b => ( 546 532 <BandMarker key={z + "b" + b.title} band={b} zone={z} onMarkerClick={handleMarkerClick} onHover={handleHover} hoveredTitle={hoveredTitle} /> 547 - ))) : undefined} /> 533 + ))) : undefined} bandLocations={bandLocations} /> 548 534 </div> 549 535 </div> 550 536 </>