Skip to content

Repository files navigation

Tip Calculator

An offline-capable tip calculator PWA. No build step, no dependencies — the files that ship are the files in this repo.

Running it

npm run serve      # http://localhost:8000

A plain file server is enough, but the service worker needs http://localhost or HTTPS; opening index.html over file:// will not register it.

Tests

npm test

Two suites, no dependencies:

  • test/calc.test.js runs app.js unmodified against a DOM stub (test/dom-stub.js) rather than pulling in jsdom. It covers the money arithmetic — including a sweep of every bill from $0.01 to $500.00 against all four rates, asserting the displayed tip plus the bill always equals the displayed total — and the custom panel's field-derivation rules.
  • test/sw.test.js runs sw.js against a service worker stub (test/sw-stub.js) with a manually driven clock, covering the precache, cache eviction, and the offline and slow-network fallbacks.

Layout

Path What it is
index.html Markup; the only page
app.js All behaviour: calculations, custom panel, SW registration
style.css Styles and design tokens
sw.js Service worker — precache and fetch strategy
manifest.json Web app manifest
fonts/ Self-hosted Outfit (variable, 47KB for both subsets)
icons/ Generated icon set — do not edit by hand
icon.png Master artwork the icon set is generated from
tools/build-icons.py Regenerates icons/ from icon.png
tools/stage-dist.sh Assembles dist/ — the files that actually ship
CLAUDE.md Notes for agents working in this repo

Money is handled in cents

Every amount is an integer number of cents, and each card's total is derived from its already-rounded tip. Rounding the tip and the total independently at float precision lets them disagree once displayed — a $1.25 bill at 18% used to render a $0.22 tip beside a $1.48 total. If you touch the arithmetic, keep the sweep in test/calc.test.js green.

Version stamp

The bottom of the page shows the running build, e.g. v4 · 826bab6. It is stamped by tools/stage-dist.sh from VERSION in sw.js plus the short commit hash, so there is no second place to bump. Served straight from the repo it reads dev.

Because assets are cache-first, this reports the build actually running, not the one currently deployed — which is the point: if it lags after a deploy, the old bundle is still cached.

The custom panel

The three custom fields are three views of one number. Exactly one of them is authoritative — whichever was last typed into — and the other two are derived from it and the bill. The authoritative percent is kept unrounded in memory even though it displays to one decimal, so a later bill edit does not recalculate from the rounded figure.

A value that cannot produce a tip (a negative bill or percent, or a total below the bill) marks its own field invalid and blanks the derived fields rather than rendering negative money.

Deploying

Pushing to main deploys to Cloudflare Pages (tip-calchttps://tip.prenticew.com) via .github/workflows/deploy.yml. Tests must pass first. Pull requests run the tests but do not deploy.

The workflow stages only the files that belong on the site (npm run builddist/), so tests, tooling, docs and the 873KB master artwork are not published. It resolves the project's production branch from the Cloudflare API rather than assuming it — guessing wrong publishes a preview deployment, which succeeds loudly while leaving the live site untouched.

Two repository secrets are required:

Secret Where it comes from
CLOUDFLARE_API_TOKEN Cloudflare → My Profile → API Tokens, with the Cloudflare Pages: Edit permission
CLOUDFLARE_ACCOUNT_ID Cloudflare dashboard URL, or Workers & Pages → Account details

Every path is relative, so the app works from a domain root or a subdirectory (project pages, preview deploys) without changes.

Bump VERSION in sw.js on every deploy. Assets are cache-first and keyed by that constant; without a bump, returning visitors keep the old files. The worker calls skipWaiting()/clients.claim() and the page reloads once when a new worker takes over, so an update lands on the next load rather than waiting for every tab to close.

Offline behaviour

Everything the app needs is precached, and nothing loads off-origin, so an installed app works fully offline — it is pure arithmetic with no backend.

Navigations are network-first with a 3 second timeout (NAVIGATION_TIMEOUT_MS in sw.js). True offline is the easy case: fetch rejects immediately and the cached shell is served. A degraded connection is the hard one — a captive portal or one bar of signal leaves fetch hanging, which would strand the user on a blank screen while a good cached copy sits unused. After the timeout the cache is served instead; if the abandoned request later lands, its response still refreshes the cache for next time.

The app must be loaded online once for the worker to install. iOS may evict script-writable storage, the Cache API included, after prolonged non-use, in which case one online load restores it.

Regenerating icons

pip install Pillow
npm run icons

Edit icon.png and re-run; icons/ is generated output. The script flattens the master's radial glow, which is what kept the original 894KB icon from compressing, and fits the maskable variant inside the centre-80% safe zone.

Licence

MIT © 2026 Prentice Wongvibulsin.

The Outfit typeface in fonts/ is licensed separately under the SIL Open Font License 1.1, which the MIT licence above does not cover.

About

A Progressive Web Application (PWA) tip calculator that can easily be installed on your mobile device and accessed completely offline.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages