Refactor mobile UI and centralize HAFAS parsing
Introduce useColors hook to replace direct theme usage in mobile screens. Extract EventHeader, JourneyList, BikeSection, and NearbyStops components to reduce complexity in EventDetailScreen. Move parseHafasJourneys to packages/core for shared usage between web and mobile clients. Update web API route with stricter HAFAS validation and consistent client instantiation. Add comprehensive codebase function guide documenting data flow, shared packages, and service integrations.
This commit is contained in:
@@ -8,7 +8,7 @@ import type {
|
||||
Station,
|
||||
WienerLinienDeparture,
|
||||
} from "@timetoleave/core";
|
||||
import { hafasDateTime } from "@timetoleave/core";
|
||||
import { hafasDateTime, parseHafasJourneys } from "@timetoleave/core";
|
||||
|
||||
const DEFAULT_BASE_URL = "";
|
||||
|
||||
@@ -115,7 +115,6 @@ export class ApiClient {
|
||||
toStationExtId: string,
|
||||
date: Date,
|
||||
): Promise<Journey[]> {
|
||||
// Build a proper HAFAS TripSearch body — the /api/hafas endpoint expects svcReqL.
|
||||
const { date: hafasDate, time: hafasTime } = hafasDateTime(date);
|
||||
|
||||
const body = {
|
||||
@@ -140,7 +139,8 @@ export class ApiClient {
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`Journey search failed: ${res.status}`);
|
||||
return res.json();
|
||||
const data = await res.json();
|
||||
return parseHafasJourneys(data, hafasDate, date);
|
||||
}
|
||||
|
||||
async reverseGeocode(lat: number, lng: number): Promise<GeocodeResult | null> {
|
||||
@@ -166,18 +166,20 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
async searchStation(query: string): Promise<Station[]> {
|
||||
// Use HAFAS LocMatch to find real stations by name — returns proper extIds.
|
||||
const result = await this.hafasRequest<{
|
||||
svcReqL?: Array<{ res?: { locL?: Station[] } }>;
|
||||
svcResL?: Array<{ res?: { match?: { locL?: Array<{ type: string; name: string; extId: string }> } } }>;
|
||||
}>({
|
||||
svcReqL: [
|
||||
{
|
||||
meth: "LocMatch",
|
||||
req: { searchTxt: query, maxMatches: 5 },
|
||||
req: { input: { loc: { name: query, type: "S" }, maxLoc: 5, field: "S" } },
|
||||
},
|
||||
],
|
||||
});
|
||||
return result?.svcReqL?.[0]?.res?.locL ?? [];
|
||||
const locL = result?.svcResL?.[0]?.res?.match?.locL ?? [];
|
||||
return locL
|
||||
.filter((l) => l.type === "S")
|
||||
.map((l) => ({ name: l.name, extId: l.extId }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,7 +194,7 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
const result = await this.hafasRequest<{
|
||||
svcReqL?: Array<{ res?: { match?: { locL?: HafasLocation[] } } }>;
|
||||
svcResL?: Array<{ res?: { match?: { locL?: HafasLocation[] } } }>;
|
||||
}>({
|
||||
svcReqL: [
|
||||
{
|
||||
@@ -214,7 +216,7 @@ export class ApiClient {
|
||||
],
|
||||
});
|
||||
|
||||
const stations: HafasLocation[] = result?.svcReqL?.[0]?.res?.match?.locL ?? [];
|
||||
const stations: HafasLocation[] = result?.svcResL?.[0]?.res?.match?.locL ?? [];
|
||||
if (stations.length === 0) return null;
|
||||
|
||||
// Filter to only "S" (station) type results, then pick the closest
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Journey } from "./types";
|
||||
import { parseHafasTime } from "./hafas-time";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type RawJson = any;
|
||||
|
||||
export function parseHafasJourneys(json: RawJson, hafasDate: string, queryDate: Date): Journey[] {
|
||||
const outConL: RawJson[] = json?.svcResL?.[0]?.res?.outConL ?? [];
|
||||
|
||||
return outConL.map((con: RawJson, i: number): Journey => {
|
||||
const first = con.secL?.[0];
|
||||
const last = con.secL?.[con.secL.length - 1];
|
||||
const dep = first?.dep;
|
||||
const arr = last?.arr;
|
||||
|
||||
const sD = dep?.dTimeS ? parseHafasTime(hafasDate, dep.dTimeS) : queryDate;
|
||||
const rD = dep?.dTimeR ? parseHafasTime(hafasDate, dep.dTimeR) : sD;
|
||||
const sA = arr?.aTimeS ? parseHafasTime(hafasDate, arr.aTimeS) : sD;
|
||||
const rA = arr?.aTimeR ? parseHafasTime(hafasDate, arr.aTimeR) : sA;
|
||||
|
||||
const delay = Math.max(0, Math.round((rD.getTime() - sD.getTime()) / 60000));
|
||||
const trains: string[] = (con.secL ?? [])
|
||||
.filter((s: RawJson) => s.jny)
|
||||
.map((s: RawJson) => s.jny?.stopL?.[0]?.name ?? "")
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
id: con.ctxRecon ?? `journey-${i}`,
|
||||
sD, rD, sA, rA, delay,
|
||||
platform: dep?.dPlatfS ?? "",
|
||||
changes: Math.max(0, (con.secL ?? []).filter((s: RawJson) => s.jny).length - 1),
|
||||
trains,
|
||||
cancelled: (con.secL ?? []).some((s: RawJson) => s.jny?.isCncl === true),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './countdown-utils';
|
||||
export * from './formatting';
|
||||
export * from './status-utils';
|
||||
export * from './hafas-time';
|
||||
export * from './hafas-parser';
|
||||
|
||||
Reference in New Issue
Block a user