All good
Some checks failed
CI / Check (push) Has been cancelled
Build and publish Docker image / build-and-push (push) Has been cancelled

This commit is contained in:
Andras Schmelczer 2026-05-18 21:20:10 +01:00
parent 6ea544a0f6
commit 6cc7288126
45 changed files with 929 additions and 1043 deletions

View file

@ -68,6 +68,34 @@ const ROUTE_COLORS: Record<string, { color: string; darkText?: boolean }> = {
};
const NON_TUBE_NAMES = new Set(['DLR', 'London Overground', 'Elizabeth line']);
const GOOGLE_MAPS_DEPARTURE_TIME_ZONE = 'Europe/London';
const londonDateFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: GOOGLE_MAPS_DEPARTURE_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const londonDateTimeFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: GOOGLE_MAPS_DEPARTURE_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
hourCycle: 'h23',
});
function dateTimeParts(formatter: Intl.DateTimeFormat, date: Date): Record<string, number> {
const parts: Record<string, number> = {};
formatter.formatToParts(date).forEach((part) => {
if (part.type !== 'literal') {
parts[part.type] = Number(part.value);
}
});
return parts;
}
/** Strip trailing parenthesized GTFS route IDs and NaPTAN stop codes (e.g. "(6757261)", "(9400ZZLUCGT1)") */
function stripId(label: string): string {
@ -87,15 +115,48 @@ function getRouteDisplay(mode: string): { label: string; color: string; darkText
return { label: clean, color: '#6b7280', darkText: false };
}
/** Returns a Unix timestamp for the next Monday at 07:30 local time. */
function londonOffsetMs(utcMs: number): number {
const parts = dateTimeParts(londonDateTimeFormatter, new Date(utcMs));
const londonAsUtcMs = Date.UTC(
parts.year,
parts.month - 1,
parts.day,
parts.hour,
parts.minute,
parts.second
);
return londonAsUtcMs - utcMs;
}
function londonTimeToUtcMs(
year: number,
month: number,
day: number,
hour: number,
minute: number
): number {
const localAsUtcMs = Date.UTC(year, month - 1, day, hour, minute, 0, 0);
const offsetMs = londonOffsetMs(localAsUtcMs);
const utcMs = localAsUtcMs - offsetMs;
const correctedOffsetMs = londonOffsetMs(utcMs);
return correctedOffsetMs === offsetMs ? utcMs : localAsUtcMs - correctedOffsetMs;
}
/** Returns a Unix timestamp for the next Monday at 07:30 Europe/London time. */
function nextMondayAt730(): number {
const now = new Date();
const day = now.getDay(); // 0=Sun … 6=Sat
const today = dateTimeParts(londonDateFormatter, now);
const day = new Date(Date.UTC(today.year, today.month - 1, today.day)).getUTCDay();
const daysUntil = day === 0 ? 1 : day === 1 ? 7 : 8 - day;
const monday = new Date(now);
monday.setDate(now.getDate() + daysUntil);
monday.setHours(7, 30, 0, 0);
return Math.floor(monday.getTime() / 1000);
const monday = new Date(Date.UTC(today.year, today.month - 1, today.day + daysUntil));
const utcMs = londonTimeToUtcMs(
monday.getUTCFullYear(),
monday.getUTCMonth() + 1,
monday.getUTCDate(),
7,
30
);
return Math.floor(utcMs / 1000);
}
function googleMapsDestination(

View file

@ -419,6 +419,7 @@ export default function MapPage({
const { listings: actualListings } = useActualListings(mapData.bounds, {
filterParam: actualListingsFilterParam,
travelParam: actualListingsTravelParam,
shareCode,
});
const [isAreaGroupExpanded, toggleAreaGroup] = useCollapsibleGroups(true);