mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(roadtrip): read the wall clock minutes and today through localDate
useMRoadtrip and useMRtCorridor each kept a nowMinutes helper and their own check whether the selected day is today. Both now use localMinutes and isLocalToday from utils/localDate.ts, which hold the same rules.
This commit is contained in:
@@ -8,13 +8,7 @@ import type { RoadtripDay } from '@trek/shared/roadtrip'
|
||||
import type { MTripShellApi, TripPlanner } from '../MTripShell'
|
||||
import type { RoadtripRow, StopRow } from '../../../../components/Roadtrip/roadtripRowModel'
|
||||
import type { Category, Place } from '../../../../types'
|
||||
import { localIsoDate } from '../../../../utils/localDate'
|
||||
|
||||
/** Minutes since midnight, local time. */
|
||||
const nowMinutes = (): number => {
|
||||
const d = new Date()
|
||||
return d.getHours() * 60 + d.getMinutes()
|
||||
}
|
||||
import { isLocalToday, localMinutes } from '../../../../utils/localDate'
|
||||
|
||||
/** The same 30 s tick the day timeline runs on, so the two never disagree by a minute. */
|
||||
const TICK_MS = 30_000
|
||||
@@ -52,11 +46,11 @@ export interface MRoadtripController {
|
||||
export function useMRoadtrip(planner: TripPlanner): MRoadtripController {
|
||||
const { roadtripRoutes, selectedDayId, days } = planner
|
||||
const vehicle = useRoadtripSettings(s => s.roadtrip_vehicle, planner.tripId)
|
||||
const [minutes, setMinutes] = useState(nowMinutes)
|
||||
const [minutes, setMinutes] = useState(localMinutes)
|
||||
const [offline, setOffline] = useState(isEffectivelyOffline)
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => setMinutes(nowMinutes()), TICK_MS)
|
||||
const id = window.setInterval(() => setMinutes(localMinutes()), TICK_MS)
|
||||
return () => window.clearInterval(id)
|
||||
}, [])
|
||||
|
||||
@@ -88,14 +82,13 @@ export function useMRoadtrip(planner: TripPlanner): MRoadtripController {
|
||||
|
||||
// "Today" is the stage's own date, not the selected day's index: a trip can be
|
||||
// planned for next year, and a countdown on a day in March is noise.
|
||||
const isToday = useMemo(() => {
|
||||
const date = days.find(d => d.id === selectedDayId)?.date
|
||||
if (!date) return false
|
||||
// The wall clock, not UTC: `nowMinutes` above is local, and between local midnight
|
||||
// and the UTC rollover the two disagree, so east of Greenwich the card hung on
|
||||
// yesterday's stage for the first hours of every night. `localDate.ts` says as much.
|
||||
return date.slice(0, 10) === localIsoDate()
|
||||
}, [days, selectedDayId])
|
||||
// The wall clock, not UTC: the minutes above are local, and between local midnight
|
||||
// and the UTC rollover the two disagree, so east of Greenwich the card hung on
|
||||
// yesterday's stage for the first hours of every night. `localDate.ts` says as much.
|
||||
const isToday = useMemo(
|
||||
() => isLocalToday(days.find(d => d.id === selectedDayId)?.date),
|
||||
[days, selectedDayId],
|
||||
)
|
||||
|
||||
return {
|
||||
stage,
|
||||
|
||||
@@ -4,13 +4,7 @@ import { stageOf, upNextStop } from '../../../../components/Roadtrip/roadtripRow
|
||||
import { useNetworkMode } from '../../../../hooks/useNetworkMode'
|
||||
import type { CorridorPoi } from '../../../../components/Roadtrip/useCorridorPois'
|
||||
import type { MTripShellApi, TripPlanner } from '../MTripShell'
|
||||
import { localIsoDate } from '../../../../utils/localDate'
|
||||
|
||||
/** Minutes since midnight, local time: the same reading the stage screen takes. */
|
||||
const nowMinutes = (): number => {
|
||||
const d = new Date()
|
||||
return d.getHours() * 60 + d.getMinutes()
|
||||
}
|
||||
import { isLocalToday, localMinutes } from '../../../../utils/localDate'
|
||||
|
||||
export interface MRtCorridorController {
|
||||
/** What is being looked for. Several at once: they ride one query per box. */
|
||||
@@ -94,8 +88,8 @@ export function useMRtCorridor(planner: TripPlanner, shell: MTripShellApi): MRtC
|
||||
const date = planner.days.find(d => d.id === planner.selectedDayId)?.date
|
||||
// Local, for the reason useMRoadtrip gives: the clock this is compared against is the
|
||||
// wall clock, and UTC disagrees with it for the first hours of every night.
|
||||
const isToday = !!date && date.slice(0, 10) === localIsoDate()
|
||||
const next = upNextStop(stage, nowMinutes(), isToday)
|
||||
const isToday = isLocalToday(date)
|
||||
const next = upNextStop(stage, localMinutes(), isToday)
|
||||
if (!next) return -1
|
||||
return stage.stops.findIndex(s => s.assignmentId === next.row.stop.assignmentId)
|
||||
}, [stage, planner.days, planner.selectedDayId])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { localIsoDate } from './localDate'
|
||||
import { isLocalToday, localIsoDate, localMinutes } from './localDate'
|
||||
|
||||
/**
|
||||
* "Today" must be the user's local calendar date. `toISOString()` is the UTC
|
||||
@@ -26,3 +26,43 @@ describe('localIsoDate', () => {
|
||||
expect(localIsoDate()).toBe(localIsoDate(new Date()))
|
||||
})
|
||||
})
|
||||
|
||||
describe('localMinutes', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('counts the minutes since local midnight', () => {
|
||||
expect(localMinutes(new Date(2026, 7, 25, 0, 0))).toBe(0)
|
||||
expect(localMinutes(new Date(2026, 7, 25, 13, 45, 59))).toBe(825)
|
||||
expect(localMinutes(new Date(2026, 7, 25, 23, 59))).toBe(1439)
|
||||
})
|
||||
|
||||
it('defaults to now', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date(2026, 7, 25, 0, 30))
|
||||
expect(localMinutes()).toBe(30)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLocalToday', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date(2026, 7, 25, 0, 30)) // local 2026-08-25 00:30
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('matches the local calendar date, with or without a time part', () => {
|
||||
expect(isLocalToday('2026-08-25')).toBe(true)
|
||||
expect(isLocalToday('2026-08-25T00:00:00.000Z')).toBe(true)
|
||||
})
|
||||
|
||||
it('is false for another date or no date at all', () => {
|
||||
expect(isLocalToday('2026-08-24')).toBe(false)
|
||||
expect(isLocalToday('')).toBe(false)
|
||||
expect(isLocalToday(null)).toBe(false)
|
||||
expect(isLocalToday(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,3 +14,16 @@
|
||||
export function localIsoDate(d: Date = new Date()): string {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** Minutes since local midnight: the wall clock a day's schedule is read against. */
|
||||
export function localMinutes(d: Date = new Date()): number {
|
||||
return d.getHours() * 60 + d.getMinutes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a trip day's date (anything starting 'YYYY-MM-DD') is today on the wall
|
||||
* clock. Local for the reason `localIsoDate` gives; a day without a date is never today.
|
||||
*/
|
||||
export function isLocalToday(date: string | null | undefined): boolean {
|
||||
return !!date && date.slice(0, 10) === localIsoDate()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user