A mobile-first workout tracking SPA built with React + TypeScript, with a separate REST API backend using Azure Table Storage.
The project is split into two parts:
- UI (
/src) - React frontend with Azure Static Web Apps authentication - API (
/api) - Azure Functions backend with Shared Key authentication and Azure Table Storage
- UI authenticates users via Azure SWA (Microsoft SSO)
- UI calls API with Shared Key header (
x-api-key) - API stores data in Azure Table Storage (replaces client-side IndexedDB)
- userId from Azure SWA auth is used to partition data in Table Storage
- Monthly calendar view with day indicators for logged workouts
- Multiple routines per day, each with named exercises
- Exercise fields: name, reps, weight, sets, time (mm:ss), distance (miles)
- Per-set completion tracking (tap to toggle)
- Drag-and-drop exercise reordering within routines
- Saved exercises library with autocomplete picker
- Inline routine renaming
- Export/import data as JSON
- Settings menu: export, import, clear all data
- Microsoft SSO via Azure Static Web Apps built-in auth
- Auth bypassed automatically in development mode
- React 18 + TypeScript + Vite
- @dnd-kit (drag-and-drop: core, sortable, utilities)
- Plain CSS (mobile-first, dark green theme)
- Azure SWA built-in authentication (AAD)
- Azure Functions (Node.js 18+)
- Azure Table Storage
- Shared Key authentication
- Node.js 18+
- Azure Functions Core Tools:
npm install -g azure-functions-core-tools@4 - Azure Storage Account or Azurite for local development
- Install dependencies:
# Frontend
npm install
# API
cd api
npm install
cd ..- Configure environment variables:
Create .env in project root:
VITE_API_URL=http://localhost:7071/api
VITE_API_KEY=your-shared-key-here
Create api/local.settings.json:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"AZURE_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true",
"API_SHARED_KEY": "your-shared-key-here"
}
}Note: Use the same key in both .env and api/local.settings.json.
- Start Azurite (local Azure Storage emulator):
npm install -g azurite
azurite --silent --location ./azurite --debug ./azurite/debug.log- Create tables in Azurite:
# Using Azure Storage Explorer or Azure CLI
az storage table create --name routines --connection-string "UseDevelopmentStorage=true"
az storage table create --name exercises --connection-string "UseDevelopmentStorage=true"Or use Azure Storage Explorer (GUI) to create the tables.
Run both frontend and backend:
# Terminal 1 - Start API
cd api
npm start
# Terminal 2 - Start UI
npm run dev- UI:
http://localhost:5173 - API:
http://localhost:7071
Auth is skipped in dev mode (hardcoded user).
# Build frontend
npm run build
# Build and deploy API
cd api
npm run build
func azure functionapp publish <function-app-name>Run frontend and API unit tests:
# Frontend tests
npm test
# API tests
cd api
npm test
# Run tests in watch mode
npm test -- --watch
# Generate coverage report
npm run test:coverageTests run automatically on:
- Every push to
mainorfeature/**branches - Every pull request to
main
See test results in the Actions tab of the GitHub repository.
Current test coverage:
- Frontend: API client, custom hooks (useRoutines, useExercises)
- API: Storage operations (routines, exercises, CRUD)
- Total: 18 unit tests, 3 integration tests (skipped in CI)
Integration tests require Azurite (Azure Storage Emulator). See api/test/README.md for setup instructions.
This project uses Azure Static Web Apps which handles both frontend (UI) and backend (API) deployment automatically via GitHub Actions.
Required:
- Azure Static Web App (Standard tier recommended for production)
- Azure Storage Account (for Table Storage)
Not needed: Separate Azure Function App (API is deployed as part of Static Web App)
- Create Storage Account (General-purpose v2)
- Create two tables:
routinesexercises
- Copy the Connection String from Storage Account → Access keys
Application Settings (Azure Portal → Static Web App → Configuration):
| Name | Value | Description |
|---|---|---|
AZURE_STORAGE_CONNECTION_STRING |
DefaultEndpointsProtocol=https;AccountName=... |
From Storage Account |
API_SHARED_KEY |
your-secret-key |
Generate a secure random string |
Important: Do NOT configure VITE_* variables in Azure Portal - these are configured in GitHub Actions.
GitHub Secrets (Repo → Settings → Secrets and variables → Actions):
| Name | Value | Description |
|---|---|---|
API_SHARED_KEY |
your-secret-key |
Must match the value in Azure Portal |
AZURE_STATIC_WEB_APPS_API_TOKEN_* |
Auto-generated | Created automatically by Azure |
How to add secret:
- Go to GitHub repo → Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name:
API_SHARED_KEY - Value: Same value as configured in Azure Portal
- Click "Add secret"
Deployment is automatic via GitHub Actions:
- Push to
mainbranch → Triggers production deployment - Open Pull Request → Creates staging environment (preview)
- Merge PR → Deploys to production
GitHub Workflow (.github/workflows/azure-static-web-apps-*.yml) handles:
- Building frontend (Vite)
- Building API (Azure Functions v4)
- Deploying both to Azure Static Web Apps
No manual deployment needed!
After deployment completes (~2-3 minutes):
- Check deployment status: GitHub Actions tab → Latest workflow run
- Test frontend: Open Static Web App URL
- Test API: Open browser console, check for 200 responses (not 401/404)
- Test auth: Log in with Microsoft account
Common issues:
- 401 Unauthorized: GitHub secret
API_SHARED_KEYnot configured or doesn't match Azure Portal - 404 Not Found: API not deployed (check workflow logs)
- 500 Internal Server Error: Check
AZURE_STORAGE_CONNECTION_STRINGin Azure Portal
All data stored in Azure Table Storage:
| Field | Type | Description |
|---|---|---|
| PartitionKey | string | userId (from Azure SWA auth) |
| RowKey | string | routineId (auto-generated) |
| date | string | YYYY-MM-DD |
| name | string | Routine name |
| order | number | Display order |
| Field | Type | Description |
|---|---|---|
| PartitionKey | string | userId |
| RowKey | string | exerciseId (auto-generated) |
| routineId | string | Reference to routine |
| name | string | Exercise name |
| repetitions | number | Number of reps |
| weight | number | Weight in lbs |
| sets | number | Total sets |
| setsCompleted | number | Completed sets |
| time | string | Duration (mm:ss) |
| distance | number | Distance in miles |
| order | number | Display order |
| timestamp | number | Unix timestamp |
9193a99 (refactor: Remove photo feature completely)
pump/
├── index.html
├── package.json
├── .env.example
├── staticwebapp.config.json
├── vite.config.ts
├── tsconfig.json
├── README.md
├── src/ # Frontend
│ ├── main.tsx
│ ├── App.tsx
│ ├── db.ts # API client wrapper
│ ├── api.ts # HTTP API client
│ ├── types.ts
│ ├── vite-env.d.ts
│ ├── hooks/
│ │ ├── useAuth.ts
│ │ └── useAutoSave.ts
│ ├── components/
│ │ ├── MonthCalendar.tsx
│ │ ├── DayView.tsx
│ │ ├── RoutineCard.tsx
│ │ ├── ExerciseRow.tsx
│ │ ├── ExerciseForm.tsx
<<<<<<< HEAD
│ │ ├── ExercisePicker.tsx
│ │ ├── DraggableExerciseList.tsx
=======
│ │ ├── │ │ ├── DraggableExerciseList.tsx
>>>>>>> 9193a99 (refactor: Remove photo feature completely)
│ │ └── SettingsMenu.tsx
│ ├── styles/
│ │ └── app.css
│ └── utils/
│ └── export.ts
└── api/ # Backend
├── package.json
├── tsconfig.json
├── host.json
├── local.settings.json
├── README.md
├── auth.ts # Shared key validation
├── storage.ts # Table Storage operations
├── routines.ts # Routines endpoints
└── exercises.ts # Exercises endpoints
See api/README.md for detailed API documentation.
This version uses Azure Table Storage for server-side data persistence with:
- Azure Table Storage (server-side)
- RESTful API with Shared Key authentication
- Multi-user support via userId partitioning
The frontend no longer uses IndexedDB - all data is stored in Azure and accessed via the API.
MIT