mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(planner): walk legs as centred dividers + journey-card note line
Walk segments in the journey modal and the day-plan inline itinerary collapse from two lines into a single centred divider — dashed rules left and right, the walk in the middle (foot icon, destination, minutes). Leg meta badges sit tighter under their titles. The Transports-tab journey card shows a dimmed first-line note preview, and the journey modal now reads the reservation from the live store, so an update is visible the moment the entry reopens.
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
Plane, Hotel, Utensils, Train, Car, Ship, Bus, Sailboat, Bike, CarTaxiFront, Route, Ticket, FileText, MapPin,
|
||||
Calendar, Hash, CheckCircle2, Circle, Pencil, Trash2, Plus, ChevronDown, ChevronRight, Users,
|
||||
ExternalLink, BookMarked, Lightbulb, Link2, Clock, ArrowRight, AlertCircle, Download,
|
||||
TramFront, Footprints,
|
||||
TramFront, Footprints, StickyNote,
|
||||
} from 'lucide-react'
|
||||
import { openFile } from '../../utils/fileDownload'
|
||||
import { TransitTitle, TransitLegChips, TransitMetaBadges, fmtTransitDuration } from './transitDisplay'
|
||||
@@ -531,7 +531,7 @@ function TransitJourneyCard({ r, days, onOpen, onDelete, canEdit }: {
|
||||
<div className="text-content" style={{ fontSize: 'calc(13.5px * var(--fs-scale-body, 1))', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
<TransitTitle title={r.title} iconSize={12} />
|
||||
</div>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<TransitMetaBadges size="sm" items={[
|
||||
{ text: day ? (day.title || t('dayplan.dayN', { n: day.day_number })) : '' },
|
||||
{ icon: Calendar, text: dateStr || '' },
|
||||
@@ -558,6 +558,12 @@ function TransitJourneyCard({ r, days, onOpen, onDelete, canEdit }: {
|
||||
<TransitLegChips legs={transit.legs} size="md" t={t} />
|
||||
</div>
|
||||
)}
|
||||
{r.notes && (
|
||||
<div className="text-content-faint" style={{ paddingLeft: 44, display: 'flex', alignItems: 'center', gap: 5, fontSize: 'calc(11px * var(--fs-scale-caption, 1))', minWidth: 0 }}>
|
||||
<StickyNote size={11} style={{ flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.notes.split('\n')[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
{confirmOpen && ReactDOM.createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.35)]" style={{ position: 'fixed', inset: 0, zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={e => { e.stopPropagation(); setConfirmOpen(false) }}>
|
||||
<div className="bg-surface-card" style={{ borderRadius: 14, padding: 20, width: 340, boxShadow: '0 16px 48px rgba(0,0,0,0.22)' }} onClick={e => e.stopPropagation()}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { splitReservationDateTime, formatTime } from '../../utils/formatters'
|
||||
import { TransitTitle, TransitMetaBadges, fmtTransitDuration } from './transitDisplay'
|
||||
import { TransitTitle, TransitMetaBadges, TransitWalkDivider, fmtTransitDuration } from './transitDisplay'
|
||||
import type { Reservation } from '../../types'
|
||||
|
||||
/**
|
||||
@@ -231,40 +231,30 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{(transit.legs as TransitLegMeta[]).map((leg, i) => {
|
||||
const isWalk = leg.mode === 'WALK'
|
||||
if (leg.mode === 'WALK') return <TransitWalkDivider key={i} leg={leg} t={t} />
|
||||
const mins = leg.duration ? Math.round(leg.duration / 60) : null
|
||||
return (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div className="text-content-muted" style={{ width: 44, flexShrink: 0, textAlign: 'right', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, paddingTop: 1 }}>
|
||||
{!isWalk && leg.from?.time ? leg.from.time : ''}
|
||||
{leg.from?.time || ''}
|
||||
</div>
|
||||
{isWalk ? (
|
||||
<span className="text-content-faint" style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0, paddingTop: 2 }}>
|
||||
<Footprints size={14} />
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', background: leg.line_color || 'var(--bg-hover)', color: leg.line_color ? (leg.line_text_color || '#fff') : 'var(--text-primary)', borderRadius: 6, padding: '2px 8px', fontSize: 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 700, flexShrink: 0 }}>
|
||||
{leg.line || leg.mode}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', background: leg.line_color || 'var(--bg-hover)', color: leg.line_color ? (leg.line_text_color || '#fff') : 'var(--text-primary)', borderRadius: 6, padding: '2px 8px', fontSize: 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 700, flexShrink: 0 }}>
|
||||
{leg.line || leg.mode}
|
||||
</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="text-content" style={{ fontSize: 'calc(13.5px * var(--fs-scale-body, 1))', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' }}>
|
||||
{isWalk
|
||||
? <span className="text-content-muted" style={{ fontWeight: 500 }}>{t('transit.walkTo', { name: leg.to?.name || '' })}</span>
|
||||
: <>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{leg.from?.name}</span>
|
||||
{leg.from?.track && <span className="text-content-faint" style={{ fontWeight: 500 }}>({t('transit.platform', { track: leg.from.track })})</span>}
|
||||
<ArrowRight size={12} className="text-content-faint" style={{ flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{leg.to?.name}</span>
|
||||
</>}
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{leg.from?.name}</span>
|
||||
{leg.from?.track && <span className="text-content-faint" style={{ fontWeight: 500 }}>({t('transit.platform', { track: leg.from.track })})</span>}
|
||||
<ArrowRight size={12} className="text-content-faint" style={{ flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{leg.to?.name}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<div style={{ marginTop: 3 }}>
|
||||
<TransitMetaBadges items={[
|
||||
{ icon: Clock, text: !isWalk && leg.from?.time ? `${leg.from.time}${leg.to?.time ? ` – ${leg.to.time}` : ''}` : '' },
|
||||
{ icon: Clock, text: leg.from?.time ? `${leg.from.time}${leg.to?.time ? ` – ${leg.to.time}` : ''}` : '' },
|
||||
{ text: mins ? t('transit.min', { count: mins }) : '' },
|
||||
{ text: !isWalk && leg.stops ? t('transit.stops', { count: leg.stops }) : '' },
|
||||
{ icon: MoveRight, text: !isWalk && leg.headsign ? leg.headsign : '' },
|
||||
{ text: !isWalk && leg.agency ? leg.agency : '', dim: true },
|
||||
{ text: leg.stops ? t('transit.stops', { count: leg.stops }) : '' },
|
||||
{ icon: MoveRight, text: leg.headsign || '' },
|
||||
{ text: leg.agency || '', dim: true },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,31 @@ export interface TransitLegDisplay {
|
||||
to?: { name?: string; time?: string | null; track?: string | null }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A walk leg as a one-line centred divider — dashed rules left and right,
|
||||
* the walk itself in the middle (🚶 Walk to X · 4 min). Used by the journey
|
||||
* modal and the day-plan inline itinerary.
|
||||
*/
|
||||
export function TransitWalkDivider({ leg, t, size = 'md' }: {
|
||||
leg: TransitLegDisplay
|
||||
t: (k: string, p?: Record<string, string | number>) => string
|
||||
size?: 'sm' | 'md'
|
||||
}) {
|
||||
const mins = leg.duration ? Math.round(leg.duration / 60) : null
|
||||
const ruleStyle: React.CSSProperties = { flex: 1, borderTop: '1px dashed var(--border-primary)', opacity: 0.6, minWidth: 12 }
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: size === 'sm' ? '1px 0' : '2px 0' }}>
|
||||
<span style={ruleStyle} />
|
||||
<span className="text-content-faint" style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: size === 'sm' ? 'calc(10px * var(--fs-scale-caption, 1))' : 'calc(11.5px * var(--fs-scale-caption, 1))', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '75%' }}>
|
||||
<Footprints size={size === 'sm' ? 11 : 12} style={{ flexShrink: 0 }} />
|
||||
{t('transit.walkTo', { name: leg.to?.name || '' })}{mins ? ` · ${t('transit.min', { count: mins })}` : ''}
|
||||
</span>
|
||||
<span style={ruleStyle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The itinerary folded out right inside the day-plan row (#1065): one compact
|
||||
* line per leg — time, badge or foot icon, stations — sized for the sidebar.
|
||||
@@ -31,40 +56,32 @@ export function TransitItineraryInline({ legs, t }: {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
{legs.map((leg, i) => {
|
||||
const isWalk = leg.mode === 'WALK'
|
||||
if (leg.mode === 'WALK') return <TransitWalkDivider key={i} leg={leg} t={t} size="sm" />
|
||||
const mins = leg.duration ? Math.round(leg.duration / 60) : null
|
||||
return (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: 7 }}>
|
||||
<span className="text-content-muted" style={{ width: 34, flexShrink: 0, textAlign: 'right', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, paddingTop: 1 }}>
|
||||
{!isWalk && leg.from?.time ? leg.from.time : ''}
|
||||
{leg.from?.time || ''}
|
||||
</span>
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', borderRadius: 4, padding: '0 5px',
|
||||
fontSize: 'calc(9.5px * var(--fs-scale-caption, 1))', fontWeight: 700, lineHeight: '15px', flexShrink: 0,
|
||||
background: leg.line_color || 'var(--bg-tertiary)',
|
||||
color: leg.line_color ? (leg.line_text_color || '#fff') : 'var(--text-primary)',
|
||||
}}>
|
||||
{leg.line || leg.mode}
|
||||
</span>
|
||||
{isWalk ? (
|
||||
<Footprints size={11} className="text-content-faint" style={{ flexShrink: 0, marginTop: 2 }} />
|
||||
) : (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', borderRadius: 4, padding: '0 5px',
|
||||
fontSize: 'calc(9.5px * var(--fs-scale-caption, 1))', fontWeight: 700, lineHeight: '15px', flexShrink: 0,
|
||||
background: leg.line_color || 'var(--bg-tertiary)',
|
||||
color: leg.line_color ? (leg.line_text_color || '#fff') : 'var(--text-primary)',
|
||||
}}>
|
||||
{leg.line || leg.mode}
|
||||
</span>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="text-content" style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 4, minWidth: 0 }}>
|
||||
{isWalk
|
||||
? <span className="text-content-muted">{t('transit.walkTo', { name: leg.to?.name || '' })}</span>
|
||||
: <>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{leg.from?.name}</span>
|
||||
<MoveRight size={10} className="text-content-faint" style={{ flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{leg.to?.name}</span>
|
||||
</>}
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{leg.from?.name}</span>
|
||||
<MoveRight size={10} className="text-content-faint" style={{ flexShrink: 0 }} />
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{leg.to?.name}</span>
|
||||
</div>
|
||||
<div className="text-content-faint" style={{ fontSize: 'calc(9.5px * var(--fs-scale-caption, 1))', marginTop: 1 }}>
|
||||
{[
|
||||
mins ? t('transit.min', { count: mins }) : null,
|
||||
!isWalk && leg.stops ? t('transit.stops', { count: leg.stops }) : null,
|
||||
!isWalk && leg.from?.track ? t('transit.platform', { track: leg.from.track }) : null,
|
||||
leg.stops ? t('transit.stops', { count: leg.stops }) : null,
|
||||
leg.from?.track ? t('transit.platform', { track: leg.from.track }) : null,
|
||||
].filter(Boolean).join(' · ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -724,7 +724,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
||||
{/* Journey view for a saved public-transit entry (#1065) */}
|
||||
{transitJourney && (
|
||||
<TransitJourneyModal
|
||||
reservation={transitJourney}
|
||||
reservation={reservations.find(r => r.id === transitJourney.id) ?? transitJourney}
|
||||
canEdit={can('day_edit', trip)}
|
||||
onClose={() => setTransitJourney(null)}
|
||||
onSave={async (fields) => { await tripActions.updateReservation(tripId, transitJourney.id, fields); setTransitJourney(null) }}
|
||||
|
||||
Reference in New Issue
Block a user