mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(client): the planner map view toggles and first fit move into usePlannerMapView
The day route toggle and its travel profile, the whole-trip overview, the map lock, the recorded Dawarich trail, the fit key and the effect that fits each trip once move out of useTripPlanner into usePlannerMapView. A new test calls every toggle and the automatic route, which no test reached before. useTripPlanner calls the hook where that code used to sit, so the Dawarich fetch and the first fit still run in the same order and the planner returns the same object. handleSelectDay now lists the map lock ref, the phone ref and setFitKey it receives from the hook; they never change identity, so nothing behaves differently.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
// FE-TP-MAPVIEW-001 to FE-TP-MAPVIEW-012
|
||||
//
|
||||
// The planner's map view switches and its first fit, driven straight through
|
||||
// usePlannerMapView. Every toggle is called here, together with the storage each one
|
||||
// remembers its state in: the planner tests only ever read these values.
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { resetAllStores } from '../../../tests/helpers/store'
|
||||
import { buildPlace, buildTrip } from '../../../tests/helpers/factories'
|
||||
import type { Place, Trip } from '../../types'
|
||||
import { usePlannerMapView } from './usePlannerMapView'
|
||||
|
||||
interface Props {
|
||||
tripId: number
|
||||
trip: Trip | null
|
||||
places: Place[]
|
||||
selectedDayId: number | null
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
function renderMapView(initial: Partial<Props> = {}) {
|
||||
const initialProps: Props = { tripId: 7, trip: null, places: [], selectedDayId: null, isMobile: false, ...initial }
|
||||
return renderHook((props: Props) => usePlannerMapView(props), { initialProps })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
resetAllStores()
|
||||
})
|
||||
|
||||
describe('usePlannerMapView', () => {
|
||||
it('FE-TP-MAPVIEW-001: a trip nobody has looked at opens with every layer off and the map free', () => {
|
||||
const { result } = renderMapView()
|
||||
|
||||
expect(result.current.routeShown).toBe(false)
|
||||
expect(result.current.transitRoutesShown).toBe(false)
|
||||
expect(result.current.routeProfile).toBe('driving')
|
||||
expect(result.current.overviewShown).toBe(false)
|
||||
expect(result.current.mapLocked).toBe(false)
|
||||
expect(result.current.mapLockedRef.current).toBe(false)
|
||||
expect(result.current.dawarichTrailShown).toBe(false)
|
||||
expect(result.current.dawarichTrail.status).toBe('idle')
|
||||
expect(result.current.fitKey).toBe(0)
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-002: the day route remembers an explicit choice per trip, given as a value or an updater', () => {
|
||||
const { result, rerender } = renderMapView()
|
||||
|
||||
act(() => { result.current.setRouteShown(true) })
|
||||
expect(result.current.routeShown).toBe(true)
|
||||
expect(localStorage.getItem('trek:day-route:7')).toBe('true')
|
||||
// No day selected: the transit lines stay off whatever the toggle says (#2019).
|
||||
expect(result.current.transitRoutesShown).toBe(false)
|
||||
|
||||
rerender({ tripId: 7, trip: null, places: [], selectedDayId: 3, isMobile: false })
|
||||
expect(result.current.transitRoutesShown).toBe(true)
|
||||
|
||||
act(() => { result.current.setRouteShown(prev => !prev) })
|
||||
expect(result.current.routeShown).toBe(false)
|
||||
expect(localStorage.getItem('trek:day-route:7')).toBe('false')
|
||||
expect(result.current.transitRoutesShown).toBe(false)
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-003: a stored choice comes back with the trip, and anything else counts as no choice', () => {
|
||||
localStorage.setItem('trek:day-route:7', 'true')
|
||||
localStorage.setItem('trek:day-route:8', 'false')
|
||||
localStorage.setItem('trek:day-route:9', 'maybe')
|
||||
|
||||
expect(renderMapView({ tripId: 7 }).result.current.routeShown).toBe(true)
|
||||
expect(renderMapView({ tripId: 8 }).result.current.routeShown).toBe(false)
|
||||
|
||||
const unknown = renderMapView({ tripId: 9 }).result
|
||||
expect(unknown.current.routeShown).toBe(false)
|
||||
act(() => { unknown.current.autoShowRoute() })
|
||||
expect(unknown.current.routeShown).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-004: the automatic route is a default that never overrides an explicit off or reaches storage', () => {
|
||||
const fresh = renderMapView().result
|
||||
act(() => { fresh.current.autoShowRoute() })
|
||||
expect(fresh.current.routeShown).toBe(true)
|
||||
expect(localStorage.getItem('trek:day-route:7')).toBeNull()
|
||||
|
||||
localStorage.setItem('trek:day-route:8', 'false')
|
||||
const declined = renderMapView({ tripId: 8 }).result
|
||||
act(() => { declined.current.autoShowRoute() })
|
||||
expect(declined.current.routeShown).toBe(false)
|
||||
expect(localStorage.getItem('trek:day-route:8')).toBe('false')
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-005: without a trip id the route toggle works but is remembered nowhere', () => {
|
||||
const setItem = vi.spyOn(Storage.prototype, 'setItem')
|
||||
const { result } = renderMapView({ tripId: Number.NaN })
|
||||
|
||||
act(() => { result.current.setRouteShown(true) })
|
||||
|
||||
expect(result.current.routeShown).toBe(true)
|
||||
expect(setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-006: the route profile is a plain per-session choice', () => {
|
||||
const { result } = renderMapView()
|
||||
act(() => { result.current.setRouteProfile('plugin:bikes/gravel') })
|
||||
expect(result.current.routeProfile).toBe('plugin:bikes/gravel')
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-007: the whole-trip overview flips and is kept per trip for the session', () => {
|
||||
const { result } = renderMapView()
|
||||
|
||||
act(() => { result.current.toggleOverview() })
|
||||
expect(result.current.overviewShown).toBe(true)
|
||||
expect(sessionStorage.getItem('trip-overview-7')).toBe('1')
|
||||
expect(renderMapView().result.current.overviewShown).toBe(true)
|
||||
expect(renderMapView({ tripId: 8 }).result.current.overviewShown).toBe(false)
|
||||
|
||||
act(() => { result.current.toggleOverview() })
|
||||
expect(result.current.overviewShown).toBe(false)
|
||||
expect(sessionStorage.getItem('trip-overview-7')).toBe('0')
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-008: the map lock flips, is read through its ref and is remembered by the browser', () => {
|
||||
const { result } = renderMapView()
|
||||
|
||||
act(() => { result.current.toggleMapLocked() })
|
||||
expect(result.current.mapLocked).toBe(true)
|
||||
expect(result.current.mapLockedRef.current).toBe(true)
|
||||
expect(localStorage.getItem('trek:map-locked')).toBe('1')
|
||||
// A browser-wide choice, so another trip opens locked too.
|
||||
expect(renderMapView({ tripId: 8 }).result.current.mapLocked).toBe(true)
|
||||
|
||||
act(() => { result.current.toggleMapLocked() })
|
||||
expect(result.current.mapLocked).toBe(false)
|
||||
expect(result.current.mapLockedRef.current).toBe(false)
|
||||
expect(localStorage.getItem('trek:map-locked')).toBe('0')
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-009: a storage that refuses the lock still leaves a working toggle', () => {
|
||||
const realGet = Storage.prototype.getItem
|
||||
const realSet = Storage.prototype.setItem
|
||||
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(function (this: Storage, key: string) {
|
||||
if (key === 'trek:map-locked') throw new DOMException('denied', 'SecurityError')
|
||||
return realGet.call(this, key)
|
||||
})
|
||||
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(function (this: Storage, key: string, value: string) {
|
||||
if (key === 'trek:map-locked') throw new DOMException('denied', 'SecurityError')
|
||||
realSet.call(this, key, value)
|
||||
})
|
||||
|
||||
const { result } = renderMapView()
|
||||
expect(result.current.mapLocked).toBe(false)
|
||||
|
||||
act(() => { result.current.toggleMapLocked() })
|
||||
expect(result.current.mapLocked).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-010: the phone flag is read through a ref that follows every render', () => {
|
||||
const { result, rerender } = renderMapView()
|
||||
expect(result.current.isMobileRef.current).toBe(false)
|
||||
|
||||
rerender({ tripId: 7, trip: null, places: [], selectedDayId: null, isMobile: true })
|
||||
expect(result.current.isMobileRef.current).toBe(true)
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-011: the recorded trail flips and is kept per trip for the session', () => {
|
||||
const { result } = renderMapView()
|
||||
|
||||
act(() => { result.current.toggleDawarichTrail() })
|
||||
expect(result.current.dawarichTrailShown).toBe(true)
|
||||
expect(sessionStorage.getItem('trip-dawarich-7')).toBe('1')
|
||||
// The addon is off in this store, so switching the layer on asks nothing of the network.
|
||||
expect(result.current.dawarichTrail.status).toBe('idle')
|
||||
expect(renderMapView().result.current.dawarichTrailShown).toBe(true)
|
||||
|
||||
act(() => { result.current.toggleDawarichTrail() })
|
||||
expect(result.current.dawarichTrailShown).toBe(false)
|
||||
expect(sessionStorage.getItem('trip-dawarich-7')).toBe('0')
|
||||
})
|
||||
|
||||
it('FE-TP-MAPVIEW-012: each trip gets one automatic fit, once it has a place to fit to', () => {
|
||||
const tripA = buildTrip({ id: 1 })
|
||||
const tripB = buildTrip({ id: 2 })
|
||||
const unplaced = buildPlace({ lat: null, lng: null })
|
||||
const placed = buildPlace({ lat: 46.95, lng: 7.45 })
|
||||
const { result, rerender } = renderMapView({ places: [placed] })
|
||||
const show = (over: Partial<Props>) => rerender({ tripId: 7, trip: null, places: [], selectedDayId: null, isMobile: false, ...over })
|
||||
|
||||
// No trip loaded yet: nothing to fit.
|
||||
expect(result.current.fitKey).toBe(0)
|
||||
|
||||
show({ trip: tripA, places: [unplaced] })
|
||||
expect(result.current.fitKey).toBe(0)
|
||||
|
||||
show({ trip: tripA, places: [unplaced, placed] })
|
||||
expect(result.current.fitKey).toBe(1)
|
||||
|
||||
show({ trip: tripA, places: [placed] })
|
||||
expect(result.current.fitKey).toBe(1)
|
||||
|
||||
show({ trip: tripB, places: [placed] })
|
||||
expect(result.current.fitKey).toBe(2)
|
||||
|
||||
act(() => { result.current.setFitKey(k => k + 1) })
|
||||
expect(result.current.fitKey).toBe(3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useDawarichTrail } from '../../components/Map/useDawarichTrail'
|
||||
import type { TripStoreState } from '../../store/tripStore'
|
||||
import type { Place } from '../../types'
|
||||
import type { PlannerBase } from './plannerTypes'
|
||||
|
||||
interface PlannerMapViewOptions extends Pick<PlannerBase, 'tripId' | 'trip'> {
|
||||
places: Place[]
|
||||
selectedDayId: TripStoreState['selectedDayId']
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* How the planner's map is shown: the day route toggle and its travel profile, the
|
||||
* whole-trip overview, the map lock, the recorded Dawarich trail, and the fit key
|
||||
* with the one automatic fit each trip gets when it first has places to show.
|
||||
*/
|
||||
export function usePlannerMapView(options: PlannerMapViewOptions) {
|
||||
const { tripId, trip, places, selectedDayId, isMobile } = options
|
||||
// Manual route planning: off by default, toggled from the day-plan footer. Mode
|
||||
// is per-session and selects which travel time the connectors show: either a
|
||||
// built-in OSRM profile or a plugin route profile ('plugin:<id>/<profile>').
|
||||
// Per-trip route visibility. `null` = the user has never said anything, which
|
||||
// is what lets the mobile map switch it on by default; an explicit false has to
|
||||
// survive every later map entry, and it used to be clobbered on each one (#2003).
|
||||
const routeStorageKey = tripId ? `trek:day-route:${tripId}` : null
|
||||
const [routeChoice, setRouteChoice] = useState<boolean | null>(() => {
|
||||
if (typeof window === 'undefined' || !routeStorageKey) return null
|
||||
const raw = window.localStorage.getItem(routeStorageKey)
|
||||
return raw === 'true' ? true : raw === 'false' ? false : null
|
||||
})
|
||||
const routeShown = routeChoice === true
|
||||
const setRouteShown = useCallback((v: boolean | ((prev: boolean) => boolean)) => {
|
||||
setRouteChoice(prev => {
|
||||
const next = typeof v === 'function' ? v(prev === true) : v
|
||||
if (routeStorageKey && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(routeStorageKey, String(next))
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [routeStorageKey])
|
||||
// The mobile map opens with the day's route drawn. That is a default, not a choice,
|
||||
// so it never overwrites an explicit off and is never written to storage itself.
|
||||
const autoShowRoute = useCallback(() => {
|
||||
setRouteChoice(prev => (prev === null ? true : prev))
|
||||
}, [])
|
||||
// What the planner maps actually draw. The persisted toggle can rehydrate as
|
||||
// true while no day is selected yet (trip re-entry resets the selection, and
|
||||
// a second click on the day header clears it). Without a day context the
|
||||
// per-day transit filter is off, so the map would draw every automated
|
||||
// transport in the trip (#2019).
|
||||
const transitRoutesShown = routeShown && selectedDayId != null
|
||||
const [routeProfile, setRouteProfile] = useState<string>('driving')
|
||||
// Whole-trip route overview (#1736): every day's route at once, each in its own
|
||||
// colour. Per trip and per session like road trip mode: it answers "what does the
|
||||
// whole thing look like", which is a question you ask of one trip, not a preference.
|
||||
const [overviewShown, setOverviewShown] = useState<boolean>(() => sessionStorage.getItem(`trip-overview-${tripId}`) === '1')
|
||||
const toggleOverview = useCallback(() => {
|
||||
setOverviewShown(prev => {
|
||||
const next = !prev
|
||||
sessionStorage.setItem(`trip-overview-${tripId}`, next ? '1' : '0')
|
||||
return next
|
||||
})
|
||||
}, [tripId])
|
||||
// A locked map stays where the traveller put it (#2010): picking a day or a place no
|
||||
// longer zooms or pans it. Remembered per browser, like the other view toggles; the
|
||||
// first frame of a trip still fits, or the map would open on nothing.
|
||||
const [mapLocked, setMapLocked] = useState<boolean>(() => {
|
||||
try { return localStorage.getItem('trek:map-locked') === '1' } catch { return false }
|
||||
})
|
||||
const mapLockedRef = useRef(mapLocked)
|
||||
mapLockedRef.current = mapLocked
|
||||
const isMobileRef = useRef(isMobile)
|
||||
isMobileRef.current = isMobile
|
||||
const toggleMapLocked = useCallback(() => {
|
||||
setMapLocked(prev => {
|
||||
const next = !prev
|
||||
try { localStorage.setItem('trek:map-locked', next ? '1' : '0') } catch { /* private mode keeps it for the session */ }
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
// The recorded route from Dawarich (#2279), per trip and per session for the
|
||||
// same reason as the overview above: it answers "what actually happened on
|
||||
// this trip", which is a question about one trip rather than a preference.
|
||||
const [dawarichTrailShown, setDawarichTrailShown] = useState<boolean>(
|
||||
() => sessionStorage.getItem(`trip-dawarich-${tripId}`) === '1',
|
||||
)
|
||||
const toggleDawarichTrail = useCallback(() => {
|
||||
setDawarichTrailShown(prev => {
|
||||
const next = !prev
|
||||
sessionStorage.setItem(`trip-dawarich-${tripId}`, next ? '1' : '0')
|
||||
return next
|
||||
})
|
||||
}, [tripId])
|
||||
// Fetched here rather than in MapViewAuto so the desktop and the phone share
|
||||
// one request, and so the pill that toggles it can show why there is no line.
|
||||
const dawarichTrail = useDawarichTrail(tripId, dawarichTrailShown)
|
||||
|
||||
const [fitKey, setFitKey] = useState<number>(0)
|
||||
const initialFitTripId = useRef<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!trip) return
|
||||
if (initialFitTripId.current === trip.id) return
|
||||
const hasGeoPlaces = places.some(p => p.lat != null && p.lng != null)
|
||||
if (!hasGeoPlaces) return
|
||||
initialFitTripId.current = trip.id
|
||||
setFitKey(k => k + 1)
|
||||
}, [trip, places])
|
||||
|
||||
return {
|
||||
routeShown, setRouteShown, autoShowRoute, transitRoutesShown, routeProfile, setRouteProfile,
|
||||
overviewShown, toggleOverview, mapLocked, toggleMapLocked, mapLockedRef, isMobileRef,
|
||||
dawarichTrailShown, toggleDawarichTrail, dawarichTrail, fitKey, setFitKey,
|
||||
}
|
||||
}
|
||||
|
||||
export type PlannerMapView = ReturnType<typeof usePlannerMapView>
|
||||
@@ -35,7 +35,6 @@ import type { DayBoundaryControls } from '../../components/Map/dayBoundaryDrag'
|
||||
import { dayWindow, roadtripInsertion } from '../../components/Roadtrip/dayWindow'
|
||||
import { carrierReservationIds, isStoredStop, viasLeaving, type CarrierTerminal } from '@trek/shared/roadtrip'
|
||||
import { useTripRouteOverview } from '../../components/Map/useTripRouteOverview'
|
||||
import { useDawarichTrail } from '../../components/Map/useDawarichTrail'
|
||||
import { collapsedDayDates } from '../../components/Map/dawarichTrail'
|
||||
import { useRoadtripCorridor } from '../../components/Roadtrip/useRoadtripCorridor'
|
||||
import { PHONE_CORRIDOR_OPTIONS } from '../../components/Roadtrip/corridorSearchModel'
|
||||
@@ -86,6 +85,7 @@ import { pendingStayPlaceIds } from '../../utils/pendingStays'
|
||||
import { useDayDelete } from './useDayDelete'
|
||||
import { useDayAdd } from './useDayAdd'
|
||||
import { usePlannerDialogs } from './usePlannerDialogs'
|
||||
import { usePlannerMapView } from './usePlannerMapView'
|
||||
import { useIsPhone } from '../../mobile/useIsPhone'
|
||||
|
||||
/** Stable empty list so the road trip hook stays inert while its mode is off. */
|
||||
@@ -373,87 +373,11 @@ export function useTripPlanner() {
|
||||
// by the global widget, so by then the state has remounted back to its default.
|
||||
// The value comes off the persisted job (#2076).
|
||||
const importKindRef = useRef<'transports' | 'bookings'>('bookings')
|
||||
// Manual route planning: off by default, toggled from the day-plan footer. Mode
|
||||
// is per-session and selects which travel time the connectors show — either a
|
||||
// built-in OSRM profile or a plugin route profile ('plugin:<id>/<profile>').
|
||||
// Per-trip route visibility. `null` = the user has never said anything, which
|
||||
// is what lets the mobile map switch it on by default; an explicit false has to
|
||||
// survive every later map entry, and it used to be clobbered on each one (#2003).
|
||||
const routeStorageKey = tripId ? `trek:day-route:${tripId}` : null
|
||||
const [routeChoice, setRouteChoice] = useState<boolean | null>(() => {
|
||||
if (typeof window === 'undefined' || !routeStorageKey) return null
|
||||
const raw = window.localStorage.getItem(routeStorageKey)
|
||||
return raw === 'true' ? true : raw === 'false' ? false : null
|
||||
})
|
||||
const routeShown = routeChoice === true
|
||||
const setRouteShown = useCallback((v: boolean | ((prev: boolean) => boolean)) => {
|
||||
setRouteChoice(prev => {
|
||||
const next = typeof v === 'function' ? v(prev === true) : v
|
||||
if (routeStorageKey && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(routeStorageKey, String(next))
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [routeStorageKey])
|
||||
// The mobile map opens with the day's route drawn — a default, not a choice, so
|
||||
// it never overwrites an explicit off and is never written to storage itself.
|
||||
const autoShowRoute = useCallback(() => {
|
||||
setRouteChoice(prev => (prev === null ? true : prev))
|
||||
}, [])
|
||||
// What the planner maps actually draw. The persisted toggle can rehydrate as
|
||||
// true while no day is selected yet (trip re-entry resets the selection, and
|
||||
// a second click on the day header clears it) — without a day context the
|
||||
// per-day transit filter is off, so the map would draw every automated
|
||||
// transport in the trip (#2019).
|
||||
const transitRoutesShown = routeShown && selectedDayId != null
|
||||
const [routeProfile, setRouteProfile] = useState<string>('driving')
|
||||
// Whole-trip route overview (#1736): every day's route at once, each in its own
|
||||
// colour. Per trip and per session like road trip mode — it answers "what does the
|
||||
// whole thing look like", which is a question you ask of one trip, not a preference.
|
||||
const [overviewShown, setOverviewShown] = useState<boolean>(() => sessionStorage.getItem(`trip-overview-${tripId}`) === '1')
|
||||
const toggleOverview = useCallback(() => {
|
||||
setOverviewShown(prev => {
|
||||
const next = !prev
|
||||
sessionStorage.setItem(`trip-overview-${tripId}`, next ? '1' : '0')
|
||||
return next
|
||||
})
|
||||
}, [tripId])
|
||||
// A locked map stays where the traveller put it (#2010): picking a day or a place no
|
||||
// longer zooms or pans it. Remembered per browser, like the other view toggles; the
|
||||
// first frame of a trip still fits, or the map would open on nothing.
|
||||
const [mapLocked, setMapLocked] = useState<boolean>(() => {
|
||||
try { return localStorage.getItem('trek:map-locked') === '1' } catch { return false }
|
||||
})
|
||||
const mapLockedRef = useRef(mapLocked)
|
||||
mapLockedRef.current = mapLocked
|
||||
const isMobileRef = useRef(isMobile)
|
||||
isMobileRef.current = isMobile
|
||||
const toggleMapLocked = useCallback(() => {
|
||||
setMapLocked(prev => {
|
||||
const next = !prev
|
||||
try { localStorage.setItem('trek:map-locked', next ? '1' : '0') } catch { /* private mode keeps it for the session */ }
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
// The recorded route from Dawarich (#2279), per trip and per session for the
|
||||
// same reason as the overview above: it answers "what actually happened on
|
||||
// this trip", which is a question about one trip rather than a preference.
|
||||
const [dawarichTrailShown, setDawarichTrailShown] = useState<boolean>(
|
||||
() => sessionStorage.getItem(`trip-dawarich-${tripId}`) === '1',
|
||||
)
|
||||
const toggleDawarichTrail = useCallback(() => {
|
||||
setDawarichTrailShown(prev => {
|
||||
const next = !prev
|
||||
sessionStorage.setItem(`trip-dawarich-${tripId}`, next ? '1' : '0')
|
||||
return next
|
||||
})
|
||||
}, [tripId])
|
||||
// Fetched here rather than in MapViewAuto so the desktop and the phone share
|
||||
// one request, and so the pill that toggles it can show why there is no line.
|
||||
const dawarichTrail = useDawarichTrail(tripId, dawarichTrailShown)
|
||||
|
||||
const [fitKey, setFitKey] = useState<number>(0)
|
||||
const initialFitTripId = useRef<number | null>(null)
|
||||
const {
|
||||
mapLocked, toggleMapLocked, mapLockedRef, isMobileRef, fitKey, setFitKey,
|
||||
overviewShown, toggleOverview, dawarichTrailShown, toggleDawarichTrail, dawarichTrail,
|
||||
routeShown, setRouteShown, autoShowRoute, transitRoutesShown, routeProfile, setRouteProfile,
|
||||
} = usePlannerMapView({ tripId, trip, places, selectedDayId, isMobile })
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<'left' | 'right' | null>(null)
|
||||
const mobilePlanScrollTopRef = useRef<number>(0)
|
||||
const mobilePlacesScrollTopRef = useRef<number>(0)
|
||||
@@ -498,15 +422,6 @@ export function useTripPlanner() {
|
||||
[deletePlaceIds, bookedNightsNote],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!trip) return
|
||||
if (initialFitTripId.current === trip.id) return
|
||||
const hasGeoPlaces = places.some(p => p.lat != null && p.lng != null)
|
||||
if (!hasGeoPlaces) return
|
||||
initialFitTripId.current = trip.id
|
||||
setFitKey(k => k + 1)
|
||||
}, [trip, places])
|
||||
|
||||
useEffect(() => {
|
||||
// The server runs the import when EITHER kitinerary or the LLM parser is
|
||||
// there (booking-import.service.ts), so gating the entry point on kitinerary
|
||||
@@ -761,7 +676,7 @@ export function useTripPlanner() {
|
||||
if (!skipFit && !(mapLockedRef.current && !isMobileRef.current)) setFitKey(k => k + 1)
|
||||
setMobileSidebarOpen(null)
|
||||
updateRouteForDay(dayId)
|
||||
}, [updateRouteForDay])
|
||||
}, [updateRouteForDay, isMobileRef, mapLockedRef, setFitKey])
|
||||
|
||||
const handlePlaceClick = useCallback((placeId: number | null, assignmentId?: number | null) => {
|
||||
if (assignmentId) {
|
||||
|
||||
Reference in New Issue
Block a user