A modern PWA for visualizing and analyzing cycling tours. Import GPX files, explore your ride history on an interactive map, spot duplicates, and review stats — all in the browser, offline-capable.
- GPX import — drag & drop or file picker, batch import (100s of files at once)
- Deduplication — automatically detects identical rides imported multiple times
- Interactive map — dark CartoDB basemap rendered via MapLibre GL (WebGL)
- Heatmap mode — density heatmap showing your most-ridden areas
- Route visualization — per-route color, visibility toggle, click-to-select
- Yearly / monthly stats — distance, elevation, count with bar charts
- Year filter — quickly scope the map and stats to a single year
- Offline — Service Worker caches app shell + map tiles (CartoDB Dark)
- PWA installable — runs as a standalone app on desktop and mobile
- Node.js ≥ 20
- npm (or pnpm)
npm install
npm run dev # bundles GPX worker + starts Next.js dev serverOpen http://localhost:3000.
npm run build
npm run startdocker-compose up --buildnpm test # Vitest unit tests
npm run e2e # Playwright E2E (requires running server on :3000)
npm run test:all # bothapp/
page.tsx Full-screen layout: collapsible sidebar + MapLibre map
api/sw/route.ts Service Worker (offline fallback, tile cache)
components/
map/map-container.tsx MapLibre GL map — routes, heatmap, selection
sidebar/sidebar.tsx Route list, year filter, search, sort
sidebar/route-card.tsx Per-route card with color picker + delete
sidebar/stats-panel.tsx Distance/elevation totals, yearly/monthly bars
import/dropzone.tsx Drag & drop import UI with progress + error reporting
lib/
types.ts Core types (Route, RouteStats, MapMode …)
store.ts Zustand global store + selectors
gpx/parse.ts GPX XML parser (RDP simplification, stats, dedup fingerprint)
gpx/dedup.ts Route fingerprinting and duplicate detection
gpx/parse.worker.ts Web Worker entry point (bundled → public/parse.worker.js)
storage/db.ts IndexedDB storage via idb
| Concern | Choice | Why |
|---|---|---|
| Map rendering | MapLibre GL JS | WebGL, native heatmap, fast with 1000s of points |
| Basemap | CartoDB Dark (raster) | Free, no API key, dark look that makes routes pop |
| State | Zustand | Minimal, avoids prop-drilling across map + sidebar |
| Parsing | Web Worker + fast-xml-parser | Non-blocking UI even for large GPX batches |
| Storage | IndexedDB (idb) | Browser-native, works offline, large capacity |
| Dedup | Coordinate fingerprint | Start/mid/end snapped to ~110m grid + distance bucket |
No server, no account. All GPX data is stored in the browser's IndexedDB. The only external requests are map tile fetches from CartoDB (cached by the Service Worker).
MIT