1- import { useEffect , useMemo , useRef , useState } from 'react' ;
1+ import { useEffect , useMemo , useState } from 'react' ;
22import { Link , useParams } from 'react-router-dom' ;
33import {
44 CartesianGrid ,
@@ -20,7 +20,9 @@ import {
2020 formatPaceMinKm ,
2121 formatSpeedKmh
2222} from '@/lib/format' ;
23- import { US_DEFAULT_CENTER , US_DEFAULT_ZOOM , getMapStyle } from '@/lib/mapStyles' ;
23+ import { US_DEFAULT_CENTER , US_DEFAULT_ZOOM } from '@/lib/mapStyles' ;
24+ import { useManagedMapLibre } from '@/lib/useManagedMapLibre' ;
25+ import { MaximizableMapFrame } from '@/components/MaximizableMapFrame' ;
2426import { MetricCard } from '@/components/MetricCard' ;
2527import type { ActivityDetail , TrackPoint } from '@/types' ;
2628
@@ -82,36 +84,20 @@ function fitMapToTrack(map: maplibregl.Map, track: TrackPoint[]) {
8284 } ) ;
8385}
8486
85- function ActivityRouteMap ( { track } : { track : TrackPoint [ ] } ) {
86- const containerRef = useRef < HTMLDivElement | null > ( null ) ;
87- const mapRef = useRef < maplibregl . Map | null > ( null ) ;
87+ function ActivityRouteMap ( {
88+ track,
89+ reducedComplexity
90+ } : {
91+ track : TrackPoint [ ] ;
92+ reducedComplexity : boolean ;
93+ } ) {
94+ const { containerRef, mapRef } = useManagedMapLibre ( {
95+ reducedComplexity,
96+ initialCenter : US_DEFAULT_CENTER ,
97+ initialZoom : US_DEFAULT_ZOOM
98+ } ) ;
8899 const trackSource = useMemo ( ( ) => toRouteFeatureCollection ( track ) , [ track ] ) ;
89100
90- useEffect ( ( ) => {
91- if ( ! containerRef . current ) {
92- return undefined ;
93- }
94-
95- const map = new maplibregl . Map ( {
96- container : containerRef . current ,
97- style : getMapStyle ( false ) ,
98- center : US_DEFAULT_CENTER ,
99- zoom : US_DEFAULT_ZOOM ,
100- pitchWithRotate : false ,
101- dragRotate : false
102- } ) ;
103- mapRef . current = map ;
104-
105- map . addControl ( new maplibregl . NavigationControl ( { showCompass : false } ) , 'top-right' ) ;
106- map . scrollZoom . setWheelZoomRate ( 1 / 520 ) ;
107- map . scrollZoom . setZoomRate ( 1 / 130 ) ;
108-
109- return ( ) => {
110- map . remove ( ) ;
111- mapRef . current = null ;
112- } ;
113- } , [ ] ) ;
114-
115101 useEffect ( ( ) => {
116102 const map = mapRef . current ;
117103 if ( ! map ) {
@@ -145,7 +131,6 @@ function ActivityRouteMap({ track }: { track: TrackPoint[] }) {
145131 } ) ;
146132 }
147133
148- fitMapToTrack ( map , track ) ;
149134 } ;
150135
151136 if ( map . isStyleLoaded ( ) ) {
@@ -157,16 +142,68 @@ function ActivityRouteMap({ track }: { track: TrackPoint[] }) {
157142 return ( ) => {
158143 map . off ( 'load' , syncTrack ) ;
159144 } ;
160- } , [ track , trackSource ] ) ;
145+ } , [ track , trackSource , reducedComplexity ] ) ;
146+
147+ useEffect ( ( ) => {
148+ const map = mapRef . current ;
149+ if ( ! map ) {
150+ return undefined ;
151+ }
152+
153+ const fitTrack = ( ) => {
154+ fitMapToTrack ( map , track ) ;
155+ } ;
156+
157+ if ( map . isStyleLoaded ( ) ) {
158+ fitTrack ( ) ;
159+ return undefined ;
160+ }
161+
162+ map . once ( 'load' , fitTrack ) ;
163+ return ( ) => {
164+ map . off ( 'load' , fitTrack ) ;
165+ } ;
166+ } , [ track ] ) ;
161167
162168 return < div ref = { containerRef } className = "h-full w-full" /> ;
163169}
164170
171+ function ReducedComplexityMapToggle ( {
172+ enabled,
173+ onChange
174+ } : {
175+ enabled : boolean ;
176+ onChange : ( enabled : boolean ) => void ;
177+ } ) {
178+ return (
179+ < button
180+ type = "button"
181+ onClick = { ( ) => onChange ( ! enabled ) }
182+ aria-pressed = { enabled }
183+ className = { `flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs shadow-sm backdrop-blur transition-colors ${
184+ enabled
185+ ? 'border-accent/60 bg-panel/90 text-foreground'
186+ : 'border-border bg-panel/80 text-muted hover:text-foreground'
187+ } `}
188+ >
189+ < span
190+ className = { `inline-flex h-3.5 w-3.5 items-center justify-center rounded-sm border text-[10px] leading-none ${
191+ enabled ? 'border-accent bg-accent text-white' : 'border-border bg-bg/90 text-transparent'
192+ } `}
193+ >
194+ ✓
195+ </ span >
196+ Reduced complexity
197+ </ button >
198+ ) ;
199+ }
200+
165201export function ActivityDetailPage ( ) {
166202 const { id } = useParams < { id : string } > ( ) ;
167203 const [ detail , setDetail ] = useState < ActivityDetail | null > ( null ) ;
168204 const [ loading , setLoading ] = useState ( true ) ;
169205 const [ error , setError ] = useState < string | null > ( null ) ;
206+ const [ reducedMapComplexity , setReducedMapComplexity ] = useState ( false ) ;
170207
171208 useEffect ( ( ) => {
172209 if ( ! id ) {
@@ -270,15 +307,26 @@ export function ActivityDetailPage() {
270307 < div className = "border-b border-border px-4 py-3" >
271308 < h3 className = "text-lg font-semibold text-foreground" > Route</ h3 >
272309 </ div >
273- < div className = "h-80" >
310+ < MaximizableMapFrame
311+ label = "route map"
312+ collapsedHeightClassName = "h-80"
313+ topLeftActions = {
314+ detail . track . length > 0 ? (
315+ < ReducedComplexityMapToggle
316+ enabled = { reducedMapComplexity }
317+ onChange = { setReducedMapComplexity }
318+ />
319+ ) : null
320+ }
321+ >
274322 { detail . track . length === 0 ? (
275323 < div className = "flex h-full items-center justify-center text-sm text-muted" >
276324 No GPS track available
277325 </ div >
278326 ) : (
279- < ActivityRouteMap track = { detail . track } />
327+ < ActivityRouteMap track = { detail . track } reducedComplexity = { reducedMapComplexity } />
280328 ) }
281- </ div >
329+ </ MaximizableMapFrame >
282330 </ section >
283331
284332 < div className = "grid gap-6 xl:grid-cols-3" >
0 commit comments