A sports activity tracking web app for logging workouts, comparing performance across time periods, and visualizing trends with interactive charts. Built with a modern React stack, Azure Static Web Apps, and Firebase for authentication and data storage.
- Dashboard — Quick activity logging with autocomplete, stat cards (total, last 7/30 days, last activity), recent activity feed with clickable navigation, and first-time onboarding with category picker
- Activity List — Full activity history with search, date range filtering, pagination, month grouping, inline editing via dialogs, and bulk operations (rename, reassign category, delete by category)
- Charts — Stacked bar charts and multi-ring pie charts for activity distribution, with group-by-category toggle and date range filtering
- Compare — Side-by-side comparison of activity periods (month vs month, year vs year) with trend line charts and metric cards (total, most popular, most active day/month)
- Settings — Category management (add/edit/delete with activity reassignment), activity name management (rename, assign to category), and chart display preferences
- Export / Import — Full data export to JSON and import from JSON for backup and migration
- PWA — Installable as a Progressive Web App with offline support via service worker
- Dark mode — Dark/light theme toggle, persisted in user preferences
- React 19
- TypeScript 7
- React Router v8 with SPA in framework mode
- TanStack React Query for server state management, caching, and optimistic updates
- TanStack Form for form state and validation
- Tailwind CSS v4 with
@tailwindcss/viteplugin and CSS-first configuration - shadcn/ui components built on Radix UI primitives
- cn for Tailwind class merging, re-exported from
@/utils/cn; Radix wrappers use per-primitiveradix-ui/*imports. - Chart.js with
react-chartjs-2for data visualization - Vite 8 as build tool with HMR and PWA plugin; native TypeScript 7 checks run separately
- Storybook 10 with play functions for component and interaction testing
- Lucide React for icons, Sonner for toast notifications, cmdk for command palette, date-fns for date utilities
- Azure Functions (Node.js 22) — serverless API with 19 functions covering CRUD for activities, categories, preferences, bulk operations (rename, reassign, delete by category), and data export/import
- Firebase Admin SDK for ID token verification and Realtime Database access
- Rate limiting middleware (per-user, per-function)
- Firebase Auth for client-side authentication (Google sign-in provider)
- Firebase Realtime Database as the primary data store
- Azure Static Web Apps for hosting with integrated serverless API routing
Install Bun and a supported Node 22 (>=22.22.1) or Node 24+ runtime, plus Git 2.32+ for lint-staged. Keep Node on PATH: some dependency lifecycle and build scripts invoke it even when run through Bun.
bun install # Install dependencies
bun run dev # Start frontend (:3000) and API (:7071)The app is split into two workspaces (/client and /api) managed as a bun monorepo.
Domain terminology is defined in CONTEXT.md. Three modules own the cross-cutting data behavior:
shared/defines stored records, backup validation, and activity-name ownership rules. Stored records and enriched display records are separate contracts.api/database/activityNames.tsapplies name changes in Firebase transactions, including conflict handling when transactions retry against newer data.client/src/data/actions.tsimplements mutations and cache invalidation for both production routes and Storybook; only the HTTP adapter changes.action-plan.tskeeps each route's allowed intents and each mutation's affected queries and failure context together. The executor knows no workflow-specific category-deletion messages.
- In local development, Vite proxies
/api/*requests from the frontend (port 3000) to the Azure Functions backend (port 7071) - In production, Azure Static Web Apps handles the routing natively — the client and API are deployed together, and
/api/*requests are routed to the Functions backend automatically without any proxy
- Client: Firebase Auth handles sign-in (Google provider). The auth state is managed via
AuthContextand a route loader redirects unauthenticated users to/login - API: Each function extracts the Firebase ID token from the
x-auth-tokenheader, verifies it via the Admin SDK, and uses the resultinguserIdto scope all database operations
The data model keeps the client simple by pushing business logic to the API:
- Activities are stored as flat records keyed by Firebase push IDs, each containing
name,date, and optional fields (description,intensity,timeSpent) - Categories each contain an
activityNamesarray — the single source of truth for which activities belong to which category, plus metadata (name,active,description) - Enrichment on read: The API enriches activities with computed
categoryIdandactivefields based on the category lookup before returning them to the client. This means the client never needs to perform the category ↔ activity join — it receives ready-to-render data - User preferences (e.g., theme, chart grouping) are stored per-user and merged with defaults on read
- Export/Import: The
exportDataendpoint returns the complete user dataset (activities, categories, preferences) as a single JSON payload;importDatareplaces it atomically - Activity editing:
PUT /api/activities/:idreplaces the complete stored record. Omitting an optional field clears it; it does not preserve an old value. - Rename/Merge: Renaming preserves category membership. An existing target name requires an explicit merge confirmation showing its category and the affected entry count. A merge retains all entry IDs, dates, and details, adopts the target name's membership, and removes only the source name. No entries are deduplicated. The server rejects a merge if the confirmed target name or category has changed.
- Names and identities: Category display names are trimmed on creation, editing, and restore; spaces inside a multiword name are preserved. Existing activity names and category IDs are exact identities, including those in older backups. Matching a potential merge target ignores surrounding whitespace and case, but never changes which source history or canonical target the user selected. Editing or restoring category membership preserves those distinct identities; new categories still reject names that differ only by case or edge whitespace.
- Server state: TanStack React Query manages all API data with automatic caching, background refetching, and cache invalidation on mutations
- Client state: React Router loaders pre-fetch data, client actions handle mutations (edit, delete, bulk ops) via
useFetcher - UI state: Component-local state with
useState; theme preference synced to<html>class for Tailwind dark mode
- Welcome accepts add/import; Activity List accepts edit/delete/delete-all/import;
Settings accepts category and name management plus its
edit-activityintent. Other intents fail before payload parsing, authentication, or HTTP calls. - Entry mutations refresh activity queries only. Category/name mutations also refresh categories because activity responses contain derived membership and active state. Only import refreshes preferences.
- Each successful, conflicting, or uncertain request contributes its own affected queries. A rejected or never-started step does not invalidate unrelated data. Multi-step failure messages belong to the workflow, not the shared executor.
The frontend uses Storybook's test addon as the primary testing strategy — stories with play functions serve as both living documentation and executable tests. This approach provides component-level testing that runs in a real browser, striking the right balance between the isolation of unit tests and the confidence of end-to-end tests.
- Stories are run as Vitest tests via
@storybook/addon-vitest, executing in a real Chromium browser through@vitest/browser-playwright - MSW (Mock Service Worker) is used extensively to mock all API responses at the network level, providing realistic test data without a backend. MSW handlers cover all 19 API endpoints with representative datasets
- Storybook resets both mutable mock data and Sonner notifications before each story. Sonner replays active notifications when a new toaster mounts; without dismissal, a previous story's success message can falsely signal completion.
- API tests use Vitest with an in-memory Firebase mock for fast, isolated database operation testing
- Regression coverage follows export -> upload -> restore and edit -> save -> read, rather than treating a success toast or closed dialog as proof of persistence. The Firebase adapter scenarios include cold-cache null snapshots, transaction retries, failed commits, and omitted empty arrays. A null first callback is not proof of missing data. Transaction callbacks abort on domain errors and rethrow after settlement; throwing directly during an SDK retry can escape its promise.
- Backup and name-identity regressions use actual Firebase SDK snapshots with networking disabled, so numeric-key arrays and omitted empty arrays are not approximated by plain JSON mocks. These fixtures use explicit Node built-ins rather than browser globals. Server acknowledgements and retry races are still simulated; this is not a live Firebase/emulator test suite.
- Compatibility coverage also restores older array-shaped backups, retaining numeric IDs while skipping null placeholders. Cache tests model lost write acknowledgements: a failed response can follow a successful write, so affected queries are invalidated even when the client cannot confirm the outcome.
- Mutation stories explicitly select the production route via
actionRouting. Set both the story route and its initial location: leaving the location at/renders an empty route rather than exercising the intended page. Keep parameterized failure cases at suite scope so Vitest collects every case, not inside another running test.
cd client && bun run test # Storybook play function tests via Vitest + Playwright
cd api && bun run test # API unit tests (Vitest)
cd client && bun run storybook # Start Storybook on :6006 for interactive development/client React frontend (Vite + React Router)
/src/app Routes, root layout, global CSS
/src/components Reusable components (ui/, forms/, navigation/, etc.)
/src/pages Page components (Welcome, Charts, Compare, Settings)
/src/data React Query hooks, query options, types
/src/auth Firebase auth context and service
/src/mocks MSW handlers and Storybook decorators
/src/utils Helpers (colors, icons, cn)
/api Azure Functions backend
/functions Individual function endpoints (19 functions)
/database Firebase DB abstraction layer with enrichment logic
/utils Shared types, validators, rate limiter
/scripts Migration and utility scripts
React components are optimized by Oxc's native React Compiler
(oxc-transform-react). It is experimental and pinned deliberately. A shared
Vite plugin applies it to client code in both the app and Storybook, leaving JSX
and Fast Refresh to their existing framework plugins. Babel may remain as a
transitive framework dependency, but is not our React Compiler implementation.
bun run --filter '*' build # Build both frontend and API
bun run --cwd client typecheck # Generate route types, then tsc --noEmit
bun run --cwd api typecheck # tsc --noEmit (requires Firebase config)Client production, end-to-end and Storybook builds run the client typecheck first and stop on errors. CI also checks the client explicitly before tests; the deployment job checks the API after supplying its Firebase configuration. Development servers rely on editor TypeScript diagnostics rather than a Vite type-checking overlay.
bun run lint # Oxlint with zero warnings
bun run lint:fix # Apply safe lint fixes
bun run format # Format with Oxfmt
bun run format:check # Check formatting without writingOxlint's recommended type-aware checks cover both workspaces. Oxlint and Oxfmt
run automatically on staged files via lint-staged + the Husky pre-commit hook,
followed by a whole-project typed lint pass. Native rules cover TypeScript,
React, and React Compiler; Storybook rules run through Oxlint's JS plugin API
rather than a second linter. React deprecations use native type-aware checking,
and component/hook factory checks use native react/static-components.
VS Code users should install the workspace's recommended Oxc and TypeScript Native extensions for matching formatting, linting, and TypeScript 7 diagnostics.
The app is deployed on Azure Static Web Apps with CI/CD via GitHub Actions (.github/workflows/azure-static-web-apps.yml).
- Install —
bun install - Firebase config — Decodes
API_FIREBASE_CONFIGsecret intoapi/firebase/firebaseConfig.json - Typecheck —
bun run --filter api typecheck - Build —
bun run --filter '*' build(client →client/build/client, API →api/dist) - Deploy — Uses
@azure/static-web-apps-clito deploy:- Push to
main→ production environment - Pull requests → preview environment (auto-created, auto-destroyed on PR close)
- Push to
- Static content served from
client/build/client - API functions served from
api/dist(Node.js 22) /api/*routes automatically routed to the Functions backend by Azure SWA — no proxy or routing config needed in production