All projects07 / 08

Copenhagen Pass Maximizer

A trip planner that works out which city card pays off.

retired2026

07retired

  • automated tests73
  • itinerary variants per trip5
  • card validity it is built around96 h

01 / Problem

A city card only pays off if the plan is feasible

City passes are sold on 'save up to X%'. Whether they save money depends on opening hours, one-visit-per-attraction rules, transit times and the exact minute the card expires, and nobody checks those. I built a planner that checks them for one real trip.

Copenhagen Pass Maximizer trip overview in light mode: departure and card-expiry cards, a card validity bar and a side panel with top warnings

02 / How it works

Research as typed data, rules as pure functions

Every attraction, price, opening hour and pass rule is a typed constant with a source link. Pass expiry, feasibility and the warning engine are pure functions with no I/O, and the React UI only displays their results. Five itinerary variants, such as rainy day or low energy, can be switched per day.

Itinerary page in dark mode: plan variant buttons, day tabs and a day timeline with a confirmed-flight notice and a warning

03 / The hard part

Edge cases that quietly cost money

Does a train count if the journey ends after the card expires, and does a daylight-saving change shift the expiry? I wrote tests for expiry, exclusivity, DST and the 72-hour versus 96-hour difference. A data integrity suite fails if any fact lacks a source citation or a fallback block points nowhere, so the research is checked like code.

Calculator page in light mode: inputs for party size, card type, duration and activation time beside a pass status panel

04 / Result

Usable on patchy roaming data

The finished app is an installable offline PWA, so it keeps working abroad on a weak connection, and it has 73 automated tests. Its warning engine flagged visits outside opening hours and unverified facts before the trip. After the trip I retired it.

Mobile itinerary view with the day header, plan variant chips and a bottom navigation bar

How it works

Facts in, feasibility out

Research is typed data with a source for every fact. The rules are pure functions over it, and the interface only displays their answers.

The same flow as a list, in order.

  1. Inputs: Research data. Typed facts, each with a source link. Goes to Pass expiry and exclusivity, Feasibility.
  2. Inputs: Trip inputs. Party, card type, duration, activation time. Goes to Pass expiry and exclusivity, Feasibility.
  3. Inputs: Itinerary variants. Five, such as rainy day or low energy. Goes to Feasibility.
  4. Rules: Pass expiry and exclusivity. Pure functions, no I/O. Goes to Warning engine.
  5. Rules: Feasibility. Opening hours, one visit each, transit. Goes to Warning engine.
  6. Check: Warning engine. Flags visits outside hours and unverified facts. Goes to React interface.
  7. Show: React interface. Displays results only. Goes to Installable PWA.
  8. Offline: Installable PWA. Service worker keeps it working on roaming data. End of the path.

Stack, by layer

App
ViteReactTypeScriptReact Router
Offline
PWA service worker
Quality
VitestData integrity tests
Delivery
nginxDocker

Live from the Copenhagen Pass Maximizer repoOffline, last known values

Commits
n/a
Last commit
n/a
Service
Retired