A mobile-first web application that allows users to upload workout photos and sync them to Hevy. Built with Next.js, TypeScript, Tailwind CSS, and shadcn/ui.
- πΈ Photo Upload: Upload workout screenshots from gym screens or selfies with EXIF date extraction
- π€ AI-Powered Workout Detection: Uses Groq Vision API (Llama 4 Scout) to automatically extract exercises, sets, reps, and weights from workout photos
- π Intelligent Exercise Matching: Fuzzy matching system maps detected exercises to Hevy's database of 453 exercises
- βοΈ Edit & Review: Review and edit detected exercises, sets, reps, and weight with image zoom/pan
- π Date & Time Management: Automatic date extraction from image EXIF data with manual override
- π Hevy Sync: Real-time sync to Hevy API with duplicate workout detection
- π Exercise Search: Search and replace exercises from Hevy's exercise database
- π± Mobile-First Design: Optimized for mobile devices with smooth animations
- π¨ Modern UI: Clean, professional interface using shadcn/ui components
- Framework: Next.js 16.1.1 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS 4
- UI Components: shadcn/ui (New York style)
- Icons: Lucide React
- State Management: React Context API
- AI/ML: Groq Vision API (Llama 4 Scout 17B)
- APIs: Hevy API, Groq API
- Image Processing: EXIF extraction, zoom/pan with react-zoom-pan-pinch
- Containerization: Docker/Podman support with multi-stage builds
- Deployment: Optimized for TrueNAS Scale and Docker Hub
- Node.js 22+ (using nvm)
- npm or yarn
- Clone the repository:
git clone <repository-url>
cd workout-sync- Install dependencies:
npm install- Run the development server:
npm run dev- Create a
.env.localfile with your API keys (seeenv.example):
cp env.example .env.local
# Edit .env.local with your actual API keys- Open http://localhost:3000 in your browser
The application requires the following environment variables:
HEVY_API_KEY- Your Hevy API key (get from Hevy app settings)GROQ_API_KEY- Your Groq API key (get from https://console.groq.com/)
See env.example for a template.
- Build the Podman image:
podman build -t workout-sync:latest .- Test locally:
podman run -d \
--name workout-sync \
-p 3000:3000 \
-e HEVY_API_KEY="your_hevy_api_key" \
-e GROQ_API_KEY="your_groq_api_key" \
workout-sync:latest- Push to Docker Hub:
# Login to Docker Hub
podman login docker.io
# Tag the image (include full registry URL)
podman tag workout-sync:latest docker.io/YOUR_DOCKERHUB_USERNAME/workout-sync:latest
# Push to Docker Hub
podman push docker.io/YOUR_DOCKERHUB_USERNAME/workout-sync:latestNote: With Podman, always include the full registry URL (docker.io/) when tagging and pushing.
Instead of always using latest, consider versioning your images:
# Docker
docker tag workout-sync:latest YOUR_DOCKERHUB_USERNAME/workout-sync:v1.0.0
docker push YOUR_DOCKERHUB_USERNAME/workout-sync:v1.0.0
# Podman
podman tag workout-sync:latest docker.io/YOUR_DOCKERHUB_USERNAME/workout-sync:v1.0.0
podman push docker.io/YOUR_DOCKERHUB_USERNAME/workout-sync:v1.0.0For detailed deployment instructions on TrueNAS Scale, see docs/DOCKER_DEPLOYMENT.md.
Quick steps:
- Push your image to Docker Hub (see above)
- In TrueNAS Scale UI: Apps β Custom App β Install
- Set image repository:
YOUR_DOCKERHUB_USERNAME/workout-sync - Configure environment variables (
HEVY_API_KEY,GROQ_API_KEY) - Set port mapping (container: 3000, host: your preferred port)
- Configure Docker Hub credentials for private repositories
Uses Groq Vision API with the meta-llama/llama-4-scout-17b-16e-instruct model to:
- Extract exercise names, sets, reps, and weights from workout photos
- Parse structured JSON responses from the AI model
- Automatic fallback to sample data if
GROQ_API_KEYis not configured (allows testing without API key) - Image validation (file type and size limits)
Fuzzy matching system that maps detected exercise names to Hevy's official database:
- 453 exercises in database (429 official, 24 custom)
- Multi-factor scoring: Levenshtein distance, word overlap, equipment matching
- Handles abbreviations, variations, and compound exercises
- Creates custom exercises only when no match is found above threshold
Real-time sync to Hevy's workout API:
- Transforms workout data to Hevy's format
- Validates workout data before submission
- Handles API errors with user-friendly messages
- Duplicate workout detection by date
Automatically extracts workout date and time from image metadata:
- Reads EXIF DateTimeOriginal tag
- Calculates workout start time
- Falls back to manual date/time selection if EXIF not available
Uses React Context API for global state:
- Uploaded image
- Processed exercises
- Sync preferences
- Caption/notes
- Mobile-first approach
- Touch-optimized buttons (min 44px targets)
- Safe area padding for iOS devices
- Smooth animations and transitions
- Custom scrollbar styling
Exercises follow the Hevy API structure:
{
id: "uuid",
title: "Exercise Name",
type: "weight_reps",
primary_muscle_group: "chest",
secondary_muscle_groups: ["triceps"],
is_custom: false,
equipment: "barbell"
}Workouts are structured as:
{
id: "uuid",
date: Date,
duration_minutes: number,
caption: string,
exercises: WorkoutExercise[]
}npm run dev- Start development servernpm run build- Build for productionnpm run start- Start production servernpm run lint- Run ESLintnpm test- Run testsnpm run test:watch- Run tests in watch mode
docker build -t workout-sync:latest .- Build Docker imagepodman build -t workout-sync:latest .- Build Podman imagedocker run -p 3000:3000 -e HEVY_API_KEY=... -e GROQ_API_KEY=... workout-sync:latest- Run container locally
See the Docker Deployment section above for detailed instructions.
npx shadcn@latest add [component-name]This project is licensed under the MIT License - see the LICENSE file for details.
For questions or issues, please contact the development team.


