Skip to content

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