mobile app phase 1

This commit is contained in:
2026-05-10 21:19:39 +02:00
parent 442a00dbbe
commit d08afc1dcb
1679 changed files with 392410 additions and 3005 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@timetoleave/api-client",
"version": "0.1.0",
"private": true,
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src/"
},
"dependencies": {
"@timetoleave/core": "*"
},
"devDependencies": {
"typescript": "^5"
}
}
+98
View File
@@ -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();
}
}
+1
View File
@@ -0,0 +1 @@
export { ApiClient } from './client';
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}