feat: add service window component and related functionality
- 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.
This commit is contained in:
1
test/fixtures/board-features.html
vendored
Normal file
1
test/fixtures/board-features.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>RideBus — проверка режимов</title></head><body><div id="root"></div><script type="module" src="./board-features.tsx"></script></body></html>
|
||||
61
test/fixtures/board-features.tsx
vendored
Normal file
61
test/fixtures/board-features.tsx
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
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 />);
|
||||
Reference in New Issue
Block a user