navi/src/components/LayerControl.jsx

452 lines
14 KiB
React
Raw Normal View History

import { useState, useEffect, useRef } from 'react'
import { Layers, Map, Satellite, Globe } from 'lucide-react'
import { hasFeature, getConfig } from '../config'
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
import { useConfig } from '../hooks/useConfig'
import { useStore } from '../store'
const STORAGE_KEY = 'navi-layer-prefs'
function loadPrefs() {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (raw) return JSON.parse(raw)
} catch {}
return null
}
function savePrefs(prefs) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
}
export default function LayerControl({ mapRef }) {
const [open, setOpen] = useState(false)
const [hillshade, setHillshade] = useState(false)
const [traffic, setTraffic] = useState(false)
const [publicLands, setPublicLands] = useState(false)
const [contours, setContours] = useState(false)
const [contoursTest, setContoursTest] = useState(false)
const [contoursTest10ft, setContoursTest10ft] = useState(false)
const [usfsTrails, setUsfsTrails] = useState(false)
const [blmTrails, setBlmTrails] = useState(false)
const panelRef = useRef(null)
// View mode: map | satellite | hybrid
const viewMode = useStore((s) => s.viewMode)
const setViewMode = useStore((s) => s.setViewMode)
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
// Auth state — Traffic tiles are auth-gated at the edge (Caddy @authed_api),
// so the toggle is only usable when authenticated. config drives re-init once
// /api/config resolves (so saved prefs hydrate against known feature flags).
const auth = useStore((s) => s.auth)
const config = useConfig()
const trafficDisabled = !auth.loaded || !auth.authenticated
// Initialize from localStorage or defaults on mount (re-runs when config loads)
useEffect(() => {
const saved = loadPrefs()
const hsAvailable = hasFeature('has_hillshade')
const trAvailable = hasFeature('has_traffic_overlay')
const plAvailable = hasFeature('has_public_lands_layer')
const ctAvailable = hasFeature('has_contours')
const ctTestAvailable = hasFeature('has_contours_test')
const ctTest10ftAvailable = hasFeature('has_contours_test_10ft')
const usfsAvailable = hasFeature('has_usfs_trails')
const blmAvailable = hasFeature('has_blm_trails')
if (saved) {
setHillshade(hsAvailable && (saved.hillshade ?? true))
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
setTraffic(trAvailable && auth.authenticated && (saved.traffic ?? false))
setPublicLands(plAvailable && (saved.publicLands ?? false))
setContours(ctAvailable && (saved.contours ?? false))
setContoursTest(ctTestAvailable && (saved.contoursTest ?? false))
setContoursTest10ft(ctTest10ftAvailable && (saved.contoursTest10ft ?? false))
setUsfsTrails(usfsAvailable && (saved.usfsTrails ?? false))
setBlmTrails(blmAvailable && (saved.blmTrails ?? false))
} else {
// Defaults: hillshade ON if available, others OFF
setHillshade(hsAvailable)
setTraffic(false)
setPublicLands(false)
setContours(false)
setContoursTest(false)
setContoursTest10ft(false)
setUsfsTrails(false)
}
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
}, [config])
// Tear down traffic when the session goes anonymous (only after auth has
// loaded, so we don't tear down during the brief pre-whoami window on reload).
// Flipping the pref off drives the apply effect below -> removeTrafficLayer.
useEffect(() => {
if (auth.loaded && !auth.authenticated && traffic) setTraffic(false)
}, [auth.loaded, auth.authenticated]) // eslint-disable-line react-hooks/exhaustive-deps
// Apply layers when prefs change
useEffect(() => {
const mapView = mapRef?.current
if (!mapView) return
const map = mapView.getMap?.()
if (!map) return
const apply = () => {
if (hillshade && hasFeature('has_hillshade')) {
mapView.addHillshadeLayer?.()
} else {
mapView.removeHillshadeLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [hillshade, mapRef])
useEffect(() => {
const mapView = mapRef?.current
if (!mapView) return
const map = mapView.getMap?.()
if (!map) return
const apply = () => {
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
if (traffic && hasFeature('has_traffic_overlay') && auth.authenticated) {
mapView.addTrafficLayer?.()
} else {
mapView.removeTrafficLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
}, [traffic, mapRef, auth.authenticated])
useEffect(() => {
const mapView = mapRef?.current
if (!mapView) return
const map = mapView.getMap?.()
if (!map) return
const apply = () => {
if (publicLands && hasFeature('has_public_lands_layer')) {
mapView.addPublicLandsLayer?.()
} else {
mapView.removePublicLandsLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [publicLands, mapRef])
useEffect(() => {
const mapView = mapRef?.current
if (!mapView) return
const map = mapView.getMap?.()
if (!map) return
const apply = () => {
if (contours && hasFeature('has_contours')) {
mapView.addContoursLayer?.()
} else {
mapView.removeContoursLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [contours, mapRef])
useEffect(() => {
const mapView = mapRef?.current
if (!mapView) return
const map = mapView.getMap?.()
if (!map) return
const apply = () => {
if (contoursTest && hasFeature('has_contours_test')) {
mapView.addContoursTestLayer?.()
} else {
mapView.removeContoursTestLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [contoursTest, mapRef])
// Apply contoursTest10ft layer
useEffect(() => {
const map = mapRef.current?.getMap?.()
if (!map) return
const apply = () => {
if (contoursTest10ft && hasFeature('has_contours_test_10ft')) {
mapRef.current?.addContoursTest10ftLayer?.()
} else {
mapRef.current?.removeContoursTest10ftLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
}, [contoursTest10ft, mapRef])
// Apply usfsTrails layer
useEffect(() => {
const map = mapRef.current?.getMap?.()
if (!map) return
const apply = () => {
if (usfsTrails && hasFeature('has_usfs_trails')) {
mapRef.current?.addUsfsTrailsLayer?.()
} else {
mapRef.current?.removeUsfsTrailsLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
}, [usfsTrails, mapRef])
// Apply blmTrails layer
useEffect(() => {
const map = mapRef.current?.getMap?.()
if (!map) return
const apply = () => {
if (blmTrails && hasFeature("has_blm_trails")) {
mapRef.current?.addBlmTrailsLayer?.()
} else {
mapRef.current?.removeBlmTrailsLayer?.()
}
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once("style.load", apply)
}
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
}, [blmTrails, mapRef])
// Apply view mode changes
useEffect(() => {
const mapView = mapRef?.current
if (!mapView) return
const map = mapView.getMap?.()
if (!map) return
const apply = () => {
mapView.setViewMode?.(viewMode)
}
if (map.isStyleLoaded()) {
apply()
} else {
map.once('style.load', apply)
}
return () => map.off('style.load', apply)
}, [viewMode, mapRef])
// Close on outside click
useEffect(() => {
if (!open) return
function handleClick(e) {
if (panelRef.current && !panelRef.current.contains(e.target)) {
setOpen(false)
}
}
document.addEventListener('pointerdown', handleClick)
return () => document.removeEventListener('pointerdown', handleClick)
}, [open])
const showHillshade = hasFeature('has_hillshade')
const showTraffic = hasFeature('has_traffic_overlay')
const showPublicLands = hasFeature('has_public_lands_layer')
const showContours = hasFeature('has_contours')
const showContoursTest = hasFeature('has_contours_test')
const showContoursTest10ft = hasFeature('has_contours_test_10ft')
const showUsfsTrails = hasFeature('has_usfs_trails')
const showBlmTrails = hasFeature('has_blm_trails')
// Don't render if no overlay features available
if (!showHillshade && !showTraffic && !showPublicLands && !showContours && !showContoursTest && !showContoursTest10ft && !showUsfsTrails && !showBlmTrails) return null
return (
<div ref={panelRef} className="layer-control">
<button
className="layer-control-btn"
onClick={() => setOpen((v) => !v)}
title="Map layers"
aria-label="Toggle map layers"
>
<Layers size={20} />
</button>
{open && (
<div className="layer-control-popover">
{/* View mode segmented control */}
<div className="view-mode-control">
<button
className={`view-mode-btn ${viewMode === 'map' ? 'active' : ''}`}
onClick={() => setViewMode('map')}
title="Map view"
>
<Map size={14} />
<span>Map</span>
</button>
<button
className={`view-mode-btn ${viewMode === 'satellite' ? 'active' : ''}`}
onClick={() => setViewMode('satellite')}
title="Satellite view"
>
<Satellite size={14} />
<span>Satellite</span>
</button>
<button
className={`view-mode-btn ${viewMode === 'hybrid' ? 'active' : ''}`}
onClick={() => setViewMode('hybrid')}
title="Hybrid view"
>
<Globe size={14} />
<span>Hybrid</span>
</button>
</div>
<div className="layer-control-header">Layers</div>
{showHillshade && (
<label className="layer-control-item">
<span className="layer-control-label">Hillshade</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={hillshade}
onChange={(e) => setHillshade(e.target.checked)}
/>
</label>
)}
{showTraffic && (
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
<label
className="layer-control-item"
title={trafficDisabled ? 'Sign in to enable traffic' : undefined}
style={trafficDisabled ? { opacity: 0.5, cursor: 'not-allowed' } : undefined}
>
<span className="layer-control-label">Traffic</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={traffic}
Gate Traffic toggle on auth.authenticated (#3) Root cause: /api/traffic is on Caddy's @authed_api, so when logged out MapLibre's raster tile fetches receive a 302 to the Authentik login (HTML), which it can't decode as an image and retries on every map move — console spam and a stuck-feeling Traffic toggle. Fix (frontend-only; /api/traffic stays auth-gated in Caddy): - LayerControl: the Traffic toggle is always rendered but disabled (greyed, "Sign in to enable traffic" tooltip) until auth has loaded AND the user is authenticated — mirroring Panel.jsx's contacts gating. The add-traffic apply effect now also requires auth.authenticated (and lists it in deps), and the mount init only restores saved traffic=true when authenticated. - Teardown on session -> anonymous: an effect flips traffic:false once auth has loaded and the user is not authenticated, which drives the apply effect to removeTrafficLayer (no further tile requests). - MapView: the style-reload re-apply (which re-adds layers from localStorage on theme/style changes) now also checks auth.authenticated for traffic, so it can't re-add the source for an anonymous session — the second add path that would otherwise reintroduce the 302 retry loop. - localStorage hydration: LayerControl now subscribes via useConfig() and its init effect depends on [config] instead of [], so saved layer prefs hydrate correctly once /api/config resolves (previously, mounting before config loaded left toggles stuck off and never re-initialized). Shown-but-disabled (not hidden) so logged-in users see no flicker on reload during the brief pre-whoami window. Tests: the navi repo has no test infrastructure (no vitest/jest); bootstrapping is out of scope. Follow-up: seed a vitest + RTL test asserting the Traffic toggle is disabled when !auth.authenticated. Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:40:49 -06:00
disabled={trafficDisabled}
onChange={(e) => setTraffic(e.target.checked)}
/>
</label>
)}
{showPublicLands && (
<label className="layer-control-item">
<span className="layer-control-label">Public Lands</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={publicLands}
onChange={(e) => setPublicLands(e.target.checked)}
/>
</label>
)}
{showContours && (
<label className="layer-control-item">
<span className="layer-control-label">Contours</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={contours}
onChange={(e) => setContours(e.target.checked)}
/>
</label>
)}
{showContoursTest && (
<label className="layer-control-item">
<span className="layer-control-label">Contours (Test)</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={contoursTest}
onChange={(e) => setContoursTest(e.target.checked)}
/>
</label>
)}
{showContoursTest10ft && (
<label className="layer-control-item">
<span className="layer-control-label">Contours (Test 10ft)</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={contoursTest10ft}
onChange={(e) => setContoursTest10ft(e.target.checked)}
/>
</label>
)}
{showUsfsTrails && (
<label className="layer-control-item">
<span className="layer-control-label">USFS Trails</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={usfsTrails}
onChange={(e) => setUsfsTrails(e.target.checked)}
/>
</label>
)}
{showBlmTrails && (
<label className="layer-control-item">
<span className="layer-control-label">BLM Roads</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={blmTrails}
onChange={(e) => setBlmTrails(e.target.checked)}
/>
</label>
)}
</div>
)}
</div>
)
}