A lightweight, accessible todo app built with React and TypeScript. Todos persist across page reloads via localStorage and the UI supports both light and dark themes.
Live demo: https://alan-eicker.github.io/claude-code-todo-list/
| Layer | Tool |
|---|---|
| UI | React 18 |
| Language | TypeScript 5 (strict mode) |
| Build | webpack 5 + Babel |
| Styling | CSS Modules + CSS custom properties |
| Unit tests | Jest 29 + React Testing Library + jest-axe |
| E2E tests | Playwright + @axe-core/playwright |
| Linting | ESLint 9 (flat config) |
| Formatting | Prettier |
- Node.js 20+ — the CI pipeline runs on Node 20; using an older version may produce unexpected results.
- npm 10+ — comes bundled with Node 20.
# 1. Clone the repository
git clone https://github.com/alan-eicker/claude-code-todo-list.git
cd claude-code-todo-list
# 2. Install dependencies
npm install
# 3. Install Playwright browsers (required for E2E tests)
npx playwright install --with-deps chromium
# 4. Start the development server
npm run devThe app is served at http://localhost:8080.
| Script | Description |
|---|---|
npm run dev |
Start the webpack development server at http://localhost:8080 |
npm run build |
Produce a production build in dist/ |
npm run preview |
Serve the production build locally via npx serve |
npm run lint |
Run ESLint across the entire project |
npm run format |
Format all files with Prettier |
npm run format:check |
Check formatting without writing (used in CI) |
npm test |
Run all Jest unit and integration tests |
npm run test:watch |
Run Jest in watch mode |
npm run test:coverage |
Run Jest and generate a coverage report in coverage/ |
npm run test:e2e |
Run all Playwright E2E tests (starts the dev server automatically) |
npm run test:e2e:ui |
Open the Playwright interactive UI |
npm run test:e2e:debug |
Run E2E tests in debug mode |
todo-list/
├── e2e/ # Playwright E2E tests
│ ├── todo-app.spec.ts # Feature and user-flow tests
│ └── accessibility.spec.ts # Axe accessibility checks
├── src/
│ ├── components/ # Shared, reusable UI components
│ │ └── ThemeToggle/
│ ├── features/ # Feature modules (vertical slices)
│ │ └── Todos/
│ │ ├── components/ # UI components scoped to this feature
│ │ │ ├── TodoFilter/
│ │ │ ├── TodoForm/
│ │ │ ├── TodoItem/
│ │ │ └── TodoList/
│ │ ├── hooks/
│ │ │ └── useTodos.ts # State management for the todo feature
│ │ ├── types.ts # Feature-specific TypeScript types
│ │ └── index.ts # Public barrel export
│ ├── hooks/ # Shared custom hooks
│ │ ├── usePersistedReducer.ts # localStorage persistence wrapper
│ │ └── useTheme.ts # Light/dark theme management
│ ├── styles/
│ │ └── global.css # CSS design tokens and global resets
│ ├── App.tsx
│ ├── main.tsx
│ └── setupTests.ts # Jest global setup (jsdom polyfills)
├── .github/workflows/
│ └── deploy.yml # CI/CD pipeline (test → deploy to GitHub Pages)
├── .vscode/
│ └── settings.json # Prettier format-on-save config
├── eslint.config.mjs
├── jest.config.ts
├── playwright.config.ts
├── webpack.config.js
├── tsconfig.json # Root — composite project references only
├── tsconfig.app.json # App source type-checking (browser target)
├── tsconfig.test.json # Jest/ts-jest overrides (Node/CommonJS target)
└── CLAUDE.md # Engineering standards and AI coding guidelines
Each component follows the colocation pattern:
ComponentName/
├── index.tsx
├── ComponentName.module.css
└── ComponentName.test.tsx
The Todos feature uses a useReducer-based approach with a discriminated union action type. State is persisted to localStorage via the shared usePersistedReducer hook, which hydrates from storage on mount and syncs on every dispatch.
App
└── useTodos (state + actions via usePersistedReducer)
├── TodoForm (local input state via useState)
├── TodoFilter (reads filter, dispatches SET_FILTER)
└── TodoList
└── TodoItem (memoised; receives stable callbacks via useCallback)
Theme preference (light | dark) is stored under the theme-preference key in localStorage. An inline <script> in index.html reads this value and sets data-theme on <html> before the first paint, preventing a flash of the wrong colour scheme. The useTheme hook then manages subsequent toggles.
All user-generated state goes through usePersistedReducer. Direct localStorage reads and writes in components are prohibited — use the hook.
The project uses three tsconfig files, each with a distinct purpose:
| File | Purpose |
|---|---|
tsconfig.json |
Root config. Contains no compiler options — acts only as a composite project reference pointer for tsc and VS Code. |
tsconfig.app.json |
Used for type-checking application source (src/). Targets modern browsers (ES2020, DOM libs), enforces strict mode, and sets noEmit: true since webpack/Babel handle transpilation. |
tsconfig.test.json |
Used by Jest via ts-jest. Overrides module: "CommonJS" and moduleResolution: "node" because Jest runs in Node, not a browser bundler. Also sets noEmit: false so ts-jest can emit code to execute. |
The separation prevents test-environment overrides (CommonJS modules, Node resolution) from leaking into the app type-check and vice versa. Always run npx tsc --noEmit against tsconfig.app.json (the default) to validate application types.
npm test # run all tests
npm run test:coverage # run with coverage reportCoverage thresholds are enforced at 80% for branches, functions, lines, and statements. The build fails if any threshold is not met.
Every component test includes an axe accessibility assertion via jest-axe. A failing axe check is treated as a bug and blocks merging.
npm run test:e2e # headless Chromium, starts dev server automatically
npm run test:e2e:ui # interactive Playwright UI
npm run test:e2e:debug # step through tests with the Playwright inspectorE2E tests cover all primary CRUD operations, filtering, data persistence across page reloads, theme toggling, and full-page axe scans. Each test resets localStorage before running to ensure isolation.
The Playwright report is written to playwright-report/. Open playwright-report/index.html to inspect results after a run.
Deployment is fully automated via GitHub Actions. It is triggered by pushing a version tag to the master branch.
# 1. Bump the version in package.json
npm version patch # or minor / major
# 2. Push the commit and the generated tag
git push origin master --follow-tagsThe workflow (.github/workflows/deploy.yml) will:
- Verify the tag is on
master - Run lint, type-check, Jest (with coverage), and Playwright
- Upload the coverage report as a CI artifact (retained for 30 days)
- Build the app with the correct GitHub Pages base path
- Deploy
dist/to GitHub Pages
The deploy job will not run if any test step fails.
To verify the production build locally before tagging a release:
npm run build
npm run previewThis is the correct way to run Lighthouse audits — the dev server produces artificially low performance scores.
The repo includes .vscode/settings.json that configures Prettier as the default formatter with format-on-save enabled. Install the Prettier – Code formatter VS Code extension and formatting will work automatically.
No additional editor configuration is required.
- Read
CLAUDE.md— it defines the engineering standards for this project, covering state management, styling, accessibility, testing, performance, and security. All rules there apply to every contribution.
A task is not done until all four gates pass locally:
npm run lint # zero errors
npx tsc --noEmit # zero type errors
npm test # all tests pass, coverage ≥ 80%
npm run test:e2e # all E2E tests passCI enforces the same gates on every tagged release. Do not open a PR with failing checks.
- Arrow functions everywhere — no
functiondeclarations for components or hooks. - Named exports — default exports only at page/route boundaries.
- Feature barrel imports — import from
src/features/Todos(theindex.ts), never from internal feature paths. - No direct
localStorageaccess — useusePersistedReducerfor persistent state. - New components require tests — including at minimum an axe assertion and a render smoke test.
- New E2E flows go in the appropriate spec file — feature flows in
todo-app.spec.ts, accessibility checks inaccessibility.spec.ts.