|
|
|
@@ -0,0 +1,98 @@
|
|
|
|
|
import type {
|
|
|
|
|
GeocodeResult,
|
|
|
|
|
BikeRoute,
|
|
|
|
|
CalendarEvent,
|
|
|
|
|
Journey,
|
|
|
|
|
Station,
|
|
|
|
|
} from "@timetoleave/core";
|
|
|
|
|
|
|
|
|
|
const DEFAULT_BASE_URL = "";
|
|
|
|
|
|
|
|
|
|
function buildUrl(base: string, path: string, params: Record<string, string> = {}): string {
|
|
|
|
|
const queryString = Object.entries(params)
|
|
|
|
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
|
|
|
.join("&");
|
|
|
|
|
const full = `${base}${path}`;
|
|
|
|
|
return queryString ? `${full}?${queryString}` : full;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ApiClient {
|
|
|
|
|
private readonly baseUrl: string;
|
|
|
|
|
|
|
|
|
|
constructor(baseUrl?: string) {
|
|
|
|
|
this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getHealth(): Promise<{ status: "ok"; uptime: number }> {
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/health`);
|
|
|
|
|
if (!res.ok) throw new Error(`Health check failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async geocode(name: string, countrycodes?: string): Promise<GeocodeResult[]> {
|
|
|
|
|
const params: Record<string, string> = { name };
|
|
|
|
|
if (countrycodes) params.countrycodes = countrycodes;
|
|
|
|
|
const url = buildUrl(this.baseUrl, "/api/geocode", params);
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
if (!res.ok) throw new Error(`Geocode failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getBikeRoute(
|
|
|
|
|
fromLat: number,
|
|
|
|
|
fromLng: number,
|
|
|
|
|
toLat: number,
|
|
|
|
|
toLng: number,
|
|
|
|
|
): Promise<BikeRoute> {
|
|
|
|
|
const url = buildUrl(this.baseUrl, "/api/bike-route", {
|
|
|
|
|
fromLat: String(fromLat),
|
|
|
|
|
fromLng: String(fromLng),
|
|
|
|
|
toLat: String(toLat),
|
|
|
|
|
toLng: String(toLng),
|
|
|
|
|
});
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
if (!res.ok) throw new Error(`Bike route failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetchCalendar(url: string, days?: number): Promise<CalendarEvent[]> {
|
|
|
|
|
const params: Record<string, string> = { url };
|
|
|
|
|
if (days) params.days = String(days);
|
|
|
|
|
const api = buildUrl(this.baseUrl, "/api/calendar", params);
|
|
|
|
|
const res = await fetch(api);
|
|
|
|
|
if (!res.ok) throw new Error(`Calendar fetch failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async parseCalendarIcs(content: string): Promise<CalendarEvent[]> {
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/calendar/parse`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "text/calendar" },
|
|
|
|
|
body: content,
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error(`Calendar parse failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async searchJourneys(
|
|
|
|
|
fromStationExtId: string,
|
|
|
|
|
toStationExtId: string,
|
|
|
|
|
date: Date,
|
|
|
|
|
): Promise<Journey[]> {
|
|
|
|
|
const url = buildUrl(this.baseUrl, "/api/hafas", {
|
|
|
|
|
from: fromStationExtId,
|
|
|
|
|
to: toStationExtId,
|
|
|
|
|
date: date.toISOString(),
|
|
|
|
|
});
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
if (!res.ok) throw new Error(`Journey search failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async searchStation(query: string): Promise<Station[]> {
|
|
|
|
|
const url = buildUrl(this.baseUrl, "/api/station", { q: query });
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
if (!res.ok) throw new Error(`Station search failed: ${res.status}`);
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
}
|