Architecture¶
This page explains the key modules and data flow that make up Tally.
High-level overview¶
┌──────────────────────────────────────────────┐
│ Browser (React SPA) │
│ │
│ main.tsx → App.tsx │
│ │ │
│ useAppState (hook) │
│ │ │
│ ┌────────────┼────────────────┐ │
│ │ │ │ │
│ Tabs Components persistence.ts │
│ (tabs/) (PlayerBar, …) (localStorage) │
└──────────────────────────────────────────────┘
All application state lives in a single useAppState React hook.
Every mutation calls saveState, which serialises the new state to localStorage.
There is no remote API and no external state-management library.
Folder structure¶
src/
├── App.tsx # Root component — wires state hook + tab routing
├── main.tsx # React entry point
├── style.css # Global styles
├── index.css # CSS custom properties / design tokens
│
├── types.ts # Shared TypeScript types (AppState, FixedTask, Goal, …)
├── constants.ts # LEVELS, ACHS, FREQ_LBL, PRESET_COLORS, STORAGE_KEY
│
├── persistence.ts # Typed persistence layer with schema versioning
│
├── utils/
│ ├── date.ts # today(), weekStart(), weekDates(), uid(), updateStreak()
│ ├── xp.ts # levelFor(), nextLevel(), xpProgress()
│ ├── storage.ts # Thin wrappers: loadState(), saveState(), defaultState()
│ └── esc.ts # HTML-escape helper (prevents XSS in dynamic strings)
│
├── hooks/
│ └── useAppState.ts # Main state hook — all actions + toast queue
│
└── components/
├── PlayerBar.tsx # Player header (avatar, name, XP bar)
├── StatsBar.tsx # Stat cards (streak, tasks, XP, project)
├── TabNav.tsx # Tab navigation bar
├── CatTag.tsx # Reusable category pill tag
├── ToastContainer.tsx # Toast notification overlay
├── tabs/
│ ├── TodayTab.tsx # Today — fixed & variable tasks
│ ├── WeekTab.tsx # This Week — weekly overview & progress
│ ├── ProgressTab.tsx # Progress — project tracker, category chart, achievements
│ ├── GoalsTab.tsx # Goals — active/planned goals
│ ├── LogTab.tsx # Log — activity history
│ └── SettingsTab.tsx # Settings — name, fixed tasks, project tracker, reset
└── onboarding/
└── OnboardingWizard.tsx # 5-step first-run setup wizard
Key modules¶
persistence.ts¶
The single source of truth for all localStorage I/O.
| Symbol | Purpose |
|---|---|
STORAGE_KEY |
The localStorage key (questlog_v3) |
CURRENT_SCHEMA_VERSION |
Current schema version integer (currently 2) |
defaultState() |
Returns a fresh AppState with safe zero-values |
loadState() |
Parses, validates and migrates stored JSON; returns LoadResult |
saveState(state) |
Serialises state with __v stamp and writes to localStorage |
clearState() |
Removes the stored key |
createBackupJson(state) |
Pretty-printed JSON for user download |
validateImport(json) |
Parses and validates a backup; returns { state } or { error } |
Schema migration pattern — each migration is an if (v < N) block in applyMigrations().
To add a migration: increment CURRENT_SCHEMA_VERSION and add the block.
hooks/useAppState.ts¶
Owns the entire runtime AppState.
Exposes fine-grained action callbacks (e.g. completeTask, addGoal, logCounter) that are
passed as props to tab components.
Every action ends with a saveState call — there is no separate "flush" step.
utils/date.ts¶
Pure date utilities: today(), weekStart(), weekDates(), uid(), and updateStreak().
updateStreak accepts an injectable now: Date parameter for deterministic unit testing.
utils/xp.ts¶
XP → level calculation helpers: levelFor(xp), nextLevel(xp), xpProgress(xp).
Driven by the LEVELS constant array in constants.ts.
utils/esc.ts¶
esc(str) — HTML-escapes a string to prevent injection when values are inserted into markup.
Data flow¶
User action (click / form submit)
│
▼
Action callback in useAppState
│
├─ Compute next state (pure transformation)
├─ setAppState(next) ← React re-render
└─ saveState(next) ← localStorage write
On first render:
loadState() ──► applyMigrations() ──► useState initialiser
Design decisions¶
| Concern | Approach |
|---|---|
| State management | Single useAppState hook; mutations are synchronous and persisted on every commit |
| Rendering | All tabs are controlled React components — no innerHTML, no DOM refs for data |
| Toasts | Managed as an array in React state; each toast auto-removes after its display duration |
| Persistence | persistence.ts owns all localStorage I/O; state is loaded once on mount |
| XSS prevention | User-supplied strings rendered in markup are run through esc() |
| Schema migrations | Versioned via __v; applyMigrations upgrades any older payload in-order |