Skip to content

About

PWA to track my sports and exercise

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Activity Tracker

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.

Features

  • 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 bars and multi-ring pies with a compact multi-column legend, date/inactive filters, and keyboard exploration
  • Compare — Side-by-side comparison of activity periods (month vs month, year vs year) with trend lines, dashed partial-period indicators, and metric cards (total, most popular, most active day/month). Future buckets are omitted instead of shown as zero.
  • 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

Demo

Activity Tracker demo

Tech Stack

Frontend

  • React 19
  • TypeScript 7
  • TanStack Router with generated file routes and client-only rendering
  • TanStack React Query for server state management, caching, and optimistic updates
  • TanStack Form for form state and validation
  • Tailwind CSS v4 with @tailwindcss/vite plugin 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-primitive radix-ui/* imports.
  • TanStack Charts with SVG rendering, typed chart data, accessible legend buttons, keyboard point navigation, and reduced-motion-aware transitions
  • Vite 8 as build tool with HMR and PWA plugin; Bun's TypeScript 7 checker runs separately
  • Storybook 10 with the official @storybook/tanstack-react framework and play functions for component and interaction testing
  • Lucide React for icons, Sonner for toast notifications, cmdk for command palette, date-fns for date utilities

Backend

  • 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)

Infrastructure

  • 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

Quick Start

Install Bun >=1.4.4 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)

After changing dependencies, restart the frontend rather than leaving its old Vite process running. If the browser reports 504 Outdated Optimize Dep, stop that frontend, run bun run --cwd client start --force, and reload the page. The API can keep running.

Architecture

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.ts applies name changes in Firebase transactions, including conflict handling when transactions retry against newer data.
  • The client mutation module uses typed inputs and native Query submission state. actions.ts declares each write once; the shared options factory binds it to the current account's token supplier and QueryClient when called. Production and Storybook exercise the same ordered writes, affected-query policy, and partial-failure handling; MSW substitutes HTTP responses only.

Client → API Communication

  • 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

Authentication Flow

  • Client: Firebase Auth handles Google and email sign-in. A session runtime owns auth initialization, Query/Router lifetime and account-bound tokens. _authenticated checks authentication before child loaders and redirects to /login, preserving a safe local return destination. The subscribed AuthContext sits above memoized route matches so same-account profile updates render without clearing server data. Explicit expired-session recovery signs out before reopening login.
  • API: Each function extracts the Firebase ID token from the x-auth-token header, verifies it via the Admin SDK, and uses the resulting userId to scope all database operations

Data Model

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 activityNames array — 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 categoryId and active fields 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 exportData endpoint returns the complete user dataset (activities, categories, preferences) as a single JSON payload; importData replaces it atomically
  • Activity editing: PUT /api/activities/:id replaces 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.

State Management

  • Server state: TanStack React Query manages all API data with automatic caching, background refetching, and cache invalidation on mutations
  • Routing: File-route context creates concrete query options reused by loaders and Query observers. Query owns freshness; loaders coordinate fetching early. Imperative cache-first reads use a local "static" override, never static observer options. Search parameters remain compatible with existing URLs.
  • Mutations: Typed Query mutations handle edit/delete/bulk operations and targeted reconciliation; UI callers own form reset and dialog dismissal.
  • UI state: Component-local state with useState; theme preference synced to <html> class for Tailwind dark mode

Mutation policies

  • Callers submit typed operations rather than routing writes through page URLs and FormData intents. Inputs preserve shared validation and exact name/entry identity rules.
  • 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.

Charts

TanStack Charts renders the analytics bars, concentric distribution pie, and comparison lines as responsive SVG. Grouping and animation preferences use the existing preferences queries; reduced-motion settings are respected. Desktop pie legends form compact columns within the plot height. Hiding an activity recalculates every ring from the visible activities.

Comparison periods keep a zero baseline and full shared bucket domain when a series is hidden. Incomplete periods have dashed lines and partial labels; future buckets are omitted, not represented as zero. The existing five weekly buckets per month are retained. Keyboard point navigation exposes values without pointer hover. Comparisons live on the Compare page; the Charts page contains only frequency and distribution plots. Activity-name/category matching preserves exact stored identities, including names that differ only by case. Fixed chart domains use configured scale instances rather than factories that infer a new domain from visible data. Flat bars use explicit zero-baseline intervals so the chart library does not allocate a position-by-series stack matrix. Legend and summary labels wrap uninterrupted activity names rather than widening the mobile page. Comparison stories with completed-year fixtures pin their clock after those years; a separate story exercises a genuinely partial year. Keep the SVG scene height synchronized with its container, not just its CSS size.

Testing

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.

  • Story assertions cover user-visible data, accessible controls, and interactions. Leave colors, styles, dimensions, layout, and SVG rendering details to Chromatic screenshot comparisons rather than asserting them in stories or E2E tests.
  • Stories are run as Vitest tests via @storybook/addon-vitest, executing in a real Chromium browser through @vitest/browser-playwright
  • The native TanStack framework clones the generated route tree and injects each story at parameters.tanstack.router.path. Use query for search state and routeOverrides for story-specific loaders/layouts. .storybook/tanstack.tsx supplies application defaults and Query/auth providers; MSW initialization and per-story services complete before the framework loads routes.
  • Router-dependent story layouts belong inside render or a route component override: ordinary decorators wrap the framework's router. Loading stories can override the leaf prefetch while their real Query hook remains pending. Framework Link mocks record attempts; useNavigate remains real, and full Link navigation is covered by application E2E flows.
  • Live arg updates require the running Storybook preview channel. The dedicated LiveStoryArgs example is interactive (!test); its behavior was exercised in actual Storybook rather than treated as a Vitest portable-story capability.
  • 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 and Playwright collect 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

Project Structure

/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

Development

Building

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 bun check
bun run --cwd api typecheck     # bun check (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. TypeScript 7 remains installed for editor tooling and the API's tsc -w watch script; CI and production builds use bun check.

The API uses Bun.build() to emit self-contained Node ESM bundles; Azure still runs them on Node 22. Bun does not downlevel syntax, so the API tests load every emitted function under Node with generated Firebase credentials and a test database URL. bun run --cwd api build --verbose prints Bun's dependency and output metadata. Identifier minification is disabled because Bun 1.4.4 ignores keepNames; whitespace and syntax minification remain enabled.

client/tsr.config.json is shared by route generation and the Vite Router plugin. Commit routeTree.gen.ts after changing routes; CI rejects generated drift. Colocated tests/stories are excluded from route discovery. Browser tests explicitly prebundle Router, Query, and Zod to avoid optimizer reloads when split routes load. Production and E2E builds use separate HTML entry modules; mock auth is excluded from the production entry.

Linting & Formatting

bun run lint                    # Oxlint with zero warnings
bun run lint:fix                 # Apply safe lint fixes
bun run format                  # Format with bun fmt
bun run format:check            # Check formatting without writing

Oxlint's recommended type-aware checks cover both workspaces. Oxlint and bun fmt 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. bun fmt reuses .oxfmtrc.json to preserve the existing formatting settings.

VS Code users should install the workspace's recommended Oxc and TypeScript Native extensions for matching formatting, linting, and TypeScript 7 diagnostics.

Deployment

The app is deployed on Azure Static Web Apps with CI/CD via GitHub Actions (.github/workflows/azure-static-web-apps.yml).

Pipeline

  1. Install — bun install
  2. Firebase config — Decodes API_FIREBASE_CONFIG secret into api/firebase/firebaseConfig.json
  3. Typecheck — bun run --filter api typecheck
  4. Build — bun run --filter '*' build (client → client/build/client, API → api/dist)
  5. Deploy — Uses @azure/static-web-apps-cli to deploy:
    • Push to main → production environment
    • Pull requests → preview environment (auto-created, auto-destroyed on PR close)

Azure SWA Configuration

  • 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

About

PWA to track my sports and exercise

Resources

Stars

1 star

Watchers

0 watching

Forks

Used by

Contributors

Languages