mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
refactor(tours): bring the tour time parts, collection rows and flyover back under the lint baseline
Move workedOutEndHint and totalText into tourDayTimeHint.ts so TourDayTimeParts only exports components, render the collection row's type icon without a component made during render, and pass the chase camera's lens and scene as one view object.
This commit is contained in:
@@ -4,7 +4,8 @@ import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { render, screen } from '../../../tests/helpers/render';
|
||||
import { seedStore } from '../../../tests/helpers/store';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { TourDayTimeText, TourEndHint, TourOverrunLine, workedOutEndHint } from './TourDayTimeParts';
|
||||
import { TourDayTimeText, TourEndHint, TourOverrunLine } from './TourDayTimeParts';
|
||||
import { workedOutEndHint } from './tourDayTimeHint';
|
||||
|
||||
const tour: TourListItem = {
|
||||
place_id: 7,
|
||||
@@ -107,7 +108,9 @@ describe('TourOverrunLine', () => {
|
||||
|
||||
it('TIME-02-014b: an item without a name is called the next item', () => {
|
||||
render(<TourOverrunLine overrun={{ name: '', time: '12:00', end: '13:00' }} />);
|
||||
expect(screen.getByTestId('tour-day-overrun')).toHaveTextContent('Ends at 13:00, after the next item starts at 12:00');
|
||||
expect(screen.getByTestId('tour-day-overrun')).toHaveTextContent(
|
||||
'Ends at 13:00, after the next item starts at 12:00'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,22 +7,13 @@ import { formatTime } from '../../utils/formatters';
|
||||
import { fs } from '../shared/DialogShell';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import { tourDayTimeOf, type TourDayEntry, type TourDayTime, type TourOverrun } from './tourDayTime';
|
||||
import { formatPlannedTourDuration, tourDayMinutes, tourPlannedTimes } from './tourPresentation';
|
||||
import { totalText, workedOutEndHint, type Translate } from './tourDayTimeHint';
|
||||
|
||||
/**
|
||||
* TIME-02: how the day plan says a tour's time on a day, on the desktop and
|
||||
* the phone alike; each only places it in its own badge or chip.
|
||||
*/
|
||||
|
||||
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
||||
|
||||
/** A total as said beside a worked-out end: `≈` for an estimate, `≥` where a stay is not known yet. */
|
||||
function totalText(time: Pick<TourDayTime, 'totalMinutes' | 'estimated' | 'atLeast'>): string {
|
||||
const total = formatPlannedTourDuration(time.totalMinutes ?? 0);
|
||||
if (time.estimated) return `≈ ${total}`;
|
||||
return time.atLeast ? `≥ ${total}` : total;
|
||||
}
|
||||
|
||||
/** The words a worked-out end is explained by: when it ends and the total it follows from. */
|
||||
function workedOutLabel(time: TourDayTime, t: Translate, clock: (value: string) => string): string {
|
||||
const params = { time: clock(time.end ?? ''), total: totalText(time) };
|
||||
@@ -97,29 +88,6 @@ export function TourOverrunLine({ overrun }: { overrun: TourOverrun | null | und
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TIME-02: what the time fields of a tour's day entry say below them, while
|
||||
* no end is typed: the end the start and the tour's time work out to, or,
|
||||
* before a start is picked, that one will. Nothing where the tour has no
|
||||
* known time or the day holds one stage of a multi-day tour.
|
||||
*/
|
||||
export function workedOutEndHint(
|
||||
entry: TourDayEntry,
|
||||
tour: TourListItem | null | undefined,
|
||||
t: Translate,
|
||||
clock: (value: string) => string
|
||||
): string | null {
|
||||
if (!tour || entry.stageKey || (entry.end ?? '') !== '') return null;
|
||||
const time = tourDayTimeOf(entry, tour);
|
||||
if (time?.workedOut && time.end) {
|
||||
return t('tours.dayTime.formHint', { time: clock(time.end), total: totalText(time) });
|
||||
}
|
||||
const total = tourDayMinutes(tourPlannedTimes(tour));
|
||||
if (time || !total) return null;
|
||||
const said = totalText({ totalMinutes: total.minutes, estimated: total.estimated, atLeast: total.atLeast });
|
||||
return t('tours.dayTime.formNoStart', { total: said });
|
||||
}
|
||||
|
||||
/**
|
||||
* TIME-02: the hint below the time fields of a tour's day entry, on the
|
||||
* desktop form and the phone sheet alike, read while the fields are typed in.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionTourItem } from '@trek/shared';
|
||||
import { AlertTriangle, CopyPlus, Gauge, Moon, Route, Trash2, Waypoints, WifiOff } from 'lucide-react';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { createElement, type MouseEvent } from 'react';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { useSettingsStore } from '../../../store/settingsStore';
|
||||
import { WhiteBadge } from '../../Planner/planParts';
|
||||
@@ -68,7 +68,6 @@ function KeptTourRow({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tile = { ...tour, has_waypoints: tour.waypoint_count > 0 };
|
||||
const TypeIcon = tourDayRowIcon(tour.tour_type);
|
||||
const points = t('tours.collection.points', { count: tour.waypoint_count });
|
||||
const stop = (act: () => void) => (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
@@ -94,7 +93,7 @@ function KeptTourRow({
|
||||
<div className="li">
|
||||
<div className="t">{tour.name}</div>
|
||||
<div className="s">
|
||||
<TypeIcon size={11} />
|
||||
{createElement(tourDayRowIcon(tour.tour_type), { size: 11 })}
|
||||
<span>{t(`tourTypes.${tour.tour_type}`)}</span>
|
||||
<Tooltip label={sourceText(tour, t)}>
|
||||
<span className="col-lrow-from">
|
||||
|
||||
@@ -70,14 +70,19 @@ function followGestures(map: MapLibreMap): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
/** How the chase camera frames the route: the lens the user picked and the scene it looks into. */
|
||||
interface ChaseView {
|
||||
lens: ChaseLens;
|
||||
scene: TerrainScene;
|
||||
}
|
||||
|
||||
/** Moves the chase camera to `distance` along the route and returns where that is, if anywhere. */
|
||||
function chase(
|
||||
map: MapLibreMap,
|
||||
samples: DistanceIndexedProfileSample[],
|
||||
distance: number,
|
||||
bearing: RefObject<number>,
|
||||
lens: ChaseLens,
|
||||
scene: TerrainScene
|
||||
{ lens, scene }: ChaseView
|
||||
): RouteProfileFocus | null {
|
||||
const here = focusRouteProfileAtDistance(samples, distance);
|
||||
if (!here) return null;
|
||||
@@ -155,7 +160,10 @@ export function useTerrainReplay({ gl, ready, samples, focus, onFocusChange, sce
|
||||
totalMeters,
|
||||
distanceRef.current + replayMetresPerSecond(totalMeters) * speed * dt
|
||||
);
|
||||
const here = chase(map, samples, distanceRef.current, bearingRef, lensRef.current, scene.current);
|
||||
const here = chase(map, samples, distanceRef.current, bearingRef, {
|
||||
lens: lensRef.current,
|
||||
scene: scene.current,
|
||||
});
|
||||
if (here) {
|
||||
riderRef.current?.setLngLat([here.lng, here.lat]).addTo(map);
|
||||
if (now - lastFocus >= FOCUS_INTERVAL_MS || distanceRef.current >= totalMeters) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { TourListItem } from '@trek/shared';
|
||||
import { tourDayTimeOf, type TourDayEntry, type TourDayTime } from './tourDayTime';
|
||||
import { formatPlannedTourDuration, tourDayMinutes, tourPlannedTimes } from './tourPresentation';
|
||||
|
||||
/** TIME-02: the words the day plan says a tour's worked-out time in, apart from the components showing them. */
|
||||
|
||||
export type Translate = (key: string, params?: Record<string, unknown>) => string;
|
||||
|
||||
/** A total as said beside a worked-out end: `≈` for an estimate, `≥` where a stay is not known yet. */
|
||||
export function totalText(time: Pick<TourDayTime, 'totalMinutes' | 'estimated' | 'atLeast'>): string {
|
||||
const total = formatPlannedTourDuration(time.totalMinutes ?? 0);
|
||||
if (time.estimated) return `≈ ${total}`;
|
||||
return time.atLeast ? `≥ ${total}` : total;
|
||||
}
|
||||
|
||||
/**
|
||||
* TIME-02: what the time fields of a tour's day entry say below them, while
|
||||
* no end is typed: the end the start and the tour's time work out to, or,
|
||||
* before a start is picked, that one will. Nothing where the tour has no
|
||||
* known time or the day holds one stage of a multi-day tour.
|
||||
*/
|
||||
export function workedOutEndHint(
|
||||
entry: TourDayEntry,
|
||||
tour: TourListItem | null | undefined,
|
||||
t: Translate,
|
||||
clock: (value: string) => string
|
||||
): string | null {
|
||||
if (!tour || entry.stageKey || (entry.end ?? '') !== '') return null;
|
||||
const time = tourDayTimeOf(entry, tour);
|
||||
if (time?.workedOut && time.end) {
|
||||
return t('tours.dayTime.formHint', { time: clock(time.end), total: totalText(time) });
|
||||
}
|
||||
const total = tourDayMinutes(tourPlannedTimes(tour));
|
||||
if (time || !total) return null;
|
||||
const said = totalText({ totalMinutes: total.minutes, estimated: total.estimated, atLeast: total.atLeast });
|
||||
return t('tours.dayTime.formNoStart', { total: said });
|
||||
}
|
||||
Reference in New Issue
Block a user