A personal finance Progressive Web App (PWA) + Android application. Record income and expenses, track vehicle fuel and maintenance, and view reports. No backend, no database service — all data lives on your device via IndexedDB.
| Tool | Version | Install |
|---|---|---|
| Node.js | ≥ 20 LTS | https://nodejs.org |
| npm | ≥ 10 (bundled with Node) | — |
| Git | any | https://git-scm.com |
Android builds additionally require Android Studio and the Java 17+ JDK (see Android section).
# 1. Clone the repository
git clone <repo-url>
cd expense-tracker
# 2. Install all dependencies
npm install
# 3. Start the development server
npm run devThe app will be available at http://localhost:5173 (Vite may pick 5174+ if 5173 is in use).
| Command | Description |
|---|---|
npm run dev |
Start Vite dev server with hot-reload |
npm run build |
Type-check + production build → dist/ |
npm run preview |
Serve the production build locally |
npm run test |
Run unit tests once (Vitest run mode) |
npm run test:watch |
Run unit tests in watch mode during development |
npm run lint |
Run ESLint across all of src/ (zero warnings allowed) |
npm run lint:fix |
Run ESLint and auto-fix what it can |
npm run publish:intranet |
Build and expose PWA on local network (PowerShell helper) |
| Layer | Library | Version |
|---|---|---|
| Framework | React | ^19 |
| Bundler | Vite | ^6 |
| Language | TypeScript (strict) | ~5.7 |
| Styling | Tailwind CSS v4 + shadcn/ui | ^4 |
| Routing | react-router-dom | ^7 |
| State | Zustand | ^5 |
| Local DB | Dexie.js (IndexedDB) | ^4 |
| Forms | react-hook-form | ^7 |
| Validation | zod | ^3 |
| Toasts | sonner | ^1 |
| Icons | lucide-react | ^0.475 |
| i18n | react-i18next | ^15 |
| Date utils | date-fns | ^4 |
| ID generation | uuid | ^13 |
src/
├── app/
│ ├── App.tsx # Root component, store bootstrapping, RouterProvider
│ └── router.tsx # All routes (createBrowserRouter)
├── components/
│ ├── ui/ # shadcn/ui primitives — do not edit manually
│ └── layout/ # Shell, Header, BottomNav
├── features/
│ ├── transactions/ # Form, list, schemas
│ ├── vehicles/
│ ├── reports/
│ ├── budgets/
│ ├── insights/
│ └── settings/ # Accounts, categories, labels, exchange rates
├── db/
│ └── index.ts # Dexie singleton — all table definitions
├── stores/ # Zustand stores (one per domain)
├── lib/ # Pure utility functions (currency, budgets, vehicles, dates)
├── hooks/ # Shared custom hooks
├── types/ # TypeScript interfaces and enums
└── i18n/ # react-i18next setup + en.json / es.json
Components are added individually using the shadcn CLI:
npx shadcn@latest add <component-name>
# Examples:
npx shadcn@latest add button
npx shadcn@latest add card badge input label select textarea sheet dialog drawerComponents are generated into src/components/ui/. Do not edit these files manually — re-run the CLI to update them.
The @/ alias maps to src/. Configured in both vite.config.ts and tsconfig.json:
// vite.config.ts
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }
// tsconfig.json
"paths": { "@/*": ["./src/*"] }All data is stored locally in IndexedDB via Dexie.js. No data ever leaves the device unless the user explicitly triggers an export (JSON backup, PDF, CSV) or a cloud sync (Google Drive / Dropbox — planned).
Database tables: transactions, accounts, categories, budgets, vehicles, fuelLogs, vehicleServices, labels, exchangeRates, settings.
Every structural change to the Dexie schema must add a new .version(n) block in src/db/index.ts. Never modify existing version blocks.
The app ships with English (en) and Spanish (es) locales in src/i18n/. All user-facing strings go through react-i18next — no hardcoded strings in JSX.
Capacitor integration is planned but not yet configured. These steps will apply once
@capacitor/coreand@capacitor/androidare added.
# 1. Build the web app
npm run build
# 2. Sync to native project
npx cap sync android
# 3. Open in Android Studio
npx cap open androidnpm run build
# Output: dist/To preview the production build locally:
npm run preview
# Available at http://localhost:4173For Android/local-network testing, use the helper script:
npm run publish:intranetIt will:
- install dependencies (unless skipped)
- build production assets
- start Vite preview on
0.0.0.0:4173 - validate
/,/manifest.webmanifest, and/sw.js - print intranet URLs (
http://<your-ip>:4173/)
Full manual and advanced options are documented in docs/PWA-INTRANET-MANUAL.md.
ESLint is configured in eslint.config.js with TypeScript-aware rules and React hooks checks. The key enforced rule is @typescript-eslint/no-unused-vars: error, which catches unused imports like the one that prompted this setup.
# Check all source files
npm run lint
# Auto-fix what ESLint can (formatting, simple issues)
npm run lint:fixPre-commit gate: Husky runs lint-staged automatically before every git commit. Only staged src/**/*.{ts,tsx} files are checked. If any file has a lint error (including unused imports), the commit is blocked until the issue is fixed.
Unit tests are powered by Vitest.
# Run all tests once
npm run test
# Keep tests running while you code
npm run test:watch
# Run a single file
npm run test -- src/lib/categories.test.tsCurrent regression coverage includes the category label resolver in src/lib/categories.test.ts, including the case where a built-in category is renamed and must show the stored name instead of the locale fallback.
npx tsc --noEmitMust return zero errors before committing.