feat: add passenger information and settings components
- Introduced `PassengerInfo.tsx` for displaying trip details, including upcoming trips and fare information. - Added `PassengerSettings.tsx` for managing user-defined important places and fare settings. - Implemented validation functions for passenger settings in `passenger-settings.mjs`. - Created utility functions for transliterating stop names in `transliteration.ts`. - Developed weather hint functionality in `weather-hint.ts` to provide precipitation probabilities. - Established a mock server for testing passenger-related features in `passenger-server.mjs`. - Added comprehensive tests for passenger guide logic and transliteration functionality.
This commit is contained in:
@@ -1,17 +1,16 @@
|
||||
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { NumericText } from './NumericText';
|
||||
import { RouteBadge } from './RouteBadge';
|
||||
import { FittedDestination } from './FittedDestination';
|
||||
import { localDate } from './lib/timetable.mjs';
|
||||
import { durationParts } from './lib/presentation.mjs';
|
||||
import { readability } from './lib/departures';
|
||||
import { transliterateStopName } from './lib/transliteration';
|
||||
import type { OnwardJourney } from './lib/journey';
|
||||
import type { Departure, Settings } from './types';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
// Fixed-size cells move on the compositor. Both text sizes are fitted once,
|
||||
// so moving to the next stop never resizes a cell or remounts its heading.
|
||||
const JourneyRail = memo(function JourneyRail({ journey, focus }: { journey: OnwardJourney; focus: number }) {
|
||||
const JourneyRail = memo(function JourneyRail({ journey, focus, translit }: { journey: OnwardJourney; focus: number; translit: boolean }) {
|
||||
const rail = useRef<HTMLDivElement>(null);
|
||||
const [geometry, setGeometry] = useState({ slot: 0, focused: 0, width: 0 });
|
||||
useLayoutEffect(() => {
|
||||
@@ -38,27 +37,32 @@ const JourneyRail = memo(function JourneyRail({ journey, focus }: { journey: Onw
|
||||
'--journey-name-scale': geometry.focused > 0 ? geometry.slot / geometry.focused : .4 } as CSSProperties}>
|
||||
<span className="journey-line" aria-hidden="true" style={{ transform: `scaleX(${scale})` }} />
|
||||
<ol className="journey-rail" aria-label="Следующие остановки после посадки">
|
||||
{journey.stops.map((stop, index) => <li className="journey-station" key={stop.id} data-focused={index === focus} data-before={index < focus} data-after={index > focus} aria-current={index === focus ? 'step' : undefined}>
|
||||
<span className="journey-duration" aria-label={stop.minutes === null ? 'Время пути неизвестно' : `В пути ${stop.minutes} мин по расписанию`}>
|
||||
{stop.minutes === null ? '—' : stop.minutes === 0 ? <><span><1</span><small>мин</small></> : durationParts(stop.minutes).map(part => <span key={part.unit}><NumericText value={part.value} /><small>{part.unit}</small></span>)}
|
||||
</span>
|
||||
<div className="journey-station-name">
|
||||
<div className="journey-name-layer journey-name-context" aria-hidden={index === focus}><FittedDestination text={stop.name} /></div>
|
||||
<div className="journey-name-layer journey-name-focus" aria-hidden={index !== focus}><FittedDestination text={stop.name} /></div>
|
||||
</div>
|
||||
<span className="journey-node" aria-hidden="true" />
|
||||
</li>)}
|
||||
{journey.stops.map((stop, index) => {
|
||||
const latinName = translit ? transliterateStopName(stop.name) : stop.name;
|
||||
return <li className="journey-station" key={stop.id} data-focused={index === focus} data-before={index < focus} data-after={index > focus} aria-current={index === focus ? 'step' : undefined}>
|
||||
<span className="journey-duration" aria-label={stop.minutes === null ? 'Время пути неизвестно' : `В пути ${stop.minutes} мин по расписанию`}>
|
||||
{stop.minutes === null ? '—' : stop.minutes === 0 ? <><span><1</span><small>мин</small></> : durationParts(stop.minutes).map(part => <span key={part.unit}><NumericText value={part.value} /><small>{part.unit}</small></span>)}
|
||||
</span>
|
||||
<div className="journey-station-name">
|
||||
{(['context', 'focus'] as const).map(size => <div key={size} className={`journey-name-layer journey-name-${size}`} aria-hidden={size === 'focus' ? index !== focus : index === focus}>
|
||||
{latinName !== stop.name && <span className="journey-transliteration" lang="ru-Latn" aria-hidden="true">{latinName}</span>}
|
||||
<FittedDestination key={translit ? 'bilingual' : 'native'} text={stop.name} />
|
||||
</div>)}
|
||||
</div>
|
||||
<span className="journey-node" aria-hidden="true" />
|
||||
</li>;
|
||||
})}
|
||||
{journey.more > 0 && <li className="journey-continuation"><span className="journey-continuation-label">Ещё {journey.more} остановок</span><span className="journey-continuation-line" aria-hidden="true" /></li>}
|
||||
</ol>
|
||||
</div>;
|
||||
}, (previous, next) => previous.focus === next.focus && previous.journey.more === next.journey.more
|
||||
}, (previous, next) => previous.focus === next.focus && previous.translit === next.translit && previous.journey.more === next.journey.more
|
||||
&& previous.journey.stops.length === next.journey.stops.length && previous.journey.stops.every((stop, index) => {
|
||||
const other = next.journey.stops[index];
|
||||
return stop.id === other.id && stop.name === other.name && stop.minutes === other.minutes;
|
||||
}));
|
||||
|
||||
export function NextStops({ departure, journey, loading, now, paused, settings, active, onPages }: {
|
||||
departure?: Departure; journey: OnwardJourney | null; loading: boolean; now: number; paused: boolean; settings: Settings;
|
||||
export function NextStops({ departure, journey, loading, paused, settings, active, onPages }: {
|
||||
departure?: Departure; journey: OnwardJourney | null; loading: boolean; paused: boolean; settings: Settings;
|
||||
active: boolean; onPages: (pages: number) => void;
|
||||
}) {
|
||||
const [page, setPage] = useState(0);
|
||||
@@ -73,14 +77,12 @@ export function NextStops({ departure, journey, loading, now, paused, settings,
|
||||
const timer = setInterval(() => setPage(value => value + 1), reading.calm ? 6000 : 4000);
|
||||
return () => clearInterval(timer);
|
||||
}, [paused, active, pages, reading.calm]);
|
||||
const tomorrow = departure && localDate(Date.parse(departure.scheduledAt)) > localDate(now);
|
||||
return <><h3>Дальше по маршруту</h3>
|
||||
return <><h3>Дальше по ближайшему рейсу</h3>
|
||||
{!departure ? <p className="journey-empty">{loading ? 'Получаем расписание' : 'Нет доступного рейса'}</p> : <>
|
||||
<div className="journey-route"><RouteBadge number={departure.routeNumber} filled /><div className="journey-trip"><div className="journey-route-name"><FittedDestination text={departure.destination} /></div><time dateTime={departure.scheduledAt}><NumericText value={departure.scheduledTime} />{tomorrow && <span className="journey-day">завтра</span>}</time></div></div>
|
||||
{!journey ? <p className="journey-empty">{loading ? 'Загружаем остановки' : 'Схема маршрута пока недоступна'}</p> : !count ? <p className="journey-empty">Здесь конечная этого рейса</p> : <>
|
||||
<div className="journey-metro" data-active={active} data-calm={reading.calm} data-loop={page > 0 && focus === 0}>
|
||||
<div className="journey-tour-heading"><span>{settings.demo ? 'Демонстрация поездки' : 'После посадки'}</span><span className="journey-order">{focus + 1}-я остановка</span></div>
|
||||
<div className="journey-map-space"><JourneyRail journey={journey} focus={focus} /></div>
|
||||
<div className="journey-map-space"><JourneyRail journey={journey} focus={focus} translit={settings.journeyTranslitEnabled} /></div>
|
||||
</div>
|
||||
<div className="journey-caption"><span className="journey-caption-full">Время пути по расписанию</span><span className="journey-caption-compact">После посадки · по расписанию</span>{pages > 1 && <span className="journey-page" aria-label={`${focus + 1} из ${count} остановок в обзоре`}>{focus + 1} / {count}</span>}</div>
|
||||
</>}
|
||||
|
||||
Reference in New Issue
Block a user