How it was built
Building the Habit Tracker
A local-first, installable PWA where the tricky parts — dates, streaks, schedules — were built test-first as pure functions before any UI existed. The most heavily engineered project on this site, by design.
The idea
Habit apps tend to fail in one of two ways: they demand an account and quietly own your data, or they're so bare-bones they stop being motivating. The brief was a tracker that's genuinely pleasant to open daily and completely private — which dictated the architecture: no backend at all. All data on-device, app installable and offline, motivation features (streaks, heatmap, nudges) computed locally from first principles.
Under the hood
Storage: one key, versioned, validated
Everything lives in localStorage under habit-tracker:v1, accessed only
through a typed store module — so a future schema migration or IndexedDB swap touches
one file. The model is two arrays: habits (type, optional target/unit,
scheduled weekdays, createdAt, soft-delete archivedAt) and entries
(habitId, date, value), where the absence of an entry means "not done." Every load
runs full shape validation; corrupted data produces a visible warning and a recovery
path, never a silent wipe.
A test-driven pure-function core
Date and streak logic is where habit trackers quietly lie to their users. Here it's a
lib/ of pure TypeScript functions — dates, completion math, streaks,
aggregation, nudges — written test-first with Vitest before the UI existed. The edge
cases have named tests: weekday-only schedules, archived habits mid-streak,
week/month/quarter/year boundaries, export/import round-trips. Completion only counts
days that are after createdAt, before archivedAt, and
actually scheduled — so a Mon/Wed/Fri habit can't lose its streak on a Sunday.
The nudge engine
One priority-ordered pure function: (habits, entries, today) → nudge | null.
Streak-in-danger beats week-slump beats habit-gone-cold beats positive reinforcement,
so the user sees exactly one message, and the most actionable one. Wording is picked
from template pools by hashing the date — varied day to day with zero stored state.
PWA packaging
vite-plugin-pwa generates the manifest and a Workbox service worker that
precaches the entire app — installable on iOS, Android, and desktop, fully functional
offline. Charts are Recharts; the heatmap and progress ring are hand-rolled SVG;
styling is Tailwind with system-preference dark mode.
The portfolio demo
The demo here is the unmodified production build plus one seed script: on first visit, if no data exists, it writes five sample habits with six weeks of deterministic pseudo-random history (improving over time, streaks left alive) so the charts and nudges demonstrate themselves. It never overwrites existing data — the local-first design guaranteed that for free.
Key design decisions
Local-first, no accounts
Habit data is intimate. On-device storage removes the privacy question and makes the app free to run forever; JSON export/import bridges devices without a server.
TDD where it hurts
UI bugs are visible; streak-math bugs silently erode trust. The pure-logic layer got the test rigor, and the UI got manual verification at phone and laptop widths.
Scheduled days, honest stats
Stats that punish unscheduled days train users to game the tracker. Counting only scheduled days keeps the numbers truthful and the motivation fair.
Ruthless v1 scope
Notifications, sync, per-habit colors, timed habits — all explicitly cut and written down for v2. A tracker people actually open daily beats a feature list.
The Claude Code process
This project used the most structured process on the site: a brainstormed design spec (screens, data model, nudge rules, and the cut list) was written and approved before implementation, then built with Claude Code test-first from the data layer up — storage, then the tested logic core, then pages, then PWA packaging. The spec lives in the repo alongside the code, which is why this page could quote its decisions verbatim.