mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 14:36:31 +00:00
c5360be242
* feat(dayplan): persist per-leg and per-day travel mode (#1281) The day-plan route used one transport mode for the whole day, held only in ephemeral state. Store it instead: leg_transport_mode on an assignment is the mode of the leg leaving that stop (null = inherit the day default), and days gains a default_transport_mode. Both nullable, so existing itineraries are unchanged. Adds PUT /assignments/:id/transport and PUT /days/:id/transport — their own endpoints so setting the day default can't wipe notes/title the way the general day update would — mirroring the assignment_time path. * feat(dayplan): choose the travel mode per segment (#1281) Each leg between two stops now routes with its own mode, so a day can mix walking, driving and plugin-routed legs. Tap the connector between two places to change that leg's mode (desktop popup / mobile sheet); the whole-day Foot/Car picker sets the persisted day default and leaves explicitly-set legs alone. The connector and map redraw in the chosen mode, and each route segment is tagged with the mode it was drawn in. New strings across all 23 locales.
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
import { Car, Footprints, Hotel, Zap } from 'lucide-react'
|
|
import type { RouteSegment } from '../../types'
|
|
|
|
// Walking gets the foot icon; a plugin route profile ('plugin:…') gets the bolt —
|
|
// its legs carry the profile-true durationText anyway, the icon just signals that
|
|
// the time came from a plugin router (e.g. EV routing with charge time folded in).
|
|
function profileIcon(profile: string) {
|
|
if (profile === 'walking') return Footprints
|
|
if (profile.startsWith('plugin:')) return Zap
|
|
return Car
|
|
}
|
|
|
|
/** Slim travel-time connector shown between two consecutive located stops in a day. */
|
|
export function RouteConnector({ seg, profile }: { seg: RouteSegment; profile: string }) {
|
|
// The leg's own mode (#1281) wins over the day-wide fallback for icon + text.
|
|
const effProfile = seg.mode ?? profile
|
|
const driving = effProfile !== 'walking'
|
|
const Icon = profileIcon(effProfile)
|
|
const line = { flex: 1, height: 1, minHeight: 1, alignSelf: 'center', background: 'var(--border-primary)' }
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '3px 14px', fontSize: 'calc(10.5px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', lineHeight: 1.2 }}>
|
|
<div style={line} />
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
|
|
<Icon size={11} strokeWidth={2} />
|
|
<span>{seg.durationText ?? (driving ? seg.drivingText : seg.walkingText)}</span>
|
|
<span style={{ opacity: 0.4 }}>·</span>
|
|
<span>{seg.distanceText}</span>
|
|
{seg.noteText && (
|
|
<>
|
|
<span style={{ opacity: 0.4 }}>·</span>
|
|
<span style={{ color: 'var(--text-muted)' }}>{seg.noteText}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div style={line} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* The hotel's bookend legs for a day: a two-line connector naming the day's
|
|
* accommodation with the drive to/from it. Rendered above the first place (the
|
|
* morning departure from the hotel) and below the last place (the evening return),
|
|
* when the "optimize from accommodation" setting is on and the day has a hotel.
|
|
*/
|
|
export function HotelRouteConnector({
|
|
seg,
|
|
profile,
|
|
name,
|
|
placement,
|
|
}: {
|
|
seg: RouteSegment
|
|
profile: string
|
|
name: string
|
|
placement: 'top' | 'bottom'
|
|
}) {
|
|
const effProfile = seg.mode ?? profile
|
|
const driving = effProfile !== 'walking'
|
|
const Icon = profileIcon(effProfile)
|
|
const line = { flex: 1, height: 1, minHeight: 1, alignSelf: 'center', background: 'var(--border-primary)' }
|
|
const hotelRow = (
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, padding: '0 14px', minWidth: 0 }}>
|
|
<Hotel size={12} strokeWidth={1.8} style={{ flexShrink: 0, color: 'var(--text-muted)' }} />
|
|
<span style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: 1.2 }}>
|
|
{name}
|
|
</span>
|
|
</div>
|
|
)
|
|
const travelRow = (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '3px 14px', fontSize: 'calc(10.5px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', lineHeight: 1.2 }}>
|
|
<div style={line} />
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
|
|
<Icon size={11} strokeWidth={2} />
|
|
<span>{seg.durationText ?? (driving ? seg.drivingText : seg.walkingText)}</span>
|
|
<span style={{ opacity: 0.4 }}>·</span>
|
|
<span>{seg.distanceText}</span>
|
|
{seg.noteText && (
|
|
<>
|
|
<span style={{ opacity: 0.4 }}>·</span>
|
|
<span style={{ color: 'var(--text-muted)' }}>{seg.noteText}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div style={line} />
|
|
</div>
|
|
)
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: placement === 'top' ? '2px 0 6px' : '6px 0 2px' }}>
|
|
{placement === 'top' ? (
|
|
<>
|
|
{hotelRow}
|
|
{travelRow}
|
|
</>
|
|
) : (
|
|
<>
|
|
{travelRow}
|
|
{hotelRow}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|