diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..87a2f1a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# dependencies +node_modules +npm-debug.log* + +# next.js +.next/ +out/ + +# misc +.DS_Store +*.pem +coverage/ + +# env files +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# IDE / editors +.idea/ +.vscode/ +.zed/ +.claude/ + +# git +.git/ +.gitignore + +# docker +Dockerfile +docker-compose.yml +.dockerignore + +# docs +AGENTS.md +CLAUDE.md +CHECKLIST.md +REWRITE_PLAN.md + +# tests +vitest.config.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6bd487d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# ---- Stage 1: Install dependencies ---- +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +# ---- Stage 2: Build the app ---- +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# ---- Stage 3: Production image ---- +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy built artifacts +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json + +# Use standalone output if available, otherwise copy .next +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# Ensure correct permissions +RUN chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 3aa50ad..2aee71e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,150 @@ -# TimeToLeave - Next.js Rewrite +# TimeToLeave -This is a rewrite of the TimeToLeave application using Next.js App Router with TypeScript. +> Know when to leave the house so you're never late for your next meeting. -## Development +TimeToLeave is a web app that tells you **when to leave** for upcoming events based on real-time Austrian train (ÖBB) schedules — and optionally compares train journeys with bicycle routes. + +## Features + +- **Real-time train journeys** — Query the ÖBB HAFAS API for live departure times, delays, and platform information. +- **Calendar import** — Import events from any ICS calendar (CalDAV, Google Calendar, etc.). +- **Bicycle routing** — Compare train journeys with bicycle routes powered by OSRM, including distance, duration, and turn-by-turn directions. +- **Train vs bicycle comparison** — See both travel modes side by side for each event, so you can decide what works best. +- **Calendar month view** — Browse events in a month grid, see event indicators on dates, and drill into train/bike details per day. +- **Geolocation support** — Use your browser's current location as the origin station. +- **Countdown & status indicators** — Visual countdown timers and live status badges so you know exactly when to go. + +## Architecture + +Built with **Next.js App Router** (v16), **React 19**, **TypeScript**, and **Tailwind CSS v4**. + +``` +src/ +├── app/ # Next.js App Router pages & layout +│ ├── api/ # Server-side API routes +│ │ ├── hafas/ # ÖBB train journey search +│ │ ├── calendar/ # ICS calendar parsing +│ │ ├── geocode/ # Nominatim geocoding +│ │ ├── bike-route/ # OSRM bicycle routing +│ │ └── health/ # Server health check +│ ├── ui/ # Shared UI components +│ ├── page.tsx # Home page +│ └── layout.tsx # Root layout +├── hooks/ # Custom React hooks +│ ├── useJourneys.ts # Fetch train journeys per event +│ ├── useBikeRoute.ts # Fetch bicycle routes +│ ├── useCalendar.ts # Calendar import & parsing +│ ├── useEventsStore.tsx # Events state management +│ ├── useGeolocation.ts # Browser geolocation +│ ├── useOriginStation.ts # Origin station selection +│ ├── useClock.ts # Live clock for countdowns +│ └── useServerHealth.ts # API health monitoring +├── lib/ # Shared utilities & clients +│ ├── hafas-client.ts # ÖBB HAFAS API client +│ ├── geocoding-client.ts # Nominatim geocoding client +│ ├── bike-routing-client.ts# OSRM bicycle routing client +│ ├── calendar-utils.ts # ICS parsing utilities +│ ├── countdown-utils.ts # Countdown time calculations +│ ├── formatting.ts # Date/time formatting helpers +│ └── constants.ts # App-wide constants +├── types/ # TypeScript type definitions +└── test/ # Test utilities +``` + +## Getting Started + +### Prerequisites + +- Node.js 20+ +- npm (or yarn / pnpm) + +### Installation + +```bash +npm install +``` + +### Development ```bash npm run dev ``` -## Building +The app will be available at [http://localhost:3000](http://localhost:3000). + +### Build & Start ```bash npm run build +npm start ``` -## Testing +### Docker Deployment + +Build and run with Docker Compose: ```bash -npm test -``` \ No newline at end of file +docker compose up --build -d +``` + +The app will be available on port `3000` by default. Override the port or any other variable by creating a `.env` file at the project root: + +```env +PORT=8080 +HAFAS_URL=https://fahrplan.oebb.at/bin/mgate.exe +NOMINATIM_URL=https://nominatim.openstreetmap.org +NOMINATIM_USER_AGENT=TimeToLeave/2.0 +OSRM_URL=https://router.project-osrm.org +``` + +The `Dockerfile` uses a 3-stage build (deps → builder → runner) with `output: "standalone"`, producing a small final image that runs as a non-root user. Stop and remove the container with `docker compose down`. + +### Testing + +```bash +npm test # Run tests once +npm run test:watch # Run tests in watch mode +``` + +### Linting & Type Checking + +```bash +npm run lint +npm run typecheck +``` + +## Environment Variables + +The following environment variables can be configured (defaults are provided for development): + +| Variable | Description | Default | +|----------|-------------|---------| +| `HAFAS_URL` | ÖBB HAFAS API endpoint | `https://fahrplan.oebb.at/bin/mgate.exe` | +| `NOMINATIM_URL` | Nominatim geocoding endpoint | `https://nominatim.openstreetmap.org` | +| `NOMINATIM_USER_AGENT` | User-Agent for Nominatim requests | `TimeToLeave/2.0` | +| `OSRM_URL` | OSRM routing endpoint | `https://router.project-osrm.org` | + +## Tech Stack + +| Category | Technology | +|----------|-----------| +| Framework | [Next.js 16](https://nextjs.org/) (App Router) | +| UI | [React 19](https://react.dev/) + [Tailwind CSS v4](https://tailwindcss.com/) | +| Language | [TypeScript 5](https://www.typescriptlang.org/) | +| Train Data | [ÖBB HAFAS API](https://fahrplan.oebb.at/) | +| Geocoding | [Nominatim](https://nominatim.openstreetmap.org/) | +| Bicycle Routing | [OSRM](https://project-osrm.org/) | +| Calendar | [node-ical](https://www.npmjs.com/package/node-ical) | +| Dates | [date-fns](https://date-fns.org/) | +| Testing | [Vitest](https://vitest.dev/) + [React Testing Library](https://testing-library.com/) | + +## How It Works + +1. **Add events** — Manually enter events or import from an ICS calendar URL. +2. **Pick an origin** — Use your current location or search for a departure station. +3. **Get journey info** — The app queries ÖBB for real-time train schedules and optionally calculates bicycle routes via OSRM. +4. **Decide when to leave** — Each event shows a countdown, the best available train, and (if enabled) a bicycle alternative with estimated travel time. + +## License + +MIT diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ea2527d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: time-to-leave + restart: unless-stopped + ports: + - "${PORT:-3000}:3000" + environment: + - NODE_ENV=production + - HAFAS_URL=${HAFAS_URL:-https://fahrplan.oebb.at/bin/mgate.exe} + - NOMINATIM_URL=${NOMINATIM_URL:-https://nominatim.openstreetmap.org} + - NOMINATIM_USER_AGENT=${NOMINATIM_USER_AGENT:-TimeToLeave/2.0} + - OSRM_URL=${OSRM_URL:-https://router.project-osrm.org} diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig;