mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
5ef8bf31f4
The button carried nothing and the form hard-wired the kind, so the categories sitting directly above it had no way through and every manual stop started life as a fuel stop with ten minutes on it. The click now carries what the corridor is looking for, dwell included. The picker is multi-select, so the first kind the panel lists wins rather than a fallback nobody switched on; with nothing selected the form falls back on its own, because a stop without a kind is a numbered destination that counts in every total. This also closes the case it was noticed on: an electric car seeds the panel to charging, so the picker said Charging over a form that said Fuel.
130 lines
6.9 KiB
TypeScript
130 lines
6.9 KiB
TypeScript
import { Fuel, Zap, ParkingSquare, Tent, Utensils, Camera, BedDouble, type LucideIcon } from 'lucide-react'
|
|
import { SERVICE_COLORS, SERVICE_STOP_TYPES } from './roadtripModel'
|
|
import type { RoadtripStopType } from '@trek/shared'
|
|
|
|
/**
|
|
* One row per kind of stop a drive can have, and the only place a new one is added.
|
|
*
|
|
* The six kinds used to be written out in nine separate lists: the enum in `@trek/shared`,
|
|
* `SERVICE_STOP_TYPES` and `SERVICE_COLORS` here in the model, the corridor's category
|
|
* keys, the map's `ROADTRIP_POI_CATEGORIES`, the popup's own `STOP_KINDS`, two lookups in
|
|
* the rail, one in the map markers and one in the corridor panel. Every one of those
|
|
* carried a comment telling the next person to keep the others in step, and it still came
|
|
* apart: a rest area was a parking sign in the popup, on the map pill and in the panel,
|
|
* and a coffee cup on the rail disc and the map marker, for the same stop. That is fixed
|
|
* here in favour of the parking sign, because the colour is already documented as "the
|
|
* blue every parking sign in Europe is printed in".
|
|
*
|
|
* The table lives beside the model rather than in it because an icon is a React component
|
|
* and `roadtripModel.ts` is deliberately free of React so it can be unit tested on its
|
|
* own. The colours and the service list stay there and are read from here.
|
|
*
|
|
*/
|
|
export interface StopKind {
|
|
key: RoadtripStopType
|
|
/** Two of the six do not match their key: `rest_area` is named under `rest`, `restaurant` under `food`. */
|
|
labelKey: string
|
|
Icon: LucideIcon
|
|
color: string
|
|
/** What the popup offers before anyone changes it — how long this kind usually takes. */
|
|
defaultMinutes: number
|
|
/** Interrupts the drive: drawn on the dashed line, no number, left out of every stop count. */
|
|
isService: boolean
|
|
/** Offered as a category in the corridor search. */
|
|
isCorridorCategory: boolean
|
|
}
|
|
|
|
export const STOP_KINDS: StopKind[] = [
|
|
{ key: 'hotel', labelKey: 'poi.cat.hotels', Icon: BedDouble, color: SERVICE_COLORS.hotel, defaultMinutes: 30, isService: true, isCorridorCategory: true },
|
|
{ key: 'fuel', labelKey: 'roadtrip.poi.fuel', Icon: Fuel, color: SERVICE_COLORS.fuel, defaultMinutes: 10, isService: true, isCorridorCategory: true },
|
|
{ key: 'charging', labelKey: 'roadtrip.poi.charging', Icon: Zap, color: SERVICE_COLORS.charging, defaultMinutes: 30, isService: true, isCorridorCategory: true },
|
|
{ key: 'rest_area', labelKey: 'roadtrip.poi.rest', Icon: ParkingSquare, color: SERVICE_COLORS.rest_area, defaultMinutes: 20, isService: true, isCorridorCategory: true },
|
|
{ key: 'campsite', labelKey: 'roadtrip.poi.campsite', Icon: Tent, color: SERVICE_COLORS.campsite, defaultMinutes: 60, isService: true, isCorridorCategory: true },
|
|
{ key: 'restaurant', labelKey: 'roadtrip.poi.food', Icon: Utensils, color: SERVICE_COLORS.restaurant, defaultMinutes: 45, isService: true, isCorridorCategory: true },
|
|
{ key: 'sights', labelKey: 'roadtrip.poi.sights', Icon: Camera, color: SERVICE_COLORS.sights, defaultMinutes: 30, isService: true, isCorridorCategory: true },
|
|
]
|
|
|
|
/** By key, for the lookups the rail, the markers and the panel used to keep themselves. */
|
|
export const STOP_KIND_BY_KEY: Record<string, StopKind> = Object.fromEntries(
|
|
STOP_KINDS.map(k => [k.key, k]),
|
|
)
|
|
|
|
export interface CorridorCategory {
|
|
key: string
|
|
labelKey: string
|
|
Icon: LucideIcon
|
|
color: string
|
|
/** The kind a hit of this category becomes, or null when it becomes something else. */
|
|
stopKind: RoadtripStopType | null
|
|
}
|
|
|
|
/** Somewhere to sleep. Blue rather than the campsite's green: both are a night, but one
|
|
* of them has a roof, and telling the two apart on a map matters more than grouping them.
|
|
* theme-lint-disable — the road-signage palette, shared with the general place search so
|
|
* a hotel found along a route looks like the hotels found anywhere else. */
|
|
export const HOTEL_COLOR = '#2563EB'
|
|
|
|
/**
|
|
* The categories, in the order the panel shows them.
|
|
*
|
|
* Written out rather than derived, because the order is a judgement: sleeping sits next
|
|
* to sleeping, so `hotel` follows `campsite` instead of landing wherever the stop-kind
|
|
* table happens to end.
|
|
*/
|
|
export const CORRIDOR_CATEGORIES: CorridorCategory[] = [
|
|
...STOP_KINDS.filter(k => k.isCorridorCategory && k.key !== 'restaurant' && k.key !== 'sights' && k.key !== 'hotel')
|
|
.map(k => ({ key: k.key as string, labelKey: k.labelKey, Icon: k.Icon, color: k.color, stopKind: k.key })),
|
|
{ key: 'hotel', labelKey: 'poi.cat.hotels', Icon: BedDouble, color: HOTEL_COLOR, stopKind: 'hotel' },
|
|
...STOP_KINDS.filter(k => k.key === 'restaurant' || k.key === 'sights')
|
|
.map(k => ({ key: k.key as string, labelKey: k.labelKey, Icon: k.Icon, color: k.color, stopKind: k.key })),
|
|
]
|
|
|
|
export const CORRIDOR_CATEGORY_BY_KEY: Record<string, CorridorCategory> = Object.fromEntries(
|
|
CORRIDOR_CATEGORIES.map(c => [c.key, c]),
|
|
)
|
|
|
|
/** The keys the corridor search offers, in the order the panel shows them. */
|
|
export const CORRIDOR_CATEGORY_KEYS = CORRIDOR_CATEGORIES.map(c => c.key)
|
|
|
|
/**
|
|
* The kind a stop added by hand opens on, read off what the corridor is looking for.
|
|
*
|
|
* The categories are what the traveller has just told the search to find, so they are
|
|
* also the best reading of what they are about to add themselves, and on an electric
|
|
* car the panel seeds itself to charging, which is precisely the case where a form
|
|
* opening on a fuel pump read as a fault.
|
|
*
|
|
* The picker is multi-select, so the first category the panel LISTS wins. Falling back
|
|
* instead would open on a kind that is not among the ones switched on, which is a worse
|
|
* answer than any of them. Null only when nothing is selected at all; the caller decides
|
|
* what that means, because a form must never open on no kind (see `DEFAULT_SERVICE_KIND`).
|
|
*/
|
|
export function manualStopKindFor(categories: readonly string[]): RoadtripStopType | null {
|
|
for (const category of CORRIDOR_CATEGORIES) {
|
|
if (categories.includes(category.key) && category.stopKind) return category.stopKind
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Categories whose hit becomes a night rather than a pause. */
|
|
export function isOvernightCategory(category: string | null | undefined): boolean {
|
|
// Campsite is both: it is a service stop today and stays one, but a night can be
|
|
// booked there just as well, so the popup offers the choice rather than deciding.
|
|
return category === 'hotel' || category === 'campsite'
|
|
}
|
|
|
|
/**
|
|
* A guard against the drift this file exists to end.
|
|
*
|
|
* `SERVICE_STOP_TYPES` is the model's own list and stays there, because the arithmetic
|
|
* must not depend on anything that imports React. The two are checked against each other
|
|
* in `stopKinds.test.ts` rather than derived, so a kind added to one and forgotten in the
|
|
* other fails a test instead of quietly counting as a destination.
|
|
*/
|
|
export const SERVICE_KIND_KEYS = STOP_KINDS.filter(k => k.isService).map(k => k.key)
|
|
|
|
/** The kinds where a tank or a battery is filled, so a range budget starts over. */
|
|
export const REFUELLING_STOP_TYPES = ['fuel', 'charging'] as const
|
|
|
|
export { SERVICE_STOP_TYPES }
|