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:
2026-10-12 20:30:49 +03:00
parent 823ab14367
commit e239de45dd
34 changed files with 687 additions and 68 deletions

50
test/fixtures/passenger-server.mjs vendored Normal file
View File

@@ -0,0 +1,50 @@
// Explicit local UI fixture. No production database, telemetry or transit API.
import express from 'express';
import { fileURLToPath } from 'node:url';
import { createApp } from '../../server/index.mjs';
import { createDeviceBridge } from '../../server/device.mjs';
import { defaultSettings } from '../../src/lib/storage.ts';
import { localDate, timetableDepartures } from '../../src/lib/timetable.mjs';
let firstType = 'bus', started = Date.now();
const stop = { id: 1, name: 'Тестовая остановка', direction: 'Фиктивное расписание' };
const targets = [{ id: 901, name: 'Больница', sequence: 1, offsetMinutes: 12 }, { id: 902, name: 'Вокзал', sequence: 2, offsetMinutes: 20 }, { id: 903, name: 'Центр', sequence: 3, offsetMinutes: 28 }];
function resources() {
const now = Date.now(), start = Math.floor(started / 60000) * 60000;
const t = { version: 1, cityId: 1, cityName: 'Полоцк', stop, routes: ['bus', 'minibus'].map((type, i) => {
const at = new Date(start + (type === firstType ? 20 : 40) * 60000);
const civil = new Date(at.getTime() + 10800000);
const seconds = civil.getUTCHours() * 3600 + civil.getUTCMinutes() * 60 + (localDate(at) > localDate(start) ? 86400 : 0);
return { id: i + 1, number: type === 'bus' ? '1' : '2тк', name: 'Тестовая остановка — Центр', type,
stops: [{ id: 1, name: stop.name, sequence: 0, offsetMinutes: 0 }, ...targets],
schedules: ['weekday', 'saturday', 'sunday'].map(dayType => ({ dayType, validFrom: null, validUntil: null,
trips: [{ arrivalSeconds: seconds, originStopId: 1, endStopId: 903, originTime: `${String(Math.floor(seconds / 3600) % 24).padStart(2, '0')}:${String(Math.floor(seconds % 3600 / 60)).padStart(2, '0')}`,
destination: 'Центр', note: 'Тестовые данные', daysOfWeek: [], exceptDaysOfWeek: [] }] })) };
}), calendarYears: [2026, 2027], holidays: [], updatedAt: new Date(now).toISOString(), expiresAt: new Date(now + 86400000).toISOString() };
const board = { cityId: 1, cityName: 'Полоцк', stop, routes: t.routes.map(({ stops, schedules, ...route }) => route),
departures: timetableDepartures(t, localDate(now)).filter(trip => Date.parse(trip.scheduledAt) + 60000 > now), alerts: [], alertsAvailable: true, partial: false, serviceDate: localDate(now), updatedAt: new Date(now).toISOString(), serverTime: new Date(now).toISOString(), mode: 'live' };
const nextHour = Math.ceil(now / 3600000) * 3600000;
const weather = { cityId: 1, stopId: 1, updatedAt: new Date(now).toISOString(), observedAt: new Date(now).toISOString(), temperature: 11, apparentTemperature: 10, code: 3, isDay: true, windSpeed: 3,
hourly: [0, 1, 2].map(i => ({ at: new Date(nextHour + i * 3600000).toISOString(), temperature: 12 - i, code: i ? 61 : 3, precipitationProbability: 80 - i * 10 })) };
return { t, board, weather, settings: { ...defaultSettings, stopId: 1, holdNear: false, nightMode: 'off', places: targets.map(s => ({ stopId: s.id, name: s.name })) } };
}
const client = { config: () => ({ cityId: 1, cityName: 'Полоцк', stopName: stop.name, defaultStopId: 1, configured: true, refreshSeconds: 20, timeZone: 'Europe/Minsk', passengerBaseUrl: 'https://pwa.ridebus.by/' }),
stops: async () => [stop, ...targets.map(s => ({ id: s.id, name: s.name, direction: 'тест' }))], board: async () => resources().board, timetable: async () => resources().t, coordinates: async () => null };
const app = express();
app.get('/__fixture', (req, res) => {
if (['bus', 'minibus'].includes(req.query.first)) { firstType = req.query.first; started = Date.now(); }
const payload = JSON.stringify(resources()).replaceAll('<', '\\u003c');
res.type('html').send(`<html lang="ru"><body style="font:20px system-ui;padding:32px"><h1>Пассажирские блоки · тестовый стенд</h1><p>Фиктивное расписание и погода. Тарифы — подтверждённые пользователем значения для Полоцка. Рабочая БД не используется.</p><form><button name="first" value="bus">Автобус ближайший</button><button name="first" value="minibus">Маршрутка ближайшая</button></form><p>Ближайший: ${firstType === 'bus' ? 'автобус' : 'маршрутка'}. Важные места здесь только для проверки.</p><button id="seed">Открыть тестовый пример</button><script>
const seed = ${payload}; document.querySelector('#seed').onclick = async () => {
const entries = [['ridebus-board-snapshot:1:1',seed.board],['ridebus-board-timetable:1:1',seed.t],['ridebus-board-weather:1:1',seed.weather]];
localStorage.setItem('ridebus-board-settings-v1', JSON.stringify(seed.settings));
for(const [key,value] of entries) localStorage.setItem(key,JSON.stringify(value));
try { await new Promise((resolve,reject)=>{const req=indexedDB.open('ridebus-board-v1',1);req.onupgradeneeded=()=>req.result.createObjectStore('snapshots');req.onerror=reject;req.onsuccess=()=>{const db=req.result,tx=db.transaction('snapshots','readwrite');for(const [key,value] of entries)tx.objectStore('snapshots').put(value,key);tx.oncomplete=()=>{db.close();resolve();};tx.onerror=reject;};}); } catch {}
location.href='/';
};</script></body></html>`);
});
app.use(createApp(client, { weather: async () => resources().weather }, createDeviceBridge({ enabled: false })));
const dist = fileURLToPath(new URL('../../dist/', import.meta.url));
app.use(express.static(dist));app.get('/{*path}', (_req,res) => res.sendFile(`${dist}/index.html`));
await new Promise(resolve => app.listen(5194, '127.0.0.1', resolve));
console.log('Passenger UI fixture http://127.0.0.1:5194/__fixture');