This code runs the Reform Map app for the Parking Reform Network: https://parkingreform.org/mandates-map/.
We do not use frameworks like React or Svelte to keep things simple. However, we do use these techniques:
- TypeScript
- Sass and the folder
src/css/theme, which should stay aligned with https://github.com/ParkingReformNetwork/parking-lot-map - Reactive state management - see State diagram
The main files are index.html, src/js/main.ts, and data/*.json. main.ts will load the JSON data to load all our data.
The database is stored in Directus and synced nightly to JSON to simplify how the app consumes the data.
Prerequisites:
- Install Node Package Manager (npm).
If you are using Windows OS, install Windows Subsystem for Linux (WSL). Currently, there are 2 versions out. WSL 1 will run npm way faster1. You can switch to version 1 with wsl --set-version Ubuntu 1. Run all npm commands in wsl/Ubuntu.
- Run
npm iin the main folder.
❯ npm startThen open http://127.0.0.1:1234 in a browser. Hit CTRL-C to stop the development server.
When the server is running, you can make any changes you want to the project. Reload the page in the browser to see those changes. (You may need to force reload, e.g. hold the shift key while reloading on macOS.)
We write our code in TypeScript. The types are ignored when starting the server and running tests, but it's useful to manually check for any errors caught by TypeScript:
❯ npm run check❯ npm testIf the tests are taking a long time to start or have unexpected failures, run rm -rf .parcel-cache and try the tests again.
We use Playwright snapshot tests, which save a "snapshot" to the filesystem of the expected output, such as how our generated pages render. If the expected output ever changes, you can use Playwright to regenerate the snapshots with npx playwright test -u.
We use Biome to nicely format code.
❯ npm run fmtBefore pushing code, run this command and commit the changes. Otherwise, PR checks will not pass.
"Linting" means using tools that check for common issues that may be bugs or low code quality.
❯ npm run lintTo auto-fix issues, run npm run fix.
You can preview what a build will look like by running npm run build. Then use npm run serve-dist to start the server. A 'build' are the files sent for production on the real site. This is slightly different from the development server run by npm start, which prioritizes a quick start for development.
npm run test-dist will be implemented soon, while npm test is the development equivalent.
All icons are inlined as an SVG sprite in index.html using symbols from Font Awesome Free 6.7.2 (CC BY 4.0).
Using an icon:
In TypeScript, import iconHtml() or createIcon() from src/js/layout/icons.ts:
import { iconHtml, createIcon, type IconName } from "./layout/icons";
// As HTML string (e.g., for setting innerHTML)
const html = iconHtml("magnifying-glass");
element.innerHTML = html;
// As a DOM element
const icon = createIcon("magnifying-glass", "my-class");
element.appendChild(icon);Adding a new icon:
-
Choose an icon from Font Awesome Free 6.7.2 (only Free tier icons are permitted).
-
Download or copy the SVG path data (the
d="..."attribute). -
Add a new
<symbol>in index.html withid="icon-{name}":<symbol id="icon-my-icon" viewBox="0 0 512 512"> <path fill="currentColor" d="..." /> </symbol>
-
Add the icon name to the
IconNametype in src/js/layout/icons.ts.
npm run benchmark measures the high-level tasks a user performs — initial page load, opening the table view, switching the policy-type filter (to "reduce minimums" and back to "any reform"), and loading the search widget — plus cold-load transfer size, using a headless browser. It expects a production build to already be served, so in one terminal run:
❯ npm run build
❯ npm run serve-distThen, in another terminal:
❯ npm run benchmarkIt runs a few times with the browser cache disabled (so bundle size counts) and writes benchmark-results/latest.json for before/after comparison. Each task is reported as one overall median (with min/max) plus the granular stages that make it up, indented underneath. For example, the initial page load breaks down into network, first paint, data fetch, JS build, and marker paint, which sum to the total; the filter and search tasks split their time-to-paint into a synchronous JS portion and the remaining paint. Options: --runs N, --out <path>, --headed, and PORT to override the port.
To compare two saved benchmark files, use scripts/compare-benchmarks.py benchmark-results/<before.json> benchmark-results/<after.json>. It prints each task's total delta (absolute and percent change) with its stage deltas indented underneath, and warns if the two runs have different place counts, since that would make the comparison not apples-to-apples.
We use continuous deployment, meaning that we re-deploy the site every time we merge a pull request to staging at https://parkingreform.org/mandates-map-staging/. You can check how the site renders about ~1-2 minutes after your change merges.
First, check that staging looks good at https://parkingreform.org/mandates-map-staging/.
Then, when ready, click "Run workflow" at https://github.com/ParkingReformNetwork/reform-map/actions/workflows/deploy-map-prod.yaml with the default option. Check that the deploy worked by confirming https://parkingreform.org/mandates-map/ looks good.
You usually should not need to manually do this. We have a GitHub Action that runs every night to open a PR with any updates.
You can trigger the GitHub Action to run early by clicking "Run workflow" at https://github.com/ParkingReformNetwork/reform-map/actions/workflows/update-data.yaml with the default option, if you're an admin. This will create a pull request that you then need to merge.
To instead manually update the data, first run npm install. Then, run npm run sync-directus.
This shows the main user interactions on the map, and what triggers what.
graph TD
E[zoom buttons]
F[zoom level]
G[map position]
H[user scrolling]
Q[table entries]
T[counter text]
AE[AND]
U[filter value]
V[user interaction]
X[search value]
Y[user input]
Z[view icon]
AA[view: map-table]
AB[selected cities]
AC[AND]
AD[shown cities]
AF[scorecard]
AG[clicking city dot]
AH[clicking outside popup]
%% Relationships
Z -->|toggles| AA
E -->|controls| F
X -->|resets| F
H -->|controls| G
X -->|resets| G
Y -->|controls| X
V -->|controls| U
X --> AC
U --> AC
AC -->|controls| AB
AB -->|controls| Q
AB -->|controls| AD
X --> AE
U --> AE
AB --> AE
AE -->|controls| T
AG -->|opens up| AF
AH -->|closes| AF
X -->|opens up| AF
AF -->|resets on close| X
We use reactive programming for state management. See https://github.com/ParkingReformNetwork/parking-lot-map/blob/main/README.md#state-diagram for an explanation.
Our image assets (screenshots and attachments) are stored in Directus at mandates-map.directus.app. However, we proxy all requests for these assets through a Cloudflare Worker at assets.parkingreform.org to protect against scrapers and reduce Directus bandwidth costs. Public access to the assets is restricted to requests containing an authorization token.
The Worker:
- Blocks known AI crawlers and scrapers by user agent
- Checks Cloudflare's edge cache — if the asset is cached, Directus is never contacted
- On a cache miss, fetches the asset from Directus using a service account token and caches it for 30 days
- Passes through the response to the user