mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(roadtrip): the Google route import and the track dialog use the shared dialog frame, and Search and Manual match the day plan's buttons
Both dialogs now open in the same frame as the stay and stop dialogs: an icon tile and title in the head, the content in a card, and the actions in the footer. Closing stays blocked while an import runs. In the right column, Search and Manual take the size, text and icon of the day plan's toolbar buttons.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import { MoreHorizontal, Link2 } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n/TranslationContext'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
@@ -6,7 +6,7 @@ import { googleRouteRepo } from '../../repo/googleRouteRepo'
|
||||
import { generateUUID } from '../../sync/mutationQueue'
|
||||
import { getApiErrorMessage } from '../../types'
|
||||
import type { GoogleRoutePreview } from '@trek/shared'
|
||||
import Modal from '../shared/Modal'
|
||||
import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell'
|
||||
import CustomSelect from '../shared/CustomSelect'
|
||||
import { ContextMenu, useContextMenu } from '../shared/ContextMenu'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
@@ -24,6 +24,7 @@ export default function GoogleRouteImport({ tripId, dayId }: { tripId: number; d
|
||||
const controller = useRef<AbortController | null>(null)
|
||||
const requestId = useRef('')
|
||||
const menu = useContextMenu()
|
||||
const labelId = useId()
|
||||
useEffect(() => () => controller.current?.abort(), [])
|
||||
const inspect = async () => {
|
||||
controller.current?.abort()
|
||||
@@ -51,6 +52,8 @@ export default function GoogleRouteImport({ tripId, dayId }: { tripId: number; d
|
||||
setTarget(String(dayId ?? days[0]?.id ?? ''))
|
||||
requestId.current = generateUUID(); setOpen(true)
|
||||
}
|
||||
// Every way out is held while a preview or an import is in flight, as before.
|
||||
const close = () => { if (!busy) setOpen(false) }
|
||||
const resolved = preview?.stops.filter(s => s.lat != null && s.lng != null) ?? []
|
||||
return <>
|
||||
<Tooltip label={t('roadtrip.import.title')}>
|
||||
@@ -60,29 +63,48 @@ export default function GoogleRouteImport({ tripId, dayId }: { tripId: number; d
|
||||
</button>
|
||||
</Tooltip>
|
||||
<ContextMenu menu={menu.menu} onClose={menu.close} />
|
||||
<Modal isOpen={open} onClose={() => { if (!busy) setOpen(false) }} title={t('roadtrip.import.title')} size="lg">
|
||||
<div className="space-y-4">
|
||||
<p className="text-caption text-content-muted">{t('roadtrip.import.note')}</p>
|
||||
{!preview ? <input type="url" value={url} disabled={busy} onChange={e => setUrl(e.target.value)} aria-label="Google Maps URL" placeholder="https://maps.app.goo.gl/…"
|
||||
className="w-full rounded-xl border border-edge bg-surface-input p-3 text-body text-content" /> : <>
|
||||
<DialogShell
|
||||
open={open}
|
||||
onClose={close}
|
||||
blocked={busy}
|
||||
labelledBy={labelId}
|
||||
width="narrow"
|
||||
header={(
|
||||
<DialogHeader
|
||||
tile={<DialogTile><Link2 size={20} strokeWidth={1.9} className="text-content-muted" /></DialogTile>}
|
||||
tint={NEUTRAL_TINT}
|
||||
labelId={labelId}
|
||||
onClose={close}
|
||||
title={t('roadtrip.import.title')}
|
||||
sub={t('roadtrip.import.note')}
|
||||
subWraps
|
||||
/>
|
||||
)}
|
||||
footer={(
|
||||
<DialogFooter>
|
||||
<FooterSpacer />
|
||||
<DialogButton disabled={busy} onClick={close}>{t('common.cancel')}</DialogButton>
|
||||
<DialogButton variant="primary" disabled={busy || saved || (!preview ? !url.trim() : resolved.length < 2 || !target)} onClick={() => void (preview ? save() : inspect())}>
|
||||
{busy ? t('common.loading') : t(preview ? 'common.import' : 'common.preview')}
|
||||
</DialogButton>
|
||||
</DialogFooter>
|
||||
)}
|
||||
>
|
||||
{!preview ? <input type="url" value={url} disabled={busy} onChange={e => setUrl(e.target.value)} aria-label="Google Maps URL" placeholder="https://maps.app.goo.gl/…"
|
||||
className="w-full rounded-[10px] border border-edge bg-surface-input px-3 py-2.5 text-content outline-none placeholder:text-content-faint focus:ring-2 focus:ring-[color:var(--text-primary)] disabled:opacity-50"
|
||||
style={fs(13, 'body')} /> : <>
|
||||
<DialogSection label={t('roadtrip.import.day')}>
|
||||
<CustomSelect value={target} onChange={value => { setTarget(String(value)); requestId.current = generateUUID() }}
|
||||
options={days.map(day => ({ value: String(day.id), label: day.title || `${t('roadtrip.import.day')} ${day.day_number}` }))} disabled={busy || saved} />
|
||||
<ol className="max-h-64 space-y-2 overflow-auto">
|
||||
{preview.stops.map((stop, index) => <li key={index} className="flex items-baseline gap-3 text-body text-content">
|
||||
<span className="text-caption tabular-nums text-content-muted">{index + 1}</span>
|
||||
<span className="min-w-0">{stop.name}{stop.name !== `${stop.lat}, ${stop.lng}` && <span className="block text-caption text-content-muted">{stop.lat == null || stop.lng == null ? t('roadtrip.import.unresolved') : `${stop.lat.toFixed(5)}, ${stop.lng.toFixed(5)}`}</span>}</span>
|
||||
</li>)}
|
||||
</ol>
|
||||
</>}
|
||||
{failure && <p role="alert" className="text-caption text-danger">{saved ? t('common.saved') + ': ' : ''}{failure}</p>}
|
||||
<div className="flex justify-end gap-2">
|
||||
<button type="button" disabled={busy} className="rounded-lg px-3 py-2 text-body text-content-secondary" onClick={() => setOpen(false)}>{t('common.cancel')}</button>
|
||||
<button type="button" disabled={busy || saved || (!preview ? !url.trim() : resolved.length < 2 || !target)} onClick={() => void (preview ? save() : inspect())}
|
||||
className="rounded-lg bg-accent px-4 py-2 text-body font-medium text-accent-text disabled:opacity-50">
|
||||
{busy ? t('common.loading') : t(preview ? 'common.import' : 'common.preview')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogSection>
|
||||
<ol className="m-0 flex max-h-64 list-none flex-col gap-2.5 overflow-auto rounded-[14px] border border-edge-faint bg-surface-secondary p-3.5">
|
||||
{preview.stops.map((stop, index) => <li key={index} className="flex items-baseline gap-3 text-content" style={fs(13, 'body')}>
|
||||
<span className="grid h-5 w-5 flex-none place-items-center rounded-full bg-surface-card font-semibold tabular-nums text-content-muted shadow-sm" style={fs(10.5)}>{index + 1}</span>
|
||||
<span className="min-w-0 break-words">{stop.name}{stop.name !== `${stop.lat}, ${stop.lng}` && <span className="block font-geist text-content-muted" style={fs(11.5)}>{stop.lat == null || stop.lng == null ? t('roadtrip.import.unresolved') : `${stop.lat.toFixed(5)}, ${stop.lng.toFixed(5)}`}</span>}</span>
|
||||
</li>)}
|
||||
</ol>
|
||||
</>}
|
||||
{failure && <p role="alert" className="m-0 text-caption text-danger">{saved ? t('common.saved') + ': ' : ''}{failure}</p>}
|
||||
</DialogShell>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { serviceColor } from './roadtripModel'
|
||||
import { alongLabel, offRouteLabel } from './corridorSearchModel'
|
||||
import { CORRIDOR_CATEGORY_BY_KEY, manualStopKindFor } from './stopKinds'
|
||||
import { FS } from './typeScale'
|
||||
import { fs } from '../shared/DialogShell'
|
||||
import { CORRIDOR_CATEGORY_KEYS, CORRIDOR_SECTION_KM, CORRIDOR_WIDTHS_KM, type RoadtripCorridor } from './useRoadtripCorridor'
|
||||
import type { RoadtripStopType } from '@trek/shared'
|
||||
import type { CorridorPoi } from './useCorridorPois'
|
||||
@@ -457,11 +458,12 @@ export default function RoadtripCorridorPanel({
|
||||
disabled={!canSearch}
|
||||
aria-label={t('roadtrip.poi.search')}
|
||||
title={narrow ? t('roadtrip.poi.search') : undefined}
|
||||
className="flex h-[32px] min-w-0 flex-1 items-center justify-center gap-2 rounded-lg bg-accent text-body font-semibold text-accent-text transition-opacity disabled:opacity-50"
|
||||
className="flex h-8 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[10px] bg-accent px-3 font-semibold text-accent-text transition-opacity enabled:hover:opacity-90 disabled:opacity-50"
|
||||
style={fs(12, 'body')}
|
||||
>
|
||||
{search.loading
|
||||
? <RotateCw size={15} className="shrink-0 animate-spin" aria-hidden />
|
||||
: <Search size={15} strokeWidth={2} className="shrink-0" aria-hidden />}
|
||||
? <RotateCw size={13} className="shrink-0 animate-spin" aria-hidden />
|
||||
: <Search size={13} strokeWidth={2.2} className="shrink-0" aria-hidden />}
|
||||
{/* The word stays put while a run is on, and the spinner in front of it says
|
||||
the run is on. It used to become "Searching 3 of 12", which is both the
|
||||
longest label in the panel and the very sentence the progress row right
|
||||
@@ -481,9 +483,10 @@ export default function RoadtripCorridorPanel({
|
||||
// the button carries the one word that fits beside "Search".
|
||||
aria-label={t('roadtrip.poi.addManual')}
|
||||
title={t('roadtrip.poi.addManual')}
|
||||
className="flex h-[32px] min-w-0 flex-1 items-center justify-center gap-2 rounded-lg border border-edge bg-surface-card text-body font-semibold text-content transition-colors hover:bg-surface-hover"
|
||||
className="flex h-8 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[10px] border border-edge bg-surface-card px-3 font-semibold text-content transition-colors hover:bg-surface-hover"
|
||||
style={fs(12, 'body')}
|
||||
>
|
||||
<Plus size={15} strokeWidth={2.2} className="shrink-0" aria-hidden />
|
||||
<Plus size={13} strokeWidth={2.2} className="shrink-0" aria-hidden />
|
||||
{narrow ? null : <span className="min-w-0 truncate">{t('roadtrip.poi.addManualShort')}</span>}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useId, useState } from 'react'
|
||||
import { AlertTriangle, Check, RotateCw, Spline, Trash2 } from 'lucide-react'
|
||||
import Modal from '../shared/Modal'
|
||||
import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell'
|
||||
import EmptyState from '../shared/EmptyState'
|
||||
import { useTranslation } from '../../i18n/TranslationContext'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { formatDistance } from '../../utils/units'
|
||||
import { FS } from './typeScale'
|
||||
import type { FollowTrack, TrackChoice } from './useFollowTrack'
|
||||
|
||||
/**
|
||||
@@ -24,8 +23,9 @@ import type { FollowTrack, TrackChoice } from './useFollowTrack'
|
||||
/** A track close enough that it is plainly about this day rather than another one. */
|
||||
const ON_THIS_DAY_KM = 2
|
||||
|
||||
function TrackRow({ track, disabled, onPick }: {
|
||||
function TrackRow({ track, picked, disabled, onPick }: {
|
||||
track: TrackChoice
|
||||
picked: boolean
|
||||
disabled: boolean
|
||||
onPick: () => void
|
||||
}): React.ReactElement {
|
||||
@@ -39,7 +39,10 @@ function TrackRow({ track, disabled, onPick }: {
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onPick}
|
||||
className="flex w-full items-center gap-3 rounded-xl px-2 py-2 text-start transition-colors hover:bg-surface-hover disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||
aria-pressed={picked}
|
||||
className={`flex w-full items-center gap-3 rounded-[10px] px-3 py-2.5 text-start transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent ${
|
||||
picked ? 'bg-surface-card shadow-sm ring-1 ring-edge-faint' : 'hover:bg-surface-card'
|
||||
}`}
|
||||
>
|
||||
{/* The line's own colour, as a short stroke rather than a dot: it is a line on the
|
||||
map, and a dot beside a name reads as a place. */}
|
||||
@@ -50,10 +53,10 @@ function TrackRow({ track, disabled, onPick }: {
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate font-semibold text-content" style={{ fontSize: FS.name }}>
|
||||
<span className="block truncate font-semibold text-content" style={fs(13, 'body')}>
|
||||
{track.name || t('roadtrip.track.untitled')}
|
||||
</span>
|
||||
<span className="block truncate tabular-nums text-content-faint" style={{ fontSize: FS.meta }}>
|
||||
<span className="block truncate font-geist tabular-nums text-content-faint" style={fs(11)}>
|
||||
{t('roadtrip.track.length', { distance: formatDistance(track.lengthKm, distanceUnit) })}
|
||||
{' · '}
|
||||
{onDay
|
||||
@@ -61,6 +64,7 @@ function TrackRow({ track, disabled, onPick }: {
|
||||
: t('roadtrip.track.gap', { distance: formatDistance(track.gapKm, distanceUnit) })}
|
||||
</span>
|
||||
</span>
|
||||
{picked ? <Check size={15} className="shrink-0 text-content" aria-hidden /> : null}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
@@ -74,6 +78,7 @@ export default function RoadtripTrackModal({ follow, dayNumber }: {
|
||||
const { t } = useTranslation()
|
||||
const distanceUnit = useSettingsStore(s => s.settings.distance_unit)
|
||||
const [picked, setPicked] = useState<number | null>(null)
|
||||
const labelId = useId()
|
||||
|
||||
const { dayId, busy, round, error, outcome, tracks, viaCount, current } = follow
|
||||
|
||||
@@ -85,109 +90,99 @@ export default function RoadtripTrackModal({ follow, dayNumber }: {
|
||||
const chosen = tracks.find(track => track.id === picked) ?? null
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
<DialogShell
|
||||
onClose={follow.close}
|
||||
size="sm"
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<Spline size={15} className="text-content-faint" aria-hidden />
|
||||
{t('roadtrip.track.title', { number: dayNumber })}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* What the day follows today, before anything is picked. Read back from the day
|
||||
rather than remembered from the run that applied it, which is the whole reason
|
||||
it is stored: it has to survive a reload. */}
|
||||
{current ? (
|
||||
<p className="rounded-xl bg-surface-secondary px-3 py-2 text-caption text-content-secondary">
|
||||
{t('roadtrip.track.current', { name: current.name })}
|
||||
</p>
|
||||
) : null}
|
||||
{tracks.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2 py-2">
|
||||
<EmptyState scene="transport" size={96} compact title={t('roadtrip.track.empty')} />
|
||||
<p className="text-center text-caption text-content-faint">{t('roadtrip.track.emptyHint')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="-mx-2 max-h-[280px] overflow-y-auto">
|
||||
{tracks.map(track => (
|
||||
<TrackRow
|
||||
key={track.id}
|
||||
track={track}
|
||||
disabled={busy}
|
||||
onPick={() => setPicked(track.id === picked ? null : track.id)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* What the pick will do, before it is done. The count is the honest part: a track
|
||||
becomes a dozen waypoints on the day, and a traveller who later wonders where
|
||||
they came from should have been told here. */}
|
||||
{chosen ? (
|
||||
<p className="rounded-xl bg-surface-secondary px-3 py-2 text-caption text-content-secondary">
|
||||
{t('roadtrip.track.explain')}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{busy ? (
|
||||
<p className="flex items-center gap-2 text-caption text-content-secondary" role="status">
|
||||
<RotateCw size={13} className="animate-spin shrink-0" aria-hidden />
|
||||
{t('roadtrip.track.working', { round })}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="flex items-start gap-1.5 text-caption text-danger" role="alert">
|
||||
<AlertTriangle size={13} className="mt-px shrink-0" aria-hidden />
|
||||
{t(error === 'route' ? 'roadtrip.track.errorRoute' : 'roadtrip.track.errorSave')}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{outcome && !busy && !error ? (
|
||||
<p className="flex items-start gap-1.5 text-caption text-content-secondary" role="status">
|
||||
<Check size={13} className="mt-px shrink-0 text-success" aria-hidden />
|
||||
{outcome.vias === 0
|
||||
? t('roadtrip.track.already')
|
||||
: outcome.capped
|
||||
? t('roadtrip.track.capped', {
|
||||
count: outcome.vias,
|
||||
distance: formatDistance(outcome.strayKm, distanceUnit),
|
||||
})
|
||||
: t('roadtrip.track.done', {
|
||||
count: outcome.vias,
|
||||
distance: formatDistance(outcome.strayKm, distanceUnit),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
labelledBy={labelId}
|
||||
width="narrow"
|
||||
header={(
|
||||
<DialogHeader
|
||||
tile={<DialogTile><Spline size={20} strokeWidth={1.9} className="text-content-muted" /></DialogTile>}
|
||||
tint={NEUTRAL_TINT}
|
||||
labelId={labelId}
|
||||
onClose={follow.close}
|
||||
title={t('roadtrip.track.title', { number: dayNumber })}
|
||||
// What the day follows today, before anything is picked. Read back from the day
|
||||
// rather than remembered from the run that applied it, which is the whole reason
|
||||
// it is stored: it has to survive a reload.
|
||||
sub={current ? t('roadtrip.track.current', { name: current.name }) : undefined}
|
||||
subWraps
|
||||
/>
|
||||
)}
|
||||
footer={(
|
||||
<DialogFooter>
|
||||
{/* Offered whenever the day carries vias, whether this dialog put them there or
|
||||
a dragged handle did: "put it back the way the router had it" is the same
|
||||
wish either way. */}
|
||||
{viaCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => { void follow.clear() }}
|
||||
className="flex items-center gap-1.5 rounded-lg px-2.5 py-2 text-caption text-content-secondary transition-colors hover:bg-surface-hover disabled:opacity-50"
|
||||
>
|
||||
<Trash2 size={13} aria-hidden />
|
||||
<DialogButton disabled={busy} onClick={() => { void follow.clear() }} icon={<Trash2 size={13} aria-hidden />}>
|
||||
{t('roadtrip.track.clear', { count: viaCount })}
|
||||
</button>
|
||||
</DialogButton>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!chosen || busy}
|
||||
onClick={() => { if (chosen) void follow.apply(chosen.id) }}
|
||||
className="ms-auto rounded-lg bg-accent px-3 py-2 text-caption font-semibold text-accent-text transition-opacity hover:opacity-90 disabled:opacity-40"
|
||||
>
|
||||
<FooterSpacer />
|
||||
<DialogButton variant="primary" disabled={!chosen || busy} onClick={() => { if (chosen) void follow.apply(chosen.id) }}>
|
||||
{t('roadtrip.track.action')}
|
||||
</button>
|
||||
</DialogButton>
|
||||
</DialogFooter>
|
||||
)}
|
||||
>
|
||||
{tracks.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2 rounded-[14px] border border-edge-faint bg-surface-secondary px-4 py-5">
|
||||
<EmptyState scene="transport" size={96} compact title={t('roadtrip.track.empty')} />
|
||||
<p className="m-0 text-center text-content-faint" style={fs(12, 'body')}>{t('roadtrip.track.emptyHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : (
|
||||
<ul className="m-0 flex max-h-[280px] list-none flex-col gap-0.5 overflow-y-auto rounded-[14px] border border-edge-faint bg-surface-secondary p-1.5">
|
||||
{tracks.map(track => (
|
||||
<TrackRow
|
||||
key={track.id}
|
||||
track={track}
|
||||
picked={track.id === picked}
|
||||
disabled={busy}
|
||||
onPick={() => setPicked(track.id === picked ? null : track.id)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* What the pick will do, before it is done. The count is the honest part: a track
|
||||
becomes a dozen waypoints on the day, and a traveller who later wonders where
|
||||
they came from should have been told here. */}
|
||||
{chosen ? (
|
||||
<p className="m-0 rounded-[12px] bg-surface-secondary px-3 py-2.5 text-content-secondary" style={fs(12, 'body')}>
|
||||
{t('roadtrip.track.explain')}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{busy ? (
|
||||
<p className="m-0 flex items-center gap-2 text-content-secondary" style={fs(12, 'body')} role="status">
|
||||
<RotateCw size={13} className="animate-spin shrink-0" aria-hidden />
|
||||
{t('roadtrip.track.working', { round })}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="m-0 flex items-start gap-1.5 rounded-[12px] bg-danger-soft px-3 py-2.5 text-danger" style={fs(12, 'body')} role="alert">
|
||||
<AlertTriangle size={13} className="mt-0.5 shrink-0" aria-hidden />
|
||||
{t(error === 'route' ? 'roadtrip.track.errorRoute' : 'roadtrip.track.errorSave')}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{outcome && !busy && !error ? (
|
||||
<p className="m-0 flex items-start gap-1.5 text-content-secondary" style={fs(12, 'body')} role="status">
|
||||
<Check size={13} className="mt-0.5 shrink-0 text-success" aria-hidden />
|
||||
{outcome.vias === 0
|
||||
? t('roadtrip.track.already')
|
||||
: outcome.capped
|
||||
? t('roadtrip.track.capped', {
|
||||
count: outcome.vias,
|
||||
distance: formatDistance(outcome.strayKm, distanceUnit),
|
||||
})
|
||||
: t('roadtrip.track.done', {
|
||||
count: outcome.vias,
|
||||
distance: formatDistance(outcome.strayKm, distanceUnit),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</DialogShell>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user