- Introduced ServiceWindow component to display the last departure today and the first tomorrow. - Implemented cancellation notice parsing to handle route cancellations effectively. - Enhanced departure display logic to include cancellation alerts and status updates. - Created information blocks for displaying service information, weather, and QR codes. - Added tests for departure features, information handling, and service window functionality. - Updated fixtures for testing various scenarios including cancellations and long wait times.
62 lines
7.2 KiB
TypeScript
62 lines
7.2 KiB
TypeScript
import { useState } from 'react';
|
||
import { createRoot } from 'react-dom/client';
|
||
import '@fontsource-variable/manrope';
|
||
import '../../src/styles.css';
|
||
import { DeparturesScreen } from '../../src/App';
|
||
import { defaultSettings } from '../../src/lib/storage';
|
||
import { displayDepartures, readability } from '../../src/lib/departures';
|
||
import { normalizeAlerts } from '../../server/board.mjs';
|
||
import { colorScheme } from '../../src/lib/presentation.mjs';
|
||
import type { BoardData, Departure, Settings } from '../../src/types';
|
||
import type { CSSProperties } from 'react';
|
||
|
||
const stop = { id: 1, name: 'Швейная фабрика', direction: '' };
|
||
function Fixture() {
|
||
const [scenario, setScenario] = useState('cancellation');
|
||
const [settings, setSettings] = useState<Settings>({ ...defaultSettings, rowCount: 6 });
|
||
const [paused, setPaused] = useState(true);
|
||
const [approaching, setApproaching] = useState(false);
|
||
const tomorrow = scenario === 'tomorrow';
|
||
const distant = tomorrow || scenario === 'today';
|
||
const baseNow = Date.parse(tomorrow ? '2026-10-09T23:20:00+03:00' : '2026-10-09T12:00:00+03:00');
|
||
const routeNumbers = ['6', '1', '6', '2тк', '1', '17тк', '21тк', '6'];
|
||
const minutes = distant ? [125, 140, 165, 185, 195, 210, 220, 240] : [4, 8, 12, 17, 23, 31, 38, 44];
|
||
const departures: Departure[] = minutes.map((minutes, index) => {
|
||
const at = tomorrow ? Date.parse('2026-10-10T05:40:00+03:00') + (minutes - 125) * 60000 : baseNow + minutes * 60000;
|
||
const number = routeNumbers[index];
|
||
const destination = number === '1' ? 'Богатырская' : number === '6' ? 'Автовокзал' : number === '2тк' ? 'Больница (Новка) (через Аэропорт)' : 'Тросницкая';
|
||
return { id: String(index), routeId: index === 3 ? 20 : Number.parseInt(number), routeNumber: number,
|
||
routeName: `Швейная фабрика — ${destination}`, destination, type: number.includes('тк') ? 'minibus' : 'bus', scheduledAt: new Date(at).toISOString(),
|
||
scheduledTime: new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', hour: '2-digit', minute: '2-digit' }).format(at), note: '', source: 'schedule' };
|
||
});
|
||
const now = approaching ? Date.parse(departures[0].scheduledAt) - 10 * 60000 : baseNow;
|
||
const routes = [...new Map(departures.map(item => [item.routeId, { id: item.routeId, number: item.routeNumber, name: item.routeName, type: item.type }])).values()];
|
||
const all = scenario === 'all-cancelled';
|
||
const message = all ? `09.10.2026: не выполняются рейсы:\n${routes.map(route => `Маршрут №${route.number}:\nШвейная фабрика — ${departures.filter(item => item.routeId === route.id).map(item => item.scheduledTime).join(', ')}`).join('\n')}`
|
||
: '09.10.2026: не выполняются рейсы:\nШвейная фабрика — 12:04';
|
||
const alerts = distant ? [] : normalizeAlerts([{ id: 1, city_id: 1, route_numbers: all ? routes.map(item => item.number) : ['6'], alert_type: 'cancellation', title: 'Отмена отдельных рейсов', message, start_time: '2026-10-09T00:00:00+03:00', end_time: '2026-10-10T00:00:00+03:00' }], routes, 1, now, [stop]);
|
||
const data: BoardData = { cityId: 1, cityName: 'Полоцк', stop, departures, routes, alerts, alertsAvailable: true, partial: false, updatedAt: new Date(now).toISOString(), serviceDate: '2026-10-09', mode: 'demo' };
|
||
const reading = readability(settings);
|
||
const update = <K extends keyof Settings>(key: K, value: Settings[K]) => setSettings(previous => ({ ...previous, [key]: value }));
|
||
return <div className="board" data-theme={settings.theme} data-profile={settings.displayProfile} data-distance={settings.viewingDistance} data-row-count={settings.rowCount} data-motion={reading.calm ? 'calm' : 'full'} style={{ '--reading-scale': reading.scale, '--accent': colorScheme(settings.theme === 'light' ? settings.lightColorScheme : settings.colorScheme, settings.theme).color } as CSSProperties}>
|
||
<header className="board-header"><h1>{stop.name}</h1><div className="header-right"><div className="clock">{new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', hour: '2-digit', minute: '2-digit' }).format(now)}</div></div></header>
|
||
<main className="scene-container"><div className="scene"><DeparturesScreen data={data} departures={displayDepartures(departures, alerts, 1, now, true)} now={now} alerts={alerts} loading={false} rowCount={settings.rowCount} fresh error="" paused={paused} settings={settings} /></div></main>
|
||
<footer className="board-footer" style={{ position: 'relative', minHeight: 48, paddingBlock: 12 }}>
|
||
<details><summary style={{ cursor: 'pointer', color: 'var(--muted)', fontSize: 14 }}>Тестовые данные · настройки проверки</summary>
|
||
<div style={{ position: 'absolute', bottom: 48, left: 0, right: 0, padding: 20, border: '1px solid var(--outline)', borderRadius: 16, background: 'var(--input)', zIndex: 10, display: 'flex', flexWrap: 'wrap', gap: 16 }}>
|
||
<label>Сценарий<select value={scenario} onChange={event => { setScenario(event.target.value); setApproaching(false); }}><option value="cancellation">Отмена</option><option value="all-cancelled">Все отменены</option><option value="today">Долгое ожидание сегодня</option><option value="tomorrow">Долгое ожидание завтра</option></select></label>
|
||
<label>Профиль<select value={settings.displayProfile} onChange={event => update('displayProfile', event.target.value as Settings['displayProfile'])}><option value="tv">Телевизор</option><option value="street">Уличное табло</option></select></label>
|
||
<label>Расстояние<select value={settings.viewingDistance} onChange={event => update('viewingDistance', event.target.value as Settings['viewingDistance'])}><option value="near">Близко</option><option value="medium">Средне</option><option value="far">Далеко</option></select></label>
|
||
<label>Тема<select value={settings.theme} onChange={event => update('theme', event.target.value as Settings['theme'])}><option value="dark">Тёмная</option><option value="light">Светлая</option></select></label>
|
||
<label>Строки<select value={settings.rowCount} onChange={event => update('rowCount', Number(event.target.value))}>{[2,4,6].map(value => <option key={value} value={value}>{value}</option>)}</select></label>
|
||
<label>Следующий рейс<input type="checkbox" checked={settings.showNextSameRoute} onChange={event => update('showNextSameRoute', event.target.checked)} /></label>
|
||
<label>Длительное ожидание<input type="checkbox" checked={settings.longWaitEnabled} onChange={event => update('longWaitEnabled', event.target.checked)} /></label>
|
||
<label>До рейса 10 минут<input type="checkbox" checked={approaching} onChange={event => setApproaching(event.target.checked)} /></label>
|
||
<label>Смена страниц<input type="checkbox" checked={!paused} onChange={event => setPaused(!event.target.checked)} /></label>
|
||
</div>
|
||
</details>
|
||
</footer>
|
||
</div>;
|
||
}
|
||
createRoot(document.getElementById('root')!).render(<Fixture />);
|