diff --git a/apps/mobile/eslint.config.js b/apps/mobile/eslint.config.js index ba04dc4..2fb472a 100644 --- a/apps/mobile/eslint.config.js +++ b/apps/mobile/eslint.config.js @@ -1,6 +1,10 @@ import js from "@eslint/js"; import ts from "typescript-eslint"; import reactPlugin from "eslint-plugin-react"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); // We have no .eslintrc, so we must define everything here. @@ -40,6 +44,7 @@ export default ts.config( ecmaFeatures: { jsx: true, }, + tsconfigRootDir: path.resolve(__dirname), }, }, plugins: { diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index 72a7928..f45afb3 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -1,6 +1,10 @@ import { defineConfig, globalIgnores } from "eslint/config"; import nextVitals from "eslint-config-next/core-web-vitals"; import nextTs from "eslint-config-next/typescript"; +import { fileURLToPath } from "url"; +import path from "path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const eslintConfig = defineConfig([ ...nextVitals, @@ -14,6 +18,11 @@ const eslintConfig = defineConfig([ "next-env.d.ts", ]), { + languageOptions: { + parserOptions: { + tsconfigRootDir: path.resolve(__dirname), + }, + }, rules: { // Allow _-prefixed parameters and variables to signal intentionally unused. "@typescript-eslint/no-unused-vars": [ diff --git a/apps/web/src/app/calendar/page.tsx b/apps/web/src/app/calendar/page.tsx index b13ad7a..d1416f9 100644 --- a/apps/web/src/app/calendar/page.tsx +++ b/apps/web/src/app/calendar/page.tsx @@ -15,9 +15,9 @@ export default function CalendarPage() { return (
-

Calendar sync

+

Calendar sync

Import, inspect, and time your day.

-

View and manage every appointment from a single departure-focused calendar.

+

View and manage every appointment from a single departure-focused calendar.

diff --git a/apps/web/src/app/event/BikeSection.tsx b/apps/web/src/app/event/BikeSection.tsx index 321fe01..051c0c3 100644 --- a/apps/web/src/app/event/BikeSection.tsx +++ b/apps/web/src/app/event/BikeSection.tsx @@ -32,7 +32,7 @@ const BikeSection: React.FC = ({

Bicycle Route

-

Door-to-door route to the event

+

Door-to-door route to the event

{onRefresh && (
) : bikeError ? ( -
Error: {bikeError}
+
Error: {bikeError}
) : bikeRoute ? (
- Distance + Distance {Math.round(bikeRoute.distance / 1000)} km ({Math.round(bikeRoute.distance)} m)
- Duration + Duration {Math.floor(bikeRoute.duration / 60)} min {bikeRoute.duration % 60} s
{bikeRoute.steps && bikeRoute.steps.length > 0 && (
-

Steps

+

Steps

    {bikeRoute.steps.map((step, index) => (
  • - {step.name} - {step.instruction} + {step.name} + {step.instruction}
  • ))}
@@ -77,7 +77,7 @@ const BikeSection: React.FC = ({ )}
) : ( -
+
Add origin and destination coordinates to calculate a bike route.
)} diff --git a/apps/web/src/app/event/EventCard.tsx b/apps/web/src/app/event/EventCard.tsx index f16e2f9..605037e 100644 --- a/apps/web/src/app/event/EventCard.tsx +++ b/apps/web/src/app/event/EventCard.tsx @@ -86,7 +86,7 @@ export default function EventCard({ event, originStation }: EventCardProps) {
-

Next stop

+

Next stop

{event.title}

@@ -94,12 +94,12 @@ export default function EventCard({ event, originStation }: EventCardProps) {
-

Destination

-

{event.destination}

+

Destination

+

{event.destination}

-

Appointment

-

{format(event.eventTime, "EEE dd MMM yyyy HH:mm")}

+

Appointment

+

{format(event.eventTime, "EEE dd MMM yyyy HH:mm")}

@@ -109,8 +109,8 @@ export default function EventCard({ event, originStation }: EventCardProps) { key={option.id} className={`rounded-xl border p-3 text-left transition-all ${ activeMode === option.id - ? "border-[#D946EF]/70 bg-[#B23CFF]/18 shadow-[0_14px_34px_rgba(178,60,255,0.2)]" - : "border-white/10 bg-white/[0.045] hover:border-[#D946EF]/40 hover:bg-white/[0.07]" + ? "border-brand-fuchsia/70 bg-brand-purple/18 shadow-[0_14px_34px_rgba(178,60,255,0.2)]" + : "border-white/10 bg-white/[0.045] hover:border-brand-fuchsia/40 hover:bg-white/[0.07]" } ${option.disabled ? "cursor-not-allowed opacity-45 hover:border-white/10 hover:bg-white/[0.045]" : ""}`} onClick={() => { if (!option.disabled) { @@ -122,31 +122,31 @@ export default function EventCard({ event, originStation }: EventCardProps) { {option.label} {activeMode === option.id && ( - + Active )} - {option.meta} + {option.meta} ))}
-

Leave by

+

Leave by

{departureTime ? format(departureTime, "HH:mm") : "Pending"}

-

Arrive by

+

Arrive by

{arrivalTime ? format(arrivalTime, "HH:mm") : format(new Date(event.eventTime.getTime() - arrivalBufferMinutes * 60_000), "HH:mm")}

-

Buffer

+

Buffer

{arrivalBufferMinutes} min {calculatedMode ? `via ${calculatedMode}` : ""}

diff --git a/apps/web/src/app/event/JourneyList.tsx b/apps/web/src/app/event/JourneyList.tsx index 63f0967..8d004c8 100644 --- a/apps/web/src/app/event/JourneyList.tsx +++ b/apps/web/src/app/event/JourneyList.tsx @@ -22,7 +22,7 @@ const JourneyList: React.FC = ({ const targetArrivalTime = new Date(eventTime.getTime() - arrivalBufferMinutes * 60000); if (journeys.length === 0) { - return
No journeys found
; + return
No journeys found
; } return ( @@ -37,20 +37,20 @@ const JourneyList: React.FC = ({ key={journey.id} className={`rounded-xl border p-3 ${ arrivesTooLate || journey.cancelled - ? "border-[#FF2D8D]/18 bg-[#FF2D8D]/7 opacity-40 line-through" + ? "border-brand-pink/18 bg-brand-pink/7 opacity-40 line-through" : "border-white/10 bg-white/[0.04]" }`} >
{formatTime(departure)} - {journey.platform} + {journey.platform} {journey.delay > 0 && ( {`+${journey.delay}'`} )}
- + {journey.cancelled ? "Cancelled" : journey.trains.join(" -> ")}
@@ -58,11 +58,11 @@ const JourneyList: React.FC = ({
-
+
{journey.changes > 0 ? `Change(s): ${journey.changes}` : "Direct"} Arrives {formatTime(arrival)} {arrivesTooLate && ( - + misses {arrivalBufferMinutes} min buffer )} diff --git a/apps/web/src/app/event/TrainSection.tsx b/apps/web/src/app/event/TrainSection.tsx index 64180c2..08f261a 100644 --- a/apps/web/src/app/event/TrainSection.tsx +++ b/apps/web/src/app/event/TrainSection.tsx @@ -43,11 +43,11 @@ const TrainSection: React.FC = ({

Trains

-

+

To {destName} {formatDateTime(eventTime)}

{(arrivalBufferMinutes ?? 0) > 0 && ( -

+

Target arrival: {arrivalBufferMinutes} min early

)} @@ -65,7 +65,7 @@ const TrainSection: React.FC = ({
) : error ? ( -
{error}
+
{error}
) : ( <> diff --git a/apps/web/src/app/event/WalkingOption.tsx b/apps/web/src/app/event/WalkingOption.tsx index 94577fb..55db2dc 100644 --- a/apps/web/src/app/event/WalkingOption.tsx +++ b/apps/web/src/app/event/WalkingOption.tsx @@ -24,7 +24,7 @@ const WalkingOption: React.FC = ({ walkRoute, walkLoading, w

Final Walk

-

From arrival station to destination

+

From arrival station to destination

{onRefresh && (
) : walkError ? ( -
Error: {walkError}
+
Error: {walkError}
) : walkRoute ? (
- Distance + Distance {Math.round(walkRoute.distance / 1000)} km ({Math.round(walkRoute.distance)} m)
- Duration + Duration {Math.floor(walkRoute.duration / 60)} min {walkRoute.duration % 60} s
{walkRoute.steps && walkRoute.steps.length > 0 && (
-

Steps

+

Steps

    {walkRoute.steps.map((step: { name: string; instruction: string }, index: number) => (
  • - {step.name} - {step.instruction} + {step.name} + {step.instruction}
  • ))}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 9005d3a..6f4f80c 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -31,7 +31,7 @@ export default function RootLayout({ }>) { return ( - + diff --git a/apps/web/src/app/layout/Header.tsx b/apps/web/src/app/layout/Header.tsx index 43caf7d..bf4cf1e 100644 --- a/apps/web/src/app/layout/Header.tsx +++ b/apps/web/src/app/layout/Header.tsx @@ -28,14 +28,14 @@ const Header: React.FC = ({ className = "" }) => {
-
+
{events.length} @@ -46,8 +46,8 @@ const Header: React.FC = ({ className = "" }) => { Online ) : status === false ? ( - - + + Offline ) : null} @@ -58,7 +58,7 @@ const Header: React.FC = ({ className = "" }) => { -

Settings

+

Settings

diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 3ad2a56..ad88c81 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -17,7 +17,7 @@ export default function Home() {
-

Departure desk

+

Departure desk

Know when to leave before the clock turns hostile.

@@ -25,22 +25,22 @@ export default function Home() {

{upcoming.length}

-

upcoming

+

upcoming

{events.length}

-

total events

+

total events

{upcoming.length === 0 ? (
-
+
T

No upcoming events

-

+

Add an event or import your calendar to turn this into a live departure board.

diff --git a/apps/web/src/app/ui/LoadingSpinner.tsx b/apps/web/src/app/ui/LoadingSpinner.tsx index 5d6c4ac..3673bc2 100644 --- a/apps/web/src/app/ui/LoadingSpinner.tsx +++ b/apps/web/src/app/ui/LoadingSpinner.tsx @@ -20,7 +20,7 @@ const LoadingSpinner: React.FC = ({ return (
diff --git a/apps/web/src/app/ui/ReminderSettingsPanel.tsx b/apps/web/src/app/ui/ReminderSettingsPanel.tsx index 4356b2e..43f7101 100644 --- a/apps/web/src/app/ui/ReminderSettingsPanel.tsx +++ b/apps/web/src/app/ui/ReminderSettingsPanel.tsx @@ -30,7 +30,7 @@ export default function ReminderSettingsPanel({ className = "" }: ReminderSettin
{/* Toggle */}
- + Leave reminders diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..84a6cdc --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,149 @@ +# Core & API Client Reference + +This document provides a detailed reference for the internal packages: `@timetoleave/core` and `@timetoleave/api-client`. + +## πŸ“¦ `@timetoleave/core` + +The core package contains all domain types, status calculation logic, HAFAS time parsing, and formatting utilities. It has zero runtime dependencies outside of the standard library. + +### Types (`types.ts`) + +| Interface | Description | +| :--- | :--- | +| `Event` | Represents a calendar event with an assigned destination and event time. | +| `CalendarEvent` | Raw calendar event data (typically `string` dates before conversion to `Event`). | +| `Station` | A transport station with a name, HAFAS `extId`, and optional coordinates. | +| `Journey` | A public transport connection including scheduled/real departure/arrival, delay, platform, and cancellation status. | +| `BikeRoute` / `WalkRoute` | Routing data including total distance, duration, and step-by-step instructions. | +| `CountdownInfo` | Output of the countdown utility containing a label, color code, and urgency flag. | +| `ReminderSettings` | User preferences for buffers, walking/bike options, and reminder toggles. | +| `WienerLinienDeparture` | Specific types for WienerLinien (Vienna public transport) monitor responses. | + +### HAFAS Time Utilities (`hafas-time.ts`) + +HAFAS timestamps are strictly tied to the `Europe/Vienna` timezone (CET/CEST). These utilities handle the bi-directional conversion between UTC JavaScript `Date` objects and HAFAS strings, correctly handling Daylight Saving Time (DST) transitions. + +```typescript +import { parseHafasTime, hafasDateTime } from '@timetoleave/core'; + +// Parse HAFAS date ("YYYYMMDD") and time ("HHMMSS") into a UTC Date object +const dateObj = parseHafasTime("20231027", "143000"); + +// Convert a UTC Date object back to HAFAS date and time strings +const { date, time } = hafasDateTime(dateObj); +// Returns: { date: "20231027", time: "143000" } +``` + +> **Note:** `getTimezoneOffsetMinutes` is designed exclusively for full-hour offsets like `Europe/Vienna`. It will produce incorrect results for fractional timezones (e.g., India +05:30). + +### Countdown & Status Utilities + +#### `calculateCountdown(targetDate: Date): CountdownInfo` +Calculates the time delta between `now` and `targetDate`, returning a human-readable label, a color code, and an urgency boolean. + +| Time Delta | Label | Color | Urgent | +| :--- | :--- | :--- | :--- | +| `<= 0 min` | `Now` | `red` | `true` | +| `<= 10 min` | `[X]min` | `orange` | `true` | +| `<= 30 min` | `[X]min` | `yellow` | `false` | +| `<= 60 min` | `[X]min` | `green` | `false` | +| `> 60 min` | `[X]h [Y]min` | `blue` | `false` | + +#### `getLeaveStatus(event: Event, journeys: Journey[]): string` +Derives a human-readable leave-by status by finding the earliest non-cancelled journey and comparing its real departure time (`rD`) against the current time. + +**Possible Returns:** +- `"No journey data"` +- `"All journeys cancelled"` +- `"Departure missed"` (if `rD` is in the past) +- `"Delayed +[X] min"` (if delay exceeds 10 minutes) +- `"Leave now"` (if departure is within 15 minutes) +- `"On time"` + +### Formatting Utilities (`formatting.ts`) + +All formatting functions default to the `de-AT` locale to match the primary target region. + +- `formatTime(date: Date)` -> `"14:30"` +- `formatDate(date: Date)` -> `"Mi., 27. Oktober 2023"` +- `formatDateTime(date: Date)` -> `"27. Oktober 2023, 14:30"` +- `formatDuration(seconds: number)` -> `"1h 23min"` or `"45min"` +- `formatDistance(meters: number)` -> `"1.2km"` or `"800m"` + +--- + +## 🌐 `@timetoleave/api-client` + +The API client is a lightweight wrapper around the Web App's Next.js API routes. It handles URL construction, query parameters, and JSON serialization. + +### Initialization + +```typescript +import { ApiClient } from '@timetoleave/api-client'; + +// Initialize with the base URL of the backend proxy +const api = new ApiClient('http://localhost:3000'); +``` + +### Methods + +#### `getHealth()` +```typescript +getHealth(): Promise<{ status: 'ok'; uptime: number }> +``` +Checks the `/api/health` endpoint to verify backend availability. + +#### `geocode(name: string, countrycodes?: string)` +```typescript +geocode(name: string, countrycodes?: string): Promise +``` +Performs forward geocoding via `/api/geocode`. + +#### `reverseGeocode(lat: number, lng: number)` +```typescript +reverseGeocode(lat: number, lng: number): Promise +``` +Performs reverse geocoding via `/api/geocode/reverse`. + +#### `fetchCalendar(url: string, days?: number)` +```typescript +fetchCalendar(url: string, days?: number): Promise +``` +Fetches and parses a remote `.ics` file via `/api/calendar`. The optional `days` parameter limits the fetch to upcoming events. + +#### `parseCalendarIcs(content: string)` +```typescript +parseCalendarIcs(content: string): Promise +``` +Parses raw `.ics` string content via `/api/calendar/parse`. + +#### `searchStation(query: string)` +```typescript +searchStation(query: string): Promise +``` +Searches for stations by name using the HAFAS `LocMatch` method. Returns up to 5 matches with valid `extId`s. + +#### `searchJourneys(fromStationExtId: string, toStationExtId: string, date: Date)` +```typescript +searchJourneys(from: string, to: string, date: Date): Promise +``` +Sends a HAFAS `TripSearch` request to find public transport connections between two stations. It automatically converts the `Date` to HAFAS-compatible strings. + +#### `getBikeRoute(fromLat, fromLng, toLat, toLng)` & `getWalkRoute(fromLat, fromLng, toLat, toLng)` +```typescript +getBikeRoute(...): Promise +getWalkRoute(...): Promise +``` +Retrieves routing data for the "first mile / last mile" segment (e.g., biking from home to the train station). + +#### `findNearbyStops(lat: number, lng: number, radius?: number)` +```typescript +findNearbyStops(lat: number, lng: number, radius: number = 1000): Promise +``` +Finds public transport stops within a specific radius using the WienerLinien API (`/api/wienerlinien/stops`). + +#### `hafasRequest(body: unknown)` +```typescript +hafasRequest(body: unknown): Promise +``` +A generic method to send arbitrary HAFAS protocol bodies to `/api/hafas`. Useful for advanced use-cases not covered by the wrapper methods. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..e9ead95 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,64 @@ +# TimeToLeave Architecture + +This document outlines the architectural decisions, directory structure, and data flow of the TimeToLeave application. + +## πŸ—οΈ Monorepo Structure + +TimeToLeave uses an **npm Workspaces Monorepo** to manage its interconnected components. This allows for seamless code sharing and dependency management between the Web dashboard, the Mobile client, and the shared packages. + +```text +TimeToLeave/ +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ web/ # Next.js 16 Web Dashboard & Backend Proxy +β”‚ └── mobile/ # React Native 0.81 / Expo 54 Mobile Client +β”œβ”€β”€ packages/ +β”‚ β”œβ”€β”€ api-client/ # Unified API Client for Backend Proxies +β”‚ └── core/ # Shared Domain Types, Logic, and Utilities +β”œβ”€β”€ docs/ # Comprehensive Documentation +└── package.json # Root Workspace Configuration +``` + +## 🧩 Component Overview + +### 1. Web Application (`apps/web`) +- **Role:** Primary dashboard for desktop users and the **Backend Proxy** for HAFAS/Geocoding APIs. +- **Framework:** Next.js 16 (App Router) with React 19. +- **Styling:** Tailwind CSS 4. +- **State Management:** React Context (`EventsProvider`, `ReminderSettingsProvider`). +- **Backend Proxy:** The Next.js API routes (`/api/*`) act as a server-side proxy. This is crucial because the HAFAS protocol and various geocoding APIs require server-side execution to protect API keys, handle CORS, and manage protocol-specific payloads. + +### 2. Mobile Application (`apps/mobile`) +- **Role:** On-the-go companion app for real-time status checks and native device integration. +- **Framework:** React Native 0.81 via Expo 54. +- **Navigation:** React Navigation 7 (Native Stack Navigator). +- **Native APIs:** + - `expo-location`: Geolocation for finding nearby stations and calculating local transit (bike/walk) to the station. + - `expo-calendar`: Native integration to read local calendar events directly on the device. + - `expo-notifications`: Push notifications to alert the user when it's time to leave. + - `@react-native-async-storage/async-storage`: Persisting user settings (buffers, toggles) and cached events locally. + +### 3. Core Package (`packages/core`) +- **Role:** The single source of truth for domain logic and TypeScript types across the entire monorepo. +- **Key Modules:** + - **Types:** Defines `Event`, `Journey`, `Station`, `BikeRoute`, `CountdownInfo`, and `WienerLinien` specific types. + - **HAFAS Time Parsing:** Specialized utilities to parse Vienna-centric (CET/CEST) timestamps. Handles Daylight Saving Time (DST) transitions accurately using `Intl.DateTimeFormat`. + - **Countdown & Status Logic:** Algorithms to translate raw journey data into human-readable statuses like *"Leave now"*, *"On time"*, or *"Delayed +12 min"*. + - **Formatting:** Standardized formatting for dates, times, distances, and durations. + +### 4. API Client Package (`packages/api-client`) +- **Role:** A lightweight HTTP client that wraps the Web App's API routes. +- **Usage:** Used by both the Web frontend (for server/client data sync) and the Mobile client to communicate with the backend proxy. +- **Features:** Handles URL building, query parameters, and JSON serialization for HAFAS requests, calendar fetching, and routing requests. + +## πŸ”„ Data Flow + +1. **Calendar Sync:** The user provides an `.ics` URL or uploads a file. The `ApiClient` sends this to the Web App's `/api/calendar` route, which parses the events and returns standardized `CalendarEvent` objects. +2. **Station Search:** The user searches for a station. The `ApiClient` triggers a HAFAS `LocMatch` request via `/api/hafas`. +3. **Journey Calculation:** Using the station `extId` and the event time, the `ApiClient` sends a `TripSearch` request to `/api/hafas`. The backend returns real-time journey data (`Journey[]`). +4. **Local Routing:** Using `expo-location` (mobile) or browser geolocation (web), the app calculates the bike/walk route from the user's home/location to the departure station via `/api/bike-route` or `/api/walk-route`. +5. **Real-Time Status:** The `packages/core` logic continuously compares the `Journey.rD` (real departure) against the current time and local travel duration to update the Leave Status dynamically. + +## ⚠️ Technical Constraints & Guidelines + +- **Next.js Version:** The project uses **Next.js 16.2+**, which includes breaking changes compared to previous versions. Always refer to `node_modules/next/dist/docs/` when modifying Web routing or API conventions. +- **HAFAS Timezone:** HAFAS timestamps are strictly tied to `Europe/Vienna`. The `hafas-time.ts` module handles the bi-directional conversion between UTC `Date` objects and Vienna-local HAFAS strings. Never use standard `Date` methods for HAFAS times; always use `parseHafasTime()` and `hafasDateTime()`. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..7d738c8 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,101 @@ +# Development Guide + +This guide covers setting up the development environment, running the applications, and maintaining code quality. + +## πŸ›  Prerequisites + +- **Node.js:** Version 20.x or higher. +- **npm:** Version 9.x or higher. + +## πŸ“₯ Installation + +1. **Clone the repository:** + ```bash + git clone + cd TimeToLeave + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + *This installs dependencies for the root workspace as well as all apps and packages.* + +3. **Environment Variables:** + - Web App: Copy `apps/web/.env.example` to `apps/web/.env` and configure your HAFAS/Geocoding API keys. + - Mobile App: Copy `apps/mobile/.env.example` to `apps/mobile/.env` and set the backend API URL. + +## ▢️ Running the Applications + +| Script | Command | Description | +| :--- | :--- | :--- | +| `dev` | `npm run dev` | Starts the Next.js development server (Web) on `http://localhost:3000`. | +| `dev:mobile` | `npm run dev:mobile` | Starts the Expo development server (Mobile) and opens the Expo Go simulator. | +| `build` | `npm run build` | Builds the production bundle for the Web application. | + +## πŸ§ͺ Testing & Quality Assurance + +The project enforces strict typing and linting across all workspaces. + +| Script | Command | Description | +| :--- | :--- | :--- | +| `test` | `npm run test` | Runs **Vitest** for the Web app (using jsdom & React Testing Library) and **Jest** for the Mobile app (using jest-expo). | +| `lint` | `npm run lint` | Runs **ESLint 9** across the entire monorepo. | +| `typecheck` | `npm run typecheck` | Runs **TypeScript 5** type checking across all workspaces. | + +### Testing Specific Packages +If you want to run tests for a specific workspace: +```bash +cd apps/web && npm test +cd apps/mobile && npm test +cd packages/core && npm run typecheck +``` + +## 🐳 Docker Support (Web App) + +The web application includes a `Dockerfile` and `docker-compose.yml` for containerized development or deployment. + +```bash +cd apps/web +docker-compose up --build +``` + +## πŸ“ Web App Routing (Next.js 16) + +The web app uses the Next.js App Router. Key routes include: + +| Route | Description | +| :--- | :--- | +| `/` | **Dashboard:** Lists upcoming events and their real-time leave status. | +| `/calendar` | **Calendar Sync:** Interface to import `.ics` files or paste calendar URLs. | +| `/add-event` | **Manual Entry:** Add a new event manually without a calendar source. | +| `/event/[id]` | **Event Details:** Deep dive into a specific event, showing journey options, delays, and local routing. | + +### Backend API Routes +The web app acts as a proxy for external APIs. These are located in `apps/web/src/app/api/`: + +| Endpoint | Method | Description | +| :--- | :--- | :--- | +| `/api/health` | `GET` | Health check for the backend proxy. | +| `/api/calendar` | `GET` | Fetch and parse remote `.ics` files. | +| `/api/calendar/parse` | `POST` | Parse raw `.ics` content. | +| `/api/hafas` | `POST` | Generic HAFAS protocol endpoint (TripSearch, LocMatch). | +| `/api/geocode` | `GET` | Forward geocoding (Nominatim). | +| `/api/geocode/reverse`| `GET` | Reverse geocoding. | +| `/api/bike-route` | `GET` | Bicycle routing between coordinates. | +| `/api/walk-route` | `GET` | Walking routing between coordinates. | +| `/api/wienerlinien/stops`| `GET` | Find nearby WienerLinien stops. | + +## πŸ“± Mobile App Structure + +The mobile app is organized into the following directories: + +- `src/screens/`: UI screens (`EventListScreen`, `EventDetailScreen`, `AddEventScreen`, `CalendarImportScreen`, `SettingsScreen`). +- `src/navigation/`: React Navigation configuration (`AppNavigator.tsx`). +- `src/services/`: API integration and data fetching services. +- `src/store/`: Local state management and AsyncStorage integration. + +## ⚠️ Development Notes + +1. **Next.js 16 Breaking Changes:** The web app runs on Next.js 16.2+, which introduces breaking changes in routing and API conventions. Always check `node_modules/next/dist/docs/` if you encounter unexpected behavior. +2. **HAFAS Timezones:** Never parse HAFAS times using standard `Date` methods. Use `@timetoleave/core` utilities (`parseHafasTime`, `hafasDateTime`) to ensure accurate CET/CEST and DST handling. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..2ec0a42 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,59 @@ +# πŸ“š TimeToLeave Documentation + +Welcome to the official documentation for TimeToLeave, the smart departure planner that syncs with your calendar and monitors real-time public transport to tell you exactly when to leave home. + +## πŸ—ΊοΈ Table of Contents + +### 1. [Architecture](./ARCHITECTURE.md) +Understand the monorepo structure, tech stack, data flow, and component relationships between the Web Dashboard, Mobile Client, and shared packages. + +### 2. [Core & API Reference](./API_REFERENCE.md) +Detailed reference for the internal packages: +- **`@timetoleave/core`**: Domain types, HAFAS time parsing, countdown/status logic, and formatting utilities. +- **`@timetoleave/api-client`**: The unified HTTP client that wraps the backend proxy routes. + +### 3. [Development Guide](./DEVELOPMENT.md) +Everything a developer needs to get started: +- Prerequisites and installation instructions. +- Running the Web and Mobile apps in development mode. +- Testing (Vitest/Jest), Linting (ESLint), and Type Checking (TypeScript). +- Backend API endpoints and Next.js 16 routing conventions. + +### 4. [User Guide](./USER_GUIDE.md) +A guide for end-users explaining how to sync calendars, view event details, interpret the "Leave Status" color codes, and configure mobile notifications. + +--- + +## ⚑ Quick Start + +```bash +# Clone the repository +git clone +cd TimeToLeave + +# Install dependencies for the entire monorepo +npm install + +# Start the Web development server +npm run dev + +# Start the Mobile development server +npm run dev:mobile +``` + +## πŸ›‘οΈ Code Quality + +```bash +# Run all tests (Web Vitest + Mobile Jest) +npm run test + +# Run ESLint across the monorepo +npm run lint + +# Run TypeScript type checking +npm run typecheck +``` + +--- + +*Built for developers who bike to the train and hate missing their connections.* diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..a175621 --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,93 @@ +# User Guide + +Welcome to the TimeToLeave User Guide! This document explains how to use the TimeToLeave web dashboard and mobile application to plan your departures seamlessly. + +## πŸš€ How TimeToLeave Works + +TimeToLeave acts as your personal departure planner. Instead of manually checking train schedules, you simply sync your calendar. The app calculates the best public transport connections to your upcoming appointments, monitors real-time delays, and tells you exactly when to leave home. + +### Core Workflow +1. **Sync Your Calendar:** Import your `.ics` file or provide a calendar URL. +2. **Set Your Origin:** Define your home station or let the app use your current location. +3. **Automatic Planning:** The app queries real-time public transport data (via HAFAS and WienerLinien) to find the best connections. +4. **Leave Status:** The dashboard displays a clear status: `Leave now`, `On time`, `Delayed +X min`, or `Departure missed`. + +--- + +## 🌐 Web Dashboard + +The web dashboard is designed for planning and monitoring your upcoming events from a desktop or laptop. + +### Main Dashboard (`/`) +This is the default view when you open the application. It displays a list of your upcoming events sorted by date. Each event card shows: +- **Event Title & Time:** The name of the appointment and when it starts. +- **Destination Station:** The nearest station to your event. +- **Leave Status:** A color-coded indicator of your current standing: + - πŸ”΄ **Red:** Time is up ("Now") or you're extremely close to departure. + - 🟠 **Orange:** Urgent! You need to leave within 10 minutes. + - 🟑 **Yellow:** Moderate urgency (within 30 minutes). + - 🟒 **Green:** Relaxed (within 60 minutes). + - πŸ”΅ **Blue:** Plenty of time remaining (over 1 hour). + +### Calendar Sync (`/calendar`) +Use this page to connect your personal calendar. +- **URL Import:** Paste a public `.ics` calendar URL. The app will fetch upcoming events automatically. +- **File Import:** Upload a `.ics` file directly from your computer. + +### Event Details (`/event/[id]`) +Click on any event from the dashboard to view detailed planning information: +- **Journey Options:** A list of available trains/buses with scheduled vs. real departure times. +- **Delay Information:** Real-time delays are highlighted. Cancelled journeys are clearly marked. +- **Local Routing:** See how long it takes to bike or walk from your home to the departure station. + +### Manual Event Entry (`/add-event`) +If you don't have a calendar synced, or you have a one-off appointment, you can manually add an event by specifying the title, destination, and time. + +--- + +## πŸ“± Mobile Application + +The mobile app is perfect for on-the-go checks, leveraging your phone's native capabilities. + +### Event List Screen +The home screen mirrors the web dashboard, showing your upcoming events and their real-time status. You can pull-to-refresh to get the latest transit data. + +### Event Detail Screen +Tap on an event to see: +- The best journey options and real-time platform information. +- A countdown timer to your departure. +- Step-by-step bike/walk directions to the station. + +### Calendar Import Screen +Import your calendar directly on the device. The mobile app can read your device's native calendar apps (via `expo-calendar`) if you prefer not to use a remote `.ics` URL. + +### Settings Screen +Customize your experience: +- **Buffer Time:** Set a default buffer (e.g., arrive 5 minutes early). +- **Departure Buffer:** Add extra time for the actual transit journey. +- **Toggle Options:** Enable/disable the walking or biking route suggestions based on your preference. +- **Notifications:** Configure push notifications so you get an alert exactly when it's time to leave. + +--- + +## ⏱️ Understanding "Leave Status" + +The "Leave Status" is the heart of TimeToLeave. It is calculated dynamically by comparing the **real departure time** of your best non-cancelled journey against the **current time**. + +| Status | Meaning | +| :--- | :--- | +| **Leave now** | Your train departs within 15 minutes. Head out! | +| **On time** | Everything is running smoothly, and you have a comfortable window. | +| **Delayed +X min** | Your train is delayed. You can stay home a bit longer! | +| **Departure missed** | The best available journey has already departed. A new search may be required. | +| **All journeys cancelled** | Unfortunately, all connections for this time slot are cancelled. | + +## πŸ”’ Privacy & Data + +TimeToLeave is designed with privacy in mind: +- Calendar data is processed server-side solely for the purpose of event extraction and is not permanently stored beyond the active session. +- Geolocation data is used exclusively for calculating routes and finding nearby stations. It is never shared with third parties. + +--- + +*Happy traveling! Built for developers who bike to the train and hate missing their connections.* diff --git a/eslint.config.mjs b/eslint.config.mjs index 02fa855..820556f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,9 @@ import js from "@eslint/js"; import ts from "typescript-eslint"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const runtimeGlobals = { AbortSignal: "readonly", @@ -17,12 +21,7 @@ const runtimeGlobals = { export default ts.config( { - ignores: [ - "**/dist/**", - "**/.next/**", - "**/node_modules/**", - "**/coverage/**", - ], + ignores: ["**/dist/**", "**/.next/**", "**/node_modules/**", "**/coverage/**"], }, { extends: [js.configs.recommended, ...ts.configs.recommended], @@ -32,6 +31,7 @@ export default ts.config( globals: runtimeGlobals, parserOptions: { sourceType: "module", + tsconfigRootDir: path.resolve(__dirname), }, }, }, diff --git a/tsconfig.json b/tsconfig.json index 5b99469..fe3dc51 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1 +1,22 @@ -apps/web/src/* +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [], + "exclude": ["node_modules"], + "references": [ + { "path": "apps/web" }, + { "path": "apps/mobile" }, + { "path": "packages/core" }, + { "path": "packages/api-client" } + ] +}