feat: implement night mode functionality with UI updates and journey tracking

- Added NightScreen component for displaying night mode information and alerts.
- Introduced NextStops component to show upcoming stops during a journey.
- Implemented logic for determining night mode activation based on schedule and user settings.
- Created journey utility functions to manage onward journey data and stop information.
- Updated tests to cover new night mode features and journey logic.
- Added fixture for testing night mode scenarios with specific timestamps.
- Removed QR code from night screen for a cleaner UI experience.
- Enhanced admin settings to manage night mode configurations.
This commit is contained in:
2026-10-11 05:04:00 +03:00
parent d04b364047
commit 5c69f6691d
49 changed files with 590 additions and 121 deletions

View File

@@ -1,6 +1,19 @@
import { ApiError } from './board.mjs';
import { TIMETABLE_MAX_MS, localDate, shiftDate, validDate } from '../src/lib/timetable.mjs';
function normalizeStops(raw) {
if (!Array.isArray(raw)) return [];
const stops = raw.map(stop => {
const offset = stop.time_offset_minutes == null || stop.time_offset_minutes === '' ? NaN : Number(stop.time_offset_minutes);
return { id: Number(stop.stop_id), name: typeof stop.name === 'string' ? stop.name.trim() : '', sequence: Number(stop.sequence),
offsetMinutes: Number.isFinite(offset) && offset >= 0 && offset < 1440 ? offset : null };
});
// An ambiguous loop or incomplete stop identity cannot produce a reliable diagram.
if (stops.some(stop => !Number.isSafeInteger(stop.id) || stop.id < 1 || !stop.name || !Number.isSafeInteger(stop.sequence) || stop.sequence < 0)
|| new Set(stops.map(stop => stop.id)).size !== stops.length || new Set(stops.map(stop => stop.sequence)).size !== stops.length) return [];
return stops.sort((a, b) => a.sequence - b.sequence);
}
function normalizeSchedules(raw, route, rawStops, stopId) {
if (!Array.isArray(raw) || !Array.isArray(rawStops)) throw new ApiError();
const stops = new Map(rawStops.map(stop => [Number(stop.stop_id), stop]));
@@ -35,7 +48,7 @@ function normalizeSchedules(raw, route, rawStops, stopId) {
const terminal = end || [...rawStops].sort((a, b) => Number(b.sequence) - Number(a.sequence))[0];
const origin = start || [...rawStops].sort((a, b) => Number(a.sequence) - Number(b.sequence))[0];
return [{ arrivalSeconds: Number(time[1]) * 3600 + Number(time[2]) * 60 + Number(time[3] || 0) + offset * 60,
originStopId: Number(origin?.stop_id), originTime: `${time[1].padStart(2, '0')}:${time[2]}`,
originStopId: Number(origin?.stop_id), originTime: `${time[1].padStart(2, '0')}:${time[2]}`, endStopId: Number(terminal?.stop_id),
destination: terminal?.name?.trim() || route.name.split(/\s+[-–—]\s+/).at(-1) || route.name,
note: typeof trip.note === 'string' ? trip.note.trim() : '', daysOfWeek: days(trip.daysOfWeek), exceptDaysOfWeek: days(trip.exceptDaysOfWeek) }];
});
@@ -67,7 +80,7 @@ export async function loadTimetable({ upstream, cityId, cityName, stop, now }) {
while (index < routes.length) {
const current = index++, route = routes[current];
const [schedules, stops] = await Promise.all([upstream(`api/v1/routes/${route.id}/schedules`), upstream(`api/v1/routes/${route.id}/stops`)]);
result[current] = { ...route, schedules: normalizeSchedules(schedules, route, stops, Number(stop.id)) };
result[current] = { ...route, stops: normalizeStops(stops), schedules: normalizeSchedules(schedules, route, stops, Number(stop.id)) };
}
}));
return { version: 1, cityId, cityName, stop: { id: Number(stop.id), name: stop.name.trim(), direction: stop.description?.trim() || '' },