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.

  • React
  • TypeScript
  • Tailwind CSS
  • Recharts
  • PWA
  • Vitest

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.