diff --git a/.gitignore b/.gitignore index 591481a..a509772 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,11 @@ __pycache__/ Pods/ +# Scratch / archive +temp/ +archive/ +.gemini/ + # PyInstaller build artifacts Dev_tools/build/ Dev_tools/dist/ diff --git a/App Core/README.md b/App Core/README.md index fc3ce19..ec0f2d9 100644 --- a/App Core/README.md +++ b/App Core/README.md @@ -1,6 +1,6 @@ # mAI_Coach iOS App -This folder contains the source code for the mAI_Coach iOS application, which provides real-time motion capture and feedback for strength training using on-device MediaPipe inference. +This folder contains the source code for the mAI_Coach iOS application, which provides real-time motion capture and feedback for strength training using on-device MediaPipe inference, plus a full-featured workout tracker with superset support. ## Prerequisites @@ -39,19 +39,52 @@ open "mAICoach.xcworkspace" ## Architecture +### App Entry & Navigation + | File | Purpose | | :--- | :--- | -| `mAICoachApp.swift` | App entry point. | -| `RootView.swift` | Navigation root, manages auth state. | -| `HomeView.swift` | Main menu with "Coach me!" and "My Workouts" buttons. | -| `CoachView.swift` | Exercise selection (Live / Demo modes). | +| `mAICoachApp.swift` | App entry point. Injects `AuthSession` and `WorkoutStore`. | +| `RootView.swift` | Navigation root, manages auth state, loads admin demo data. | +| `MainTabView.swift` | 3-tab layout: Home, Workout, Profile with frosted glass tab bar. | +| `AuthSession.swift` | Authentication manager, admin account support. | + +### Views + +| File | Purpose | +| :--- | :--- | +| `HomeView.swift` | Calendar with workout dots, day cards, template shortcuts. | +| `WorkoutView.swift` | Active workout tab with live session tracking. | +| `WorkoutDetailView.swift` | Full workout/template detail: exercise cards, supersets, edit mode, fill-from-history, workout notes. | +| `WorkoutBuilderView.swift` | Create new workouts with name, exercises, save-as-template. | +| `AllTemplatesView.swift` | Template management: favorites, archive, reorder. | +| `ExerciseCardView.swift` | Exercise card with sets grid (weight/reps/RPE), checkmarks, notes, mAI Coach button. | +| `SupersetBlockView.swift` | Superset group block with label, notes, inner exercise cards, add/move/delete. | +| `ExercisePickerView.swift` | Searchable exercise catalog picker grouped by muscle group. | +| `ExerciseStatsView.swift` | Per-exercise historical stats: best sets, volume trend, session count. | +| `PersonalRecordsView.swift` | All-time PR board across all exercises. | +| `ProfileView.swift` | Account card, settings, stats link. | +| `SettingsView.swift` | Coach voice, appearance, template shortcuts, PR badges, dev data toggles. | +| `ConfettiView.swift` | On-screen confetti animation for PR celebrations. | | `BenchSessionView.swift` | Camera feed with pose overlay, HUD (rep count, form feedback), and centering guide. | + +### Models & Logic + +| File | Purpose | +| :--- | :--- | +| `WorkoutModel.swift` | `Workout`, `WorkoutItem`, `SupersetGroup`, `Exercise`, `ExerciseSet` types + `WorkoutStore` with CRUD, templates, PR detection, 16-week admin demo data. | +| `WorkoutStatsEngine.swift` | Stats computation: totals, PRs, exercise trends, streaks. | +| `RestTimerManager.swift` | Observable rest timer with auto-start on set completion. | +| `ExerciseCatalog.swift` | Equipment types enum, exercise catalog loader. | +| `exercise_library.json` | 100+ exercises with name, equipment, and muscle group. | + +### AI Coaching + +| File | Purpose | +| :--- | :--- | | `BenchInferenceEngine.swift` | MLP inference, rep detection (wrist-Y peak), and audio coaching triggers. | | `PoseLandmarkerService.swift` | MediaPipe pose detection, landmark smoothing, and skeleton constraints. | | `AudioCoach.swift` | Audio feedback system with tag-based coaching clips. | | `DemoPlayerPipeline.swift` | Pre-recorded demo video playback with frame-by-frame pose processing. | -| `SettingsView.swift` | User preferences (coach voice selection, dev data toggle). | -| `MyWorkoutsView.swift` | Workout tracking placeholder (coming soon). | ### Model diff --git a/App Core/Resources/Swift Code/AllTemplatesView.swift b/App Core/Resources/Swift Code/AllTemplatesView.swift new file mode 100644 index 0000000..44b1dbd --- /dev/null +++ b/App Core/Resources/Swift Code/AllTemplatesView.swift @@ -0,0 +1,270 @@ +// AllTemplatesView.swift +// mAI Coach — Full template list with favorites management, delete/archive support. + +import SwiftUI + +struct AllTemplatesView: View { + @EnvironmentObject var store: WorkoutStore + @State private var editMode: EditMode = .inactive + @State private var showMaxAlert = false + @State private var showCreator = false + @State private var templateToDelete: String? + @State private var showDeleteDialog = false + @State private var searchText = "" + @State private var selectedMuscleGroups: Set = [] + @AppStorage(SettingsKeys.maxTemplateShortcuts) private var maxShortcuts = 4 + + private var isEditing: Bool { editMode == .active } + + private static let muscleGroupIcons: [String: String] = [ + "Chest": "muscle_chest", + "Back": "muscle_back", + "Shoulders": "muscle_shoulders", + "Arms": "muscle_arms", + "Legs": "muscle_legs", + "Core": "muscle_core", + "Other": "muscle_other", + ] + + /// All template names that match the current search text and muscle group filter. + private var filteredTemplateNames: [String] { + store.allTemplateNames.filter { name in + let matchesSearch = searchText.isEmpty || name.localizedCaseInsensitiveContains(searchText) + guard matchesSearch else { return false } + guard !selectedMuscleGroups.isEmpty else { return true } + // Template matches only if it contains ALL selected muscle groups + if let tpl = store.templateWorkout(named: name) { + return selectedMuscleGroups.isSubset(of: Set(tpl.tags)) + } + return false + } + } + + var body: some View { + VStack(spacing: 0) { + // Search bar + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .foregroundStyle(AppColors.textTertiary) + TextField("Search templates", text: $searchText) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + if !searchText.isEmpty { + Button { searchText = "" } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(AppColors.textTertiary) + } + } + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(AppColors.bgSecondary) + .cornerRadius(10) + .padding(.horizontal, 16) + .padding(.top, 8) + + // Muscle group filter pills + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + filterPill(label: "All", icon: "square.grid.2x2", isSystemImage: true, isSelected: selectedMuscleGroups.isEmpty) { + selectedMuscleGroups.removeAll() + } + + ForEach(ExerciseCatalog.muscleGroups, id: \.self) { group in + let iconName = Self.muscleGroupIcons[group] ?? "muscle_other" + filterPill(label: group, icon: iconName, isSystemImage: false, isSelected: selectedMuscleGroups.contains(group)) { + if selectedMuscleGroups.contains(group) { + selectedMuscleGroups.remove(group) + } else { + selectedMuscleGroups.insert(group) + } + } + } + } + .padding(.horizontal, 16) + } + .padding(.vertical, 8) + + List { + // MARK: Favorites Section + let filteredFavs = store.allTemplatesGrouped.favorites.filter { filteredTemplateNames.contains($0.name) } + if !filteredFavs.isEmpty { + Section { + ForEach(filteredFavs, id: \.name) { tpl in + templateRow(name: tpl.name, count: tpl.count, isFav: true) + } + .onMove { from, to in + store.moveFavorite(from: from, to: to) + } + } header: { + Text("Favorites") + } + } + + // MARK: All Templates Section (filtered) + Section { + let grouped = Dictionary(grouping: store.workouts) { $0.name } + let allSorted = filteredTemplateNames.compactMap { name -> (name: String, count: Int)? in + guard let latest = grouped[name]?.last else { return nil } + return (name: name, count: latest.exercises.count) + } + ForEach(allSorted, id: \.name) { tpl in + templateRow(name: tpl.name, count: tpl.count, isFav: store.isFavorite(tpl.name)) + } + } header: { + Text("All Templates") + } + + // MARK: Add Template + Section { + Button { showCreator = true } label: { + HStack { + Image(systemName: "plus.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(AppColors.accentGold) + Text("Add Template") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.accentText) + } + } + } + } + .listStyle(.insetGrouped) + } + .navigationTitle("Templates") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + HStack(spacing: 12) { + if isEditing { + NavigationLink(destination: ArchivedTemplatesView()) { + Image(systemName: "archivebox") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + } + } + EditButton() + } + } + } + .environment(\.editMode, $editMode) + .alert("Favorites Limit Reached", isPresented: $showMaxAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("You've reached your maximum of \(maxShortcuts) favorites. Remove one to add another.") + } + .confirmationDialog( + "Template Options", + isPresented: $showDeleteDialog, + titleVisibility: .visible + ) { + Button("Archive", role: .destructive) { + if let name = templateToDelete { + store.archiveTemplate(name: name) + } + } + Button("Delete Permanently", role: .destructive) { + if let name = templateToDelete { + store.deleteTemplate(name: name) + } + } + Button("Cancel", role: .cancel) { + templateToDelete = nil + } + } message: { + if let name = templateToDelete { + Text("What would you like to do with \"\(name)\"?\n\nArchive keeps it recoverable. Delete removes it permanently.") + } + } + .sheet(isPresented: $showCreator) { + TemplateCreatorView() + } + } + + // MARK: - Filter Pill + private func filterPill(label: String, icon: String, isSystemImage: Bool, isSelected: Bool, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: 4) { + if isSystemImage { + Image(systemName: icon) + .font(.system(size: 12)) + } else { + Image(icon) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 14, height: 14) + } + Text(label) + .font(AppFonts.footnote()) + .fontWeight(.medium) + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(isSelected ? AppColors.accentGold : AppColors.bgSecondary) + .foregroundStyle(isSelected ? .black : AppColors.textSecondary) + .cornerRadius(16) + } + .buttonStyle(.plain) + } + + // MARK: - Template Row + @ViewBuilder + private func templateRow(name: String, count: Int, isFav: Bool) -> some View { + HStack(spacing: 12) { + // Red minus delete button in edit mode + if isEditing { + Button { + templateToDelete = name + showDeleteDialog = true + } label: { + Image(systemName: "minus.circle.fill") + .font(.system(size: 22)) + .foregroundStyle(.red) + } + .buttonStyle(.plain) + } + + // Nav link wrapping the content + NavigationLink(destination: TemplateDetailView(templateName: name)) { + HStack(spacing: 12) { + // Star toggle + Button { + if isFav { + _ = store.toggleFavorite(name) + } else { + if !store.toggleFavorite(name) { + showMaxAlert = true + } + } + } label: { + Image(systemName: isFav ? "star.fill" : "star") + .font(.system(size: 18)) + .foregroundStyle(isFav ? AppColors.accentGold : AppColors.textTertiary) + } + .buttonStyle(.plain) + + // Template icon + RoundedRectangle(cornerRadius: 10) + .fill(AppColors.accentMuted) + .frame(width: 36, height: 36) + .overlay( + Image(systemName: "figure.strengthtraining.traditional") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + ) + + // Name + count + VStack(alignment: .leading, spacing: 2) { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + } + } + .padding(.vertical, 4) + } +} diff --git a/App Core/Resources/Swift Code/ArchivedTemplatesView.swift b/App Core/Resources/Swift Code/ArchivedTemplatesView.swift new file mode 100644 index 0000000..09ccec1 --- /dev/null +++ b/App Core/Resources/Swift Code/ArchivedTemplatesView.swift @@ -0,0 +1,78 @@ +// ArchivedTemplatesView.swift +// mAI Coach — View archived templates with option to restore. + +import SwiftUI + +struct ArchivedTemplatesView: View { + @EnvironmentObject var store: WorkoutStore + + var body: some View { + Group { + if store.archivedTemplateNames.isEmpty { + VStack(spacing: 12) { + Spacer() + Image(systemName: "archivebox") + .font(.system(size: 48)) + .foregroundStyle(AppColors.textTertiary) + .opacity(0.4) + Text("No Archived Templates") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + Text("Archived templates will appear here.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + Spacer() + } + } else { + List { + ForEach(store.archivedTemplateNames, id: \.self) { name in + let grouped = Dictionary(grouping: store.workouts) { $0.name } + let count = grouped[name]?.last?.exercises.count ?? 0 + + HStack(spacing: 12) { + // Template icon + RoundedRectangle(cornerRadius: 10) + .fill(AppColors.accentMuted.opacity(0.5)) + .frame(width: 36, height: 36) + .overlay( + Image(systemName: "figure.strengthtraining.traditional") + .font(.system(size: 16)) + .foregroundStyle(AppColors.textTertiary) + ) + + VStack(alignment: .leading, spacing: 2) { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + + Spacer() + + Button { + store.restoreTemplate(name: name) + } label: { + Text("Restore") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 14) + .padding(.vertical, 6) + .background(AppColors.accentMuted) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + .padding(.vertical, 4) + } + } + .listStyle(.insetGrouped) + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("Archived") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Contents.json index f395ef0..587f41e 100644 --- a/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,36 +1,38 @@ { - "images" : [ + "images": [ { - "filename" : "Untitled design.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "filename": "app_icon_1024.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "filename": "app_icon_1024.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "tinted" + "appearance": "luminosity", + "value": "tinted" } ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "filename": "app_icon_1024.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } -} +} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Untitled design.png b/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Untitled design.png deleted file mode 100644 index 6498b3d..0000000 Binary files a/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/Untitled design.png and /dev/null differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..c7da476 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/AppLogo.png b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/AppLogo.png new file mode 100644 index 0000000..381a860 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/AppLogo.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/AppLogo_light.png b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/AppLogo_light.png new file mode 100644 index 0000000..f3dea78 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/AppLogo_light.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Contents.json index 4137439..b44bfcb 100644 --- a/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Contents.json +++ b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Contents.json @@ -1,16 +1,25 @@ { - "images" : [ + "images": [ { - "filename" : "Vector.pdf", - "idiom" : "universal" + "filename": "AppLogo_light.png", + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "filename": "AppLogo.png", + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" + "properties": { + "template-rendering-intent": "original" } -} +} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Vector.pdf b/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Vector.pdf deleted file mode 100644 index 1edab9f..0000000 Binary files a/App Core/Resources/Swift Code/Assets.xcassets/AppLogo.imageset/Vector.pdf and /dev/null differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_abWheel.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_abWheel.imageset/Contents.json new file mode 100644 index 0000000..738b57e --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_abWheel.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_abWheel.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_abWheel.imageset/equip_abWheel.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_abWheel.imageset/equip_abWheel.png new file mode 100644 index 0000000..9957fa9 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_abWheel.imageset/equip_abWheel.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_barbell.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_barbell.imageset/Contents.json new file mode 100644 index 0000000..6fed1d4 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_barbell.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_barbell.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_barbell.imageset/equip_barbell.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_barbell.imageset/equip_barbell.png new file mode 100644 index 0000000..db42a94 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_barbell.imageset/equip_barbell.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_battleRopes.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_battleRopes.imageset/Contents.json new file mode 100644 index 0000000..c61c8e3 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_battleRopes.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_battleRopes.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_battleRopes.imageset/equip_battleRopes.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_battleRopes.imageset/equip_battleRopes.png new file mode 100644 index 0000000..d5e6baa Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_battleRopes.imageset/equip_battleRopes.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_bodyweight.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_bodyweight.imageset/Contents.json new file mode 100644 index 0000000..6506e94 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_bodyweight.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_bodyweight.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_bodyweight.imageset/equip_bodyweight.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_bodyweight.imageset/equip_bodyweight.png new file mode 100644 index 0000000..aa7aef9 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_bodyweight.imageset/equip_bodyweight.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_box.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_box.imageset/Contents.json new file mode 100644 index 0000000..c36163e --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_box.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_box.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_box.imageset/equip_box.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_box.imageset/equip_box.png new file mode 100644 index 0000000..8a4ffa2 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_box.imageset/equip_box.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_cable.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_cable.imageset/Contents.json new file mode 100644 index 0000000..1d9b8b7 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_cable.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_cable.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_cable.imageset/equip_cable.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_cable.imageset/equip_cable.png new file mode 100644 index 0000000..1dfb5e8 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_cable.imageset/equip_cable.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_cardioMachine.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_cardioMachine.imageset/Contents.json new file mode 100644 index 0000000..af547a7 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_cardioMachine.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_cardioMachine.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_cardioMachine.imageset/equip_cardioMachine.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_cardioMachine.imageset/equip_cardioMachine.png new file mode 100644 index 0000000..15b1b38 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_cardioMachine.imageset/equip_cardioMachine.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_dipStation.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_dipStation.imageset/Contents.json new file mode 100644 index 0000000..89c0dff --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_dipStation.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_dipStation.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_dipStation.imageset/equip_dipStation.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_dipStation.imageset/equip_dipStation.png new file mode 100644 index 0000000..b6e193b Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_dipStation.imageset/equip_dipStation.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_dumbbell.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_dumbbell.imageset/Contents.json new file mode 100644 index 0000000..beae085 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_dumbbell.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_dumbbell.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_dumbbell.imageset/equip_dumbbell.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_dumbbell.imageset/equip_dumbbell.png new file mode 100644 index 0000000..9791f96 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_dumbbell.imageset/equip_dumbbell.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_ezCurlBar.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_ezCurlBar.imageset/Contents.json new file mode 100644 index 0000000..fd6ad69 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_ezCurlBar.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_ezCurlBar.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_ezCurlBar.imageset/equip_ezCurlBar.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_ezCurlBar.imageset/equip_ezCurlBar.png new file mode 100644 index 0000000..45e26d8 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_ezCurlBar.imageset/equip_ezCurlBar.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_foamRoller.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_foamRoller.imageset/Contents.json new file mode 100644 index 0000000..2ea793c --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_foamRoller.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_foamRoller.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_foamRoller.imageset/equip_foamRoller.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_foamRoller.imageset/equip_foamRoller.png new file mode 100644 index 0000000..ad47e32 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_foamRoller.imageset/equip_foamRoller.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_kettlebell.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_kettlebell.imageset/Contents.json new file mode 100644 index 0000000..1931622 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_kettlebell.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_kettlebell.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_kettlebell.imageset/equip_kettlebell.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_kettlebell.imageset/equip_kettlebell.png new file mode 100644 index 0000000..1b8c311 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_kettlebell.imageset/equip_kettlebell.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_landmine.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_landmine.imageset/Contents.json new file mode 100644 index 0000000..486adb6 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_landmine.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_landmine.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_landmine.imageset/equip_landmine.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_landmine.imageset/equip_landmine.png new file mode 100644 index 0000000..4b8d218 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_landmine.imageset/equip_landmine.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_machine.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_machine.imageset/Contents.json new file mode 100644 index 0000000..f069adb --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_machine.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_machine.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_machine.imageset/equip_machine.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_machine.imageset/equip_machine.png new file mode 100644 index 0000000..0182424 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_machine.imageset/equip_machine.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_medicineBall.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_medicineBall.imageset/Contents.json new file mode 100644 index 0000000..bb13fdc --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_medicineBall.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_medicineBall.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_medicineBall.imageset/equip_medicineBall.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_medicineBall.imageset/equip_medicineBall.png new file mode 100644 index 0000000..5b72df7 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_medicineBall.imageset/equip_medicineBall.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_other.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_other.imageset/Contents.json new file mode 100644 index 0000000..c8b5207 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_other.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_other.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_other.imageset/equip_other.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_other.imageset/equip_other.png new file mode 100644 index 0000000..3b5b179 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_other.imageset/equip_other.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_plate.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_plate.imageset/Contents.json new file mode 100644 index 0000000..38c1572 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_plate.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_plate.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_plate.imageset/equip_plate.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_plate.imageset/equip_plate.png new file mode 100644 index 0000000..a82d9b3 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_plate.imageset/equip_plate.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_pullUpBar.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_pullUpBar.imageset/Contents.json new file mode 100644 index 0000000..303f952 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_pullUpBar.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_pullUpBar.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_pullUpBar.imageset/equip_pullUpBar.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_pullUpBar.imageset/equip_pullUpBar.png new file mode 100644 index 0000000..e1487ad Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_pullUpBar.imageset/equip_pullUpBar.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_resistanceBand.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_resistanceBand.imageset/Contents.json new file mode 100644 index 0000000..6f3c28e --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_resistanceBand.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_resistanceBand.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_resistanceBand.imageset/equip_resistanceBand.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_resistanceBand.imageset/equip_resistanceBand.png new file mode 100644 index 0000000..2c5fbfe Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_resistanceBand.imageset/equip_resistanceBand.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_sled.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_sled.imageset/Contents.json new file mode 100644 index 0000000..a0038e0 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_sled.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_sled.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_sled.imageset/equip_sled.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_sled.imageset/equip_sled.png new file mode 100644 index 0000000..a3baa3b Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_sled.imageset/equip_sled.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_stabilityBall.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_stabilityBall.imageset/Contents.json new file mode 100644 index 0000000..1fac83e --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_stabilityBall.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_stabilityBall.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_stabilityBall.imageset/equip_stabilityBall.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_stabilityBall.imageset/equip_stabilityBall.png new file mode 100644 index 0000000..ea68052 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_stabilityBall.imageset/equip_stabilityBall.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_suspension.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_suspension.imageset/Contents.json new file mode 100644 index 0000000..43d5770 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_suspension.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_suspension.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_suspension.imageset/equip_suspension.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_suspension.imageset/equip_suspension.png new file mode 100644 index 0000000..1c05dc5 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_suspension.imageset/equip_suspension.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_trapBar.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/equip_trapBar.imageset/Contents.json new file mode 100644 index 0000000..183dba9 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/equip_trapBar.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"equip_trapBar.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/equip_trapBar.imageset/equip_trapBar.png b/App Core/Resources/Swift Code/Assets.xcassets/equip_trapBar.imageset/equip_trapBar.png new file mode 100644 index 0000000..bf30962 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/equip_trapBar.imageset/equip_trapBar.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_arms.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_arms.imageset/Contents.json new file mode 100644 index 0000000..d70c623 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_arms.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_arms.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_arms.imageset/muscle_arms.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_arms.imageset/muscle_arms.png new file mode 100644 index 0000000..61ea110 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_arms.imageset/muscle_arms.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_back.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_back.imageset/Contents.json new file mode 100644 index 0000000..16dfd20 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_back.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_back.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_back.imageset/muscle_back.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_back.imageset/muscle_back.png new file mode 100644 index 0000000..dfe35db Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_back.imageset/muscle_back.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_chest.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_chest.imageset/Contents.json new file mode 100644 index 0000000..d7ea394 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_chest.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_chest.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_chest.imageset/muscle_chest.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_chest.imageset/muscle_chest.png new file mode 100644 index 0000000..a592167 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_chest.imageset/muscle_chest.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_core.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_core.imageset/Contents.json new file mode 100644 index 0000000..1db3400 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_core.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_core.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_core.imageset/muscle_core.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_core.imageset/muscle_core.png new file mode 100644 index 0000000..9a65d89 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_core.imageset/muscle_core.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_legs.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_legs.imageset/Contents.json new file mode 100644 index 0000000..d579da0 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_legs.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_legs.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_legs.imageset/muscle_legs.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_legs.imageset/muscle_legs.png new file mode 100644 index 0000000..4a4bb20 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_legs.imageset/muscle_legs.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_other.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_other.imageset/Contents.json new file mode 100644 index 0000000..1a0f175 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_other.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_other.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_other.imageset/muscle_other.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_other.imageset/muscle_other.png new file mode 100644 index 0000000..a3cafe9 Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_other.imageset/muscle_other.png differ diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_shoulders.imageset/Contents.json b/App Core/Resources/Swift Code/Assets.xcassets/muscle_shoulders.imageset/Contents.json new file mode 100644 index 0000000..b428356 --- /dev/null +++ b/App Core/Resources/Swift Code/Assets.xcassets/muscle_shoulders.imageset/Contents.json @@ -0,0 +1 @@ +{"images":[{"filename":"muscle_shoulders.png","idiom":"universal","scale":"1x"},{"idiom":"universal","scale":"2x"},{"idiom":"universal","scale":"3x"}],"info":{"author":"xcode","version":1},"properties":{"template-rendering-intent":"template"}} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/Assets.xcassets/muscle_shoulders.imageset/muscle_shoulders.png b/App Core/Resources/Swift Code/Assets.xcassets/muscle_shoulders.imageset/muscle_shoulders.png new file mode 100644 index 0000000..3bd80aa Binary files /dev/null and b/App Core/Resources/Swift Code/Assets.xcassets/muscle_shoulders.imageset/muscle_shoulders.png differ diff --git a/App Core/Resources/Swift Code/AuthSession.swift b/App Core/Resources/Swift Code/AuthSession.swift index e01b2c6..0f168b8 100644 --- a/App Core/Resources/Swift Code/AuthSession.swift +++ b/App Core/Resources/Swift Code/AuthSession.swift @@ -24,6 +24,12 @@ final class AuthSession: ObservableObject { func continueAsGuest() { state = .guest } + /// True when signed in with the admin test account. + var isAdmin: Bool { + if case .signedIn(let email) = state, email == "admin" { return true } + return false + } + private func setSignedIn(token: String, email: String) { UserDefaults.standard.set(token, forKey: tokenKey) UserDefaults.standard.set(email, forKey: emailKey) @@ -38,7 +44,12 @@ final class AuthSession: ObservableObject { func signIn(email: String, password: String) async throws { guard !email.isEmpty, !password.isEmpty else { throw NSError(domain: "auth", code: 1, userInfo: [NSLocalizedDescriptionKey: "Email and password are required"]) } - + + // Offline test account bypass + if email == "admin" && password == "password" { + setSignedIn(token: "test_admin_token", email: "admin") + return + } guard let url = URL(string: "\(baseURL)/login") else { throw NSError(domain: "auth", code: 0) } var request = URLRequest(url: url) request.httpMethod = "POST" diff --git a/App Core/Resources/Swift Code/BenchSessionView.swift b/App Core/Resources/Swift Code/BenchSessionView.swift index 713abb8..ba71cc2 100644 --- a/App Core/Resources/Swift Code/BenchSessionView.swift +++ b/App Core/Resources/Swift Code/BenchSessionView.swift @@ -1,14 +1,16 @@ import SwiftUI import AVFoundation +import AVKit import Combine struct BenchSessionView: View { - enum Mode { + enum Mode: String, Codable { case live case demo } let mode: Mode + @Environment(\.dismiss) private var dismiss @StateObject private var camera = CameraController() @StateObject private var pose = PoseLandmarkerService() @@ -84,20 +86,21 @@ struct BenchSessionView: View { var body: some View { ZStack { - if mode == .demo, let player = demoPipeline.player { + Color.black.ignoresSafeArea() + if mode == .demo { GeometryReader { geo in ZStack { - DemoPlayerLayer(player: player) - .onDisappear { player.pause() } + DemoPlayerLayer(player: demoPipeline.player) PoseOverlay(landmarks: pose.landmarks, mirrorX: false, allowedLandmarkIndices: upperBodyIds) .allowsHitTesting(false) } - .aspectRatio(demoPipeline.aspectRatio, contentMode: .fit) - .frame(maxWidth: geo.size.width, maxHeight: geo.size.height) - .frame(maxWidth: geo.size.width, maxHeight: geo.size.height) + .aspectRatio(demoPipeline.aspectRatio, contentMode: .fill) + .frame(width: geo.size.width, height: geo.size.height) + .clipped() } + .ignoresSafeArea() } else if mode == .live { CameraPreview(session: camera.session, mirrored: camera.position == .front) @@ -145,28 +148,28 @@ struct BenchSessionView: View { // Message VStack { - // Top message + // Top message — below HUD buttons Text(isUserCentered ? "Hold still…" : "Center yourself in the frame") - .font(.title3.weight(.semibold)) + .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) + .padding(.horizontal, 12) + .padding(.vertical, 8) .background( (isUserCentered ? Color.green : Color.black) .opacity(0.6), in: Capsule() ) - .padding(.top, insetY / 2) + .padding(.top, 135) .animation(.easeInOut(duration: 0.3), value: isUserCentered) Spacer() - // Bottom hint + // Bottom hint — above rep counter if !isUserCentered { Text("Position your upper body inside the box") .font(.subheadline) .foregroundStyle(.white.opacity(0.7)) - .padding(.bottom, insetY / 2) + .padding(.bottom, 90) } } } @@ -181,6 +184,24 @@ struct BenchSessionView: View { // HUD Overlays — compact, don't block video ZStack { + // TOP-LEFT: Close button + VStack { + HStack { + Button { + dismiss() + } label: { + Text("Close") + .font(.headline) + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 14) + .padding(.vertical, 6) + .background(.black.opacity(0.8), in: Capsule()) + } + Spacer() + } + Spacer() + } + // TOP: Form feedback (centered) VStack { Text(inference.lastPredictionText) @@ -191,7 +212,7 @@ struct BenchSessionView: View { .background(.black.opacity(0.5), in: Capsule()) Spacer() } - .padding(.top, 8) + .padding(.top, 35) // TOP-RIGHT: Controls VStack { @@ -221,8 +242,6 @@ struct BenchSessionView: View { } Spacer() } - .padding(.horizontal) - .padding(.top, 8) // BOTTOM-LEFT: Rep count + tracking VStack { @@ -265,10 +284,14 @@ struct BenchSessionView: View { Spacer() } } - .padding(.horizontal) - .padding(.bottom, 8) } + .padding(.horizontal) + .padding(.top, 55) + .padding(.bottom, 30) } + .background(Color.black) + .ignoresSafeArea() + .toolbar(.hidden, for: .navigationBar) .onAppear { startCurrentMode() } diff --git a/App Core/Resources/Swift Code/BigRectButton.swift b/App Core/Resources/Swift Code/BigRectButton.swift deleted file mode 100644 index cb81af4..0000000 --- a/App Core/Resources/Swift Code/BigRectButton.swift +++ /dev/null @@ -1,27 +0,0 @@ -import SwiftUI - -struct BigRectButton: View { - let title: String - var systemImage: String? = nil - - var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 24, style: .continuous) - .fill(.black) - .shadow(radius: 8, y: 4) - HStack(spacing: 12) { - if let systemImage { - Image(systemName: systemImage) - .font(.title2.weight(.semibold)) - .foregroundStyle(.white) - } - Text(title) - .font(.title2.weight(.bold)) - .foregroundStyle(.white) - } - .padding(.horizontal, 20) - } - .frame(height: 120) - .contentShape(Rectangle()) // easier tapping - } -} diff --git a/App Core/Resources/Swift Code/BootScreen.swift b/App Core/Resources/Swift Code/BootScreen.swift index 225866e..2655f5a 100644 --- a/App Core/Resources/Swift Code/BootScreen.swift +++ b/App Core/Resources/Swift Code/BootScreen.swift @@ -1,4 +1,6 @@ // BootScreen.swift +// mAI Coach — Branded launch/boot screen with animation. + import SwiftUI struct BootScreen: View { @@ -8,12 +10,12 @@ struct BootScreen: View { var body: some View { ZStack { - // Dark gradient background + // Adaptive gradient background LinearGradient( colors: [ - Color(red: 0.08, green: 0.08, blue: 0.12), - Color(red: 0.12, green: 0.12, blue: 0.18), - Color(red: 0.08, green: 0.08, blue: 0.12) + AppColors.bgPrimary, + AppColors.bgSecondary, + AppColors.bgPrimary ], startPoint: .topLeading, endPoint: .bottomTrailing @@ -21,54 +23,47 @@ struct BootScreen: View { .ignoresSafeArea() VStack(spacing: 16) { - Spacer() - - // App logo with scale + fade animation + // Logo Image("AppLogo") - .renderingMode(.template) .resizable() .scaledToFit() .frame(width: 140) - .foregroundStyle(.white) - .scaleEffect(showLogo ? 1.0 : 0.7) + .clipShape(RoundedRectangle(cornerRadius: 28)) + .shadow(color: AppColors.accentGold.opacity(0.3), radius: 20) .opacity(showLogo ? 1 : 0) - .animation(.spring(response: 0.6, dampingFraction: 0.7), value: showLogo) + .scaleEffect(showLogo ? 1 : 0.7) // App name Text("mAI Coach") - .font(.system(size: 32, weight: .bold, design: .rounded)) - .foregroundStyle(.white) + .font(.system(size: 32, weight: .heavy, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) .opacity(showText ? 1 : 0) .offset(y: showText ? 0 : 10) - .animation(.easeOut(duration: 0.5), value: showText) // Tagline Text("Your AI-Powered Form Coach") - .font(.system(size: 15, weight: .medium, design: .rounded)) - .foregroundStyle(.white.opacity(0.6)) + .font(AppFonts.subhead()) + .fontWeight(.medium) + .foregroundStyle(AppColors.accentGold.opacity(0.6)) .opacity(showTagline ? 1 : 0) - .animation(.easeOut(duration: 0.4), value: showTagline) + .offset(y: showTagline ? 0 : 10) - Spacer() - - // Subtle loading indicator + // Spinner ProgressView() - .progressViewStyle(.circular) - .tint(.white.opacity(0.5)) - .scaleEffect(0.8) + .tint(AppColors.accentGold.opacity(0.5)) + .padding(.top, 40) .opacity(showTagline ? 1 : 0) - .animation(.easeIn(duration: 0.3), value: showTagline) - .padding(.bottom, 50) } } .onAppear { - // Staggered animations - withAnimation { showLogo = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - withAnimation { showText = true } + withAnimation(.spring(response: 0.6, dampingFraction: 0.8).delay(0.1)) { + showLogo = true + } + withAnimation(.easeOut(duration: 0.5).delay(0.4)) { + showText = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) { - withAnimation { showTagline = true } + withAnimation(.easeOut(duration: 0.4).delay(0.65)) { + showTagline = true } } } diff --git a/App Core/Resources/Swift Code/CoachView.swift b/App Core/Resources/Swift Code/CoachView.swift deleted file mode 100644 index b3a9e9b..0000000 --- a/App Core/Resources/Swift Code/CoachView.swift +++ /dev/null @@ -1,32 +0,0 @@ -import SwiftUI - -struct CoachView: View { - var body: some View { - VStack(spacing: 24) { - Image("AppLogo") - .resizable() - .scaledToFit() - .frame(width: 100) - - // Tapping this pushes BenchSessionView onto the nav stack - // Tapping this pushes BenchSessionView onto the nav stack - NavigationLink { - BenchSessionView(mode: .live) - } label: { - BigRectButton(title: "Bench (Live)", systemImage: "figure.strengthtraining.traditional") - } - - NavigationLink { - BenchSessionView(mode: .demo) - } label: { - BigRectButton(title: "Demo", systemImage: "play.tv") - } - - Spacer() - } - .padding() - .navigationTitle("Coach") - .navigationBarTitleDisplayMode(.inline) - .background(Color.white.ignoresSafeArea()) - } -} diff --git a/App Core/Resources/Swift Code/ConfettiView.swift b/App Core/Resources/Swift Code/ConfettiView.swift new file mode 100644 index 0000000..22b0624 --- /dev/null +++ b/App Core/Resources/Swift Code/ConfettiView.swift @@ -0,0 +1,118 @@ +// ConfettiView.swift +// mAI Coach — Gold confetti burst for PR celebrations. + +import SwiftUI + +struct ConfettiPiece: Identifiable { + let id = UUID() + let color: Color + let x: CGFloat + let size: CGFloat + let rotation: Double + let delay: Double + let shape: Int // 0 = rect, 1 = circle, 2 = long strip +} + +struct ConfettiView: View { + @Binding var isActive: Bool + @State private var pieces: [ConfettiPiece] = [] + @State private var animate = false + + // Predominantly gold / amber palette + private let colors: [Color] = [ + Color(red: 1, green: 0.84, blue: 0), // bright gold + Color(red: 1, green: 0.76, blue: 0), // deep gold + Color(red: 0.93, green: 0.79, blue: 0.28), // amber gold + Color(red: 1, green: 0.88, blue: 0.35), // light gold + Color(red: 0.85, green: 0.65, blue: 0.13), // dark gold + Color(red: 1, green: 0.92, blue: 0.55), // pale gold + Color(red: 0.72, green: 0.53, blue: 0.04), // bronze + Color(red: 0.15, green: 0.15, blue: 0.15), // dark accent + ] + + var body: some View { + GeometryReader { geo in + ZStack { + ForEach(pieces) { piece in + confettiShape(piece) + .rotationEffect(.degrees(animate ? piece.rotation + 720 : piece.rotation)) + .position( + x: piece.x * geo.size.width, + y: animate ? geo.size.height + 80 : -30 + ) + .opacity(animate ? 0 : 1) + .animation( + .easeIn(duration: Double.random(in: 1.8...3.5)) + .delay(piece.delay), + value: animate + ) + } + } + } + .allowsHitTesting(false) + .onChange(of: isActive) { _, newValue in + if newValue { startConfetti() } + } + } + + @ViewBuilder + private func confettiShape(_ piece: ConfettiPiece) -> some View { + switch piece.shape { + case 0: + RoundedRectangle(cornerRadius: 2) + .fill(piece.color) + .frame(width: piece.size, height: piece.size * 1.5) + case 1: + Circle() + .fill(piece.color) + .frame(width: piece.size * 0.8, height: piece.size * 0.8) + default: + RoundedRectangle(cornerRadius: 1) + .fill(piece.color) + .frame(width: piece.size * 0.5, height: piece.size * 2.5) + } + } + + private func startConfetti() { + pieces = (0..<200).map { _ in + ConfettiPiece( + color: colors.randomElement()!, + x: CGFloat.random(in: -0.05...1.05), + size: CGFloat.random(in: 6...14), + rotation: Double.random(in: 0...360), + delay: Double.random(in: 0...0.8), + shape: Int.random(in: 0...2) + ) + } + + animate = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + animate = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { + isActive = false + animate = false + pieces = [] + } + } +} + +// MARK: - View Modifier for easy use +struct ConfettiModifier: ViewModifier { + @Binding var isActive: Bool + + func body(content: Content) -> some View { + content.overlay( + ConfettiView(isActive: $isActive) + .ignoresSafeArea() + ) + } +} + +extension View { + /// Add a confetti burst overlay. Set `isActive` to true to trigger. + func confetti(isActive: Binding) -> some View { + modifier(ConfettiModifier(isActive: isActive)) + } +} diff --git a/App Core/Resources/Swift Code/DataBackupManager.swift b/App Core/Resources/Swift Code/DataBackupManager.swift new file mode 100644 index 0000000..c9e9fb8 --- /dev/null +++ b/App Core/Resources/Swift Code/DataBackupManager.swift @@ -0,0 +1,113 @@ +// DataBackupManager.swift +// mAI Coach — Consolidates all user data into a single JSON backup file. +// Ready for future cloud sync when accounts are added. + +import Foundation + +/// All user data in one struct, ready for JSON export / server push. +struct UserDataBackup: Codable { + var version: Int = 1 + var lastUpdated: Date + var settings: SettingsBackup + var workouts: [Workout] + var favoriteTemplateNames: [String] + var archivedTemplateNames: [String] +} + +/// Settings snapshot. +struct SettingsBackup: Codable { + var appearance: String // "light" / "dark" + var coachVoiceGender: String // "male" / "female" + var showDevData: Bool + var maxTemplateShortcuts: Int +} + +@MainActor +final class DataBackupManager { + static let shared = DataBackupManager() + + private static let backupURL: URL = { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + return docs.appending(path: "user_backup.json") + }() + + private init() {} + + // MARK: - Export + + /// Build a full backup from the current app state. + func createBackup(from store: WorkoutStore) -> UserDataBackup { + let settings = SettingsBackup( + appearance: UserDefaults.standard.string(forKey: SettingsKeys.appearance) ?? "light", + coachVoiceGender: UserDefaults.standard.string(forKey: SettingsKeys.coachVoice) ?? "male", + showDevData: UserDefaults.standard.bool(forKey: SettingsKeys.showDevData), + maxTemplateShortcuts: { + let v = UserDefaults.standard.integer(forKey: SettingsKeys.maxTemplateShortcuts) + return v == 0 ? 4 : v + }() + ) + + return UserDataBackup( + lastUpdated: Date(), + settings: settings, + workouts: store.workouts, + favoriteTemplateNames: store.favoriteTemplateNames, + archivedTemplateNames: store.archivedTemplateNames + ) + } + + /// Save full backup to disk as JSON. + func saveBackup(from store: WorkoutStore) { + let backup = createBackup(from: store) + do { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(backup) + try data.write(to: Self.backupURL, options: .atomic) + #if DEBUG + print("[DataBackupManager] Saved backup (\(data.count) bytes) to \(Self.backupURL.lastPathComponent)") + #endif + } catch { + print("[DataBackupManager] Failed to save backup: \(error)") + } + } + + // MARK: - Import + + /// Load a backup from disk. + func loadBackup() -> UserDataBackup? { + guard FileManager.default.fileExists(atPath: Self.backupURL.path) else { return nil } + do { + let data = try Data(contentsOf: Self.backupURL) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return try decoder.decode(UserDataBackup.self, from: data) + } catch { + print("[DataBackupManager] Failed to load backup: \(error)") + return nil + } + } + + /// Restore a backup into the app state. + func restoreBackup(_ backup: UserDataBackup, into store: WorkoutStore) { + // Restore workouts + store.workouts = backup.workouts + store.save() + + // Restore favorites & archived + store.favoriteTemplateNames = backup.favoriteTemplateNames + store.archivedTemplateNames = backup.archivedTemplateNames + UserDefaults.standard.set(backup.favoriteTemplateNames, forKey: "favorite_template_names") + UserDefaults.standard.set(backup.archivedTemplateNames, forKey: "archived_template_names") + + // Restore settings + UserDefaults.standard.set(backup.settings.appearance, forKey: SettingsKeys.appearance) + UserDefaults.standard.set(backup.settings.coachVoiceGender, forKey: SettingsKeys.coachVoice) + UserDefaults.standard.set(backup.settings.showDevData, forKey: SettingsKeys.showDevData) + UserDefaults.standard.set(backup.settings.maxTemplateShortcuts, forKey: SettingsKeys.maxTemplateShortcuts) + } + + /// URL of the backup file (for future upload to server). + var backupFileURL: URL { Self.backupURL } +} diff --git a/App Core/Resources/Swift Code/DemoPlayerPipeline.swift b/App Core/Resources/Swift Code/DemoPlayerPipeline.swift index 8950d09..5dcbb5f 100644 --- a/App Core/Resources/Swift Code/DemoPlayerPipeline.swift +++ b/App Core/Resources/Swift Code/DemoPlayerPipeline.swift @@ -8,11 +8,12 @@ final class DemoPlayerPipeline: ObservableObject { @Published var framesRead: Int = 0 @Published var lastError: String = "" @Published var aspectRatio: CGFloat = 9.0 / 16.0 - private(set) var player: AVPlayer? + @Published var player: AVPlayer = AVPlayer() private var reader: AVAssetReader? private var output: AVAssetReaderVideoCompositionOutput? private let queue = DispatchQueue(label: "demo.pipeline.reader") private var isCancelled = false + private var statusObservers = Set() func start(url: URL, poseService: PoseLandmarkerService) { stop() @@ -20,6 +21,8 @@ final class DemoPlayerPipeline: ObservableObject { self.status = "Loading demo..." self.framesRead = 0 self.lastError = "" + // Clear current item just in case + self.player.replaceCurrentItem(with: nil) } // Use modern async APIs to load track properties @@ -45,18 +48,48 @@ final class DemoPlayerPipeline: ObservableObject { nominalFrameRate: nominalFrameRate ) let rs = playerComp.renderSize + + let item = AVPlayerItem(asset: playerAsset) + await MainActor.run { self.aspectRatio = rs.width / max(rs.height, 1) - } + self.statusObservers.removeAll() - // Set up visible player (no videoComposition - AVPlayer handles orientation natively) - let item = AVPlayerItem(asset: playerAsset) - let newPlayer = AVPlayer(playerItem: item) + // Observe item status — only play when truly ready + item.publisher(for: \.status) + .receive(on: DispatchQueue.main) + .sink { [weak self] status in + guard let self else { return } + switch status { + case .readyToPlay: + self.player.play() + self.status = "Playing demo..." + case .failed: + self.lastError = item.error?.localizedDescription ?? "Playback failed" + self.status = "Error" + default: + break + } + } + .store(in: &self.statusObservers) - await MainActor.run { - self.player = newPlayer - newPlayer.play() - self.status = "Playing demo..." + // Recover from unexpected pauses (e.g. layer not in window) + self.player.publisher(for: \.timeControlStatus) + .receive(on: DispatchQueue.main) + .sink { [weak self] controlStatus in + guard let self, + self.player.currentItem != nil, + self.player.currentItem?.status == .readyToPlay, + controlStatus == .paused, + self.status == "Playing demo..." + else { return } + // Player paused unexpectedly while we think it should play + self.player.play() + } + .store(in: &self.statusObservers) + + self.player.replaceCurrentItem(with: item) + self.status = "Loading demo..." } // Set up separate reader for pose inference @@ -90,13 +123,14 @@ final class DemoPlayerPipeline: ObservableObject { func stop() { isCancelled = true + statusObservers.removeAll() queue.sync { reader?.cancelReading() reader = nil output = nil } - player?.pause() - player = nil + player.pause() + player.replaceCurrentItem(with: nil) isCancelled = false DispatchQueue.main.async { self.status = "Stopped" diff --git a/App Core/Resources/Swift Code/DesignTokens.swift b/App Core/Resources/Swift Code/DesignTokens.swift new file mode 100644 index 0000000..7157590 --- /dev/null +++ b/App Core/Resources/Swift Code/DesignTokens.swift @@ -0,0 +1,105 @@ +// DesignTokens.swift +// mAI Coach — Design System +// Shared color, typography, and spacing tokens for the app. + +import SwiftUI + +// MARK: - Colors +enum AppColors { + // Accent + static let accent = Color("Accent", bundle: nil) + static let accentGold = Color(light: .init(hex: 0xAD8C04), dark: .init(hex: 0xC9A30A)) + static let accentText = Color(light: .init(hex: 0x8A7003), dark: .init(hex: 0xC9A30A)) + static let accentMuted = Color(light: .init(hex: 0xAD8C04, alpha: 0.12), + dark: .init(hex: 0xC9A30A, alpha: 0.15)) + + // Backgrounds + static let bgPrimary = Color(light: .init(hex: 0xFFFFFF), dark: .init(hex: 0x0A0A0F)) + static let bgSecondary = Color(light: .init(hex: 0xF5F5F5), dark: .init(hex: 0x141420)) + static let bgTertiary = Color(light: .init(hex: 0xEBEBEB), dark: .init(hex: 0x1E1E2C)) + static let bgElevated = Color(light: .init(hex: 0xFFFFFF), dark: .init(hex: 0x1A1A28)) + + // Text + static let textPrimary = Color(light: .init(hex: 0x1A1A1A), dark: .init(hex: 0xF0F0F0)) + static let textSecondary = Color(light: .init(hex: 0x6B6B6B), dark: .init(hex: 0x8A8A8A)) + static let textTertiary = Color(light: .init(hex: 0x999999), dark: .init(hex: 0x555555)) + + // Borders + static let border = Color(light: .init(hex: 0xE0E0E0), dark: .init(hex: 0x2A2A34)) + static let divider = Color(light: .init(hex: 0xF0F0F0), dark: .init(hex: 0x1E1E28)) + + // Semantic + static let success = Color(light: .init(hex: 0x2D9A3F), dark: .init(hex: 0x34C759)) + static let warning = Color(light: .init(hex: 0xCC8800), dark: .init(hex: 0xFF9F0A)) + static let error = Color(light: .init(hex: 0xD32F2F), dark: .init(hex: 0xFF453A)) +} + +// MARK: - Typography +enum AppFonts { + static func displayLarge() -> Font { .system(size: 34, weight: .bold, design: .rounded) } + static func displaySmall() -> Font { .system(size: 28, weight: .bold, design: .rounded) } + static func headline() -> Font { .system(size: 22, weight: .semibold, design: .rounded) } + static func title() -> Font { .system(size: 20, weight: .semibold, design: .rounded) } + static func body() -> Font { .system(size: 17, weight: .regular, design: .rounded) } + static func bodyBold() -> Font { .system(size: 17, weight: .semibold, design: .rounded) } + static func callout() -> Font { .system(size: 16, weight: .regular, design: .rounded) } + static func subhead() -> Font { .system(size: 15, weight: .regular, design: .rounded) } + static func footnote() -> Font { .system(size: 13, weight: .regular, design: .rounded) } + static func caption() -> Font { .system(size: 11, weight: .medium, design: .rounded) } +} + +// MARK: - Spacing +enum AppSpacing { + static let xs: CGFloat = 4 + static let sm: CGFloat = 8 + static let md: CGFloat = 12 + static let lg: CGFloat = 16 + static let xl: CGFloat = 24 + static let xxl: CGFloat = 32 + static let xxxl: CGFloat = 48 + static let screenPadding: CGFloat = 16 + static let cardRadius: CGFloat = 12 + static let buttonRadius: CGFloat = 12 + static let bigButtonRadius: CGFloat = 24 +} + +// MARK: - Color Helpers +extension Color { + /// Adaptive color: uses light variant in light mode, dark variant in dark mode. + init(light: UIColor, dark: UIColor) { + self.init(uiColor: UIColor { traits in + traits.userInterfaceStyle == .dark ? dark : light + }) + } +} + +extension UIColor { + /// Create a UIColor from a hex value, e.g. 0xAD8C04 + convenience init(hex: UInt, alpha: CGFloat = 1.0) { + self.init( + red: CGFloat((hex >> 16) & 0xFF) / 255, + green: CGFloat((hex >> 8) & 0xFF) / 255, + blue: CGFloat(hex & 0xFF) / 255, + alpha: alpha + ) + } +} + +// MARK: - View Modifiers +struct CardStyle: ViewModifier { + func body(content: Content) -> some View { + content + .background(AppColors.bgSecondary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(AppColors.border, lineWidth: 1) + ) + } +} + +extension View { + func cardStyle() -> some View { + modifier(CardStyle()) + } +} diff --git a/App Core/Resources/Swift Code/ExerciseCardView.swift b/App Core/Resources/Swift Code/ExerciseCardView.swift new file mode 100644 index 0000000..ce32a87 --- /dev/null +++ b/App Core/Resources/Swift Code/ExerciseCardView.swift @@ -0,0 +1,521 @@ +// ExerciseCardView.swift +// mAI Coach — Reusable exercise card with sets, weight, reps, and mAI Coach button. + +import SwiftUI + +struct ExerciseCardView: View { + @Binding var exercise: Exercise + @EnvironmentObject var store: WorkoutStore + @EnvironmentObject var restTimer: RestTimerManager + var onCoachTap: ((BenchSessionView.Mode) -> Void)? + var onAutoStart: (() -> Void)? = nil + var isEditing: Bool = false + var workoutId: UUID? = nil + @AppStorage(SettingsKeys.showPRBadges) private var showPRBadges = true + @AppStorage(SettingsKeys.showRPE) private var showRPE = false + + // History & Quick Fill state + @State private var showHistory = false + @State private var showLastConfirm = false + @State private var showNoData = false + @State private var showInfo = false + + /// Catalog entry for this exercise (for info panel). + private var catalogEntry: CatalogExercise? { + ExerciseCatalog.lookup(exercise.name) + } + + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + // Header + HStack { + Text(exercise.name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + + // PR badge + if showPRBadges, let pr = store.effectivePRs()[exercise.name] { + HStack(spacing: 3) { + Image(systemName: "trophy.fill") + .font(.system(size: 11)) + Text("\(Int(pr.weight))") + .font(.system(size: 11, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 6) + .padding(.vertical, 3) + .background(AppColors.accentMuted) + .clipShape(Capsule()) + } + + Spacer() + + // ↻ Last shortcut + Button { + let history = store.exerciseHistory(named: exercise.name, limit: 1) + guard let last = history.first else { + showNoData = true + return + } + let hasData = exercise.sets.contains { !$0.weight.isEmpty || !$0.reps.isEmpty } + if hasData { + showLastConfirm = true + } else { + fillSets(from: last.sets) + } + } label: { + HStack(spacing: 3) { + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 10, weight: .bold)) + Text("Last") + .font(.system(size: 11, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.textSecondary) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(AppColors.border, lineWidth: 1) + ) + } + + // History button + Button { showHistory = true } label: { + Image(systemName: "clock.arrow.circlepath") + .font(.system(size: 14)) + .foregroundStyle(AppColors.textSecondary) + .frame(width: 32, height: 32) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(AppColors.border, lineWidth: 1) + ) + } + + if exercise.hasCoachModel, let mode = exercise.coachMode { + Button { + onCoachTap?(mode) + } label: { + HStack(spacing: 4) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 12)) + Text("mAI Coach") + .font(.system(size: 11, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.accentGold, lineWidth: 1) + ) + .fixedSize() + } + } + } + + // Column headers + HStack(spacing: 6) { + Text("SET") + .frame(width: 36) + Text("LBS") + .frame(maxWidth: .infinity) + Text("REPS") + .frame(maxWidth: .infinity) + if showRPE { + Text("RPE") + .frame(width: 44) + } + Color.clear.frame(width: 36) // spacer for check button + } + .font(.system(size: 11, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + + Divider().background(AppColors.border) + + // Set rows + ForEach($exercise.sets) { $set in + HStack(spacing: 4) { + // Delete set button (only in edit mode) + if isEditing { + Button { + if let idx = exercise.sets.firstIndex(where: { $0.id == set.id }) { + if exercise.sets.count > 1 { + exercise.sets.remove(at: idx) + } else { + // Last set: replace with empty + exercise.sets[idx] = ExerciseSet() + } + } + } label: { + Image(systemName: "minus.circle.fill") + .font(.system(size: 16)) + .foregroundStyle(.red.opacity(0.7)) + } + .buttonStyle(.plain) + } + + SetRowView( + set: $set, + index: exercise.sets.firstIndex(where: { $0.id == set.id }).map { $0 + 1 } ?? 1, + showRPE: showRPE, + onComplete: { + // Auto-start workout if not active + onAutoStart?() + // Track set completion for duration timer + if let wid = workoutId { + store.recordSetCompletion(workoutId: wid) + } + let weight = Double(set.weight) ?? 0 + let reps = Int(set.reps) ?? 0 + store.checkSetPR(exerciseName: exercise.name, weight: weight, reps: reps) + // Auto-start rest timer if enabled + if UserDefaults.standard.object(forKey: "rest_timer_enabled") as? Bool ?? true { + withAnimation(.spring(response: 0.3)) { + restTimer.start() + } + } + } + ) + } + } + + // Add Set + Button { + exercise.sets.append(ExerciseSet()) + } label: { + Text("+ Add Set") + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.textSecondary) + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [6])) + .foregroundStyle(AppColors.border) + ) + } + + // Notes + TextField("Notes...", text: $exercise.notes) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textPrimary) + .padding(8) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.border, lineWidth: 1) + ) + + // ⓘ Info button (bottom-right) + HStack { + Spacer() + Button { + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + showInfo.toggle() + } + } label: { + HStack(spacing: 4) { + Image(systemName: showInfo ? "info.circle.fill" : "info.circle") + .font(.system(size: 13)) + Text(showInfo ? "Hide Info" : "Info") + .font(.system(size: 11, weight: .semibold, design: .rounded)) + } + .foregroundStyle(showInfo ? AppColors.accentGold : AppColors.textTertiary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + } + .buttonStyle(.plain) + } + + // ── Expandable Info Panel (below everything) ── + if showInfo, let cat = catalogEntry { + exerciseInfoPanel(cat) + .transition(.opacity.combined(with: .scale(scale: 0.95, anchor: .top))) + } + } + .padding(14) + .cardStyle() + .sheet(isPresented: $showHistory) { + ExerciseHistorySheet(exerciseName: exercise.name) { historicalSets in + fillSets(from: historicalSets) + } + .environmentObject(store) + } + .alert("Replace Current Sets?", isPresented: $showLastConfirm) { + Button("Cancel", role: .cancel) { } + Button("Fill") { + if let last = store.exerciseHistory(named: exercise.name, limit: 1).first { + fillSets(from: last.sets) + } + } + } message: { + Text("This will replace your current sets with data from your last session.") + } + .alert("No Data Found", isPresented: $showNoData) { + Button("OK", role: .cancel) { } + } message: { + Text("No previous sessions found for \(exercise.name).") + } + } + + // MARK: - Quick Fill Helper + private func fillSets(from historicalSets: [ExerciseSet]) { + exercise.sets = historicalSets.map { old in + ExerciseSet(weight: old.weight, reps: old.reps, isComplete: false) + } + } + + // MARK: - Exercise Info Panel + @ViewBuilder + private func exerciseInfoPanel(_ cat: CatalogExercise) -> some View { + VStack(alignment: .leading, spacing: 10) { + + // Description + if !cat.description.isEmpty { + Text(cat.description) + .font(AppFonts.footnote()) + .italic() + .foregroundStyle(AppColors.textSecondary) + } + + // Primary muscles + if !cat.primaryMuscles.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("MUSCLES") + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + FlowLayout(spacing: 4) { + ForEach(cat.primaryMuscles, id: \.self) { muscle in + Text(muscle.capitalized) + .font(.system(size: 11, weight: .semibold)) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(AppColors.accentMuted) + .foregroundStyle(AppColors.accentGold) + .clipShape(Capsule()) + } + } + } + } + + // Difficulty badge + HStack(spacing: 6) { + Text("DIFFICULTY") + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + Text(cat.difficulty.capitalized) + .font(.system(size: 11, weight: .bold, design: .rounded)) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(difficultyColor(cat.difficulty).opacity(0.15)) + .foregroundStyle(difficultyColor(cat.difficulty)) + .clipShape(Capsule()) + } + + // Form cues + if !cat.formCues.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("FORM CUES") + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + ForEach(cat.formCues, id: \.self) { cue in + HStack(alignment: .top, spacing: 6) { + Text("✅") + .font(.system(size: 10)) + Text(cue) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textPrimary) + } + } + } + } + + // Common mistakes + if !cat.commonMistakes.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("COMMON MISTAKES") + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + ForEach(cat.commonMistakes, id: \.self) { mistake in + HStack(alignment: .top, spacing: 6) { + Text("⚠️") + .font(.system(size: 10)) + Text(mistake) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textPrimary) + } + } + } + } + + // Watch Tutorial button + if let urlStr = cat.videoURL, let url = URL(string: urlStr) { + Link(destination: url) { + HStack(spacing: 6) { + Image(systemName: "play.circle.fill") + .font(.system(size: 16)) + Text("Watch Tutorial") + .font(AppFonts.footnote()) + .fontWeight(.semibold) + } + .foregroundStyle(.white) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background(Color.red) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } + .padding(12) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + private func difficultyColor(_ difficulty: String) -> Color { + switch difficulty.lowercased() { + case "beginner": return .green + case "intermediate": return .orange + case "advanced": return .red + default: return AppColors.textSecondary + } + } +} + +// MARK: - Set Row +struct SetRowView: View { + @Binding var set: ExerciseSet + let index: Int + var showRPE: Bool = false + var onComplete: (() -> Void)? = nil + + var body: some View { + HStack(spacing: 6) { + Text("\(index)") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.textSecondary) + .frame(width: 36) + + TextField("—", text: $set.weight) + .multilineTextAlignment(.center) + .keyboardType(.decimalPad) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + .padding(8) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.border, lineWidth: 1) + ) + .onChange(of: set.weight) { _, newValue in + let filtered = newValue.filter { $0.isNumber || $0 == "." } + if filtered != newValue { set.weight = filtered } + } + + TextField("—", text: $set.reps) + .multilineTextAlignment(.center) + .keyboardType(.numberPad) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + .padding(8) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.border, lineWidth: 1) + ) + .onChange(of: set.reps) { _, newValue in + let filtered = newValue.filter { $0.isNumber } + if filtered != newValue { set.reps = filtered } + } + + if showRPE { + Menu { + Button("—") { set.rpe = "" } + ForEach(1...10, id: \.self) { val in + Button("\(val)") { set.rpe = "\(val)" } + } + } label: { + Text(set.rpe.isEmpty ? "—" : set.rpe) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundStyle(set.rpe.isEmpty ? AppColors.textTertiary : AppColors.accentGold) + .frame(width: 44, height: 36) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.border, lineWidth: 1) + ) + } + } + + Button { + set.isComplete.toggle() + if set.isComplete { + onComplete?() + } + } label: { + Image(systemName: "checkmark") + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(set.isComplete ? .black : AppColors.textTertiary) + .frame(width: 36, height: 36) + .background(set.isComplete ? AppColors.accentGold : AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(set.isComplete ? AppColors.accentGold : AppColors.border, lineWidth: 1.5) + ) + } + } + } +} + +// MARK: - Flow Layout (wrapping horizontal layout for tags) +struct FlowLayout: Layout { + var spacing: CGFloat = 6 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let maxWidth = proposal.width ?? .infinity + var x: CGFloat = 0 + var y: CGFloat = 0 + var rowHeight: CGFloat = 0 + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x + size.width > maxWidth && x > 0 { + y += rowHeight + spacing + x = 0 + rowHeight = 0 + } + x += size.width + spacing + rowHeight = max(rowHeight, size.height) + } + return CGSize(width: maxWidth, height: y + rowHeight) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + var x = bounds.minX + var y = bounds.minY + var rowHeight: CGFloat = 0 + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x + size.width > bounds.maxX && x > bounds.minX { + y += rowHeight + spacing + x = bounds.minX + rowHeight = 0 + } + subview.place(at: CGPoint(x: x, y: y), proposal: .unspecified) + x += size.width + spacing + rowHeight = max(rowHeight, size.height) + } + } +} diff --git a/App Core/Resources/Swift Code/ExerciseCatalog.swift b/App Core/Resources/Swift Code/ExerciseCatalog.swift new file mode 100644 index 0000000..d49808a --- /dev/null +++ b/App Core/Resources/Swift Code/ExerciseCatalog.swift @@ -0,0 +1,135 @@ +// ExerciseCatalog.swift +// mAI Coach — Exercise database loaded from exercise_library.json. +// Supports 446+ exercises with extended metadata. + +import Foundation + +// MARK: - Cable Attachment Type + +enum CableAttachment: String, Codable { + case rope + case straightBar + case vBar + case ezBar + case dHandle + case wideBar + case ankleStrap +} + +// MARK: - Catalog Exercise + +struct CatalogExercise: Identifiable, Decodable { + let id: UUID + let name: String + let muscleGroup: String + let equipment: EquipmentType + let coachMode: BenchSessionView.Mode? + let videoURL: String? + let description: String + let primaryMuscles: [String] + let secondaryMuscles: [String] + let formCues: [String] + let commonMistakes: [String] + let difficulty: String + let isUnilateral: Bool + let isCompound: Bool + let cableAttachment: CableAttachment? + let notes: String? + let meta: [String: String] + + var hasCoachModel: Bool { coachMode != nil } + + // Custom Decodable to handle BenchSessionView.Mode from string + enum CodingKeys: String, CodingKey { + case name, muscleGroup, equipment, coachMode, videoURL, description + case primaryMuscles, secondaryMuscles, formCues, commonMistakes + case difficulty, isUnilateral, isCompound + case cableAttachment, notes, meta + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + self.id = UUID() + self.name = try c.decode(String.self, forKey: .name) + self.muscleGroup = try c.decode(String.self, forKey: .muscleGroup) + self.equipment = try c.decode(EquipmentType.self, forKey: .equipment) + self.description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" + self.primaryMuscles = try c.decodeIfPresent([String].self, forKey: .primaryMuscles) ?? [] + self.secondaryMuscles = try c.decodeIfPresent([String].self, forKey: .secondaryMuscles) ?? [] + self.formCues = try c.decodeIfPresent([String].self, forKey: .formCues) ?? [] + self.commonMistakes = try c.decodeIfPresent([String].self, forKey: .commonMistakes) ?? [] + self.difficulty = try c.decodeIfPresent(String.self, forKey: .difficulty) ?? "intermediate" + self.isUnilateral = try c.decodeIfPresent(Bool.self, forKey: .isUnilateral) ?? false + self.isCompound = try c.decodeIfPresent(Bool.self, forKey: .isCompound) ?? false + self.cableAttachment = try c.decodeIfPresent(CableAttachment.self, forKey: .cableAttachment) + self.videoURL = try c.decodeIfPresent(String.self, forKey: .videoURL) + self.notes = try c.decodeIfPresent(String.self, forKey: .notes) + self.meta = try c.decodeIfPresent([String: String].self, forKey: .meta) ?? [:] + + // Decode coachMode from string ("live" / "demo") + if let modeStr = try c.decodeIfPresent(String.self, forKey: .coachMode) { + switch modeStr { + case "live": self.coachMode = .live + case "demo": self.coachMode = .demo + default: self.coachMode = nil + } + } else { + self.coachMode = nil + } + } + + /// Manual init for backward compatibility / testing + init(name: String, equipment: EquipmentType, muscleGroup: String, coachMode: BenchSessionView.Mode? = nil) { + self.id = UUID() + self.name = name + self.equipment = equipment + self.muscleGroup = muscleGroup + self.coachMode = coachMode + self.videoURL = nil + self.description = "" + self.primaryMuscles = [] + self.secondaryMuscles = [] + self.formCues = [] + self.commonMistakes = [] + self.difficulty = "intermediate" + self.isUnilateral = false + self.isCompound = false + self.cableAttachment = nil + self.notes = nil + self.meta = [:] + } +} + +// MARK: - Catalog + +enum ExerciseCatalog { + /// JSON wrapper structure + private struct ExerciseLibrary: Decodable { + let exercises: [CatalogExercise] + } + + /// All exercises loaded from exercise_library.json + static let exercises: [CatalogExercise] = { + guard let url = Bundle.main.url(forResource: "exercise_library", withExtension: "json"), + let data = try? Data(contentsOf: url) else { + print("⚠️ ExerciseCatalog: exercise_library.json not found in bundle, using empty catalog") + return [] + } + do { + let library = try JSONDecoder().decode(ExerciseLibrary.self, from: data) + print("✅ ExerciseCatalog: loaded \(library.exercises.count) exercises") + return library.exercises + } catch { + print("⚠️ ExerciseCatalog: failed to decode exercise_library.json: \(error)") + return [] + } + }() + + /// All unique muscle groups in display order. + static let muscleGroups = ["Chest", "Back", "Shoulders", "Arms", "Legs", "Core", "Other"] + + /// Look up a catalog exercise by name. + static func lookup(_ name: String) -> CatalogExercise? { + exercises.first { $0.name == name } + } +} diff --git a/App Core/Resources/Swift Code/ExerciseHistorySheet.swift b/App Core/Resources/Swift Code/ExerciseHistorySheet.swift new file mode 100644 index 0000000..a15d639 --- /dev/null +++ b/App Core/Resources/Swift Code/ExerciseHistorySheet.swift @@ -0,0 +1,154 @@ +// ExerciseHistorySheet.swift +// mAI Coach — Shows past sessions for an exercise with Quick Fill. + +import SwiftUI + +struct ExerciseHistorySheet: View { + let exerciseName: String + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + /// Called when user confirms Quick Fill with a set of historical sets. + var onFill: ([ExerciseSet]) -> Void + + @State private var confirmSets: [ExerciseSet]? + @State private var showConfirmAlert = false + + private var history: [(date: Date, workoutName: String, sets: [ExerciseSet])] { + store.exerciseHistory(named: exerciseName) + } + + var body: some View { + NavigationStack { + Group { + if history.isEmpty { + emptyState + } else { + historyList + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("\(exerciseName) History") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Close") { dismiss() } + .foregroundStyle(AppColors.accentGold) + } + } + .alert("Replace Current Sets?", isPresented: $showConfirmAlert) { + Button("Cancel", role: .cancel) { confirmSets = nil } + Button("Fill") { + if let sets = confirmSets { + onFill(sets) + confirmSets = nil + dismiss() + } + } + } message: { + Text("This will replace your current sets with the selected session's data.") + } + } + } + + // MARK: - Empty State + private var emptyState: some View { + VStack(spacing: 12) { + Spacer() + Image(systemName: "clock.arrow.circlepath") + .font(.system(size: 48)) + .foregroundStyle(AppColors.textTertiary) + .opacity(0.5) + Text("No History Found") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + Text("Complete a workout with \(exerciseName) to see history here.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + } + + // MARK: - History List + private var historyList: some View { + ScrollView { + VStack(spacing: 12) { + ForEach(Array(history.enumerated()), id: \.offset) { _, entry in + historyCard(entry: entry) + } + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + } + + private func historyCard(entry: (date: Date, workoutName: String, sets: [ExerciseSet])) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Header: date + workout name + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(entry.date, style: .date) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text(entry.workoutName) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + Button { + confirmSets = entry.sets + showConfirmAlert = true + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.down.doc.fill") + .font(.system(size: 11)) + Text("Quick Fill") + .font(.system(size: 12, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.accentGold, lineWidth: 1) + ) + } + } + + Divider().background(AppColors.border) + + // Sets table + HStack(spacing: 0) { + Text("SET") + .frame(width: 36) + Text("LBS") + .frame(maxWidth: .infinity) + Text("REPS") + .frame(maxWidth: .infinity) + } + .font(.system(size: 11, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + + ForEach(Array(entry.sets.enumerated()), id: \.offset) { i, set in + HStack(spacing: 0) { + Text("\(i + 1)") + .frame(width: 36) + .foregroundStyle(AppColors.textSecondary) + Text(set.weight.isEmpty ? "—" : set.weight) + .frame(maxWidth: .infinity) + .foregroundStyle(AppColors.textPrimary) + Text(set.reps.isEmpty ? "—" : set.reps) + .frame(maxWidth: .infinity) + .foregroundStyle(AppColors.textPrimary) + } + .font(.system(size: 14, weight: .medium, design: .rounded)) + } + } + .padding(14) + .cardStyle() + } +} diff --git a/App Core/Resources/Swift Code/ExercisePickerView.swift b/App Core/Resources/Swift Code/ExercisePickerView.swift new file mode 100644 index 0000000..82c285e --- /dev/null +++ b/App Core/Resources/Swift Code/ExercisePickerView.swift @@ -0,0 +1,213 @@ +// ExercisePickerView.swift +// mAI Coach — Searchable exercise picker with equipment + muscle group filters. + +import SwiftUI + +struct ExercisePickerView: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + /// Optional callback for adding exercises directly (template/builder mode). + /// When nil, falls back to store.addExercise() (live workout mode). + var onSelect: ((Exercise) -> Void)? = nil + + @State private var searchText = "" + @State private var selectedEquipment: EquipmentType? + @State private var selectedMuscleGroup: String? + + private var filteredExercises: [CatalogExercise] { + ExerciseCatalog.exercises.filter { ex in + let matchesEquipment = selectedEquipment == nil || ex.equipment == selectedEquipment + let matchesMuscle = selectedMuscleGroup == nil || ex.muscleGroup == selectedMuscleGroup + let matchesSearch = searchText.isEmpty || + ex.name.localizedCaseInsensitiveContains(searchText) + return matchesEquipment && matchesMuscle && matchesSearch + } + } + + private var sortedExercises: [CatalogExercise] { + filteredExercises.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + // Muscle group filter row + muscleGroupFilter + .padding(.top, 8) + .padding(.bottom, 4) + + // Equipment filter row + equipmentFilter + .padding(.bottom, 4) + + // Exercise list (alphabetical) + List { + ForEach(sortedExercises) { exercise in + exerciseRow(exercise) + } + } + .listStyle(.plain) + } + .searchable(text: $searchText, prompt: "Search exercises") + .navigationTitle("Add Exercise") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + .foregroundStyle(AppColors.textSecondary) + } + } + } + } + + // MARK: - Muscle Group Filter + + private static let muscleGroupIcons: [String: String] = [ + "Chest": "muscle_chest", + "Back": "muscle_back", + "Shoulders": "muscle_shoulders", + "Arms": "muscle_arms", + "Legs": "muscle_legs", + "Core": "muscle_core", + "Other": "muscle_other", + ] + + private var muscleGroupFilter: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + filterPill(label: "All", icon: "square.grid.2x2", isSystemImage: true, isSelected: selectedMuscleGroup == nil) { + selectedMuscleGroup = nil + } + + ForEach(ExerciseCatalog.muscleGroups, id: \.self) { group in + let iconName = Self.muscleGroupIcons[group] ?? "muscle_other" + filterPill(label: group, icon: iconName, isSystemImage: false, isSelected: selectedMuscleGroup == group) { + selectedMuscleGroup = group + } + } + } + .padding(.horizontal, 16) + } + } + + // MARK: - Equipment Filter + + private var equipmentFilter: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + filterPill(label: "All", icon: "square.grid.2x2", isSystemImage: true, isSelected: selectedEquipment == nil) { + selectedEquipment = nil + } + + ForEach(EquipmentType.allCases) { type in + filterPill(label: type.label, icon: type.icon, isSystemImage: false, isSelected: selectedEquipment == type) { + selectedEquipment = type + } + } + } + .padding(.horizontal, 16) + } + } + + // MARK: - Filter Pill + + private func filterPill(label: String, icon: String, isSystemImage: Bool = false, isSelected: Bool, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: 5) { + if isSystemImage { + Image(systemName: icon) + .font(.system(size: 12)) + } else { + Image(icon) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 18, height: 18) + } + Text(label) + .font(AppFonts.footnote()) + .fontWeight(.medium) + } + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background(isSelected ? AppColors.accentGold : Color(.secondarySystemGroupedBackground)) + .foregroundStyle(isSelected ? .black : AppColors.textPrimary) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + + // MARK: - Exercise Row + + private func exerciseRow(_ exercise: CatalogExercise) -> some View { + Button { + let ex = Exercise( + name: exercise.name, + equipment: exercise.equipment, + muscleGroup: exercise.muscleGroup, + hasCoachModel: exercise.hasCoachModel, + coachMode: exercise.coachMode + ) + if let onSelect { + onSelect(ex) + } else { + store.addExercise( + name: exercise.name, + equipment: exercise.equipment, + muscleGroup: exercise.muscleGroup, + hasCoachModel: exercise.hasCoachModel, + coachMode: exercise.coachMode + ) + } + dismiss() + } label: { + HStack(spacing: 12) { + VStack(spacing: 4) { + Image(Self.muscleGroupIcons[exercise.muscleGroup] ?? "muscle_other") + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundStyle(AppColors.accentGold) + + Image(exercise.equipment.icon) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundStyle(AppColors.accentGold) + } + .frame(width: 28) + + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 6) { + Text(exercise.name) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + if exercise.hasCoachModel { + Text("mAI") + .font(.system(size: 9, weight: .black, design: .rounded)) + .foregroundStyle(.black) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + Text(exercise.equipment.label) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + + Spacer() + + Image(systemName: "plus.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(AppColors.accentGold) + } + .padding(.vertical, 4) + } + .buttonStyle(.plain) + } +} diff --git a/App Core/Resources/Swift Code/ExerciseStatsView.swift b/App Core/Resources/Swift Code/ExerciseStatsView.swift new file mode 100644 index 0000000..4af9550 --- /dev/null +++ b/App Core/Resources/Swift Code/ExerciseStatsView.swift @@ -0,0 +1,358 @@ +// ExerciseStatsView.swift +// mAI Coach — Per-exercise stats with picker, quick stats, charts, and history. + +import SwiftUI + +struct ExerciseStatsView: View { + @EnvironmentObject var store: WorkoutStore + @State private var searchText = "" + @State private var selectedExercise: String? + @State private var selectedMuscleGroup: String? + + /// Only exercises the user has performed (with data). + private var performedExercises: [String] { + WorkoutStatsEngine.allExerciseNames(from: store.workouts) + } + + /// Muscle groups available from performed exercises. + private var availableMuscleGroups: [String] { + let names = performedExercises + var groups = Set() + for name in names { + if let cat = ExerciseCatalog.exercises.first(where: { $0.name == name }) { + groups.insert(cat.muscleGroup) + } + } + return groups.sorted() + } + + /// Filtered list based on search + muscle group. + private var filteredExercises: [String] { + var result = performedExercises + + if let group = selectedMuscleGroup { + result = result.filter { name in + ExerciseCatalog.exercises.first(where: { $0.name == name })?.muscleGroup == group + } + } + + if !searchText.isEmpty { + result = result.filter { $0.localizedCaseInsensitiveContains(searchText) } + } + + return result + } + + var body: some View { + ScrollView { + VStack(spacing: 14) { + // Search + HStack(spacing: 8) { + Image(systemName: "magnifyingglass") + .font(.system(size: 14)) + .foregroundStyle(AppColors.textTertiary) + TextField("Search exercises...", text: $searchText) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + // Muscle group filter + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + filterChip(label: "All", isSelected: selectedMuscleGroup == nil) { + selectedMuscleGroup = nil + } + ForEach(availableMuscleGroups, id: \.self) { group in + filterChip(label: group, isSelected: selectedMuscleGroup == group) { + selectedMuscleGroup = selectedMuscleGroup == group ? nil : group + } + } + } + } + + // Exercise list / detail + if let selected = selectedExercise { + exerciseDetailView(name: selected) + } else { + exerciseListView + } + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("Exercise Stats") + .navigationBarTitleDisplayMode(.large) + } + + // MARK: - Exercise List + private var exerciseListView: some View { + VStack(spacing: 8) { + if filteredExercises.isEmpty { + VStack(spacing: 12) { + Image(systemName: "chart.line.downtrend.xyaxis") + .font(.system(size: 40)) + .foregroundStyle(AppColors.textTertiary) + Text("No exercise data yet") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textSecondary) + } + .padding(.vertical, 40) + } else { + ForEach(filteredExercises, id: \.self) { name in + let history = WorkoutStatsEngine.exerciseHistory(name: name, from: store.workouts) + let pr = WorkoutStatsEngine.currentPRs(from: store.workouts)[name] + + Button { + withAnimation(.spring(response: 0.3)) { + selectedExercise = name + } + } label: { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + HStack(spacing: 8) { + Text("\(history.count) sessions") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + if let pr = pr { + Text("PR: \(Int(pr.weight)) lbs") + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + } + } + } + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(14) + .cardStyle() + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Exercise Detail + private func exerciseDetailView(name: String) -> some View { + let history = WorkoutStatsEngine.exerciseHistory(name: name, from: store.workouts) + let qStats = WorkoutStatsEngine.exerciseQuickStats(name: name, from: store.workouts) + + return VStack(alignment: .leading, spacing: 14) { + // Back button + Button { + withAnimation(.spring(response: 0.3)) { + selectedExercise = nil + } + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.system(size: 12, weight: .semibold)) + Text("All Exercises") + .font(AppFonts.subhead()) + } + .foregroundStyle(AppColors.accentGold) + } + + // Exercise name + Text(name) + .font(AppFonts.title()) + .foregroundStyle(AppColors.textPrimary) + + // Quick stats row + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 8), + GridItem(.flexible(), spacing: 8) + ], spacing: 8) { + miniStatCard( + value: "\(Int(qStats.startingWeight))", + label: "Starting (lbs)", + icon: "flag.fill", + color: .blue + ) + miniStatCard( + value: "\(Int(qStats.currentWeight))", + label: "Current (lbs)", + icon: "arrow.up.right", + color: .green + ) + miniStatCard( + value: qStats.pr != nil ? "\(Int(qStats.pr!.weight))" : "—", + label: "PR (lbs)", + icon: "trophy.fill", + color: .orange + ) + miniStatCard( + value: "\(qStats.totalSessions)", + label: "Sessions", + icon: "calendar", + color: .purple + ) + } + + // Weight change callout + if qStats.totalSessions >= 2 { + let change = qStats.weightChange + let pct = qStats.weightChangePercent + HStack(spacing: 8) { + Image(systemName: change >= 0 ? "arrow.up.right.circle.fill" : "arrow.down.right.circle.fill") + .foregroundStyle(change >= 0 ? .green : .red) + Text(change >= 0 ? "+\(Int(change)) lbs (\(String(format: "%.0f", pct))%)" : "\(Int(change)) lbs (\(String(format: "%.0f", pct))%)") + .font(AppFonts.bodyBold()) + .foregroundStyle(change >= 0 ? .green : .red) + Text("since first session") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + Spacer() + } + .padding(12) + .background((change >= 0 ? Color.green : Color.red).opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + // Weight progression chart + if history.count >= 2 { + weightProgressionChart(history: history) + } + + // Session history + Text("SESSION HISTORY") + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textSecondary) + .padding(.top, 4) + + ForEach(Array(history.reversed().enumerated()), id: \.offset) { i, entry in + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text(entry.date, style: .date) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(entry.totalSets) sets") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + } + Spacer() + VStack(alignment: .trailing, spacing: 2) { + Text("\(Int(entry.bestWeight)) lbs") + .font(.system(size: 17, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + Text("× \(entry.bestReps) reps") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + .padding(14) + .cardStyle() + } + } + .transition(.opacity.combined(with: .move(edge: .trailing))) + } + + // MARK: - Weight Progression Chart + private func weightProgressionChart(history: [ExerciseHistoryEntry]) -> some View { + let maxW = history.map { $0.bestWeight }.max() ?? 1 + let minW = history.map { $0.bestWeight }.min() ?? 0 + let range = max(maxW - minW, 1) + + return VStack(alignment: .leading, spacing: 8) { + Text("WEIGHT PROGRESSION") + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textSecondary) + + // Y-axis labels + chart + HStack(alignment: .top, spacing: 4) { + // Y-axis labels + VStack { + Text("\(Int(maxW))") + .font(.system(size: 9, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + Spacer() + Text("\(Int(minW))") + .font(.system(size: 9, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + } + .frame(width: 30, height: 100) + + // Bars + HStack(alignment: .bottom, spacing: 3) { + ForEach(Array(history.suffix(15).enumerated()), id: \.offset) { i, entry in + let normalised = (entry.bestWeight - minW) / range + let height = max(6, CGFloat(normalised) * 90 + 10) + VStack(spacing: 2) { + RoundedRectangle(cornerRadius: 3) + .fill( + i == history.suffix(15).count - 1 + ? AppColors.accentGold + : AppColors.accentGold.opacity(0.4) + ) + .frame(height: height) + } + .frame(maxWidth: .infinity) + } + } + .frame(height: 100) + } + + // X-axis: first and last dates + if let first = history.first, let last = history.last { + HStack { + Text(first.date, style: .date) + .font(.system(size: 9)) + .foregroundStyle(AppColors.textTertiary) + Spacer() + Text(last.date, style: .date) + .font(.system(size: 9)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(.leading, 34) + } + } + .padding(14) + .cardStyle() + } + + // MARK: - Helpers + + private func filterChip(label: String, isSelected: Bool, action: @escaping () -> Void) -> some View { + Button(action: action) { + Text(label) + .font(AppFonts.footnote()) + .fontWeight(.medium) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(isSelected ? AppColors.accentGold : AppColors.accentMuted) + .foregroundStyle(isSelected ? .black : AppColors.accentGold) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + + private func miniStatCard(value: String, label: String, icon: String, color: Color) -> some View { + VStack(spacing: 4) { + HStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 12)) + .foregroundStyle(color) + Text(value) + .font(.system(size: 20, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + } + Text(label) + .font(.system(size: 11)) + .foregroundStyle(AppColors.textSecondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .cardStyle() + } +} diff --git a/App Core/Resources/Swift Code/HomeView.swift b/App Core/Resources/Swift Code/HomeView.swift index 6869518..dbc7d4f 100644 --- a/App Core/Resources/Swift Code/HomeView.swift +++ b/App Core/Resources/Swift Code/HomeView.swift @@ -1,81 +1,982 @@ +// HomeView.swift +// mAI Coach — Home tab with calendar, day workouts, templates, and stats. + import SwiftUI struct HomeView: View { - @EnvironmentObject var session: AuthSession + @EnvironmentObject var store: WorkoutStore + @EnvironmentObject var tutorial: TutorialManager + @Binding var homeTapCount: Int + @State private var selectedDate = Date() + @State private var displayMonth = Date() + @State private var showAddWorkoutDialog = false + @State private var navigateToTemplates = false + @State private var showTemplatePicker = false + @State private var showWorkoutBuilder = false + + // Swipe action state + @State private var workoutToDelete: Workout? + @State private var showDeleteAlert = false + @State private var workoutToMoveCopy: Workout? + @State private var showMoveCopyChoice = false + @State private var isCopyMode = false // true = copy, false = move + @State private var showDatePicker = false + @State private var pickerMonth = Date() + @State private var pickerSelectedDate = Date() + @State private var showMoveCopyConfirm = false + @State private var showAllStats = false - @State private var showSignIn = false - @State private var showCreate = false + private let calendar = Calendar.current var body: some View { NavigationStack { - content - .navigationTitle("mAI Coach") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - NavigationLink { - SettingsView() + VStack(spacing: 0) { + Text("Home") + .font(AppFonts.displayLarge()) + .foregroundStyle(AppColors.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.bottom, 8) + + ScrollViewReader { proxy in + ScrollView { + VStack(spacing: 0) { + Color.clear.frame(height: 0).id("homeTop") + calendarSection + .id("tutorial_calendar") + dayWorkoutsSection + .id("tutorial_addWorkout") + templatesSection + .id("tutorial_templateShortcuts") + statsSection + Color.clear.frame(height: 20) + } + .padding(.horizontal, AppSpacing.screenPadding) + } + .onChange(of: homeTapCount) { + withAnimation(.easeInOut(duration: 0.3)) { + proxy.scrollTo("homeTop", anchor: .top) + } + } + .onChange(of: tutorial.currentStepIndex) { _, _ in + guard tutorial.isActive, tutorial.currentStep.targetTab == 0 else { return } + let spotID = tutorial.currentStep.spotlightID + withAnimation(.easeInOut(duration: 0.3)) { + switch spotID { + case "tutorial_none", "tutorial_calendar", "tutorial_tabBar": + proxy.scrollTo("homeTop", anchor: .top) + case "tutorial_addWorkout": + proxy.scrollTo("tutorial_addWorkout", anchor: .center) + case "tutorial_templateShortcuts": + proxy.scrollTo("tutorial_templateShortcuts", anchor: .top) + case "tutorial_seeAllTemplates": + proxy.scrollTo("tutorial_templateShortcuts", anchor: .bottom) + default: + break + } + } + } + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationBarHidden(true) + .sheet(isPresented: $showWorkoutBuilder) { + WorkoutBuilderView(forDate: selectedDate) + } + .sheet(isPresented: $showTemplatePicker) { + TemplatePickerForDayView(date: selectedDate) + .environmentObject(store) + } + .alert("Delete Workout?", isPresented: $showDeleteAlert) { + Button("Delete", role: .destructive) { + if let w = workoutToDelete { + store.deleteWorkout(id: w.id) + } + } + Button("Cancel", role: .cancel) { } + } message: { + Text("This will permanently delete \"\(workoutToDelete?.name ?? "")\" and all its data.") + } + .alert("Move or Copy?", isPresented: $showMoveCopyChoice) { + Button("Copy to Another Day") { + isCopyMode = true + pickerMonth = Date() + pickerSelectedDate = Date() + showDatePicker = true + } + Button("Move to Another Day") { + isCopyMode = false + pickerMonth = Date() + pickerSelectedDate = Date() + showDatePicker = true + } + Button("Cancel", role: .cancel) { } + } message: { + Text("What would you like to do with \"\(workoutToMoveCopy?.name ?? "")\"?") + } + .sheet(isPresented: $showDatePicker) { + datePickerSheet + } + .alert(moveCopyConfirmTitle, isPresented: $showMoveCopyConfirm) { + Button(isCopyMode ? "Copy" : "Move") { + if let w = workoutToMoveCopy { + if isCopyMode { + store.copyWorkout(id: w.id, to: pickerSelectedDate) + } else { + store.moveWorkout(id: w.id, to: pickerSelectedDate) + } + } + } + Button("Cancel", role: .cancel) { } + } message: { + Text("\(isCopyMode ? "Copy" : "Move") \"\(workoutToMoveCopy?.name ?? "")\" to \(formattedPickerDate)?") + } + } + } + + // MARK: - Calendar + private var calendarSection: some View { + VStack(spacing: 12) { + // Month header + HStack { + Button { changeMonth(-1) } label: { + Image(systemName: "chevron.left") + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(AppColors.accentGold) + } + Spacer() + Text(monthYearString) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Button { changeMonth(1) } label: { + Image(systemName: "chevron.right") + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(AppColors.accentGold) + } + } + + // Day labels + let dayLabels = ["S", "M", "T", "W", "T", "F", "S"] + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 2) { + ForEach(Array(dayLabels.enumerated()), id: \.offset) { _, label in + Text(label) + .font(AppFonts.caption()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.textTertiary) + .frame(height: 24) + } + } + + // Days grid + let days = makeDays() + let workoutDays = store.daysWithWorkouts( + in: calendar.component(.month, from: displayMonth), + year: calendar.component(.year, from: displayMonth) + ) + let today = calendar.component(.day, from: Date()) + let todayMonth = calendar.component(.month, from: Date()) + let todayYear = calendar.component(.year, from: Date()) + let currentMonth = calendar.component(.month, from: displayMonth) + let currentYear = calendar.component(.year, from: displayMonth) + let selectedDay = calendar.component(.day, from: selectedDate) + let selectedMonth = calendar.component(.month, from: selectedDate) + + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 4) { + ForEach(days, id: \.self) { day in + if day == 0 { + Color.clear.frame(height: 40) + } else { + let isToday = day == today && currentMonth == todayMonth && currentYear == todayYear + let isSelected = day == selectedDay && currentMonth == selectedMonth + let hasWorkout = workoutDays.contains(day) + + Button { + selectDay(day) } label: { - Image(systemName: "gearshape.fill") - .foregroundColor(.primary) + VStack(spacing: 2) { + Text("\(day)") + .font(.system(size: 14, weight: isToday || isSelected ? .bold : .medium, design: .rounded)) + .foregroundStyle( + isToday ? .black : + isSelected ? AppColors.accentGold : + AppColors.textPrimary + ) + .frame(width: 32, height: 32) + .background( + Group { + if isToday { + Circle().fill(AppColors.accentGold) + } else if isSelected { + Circle().stroke(AppColors.accentGold, lineWidth: 1.5) + .background(AppColors.accentMuted.clipShape(Circle())) + } + } + ) + + Circle() + .fill(hasWorkout ? AppColors.accentGold : Color.clear) + .frame(width: 4, height: 4) + } } + .frame(height: 40) } } - // nav to the sign-in/create forms if you're keeping the auth-on-Home pattern - .navigationDestination(isPresented: $showSignIn) { SignInView(mode: .signIn) } - .navigationDestination(isPresented: $showCreate) { SignInView(mode: .create) } + } } + .padding(14) + .cardStyle() + .tutorialSpotlight("tutorial_calendar") + .padding(.top, 12) } - @ViewBuilder - private var content: some View { - switch session.state { - case .signedOut: - // ---- AUTH OPTIONS ON HOME (same as before) ---- - VStack(spacing: 16) { - Image("AppLogo").resizable().scaledToFit().frame(width: 120) + // MARK: - Day Workouts + private var dayWorkoutsSection: some View { + VStack(alignment: .leading, spacing: 12) { + Text(dayTitle) + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.textSecondary) + .padding(.top, 16) - Text("Welcome").font(.title2).padding(.bottom, 8) + let dayWorkouts = store.workouts(on: selectedDate) - Button("Sign in") { showSignIn = true } - .buttonStyle(.borderedProminent) - .frame(maxWidth: .infinity) + if !dayWorkouts.isEmpty { + ForEach(dayWorkouts) { workout in + SwipeableWorkoutRow(workout: workout, homeTapCount: $homeTapCount) { + workoutToDelete = workout + showDeleteAlert = true + } onMoveCopy: { + workoutToMoveCopy = workout + showMoveCopyChoice = true + } + } + } - Button("Continue without signing in") { session.continueAsGuest() } - .buttonStyle(.bordered) - .frame(maxWidth: .infinity) + Button { showAddWorkoutDialog = true } label: { + HStack { + Image(systemName: "plus") + .font(.system(size: 14, weight: .medium)) + Text("Add Workout") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + } + .foregroundStyle(AppColors.accentText) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [6])) + .foregroundStyle(AppColors.accentGold) + ) + .tutorialSpotlight("tutorial_addWorkout") + } + .confirmationDialog("Add Workout", isPresented: $showAddWorkoutDialog) { + Button("From Template") { + showTemplatePicker = true + } + Button("Create New") { + showWorkoutBuilder = true + } + Button("Cancel", role: .cancel) { } + } + } + } + + // MARK: - Templates + private var templatesSection: some View { + VStack(alignment: .leading, spacing: 12) { + Text("My Templates") + .font(AppFonts.title()) + .foregroundStyle(AppColors.textPrimary) + .padding(.top, 24) - Button("Create account") { showCreate = true } + ForEach(Array(store.templates.enumerated()), id: \.element.name) { idx, template in + if let w = store.workouts.last(where: { $0.name == template.name && $0.isTemplate }) { + NavigationLink(destination: WorkoutDetailView(workoutId: w.id)) { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 10) + .fill(AppColors.accentMuted) + .frame(width: 40, height: 40) + .overlay( + Image(systemName: "figure.strengthtraining.traditional") + .font(.system(size: 18)) + .foregroundStyle(AppColors.accentGold) + ) + + VStack(alignment: .leading, spacing: 2) { + Text(template.name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(template.count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .light)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(14) + .cardStyle() + } + .buttonStyle(.plain) + .tutorialSpotlight(idx == 0 ? "tutorial_templateShortcuts" : "") + } + } + + // See All Templates + NavigationLink(destination: AllTemplatesView()) { + HStack { + Text("See All Templates") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .light)) + } + .foregroundStyle(AppColors.accentText) + .padding(.vertical, 10) + .padding(.horizontal, 14) + .cardStyle() + .tutorialSpotlight("tutorial_seeAllTemplates") + } + .buttonStyle(.plain) + } + } + + // MARK: - Stats + private var statsSection: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Workout Stats") + .font(AppFonts.title()) + .foregroundStyle(AppColors.textPrimary) + .padding(.top, 24) + + let stats = WorkoutStatsEngine.quickStats(from: store.workouts) + let prs = WorkoutStatsEngine.currentPRs(from: store.workouts) + + // Layer 1: Always visible — quick stats grid + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10) + ], spacing: 10) { + quickStatCard(value: "\(stats.totalWorkouts)", label: "Workouts", icon: "flame.fill", color: .orange) + quickStatCard(value: "\(stats.totalDaysInGym)", label: "Days in Gym", icon: "calendar", color: .blue) + quickStatCard(value: formatWeight(stats.totalWeightLifted), label: "Volume Lifted", icon: "scalemass.fill", color: .red) + quickStatCard(value: "\(stats.currentStreak)w", label: "Streak", icon: "bolt.fill", color: .yellow) + } + + // "See All Stats" toggle + Button { + withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) { + showAllStats.toggle() + } + } label: { + HStack(spacing: 8) { + Image(systemName: "chart.bar.fill") + .font(.system(size: 14)) + Text(showAllStats ? "Hide Stats" : "See All Stats") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + Spacer() + Image(systemName: showAllStats ? "chevron.up" : "chevron.down") + .font(.system(size: 12, weight: .semibold)) + } + .foregroundStyle(AppColors.accentGold) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + } + + // Layer 2: Expandable stats + if showAllStats { + VStack(alignment: .leading, spacing: 14) { + // Extra stat cards + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10) + ], spacing: 10) { + quickStatCard(value: formatNumber(stats.totalSets), label: "Total Sets", icon: "square.stack.fill", color: .purple) + quickStatCard(value: formatNumber(stats.totalReps), label: "Total Reps", icon: "arrow.triangle.2.circlepath", color: .green) + } + + // Weekly Volume Bar Chart + weeklyVolumeChart + + // Favorite exercise + if let fav = stats.favoriteExercise { + HStack(spacing: 10) { + Image(systemName: "heart.fill") + .font(.system(size: 16)) + .foregroundStyle(.pink) + VStack(alignment: .leading, spacing: 2) { + Text("Favorite Exercise") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + Text(fav) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + } + Spacer() + if let pr = prs[fav] { + VStack(alignment: .trailing, spacing: 2) { + Text("PR") + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + Text("\(Int(pr.weight)) lbs") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + } + } + } + .padding(14) + .cardStyle() + } + + // PR Board (top 5) + if !prs.isEmpty { + Text("PERSONAL RECORDS") + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textSecondary) + .padding(.top, 4) + + ForEach(Array(prs.values.sorted { $0.weight > $1.weight }.prefix(5)), id: \.exerciseName) { pr in + HStack(spacing: 12) { + Image(systemName: "trophy.fill") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + .frame(width: 28) + + VStack(alignment: .leading, spacing: 2) { + Text(pr.exerciseName) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text(pr.date, style: .date) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text("\(Int(pr.weight)) lbs") + .font(.system(size: 17, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + Text("× \(pr.reps) reps") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + .padding(14) + .cardStyle() + } + } + + // Exercise progress (top 5) + let exerciseNames = WorkoutStatsEngine.allExerciseNames(from: store.workouts) + if !exerciseNames.isEmpty { + Text("EXERCISE PROGRESS") + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textSecondary) + .padding(.top, 4) + + ForEach(exerciseNames.prefix(5), id: \.self) { name in + let history = WorkoutStatsEngine.exerciseHistory(name: name, from: store.workouts) + if history.count >= 1 { + exerciseProgressRow(name: name, history: history, pr: prs[name]) + } + } + } + + // "See Stats by Exercise" navigation link + NavigationLink(destination: ExerciseStatsView()) { + HStack(spacing: 8) { + Image(systemName: "list.bullet.rectangle.portrait") + .font(.system(size: 14)) + Text("See Stats by Exercise") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + } + .foregroundStyle(AppColors.accentGold) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + } + } + .transition(.opacity) + } + } + } + + // MARK: - Weekly Volume Chart + private var weeklyVolumeChart: some View { + let data = WorkoutStatsEngine.weeklyVolume(from: store.workouts) + let maxVolume = data.map { $0.volume }.max() ?? 1 + + return VStack(alignment: .leading, spacing: 8) { + Text("WEEKLY VOLUME") + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textSecondary) + + HStack(alignment: .bottom, spacing: 6) { + ForEach(data) { day in + VStack(spacing: 4) { + let height = maxVolume > 0 ? max(4, CGFloat(day.volume / maxVolume) * 100) : CGFloat(4) + RoundedRectangle(cornerRadius: 4) + .fill(day.volume > 0 ? AppColors.accentGold : AppColors.bgTertiary) + .frame(height: height) + + Text(day.dayLabel) + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(AppColors.textTertiary) + } .frame(maxWidth: .infinity) + } + } + .frame(height: 120) + .padding(.top, 4) + // Total for the week + let weekTotal = data.reduce(0) { $0 + $1.volume } + if weekTotal > 0 { + Text("\(formatWeight(weekTotal)) lbs this week") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + .padding(14) + .cardStyle() + } + + // Exercise progress row (sparkline trend) + private func exerciseProgressRow(name: String, history: [ExerciseHistoryEntry], pr: PersonalRecord?) -> some View { + let dateFmt: DateFormatter = { + let f = DateFormatter() + f.dateFormat = "MMM d" + return f + }() + + return VStack(alignment: .leading, spacing: 8) { + HStack { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + if pr != nil { + Image(systemName: "trophy.fill") + .font(.system(size: 12)) + .foregroundStyle(AppColors.accentGold) + } Spacer() + if let last = history.last { + Text("\(Int(last.bestWeight)) lbs") + .font(.system(size: 15, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + } } - .padding() - case .guest, .signedIn: - // ---- YOUR MAIN HOME CONTENT ---- - VStack(spacing: 24) { - Image("AppLogo").resizable().scaledToFit().frame(width: 120) + // Sparkline + if history.count >= 2 { + let points = Array(history.suffix(15)) + let weights = points.map { $0.bestWeight } + let maxW = weights.max() ?? 1 + let minW = weights.min() ?? 0 + let range = max(maxW - minW, 1) - // Big rectangle button that navigates to the new screen - NavigationLink { - CoachView() // <-- the new screen below - } label: { - BigRectButton(title: "Coach me!", systemImage: "figure.strengthtraining.traditional") + GeometryReader { geo in + let w = geo.size.width + let h = geo.size.height + + // Draw line + Path { path in + for (i, weight) in weights.enumerated() { + let x = w * CGFloat(i) / CGFloat(max(weights.count - 1, 1)) + let y = h - (CGFloat((weight - minW) / range) * (h - 8) + 4) + if i == 0 { path.move(to: CGPoint(x: x, y: y)) } + else { path.addLine(to: CGPoint(x: x, y: y)) } + } + } + .stroke(AppColors.accentGold, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) + + // Data point dots + ForEach(weights.indices, id: \.self) { i in + let x = w * CGFloat(i) / CGFloat(max(weights.count - 1, 1)) + let y = h - (CGFloat((weights[i] - minW) / range) * (h - 8) + 4) + Circle() + .fill(i == weights.count - 1 ? AppColors.accentGold : AppColors.accentGold.opacity(0.5)) + .frame(width: i == weights.count - 1 ? 6 : 4, height: i == weights.count - 1 ? 6 : 4) + .position(x: x, y: y) + } + } + .frame(height: 40) + + // Date labels + if let first = points.first, let last = points.last { + HStack { + Text(dateFmt.string(from: first.date)) + .font(.system(size: 9)) + .foregroundStyle(AppColors.textTertiary) + Spacer() + Text(dateFmt.string(from: last.date)) + .font(.system(size: 9)) + .foregroundStyle(AppColors.textTertiary) + } + } + } + + HStack { + Text("\(history.count) sessions") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + Spacer() + if let pr = pr { + Text("PR: \(Int(pr.weight)) lbs × \(pr.reps)") + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + } + } + } + .padding(14) + .cardStyle() + } + + // Stat card helper + private func quickStatCard(value: String, label: String, icon: String, color: Color) -> some View { + VStack(spacing: 6) { + HStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 14)) + .foregroundStyle(color) + Text(value) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + } + Text(label) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .cardStyle() + } + + private func formatNumber(_ n: Int) -> String { + if n >= 1000 { return String(format: "%.1fk", Double(n) / 1000) } + return "\(n)" + } + + private func formatWeight(_ w: Double) -> String { + if w >= 1_000_000 { return String(format: "%.1fM", w / 1_000_000) } + if w >= 1000 { return String(format: "%.1fk", w / 1000) } + return "\(Int(w))" + } + + // MARK: - Helpers + private var monthYearString: String { + let fmt = DateFormatter() + fmt.dateFormat = "MMMM yyyy" + return fmt.string(from: displayMonth) + } + + private var dayTitle: String { + let fmt = DateFormatter() + fmt.dateFormat = "MMMM d" + return fmt.string(from: selectedDate) + } + + private func changeMonth(_ delta: Int) { + if let d = calendar.date(byAdding: .month, value: delta, to: displayMonth) { + displayMonth = d + } + } + + private func selectDay(_ day: Int) { + var comps = calendar.dateComponents([.year, .month], from: displayMonth) + comps.day = day + if let d = calendar.date(from: comps) { + selectedDate = d + } + } + + private func makeDays() -> [Int] { + let comps = calendar.dateComponents([.year, .month], from: displayMonth) + guard let firstOfMonth = calendar.date(from: comps), + let range = calendar.range(of: .day, in: .month, for: firstOfMonth) + else { return [] } + let weekday = calendar.component(.weekday, from: firstOfMonth) // 1=Sun + let blanks = Array(repeating: 0, count: weekday - 1) + return blanks + Array(range) + } + + // MARK: - Move/Copy Helpers + + private var moveCopyConfirmTitle: String { + isCopyMode ? "Copy Workout?" : "Move Workout?" + } + + private var formattedPickerDate: String { + let fmt = DateFormatter() + fmt.dateFormat = "MMMM d, yyyy" + return fmt.string(from: pickerSelectedDate) + } + + private var pickerMonthString: String { + let fmt = DateFormatter() + fmt.dateFormat = "MMMM yyyy" + return fmt.string(from: pickerMonth) + } + + private func changePickerMonth(_ delta: Int) { + if let d = calendar.date(byAdding: .month, value: delta, to: pickerMonth) { + pickerMonth = d + } + } + + private func makPickerDays() -> [Int] { + let comps = calendar.dateComponents([.year, .month], from: pickerMonth) + guard let firstOfMonth = calendar.date(from: comps), + let range = calendar.range(of: .day, in: .month, for: firstOfMonth) + else { return [] } + let weekday = calendar.component(.weekday, from: firstOfMonth) + let blanks = Array(repeating: 0, count: weekday - 1) + return blanks + Array(range) + } + + private func selectPickerDay(_ day: Int) { + var comps = calendar.dateComponents([.year, .month], from: pickerMonth) + comps.day = day + if let d = calendar.date(from: comps) { + pickerSelectedDate = d + } + } + + // MARK: - Date Picker Sheet + private var datePickerSheet: some View { + NavigationStack { + VStack(spacing: 16) { + Text("Select a day to \(isCopyMode ? "copy" : "move") to") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + + // Month navigation + HStack { + Button { changePickerMonth(-1) } label: { + Image(systemName: "chevron.left") + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(AppColors.accentGold) + } + Spacer() + Text(pickerMonthString) + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Button { changePickerMonth(1) } label: { + Image(systemName: "chevron.right") + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(AppColors.accentGold) + } } + .padding(.horizontal, 8) - NavigationLink { - MyWorkoutsView() + // Day headers + let dayHeaders = ["S", "M", "T", "W", "T", "F", "S"] + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 8) { + ForEach(dayHeaders, id: \.self) { h in + Text(h) + .font(.system(size: 11, weight: .semibold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + } + + ForEach(makPickerDays(), id: \.self) { day in + if day == 0 { + Color.clear.frame(height: 36) + } else { + let isSelected = { + var c = calendar.dateComponents([.year, .month], from: pickerMonth) + c.day = day + guard let d = calendar.date(from: c) else { return false } + return calendar.isDate(d, inSameDayAs: pickerSelectedDate) + }() + + Button { + selectPickerDay(day) + } label: { + Text("\(day)") + .font(.system(size: 15, weight: isSelected ? .bold : .regular, design: .rounded)) + .foregroundStyle(isSelected ? .black : AppColors.textPrimary) + .frame(width: 36, height: 36) + .background(isSelected ? AppColors.accentGold : Color.clear) + .clipShape(Circle()) + } + } + } + } + + Spacer() + + // Confirm button + Button { + showDatePicker = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + showMoveCopyConfirm = true + } } label: { - BigRectButton(title: "My Workouts", systemImage: "dumbbell.fill") + Text("Select \(formattedPickerDate)") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) } + } + .padding(AppSpacing.screenPadding) + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle(isCopyMode ? "Copy Workout" : "Move Workout") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { showDatePicker = false } + .foregroundStyle(AppColors.textSecondary) + } + } + } + } +} - // (Optional) sign out while testing - Button("Sign out") { session.signOut() } - .buttonStyle(.bordered) +// MARK: - Stat Card +struct StatCard: View { + let value: String + let label: String + var body: some View { + VStack(spacing: 4) { + Text(value) + .font(.system(size: 32, weight: .heavy, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + Text(label) + .font(AppFonts.caption()) + .foregroundStyle(AppColors.textSecondary) + } + .frame(maxWidth: .infinity) + .padding(16) + .cardStyle() + } +} + +// MARK: - Swipeable Workout Row +struct SwipeableWorkoutRow: View { + let workout: Workout + @Binding var homeTapCount: Int + let onDelete: () -> Void + let onMoveCopy: () -> Void + + @State private var showActions = false + @State private var navigateToDetail = false + + var body: some View { + // Swipeable card row + HStack(spacing: 8) { + // Card content + HStack(spacing: 12) { + // Status icon (left side) — only for active workouts + if workout.isActive { + Image(systemName: "stopwatch.fill") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + .frame(width: 24) + } + + VStack(alignment: .leading, spacing: 2) { + Text(workout.name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(workout.exercises.count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } Spacer() + Image(systemName: "chevron.left") + .font(.system(size: 14, weight: .light)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(14) + .cardStyle() + .contentShape(Rectangle()) + .onTapGesture { + if showActions { + withAnimation(.spring(response: 0.3)) { showActions = false } + } else { + navigateToDetail = true + } + } + + // Action buttons (appear on swipe) + if showActions { + Button { + withAnimation(.spring(response: 0.3)) { showActions = false } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { onMoveCopy() } + } label: { + VStack(spacing: 4) { + Image(systemName: "arrow.right.arrow.left") + .font(.system(size: 16, weight: .semibold)) + Text("Move/\nCopy") + .font(.system(size: 10, weight: .semibold)) + } + .foregroundColor(.white) + .frame(width: 60) + .frame(maxHeight: .infinity) + .background(Color.blue) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + } + + Button { + withAnimation(.spring(response: 0.3)) { showActions = false } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { onDelete() } + } label: { + VStack(spacing: 4) { + Image(systemName: "trash.fill") + .font(.system(size: 16, weight: .semibold)) + Text("Delete") + .font(.system(size: 10, weight: .semibold)) + } + .foregroundColor(.white) + .frame(width: 60) + .frame(maxHeight: .infinity) + .background(Color.red) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + } + .transition(.move(edge: .trailing).combined(with: .opacity)) + } + } + .animation(.spring(response: 0.3), value: showActions) + .navigationDestination(isPresented: $navigateToDetail) { + WorkoutDetailView(workoutId: workout.id) + } + .onChange(of: homeTapCount) { + if navigateToDetail { + navigateToDetail = false } - .padding() } + .gesture( + DragGesture(minimumDistance: 20) + .onEnded { value in + if value.translation.width < -40 { + withAnimation(.spring(response: 0.3)) { showActions = true } + } else if value.translation.width > 40 { + withAnimation(.spring(response: 0.3)) { showActions = false } + } + } + ) } } diff --git a/App Core/Resources/Swift Code/MainTabView.swift b/App Core/Resources/Swift Code/MainTabView.swift new file mode 100644 index 0000000..1230446 --- /dev/null +++ b/App Core/Resources/Swift Code/MainTabView.swift @@ -0,0 +1,109 @@ +// MainTabView.swift +// mAI Coach — Bottom tab navigation (Home, Workout, Profile). +// Uses a custom flat tab bar instead of the system TabView bar, +// because iOS 26 renders all tab bars with "Liquid Glass" rounded style. + +import SwiftUI + +struct MainTabView: View { + @EnvironmentObject var store: WorkoutStore + @EnvironmentObject var tutorial: TutorialManager + @State private var homeTapCount = 0 + + var body: some View { + VStack(spacing: 0) { + // Content area + ZStack { + HomeView(homeTapCount: $homeTapCount) + .opacity(store.selectedTab == 0 ? 1 : 0) + .allowsHitTesting(store.selectedTab == 0) + + WorkoutView() + .opacity(store.selectedTab == 1 ? 1 : 0) + .allowsHitTesting(store.selectedTab == 1) + + NavigationStack { + ProfileView() + } + .opacity(store.selectedTab == 2 ? 1 : 0) + .allowsHitTesting(store.selectedTab == 2) + } + .frame(maxHeight: .infinity) + + // Custom flat tab bar — hidden during coach session + if !store.showingSession { + Divider() + .foregroundStyle(Color(.separator).opacity(0.3)) + + HStack(spacing: 0) { + tabButton(icon: "house.fill", label: "Home", tag: 0) + tabButton(icon: "dumbbell.fill", label: "Workout", tag: 1) + tabButton(icon: "person.fill", label: "Profile", tag: 2) + } + .padding(.top, 8) + .padding(.bottom, 4) + .background(Color(.systemBackground)) + .tutorialSpotlight("tutorial_tabBar") + } + } + .coordinateSpace(name: "tutorialRoot") + .ignoresSafeArea(.keyboard) + .overlay { + TutorialOverlay() + } + .onChange(of: tutorial.currentStepIndex) { _, _ in + guard tutorial.isActive else { return } + let step = tutorial.currentStep + + // Auto-navigate to the correct tab + if let targetTab = step.targetTab { + withAnimation(.easeInOut(duration: 0.25)) { + store.selectedTab = targetTab + } + } + + // Create demo workout when entering workout phase + if step.needsDemoWorkout { + tutorial.createDemoWorkout(store: store) + } + + // Cleanup when leaving workout phase + if !step.needsDemoWorkout && tutorial.demoWorkoutId != nil { + tutorial.cleanupDemoWorkout(store: store) + } + } + .onChange(of: tutorial.isActive) { _, isActive in + // Clean up demo workout when tutorial finishes + if !isActive && tutorial.demoWorkoutId != nil { + tutorial.cleanupDemoWorkout(store: store) + } + } + .onChange(of: store.activeWorkout?.id) { oldId, newId in + // Auto-switch to Workout tab when a workout becomes active + if newId != nil && oldId == nil && !tutorial.isActive { + withAnimation(.easeInOut(duration: 0.25)) { + store.selectedTab = 1 + } + } + } + } + + private func tabButton(icon: String, label: String, tag: Int) -> some View { + Button { + if tag == 0 { + homeTapCount += 1 + } + store.selectedTab = tag + } label: { + VStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 20)) + Text(label) + .font(.system(size: 10)) + } + .foregroundStyle(store.selectedTab == tag ? AppColors.accentGold : Color(.secondaryLabel)) + .frame(maxWidth: .infinity) + } + .buttonStyle(.plain) + } +} diff --git a/App Core/Resources/Swift Code/MyWorkoutsView.swift b/App Core/Resources/Swift Code/MyWorkoutsView.swift deleted file mode 100644 index 0baa7be..0000000 --- a/App Core/Resources/Swift Code/MyWorkoutsView.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SwiftUI - -struct MyWorkoutsView: View { - var body: some View { - VStack(spacing: 20) { - Spacer() - - Image(systemName: "dumbbell.fill") - .font(.system(size: 60)) - .foregroundStyle(.secondary) - - Text("Coming Soon") - .font(.title.weight(.bold)) - - Text("Create custom workouts, generate plans,\nand track your progress over time.") - .font(.subheadline) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) - - Spacer() - } - .navigationTitle("My Workouts") - } -} diff --git a/App Core/Resources/Swift Code/PersonalRecordsView.swift b/App Core/Resources/Swift Code/PersonalRecordsView.swift new file mode 100644 index 0000000..20320ed --- /dev/null +++ b/App Core/Resources/Swift Code/PersonalRecordsView.swift @@ -0,0 +1,251 @@ +// PersonalRecordsView.swift +// mAI Coach — View and manage personal records with override support. + +import SwiftUI + +struct PersonalRecordsView: View { + @EnvironmentObject var store: WorkoutStore + @State private var searchText = "" + @State private var selectedMuscleGroup: String? + @State private var selectedEquipment: EquipmentType? + @State private var editingExercise: String? + @State private var editValue = "" + + // All exercise names that have PR data + private var exercisesWithPRs: [String] { + store.effectivePRs().keys.sorted() + } + + // Filtered list + private var filteredExercises: [String] { + exercisesWithPRs.filter { name in + let matchesSearch = searchText.isEmpty || + name.localizedCaseInsensitiveContains(searchText) + let catalog = ExerciseCatalog.lookup(name) + let matchesMuscle = selectedMuscleGroup == nil || catalog?.muscleGroup == selectedMuscleGroup + let matchesEquip = selectedEquipment == nil || catalog?.equipment == selectedEquipment + return matchesSearch && matchesMuscle && matchesEquip + } + } + + private var effectivePRs: [String: PersonalRecord] { + store.effectivePRs() + } + + var body: some View { + VStack(spacing: 0) { + // Muscle group filter + muscleGroupFilter + .padding(.top, 8) + .padding(.bottom, 4) + + // Equipment filter + equipmentFilter + .padding(.bottom, 8) + + // Exercise list + List { + ForEach(filteredExercises, id: \.self) { name in + prRow(name: name) + } + } + .listStyle(.plain) + } + .searchable(text: $searchText, prompt: "Search exercises") + .navigationTitle("Personal Records") + .navigationBarTitleDisplayMode(.inline) + .background(AppColors.bgPrimary) + } + + // MARK: - PR Row + + private func prRow(name: String) -> some View { + let pr = effectivePRs[name] + let hasOverride = store.prOverrides[name] != nil + + return HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 3) { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + + if let catalog = ExerciseCatalog.lookup(name) { + Text("\(catalog.muscleGroup) · \(catalog.equipment.label)") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + + Spacer() + + if editingExercise == name { + // Editing mode + HStack(spacing: 6) { + TextField("lbs", text: $editValue) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .font(.system(size: 16, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + .frame(width: 70) + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.accentGold, lineWidth: 1.5) + ) + .onChange(of: editValue) { _, newValue in + let filtered = newValue.filter { $0.isNumber || $0 == "." } + if filtered != newValue { editValue = filtered } + } + + Button { + if let weight = Double(editValue), weight > 0 { + store.setPROverride(exerciseName: name, weight: weight) + } + editingExercise = nil + editValue = "" + } label: { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 24)) + .foregroundStyle(AppColors.accentGold) + } + + Button { + editingExercise = nil + editValue = "" + } label: { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 24)) + .foregroundStyle(AppColors.textTertiary) + } + } + } else { + // Display mode + HStack(spacing: 6) { + Image(systemName: "trophy.fill") + .font(.system(size: 14)) + .foregroundStyle(AppColors.accentGold) + + Text(pr != nil ? formatWeight(pr!.weight) : "—") + .font(.system(size: 18, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + + Text("lbs") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + + // Edit button + Button { + editValue = pr != nil ? formatWeight(pr!.weight) : "" + editingExercise = name + } label: { + Image(systemName: "pencil") + .font(.system(size: 14)) + .foregroundStyle(AppColors.textSecondary) + .frame(width: 32, height: 32) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + } + } + .padding(.vertical, 4) + .swipeActions(edge: .trailing) { + if hasOverride { + Button("Reset") { + store.clearPROverride(exerciseName: name) + } + .tint(.orange) + } + } + } + + // MARK: - Muscle Group Filter + + private static let muscleGroupIcons: [String: String] = [ + "Chest": "muscle_chest", + "Back": "muscle_back", + "Shoulders": "muscle_shoulders", + "Arms": "muscle_arms", + "Legs": "muscle_legs", + "Core": "muscle_core", + "Other": "muscle_other", + ] + + private var muscleGroupFilter: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + filterPill(label: "All", icon: "square.grid.2x2", isSystemImage: true, isSelected: selectedMuscleGroup == nil) { + selectedMuscleGroup = nil + } + ForEach(ExerciseCatalog.muscleGroups, id: \.self) { group in + let iconName = Self.muscleGroupIcons[group] ?? "muscle_other" + filterPill(label: group, icon: iconName, isSystemImage: false, isSelected: selectedMuscleGroup == group) { + selectedMuscleGroup = group + } + } + } + .padding(.horizontal, 16) + } + } + + // MARK: - Equipment Filter + + private var equipmentFilter: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + filterPill(label: "All", icon: "square.grid.2x2", isSystemImage: true, isSelected: selectedEquipment == nil) { + selectedEquipment = nil + } + ForEach(EquipmentType.allCases) { type in + filterPill(label: type.label, icon: type.icon, isSystemImage: false, isSelected: selectedEquipment == type) { + selectedEquipment = type + } + } + } + .padding(.horizontal, 16) + } + } + + // MARK: - Filter Pill + + private func filterPill(label: String, icon: String, isSystemImage: Bool, isSelected: Bool, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: 5) { + if isSystemImage { + Image(systemName: icon) + .font(.system(size: 12)) + } else { + Image(icon) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 18, height: 18) + } + Text(label) + .font(AppFonts.footnote()) + .fontWeight(.medium) + } + .foregroundStyle(isSelected ? .black : AppColors.textSecondary) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(isSelected ? AppColors.accentGold : AppColors.bgSecondary) + .clipShape(Capsule()) + .overlay( + Capsule() + .stroke(isSelected ? Color.clear : AppColors.border, lineWidth: 1) + ) + } + } + + // MARK: - Helpers + + private func formatWeight(_ weight: Double) -> String { + if weight.truncatingRemainder(dividingBy: 1) == 0 { + return String(format: "%.0f", weight) + } + return String(format: "%.1f", weight) + } +} diff --git a/App Core/Resources/Swift Code/ProfileView.swift b/App Core/Resources/Swift Code/ProfileView.swift new file mode 100644 index 0000000..ba102a3 --- /dev/null +++ b/App Core/Resources/Swift Code/ProfileView.swift @@ -0,0 +1,447 @@ +// ProfileView.swift +// mAI Coach — Profile tab (account info + settings). + +import SwiftUI + +struct ProfileView: View { + @EnvironmentObject var session: AuthSession + @EnvironmentObject var store: WorkoutStore + @EnvironmentObject var tutorial: TutorialManager + @State private var selectedVoice: CoachVoice = .current + @State private var selectedAppearance: AppAppearance = .current + @AppStorage(SettingsKeys.showDevData) private var showDevData = false + @AppStorage(SettingsKeys.maxTemplateShortcuts) private var maxTemplateShortcuts = 4 + @AppStorage(SettingsKeys.showPRBadges) private var showPRBadges = true + @AppStorage(SettingsKeys.showRPE) private var showRPE = false + @AppStorage("rest_timer_enabled") private var restTimerEnabled = true + @State private var restTimerDuration: Int = { + let v = UserDefaults.standard.integer(forKey: "rest_timer_duration") + return v > 0 ? v : 90 + }() + @State private var showPrivacyPolicy = false + + var body: some View { + VStack(spacing: 0) { + Text("Profile") + .font(AppFonts.displayLarge()) + .foregroundStyle(AppColors.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.bottom, 8) + + ScrollViewReader { profileProxy in + ScrollView { + VStack(spacing: 0) { + Color.clear.frame(height: 0).id("profileScrollTop") + // Profile Card + HStack(spacing: 14) { + Text("TW") + .font(.system(size: 18, weight: .heavy, design: .rounded)) + .foregroundStyle(.black) + .frame(width: 50, height: 50) + .background(AppColors.accentGold) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 2) { + Text(userName) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text(userEmail) + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + } + .padding(16) + .cardStyle() + .padding(.top, 12) + .tutorialSpotlight("tutorial_profileTab") + .id("tutorial_profileTab") + + // Audio Section + settingsSection(title: "Audio") { + VStack(alignment: .leading, spacing: 10) { + Text("Coach Voice") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("Coaching feedback voice during sets.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + Picker("Voice", selection: $selectedVoice) { + Text("Male").tag(CoachVoice.male) + Text("Female").tag(CoachVoice.female) + } + .pickerStyle(.segmented) + .onChange(of: selectedVoice) { + selectedVoice.save() + AudioCoach.shared.reloadVoicePreference() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + AudioCoach.shared.playPositive() + } + } + } + .padding(16) + } + .tutorialSpotlight("tutorial_settings") + .id("tutorial_settings") + + // Appearance Section + settingsSection(title: "Appearance") { + VStack(alignment: .leading, spacing: 10) { + Text("Theme") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("Choose light, dark, or match your device.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + Picker("Appearance", selection: $selectedAppearance) { + ForEach(AppAppearance.allCases) { mode in + Text(mode.displayName).tag(mode) + } + } + .pickerStyle(.segmented) + .onChange(of: selectedAppearance) { + selectedAppearance.save() + } + } + .padding(16) + } + + // Template Shortcuts Section + settingsSection(title: "Home Screen") { + VStack(alignment: .leading, spacing: 10) { + Text("Template Shortcuts") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("How many favorite templates to show on the Home tab.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + Stepper(value: $maxTemplateShortcuts, in: 0...10) { + Text("\(maxTemplateShortcuts) template\(maxTemplateShortcuts == 1 ? "" : "s")") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + } + .onChange(of: maxTemplateShortcuts) { + store.trimFavorites(to: maxTemplateShortcuts) + } + } + .padding(16) + } + + // Workout Section (PR badges + RPE) + settingsSection(title: "Workout") { + VStack(spacing: 0) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("PR Badges") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Text("Show trophy icon on exercises with personal records") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + Toggle("", isOn: $showPRBadges) + .tint(AppColors.accentGold) + .labelsHidden() + } + .padding(16) + + Divider().padding(.leading, 16) + + // Manage PRs link + NavigationLink { + PersonalRecordsView() + .environmentObject(store) + } label: { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Manage Personal Records") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Text("View and override PRs for all exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(16) + } + + Divider().padding(.leading, 16) + + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("RPE Column") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Text("Show Rate of Perceived Exertion (1-10) on each set") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + Toggle("", isOn: $showRPE) + .tint(AppColors.accentGold) + .labelsHidden() + } + .padding(16) + } + } + .tutorialSpotlight("tutorial_prSection") + .id("tutorial_prSection") + + // Rest Timer Section + settingsSection(title: "Rest Timer") { + VStack(spacing: 0) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Auto Rest Timer") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Text("Start a countdown after completing a set") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + Toggle("", isOn: $restTimerEnabled) + .tint(AppColors.accentGold) + .labelsHidden() + } + .padding(16) + + if restTimerEnabled { + Divider().background(AppColors.border) + + HStack { + Text("Default Duration") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Picker("", selection: $restTimerDuration) { + ForEach(Array(stride(from: 30, through: 300, by: 15)), id: \.self) { secs in + Text("\(secs / 60):\(String(format: "%02d", secs % 60))") + .tag(secs) + } + } + .pickerStyle(.menu) + .tint(AppColors.accentGold) + } + .padding(16) + .onChange(of: restTimerDuration) { + UserDefaults.standard.set(restTimerDuration, forKey: "rest_timer_duration") + } + } + } + } footer: { + Text("Timer appears at the bottom of your workout screen.") + } + + // Developer Section + settingsSection(title: "Developer") { + HStack { + Text("Show Dev Data") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Toggle("", isOn: $showDevData) + .tint(AppColors.accentGold) + .labelsHidden() + } + .padding(16) + } footer: { + Text("Shows debug info during live and demo sessions.") + } + + // Tutorial Replay + settingsSection(title: "Tutorial") { + Button { + tutorial.start() + } label: { + HStack { + Image(systemName: "play.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(AppColors.accentGold) + Text("Replay Tutorial") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .light)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(16) + } + } + + // Account Section + settingsSection(title: "Account") { + VStack(spacing: 0) { + Button { showPrivacyPolicy = true } label: { + HStack { + Text("Privacy Policy") + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .light)) + .foregroundStyle(AppColors.textTertiary) + } + .padding(16) + } + + Divider().background(AppColors.border) + + Button { + session.signOut() + } label: { + Text("Sign Out") + .font(AppFonts.body()) + .foregroundStyle(AppColors.error) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(16) + } + } + } + + Color.clear.frame(height: 40) + } + .padding(.horizontal, AppSpacing.screenPadding) + } + .onChange(of: tutorial.currentStepIndex) { _, _ in + guard tutorial.isActive, tutorial.currentStep.targetTab == 2 else { return } + let spotID = tutorial.currentStep.spotlightID + guard spotID != "tutorial_none" else { return } + let scrollTarget: String? + switch spotID { + case "tutorial_profileTab": + scrollTarget = "profileScrollTop" + case "tutorial_prSection": + scrollTarget = "tutorial_prSection" + case "tutorial_settings": + scrollTarget = "tutorial_settings" + default: + scrollTarget = nil + } + if let target = scrollTarget { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + withAnimation(.easeInOut(duration: 0.4)) { + profileProxy.scrollTo(target, anchor: .center) + } + } + } + } + } // ScrollViewReader + } + .background(AppColors.bgSecondary.ignoresSafeArea()) + .sheet(isPresented: $showPrivacyPolicy) { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Group { + Text("Last Updated: February 2026") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + + policySection("Data Collection", + "mAI Coach collects workout data (exercises, sets, reps, weights) that you enter. This data is stored locally on your device and is not transmitted to external servers.") + + policySection("Camera Usage", + "The app uses your device camera for real-time exercise form analysis during coached sessions. Camera data is processed on-device using machine learning models and is never recorded, stored, or transmitted.") + + policySection("Audio", + "The app plays audio coaching cues during workout sessions. No audio is recorded from your device.") + + policySection("Data Storage", + "All workout data is stored locally on your device. Your data is not shared with third parties. You can delete all your data at any time by signing out or uninstalling the app.") + + policySection("Analytics", + "mAI Coach does not collect analytics, usage data, or personally identifiable information.") + + policySection("Changes", + "We may update this privacy policy from time to time. Any changes will be reflected in the app with an updated date.") + + policySection("Contact", + "If you have questions about this privacy policy, please reach out through the app's support channels.") + } + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("Privacy Policy") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Done") { showPrivacyPolicy = false } + .foregroundStyle(AppColors.accentGold) + } + } + } + } + } + + // MARK: - Helpers + private var userName: String { + switch session.state { + case .signedIn: return "Travis Whitney" + case .guest: return "Guest" + case .signedOut: return "Not Signed In" + } + } + + private var userEmail: String { + switch session.state { + case .signedIn(let uid): return uid == "fake-uid" ? "whitnetr@oregonstate.edu" : uid + case .guest: return "Not signed in" + case .signedOut: return "" + } + } + + @ViewBuilder + private func settingsSection( + title: String, + @ViewBuilder content: () -> Content, + footer: (() -> Text)? = nil + ) -> some View { + VStack(alignment: .leading, spacing: 6) { + Text(title.uppercased()) + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.textSecondary) + .padding(.top, 24) + + VStack(spacing: 0) { + content() + } + .background(AppColors.bgPrimary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(AppColors.border, lineWidth: 0.5) + ) + + if let footer { + footer() + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + } + + private func policySection(_ title: String, _ body: String) -> some View { + VStack(alignment: .leading, spacing: 6) { + Text(title) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text(body) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textSecondary) + } + .padding(14) + .frame(maxWidth: .infinity, alignment: .leading) + .cardStyle() + } +} diff --git a/App Core/Resources/Swift Code/RestTimerManager.swift b/App Core/Resources/Swift Code/RestTimerManager.swift new file mode 100644 index 0000000..c516546 --- /dev/null +++ b/App Core/Resources/Swift Code/RestTimerManager.swift @@ -0,0 +1,77 @@ +import Foundation +import SwiftUI +import Combine +import AudioToolbox + +final class RestTimerManager: ObservableObject { + @Published var isRunning = false + @Published var secondsRemaining: Int = 0 + @Published var totalSeconds: Int = 90 + + var defaultDuration: Int { + get { + let v = UserDefaults.standard.integer(forKey: "rest_timer_duration") + return v > 0 ? v : 90 + } + set { UserDefaults.standard.set(newValue, forKey: "rest_timer_duration") } + } + + private var timer: Timer? + + /// Start the rest timer with the default duration. + func start() { + stop() + totalSeconds = defaultDuration + secondsRemaining = defaultDuration + isRunning = true + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + DispatchQueue.main.async { + guard let self else { return } + if self.secondsRemaining > 0 { + self.secondsRemaining -= 1 + } else { + self.stop() + // Simple ding sound — NOT coaching voice + AudioServicesPlaySystemSound(1007) + } + } + } + } + + /// Add 15 seconds to the running timer. + func add15() { + secondsRemaining += 15 + totalSeconds += 15 + } + + /// Subtract 15 seconds from the running timer. + func subtract15() { + secondsRemaining = max(0, secondsRemaining - 15) + totalSeconds = max(15, totalSeconds - 15) + } + + /// Skip / dismiss the timer early. + func stop() { + timer?.invalidate() + timer = nil + isRunning = false + secondsRemaining = 0 + } + + /// Progress from 0 to 1. + var progress: Double { + guard totalSeconds > 0 else { return 0 } + return 1.0 - Double(secondsRemaining) / Double(totalSeconds) + } + + /// Formatted time string "M:SS". + var timeString: String { + let m = secondsRemaining / 60 + let s = secondsRemaining % 60 + return String(format: "%d:%02d", m, s) + } + + deinit { + timer?.invalidate() + } +} diff --git a/App Core/Resources/Swift Code/RootView.swift b/App Core/Resources/Swift Code/RootView.swift index aff0c54..9c91ad6 100644 --- a/App Core/Resources/Swift Code/RootView.swift +++ b/App Core/Resources/Swift Code/RootView.swift @@ -1,28 +1,57 @@ // RootView.swift +// mAI Coach — App root: boot screen → welcome or main tab view. + import SwiftUI struct RootView: View { - @State private var showBoot = true // state that controls which screen is visible + @EnvironmentObject var session: AuthSession + @EnvironmentObject var workoutStore: WorkoutStore + @EnvironmentObject var tutorial: TutorialManager + @State private var showBoot = true + @AppStorage(SettingsKeys.appearance) private var appearanceRaw = "light" + @AppStorage("admin_demo_loaded") private var adminDemoLoaded = false + @AppStorage("tutorial_completed") private var tutorialCompleted = false + + private var colorScheme: ColorScheme? { + (AppAppearance(rawValue: appearanceRaw) ?? .light).colorScheme + } var body: some View { ZStack { - // Main app underneath - HomeView() - .opacity(showBoot ? 0 : 1) // hidden until boot finishes + // Main content underneath + Group { + switch session.state { + case .signedOut: + WelcomeView() + case .guest, .signedIn: + MainTabView() + } + } + .opacity(showBoot ? 0 : 1) // Boot screen on top at first if showBoot { BootScreen() - .transition(.opacity) // when it disappears, fade out + .transition(.opacity) .task { - // keep boot up briefly, then flip the switch - try? await Task.sleep(nanoseconds: 1_200_000_000) // ~1.2s - withAnimation(.easeInOut(duration: 0.3)) { + try? await Task.sleep(for: .seconds(2)) + withAnimation(.easeOut(duration: 0.5)) { showBoot = false } + // Auto-start tutorial for first-time users + if !tutorialCompleted { + try? await Task.sleep(for: .milliseconds(800)) + tutorial.start() + } } } } + .preferredColorScheme(colorScheme) + .onChange(of: session.isAdmin) { _, isAdmin in + if isAdmin && !adminDemoLoaded { + workoutStore.loadAdminDemoData() + adminDemoLoaded = true + } + } } } - diff --git a/App Core/Resources/Swift Code/SettingsView.swift b/App Core/Resources/Swift Code/SettingsView.swift index 119b04c..5d5aa8d 100644 --- a/App Core/Resources/Swift Code/SettingsView.swift +++ b/App Core/Resources/Swift Code/SettingsView.swift @@ -4,6 +4,10 @@ import SwiftUI enum SettingsKeys { static let coachVoice = "coach_voice_gender" static let showDevData = "show_dev_data" + static let appearance = "app_appearance" + static let maxTemplateShortcuts = "max_template_shortcuts" + static let showPRBadges = "show_pr_badges" + static let showRPE = "show_rpe_column" } // MARK: - Voice Option @@ -39,10 +43,45 @@ enum CoachVoice: String, CaseIterable, Identifiable { } } +// MARK: - Appearance Option +enum AppAppearance: String, CaseIterable, Identifiable { + case light = "light" + case dark = "dark" + case system = "system" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .light: return "Light" + case .dark: return "Dark" + case .system: return "System" + } + } + + var colorScheme: ColorScheme? { + switch self { + case .light: return .light + case .dark: return .dark + case .system: return nil + } + } + + static var current: AppAppearance { + let raw = UserDefaults.standard.string(forKey: SettingsKeys.appearance) ?? "light" + return AppAppearance(rawValue: raw) ?? .light + } + + func save() { + UserDefaults.standard.set(rawValue, forKey: SettingsKeys.appearance) + } +} + // MARK: - Settings View struct SettingsView: View { @State private var selectedVoice: CoachVoice = .current @AppStorage(SettingsKeys.showDevData) private var showDevData = false + @AppStorage(SettingsKeys.showRPE) private var showRPE = false var body: some View { Form { @@ -74,6 +113,14 @@ struct SettingsView: View { Label("Audio", systemImage: "speaker.wave.2.fill") } + Section { + Toggle("Show RPE Column", isOn: $showRPE) + } header: { + Label("Workout Tracking", systemImage: "figure.strengthtraining.traditional") + } footer: { + Text("Adds a Rate of Perceived Exertion column (1-10) to each set.") + } + Section { Toggle("Show Dev Data", isOn: $showDevData) } header: { diff --git a/App Core/Resources/Swift Code/SignInView.swift b/App Core/Resources/Swift Code/SignInView.swift index df2004a..c9740f7 100644 --- a/App Core/Resources/Swift Code/SignInView.swift +++ b/App Core/Resources/Swift Code/SignInView.swift @@ -1,4 +1,6 @@ // SignInView.swift +// mAI Coach — Sign in / create account form. + import SwiftUI struct SignInView: View { @@ -13,22 +15,66 @@ struct SignInView: View { @State private var error: String? var body: some View { - Form { - Section { - TextField("Email", text: $email) - .textContentType(.emailAddress) - .keyboardType(.emailAddress) - .autocapitalization(.none) - SecureField("Password", text: $password) - .textContentType(.password) + VStack(spacing: 0) { + // Form fields + VStack(spacing: 0) { + VStack(spacing: 0) { + TextField("Email", text: $email) + .textContentType(.emailAddress) + .keyboardType(.emailAddress) + .autocapitalization(.none) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + .padding(14) + + Divider().background(AppColors.border) + + SecureField("Password", text: $password) + .textContentType(.password) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + .padding(14) + } + .background(AppColors.bgPrimary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(AppColors.border, lineWidth: 0.5) + ) } - if let error { Text(error).foregroundStyle(.red) } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 20) + + if let error { + Text(error) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.error) + .padding(.top, 8) + } + + // Submit Button(action: submit) { - if working { ProgressView() } - else { Text(mode == .signIn ? "Sign in" : "Create account") } + Group { + if working { + ProgressView().tint(.black) + } else { + Text(mode == .signIn ? "Sign in" : "Create account") + } + } + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) } .disabled(working) + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 20) + + Spacer() } + .background(AppColors.bgSecondary.ignoresSafeArea()) .navigationTitle(mode == .signIn ? "Sign in" : "Create account") } @@ -40,7 +86,7 @@ struct SignInView: View { case .signIn: try await session.signIn(email: email, password: password) case .create: try await session.createAccount(email: email, password: password) } - dismiss() // HomeView will auto-switch to main content + dismiss() } catch { self.error = error.localizedDescription } working = false } diff --git a/App Core/Resources/Swift Code/StatsView.swift b/App Core/Resources/Swift Code/StatsView.swift new file mode 100644 index 0000000..4f829c8 --- /dev/null +++ b/App Core/Resources/Swift Code/StatsView.swift @@ -0,0 +1,246 @@ +// StatsView.swift +// mAI Coach — Quick stats dashboard + exercise history + PR board. + +import SwiftUI + +struct StatsView: View { + @EnvironmentObject var store: WorkoutStore + @AppStorage(SettingsKeys.showPRBadges) private var showPRBadges = true + + private var stats: QuickStats { + WorkoutStatsEngine.quickStats(from: store.workouts) + } + + private var prs: [String: PersonalRecord] { + WorkoutStatsEngine.currentPRs(from: store.workouts) + } + + private var exerciseNames: [String] { + WorkoutStatsEngine.allExerciseNames(from: store.workouts) + } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + // MARK: Quick Stats Grid + quickStatsSection + + // MARK: PR Board + if !prs.isEmpty { + prBoardSection + } + + // MARK: Exercise History + if !exerciseNames.isEmpty { + exerciseHistorySection + } + + Color.clear.frame(height: 30) + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + .background(AppColors.bgSecondary.ignoresSafeArea()) + .navigationTitle("Stats") + .navigationBarTitleDisplayMode(.large) + } + + // MARK: - Quick Stats Section + private var quickStatsSection: some View { + VStack(alignment: .leading, spacing: 10) { + sectionLabel("OVERVIEW") + + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: 10), + GridItem(.flexible(), spacing: 10) + ], spacing: 10) { + statCard(value: "\(stats.totalWorkouts)", label: "Workouts", icon: "flame.fill", color: .orange) + statCard(value: "\(stats.totalDaysInGym)", label: "Days in Gym", icon: "calendar", color: .blue) + statCard(value: formatNumber(stats.totalSets), label: "Total Sets", icon: "square.stack.fill", color: .purple) + statCard(value: formatNumber(stats.totalReps), label: "Total Reps", icon: "arrow.triangle.2.circlepath", color: .green) + statCard(value: formatWeight(stats.totalWeightLifted), label: "Volume Lifted", icon: "scalemass.fill", color: .red) + statCard(value: "\(stats.currentStreak)w", label: "Streak", icon: "bolt.fill", color: .yellow) + } + + // Favorite exercise callout + if let fav = stats.favoriteExercise { + HStack(spacing: 10) { + Image(systemName: "heart.fill") + .font(.system(size: 16)) + .foregroundStyle(.pink) + VStack(alignment: .leading, spacing: 2) { + Text("Favorite Exercise") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + Text(fav) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + } + Spacer() + if let pr = prs[fav] { + VStack(alignment: .trailing, spacing: 2) { + Text("PR") + .font(.system(size: 10, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + Text("\(Int(pr.weight)) lbs") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + } + } + } + .padding(14) + .cardStyle() + } + } + } + + // MARK: - PR Board Section + private var prBoardSection: some View { + VStack(alignment: .leading, spacing: 10) { + sectionLabel("PERSONAL RECORDS") + + ForEach(Array(prs.values.sorted { $0.weight > $1.weight }.prefix(10)), id: \.exerciseName) { pr in + HStack(spacing: 12) { + // Medal icon + Image(systemName: "trophy.fill") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text(pr.exerciseName) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text(pr.date, style: .date) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text("\(Int(pr.weight)) lbs") + .font(.system(size: 17, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + Text("× \(pr.reps) reps") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + .padding(14) + .cardStyle() + } + } + } + + // MARK: - Exercise History Section + private var exerciseHistorySection: some View { + VStack(alignment: .leading, spacing: 10) { + sectionLabel("EXERCISE PROGRESS") + + ForEach(exerciseNames.prefix(8), id: \.self) { name in + let history = WorkoutStatsEngine.exerciseHistory(name: name, from: store.workouts) + if history.count >= 1 { + exerciseProgressRow(name: name, history: history, pr: prs[name]) + } + } + } + } + + // MARK: - Component Views + + private func statCard(value: String, label: String, icon: String, color: Color) -> some View { + VStack(spacing: 6) { + HStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 14)) + .foregroundStyle(color) + Text(value) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + } + Text(label) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .cardStyle() + } + + private func exerciseProgressRow(name: String, history: [ExerciseHistoryEntry], pr: PersonalRecord?) -> some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + if pr != nil { + Image(systemName: "trophy.fill") + .font(.system(size: 12)) + .foregroundStyle(AppColors.accentGold) + } + Spacer() + if let last = history.last { + Text("\(Int(last.bestWeight)) lbs") + .font(.system(size: 15, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.accentGold) + } + } + + // Mini progress bar showing weight trend + if history.count >= 2 { + let maxW = history.map { $0.bestWeight }.max() ?? 1 + HStack(spacing: 2) { + ForEach(history.suffix(10).indices, id: \.self) { i in + let entry = history[i] + let height = max(4, CGFloat(entry.bestWeight / maxW) * 28) + RoundedRectangle(cornerRadius: 2) + .fill(i == history.count - 1 ? AppColors.accentGold : AppColors.accentMuted) + .frame(height: height) + } + } + .frame(height: 28) + } + + HStack { + Text("\(history.count) sessions") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + Spacer() + if let pr = pr { + Text("PR: \(Int(pr.weight)) lbs × \(pr.reps)") + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + } + } + } + .padding(14) + .cardStyle() + } + + // MARK: - Helpers + + private func sectionLabel(_ text: String) -> some View { + Text(text) + .font(.system(size: 12, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textSecondary) + .padding(.top, 4) + } + + private func formatNumber(_ n: Int) -> String { + if n >= 1000 { + return String(format: "%.1fk", Double(n) / 1000) + } + return "\(n)" + } + + private func formatWeight(_ w: Double) -> String { + if w >= 1_000_000 { + return String(format: "%.1fM", w / 1_000_000) + } else if w >= 1000 { + return String(format: "%.1fk", w / 1000) + } + return "\(Int(w))" + } +} diff --git a/App Core/Resources/Swift Code/SupersetBlockView.swift b/App Core/Resources/Swift Code/SupersetBlockView.swift new file mode 100644 index 0000000..8afe8e7 --- /dev/null +++ b/App Core/Resources/Swift Code/SupersetBlockView.swift @@ -0,0 +1,165 @@ +// SupersetBlockView.swift +// mAI Coach — Superset group block within a workout. +// Displays multiple exercises inside a visually grouped card with a comment field. + +import SwiftUI + +struct SupersetBlockView: View { + @Binding var group: SupersetGroup + @EnvironmentObject var store: WorkoutStore + @EnvironmentObject var restTimer: RestTimerManager + + var onCoachTap: ((BenchSessionView.Mode) -> Void)? + var onAutoStart: (() -> Void)? = nil + var isEditing: Bool = false + var workoutId: UUID? = nil + + var body: some View { + VStack(spacing: 0) { + // Superset header with colored bar + HStack(spacing: 8) { + RoundedRectangle(cornerRadius: 2) + .fill(AppColors.accentGold) + .frame(width: 4) + .frame(height: 28) + + if isEditing { + TextField("Superset label...", text: $group.comment) + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textPrimary) + } else { + Text(group.comment.isEmpty ? "Superset" : group.comment) + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + } + + Spacer() + + Text("\(group.exercises.count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + } + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(AppColors.accentGold.opacity(0.08)) + + // Notes / instructions block + if isEditing || !group.notes.isEmpty { + TextField("Instructions or notes...", text: $group.notes, axis: .vertical) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + .lineLimit(1...5) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(AppColors.bgPrimary.opacity(0.5)) + } + + // Exercise cards inside the superset + VStack(spacing: 8) { + ForEach(Array(group.exercises.enumerated()), id: \.element.id) { i, exercise in + VStack(spacing: 0) { + // Reorder controls inside superset + if isEditing { + HStack(spacing: 10) { + Button { + guard i > 0 else { return } + withAnimation(.easeInOut(duration: 0.3)) { + group.exercises.swapAt(i, i - 1) + } + } label: { + Image(systemName: "chevron.up") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(i > 0 ? AppColors.textPrimary : AppColors.textTertiary.opacity(0.3)) + .frame(width: 28, height: 20) + } + .disabled(i == 0) + + Text("Move") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(AppColors.textTertiary) + + Button { + guard i < group.exercises.count - 1 else { return } + withAnimation(.easeInOut(duration: 0.3)) { + group.exercises.swapAt(i, i + 1) + } + } label: { + Image(systemName: "chevron.down") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(i < group.exercises.count - 1 ? AppColors.textPrimary : AppColors.textTertiary.opacity(0.3)) + .frame(width: 28, height: 20) + } + .disabled(i == group.exercises.count - 1) + } + .frame(height: 22) + } + + ExerciseCardView( + exercise: $group.exercises[i], + onCoachTap: onCoachTap, + onAutoStart: onAutoStart, + isEditing: isEditing, + workoutId: workoutId + ) + } + .overlay(alignment: .topTrailing) { + if isEditing { + Button { + withAnimation { + let _ = group.exercises.remove(at: i) + } + } label: { + Image(systemName: "minus.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(.red) + .background(Circle().fill(AppColors.bgPrimary)) + } + .offset(x: 6, y: isEditing ? 12 : -6) + } + } + } + + // Add exercise inside superset + if isEditing { + addExerciseInsideButton + } + } + .padding(.horizontal, 8) + .padding(.vertical, 8) + } + .background(AppColors.bgSecondary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(lineWidth: 1.5) + .foregroundStyle(AppColors.accentGold) + .opacity(0.3) + ) + } + + // MARK: - Add exercise inside superset (shows picker) + @State private var showExercisePicker = false + + private var addExerciseInsideButton: some View { + Button { + showExercisePicker = true + } label: { + Text("+ Add Exercise") + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .sheet(isPresented: $showExercisePicker) { + ExercisePickerView { exercise in + withAnimation { + group.exercises.append(exercise) + } + } + } + } +} diff --git a/App Core/Resources/Swift Code/TemplateDetailView.swift b/App Core/Resources/Swift Code/TemplateDetailView.swift new file mode 100644 index 0000000..3f02f33 --- /dev/null +++ b/App Core/Resources/Swift Code/TemplateDetailView.swift @@ -0,0 +1,196 @@ +// TemplateDetailView.swift +// mAI Coach — View and edit a workout template's exercises. + +import SwiftUI + +struct TemplateDetailView: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + let templateName: String + @State private var exercises: [Exercise] = [] + @State private var isEditing = false + @State private var showExercisePicker = false + /// Tags auto-computed from exercises' muscle groups. + private var computedTags: [String] { + let groups = exercises.compactMap { ex -> String? in + if !ex.muscleGroup.isEmpty { return ex.muscleGroup } + return ExerciseCatalog.exercises.first(where: { $0.name == ex.name })?.muscleGroup + } + return Array(Set(groups)).sorted() + } + + var body: some View { + ScrollView { + VStack(spacing: 12) { + // Tags display + if !computedTags.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(computedTags, id: \.self) { tag in + Text(tag) + .font(AppFonts.footnote()) + .fontWeight(.medium) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(AppColors.accentMuted) + .foregroundStyle(AppColors.accentGold) + .clipShape(Capsule()) + } + } + } + } + + // Exercise cards — same style as active workout + ForEach(exercises.indices, id: \.self) { i in + ExerciseCardView( + exercise: $exercises[i], + onCoachTap: nil + ) + } + + if isEditing { + // Add Exercise button (red when editing) + Button { + showExercisePicker = true + } label: { + Text("+ Add Exercise") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(.red) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [8])) + .foregroundStyle(.red.opacity(0.4)) + ) + } + } + + Color.clear.frame(height: 20) + } + .padding(.horizontal, AppSpacing.screenPadding) + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle(templateName) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + if isEditing { + // Save changes + store.updateTemplate(name: templateName, exercises: exercises) + } + isEditing.toggle() + } label: { + Text(isEditing ? "Done" : "Edit") + .fontWeight(isEditing ? .bold : .regular) + .foregroundStyle(isEditing ? .red : AppColors.accentGold) + } + } + } + .sheet(isPresented: $showExercisePicker) { + ExercisePickerView { exercise in + exercises.append(exercise) + } + } + .onAppear { + if let w = store.templateWorkout(named: templateName) { + exercises = w.exercises + } + } + } +} + +// MARK: - Template Creator View + +struct TemplateCreatorView: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + @State private var templateName = "" + @State private var exercises: [Exercise] = [] + @State private var showExercisePicker = false + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 16) { + // Name field + VStack(alignment: .leading, spacing: 6) { + Text("TEMPLATE NAME") + .font(.system(size: 11, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + TextField("e.g. Upper Body", text: $templateName) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + .padding(14) + .background(AppColors.bgSecondary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(AppColors.border, lineWidth: 1) + ) + } + + // Exercise cards + ForEach(exercises.indices, id: \.self) { i in + ExerciseCardView( + exercise: $exercises[i], + onCoachTap: nil + ) + } + + // Add Exercise + Button { + showExercisePicker = true + } label: { + Text("+ Add Exercise") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentText) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [8])) + .foregroundStyle(AppColors.accentGold) + ) + } + + Color.clear.frame(height: 20) + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("New Template") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + .foregroundStyle(AppColors.textSecondary) + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + guard !templateName.trimmingCharacters(in: .whitespaces).isEmpty else { return } + store.createTemplate(name: templateName, exercises: exercises) + dismiss() + } + .fontWeight(.bold) + .foregroundStyle(AppColors.accentGold) + .disabled(templateName.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + .sheet(isPresented: $showExercisePicker) { + ExercisePickerView { exercise in + exercises.append(exercise) + } + } + } + } +} diff --git a/App Core/Resources/Swift Code/TemplatePickerForDayView.swift b/App Core/Resources/Swift Code/TemplatePickerForDayView.swift new file mode 100644 index 0000000..19342c3 --- /dev/null +++ b/App Core/Resources/Swift Code/TemplatePickerForDayView.swift @@ -0,0 +1,187 @@ +// TemplatePickerForDayView.swift +// mAI Coach — Pick a template to add as a workout on a specific calendar day. + +import SwiftUI + +struct TemplatePickerForDayView: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + let date: Date + + @State private var previewTemplate: Workout? + + private var templateNames: [String] { + store.allTemplateNames + } + + private var dateString: String { + let fmt = DateFormatter() + fmt.dateFormat = "MMMM d" + return fmt.string(from: date) + } + + var body: some View { + NavigationStack { + Group { + if templateNames.isEmpty { + VStack(spacing: 12) { + Spacer() + Image(systemName: "doc.text") + .font(.system(size: 40)) + .foregroundStyle(AppColors.textTertiary) + Text("No Templates Yet") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + Text("Create templates from the Templates section first.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + } else { + List { + ForEach(templateNames, id: \.self) { name in + let tpl = store.templateWorkout(named: name) + Button { + store.addWorkoutFromTemplate(templateName: name, date: date) + dismiss() + } label: { + HStack(spacing: 12) { + Image(systemName: "doc.text.fill") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + .frame(width: 28) + + VStack(alignment: .leading, spacing: 2) { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(tpl?.exercises.count ?? 0) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + + Spacer() + + // Preview button + Button { + previewTemplate = tpl + } label: { + Image(systemName: "eye") + .font(.system(size: 14)) + .foregroundStyle(AppColors.accentGold) + .padding(8) + .background(AppColors.accentMuted) + .clipShape(Circle()) + } + .buttonStyle(.plain) + + // Select indicator + Image(systemName: "plus.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(AppColors.accentGold) + } + .padding(.vertical, 4) + } + .buttonStyle(.plain) + .listRowBackground(AppColors.bgSecondary) + } + } + .listStyle(.insetGrouped) + .scrollContentBackground(.hidden) + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("Add to \(dateString)") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + .foregroundStyle(AppColors.textSecondary) + } + } + .sheet(item: $previewTemplate) { tpl in + templatePreviewSheet(tpl) + } + } + } + + // MARK: - Template Preview Sheet + private func templatePreviewSheet(_ template: Workout) -> some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + // Tags + let tags = Array(Set(template.exercises.map { $0.muscleGroup }.filter { !$0.isEmpty })) + if !tags.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(tags, id: \.self) { tag in + Text(tag) + .font(.system(size: 11, weight: .semibold, design: .rounded)) + .foregroundStyle(.black) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(AppColors.accentGold) + .clipShape(Capsule()) + } + } + } + } + + // Exercise list + ForEach(template.exercises) { exercise in + HStack(spacing: 12) { + Image(exercise.equipment.icon) + .resizable() + .renderingMode(.template) + .scaledToFit() + .frame(width: 18, height: 18) + .foregroundStyle(AppColors.accentGold) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 2) { + Text(exercise.name) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Text("\(exercise.sets.count) sets • \(exercise.equipment.label)") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + } + .padding(12) + .background(AppColors.bgSecondary) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(AppColors.border, lineWidth: 1) + ) + } + } + .padding(AppSpacing.screenPadding) + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle(template.name) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Select") { + store.addWorkoutFromTemplate(templateName: template.name, date: date) + previewTemplate = nil + dismiss() + } + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentGold) + } + ToolbarItem(placement: .cancellationAction) { + Button("Back") { + previewTemplate = nil + } + .foregroundStyle(AppColors.textSecondary) + } + } + } + } +} diff --git a/App Core/Resources/Swift Code/TutorialManager.swift b/App Core/Resources/Swift Code/TutorialManager.swift new file mode 100644 index 0000000..73a1293 --- /dev/null +++ b/App Core/Resources/Swift Code/TutorialManager.swift @@ -0,0 +1,256 @@ +// TutorialManager.swift +// mAI Coach — Onboarding tutorial state manager. +// Manages the interactive tutorial flow across 4 phases. + +import SwiftUI +import Combine + +// MARK: - Tutorial Step Definition + +enum TutorialPhase: Int, CaseIterable { + case home = 0 + case workout = 1 + case features = 2 + case profile = 3 +} + +struct TutorialStep { + let id: Int + let phase: TutorialPhase + let spotlightID: String // matches a .tutorialSpotlight() key + let title: String + let message: String + let targetTab: Int? // which tab should be selected (0=Home, 1=Workout, 2=Profile) + let needsDemoWorkout: Bool // whether this step requires the demo workout to exist +} + +// MARK: - All Steps + +let tutorialSteps: [TutorialStep] = [ + + // ── Phase 1: Home (steps 0–5) ────────────────────────── + TutorialStep(id: 0, phase: .home, spotlightID: "tutorial_none", + title: "Welcome to mAI Coach! 🎉", + message: "Your AI-powered workout companion.\nLet's take a quick tour of what you can do.", + targetTab: 0, needsDemoWorkout: false), + + TutorialStep(id: 1, phase: .home, spotlightID: "tutorial_calendar", + title: "Your Calendar", + message: "See your workout history at a glance. Days with workouts get a gold dot.", + targetTab: 0, needsDemoWorkout: false), + + TutorialStep(id: 2, phase: .home, spotlightID: "tutorial_addWorkout", + title: "Add a Workout", + message: "Tap here to log a workout. Choose from a saved template or build one from scratch.", + targetTab: 0, needsDemoWorkout: false), + + TutorialStep(id: 3, phase: .home, spotlightID: "tutorial_templateShortcuts", + title: "Quick Templates", + message: "Your favorite templates live right here for one-tap access. You can choose how many show up in Settings.", + targetTab: 0, needsDemoWorkout: false), + + TutorialStep(id: 4, phase: .home, spotlightID: "tutorial_seeAllTemplates", + title: "All Templates", + message: "Manage everything here: favorite, archive, create new templates, and reorder.", + targetTab: 0, needsDemoWorkout: false), + + TutorialStep(id: 5, phase: .home, spotlightID: "tutorial_none", + title: "Navigation", + message: "Home for your calendar, Workout for your active session, Profile for stats and settings.\n\nLet's build a workout!", + targetTab: 0, needsDemoWorkout: false), + + // ── Phase 2: Live Workout (steps 6–14) ───────────────── + // Top-to-bottom flow through WorkoutDetailView: + + TutorialStep(id: 6, phase: .workout, spotlightID: "tutorial_none", + title: "Your Active Workout", + message: "Here's what a workout looks like! We've created a sample one for you. Each exercise has its own card.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 7, phase: .workout, spotlightID: "tutorial_tags", + title: "Auto Tags", + message: "These tag pills are computed automatically from your exercises' muscle groups. They update live as you add or remove exercises.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 8, phase: .workout, spotlightID: "tutorial_exerciseCard", + title: "Exercise Card", + message: "Each exercise shows your sets in a grid — weight, reps, and a checkmark. Fill them in as you go.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 9, phase: .workout, spotlightID: "tutorial_exerciseCard", + title: "Complete a Set ✓", + message: "Tap the checkmark when you finish a set. A rest timer starts automatically. Beat your personal record and you'll see confetti! 🎉", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 10, phase: .workout, spotlightID: "tutorial_supersetBlock", + title: "Supersets", + message: "Exercises grouped in a gold block are a superset. They share a label and notes for coaching cues.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 11, phase: .workout, spotlightID: "tutorial_addExercise", + title: "Add More Exercises", + message: "Browse our catalog of 100+ exercises organized by muscle group. Search or scroll to find what you need.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 12, phase: .workout, spotlightID: "tutorial_addSuperset", + title: "Add a Superset", + message: "Group exercises together into a superset block. Great for arm finishers or circuit training.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 13, phase: .workout, spotlightID: "tutorial_workoutNotes", + title: "Workout Notes", + message: "Add notes for the whole session — how you felt, what to change next time.", + targetTab: 1, needsDemoWorkout: true), + + TutorialStep(id: 14, phase: .workout, spotlightID: "tutorial_actionButton", + title: "Finish Your Workout", + message: "When you're done, tap Finish. Your workout is saved to your history and appears on the calendar.", + targetTab: 1, needsDemoWorkout: true), + + // ── Phase 3: Profile & Settings (steps 15–18) ────────── + + TutorialStep(id: 15, phase: .profile, spotlightID: "tutorial_profileTab", + title: "Your Profile", + message: "Stats, personal records, settings, and account info all live here.", + targetTab: 2, needsDemoWorkout: false), + + TutorialStep(id: 16, phase: .profile, spotlightID: "tutorial_prSection", + title: "Personal Records 🏆", + message: "Your all-time best lifts are tracked automatically. Trophy badges appear on exercise cards when you beat a PR.", + targetTab: 2, needsDemoWorkout: false), + + TutorialStep(id: 17, phase: .profile, spotlightID: "tutorial_settings", + title: "Settings", + message: "Customize your experience: coach voice, appearance, template shortcuts count, PR badges, rest timer, and more.", + targetTab: 2, needsDemoWorkout: false), + + TutorialStep(id: 18, phase: .profile, spotlightID: "tutorial_none", + title: "You're All Set! 💪", + message: "Hit the gym and let mAI Coach track your progress. You can replay this tutorial anytime from Settings.", + targetTab: 2, needsDemoWorkout: false), +] + +// MARK: - Tutorial Manager + +@MainActor +class TutorialManager: ObservableObject { + @Published var isActive = false + @Published var currentStepIndex = 0 + @Published var isEditingForTutorial = false + + /// ID of the demo workout created for the tutorial + var demoWorkoutId: UUID? + + var currentStep: TutorialStep { + guard currentStepIndex < tutorialSteps.count else { + return tutorialSteps.last! + } + return tutorialSteps[currentStepIndex] + } + + var isLastStep: Bool { currentStepIndex == tutorialSteps.count - 1 } + + var progress: Double { + Double(currentStepIndex) / Double(max(tutorialSteps.count - 1, 1)) + } + + func start() { + currentStepIndex = 0 + isActive = true + } + + func next() { + if currentStepIndex < tutorialSteps.count - 1 { + withAnimation(.easeInOut(duration: 0.3)) { + currentStepIndex += 1 + } + } else { + finish() + } + } + + func back() { + if currentStepIndex > 0 { + withAnimation(.easeInOut(duration: 0.3)) { + currentStepIndex -= 1 + } + } + } + + func finish() { + withAnimation(.easeOut(duration: 0.3)) { + isActive = false + isEditingForTutorial = false + } + UserDefaults.standard.set(true, forKey: "tutorial_completed") + } + + func skip() { + finish() + } + + // MARK: - Demo Workout + + /// Create a demo workout with exercises and a superset for the tutorial. + func createDemoWorkout(store: WorkoutStore) { + // Don't create if we already have one + guard demoWorkoutId == nil else { return } + + let bench = Exercise(name: "Bench Press", equipment: .barbell, muscleGroup: "Chest", sets: [ + ExerciseSet(weight: "135", reps: "10", isComplete: true), + ExerciseSet(weight: "155", reps: "8", isComplete: true), + ExerciseSet(weight: "175", reps: "6"), + ]) + + let rows = Exercise(name: "Barbell Rows", equipment: .barbell, muscleGroup: "Back", sets: [ + ExerciseSet(weight: "135", reps: "10"), + ExerciseSet(weight: "135", reps: "10"), + ExerciseSet(weight: "135", reps: "10"), + ]) + + // Superset: lateral raise + tricep pushdown + let laterals = Exercise(name: "Lateral Raises", equipment: .dumbbell, muscleGroup: "Shoulders", sets: [ + ExerciseSet(weight: "20", reps: "15"), + ExerciseSet(weight: "20", reps: "15"), + ]) + + let pushdowns = Exercise(name: "Rope Pushdowns", equipment: .cable, muscleGroup: "Triceps", sets: [ + ExerciseSet(weight: "40", reps: "12"), + ExerciseSet(weight: "40", reps: "12"), + ]) + + let superset = SupersetGroup( + comment: "Arm Finisher — no rest between exercises", + exercises: [laterals, pushdowns] + ) + + let workout = Workout( + name: "Tutorial Workout", + date: Date(), + items: [ + .exercise(bench), + .exercise(rows), + .superset(superset) + ], + isActive: true, + workoutNotes: "This is a sample workout for the tutorial!", + startedAt: Date() + ) + + store.workouts.append(workout) + store.activeWorkout = workout + store.selectedTab = 1 + demoWorkoutId = workout.id + } + + /// Remove the demo workout when tutorial finishes. + func cleanupDemoWorkout(store: WorkoutStore) { + guard let demoId = demoWorkoutId else { return } + store.workouts.removeAll { $0.id == demoId } + if store.activeWorkout?.id == demoId { + store.activeWorkout = nil + } + store.save() + demoWorkoutId = nil + } +} diff --git a/App Core/Resources/Swift Code/TutorialOverlay.swift b/App Core/Resources/Swift Code/TutorialOverlay.swift new file mode 100644 index 0000000..02567a7 --- /dev/null +++ b/App Core/Resources/Swift Code/TutorialOverlay.swift @@ -0,0 +1,281 @@ +// TutorialOverlay.swift +// mAI Coach — Full-screen tutorial overlay with spotlight and coach-tip bubble. +// Uses frame-reporting through TutorialManager (avoids anchorPreference merge issues). + +import SwiftUI +import Combine + +// MARK: - Preference Key (kept for backward compat but NOT used for positioning) + +struct SpotlightAnchorKey: PreferenceKey { + static var defaultValue: [String: Anchor] = [:] + static func reduce(value: inout [String: Anchor], nextValue: () -> [String: Anchor]) { + value.merge(nextValue(), uniquingKeysWith: { $1 }) + } +} + +// MARK: - Frame-based spotlight tagging + +extension View { + /// Tag a view as a tutorial spotlight target. + /// Reports its frame to TutorialManager in the "tutorialRoot" coordinate space. + func tutorialSpotlight(_ id: String) -> some View { + self.background { + if !id.isEmpty { + GeometryReader { geo in + Color.clear + .onAppear { + let frame = geo.frame(in: .named("tutorialRoot")) + TutorialFrameStore.shared.frames[id] = frame + } + .onChange(of: geo.frame(in: .named("tutorialRoot"))) { _, newFrame in + TutorialFrameStore.shared.frames[id] = newFrame + } + } + } + } + } +} + +// MARK: - Shared frame store (avoids preference key issues) + +@MainActor +class TutorialFrameStore: ObservableObject { + static let shared = TutorialFrameStore() + @Published var frames: [String: CGRect] = [:] + + func frame(for id: String) -> CGRect? { + guard id != "tutorial_none" else { return nil } + return frames[id] + } + + func clear() { + frames.removeAll() + } +} + +// MARK: - Tutorial Overlay + +struct TutorialOverlay: View { + @EnvironmentObject var tutorial: TutorialManager + @ObservedObject var frameStore = TutorialFrameStore.shared + + var body: some View { + if tutorial.isActive { + GeometryReader { geo in + let step = tutorial.currentStep + let spotlightRect = frameStore.frame(for: step.spotlightID) + let safeArea = geo.safeAreaInsets + + ZStack { + // Dimmed background with spotlight hole + dimmedBackground(spotlightRect: spotlightRect) + + // Gold border ring + if let rect = spotlightRect { + RoundedRectangle(cornerRadius: 14) + .stroke(AppColors.accentGold, lineWidth: 2) + .frame(width: rect.width + 16, height: rect.height + 16) + .position(x: rect.midX, y: rect.midY) + } + + // Tip bubble — always clamped within visible area + tipBubble( + step: step, + spotlightRect: spotlightRect, + geoSize: geo.size, + safeTop: safeArea.top, + safeBottom: safeArea.bottom + ) + } + .ignoresSafeArea() + } + .transition(.opacity) + .animation(.easeInOut(duration: 0.3), value: tutorial.currentStepIndex) + .allowsHitTesting(true) + } + } + + // MARK: - Dimmed Background + + private func dimmedBackground(spotlightRect: CGRect?) -> some View { + Color.black.opacity(0.7) + .mask { + Rectangle() + .overlay { + if let rect = spotlightRect { + RoundedRectangle(cornerRadius: 14) + .frame(width: rect.width + 16, height: rect.height + 16) + .position(x: rect.midX, y: rect.midY) + .blendMode(.destinationOut) + } + } + .compositingGroup() + } + } + + // MARK: - Tip Bubble + + private func tipBubble( + step: TutorialStep, + spotlightRect: CGRect?, + geoSize: CGSize, + safeTop: CGFloat, + safeBottom: CGFloat + ) -> some View { + let bubbleWidth: CGFloat = min(geoSize.width - 32, 340) + let estimatedBubbleHeight: CGFloat = 190 + + let yPos = calculateBubbleY( + spotlightRect: spotlightRect, + geoSize: geoSize, + bubbleHeight: estimatedBubbleHeight, + safeTop: safeTop, + safeBottom: safeBottom + ) + + return VStack(spacing: 10) { + // Step counter + Skip + HStack { + Text("Step \(step.id + 1) of \(tutorialSteps.count)") + .font(.system(size: 11, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.5)) + Spacer() + Button { tutorial.skip() } label: { + Text("Skip Tutorial") + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(.white.opacity(0.6)) + } + } + + // Progress bar + GeometryReader { barGeo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(.white.opacity(0.15)) + .frame(height: 3) + RoundedRectangle(cornerRadius: 2) + .fill(AppColors.accentGold) + .frame(width: barGeo.size.width * tutorial.progress, height: 3) + } + } + .frame(height: 3) + + // Title + Text(step.title) + .font(.system(size: 18, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity, alignment: .leading) + + // Message + Text(step.message) + .font(.system(size: 14, weight: .regular, design: .rounded)) + .foregroundStyle(.white.opacity(0.85)) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .lineSpacing(3) + + // Navigation buttons + HStack(spacing: 12) { + if step.id > 0 { + Button { + tutorial.back() + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.system(size: 12, weight: .bold)) + Text("Back") + .font(.system(size: 14, weight: .semibold, design: .rounded)) + } + .foregroundStyle(.white.opacity(0.7)) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(.white.opacity(0.12)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + + Spacer() + + Button { + if tutorial.isLastStep { + tutorial.finish() + } else { + tutorial.next() + } + } label: { + HStack(spacing: 4) { + Text(tutorial.isLastStep ? "Done" : "Next") + .font(.system(size: 14, weight: .bold, design: .rounded)) + if !tutorial.isLastStep { + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .bold)) + } + } + .foregroundStyle(.black) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + .padding(.top, 4) + } + .padding(18) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color(white: 0.12).opacity(0.97)) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(.white.opacity(0.12), lineWidth: 1) + ) + .frame(width: bubbleWidth) + .position(x: geoSize.width / 2, y: yPos) + } + + // MARK: - Bubble Y Calculator + + private func calculateBubbleY( + spotlightRect: CGRect?, + geoSize: CGSize, + bubbleHeight: CGFloat, + safeTop: CGFloat, + safeBottom: CGFloat + ) -> CGFloat { + let minY = safeTop + bubbleHeight / 2 + 8 + let maxY = geoSize.height - safeBottom - bubbleHeight / 2 - 8 + + guard let rect = spotlightRect else { + // No spotlight — center the bubble + return clamp(geoSize.height * 0.4, min: minY, max: maxY) + } + + let gap: CGFloat = 16 + + // Try below the spotlight + let belowY = rect.maxY + gap + 8 + bubbleHeight / 2 + if belowY <= maxY { + return belowY + } + + // Try above the spotlight + let aboveY = rect.minY - gap - 8 - bubbleHeight / 2 + if aboveY >= minY { + return aboveY + } + + // Fallback — whichever direction has more room, clamped + let spaceBelow = geoSize.height - rect.maxY + let spaceAbove = rect.minY + if spaceBelow >= spaceAbove { + return clamp(belowY, min: minY, max: maxY) + } else { + return clamp(aboveY, min: minY, max: maxY) + } + } + + private func clamp(_ value: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat { + Swift.min(Swift.max(value, min), max) + } +} diff --git a/App Core/Resources/Swift Code/WelcomeView.swift b/App Core/Resources/Swift Code/WelcomeView.swift new file mode 100644 index 0000000..88dd292 --- /dev/null +++ b/App Core/Resources/Swift Code/WelcomeView.swift @@ -0,0 +1,96 @@ +// WelcomeView.swift +// mAI Coach — Signed-out landing screen. + +import SwiftUI + +struct WelcomeView: View { + @EnvironmentObject var session: AuthSession + @State private var showSignIn = false + @State private var showCreate = false + + var body: some View { + VStack(spacing: 0) { + Spacer() + + // Logo + Image("AppLogo") + .resizable() + .scaledToFit() + .frame(width: 120) + + Text("Welcome") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + .padding(.top, 16) + + Spacer().frame(height: 32) + + // Buttons + VStack(spacing: 10) { + Button { + showSignIn = true + } label: { + Text("Sign in") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + + Button { + session.continueAsGuest() + } label: { + Text("Continue without signing in") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.accentGold) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + + Button { + showCreate = true + } label: { + Text("Create account") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.accentGold) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: AppSpacing.buttonRadius) + .stroke(AppColors.accentGold, lineWidth: 1.5) + ) + } + } + .padding(.horizontal, AppSpacing.screenPadding) + + Spacer() + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .sheet(isPresented: $showSignIn) { + NavigationStack { + SignInView(mode: .signIn) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Cancel") { showSignIn = false } + .foregroundStyle(AppColors.accentGold) + } + } + } + } + .sheet(isPresented: $showCreate) { + NavigationStack { + SignInView(mode: .create) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Cancel") { showCreate = false } + .foregroundStyle(AppColors.accentGold) + } + } + } + } + } +} diff --git a/App Core/Resources/Swift Code/WorkoutBuilderView.swift b/App Core/Resources/Swift Code/WorkoutBuilderView.swift new file mode 100644 index 0000000..4c4c8cd --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutBuilderView.swift @@ -0,0 +1,218 @@ +// WorkoutBuilderView.swift +// mAI Coach — Build a new workout. On finish, optionally save as a template. + +import SwiftUI + +struct WorkoutBuilderView: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + let forDate: Date + + @State private var workoutName = "" + @State private var exercises: [Exercise] = [] + @State private var showExercisePicker = false + @State private var showSaveTemplateDialog = false + @State private var showNameWarning = false + + /// Tags auto-computed from exercises' muscle groups. + private var computedTags: [String] { + let groups = exercises.compactMap { $0.muscleGroup.isEmpty ? nil : $0.muscleGroup } + return Array(Set(groups)).sorted() + } + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 16) { + // Name field + VStack(alignment: .leading, spacing: 6) { + Text("WORKOUT NAME") + .font(.system(size: 11, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textTertiary) + TextField("e.g. Push Day", text: $workoutName) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + .padding(14) + .background(AppColors.bgSecondary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(showNameWarning && workoutName.trimmingCharacters(in: .whitespaces).isEmpty ? Color.red.opacity(0.6) : AppColors.border, lineWidth: 1) + ) + .onChange(of: workoutName) { _, _ in + if !workoutName.trimmingCharacters(in: .whitespaces).isEmpty { + withAnimation(.easeOut(duration: 0.3)) { + showNameWarning = false + } + } + } + } + + // Tags (auto-computed from exercises) + if !computedTags.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(computedTags, id: \.self) { tag in + Text(tag) + .font(AppFonts.footnote()) + .fontWeight(.medium) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(AppColors.accentMuted) + .foregroundStyle(AppColors.accentGold) + .clipShape(Capsule()) + } + } + } + } + + // Exercise cards + ForEach(exercises.indices, id: \.self) { i in + ExerciseCardView( + exercise: $exercises[i], + onCoachTap: nil + ) + } + + // Add Exercise + Button { + showExercisePicker = true + } label: { + Text("+ Add Exercise") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentText) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [8])) + .foregroundStyle(AppColors.accentGold) + ) + } + + Color.clear.frame(height: 20) + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .overlay(alignment: .bottom) { + if showNameWarning && workoutName.trimmingCharacters(in: .whitespaces).isEmpty { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 14)) + Text("Please name your workout to save") + .font(.subheadline.weight(.semibold)) + } + .foregroundStyle(.white) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background( + Color.red.opacity(0.85), + in: RoundedRectangle(cornerRadius: 14) + ) + .padding(.bottom, 20) + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } + .navigationTitle("New Workout") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + .foregroundStyle(AppColors.textSecondary) + } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + if workoutName.trimmingCharacters(in: .whitespaces).isEmpty { + withAnimation(.easeInOut(duration: 0.3)) { + showNameWarning = true + } + return + } + showSaveTemplateDialog = true + } + .fontWeight(.bold) + .foregroundStyle(AppColors.accentGold) + } + } + .sheet(isPresented: $showExercisePicker) { + ExercisePickerView { exercise in + exercises.append(exercise) + } + } + .alert("Save as Template?", isPresented: $showSaveTemplateDialog) { + Button("Save as Template") { + let workout = Workout(name: workoutName, date: forDate, exercises: exercises) + store.workouts.append(workout) + store.createTemplate(name: workoutName, exercises: exercises) + store.save() + dismiss() + } + Button("Just This Workout", role: .cancel) { + let workout = Workout(name: workoutName, date: forDate, exercises: exercises) + store.workouts.append(workout) + store.save() + dismiss() + } + } message: { + Text("Would you like to save this as a reusable template?") + } + } + } +} + +// MARK: - Tag Picker View + +struct TagPickerView: View { + @Binding var selectedTags: [String] + @Environment(\.dismiss) private var dismiss + + private let availableTags = ExerciseCatalog.muscleGroups + + var body: some View { + NavigationStack { + List { + ForEach(availableTags, id: \.self) { tag in + Button { + if selectedTags.contains(tag) { + selectedTags.removeAll { $0 == tag } + } else { + selectedTags.append(tag) + } + } label: { + HStack { + Text(tag) + .font(AppFonts.body()) + .foregroundStyle(AppColors.textPrimary) + Spacer() + if selectedTags.contains(tag) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(AppColors.accentGold) + } else { + Image(systemName: "circle") + .font(.system(size: 20)) + .foregroundStyle(AppColors.textTertiary) + } + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("Muscle Tags") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { dismiss() } + .fontWeight(.bold) + .foregroundStyle(AppColors.accentGold) + } + } + } + } +} diff --git a/App Core/Resources/Swift Code/WorkoutDetailView.swift b/App Core/Resources/Swift Code/WorkoutDetailView.swift new file mode 100644 index 0000000..34fafa6 --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutDetailView.swift @@ -0,0 +1,800 @@ +// WorkoutDetailView.swift +// mAI Coach — THE unified workout view. +// Used everywhere: Workout tab (active), calendar (past), templates. +// All workouts behave identically once opened. + +import SwiftUI + +struct WorkoutDetailView: View { + @EnvironmentObject var store: WorkoutStore + @EnvironmentObject var restTimer: RestTimerManager + @EnvironmentObject var tutorial: TutorialManager + @Environment(\.dismiss) private var dismiss + + let workoutId: UUID + + /// Whether this view is embedded (no nav title / back button managed externally). + var embedded: Bool = false + + /// Live workout from the store — reactive to state changes. + private var workout: Workout { + store.workouts.first(where: { $0.id == workoutId }) ?? Workout(name: "Unknown", date: Date(), exercises: []) + } + + @State private var items: [WorkoutItem] = [] + @State private var workoutNotes: String = "" + @State private var isEditing = false + @State private var showExercisePicker = false + @State private var showSavedAlert = false + @State private var showPRCongrats = false + @State private var showConfetti = false + @State private var newPRs: [PersonalRecord] = [] + + // Fill from history state + @State private var showWorkoutHistory = false + @State private var showLastFillConfirm = false + @State private var showNoWorkoutData = false + + // Coach session state + @State private var showSession = false + @State private var sessionMode: BenchSessionView.Mode = .live + + // Live duration timer + @State private var durationTimer: Timer? + @State private var elapsedSeconds: Int = 0 + + private var alreadyTemplate: Bool { + store.hasTemplate(named: workout.name) + } + + /// Flat exercises from local items state (for tags, history, etc.) + private var exercises: [Exercise] { + items.flatMap { $0.allExercises } + } + + /// Tags auto-computed from exercises' muscle groups, with catalog fallback. + private var computedTags: [String] { + let groups = exercises.compactMap { ex -> String? in + if !ex.muscleGroup.isEmpty { return ex.muscleGroup } + return ExerciseCatalog.exercises.first(where: { $0.name == ex.name })?.muscleGroup + } + return Array(Set(groups)).sorted() + } + + /// Pre-computed PR message to avoid complex expression in body. + private var prMessage: String { + let names = newPRs.map { "\($0.exerciseName): \(Int($0.weight)) lbs" }.joined(separator: "\n") + return "Congrats! You set \(newPRs.count == 1 ? "a new PR" : "\(newPRs.count) new PRs")!\n\n\(names)" + } + + var body: some View { + coreLayout + .onAppear { + items = workout.items + workoutNotes = workout.workoutNotes + checkAutoFinish() + startDurationTimer() + } + .onDisappear { + durationTimer?.invalidate() + durationTimer = nil + } + .onChange(of: workout.isActive) { _, isActive in + if isActive { + startDurationTimer() + } else { + durationTimer?.invalidate() + durationTimer = nil + updateElapsed() + } + } + .onChange(of: store.latestPRs.count) { _, _ in + if !store.latestPRs.isEmpty { + newPRs = store.latestPRs + syncItemsToStore() + showConfetti = true + showPRCongrats = true + store.latestPRs = [] + } + } + .alert("Template Saved", isPresented: $showSavedAlert) { + Button("OK", role: .cancel) { } + } message: { + Text("\"\(workout.name)\" has been saved as a template.") + } + .alert("🏆 New Personal Record!", isPresented: $showPRCongrats) { + Button("Let's Go!", role: .cancel) { } + } message: { + Text(prMessage) + } + .confetti(isActive: $showConfetti) + .alert("Replace All Sets?", isPresented: $showLastFillConfirm) { + Button("Cancel", role: .cancel) { } + Button("Fill All") { + if let last = store.workoutHistory(named: workout.name, limit: 1).first { + fillFromHistory(last) + } + } + } message: { + Text("This will overwrite current weights and reps with your last session's data.") + } + .alert("No History", isPresented: $showNoWorkoutData) { + Button("OK", role: .cancel) { } + } message: { + Text("No previous sessions found for \"\(workout.name)\".") + } + } + + // MARK: - Core layout (extracted to reduce body complexity) + private var coreLayout: some View { + VStack(spacing: 0) { + mainContent + + if restTimer.isRunning { + restTimerBar + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle(workout.name) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + toolbarButtons + } + } + .sheet(isPresented: $showExercisePicker) { + ExercisePickerView { exercise in + items.append(.exercise(exercise)) + syncItemsToStore() + } + } + .sheet(isPresented: $showWorkoutHistory) { + WorkoutHistorySheet { histWorkout in + fillFromHistory(histWorkout) + } + .environmentObject(store) + } + .navigationDestination(isPresented: $showSession) { + BenchSessionView(mode: sessionMode) + .navigationBarBackButtonHidden(true) + } + .onChange(of: showSession) { _, newValue in + store.showingSession = newValue + } + } + + // MARK: - Main scrollable content + private var mainContent: some View { + ScrollViewReader { scrollProxy in + ScrollView { + VStack(spacing: 12) { + // Fill header scrolls with content + Color.clear.frame(height: 0).id("workoutScrollTop") + if !workout.isTemplate { + fillHeader + } + + tagsView + .tutorialSpotlight("tutorial_tags") + + // Workout-level notes + if !workoutNotes.isEmpty || isEditing { + TextField("Workout notes...", text: $workoutNotes, axis: .vertical) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + .lineLimit(1...6) + .padding(12) + .background(AppColors.bgSecondary) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .onChange(of: workoutNotes) { _, _ in + syncItemsToStore() + } + .tutorialSpotlight("tutorial_workoutNotes") + } + + // Workout items: standalone exercises and superset blocks + ForEach(Array(items.enumerated()), id: \.element.id) { i, item in + VStack(spacing: 0) { + // Reorder bar — only visible when editing + if isEditing { + HStack(spacing: 12) { + Button { + guard i > 0 else { return } + withAnimation(.easeInOut(duration: 0.3)) { + items.swapAt(i, i - 1) + } + syncItemsToStore() + } label: { + Image(systemName: "chevron.up") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(i > 0 ? AppColors.textPrimary : AppColors.textTertiary.opacity(0.3)) + .frame(width: 32, height: 24) + } + .disabled(i == 0) + + Text("Move") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(AppColors.textTertiary) + + Button { + guard i < items.count - 1 else { return } + withAnimation(.easeInOut(duration: 0.3)) { + items.swapAt(i, i + 1) + } + syncItemsToStore() + } label: { + Image(systemName: "chevron.down") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(i < items.count - 1 ? AppColors.textPrimary : AppColors.textTertiary.opacity(0.3)) + .frame(width: 32, height: 24) + } + .disabled(i == items.count - 1) + } + .frame(height: 28) + .padding(.horizontal, 8) + .background(AppColors.bgSecondary) + .clipShape(UnevenRoundedRectangle( + topLeadingRadius: 10, bottomLeadingRadius: 0, + bottomTrailingRadius: 0, topTrailingRadius: 10 + )) + } + + // Render based on item type + switch item { + case .exercise: + ExerciseCardView( + exercise: exerciseBinding(at: i), + onCoachTap: { mode in + sessionMode = mode + showSession = true + }, + onAutoStart: { + guard !workout.isActive && !workout.isTemplate else { return } + store.resumeWorkout(workout) + }, + isEditing: isEditing, + workoutId: workoutId + ) + .tutorialSpotlight(i == 0 ? "tutorial_exerciseCard" : "") + .id(i == 0 ? "tutorial_exerciseCard" : "exerciseItem_\(i)") + case .superset: + SupersetBlockView( + group: supersetBinding(at: i), + onCoachTap: { mode in + sessionMode = mode + showSession = true + }, + onAutoStart: { + guard !workout.isActive && !workout.isTemplate else { return } + store.resumeWorkout(workout) + }, + isEditing: isEditing, + workoutId: workoutId + ) + .tutorialSpotlight("tutorial_supersetBlock") + .id("tutorial_supersetBlock") + } + } + .overlay(alignment: .topTrailing) { + if isEditing { + Button { + withAnimation { + items.remove(at: i) + syncItemsToStore() + } + } label: { + Image(systemName: "minus.circle.fill") + .font(.system(size: 22)) + .foregroundStyle(.red) + .background(Circle().fill(AppColors.bgPrimary)) + } + .offset(x: 8, y: isEditing ? 14 : -8) + } + } + } + + addExerciseButton + .tutorialSpotlight("tutorial_addExercise") + .id("tutorial_addExercise") + if isEditing || (tutorial.isActive && tutorial.currentStep.spotlightID == "tutorial_addSuperset") { + addSupersetButton + .tutorialSpotlight("tutorial_addSuperset") + .id("tutorial_addSuperset") + } + durationDisplay + actionButton + .tutorialSpotlight("tutorial_actionButton") + .id("tutorial_actionButton") + Color.clear.frame(height: 20) + } + .padding(.horizontal, AppSpacing.screenPadding) + } + .onChange(of: tutorial.currentStepIndex) { _, _ in + guard tutorial.isActive, tutorial.currentStep.targetTab == 1 else { return } + let spotID = tutorial.currentStep.spotlightID + guard spotID != "tutorial_none" else { return } + // Map spotlight IDs to scroll anchors + let scrollTarget: String? + switch spotID { + case "tutorial_tags": + scrollTarget = "workoutScrollTop" + case "tutorial_exerciseCard": + scrollTarget = "tutorial_exerciseCard" + case "tutorial_supersetBlock": + scrollTarget = "tutorial_supersetBlock" + case "tutorial_addExercise": + scrollTarget = "tutorial_addExercise" + case "tutorial_addSuperset": + scrollTarget = "tutorial_addSuperset" + case "tutorial_workoutNotes": + scrollTarget = "workoutScrollTop" + case "tutorial_actionButton": + scrollTarget = "tutorial_actionButton" + default: + scrollTarget = nil + } + if let target = scrollTarget { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + withAnimation(.easeInOut(duration: 0.4)) { + scrollProxy.scrollTo(target, anchor: .center) + } + } + } + } + } // ScrollViewReader + } + + // MARK: - Tags + @ViewBuilder + private var tagsView: some View { + if !computedTags.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(computedTags, id: \.self) { tag in + Text(tag) + .font(AppFonts.footnote()) + .fontWeight(.medium) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(AppColors.accentMuted) + .foregroundStyle(AppColors.accentGold) + .clipShape(Capsule()) + } + } + } + .padding(.bottom, 4) + } + } + + + private var addExerciseButton: some View { + Button { + showExercisePicker = true + } label: { + Text("+ Add Exercise") + .font(AppFonts.subhead()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.accentText) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.cardRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.cardRadius) + .inset(by: 0.5) + .stroke(style: StrokeStyle(lineWidth: 1, dash: [8])) + .foregroundStyle(AppColors.accentGold) + ) + } + } + + // MARK: - Duration Display + @ViewBuilder + private var durationDisplay: some View { + if !workout.isTemplate, workout.startedAt != nil { + HStack(spacing: 8) { + Image(systemName: "timer") + .font(.system(size: 16)) + .foregroundStyle(AppColors.accentGold) + + Text(formatDuration(elapsedSeconds)) + .font(.system(size: 18, weight: .semibold, design: .monospaced)) + .foregroundStyle(workout.isActive ? AppColors.textPrimary : AppColors.textSecondary) + + if !workout.isActive { + Text("(finished)") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textTertiary) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + } + } + + private func startDurationTimer() { + updateElapsed() + guard workout.isActive, workout.startedAt != nil else { return } + durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [self] _ in + DispatchQueue.main.async { + self.updateElapsed() + } + } + } + + private func updateElapsed() { + if let duration = workout.duration { + elapsedSeconds = Int(duration) + } + } + + private func formatDuration(_ totalSeconds: Int) -> String { + let h = totalSeconds / 3600 + let m = (totalSeconds % 3600) / 60 + let s = totalSeconds % 60 + if h > 0 { + return String(format: "%d:%02d:%02d", h, m, s) + } + return String(format: "%d:%02d", m, s) + } + + // MARK: - Action Button (Start / Finish / Resume) + @ViewBuilder + private var actionButton: some View { + if workout.isTemplate { + // Template → create new workout + Button { + store.startWorkoutFromTemplate(workout) + if !embedded { + store.selectedTab = 1 + dismiss() + } + } label: { + Text("Start Workout") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + .padding(.top, 8) + } else if workout.isActive { + // Active → Finish Workout + Button { + syncItemsToStore() + if let idx = store.workouts.firstIndex(where: { $0.id == workout.id }) { + store.workouts[idx].isActive = false + store.workouts[idx].finishedAt = Date() + if store.activeWorkout?.id == workout.id { + store.activeWorkout = nil + } + store.save() + } + // Stop the live timer — keep the final value + durationTimer?.invalidate() + durationTimer = nil + } label: { + Text("Finish Workout") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + .padding(.top, 8) + } else if workout.hasCompletedSets || workout.startedAt != nil { + // Finished → Resume + Button { + store.resumeWorkout(workout) + } label: { + Text("Resume Workout") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + .padding(.top, 8) + } else { + // Not started → Start + Button { + store.resumeWorkout(workout) + } label: { + Text("Start Workout") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + .padding(.top, 8) + } + } + + // MARK: - Fill Header (scrolls with content) + private var fillHeader: some View { + HStack(spacing: 10) { + // Fill from last session (whole workout) + Button { + let history = store.workoutHistory(named: workout.name, limit: 1) + guard let last = history.first else { + showNoWorkoutData = true + return + } + let hasData = exercises.contains { + $0.sets.contains { !$0.weight.isEmpty || !$0.reps.isEmpty } + } + if hasData { + showLastFillConfirm = true + } else { + fillFromHistory(last) + } + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 11, weight: .bold)) + Text("Fill from Last") + .font(.system(size: 12, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.textSecondary) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.border, lineWidth: 1) + ) + } + + // Workout history + Button { showWorkoutHistory = true } label: { + HStack(spacing: 4) { + Image(systemName: "clock.arrow.circlepath") + .font(.system(size: 11, weight: .bold)) + Text("History") + .font(.system(size: 12, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.textSecondary) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(AppColors.bgTertiary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.border, lineWidth: 1) + ) + } + + Spacer() + + // Save as Template + if !alreadyTemplate && !isEditing { + Button { + store.saveAsTemplate(workout) + showSavedAlert = true + } label: { + HStack(spacing: 4) { + Image(systemName: "square.and.arrow.down") + .font(.system(size: 11)) + Text("Save") + .font(.system(size: 12, weight: .semibold, design: .rounded)) + } + .foregroundStyle(AppColors.accentGold) + } + } + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.vertical, 8) + .background(AppColors.bgPrimary) + } + + // MARK: - Toolbar Buttons (Edit only) + private var toolbarButtons: some View { + // Edit / Done toggle + Button { + if isEditing { + syncItemsToStore() + } + isEditing.toggle() + } label: { + Text(isEditing ? "Done" : "Edit") + .fontWeight(isEditing ? .bold : .regular) + .foregroundStyle(isEditing ? .red : AppColors.accentGold) + } + } + + // MARK: - Rest Timer Bar + private var restTimerBar: some View { + HStack(spacing: 16) { + // Progress ring + ZStack { + Circle() + .stroke(AppColors.bgTertiary, lineWidth: 3) + .frame(width: 40, height: 40) + Circle() + .trim(from: 0, to: restTimer.progress) + .stroke(AppColors.accentGold, style: StrokeStyle(lineWidth: 3, lineCap: .round)) + .frame(width: 40, height: 40) + .rotationEffect(.degrees(-90)) + .animation(.linear(duration: 1), value: restTimer.progress) + Image(systemName: "timer") + .font(.system(size: 14)) + .foregroundStyle(AppColors.accentGold) + } + + // Time display + VStack(alignment: .leading, spacing: 2) { + Text("Rest Timer") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(AppColors.textSecondary) + Text(restTimer.timeString) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .foregroundStyle(AppColors.textPrimary) + .monospacedDigit() + } + + Spacer() + + // -15 / +15 buttons + Button { + restTimer.subtract15() + } label: { + Text("-15s") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(AppColors.textSecondary) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(AppColors.bgTertiary) + .clipShape(Capsule()) + } + + Button { + restTimer.add15() + } label: { + Text("+15s") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(AppColors.accentMuted) + .clipShape(Capsule()) + } + + // Skip + Button { + withAnimation(.spring(response: 0.3)) { + restTimer.stop() + } + } label: { + Image(systemName: "xmark") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(AppColors.textTertiary) + .frame(width: 30, height: 30) + .background(AppColors.bgTertiary) + .clipShape(Circle()) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(AppColors.bgPrimary) + .shadow(color: .black.opacity(0.2), radius: 12, y: -4) + ) + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.bottom, 4) + } + + // MARK: - Fill from history + private func fillFromHistory(_ historicalWorkout: Workout) { + // Fill matching exercises across all items (standalone + superset) + for i in items.indices { + switch items[i] { + case .exercise(var ex): + if let match = historicalWorkout.exercises.first(where: { $0.name == ex.name }) { + ex.sets = match.sets.map { old in + ExerciseSet(weight: old.weight, reps: old.reps, isComplete: false) + } + items[i] = .exercise(ex) + } + case .superset(var group): + for j in group.exercises.indices { + if let match = historicalWorkout.exercises.first(where: { $0.name == group.exercises[j].name }) { + group.exercises[j].sets = match.sets.map { old in + ExerciseSet(weight: old.weight, reps: old.reps, isComplete: false) + } + } + } + items[i] = .superset(group) + } + } + syncItemsToStore() + } + + // MARK: - Sync local items to store + private func syncItemsToStore() { + store.updateWorkout(id: workoutId, items: items) + if let idx = store.workouts.firstIndex(where: { $0.id == workoutId }) { + store.workouts[idx].workoutNotes = workoutNotes + } + if store.activeWorkout?.id == workoutId { + store.activeWorkout?.items = items + store.activeWorkout?.workoutNotes = workoutNotes + } + store.save() + } + + // MARK: - Binding helpers + private func exerciseBinding(at index: Int) -> Binding { + Binding( + get: { + guard index < items.count, case .exercise(let ex) = items[index] else { + return Exercise(name: "Unknown", equipment: .barbell) + } + return ex + }, + set: { newValue in + guard index < items.count else { return } + items[index] = .exercise(newValue) + } + ) + } + + private func supersetBinding(at index: Int) -> Binding { + Binding( + get: { + guard index < items.count, case .superset(let g) = items[index] else { + return SupersetGroup() + } + return g + }, + set: { newValue in + guard index < items.count else { return } + items[index] = .superset(newValue) + } + ) + } + + // MARK: - Add Superset Button + private var addSupersetButton: some View { + Button { + withAnimation { + items.append(.superset(SupersetGroup(comment: "", exercises: []))) + syncItemsToStore() + } + } label: { + Label("Add Superset", systemImage: "square.stack.fill") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.accentGold) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: AppSpacing.buttonRadius) + .stroke(AppColors.accentGold.opacity(0.5), lineWidth: 1.5) + .background(AppColors.accentGold.opacity(0.08).clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius))) + ) + } + } + + // MARK: - 24h Auto-Finish + private func checkAutoFinish() { + guard workout.isActive, + let started = workout.startedAt, + Date().timeIntervalSince(started) > 24 * 60 * 60 else { return } + + // Find the last completed set time (approximate: use the workout date as base) + // Auto-finish the workout + if let idx = store.workouts.firstIndex(where: { $0.id == workout.id }) { + store.workouts[idx].isActive = false + if store.activeWorkout?.id == workout.id { + store.activeWorkout = nil + } + store.save() + } + } +} diff --git a/App Core/Resources/Swift Code/WorkoutHistorySheet.swift b/App Core/Resources/Swift Code/WorkoutHistorySheet.swift new file mode 100644 index 0000000..73aad97 --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutHistorySheet.swift @@ -0,0 +1,147 @@ +// WorkoutHistorySheet.swift +// mAI Coach — Shows past sessions of the current workout for whole-workout Quick Fill. + +import SwiftUI + +struct WorkoutHistorySheet: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + /// Called when user confirms Quick Fill with a historical workout. + var onFill: (Workout) -> Void + + @State private var confirmWorkout: Workout? + @State private var showConfirmAlert = false + + private var history: [Workout] { + guard let name = store.activeWorkout?.name else { return [] } + return store.workoutHistory(named: name) + } + + var body: some View { + NavigationStack { + Group { + if history.isEmpty { + emptyState + } else { + historyList + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("Workout History") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Close") { dismiss() } + .foregroundStyle(AppColors.accentGold) + } + } + .alert("Fill All Exercises?", isPresented: $showConfirmAlert) { + Button("Cancel", role: .cancel) { confirmWorkout = nil } + Button("Fill All") { + if let workout = confirmWorkout { + onFill(workout) + confirmWorkout = nil + dismiss() + } + } + } message: { + Text("This will replace sets for all matching exercises. Exercises not in the selected session will be left unchanged.") + } + } + } + + // MARK: - Empty State + private var emptyState: some View { + VStack(spacing: 12) { + Spacer() + Image(systemName: "clock.arrow.circlepath") + .font(.system(size: 48)) + .foregroundStyle(AppColors.textTertiary) + .opacity(0.5) + Text("No History Found") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + Text("Complete this workout to see past sessions here.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + } + + // MARK: - History List + private var historyList: some View { + ScrollView { + VStack(spacing: 12) { + ForEach(history) { workout in + historyCard(workout: workout) + } + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + } + + private func historyCard(workout: Workout) -> some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(workout.date, style: .date) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + Text("\(workout.exercises.count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + Spacer() + Button { + confirmWorkout = workout + showConfirmAlert = true + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.down.doc.fill") + .font(.system(size: 11)) + Text("Fill All") + .font(.system(size: 12, weight: .bold, design: .rounded)) + } + .foregroundStyle(AppColors.accentGold) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(AppColors.accentGold, lineWidth: 1) + ) + } + } + + Divider().background(AppColors.border) + + // Exercise summary + ForEach(workout.exercises) { exercise in + HStack { + Text(exercise.name) + .font(AppFonts.footnote()) + .fontWeight(.semibold) + .foregroundStyle(AppColors.textPrimary) + Spacer() + Text(exerciseSummary(exercise)) + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + } + .padding(14) + .cardStyle() + } + + private func exerciseSummary(_ exercise: Exercise) -> String { + let completedSets = exercise.sets.filter { !$0.weight.isEmpty || !$0.reps.isEmpty } + if completedSets.isEmpty { return "No data" } + let maxWeight = completedSets.compactMap { Double($0.weight) }.max() ?? 0 + return "\(completedSets.count) sets · \(Int(maxWeight)) lbs" + } +} diff --git a/App Core/Resources/Swift Code/WorkoutModel.swift b/App Core/Resources/Swift Code/WorkoutModel.swift new file mode 100644 index 0000000..65a79f0 --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutModel.swift @@ -0,0 +1,1186 @@ +// WorkoutModel.swift +// mAI Coach — Workout data models and store. + +import SwiftUI +import Combine + +// MARK: - Equipment Type + +enum EquipmentType: String, Codable, CaseIterable, Identifiable { + case barbell + case dumbbell + case cable + case machine + case bodyweight + case kettlebell + case resistanceBand + case medicineBall + case plate + case suspension + case stabilityBall + case foamRoller + case abWheel + case trapBar + case landmine + case sled + case battleRopes + case box + case pullUpBar + case dipStation + case ezCurlBar + case cardioMachine + case other + + var id: String { rawValue } + + var label: String { + switch self { + case .barbell: return "Barbell" + case .dumbbell: return "Dumbbell" + case .cable: return "Cable" + case .machine: return "Machine" + case .bodyweight: return "Bodyweight" + case .kettlebell: return "Kettlebell" + case .resistanceBand: return "Resistance Band" + case .medicineBall: return "Medicine Ball" + case .plate: return "Plate" + case .suspension: return "Suspension / TRX" + case .stabilityBall: return "Stability Ball" + case .foamRoller: return "Foam Roller" + case .abWheel: return "Ab Wheel" + case .trapBar: return "Trap / Hex Bar" + case .landmine: return "Landmine" + case .sled: return "Sled" + case .battleRopes: return "Battle Ropes" + case .box: return "Box / Step" + case .pullUpBar: return "Pull-Up Bar" + case .dipStation: return "Dip Station" + case .ezCurlBar: return "EZ Curl Bar" + case .cardioMachine: return "Cardio Machine" + case .other: return "Other" + } + } + + var icon: String { + switch self { + case .barbell: return "equip_barbell" + case .dumbbell: return "equip_dumbbell" + case .cable: return "equip_cable" + case .machine: return "equip_machine" + case .bodyweight: return "equip_bodyweight" + case .kettlebell: return "equip_kettlebell" + case .resistanceBand: return "equip_resistanceBand" + case .medicineBall: return "equip_medicineBall" + case .plate: return "equip_plate" + case .suspension: return "equip_suspension" + case .stabilityBall: return "equip_stabilityBall" + case .foamRoller: return "equip_foamRoller" + case .abWheel: return "equip_abWheel" + case .trapBar: return "equip_trapBar" + case .landmine: return "equip_landmine" + case .sled: return "equip_sled" + case .battleRopes: return "equip_battleRopes" + case .box: return "equip_box" + case .pullUpBar: return "equip_pullUpBar" + case .dipStation: return "equip_dipStation" + case .ezCurlBar: return "equip_ezCurlBar" + case .cardioMachine: return "equip_cardioMachine" + case .other: return "equip_other" + } + } +} + +// MARK: - Data Types + +struct ExerciseSet: Identifiable, Codable { + let id: UUID + var weight: String + var reps: String + var rpe: String + var isComplete: Bool + + init(id: UUID = UUID(), weight: String = "", reps: String = "", rpe: String = "", isComplete: Bool = false) { + self.id = id + self.weight = weight + self.reps = reps + self.rpe = rpe + self.isComplete = isComplete + } + + // Backward-compatible decoding (existing data has no rpe field) + enum CodingKeys: String, CodingKey { + case id, weight, reps, rpe, isComplete + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(UUID.self, forKey: .id) + weight = try c.decode(String.self, forKey: .weight) + reps = try c.decode(String.self, forKey: .reps) + rpe = try c.decodeIfPresent(String.self, forKey: .rpe) ?? "" + isComplete = try c.decode(Bool.self, forKey: .isComplete) + } +} + +struct Exercise: Identifiable, Codable { + let id: UUID + var name: String + var equipment: EquipmentType + var muscleGroup: String + var sets: [ExerciseSet] + var notes: String + /// If true, the mAI Coach button is shown to launch camera coaching. + var hasCoachModel: Bool + /// Which BenchSessionView.Mode to use (nil if no coach model). + var coachMode: BenchSessionView.Mode? + + init( + id: UUID = UUID(), + name: String, + equipment: EquipmentType = .barbell, + muscleGroup: String = "", + sets: [ExerciseSet] = [ExerciseSet()], + notes: String = "", + hasCoachModel: Bool = false, + coachMode: BenchSessionView.Mode? = nil + ) { + self.id = id + self.name = name + self.equipment = equipment + self.muscleGroup = muscleGroup + self.sets = sets + self.notes = notes + self.hasCoachModel = hasCoachModel + self.coachMode = coachMode + } + + // Backward-compatible decoding + enum CodingKeys: String, CodingKey { + case id, name, equipment, muscleGroup, sets, notes, hasCoachModel, coachMode + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(UUID.self, forKey: .id) + name = try c.decode(String.self, forKey: .name) + equipment = try c.decode(EquipmentType.self, forKey: .equipment) + muscleGroup = try c.decodeIfPresent(String.self, forKey: .muscleGroup) ?? "" + sets = try c.decode([ExerciseSet].self, forKey: .sets) + notes = try c.decode(String.self, forKey: .notes) + hasCoachModel = try c.decodeIfPresent(Bool.self, forKey: .hasCoachModel) ?? false + coachMode = try c.decodeIfPresent(BenchSessionView.Mode.self, forKey: .coachMode) + } +} + +// MARK: - Superset Group + +struct SupersetGroup: Identifiable, Codable { + let id: UUID + var comment: String + var notes: String + var exercises: [Exercise] + + init(id: UUID = UUID(), comment: String = "", notes: String = "", exercises: [Exercise] = []) { + self.id = id + self.comment = comment + self.notes = notes + self.exercises = exercises + } +} + +// MARK: - Workout Item (exercise or superset) + +enum WorkoutItem: Identifiable, Codable { + case exercise(Exercise) + case superset(SupersetGroup) + + var id: UUID { + switch self { + case .exercise(let e): return e.id + case .superset(let g): return g.id + } + } + + /// All exercises contained in this item (1 for standalone, N for superset). + var allExercises: [Exercise] { + switch self { + case .exercise(let e): return [e] + case .superset(let g): return g.exercises + } + } + + // Codable + enum CodingKeys: String, CodingKey { + case type, exercise, superset + } + + enum ItemType: String, Codable { + case exercise, superset + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + let t = try c.decode(ItemType.self, forKey: .type) + switch t { + case .exercise: + let ex = try c.decode(Exercise.self, forKey: .exercise) + self = .exercise(ex) + case .superset: + let g = try c.decode(SupersetGroup.self, forKey: .superset) + self = .superset(g) + } + } + + func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .exercise(let ex): + try c.encode(ItemType.exercise, forKey: .type) + try c.encode(ex, forKey: .exercise) + case .superset(let g): + try c.encode(ItemType.superset, forKey: .type) + try c.encode(g, forKey: .superset) + } + } +} + +// MARK: - Workout + +struct Workout: Identifiable, Codable { + let id: UUID + var name: String + var date: Date + var items: [WorkoutItem] + var isActive: Bool + var isTemplate: Bool + var workoutNotes: String + var startedAt: Date? + var finishedAt: Date? + var lastSetCompletedAt: Date? + + /// Flat list of all exercises (standalone + inside supersets). Read-only convenience. + var exercises: [Exercise] { + items.flatMap { $0.allExercises } + } + + /// Total exercise count (standalone + inside supersets). + var exerciseCount: Int { + exercises.count + } + + /// Tags are automatically computed from the exercises' muscle groups. + var tags: [String] { + let groups = exercises.compactMap { $0.muscleGroup.isEmpty ? nil : $0.muscleGroup } + return Array(Set(groups)).sorted() + } + + /// Whether any exercise has at least one completed set. + var hasCompletedSets: Bool { + exercises.contains { ex in ex.sets.contains { $0.isComplete } } + } + + /// Duration of the workout in seconds. + var duration: TimeInterval? { + guard let start = startedAt else { return nil } + let end = finishedAt ?? (isActive ? Date() : (lastSetCompletedAt ?? start)) + return end.timeIntervalSince(start) + } + + /// Primary init using items array. + init(id: UUID = UUID(), name: String, date: Date, items: [WorkoutItem], isActive: Bool = false, isTemplate: Bool = false, workoutNotes: String = "", startedAt: Date? = nil, finishedAt: Date? = nil, lastSetCompletedAt: Date? = nil) { + self.id = id + self.name = name + self.date = date + self.items = items + self.isActive = isActive + self.isTemplate = isTemplate + self.workoutNotes = workoutNotes + self.startedAt = startedAt + self.finishedAt = finishedAt + self.lastSetCompletedAt = lastSetCompletedAt + } + + /// Convenience init with flat exercises (wraps each as .exercise item). + init(id: UUID = UUID(), name: String, date: Date, exercises: [Exercise], isActive: Bool = false, isTemplate: Bool = false, workoutNotes: String = "", startedAt: Date? = nil, finishedAt: Date? = nil, lastSetCompletedAt: Date? = nil) { + self.init(id: id, name: name, date: date, items: exercises.map { .exercise($0) }, isActive: isActive, isTemplate: isTemplate, workoutNotes: workoutNotes, startedAt: startedAt, finishedAt: finishedAt, lastSetCompletedAt: lastSetCompletedAt) + } + + /// Find and mutate an exercise by ID (works across standalone and superset items). + mutating func mutateExercise(id: UUID, _ transform: (inout Exercise) -> Void) { + for i in items.indices { + switch items[i] { + case .exercise(var ex) where ex.id == id: + transform(&ex) + items[i] = .exercise(ex) + return + case .superset(var group): + if let j = group.exercises.firstIndex(where: { $0.id == id }) { + transform(&group.exercises[j]) + items[i] = .superset(group) + return + } + default: + break + } + } + } + + // Backward-compatible decoding + enum CodingKeys: String, CodingKey { + case id, name, date, items, exercises, isActive, isTemplate, workoutNotes, startedAt, finishedAt, lastSetCompletedAt + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(UUID.self, forKey: .id) + name = try c.decode(String.self, forKey: .name) + date = try c.decode(Date.self, forKey: .date) + + // Try new `items` key first; fall back to old `exercises` key + if let decoded = try? c.decode([WorkoutItem].self, forKey: .items) { + items = decoded + } else { + let legacy = try c.decode([Exercise].self, forKey: .exercises) + items = legacy.map { .exercise($0) } + } + + isActive = try c.decodeIfPresent(Bool.self, forKey: .isActive) ?? false + isTemplate = try c.decodeIfPresent(Bool.self, forKey: .isTemplate) ?? false + workoutNotes = try c.decodeIfPresent(String.self, forKey: .workoutNotes) ?? "" + startedAt = try c.decodeIfPresent(Date.self, forKey: .startedAt) + finishedAt = try c.decodeIfPresent(Date.self, forKey: .finishedAt) + lastSetCompletedAt = try c.decodeIfPresent(Date.self, forKey: .lastSetCompletedAt) + } + + func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + try c.encode(id, forKey: .id) + try c.encode(name, forKey: .name) + try c.encode(date, forKey: .date) + try c.encode(items, forKey: .items) + try c.encode(isActive, forKey: .isActive) + try c.encode(isTemplate, forKey: .isTemplate) + try c.encode(workoutNotes, forKey: .workoutNotes) + try c.encodeIfPresent(startedAt, forKey: .startedAt) + try c.encodeIfPresent(finishedAt, forKey: .finishedAt) + try c.encodeIfPresent(lastSetCompletedAt, forKey: .lastSetCompletedAt) + } +} + +// MARK: - Workout Store + +@MainActor +final class WorkoutStore: ObservableObject { + @Published var workouts: [Workout] = [] + @Published var activeWorkout: Workout? + @Published var selectedTab: Int = 0 + @Published var showingSession: Bool = false + @Published var favoriteTemplateNames: [String] = [] + @Published var archivedTemplateNames: [String] = [] + /// Fires when a set completion triggers a new PR. + @Published var latestPRs: [PersonalRecord] = [] + + /// Manual PR overrides: exercise name -> (weight, date set) + @Published var prOverrides: [String: PROverride] = [:] + private static let prOverridesKey = "pr_overrides_data" + + private static let saveFileURL: URL = { + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + return docs.appending(path: "workouts.json") + }() + + private static let favoritesKey = "favorite_template_names" + private static let archivedKey = "archived_template_names" + + init() { + if !loadSavedData() { + loadDemoData() + } + // Auto-finish any workout active for more than 24 hours + autoFinishStaleWorkouts() + // Load favorites from UserDefaults + if let saved = UserDefaults.standard.stringArray(forKey: Self.favoritesKey) { + favoriteTemplateNames = saved + } else { + favoriteTemplateNames = ["Bench", "Push", "Pull", "Legs"] + saveFavorites() + } + // Load archived + archivedTemplateNames = UserDefaults.standard.stringArray(forKey: Self.archivedKey) ?? [] + // Load PR overrides + loadPROverrides() + } + + /// Auto-finish any workout that has been active for more than 24 hours. + /// Truncates the finish time to when the last set was completed. + private func autoFinishStaleWorkouts() { + let now = Date() + var changed = false + for i in workouts.indices { + guard workouts[i].isActive, !workouts[i].isTemplate else { continue } + let start = workouts[i].startedAt ?? workouts[i].date + if now.timeIntervalSince(start) > 24 * 60 * 60 { + workouts[i].isActive = false + // Truncate: use lastSetCompletedAt as the finish time + workouts[i].finishedAt = workouts[i].lastSetCompletedAt ?? start + changed = true + } + } + if changed { + // Clear stale activeWorkout too + if let aw = activeWorkout, !workouts.contains(where: { $0.id == aw.id && $0.isActive }) { + activeWorkout = nil + } + save() + print("[WorkoutStore] Auto-finished stale workouts") + } + } + + // MARK: Demo Data + private func loadDemoData() { + let cal = Calendar.current + let today = Date() + + // Demo: "Bench" workout for today + let benchWorkout = Workout( + name: "Bench Tutorial", + date: today, + exercises: [ + Exercise( + name: "Bench Press", + equipment: .barbell, + muscleGroup: "Chest", + sets: [ + ExerciseSet(weight: "135", reps: "10", isComplete: true), + ExerciseSet(weight: "155", reps: "8", isComplete: true), + ExerciseSet(weight: "175", reps: "", isComplete: false) + ], + hasCoachModel: true, + coachMode: .live + ), + Exercise( + name: "Bench Demo", + equipment: .barbell, + muscleGroup: "Chest", + sets: [ + ExerciseSet() + ], + hasCoachModel: true, + coachMode: .demo + ) + ] + ) + + var demoWorkouts: [Workout] = [benchWorkout] + let templates: [(String, [String])] = [ + ("Push", ["Bench Press", "Incline Dumbbell Bench Press", "Cable Fly", "Dumbbell Shoulder Press", "Lateral Raise", "Rope Pushdown"]), + ("Pull", ["Deadlift", "Barbell Row", "Lat Pulldown", "Face Pull", "Barbell Curl"]), + ("Legs", ["Back Squat", "Leg Press", "Romanian Deadlift", "Leg Extension", "Standing Calf Raise Machine"]) + ] + + let daysBack = [2, 4, 6, 9, 11, 13, 16, 18, 20] + for (i, dayOffset) in daysBack.enumerated() { + if let d = cal.date(byAdding: .day, value: -dayOffset, to: today) { + let tpl = templates[i % templates.count] + let exercises = tpl.1.map { name in + let catalogEntry = ExerciseCatalog.exercises.first(where: { $0.name == name }) + return Exercise( + name: name, + equipment: catalogEntry?.equipment ?? .barbell, + muscleGroup: catalogEntry?.muscleGroup ?? "", + sets: [ExerciseSet(weight: "", reps: "", isComplete: false)] + ) + } + demoWorkouts.append(Workout(name: tpl.0, date: d, exercises: exercises)) + } + } + + workouts = demoWorkouts + appendDefaultTemplates(today: today, benchTutorial: true) + } + + // MARK: - Admin 16-Week Demo Data + + /// Loads 16 weeks of realistic PPL workout history for the admin test account. + func loadAdminDemoData() { + let cal = Calendar.current + let today = Date() + var allWorkouts: [Workout] = [] + + struct ExDef { + let name: String; let equip: EquipmentType; let group: String + let startWeight: Double; let weeklyGain: Double + let setCount: Int; let repTarget: Int; let hasCoach: Bool + } + + let pushMain: [ExDef] = [ + .init(name: "Bench Press", equip: .barbell, group: "Chest", startWeight: 135, weeklyGain: 2.5, setCount: 4, repTarget: 8, hasCoach: true), + .init(name: "Incline Dumbbell Bench Press", equip: .dumbbell, group: "Chest", startWeight: 50, weeklyGain: 2.5, setCount: 3, repTarget: 10, hasCoach: false), + .init(name: "Cable Fly", equip: .cable, group: "Chest", startWeight: 30, weeklyGain: 2.5, setCount: 3, repTarget: 12, hasCoach: false), + .init(name: "Dumbbell Shoulder Press", equip: .dumbbell, group: "Shoulders", startWeight: 40, weeklyGain: 2.5, setCount: 3, repTarget: 10, hasCoach: false), + ] + let pushSS: [ExDef] = [ + .init(name: "Lateral Raise", equip: .dumbbell, group: "Shoulders", startWeight: 15, weeklyGain: 1, setCount: 3, repTarget: 15, hasCoach: false), + .init(name: "Rope Pushdown", equip: .cable, group: "Arms", startWeight: 40, weeklyGain: 2.5, setCount: 3, repTarget: 12, hasCoach: false), + ] + let pullMain: [ExDef] = [ + .init(name: "Barbell Row", equip: .barbell, group: "Back", startWeight: 135, weeklyGain: 2.5, setCount: 4, repTarget: 8, hasCoach: false), + .init(name: "Lat Pulldown", equip: .cable, group: "Back", startWeight: 120, weeklyGain: 2.5, setCount: 3, repTarget: 10, hasCoach: false), + .init(name: "Seated Cable Row", equip: .cable, group: "Back", startWeight: 100, weeklyGain: 2.5, setCount: 3, repTarget: 10, hasCoach: false), + .init(name: "Face Pull", equip: .cable, group: "Shoulders", startWeight: 30, weeklyGain: 1, setCount: 3, repTarget: 15, hasCoach: false), + ] + let pullSS: [ExDef] = [ + .init(name: "Barbell Curl", equip: .barbell, group: "Arms", startWeight: 60, weeklyGain: 2.5, setCount: 3, repTarget: 10, hasCoach: false), + .init(name: "Hammer Curl", equip: .dumbbell, group: "Arms", startWeight: 25, weeklyGain: 1, setCount: 3, repTarget: 12, hasCoach: false), + ] + let legMain: [ExDef] = [ + .init(name: "Back Squat", equip: .barbell, group: "Legs", startWeight: 185, weeklyGain: 5, setCount: 4, repTarget: 6, hasCoach: false), + .init(name: "Romanian Deadlift", equip: .barbell, group: "Legs", startWeight: 155, weeklyGain: 5, setCount: 3, repTarget: 8, hasCoach: false), + .init(name: "Leg Press", equip: .machine, group: "Legs", startWeight: 270, weeklyGain: 10, setCount: 3, repTarget: 10, hasCoach: false), + ] + let legSS: [ExDef] = [ + .init(name: "Leg Extension", equip: .machine, group: "Legs", startWeight: 80, weeklyGain: 5, setCount: 3, repTarget: 12, hasCoach: false), + .init(name: "Lying Leg Curl", equip: .machine, group: "Legs", startWeight: 70, weeklyGain: 2.5, setCount: 3, repTarget: 12, hasCoach: false), + ] + let legExtra: [ExDef] = [ + .init(name: "Standing Calf Raise Machine", equip: .machine, group: "Legs", startWeight: 130, weeklyGain: 5, setCount: 4, repTarget: 15, hasCoach: false), + ] + + // Helpers + func mkEx(_ d: ExDef, w: Int, deload: Bool) -> Exercise { + var wt = d.startWeight + d.weeklyGain * Double(w - 1) + if deload { wt *= 0.7 } + let ws = wt.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", wt) : String(format: "%.1f", wt) + let sets: [ExerciseSet] = (0..= 2 ? d.repTarget - 1 : d.repTarget)) + return ExerciseSet(weight: ws, reps: "\(r)", isComplete: true) + } + return Exercise(name: d.name, equipment: d.equip, muscleGroup: d.group, sets: sets, hasCoachModel: d.hasCoach, coachMode: d.hasCoach ? .live : nil) + } + func mkSS(_ defs: [ExDef], w: Int, deload: Bool, label: String) -> WorkoutItem { + .superset(SupersetGroup(comment: label, notes: deload ? "Light — focus on form" : "", exercises: defs.map { mkEx($0, w: w, deload: deload) })) + } + + let pushNotes = ["Felt strong, paused reps on bench", "Shoulder tight during OHP", "PR attempt — nailed it", "Quick session", "", "Mind-muscle on flys", "", "Easy push day"] + let pullNotes = ["Back pumps crazy today", "Grip gave out on rows", "", "Extra set of face pulls", "Wider grip pulldowns felt great", "", "Squeezed curls at top", "Solid session"] + let legNotes = ["Squats felt heavy but moved", "Hamstrings still sore", "", "Hit depth on every rep", "RDLs smooth, good hip hinge", "Calves finally growing", "", ""] + + let weekPattern = [0, 1, 2, 0, 1, 2] + + for weeksAgo in (0..<16).reversed() { + let daysAgo = weeksAgo * 7 + guard let weekStart = cal.date(byAdding: .day, value: -daysAgo, to: today) else { continue } + let weekday = cal.component(.weekday, from: weekStart) + let daysToMonday = (weekday == 1) ? -6 : (2 - weekday) + guard let monday = cal.date(byAdding: .day, value: daysToMonday, to: weekStart) else { continue } + + let week = 16 - weeksAgo + let isDeload = (week % 4 == 0) + + for dayIndex in 0..<6 { + guard let workoutDate = cal.date(byAdding: .day, value: dayIndex, to: monday) else { continue } + if workoutDate > today { continue } + let seed = weeksAgo * 7 + dayIndex + if seed % 8 == 3 && !isDeload { continue } + + let splitIdx = weekPattern[dayIndex] + var items: [WorkoutItem] = [] + var wName = "" + var notes = "" + + let startHour = dayIndex < 3 ? 6 : 17 + guard let startTime = cal.date(bySettingHour: startHour, minute: (seed * 7) % 30, second: 0, of: workoutDate) else { continue } + let duration = isDeload ? 2700.0 : Double(3600 + (seed * 137) % 1800) + let endTime = startTime.addingTimeInterval(duration) + + switch splitIdx { + case 0: + wName = "Push"; notes = pushNotes[seed % pushNotes.count] + for d in pushMain { items.append(.exercise(mkEx(d, w: week, deload: isDeload))) } + items.append(mkSS(pushSS, w: week, deload: isDeload, label: "Burnout Superset")) + case 1: + wName = "Pull"; notes = pullNotes[seed % pullNotes.count] + for d in pullMain { items.append(.exercise(mkEx(d, w: week, deload: isDeload))) } + items.append(mkSS(pullSS, w: week, deload: isDeload, label: "Arm Superset")) + case 2: + wName = "Legs"; notes = legNotes[seed % legNotes.count] + for d in legMain { items.append(.exercise(mkEx(d, w: week, deload: isDeload))) } + items.append(mkSS(legSS, w: week, deload: isDeload, label: "Quad/Ham Superset")) + for d in legExtra { items.append(.exercise(mkEx(d, w: week, deload: isDeload))) } + default: break + } + + if isDeload { notes = "Deload week — \(notes.isEmpty ? "easy day" : notes.lowercased())" } + + allWorkouts.append(Workout( + name: wName, date: workoutDate, items: items, + workoutNotes: notes, + startedAt: startTime, finishedAt: endTime, + lastSetCompletedAt: endTime.addingTimeInterval(-60) + )) + } + } + + workouts = allWorkouts + appendDefaultTemplates(today: today, benchTutorial: true) + + let upperEx = ["Bench Press", "Barbell Row", "Dumbbell Shoulder Press", "Lat Pulldown", "Barbell Curl", "Rope Pushdown"] + let lowerEx = ["Back Squat", "Romanian Deadlift", "Leg Press", "Lying Leg Curl", "Standing Calf Raise Machine"] + + workouts.append(Workout(name: "Upper", date: today, + exercises: upperEx.map { Exercise(name: $0, sets: [ExerciseSet(), ExerciseSet(), ExerciseSet()]) }, isTemplate: true)) + workouts.append(Workout(name: "Lower", date: today, + exercises: lowerEx.map { Exercise(name: $0, sets: [ExerciseSet(), ExerciseSet(), ExerciseSet()]) }, isTemplate: true)) + + save() + print("✅ Admin demo data: \(allWorkouts.count) workouts over 16 weeks") + } + + // MARK: - Shared Template Creation + + private func appendDefaultTemplates(today: Date, benchTutorial: Bool) { + let templates: [(String, [String])] = [ + ("Push", ["Bench Press", "Incline Dumbbell Bench Press", "Cable Fly", "Dumbbell Shoulder Press", "Lateral Raise", "Rope Pushdown"]), + ("Pull", ["Barbell Row", "Lat Pulldown", "Seated Cable Row", "Face Pull", "Barbell Curl", "Hammer Curl"]), + ("Legs", ["Back Squat", "Romanian Deadlift", "Leg Press", "Leg Extension", "Lying Leg Curl", "Standing Calf Raise Machine"]) + ] + + if benchTutorial { + workouts.append(Workout( + name: "Bench Tutorial", + date: today, + exercises: [ + Exercise(name: "Bench Press", equipment: .barbell, muscleGroup: "Chest", + sets: [ExerciseSet(), ExerciseSet(), ExerciseSet()], + hasCoachModel: true, coachMode: .live), + Exercise(name: "Bench Demo", equipment: .barbell, muscleGroup: "Chest", + sets: [ExerciseSet()], + hasCoachModel: true, coachMode: .demo) + ], + isTemplate: true + )) + } + + for tpl in templates { + let exercises = tpl.1.map { name -> Exercise in + let cat = ExerciseCatalog.exercises.first(where: { $0.name == name }) + return Exercise( + name: name, + equipment: cat?.equipment ?? .barbell, + muscleGroup: cat?.muscleGroup ?? "", + sets: [ExerciseSet(), ExerciseSet(), ExerciseSet()] + ) + } + workouts.append(Workout(name: tpl.0, date: today, exercises: exercises, isTemplate: true)) + } + } + + // MARK: Queries + func workouts(on date: Date) -> [Workout] { + let cal = Calendar.current + return workouts.filter { !$0.isTemplate && cal.isDate($0.date, inSameDayAs: date) } + } + + func daysWithWorkouts(in month: Int, year: Int) -> Set { + let cal = Calendar.current + return Set(workouts.filter { !$0.isTemplate }.compactMap { w -> Int? in + let comps = cal.dateComponents([.year, .month, .day], from: w.date) + guard comps.year == year, comps.month == month else { return nil } + return comps.day + }) + } + + /// Status of workouts on a given day (for calendar icons). + enum DayWorkoutStatus { + case none + case active // at least one workout is in-progress + case finished // all workouts are finished (with completed sets) + case logged // has workouts but none completed + } + + func dayWorkoutStatus(day: Int, month: Int, year: Int) -> DayWorkoutStatus { + let cal = Calendar.current + let dayWorkouts = workouts.filter { w in + guard !w.isTemplate else { return false } + let comps = cal.dateComponents([.year, .month, .day], from: w.date) + return comps.year == year && comps.month == month && comps.day == day + } + guard !dayWorkouts.isEmpty else { return .none } + if dayWorkouts.contains(where: { $0.isActive }) { return .active } + if dayWorkouts.contains(where: { $0.hasCompletedSets }) { return .finished } + return .logged + } + + /// Returns past sessions for a named exercise, newest first. + func exerciseHistory(named exerciseName: String, limit: Int = 30) -> [(date: Date, workoutName: String, sets: [ExerciseSet])] { + let past = workouts.filter { !$0.isTemplate && !$0.isActive } + var results: [(date: Date, workoutName: String, sets: [ExerciseSet])] = [] + for workout in past { + if let ex = workout.exercises.first(where: { $0.name == exerciseName }) { + // Only include if at least one set has data + let hasData = ex.sets.contains { !$0.weight.isEmpty || !$0.reps.isEmpty } + if hasData { + results.append((date: workout.date, workoutName: workout.name, sets: ex.sets)) + } + } + } + results.sort { $0.date > $1.date } + return Array(results.prefix(limit)) + } + + /// Returns past completed workouts with the same name that have actual data, newest first. + func workoutHistory(named workoutName: String, limit: Int = 30) -> [Workout] { + workouts.filter { w in + !w.isTemplate && !w.isActive && w.name == workoutName && + w.exercises.contains { ex in + ex.sets.contains { !$0.weight.isEmpty || !$0.reps.isEmpty } + } + } + .sorted { $0.date > $1.date } + .prefix(limit) + .map { $0 } + } + + // MARK: Workout Lifecycle + func startWorkout(_ workout: Workout) { + var w = workout + w.isActive = true + w.startedAt = Date() + activeWorkout = w + } + + func startNewWorkout(name: String = "New Workout") { + let w = Workout(name: name, date: Date(), exercises: [], isActive: true, startedAt: Date()) + workouts.append(w) // persist immediately + activeWorkout = w + save() + } + + /// Create fresh copies of items with new IDs (for templates → workouts). + private func freshenItems(_ items: [WorkoutItem]) -> [WorkoutItem] { + items.map { item in + switch item { + case .exercise(let ex): + return .exercise(freshenExercise(ex)) + case .superset(let group): + let freshExercises = group.exercises.map { freshenExercise($0) } + return .superset(SupersetGroup(comment: group.comment, exercises: freshExercises)) + } + } + } + + private func freshenExercise(_ ex: Exercise) -> Exercise { + Exercise( + name: ex.name, + equipment: ex.equipment, + muscleGroup: ex.muscleGroup, + sets: ex.sets.map { _ in ExerciseSet() }, + notes: "", + hasCoachModel: ex.hasCoachModel, + coachMode: ex.coachMode + ) + } + + /// Start a workout based on a template, creating fresh Exercise/Set copies with new IDs. + /// Preserves superset structure. + func startWorkoutFromTemplate(_ template: Workout) { + let freshItems = freshenItems(template.items) + let w = Workout( + name: template.name, + date: Date(), + items: freshItems, + isActive: true, + startedAt: Date() + ) + workouts.append(w) // persist immediately — shows on calendar + activeWorkout = w + save() + } + + /// Resume an existing workout (re-activate it). + func resumeWorkout(_ workout: Workout) { + if let idx = workouts.firstIndex(where: { $0.id == workout.id }) { + workouts[idx].isActive = true + workouts[idx].startedAt = workouts[idx].startedAt ?? Date() + workouts[idx].finishedAt = nil // Clear so timer resumes + activeWorkout = workouts[idx] + save() + } + } + + func finishWorkout() { + if var w = activeWorkout { + w.isActive = false + w.finishedAt = Date() + // Update in-place if already in workouts array + if let idx = workouts.firstIndex(where: { $0.id == w.id }) { + workouts[idx] = w + } else { + workouts.append(w) + } + } + activeWorkout = nil + save() + } + + /// Record that a set was just completed (for duration tracking). + func recordSetCompletion(workoutId: UUID) { + let now = Date() + if let idx = workouts.firstIndex(where: { $0.id == workoutId }) { + workouts[idx].lastSetCompletedAt = now + // Auto-start if not started yet + if workouts[idx].startedAt == nil { + workouts[idx].startedAt = now + } + } + if activeWorkout?.id == workoutId { + activeWorkout?.lastSetCompletedAt = now + if activeWorkout?.startedAt == nil { + activeWorkout?.startedAt = now + } + } + } + + // MARK: Exercise/Set Management + func addExercise(name: String, equipment: EquipmentType = .barbell, muscleGroup: String = "", + hasCoachModel: Bool = false, coachMode: BenchSessionView.Mode? = nil) { + guard activeWorkout != nil else { return } + let ex = Exercise(name: name, equipment: equipment, muscleGroup: muscleGroup, + hasCoachModel: hasCoachModel, coachMode: coachMode) + activeWorkout?.items.append(.exercise(ex)) + } + + func addSet(to exerciseID: UUID) { + guard var aw = activeWorkout else { return } + aw.mutateExercise(id: exerciseID) { $0.sets.append(ExerciseSet()) } + activeWorkout = aw + } + + func toggleSet(exerciseID: UUID, setID: UUID) { + guard var aw = activeWorkout else { return } + aw.mutateExercise(id: exerciseID) { ex in + if let sIdx = ex.sets.firstIndex(where: { $0.id == setID }) { + ex.sets[sIdx].isComplete.toggle() + } + } + activeWorkout = aw + } + + /// Check if completing a set beat a PR. Call after set.isComplete becomes true. + func checkSetPR(exerciseName: String, weight: Double, reps: Int) { + guard weight > 0 else { return } + let effective = effectivePRs() + let currentBest = effective[exerciseName]?.weight ?? 0 + if weight > currentBest { + let pr = PersonalRecord(exerciseName: exerciseName, weight: weight, reps: reps, date: Date()) + latestPRs = [pr] + // Auto-bump override if one exists so badge updates immediately + if prOverrides[exerciseName] != nil { + setPROverride(exerciseName: exerciseName, weight: weight) + } + } + } + + // MARK: - PR Overrides + + struct PROverride: Codable { + let weight: Double + let dateSet: Date + } + + /// Effective PR for each exercise: max(override, post-override workout weight) + func effectivePRs() -> [String: PersonalRecord] { + let computedPRs = WorkoutStatsEngine.currentPRs(from: workouts) + var result = computedPRs + + for (name, override) in prOverrides { + // Find best weight from workouts logged AFTER the override date + let postOverrideBest = bestWeightAfter(exerciseName: name, afterDate: override.dateSet) + + let effectiveWeight = max(override.weight, postOverrideBest) + let effectiveReps = effectiveWeight == override.weight ? 0 : (computedPRs[name]?.reps ?? 0) + let effectiveDate = effectiveWeight == override.weight ? override.dateSet : (computedPRs[name]?.date ?? override.dateSet) + + result[name] = PersonalRecord( + exerciseName: name, + weight: effectiveWeight, + reps: effectiveReps, + date: effectiveDate + ) + } + + return result + } + + /// Best weight for an exercise from workouts after a given date. + private func bestWeightAfter(exerciseName: String, afterDate: Date) -> Double { + var best: Double = 0 + for w in workouts where !w.isTemplate && !w.isActive && w.date > afterDate { + for ex in w.exercises where ex.name == exerciseName { + for set in ex.sets where set.isComplete { + let wt = Double(set.weight) ?? 0 + if wt > best { best = wt } + } + } + } + return best + } + + /// Set a manual PR override. + func setPROverride(exerciseName: String, weight: Double) { + prOverrides[exerciseName] = PROverride(weight: weight, dateSet: Date()) + savePROverrides() + } + + /// Remove a PR override (revert to computed). + func clearPROverride(exerciseName: String) { + prOverrides.removeValue(forKey: exerciseName) + savePROverrides() + } + + private func savePROverrides() { + if let data = try? JSONEncoder().encode(prOverrides) { + UserDefaults.standard.set(data, forKey: Self.prOverridesKey) + } + } + + private func loadPROverrides() { + guard let data = UserDefaults.standard.data(forKey: Self.prOverridesKey), + let decoded = try? JSONDecoder().decode([String: PROverride].self, from: data) else { return } + prOverrides = decoded + } + + + + // MARK: Templates + + /// All unique template names (only workouts marked isTemplate), excluding archived. + var allTemplateNames: [String] { + let names = Set(workouts.filter { $0.isTemplate }.map { $0.name }) + return names.filter { !archivedTemplateNames.contains($0) }.sorted() + } + + /// Templates to display on Home screen — favorites only, limited by max setting. + var templates: [(name: String, count: Int)] { + let max = UserDefaults.standard.integer(forKey: SettingsKeys.maxTemplateShortcuts) + let limit = max == 0 ? 4 : max // default 4 if key not set + let grouped = Dictionary(grouping: workouts) { $0.name } + return favoriteTemplateNames.prefix(limit).compactMap { name in + guard let latest = grouped[name]?.last else { return nil } + return (name: name, count: latest.exercises.count) + } + } + + /// Full template list for AllTemplatesView: favorites first (in order), then remaining alphabetical. + var allTemplatesGrouped: (favorites: [(name: String, count: Int)], others: [(name: String, count: Int)]) { + let grouped = Dictionary(grouping: workouts) { $0.name } + let favs = favoriteTemplateNames.compactMap { name -> (name: String, count: Int)? in + guard let latest = grouped[name]?.last else { return nil } + return (name: name, count: latest.exercises.count) + } + let otherNames = allTemplateNames.filter { !favoriteTemplateNames.contains($0) } + let others = otherNames.compactMap { name -> (name: String, count: Int)? in + guard let latest = grouped[name]?.last else { return nil } + return (name: name, count: latest.exercises.count) + } + return (favorites: favs, others: others) + } + + func isFavorite(_ name: String) -> Bool { + favoriteTemplateNames.contains(name) + } + + /// Toggle favorite status. Returns false if at max and trying to add. + func toggleFavorite(_ name: String) -> Bool { + if let idx = favoriteTemplateNames.firstIndex(of: name) { + favoriteTemplateNames.remove(at: idx) + saveFavorites() + return true + } else { + let max = UserDefaults.standard.integer(forKey: SettingsKeys.maxTemplateShortcuts) + let limit = max == 0 ? 10 : max + if favoriteTemplateNames.count >= limit { + return false // at max + } + favoriteTemplateNames.append(name) + saveFavorites() + return true + } + } + + func moveFavorite(from source: IndexSet, to destination: Int) { + favoriteTemplateNames.move(fromOffsets: source, toOffset: destination) + saveFavorites() + } + + private func saveFavorites() { + UserDefaults.standard.set(favoriteTemplateNames, forKey: Self.favoritesKey) + } + + private func saveArchived() { + UserDefaults.standard.set(archivedTemplateNames, forKey: Self.archivedKey) + } + + /// Archive a template (soft delete — hidden but restorable). + func archiveTemplate(name: String) { + favoriteTemplateNames.removeAll { $0 == name } + saveFavorites() + if !archivedTemplateNames.contains(name) { + archivedTemplateNames.append(name) + saveArchived() + } + } + + /// Permanently delete a template (removes all workout entries with that name that are templates). + func deleteTemplate(name: String) { + favoriteTemplateNames.removeAll { $0 == name } + saveFavorites() + archivedTemplateNames.removeAll { $0 == name } + saveArchived() + workouts.removeAll { $0.isTemplate && $0.name == name } + save() + } + + /// Restore an archived template. + func restoreTemplate(name: String) { + archivedTemplateNames.removeAll { $0 == name } + saveArchived() + // Auto-favorite if under limit + let max = UserDefaults.standard.integer(forKey: SettingsKeys.maxTemplateShortcuts) + let limit = max == 0 ? 10 : max + if favoriteTemplateNames.count < limit { + favoriteTemplateNames.append(name) + saveFavorites() + } + } + + /// Trim favorites list to the given max, removing from the bottom. + func trimFavorites(to max: Int) { + if favoriteTemplateNames.count > max { + favoriteTemplateNames = Array(favoriteTemplateNames.prefix(max)) + saveFavorites() + } + } + + // MARK: Template CRUD + + /// Get the latest workout with a given template name. + func templateWorkout(named name: String) -> Workout? { + workouts.last(where: { $0.name == name }) + } + + /// Add a workout to a specific date based on a template. Preserves superset structure. + func addWorkoutFromTemplate(templateName: String, date: Date) { + guard let tpl = workouts.last(where: { $0.name == templateName && $0.isTemplate }) else { return } + let freshItems = freshenItems(tpl.items) + let w = Workout(name: tpl.name, date: date, items: freshItems) + workouts.append(w) + save() + } + + /// Create a new template (saved as a workout). + func createTemplate(name: String, exercises: [Exercise]) { + let workout = Workout(name: name, date: Date(), exercises: exercises, isTemplate: true) + workouts.append(workout) + save() + // Auto-favorite if under limit + let max = UserDefaults.standard.integer(forKey: SettingsKeys.maxTemplateShortcuts) + let limit = max == 0 ? 10 : max + if favoriteTemplateNames.count < limit && !favoriteTemplateNames.contains(name) { + favoriteTemplateNames.append(name) + saveFavorites() + } + } + + /// Update the exercises for a template. + func updateTemplate(name: String, exercises: [Exercise]) { + let workout = Workout(name: name, date: Date(), exercises: exercises, isTemplate: true) + workouts.append(workout) + save() + } + + /// Save an existing workout as a template. Preserves superset structure. + func saveAsTemplate(_ workout: Workout) { + let tpl = Workout(name: workout.name, date: Date(), items: workout.items, isTemplate: true) + workouts.append(tpl) + save() + // Auto-favorite if under limit + let max = UserDefaults.standard.integer(forKey: SettingsKeys.maxTemplateShortcuts) + let limit = max == 0 ? 10 : max + if favoriteTemplateNames.count < limit && !favoriteTemplateNames.contains(workout.name) { + favoriteTemplateNames.append(workout.name) + saveFavorites() + } + } + + /// Check if a template with this name already exists. + func hasTemplate(named name: String) -> Bool { + workouts.contains(where: { $0.isTemplate && $0.name == name }) + } + + /// Update exercises on a specific workout (by ID). + func updateWorkout(id: UUID, exercises: [Exercise]) { + if let idx = workouts.firstIndex(where: { $0.id == id }) { + workouts[idx].items = exercises.map { .exercise($0) } + save() + } + } + + /// Update items on a specific workout (by ID) — preserves supersets. + func updateWorkout(id: UUID, items: [WorkoutItem]) { + if let idx = workouts.firstIndex(where: { $0.id == id }) { + workouts[idx].items = items + save() + } + } + + // MARK: Calendar Workout Actions + + /// Delete a workout by its ID. + func deleteWorkout(id: UUID) { + workouts.removeAll { $0.id == id } + save() + } + + /// Copy a workout to a different date. Preserves superset structure. + func copyWorkout(id: UUID, to newDate: Date) { + guard let original = workouts.first(where: { $0.id == id }) else { return } + let copy = Workout(name: original.name, date: newDate, items: original.items) + workouts.append(copy) + save() + } + + /// Move a workout to a different date. + func moveWorkout(id: UUID, to newDate: Date) { + guard let idx = workouts.firstIndex(where: { $0.id == id }) else { return } + workouts[idx].date = newDate + save() + } + + // MARK: Persistence + func save() { + do { + let data = try JSONEncoder().encode(workouts) + try data.write(to: Self.saveFileURL, options: .atomic) + // Auto-backup full user data for future cloud sync + DataBackupManager.shared.saveBackup(from: self) + } catch { + print("[WorkoutStore] Save failed: \(error)") + } + } + + @discardableResult + private func loadSavedData() -> Bool { + guard FileManager.default.fileExists(atPath: Self.saveFileURL.path) else { return false } + do { + let data = try Data(contentsOf: Self.saveFileURL) + workouts = try JSONDecoder().decode([Workout].self, from: data) + // Restore active workout from persisted data + activeWorkout = workouts.first(where: { $0.isActive && !$0.isTemplate }) + return true + } catch { + print("[WorkoutStore] Load failed: \(error)") + return false + } + } +} diff --git a/App Core/Resources/Swift Code/WorkoutStatsEngine.swift b/App Core/Resources/Swift Code/WorkoutStatsEngine.swift new file mode 100644 index 0000000..11c7c42 --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutStatsEngine.swift @@ -0,0 +1,337 @@ +// WorkoutStatsEngine.swift +// mAI Coach — Computes workout statistics and detects personal records. + +import Foundation + +// MARK: - Data Models + +/// A personal record for a specific exercise. +struct PersonalRecord: Codable, Equatable { + let exerciseName: String + let weight: Double + let reps: Int + let date: Date +} + +/// Stats summary for display. +struct QuickStats { + let totalWorkouts: Int + let totalDaysInGym: Int + let totalSets: Int + let totalReps: Int + let totalWeightLifted: Double // lbs + let currentStreak: Int // consecutive weeks with ≥1 workout + let favoriteExercise: String? // most performed exercise + let prsThisMonth: Int +} + +/// History entry for a single exercise across time. +struct ExerciseHistoryEntry { + let date: Date + let bestWeight: Double + let bestReps: Int + let totalSets: Int +} + +// MARK: - Engine + +final class WorkoutStatsEngine { + + /// Compute quick stats from all workouts. + static func quickStats(from workouts: [Workout]) -> QuickStats { + let completed = workouts.filter { !$0.isTemplate && !$0.isActive } + + let totalWorkouts = completed.count + + // Unique days in gym + let cal = Calendar.current + let uniqueDays = Set(completed.map { cal.startOfDay(for: $0.date) }) + let totalDaysInGym = uniqueDays.count + + // Totals + var totalSets = 0 + var totalReps = 0 + var totalWeight: Double = 0 + var exerciseCounts: [String: Int] = [:] + + for w in completed { + for ex in w.exercises { + exerciseCounts[ex.name, default: 0] += 1 + for set in ex.sets where set.isComplete { + totalSets += 1 + let r = Int(set.reps) ?? 0 + let wt = Double(set.weight) ?? 0 + totalReps += r + totalWeight += wt * Double(r) // volume = weight × reps + } + } + } + + let favoriteExercise = exerciseCounts.max(by: { $0.value < $1.value })?.key + + // Streak (consecutive weeks with ≥1 workout) + let currentStreak = computeWeekStreak(dates: uniqueDays.sorted(), calendar: cal) + + // PRs this month + let monthStart = cal.date(from: cal.dateComponents([.year, .month], from: Date()))! + // monthWorkouts could be used for future stats + let allPRs = detectAllPRs(from: completed) + let monthPRDates = allPRs.filter { $0.date >= monthStart } + let prsThisMonth = monthPRDates.count + + return QuickStats( + totalWorkouts: totalWorkouts, + totalDaysInGym: totalDaysInGym, + totalSets: totalSets, + totalReps: totalReps, + totalWeightLifted: totalWeight, + currentStreak: currentStreak, + favoriteExercise: favoriteExercise, + prsThisMonth: prsThisMonth + ) + } + + // MARK: - Week Streak + + private static func computeWeekStreak(dates: [Date], calendar: Calendar) -> Int { + guard !dates.isEmpty else { return 0 } + let today = Date() + let currentWeek = calendar.component(.weekOfYear, from: today) + let currentYear = calendar.component(.yearForWeekOfYear, from: today) + + // Build set of (year, weekOfYear) with workouts + var weekSet: Set = [] + for d in dates { + let w = calendar.component(.weekOfYear, from: d) + let y = calendar.component(.yearForWeekOfYear, from: d) + weekSet.insert("\(y)-\(w)") + } + + var streak = 0 + var checkYear = currentYear + var checkWeek = currentWeek + + while weekSet.contains("\(checkYear)-\(checkWeek)") { + streak += 1 + // Go to previous week + if let prevWeekDate = calendar.date(byAdding: .weekOfYear, value: -1, + to: calendar.date(from: DateComponents(weekOfYear: checkWeek, yearForWeekOfYear: checkYear))!) { + checkWeek = calendar.component(.weekOfYear, from: prevWeekDate) + checkYear = calendar.component(.yearForWeekOfYear, from: prevWeekDate) + } else { + break + } + } + return streak + } + + // MARK: - PR Detection + + /// Detect all personal records across all workouts (best weight per exercise). + static func detectAllPRs(from workouts: [Workout]) -> [PersonalRecord] { + let completed = workouts.filter { !$0.isTemplate && !$0.isActive } + var bestByExercise: [String: PersonalRecord] = [:] + + // Process chronologically + let sorted = completed.sorted { $0.date < $1.date } + var allPRs: [PersonalRecord] = [] + + for w in sorted { + for ex in w.exercises { + for set in ex.sets where set.isComplete { + let weight = Double(set.weight) ?? 0 + let reps = Int(set.reps) ?? 0 + guard weight > 0 else { continue } + + let current = bestByExercise[ex.name] + if current == nil || weight > current!.weight { + let pr = PersonalRecord(exerciseName: ex.name, weight: weight, reps: reps, date: w.date) + bestByExercise[ex.name] = pr + allPRs.append(pr) + } + } + } + } + return allPRs + } + + /// Get the current best PR for each exercise. + static func currentPRs(from workouts: [Workout]) -> [String: PersonalRecord] { + let completed = workouts.filter { !$0.isTemplate && !$0.isActive } + var bestByExercise: [String: PersonalRecord] = [:] + + for w in completed { + for ex in w.exercises { + for set in ex.sets where set.isComplete { + let weight = Double(set.weight) ?? 0 + let reps = Int(set.reps) ?? 0 + guard weight > 0 else { continue } + + if let current = bestByExercise[ex.name] { + if weight > current.weight { + bestByExercise[ex.name] = PersonalRecord(exerciseName: ex.name, weight: weight, reps: reps, date: w.date) + } + } else { + bestByExercise[ex.name] = PersonalRecord(exerciseName: ex.name, weight: weight, reps: reps, date: w.date) + } + } + } + } + return bestByExercise + } + + /// Check if a workout contains any new PRs compared to historical data. + /// Returns the list of exercises where a new PR was set. + static func newPRs(in workout: Workout, against history: [Workout]) -> [PersonalRecord] { + // Get PRs from all workouts EXCEPT this one + let prior = history.filter { $0.id != workout.id && !$0.isTemplate && !$0.isActive } + let oldPRs = currentPRs(from: prior) + + var newRecords: [PersonalRecord] = [] + for ex in workout.exercises { + for set in ex.sets where set.isComplete { + let weight = Double(set.weight) ?? 0 + let reps = Int(set.reps) ?? 0 + guard weight > 0 else { continue } + + let oldBest = oldPRs[ex.name]?.weight ?? 0 + if weight > oldBest { + newRecords.append(PersonalRecord(exerciseName: ex.name, weight: weight, reps: reps, date: workout.date)) + } + } + } + // Deduplicate by exercise name (keep best in this workout) + var best: [String: PersonalRecord] = [:] + for pr in newRecords { + if let existing = best[pr.exerciseName] { + if pr.weight > existing.weight { best[pr.exerciseName] = pr } + } else { + best[pr.exerciseName] = pr + } + } + return Array(best.values) + } + + // MARK: - Exercise History + + /// Get history for a specific exercise over time. + static func exerciseHistory(name: String, from workouts: [Workout]) -> [ExerciseHistoryEntry] { + let completed = workouts.filter { !$0.isTemplate && !$0.isActive } + var entries: [ExerciseHistoryEntry] = [] + + for w in completed.sorted(by: { $0.date < $1.date }) { + for ex in w.exercises where ex.name == name { + var bestWeight: Double = 0 + var bestReps = 0 + var setCount = 0 + for set in ex.sets where set.isComplete { + let wt = Double(set.weight) ?? 0 + let r = Int(set.reps) ?? 0 + if wt > bestWeight { bestWeight = wt; bestReps = r } + setCount += 1 + } + if setCount > 0 { + entries.append(ExerciseHistoryEntry(date: w.date, bestWeight: bestWeight, bestReps: bestReps, totalSets: setCount)) + } + } + } + return entries + } + + /// All unique exercise names from completed workouts, sorted by frequency. + static func allExerciseNames(from workouts: [Workout]) -> [String] { + let completed = workouts.filter { !$0.isTemplate && !$0.isActive } + var counts: [String: Int] = [:] + for w in completed { + for ex in w.exercises { + counts[ex.name, default: 0] += 1 + } + } + return counts.sorted { $0.value > $1.value }.map { $0.key } + } + + // MARK: - Weekly Volume + + /// Daily volume for the last 7 days (for bar chart). + static func weeklyVolume(from workouts: [Workout]) -> [DailyVolume] { + let cal = Calendar.current + let today = cal.startOfDay(for: Date()) + let completed = workouts.filter { !$0.isTemplate } + + var result: [DailyVolume] = [] + for offset in (0..<7).reversed() { + guard let day = cal.date(byAdding: .day, value: -offset, to: today) else { continue } + let dayEnd = cal.date(byAdding: .day, value: 1, to: day)! + var volume: Double = 0 + var sets = 0 + for w in completed { + let wDay = cal.startOfDay(for: w.date) + if wDay >= day && wDay < dayEnd { + for ex in w.exercises { + for s in ex.sets { + let weight = Double(s.weight) ?? 0 + let reps = Double(s.reps) ?? 0 + volume += weight * reps + if s.isComplete { sets += 1 } + } + } + } + } + result.append(DailyVolume(date: day, volume: volume, sets: sets)) + } + return result + } + + // MARK: - Per-Exercise Quick Stats + + /// Quick stats for a specific exercise. + static func exerciseQuickStats(name: String, from workouts: [Workout]) -> ExerciseQuickStatsData { + let history = exerciseHistory(name: name, from: workouts) + let startingWeight = history.first?.bestWeight ?? 0 + let currentWeight = history.last?.bestWeight ?? 0 + let totalSessions = history.count + let pr = currentPRs(from: workouts)[name] + let totalVolume: Double = history.reduce(0) { acc, entry in + acc + entry.bestWeight * Double(entry.bestReps) + } + return ExerciseQuickStatsData( + startingWeight: startingWeight, + currentWeight: currentWeight, + pr: pr, + totalSessions: totalSessions, + totalVolume: totalVolume + ) + } +} + +// MARK: - Supporting Types + +struct DailyVolume: Identifiable { + let id = UUID() + let date: Date + let volume: Double + let sets: Int + + var dayLabel: String { + let fmt = DateFormatter() + fmt.dateFormat = "E" + return fmt.string(from: date) + } +} + +struct ExerciseQuickStatsData { + let startingWeight: Double + let currentWeight: Double + let pr: PersonalRecord? + let totalSessions: Int + let totalVolume: Double + + var weightChange: Double { + currentWeight - startingWeight + } + + var weightChangePercent: Double { + guard startingWeight > 0 else { return 0 } + return (weightChange / startingWeight) * 100 + } +} diff --git a/App Core/Resources/Swift Code/WorkoutTemplatePickerSheet.swift b/App Core/Resources/Swift Code/WorkoutTemplatePickerSheet.swift new file mode 100644 index 0000000..f48c03f --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutTemplatePickerSheet.swift @@ -0,0 +1,97 @@ +// WorkoutTemplatePickerSheet.swift +// mAI Coach — Pick a template to start a new workout from. + +import SwiftUI + +struct WorkoutTemplatePickerSheet: View { + @EnvironmentObject var store: WorkoutStore + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + Group { + if store.allTemplateNames.isEmpty { + emptyState + } else { + templateList + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .navigationTitle("Start From Template") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Cancel") { dismiss() } + .foregroundStyle(AppColors.accentGold) + } + } + } + } + + private var emptyState: some View { + VStack(spacing: 12) { + Spacer() + Image(systemName: "doc.text") + .font(.system(size: 48)) + .foregroundStyle(AppColors.textTertiary) + .opacity(0.5) + Text("No Templates") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + Text("Create a template first to start a workout from it.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + } + + private var templateList: some View { + ScrollView { + VStack(spacing: 12) { + ForEach(store.allTemplateNames, id: \.self) { name in + Button { + if let w = store.workouts.last(where: { $0.isTemplate && $0.name == name }) { + store.startWorkoutFromTemplate(w) + } + dismiss() + } label: { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 10) + .fill(AppColors.accentMuted) + .frame(width: 40, height: 40) + .overlay( + Image(systemName: "figure.strengthtraining.traditional") + .font(.system(size: 18)) + .foregroundStyle(AppColors.accentGold) + ) + + VStack(alignment: .leading, spacing: 2) { + Text(name) + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.textPrimary) + if let tpl = store.templateWorkout(named: name) { + Text("\(tpl.exercises.count) exercises") + .font(AppFonts.footnote()) + .foregroundStyle(AppColors.textSecondary) + } + } + + Spacer() + + Image(systemName: "play.fill") + .font(.system(size: 14)) + .foregroundStyle(AppColors.accentGold) + } + .padding(14) + .cardStyle() + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, AppSpacing.screenPadding) + .padding(.top, 8) + } + } +} diff --git a/App Core/Resources/Swift Code/WorkoutView.swift b/App Core/Resources/Swift Code/WorkoutView.swift new file mode 100644 index 0000000..0f7a862 --- /dev/null +++ b/App Core/Resources/Swift Code/WorkoutView.swift @@ -0,0 +1,108 @@ +// WorkoutView.swift +// mAI Coach — Workout tab (middle tab). +// Shows empty state or embeds WorkoutDetailView for the active workout. + +import SwiftUI + +struct WorkoutView: View { + @EnvironmentObject var store: WorkoutStore + + // Template picker for empty state + @State private var showTemplatePicker = false + + // Naming flow + @State private var showNameAlert = false + @State private var newWorkoutName = "" + + var body: some View { + NavigationStack { + Group { + if let active = store.activeWorkout { + WorkoutDetailView(workoutId: active.id, embedded: true) + } else { + emptyStateView + .tutorialSpotlight("tutorial_workoutTab") + } + } + .background(AppColors.bgPrimary.ignoresSafeArea()) + .alert("Name Your Workout", isPresented: $showNameAlert) { + TextField("e.g. Push Day, Leg Day", text: $newWorkoutName) + Button("Start") { + let name = newWorkoutName.trimmingCharacters(in: .whitespacesAndNewlines) + store.startNewWorkout(name: name.isEmpty ? "New Workout" : name) + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Give your workout a name") + } + } + } + + // MARK: - Empty State + private var emptyStateView: some View { + VStack(spacing: 12) { + Text("Workout") + .font(AppFonts.displayLarge()) + .foregroundStyle(AppColors.textPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, AppSpacing.screenPadding) + + Spacer() + + Image(systemName: "dumbbell") + .font(.system(size: 56)) + .foregroundStyle(AppColors.textTertiary) + .opacity(0.4) + + Text("No Active Workout") + .font(AppFonts.headline()) + .foregroundStyle(AppColors.textPrimary) + + Text("Start a new workout or pick one from your templates.") + .font(AppFonts.subhead()) + .foregroundStyle(AppColors.textSecondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + + VStack(spacing: 10) { + Button { + newWorkoutName = "" + showNameAlert = true + } label: { + Text("New Workout") + .font(AppFonts.bodyBold()) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentGold) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + } + .tutorialSpotlight("tutorial_startWorkout") + + Button { + showTemplatePicker = true + } label: { + Text("Start From Template") + .font(AppFonts.bodyBold()) + .foregroundStyle(AppColors.accentGold) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(AppColors.accentMuted) + .clipShape(RoundedRectangle(cornerRadius: AppSpacing.buttonRadius)) + .overlay( + RoundedRectangle(cornerRadius: AppSpacing.buttonRadius) + .stroke(AppColors.accentGold, lineWidth: 1) + ) + } + } + .padding(.horizontal, 40) + .padding(.top, 8) + + Spacer() + } + .sheet(isPresented: $showTemplatePicker) { + WorkoutTemplatePickerSheet() + .environmentObject(store) + } + } +} diff --git a/App Core/Resources/Swift Code/exercise_library.json b/App Core/Resources/Swift Code/exercise_library.json new file mode 100644 index 0000000..3db7e42 --- /dev/null +++ b/App Core/Resources/Swift Code/exercise_library.json @@ -0,0 +1,11536 @@ +{ + "_schema_version": "3.0", + "_schema": { + "name": "Exercise display name", + "muscleGroup": "Chest | Back | Shoulders | Arms | Legs | Core | Other", + "equipment": "barbell | dumbbell | cable | machine | bodyweight | kettlebell | resistanceBand | medicineBall | plate | suspension | stabilityBall | abWheel | trapBar | landmine | sled | battleRopes | box | pullUpBar | dipStation | ezCurlBar | cardioMachine | other", + "coachMode": "(optional) live | demo", + "videoURL": "(optional) URL to form demo", + "secondaryMuscles": "(optional) array", + "difficulty": "beginner | intermediate | advanced", + "isUnilateral": "bool", + "isCompound": "bool", + "cableAttachment": "(optional) rope | straightBar | vBar | ezBar | dHandle | wideBar | ankleStrap", + "notes": "(optional) string", + "meta": "(optional) dict" + }, + "exercises": [ + { + "name": "Bench Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": "live", + "videoURL": "https://www.youtube.com/watch?v=hWbUlkb5Ms4", + "secondaryMuscles": [ + "triceps", + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Bench Demo", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": "demo", + "videoURL": "https://www.youtube.com/watch?v=hWbUlkb5Ms4", + "secondaryMuscles": [ + "triceps", + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Incline Bench Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=L9UKMQw1Nss", + "secondaryMuscles": [ + "front delt", + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press a weight upward while lying on an incline bench set to 30-45 degrees to target the upper chest.", + "primaryMuscles": [ + "upper pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Set bench to 30-45 degrees", + "Retract shoulder blades into the bench", + "Lower to upper chest/collarbone area", + "Press up in a slight arc", + "Keep feet flat on floor" + ], + "commonMistakes": [ + "Bench angle too steep (becomes shoulder press)", + "Flaring elbows excessively", + "Not controlling the negative", + "Arching back off the bench" + ] + }, + { + "name": "Decline Bench Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=a-UFQE4oxWY", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press a weight upward while lying on a decline bench to target the lower chest.", + "primaryMuscles": [ + "lower pectoralis major", + "triceps" + ], + "formCues": [ + "Secure legs under pads", + "Lower bar to lower chest", + "Press up and slightly back", + "Keep shoulder blades retracted" + ], + "commonMistakes": [ + "Using too steep a decline", + "Not securing legs properly", + "Bouncing weight off chest" + ] + }, + { + "name": "Close Grip Bench Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vEUyEOVn3yM", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bench press with a narrow grip to emphasize triceps over chest.", + "primaryMuscles": [ + "triceps brachii", + "pectoralis major" + ], + "formCues": [ + "Grip shoulder-width or slightly narrower", + "Keep elbows close to body", + "Lower to lower chest", + "Press up to full lockout" + ], + "commonMistakes": [ + "Grip too narrow (wrist strain)", + "Flaring elbows outward", + "Not locking out" + ] + }, + { + "name": "Wide Grip Bench Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hWbUlkb5Ms4", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Barbell Floor Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=T2gXB8DvTvY", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Pause Bench Press", + "muscleGroup": "Chest", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hWbUlkb5Ms4", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Dumbbell Bench Press", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1V3vpcaxRYQ", + "secondaryMuscles": [ + "triceps", + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Incline Dumbbell Bench Press", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ou6s32mJgjU", + "secondaryMuscles": [ + "front delt", + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Decline Dumbbell Bench Press", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Pf1nDoqx_1A", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Dumbbell Fly", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=rk8YayRoTRQ", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Incline Dumbbell Fly", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=idAvu2HvqSQ", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Decline Dumbbell Fly", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=rk8YayRoTRQ", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Dumbbell Squeeze Press", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=G2j_lJf6ljk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Dumbbell Pullover", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=QA8rwwiQiW0", + "secondaryMuscles": [ + "lats" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using dumbbells targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Floor Press", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=UBmpZ7l5Nlk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Single Arm Dumbbell Press", + "muscleGroup": "Chest", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=KLSYjgShghk", + "secondaryMuscles": [ + "core" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Cable Fly", + "muscleGroup": "Chest", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGXIQR89-JE", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Standing between cable pulleys, bring handles together in front of your chest in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Slight forward lean", + "Keep a slight bend in elbows", + "Squeeze chest at the peak contraction", + "Control the return slowly" + ], + "commonMistakes": [ + "Using too much weight and bending arms excessively", + "Swinging the body for momentum", + "Not squeezing at the contraction point" + ] + }, + { + "name": "Cable Crossover", + "muscleGroup": "Chest", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGXIQR89-JE", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise on a cable machine targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Low Cable Fly", + "muscleGroup": "Chest", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGXIQR89-JE", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "High Cable Fly", + "muscleGroup": "Chest", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGXIQR89-JE", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Single Arm Cable Fly", + "muscleGroup": "Chest", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=E_mT1JWOp90", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Cable Chest Press", + "muscleGroup": "Chest", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGXIQR89-JE", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push handles forward on a machine while seated to work the chest muscles.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Adjust seat so handles are at chest height", + "Press forward and squeeze", + "Control the return", + "Keep back against pad" + ], + "commonMistakes": [ + "Seat too high or low", + "Not using full range of motion", + "Pushing unevenly" + ] + }, + { + "name": "Chest Press Machine", + "muscleGroup": "Chest", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=WxrKIPbeQP8", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push handles forward on a machine while seated to work the chest muscles.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Adjust seat so handles are at chest height", + "Press forward and squeeze", + "Control the return", + "Keep back against pad" + ], + "commonMistakes": [ + "Seat too high or low", + "Not using full range of motion", + "Pushing unevenly" + ] + }, + { + "name": "Incline Chest Press Machine", + "muscleGroup": "Chest", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=WxrKIPbeQP8", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push handles forward on a machine while seated to work the chest muscles.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Adjust seat so handles are at chest height", + "Press forward and squeeze", + "Control the return", + "Keep back against pad" + ], + "commonMistakes": [ + "Seat too high or low", + "Not using full range of motion", + "Pushing unevenly" + ] + }, + { + "name": "Pec Deck", + "muscleGroup": "Chest", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=fgXSA2-o0NM", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit in the machine and bring padded arms together in front of your chest.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Adjust seat so arms are at chest level", + "Squeeze chest at peak contraction", + "Control the negative", + "Keep back against pad" + ], + "commonMistakes": [ + "Using too much weight", + "Not squeezing at contraction", + "Leaning forward off the pad" + ] + }, + { + "name": "Smith Machine Bench Press", + "muscleGroup": "Chest", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=E4G-M8Vvzps", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a flat bench and press a barbell upward from chest level to full arm extension.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Retract and depress shoulder blades", + "Plant feet firmly on the floor", + "Lower bar to mid-chest with control", + "Press up and slightly back", + "Maintain a slight arch in lower back" + ], + "commonMistakes": [ + "Flaring elbows too wide (past 90°)", + "Bouncing bar off chest", + "Lifting hips off the bench", + "Not locking out at the top", + "Uneven bar path" + ] + }, + { + "name": "Smith Machine Incline Press", + "muscleGroup": "Chest", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ohRa_YRmVCk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Push-Up", + "muscleGroup": "Chest", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_YrJc-kTYA0", + "secondaryMuscles": [ + "triceps", + "front delt" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using bodyweight only targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Incline Push-Up", + "muscleGroup": "Chest", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=cfns5VDVVvk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using bodyweight only targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Decline Push-Up", + "muscleGroup": "Chest", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5QFjmotLfW4", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using bodyweight only targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Diamond Push-Up", + "muscleGroup": "Chest", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=PPTj-MW2tcs", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using bodyweight only targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Wide Push-Up", + "muscleGroup": "Chest", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=TEGkpxBvU_Y", + "secondaryMuscles": [ + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using bodyweight only targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Clap Push-Up", + "muscleGroup": "Chest", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8YyWPS6Jm-0", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A chest exercise using bodyweight only targeting the chest muscles.", + "primaryMuscles": [ + "chest" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Chest Dip", + "muscleGroup": "Chest", + "equipment": "dipStation", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yN6Q1UI_xkE", + "secondaryMuscles": [ + "triceps", + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on parallel bars and lower your body by bending elbows, then press back up.", + "primaryMuscles": [ + "pectoralis major", + "triceps", + "anterior deltoid" + ], + "formCues": [ + "Lean slightly forward for chest emphasis", + "Lower until upper arms are parallel to floor", + "Press up to full lockout", + "Keep core engaged" + ], + "commonMistakes": [ + "Going too deep and straining shoulders", + "Staying too upright (shifts to triceps only)", + "Swinging legs for momentum" + ] + }, + { + "name": "Landmine Press", + "muscleGroup": "Chest", + "equipment": "landmine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=gH7PDepHNck", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Resistance Band Fly", + "muscleGroup": "Chest", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGXIQR89-JE", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Kettlebell Floor Press", + "muscleGroup": "Chest", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4ULa6AJcjr8", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Deadlift", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vfKwjT5-86k", + "secondaryMuscles": [ + "glutes", + "hamstrings", + "core" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Sumo Deadlift", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=e7oLkRlT2CQ", + "secondaryMuscles": [ + "glutes", + "quads" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Barbell Row", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Nqh7q3zDCoQ", + "secondaryMuscles": [ + "biceps", + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Pendlay Row", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tYxEGi7ir4I", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "T-Bar Row", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8c23NBbwLBc", + "secondaryMuscles": [ + "biceps", + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Straddle a T-bar and pull the weighted end toward your chest while bent over.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius" + ], + "formCues": [ + "Keep chest up and back flat", + "Pull toward upper abdomen", + "Squeeze at the top", + "Control the negative" + ], + "commonMistakes": [ + "Rounding the back", + "Using momentum", + "Standing too upright" + ] + }, + { + "name": "Barbell Shrug", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MlqHEfydPpE", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Rack Pull", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=aAjN8zS7Idg", + "secondaryMuscles": [ + "traps", + "glutes" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hang from a bar with palms facing away and pull your body up until chin clears the bar.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Start from a dead hang", + "Pull elbows down and back", + "Get chin over the bar", + "Lower with control", + "Engage core to prevent swinging" + ], + "commonMistakes": [ + "Kipping or using momentum", + "Not going to full dead hang", + "Half reps (chin not clearing bar)" + ] + }, + { + "name": "Good Morning", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=SqJJygksQYY", + "secondaryMuscles": [ + "hamstrings", + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "With a barbell on your back, hinge forward at the hips until torso is nearly parallel to the floor.", + "primaryMuscles": [ + "hamstrings", + "erector spinae", + "glutes" + ], + "formCues": [ + "Bar on upper back like a squat", + "Push hips back", + "Slight knee bend", + "Keep back flat", + "Hinge until you feel hamstring stretch" + ], + "commonMistakes": [ + "Rounding the back", + "Going too heavy", + "Bending knees too much" + ] + }, + { + "name": "Dumbbell Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=gfUg6qWohTk", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on a bench with one hand while pulling a dumbbell to your hip with the other.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "rear deltoid" + ], + "formCues": [ + "Keep back flat and parallel to floor", + "Pull elbow toward hip", + "Squeeze at the top for 1 second", + "Lower with control", + "Don't rotate torso" + ], + "commonMistakes": [ + "Rotating torso to swing weight up", + "Not pulling high enough", + "Using a weight that's too heavy" + ] + }, + { + "name": "Single Arm Dumbbell Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qN54-QNO1eQ", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Chest Supported Dumbbell Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=czoQ_ncuqqI", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Incline Dumbbell Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=2LxN3_3atps", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Dumbbell Shrug", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=rFsSeClGnNA", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Renegade Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4qEIChzM4ZA", + "secondaryMuscles": [ + "core", + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Seal Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5V33g0dB_N4", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Dumbbell Rear Delt Fly", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LsT-bR_zxLo", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Dumbbell Pullover", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=gbDH1OvCe-M", + "secondaryMuscles": [ + "chest" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise using dumbbells targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Deadlift", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=YQgs03p3UxE", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Lat Pulldown", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=z-lxcsIN4T4", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Wide Grip Lat Pulldown", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=HWGntttgJQw", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Close Grip Lat Pulldown", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LT6rXwllEL8", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Reverse Grip Lat Pulldown", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5YC67yVpBGE", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Single Arm Lat Pulldown", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=M9xUoJYtXtc", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Seated Cable Row", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vwHG9Jfu4sw", + "secondaryMuscles": [ + "biceps", + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Wide Grip Cable Row", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vqPY3fDessY", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Single Arm Cable Row", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=G18ysBYu5Mw", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Face Pull", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8686PLZB_1Q", + "secondaryMuscles": [ + "rear delt", + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Pull a rope attachment toward your face at a high cable, externally rotating shoulders.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids", + "rotator cuff" + ], + "formCues": [ + "Set cable at face height", + "Pull toward face, spreading rope wide", + "Externally rotate at the end", + "Squeeze shoulder blades", + "Pause at peak contraction" + ], + "commonMistakes": [ + "Using too much weight", + "Not externally rotating", + "Pulling to chest instead of face", + "Leaning back excessively" + ] + }, + { + "name": "Straight Arm Pulldown", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=lnec6DdscJU", + "secondaryMuscles": [ + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Cable Shrug", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=JxaV9n9l2Y8", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Cable Reverse Fly", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Zhtb_XbKh9w", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Cable Pull-Through", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=IU-ERkjTKXA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "A back exercise on a cable machine targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Lat Pulldown Machine", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=SALxEARiMkw", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a wide bar down to your upper chest while seated at a cable machine.", + "primaryMuscles": [ + "latissimus dorsi", + "biceps", + "rear deltoid" + ], + "formCues": [ + "Grip slightly wider than shoulders", + "Pull bar to upper chest", + "Lean back slightly", + "Squeeze lats at the bottom", + "Control the return" + ], + "commonMistakes": [ + "Pulling behind the neck", + "Using too much body lean/momentum", + "Not going through full range of motion" + ] + }, + { + "name": "Seated Row Machine", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=DHA7QGDa2qg", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Chest Supported Row Machine", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=azN0upTD8Go", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "T-Bar Row Machine", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=TyLoy3n_a10", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Back Extension Machine", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=sRFFS-DZ3SU", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Assisted Pull-Up Machine", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=owMowIRkyvQ", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise on a machine targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Reverse Pec Deck", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=-TKqxK7-ehc", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit facing the pec deck machine and push arms backward to work rear deltoids.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids" + ], + "formCues": [ + "Face the pad, chest against it", + "Push arms back in a wide arc", + "Squeeze at peak contraction", + "Return slowly" + ], + "commonMistakes": [ + "Using momentum", + "Not setting seat height correctly", + "Shrugging shoulders up" + ] + }, + { + "name": "Smith Machine Row", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tGGq2VZIW_M", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Smith Machine Shrug", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MlqHEfydPpE", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Smith Machine Deadlift", + "muscleGroup": "Back", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ONRRAgNLVac", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Pull-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=bb8_5vZV5dU", + "secondaryMuscles": [ + "biceps", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Chin-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Oi3bW9nQmGI", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Wide Grip Pull-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=p40iUjf02j0", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Neutral Grip Pull-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Ai4S1uzMP7A", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Weighted Pull-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=k2mvvcU1Lq8", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Weighted Chin-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=9aA0-FbxK9E", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Muscle-Up", + "muscleGroup": "Back", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_eQ2gw_Gg5Y", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with pullUpBar targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Trap Bar Deadlift", + "muscleGroup": "Back", + "equipment": "trapBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v-SrIcAp3vM", + "secondaryMuscles": [ + "quads", + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Trap Bar Shrug", + "muscleGroup": "Back", + "equipment": "trapBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MlqHEfydPpE", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Kettlebell Swing", + "muscleGroup": "Back", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=aSYap2yhW8s", + "secondaryMuscles": [ + "glutes", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise using a kettlebell targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Kettlebell Row", + "muscleGroup": "Back", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=IyQAMOV0WAc", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Kettlebell Deadlift", + "muscleGroup": "Back", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hinonqqzatk", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Landmine Row", + "muscleGroup": "Back", + "equipment": "landmine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8c23NBbwLBc", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Inverted Row", + "muscleGroup": "Back", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5W8F6MzZ8Rk", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Back Extension", + "muscleGroup": "Back", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=EBui4Bt5N7o", + "secondaryMuscles": [ + "glutes", + "hamstrings" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Superman", + "muscleGroup": "Back", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=cZxtPxeR2H8", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise using bodyweight only targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Resistance Band Row", + "muscleGroup": "Back", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LSkyinhmA8k", + "secondaryMuscles": [ + "biceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Resistance Band Pull-Apart", + "muscleGroup": "Back", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=dn7uM0hTQHc", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A back exercise with resistanceBand targeting the back muscles.", + "primaryMuscles": [ + "back" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Overhead Press", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=zoN5EH50Dro", + "secondaryMuscles": [ + "triceps", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press a weight from shoulder level overhead to full arm extension while standing or seated.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Brace core and squeeze glutes", + "Press bar straight up, moving head out of the way", + "Lock out overhead", + "Lower to chin/upper chest level" + ], + "commonMistakes": [ + "Excessive back lean", + "Not locking out", + "Pressing in front of the body instead of overhead", + "Flaring ribs" + ] + }, + { + "name": "Seated Barbell Press", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hWbUlkb5Ms4", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Push Press", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Y0Ffbb0Ey4c", + "secondaryMuscles": [ + "triceps", + "legs" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Behind the Neck Press", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=HYvR-Niw2p0", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Barbell Upright Row", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=jaAV-rD45I0", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Barbell Front Raise", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=PEEYtHe-rfQ", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise a weight in front of your body to shoulder height.", + "primaryMuscles": [ + "anterior deltoid" + ], + "formCues": [ + "Keep arms slightly bent", + "Raise to shoulder height", + "Lower with control", + "Alternate or both arms together" + ], + "commonMistakes": [ + "Swinging for momentum", + "Raising above shoulder height", + "Arching the back" + ] + }, + { + "name": "Military Press", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=3RdVYQ8l_V8", + "secondaryMuscles": [ + "triceps", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Clean and Press", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=9lSjbxkjCqU", + "secondaryMuscles": [ + "traps", + "legs" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Dumbbell Shoulder Press", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=poD_-zaG9hk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press weight from shoulder level to overhead, emphasizing all three deltoid heads.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Keep core tight", + "Press directly overhead", + "Lower to ear level", + "Don't flare elbows too wide" + ], + "commonMistakes": [ + "Arching the back", + "Using leg drive (unless push press)", + "Not going through full range" + ] + }, + { + "name": "Seated Dumbbell Press", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=E9ShwbwZ1zw", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Single Arm Dumbbell Press", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=KLSYjgShghk", + "secondaryMuscles": [ + "triceps", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Arnold Press", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6K_N9AGhItQ", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulder press that starts with palms facing you and rotates to palms forward as you press overhead.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Start with palms facing you at chin level", + "Rotate palms forward as you press up", + "Reverse the motion on the way down", + "Full range of motion" + ], + "commonMistakes": [ + "Rushing the rotation", + "Not going through full ROM", + "Using momentum" + ] + }, + { + "name": "Lateral Raise", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Y29xKcze8Ik", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Seated Lateral Raise", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=QbDNpyucYGc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Leaning Lateral Raise", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qWif_7SOYpQ", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Front Raise", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yHx8wPv4RPo", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise a weight in front of your body to shoulder height.", + "primaryMuscles": [ + "anterior deltoid" + ], + "formCues": [ + "Keep arms slightly bent", + "Raise to shoulder height", + "Lower with control", + "Alternate or both arms together" + ], + "commonMistakes": [ + "Swinging for momentum", + "Raising above shoulder height", + "Arching the back" + ] + }, + { + "name": "Alternating Front Raise", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yHx8wPv4RPo", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise a weight in front of your body to shoulder height.", + "primaryMuscles": [ + "anterior deltoid" + ], + "formCues": [ + "Keep arms slightly bent", + "Raise to shoulder height", + "Lower with control", + "Alternate or both arms together" + ], + "commonMistakes": [ + "Swinging for momentum", + "Raising above shoulder height", + "Arching the back" + ] + }, + { + "name": "Dumbbell Rear Delt Fly", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LsT-bR_zxLo", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Incline Rear Delt Fly", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LsT-bR_zxLo", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Dumbbell Upright Row", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=K0dYqPCaO14", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Dumbbell Y-Raise", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=cmw34H8DRmc", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulders exercise using dumbbells targeting the shoulders muscles.", + "primaryMuscles": [ + "shoulders" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Shrug", + "muscleGroup": "Shoulders", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=rFsSeClGnNA", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Cable Lateral Raise", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=f_OGBg2KxgY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Single Arm Cable Lateral Raise", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1AmmsXlf8MU", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Cable Front Raise", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=NdQE5Fhfqn4", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise a weight in front of your body to shoulder height.", + "primaryMuscles": [ + "anterior deltoid" + ], + "formCues": [ + "Keep arms slightly bent", + "Raise to shoulder height", + "Lower with control", + "Alternate or both arms together" + ], + "commonMistakes": [ + "Swinging for momentum", + "Raising above shoulder height", + "Arching the back" + ] + }, + { + "name": "Cable Rear Delt Fly", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=bkejPHrPkmA", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Cable Upright Row", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MVDNQt5NgKI", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Cable Face Pull", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qEyoBOpvqR4", + "secondaryMuscles": [ + "rear delt", + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Pull a rope attachment toward your face at a high cable, externally rotating shoulders.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids", + "rotator cuff" + ], + "formCues": [ + "Set cable at face height", + "Pull toward face, spreading rope wide", + "Externally rotate at the end", + "Squeeze shoulder blades", + "Pause at peak contraction" + ], + "commonMistakes": [ + "Using too much weight", + "Not externally rotating", + "Pulling to chest instead of face", + "Leaning back excessively" + ] + }, + { + "name": "Cable Shoulder Press", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=-r4BbNR0frg", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press weight from shoulder level to overhead, emphasizing all three deltoid heads.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Keep core tight", + "Press directly overhead", + "Lower to ear level", + "Don't flare elbows too wide" + ], + "commonMistakes": [ + "Arching the back", + "Using leg drive (unless push press)", + "Not going through full range" + ] + }, + { + "name": "Cable Shrug", + "muscleGroup": "Shoulders", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=JxaV9n9l2Y8", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Shoulder Press Machine", + "muscleGroup": "Shoulders", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6v4nrRVySj0", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press weight from shoulder level to overhead, emphasizing all three deltoid heads.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Keep core tight", + "Press directly overhead", + "Lower to ear level", + "Don't flare elbows too wide" + ], + "commonMistakes": [ + "Arching the back", + "Using leg drive (unless push press)", + "Not going through full range" + ] + }, + { + "name": "Lateral Raise Machine", + "muscleGroup": "Shoulders", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=l89vNiySZ-Q", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Reverse Pec Deck", + "muscleGroup": "Shoulders", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=-TKqxK7-ehc", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit facing the pec deck machine and push arms backward to work rear deltoids.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids" + ], + "formCues": [ + "Face the pad, chest against it", + "Push arms back in a wide arc", + "Squeeze at peak contraction", + "Return slowly" + ], + "commonMistakes": [ + "Using momentum", + "Not setting seat height correctly", + "Shrugging shoulders up" + ] + }, + { + "name": "Smith Machine Overhead Press", + "muscleGroup": "Shoulders", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=E7ngsffMPR0", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press a weight from shoulder level overhead to full arm extension while standing or seated.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Brace core and squeeze glutes", + "Press bar straight up, moving head out of the way", + "Lock out overhead", + "Lower to chin/upper chest level" + ], + "commonMistakes": [ + "Excessive back lean", + "Not locking out", + "Pressing in front of the body instead of overhead", + "Flaring ribs" + ] + }, + { + "name": "Smith Machine Shrug", + "muscleGroup": "Shoulders", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MlqHEfydPpE", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Smith Machine Upright Row", + "muscleGroup": "Shoulders", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=-Anm7xrLikg", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Kettlebell Press", + "muscleGroup": "Shoulders", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=OHo4GZHem7U", + "secondaryMuscles": [ + "triceps", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Kettlebell Halo", + "muscleGroup": "Shoulders", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=13SFATc-mJ4", + "secondaryMuscles": [ + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulders exercise using a kettlebell targeting the shoulders muscles.", + "primaryMuscles": [ + "shoulders" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Kettlebell Windmill", + "muscleGroup": "Shoulders", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=q8K_Wg8OuV4", + "secondaryMuscles": [ + "core" + ], + "difficulty": "advanced", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulders exercise using a kettlebell targeting the shoulders muscles.", + "primaryMuscles": [ + "shoulders" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Turkish Get-Up", + "muscleGroup": "Shoulders", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=jgKFttG0Z7I", + "secondaryMuscles": [ + "core" + ], + "difficulty": "advanced", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulders exercise using a kettlebell targeting the shoulders muscles.", + "primaryMuscles": [ + "shoulders" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Landmine Shoulder Press", + "muscleGroup": "Shoulders", + "equipment": "landmine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6cSTRPhpubs", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press weight from shoulder level to overhead, emphasizing all three deltoid heads.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Keep core tight", + "Press directly overhead", + "Lower to ear level", + "Don't flare elbows too wide" + ], + "commonMistakes": [ + "Arching the back", + "Using leg drive (unless push press)", + "Not going through full range" + ] + }, + { + "name": "Plate Front Raise", + "muscleGroup": "Shoulders", + "equipment": "plate", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yHQi_GUNc5o", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise a weight in front of your body to shoulder height.", + "primaryMuscles": [ + "anterior deltoid" + ], + "formCues": [ + "Keep arms slightly bent", + "Raise to shoulder height", + "Lower with control", + "Alternate or both arms together" + ], + "commonMistakes": [ + "Swinging for momentum", + "Raising above shoulder height", + "Arching the back" + ] + }, + { + "name": "Handstand Push-Up", + "muscleGroup": "Shoulders", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=gSjHRuRQ4hk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulders exercise using bodyweight only targeting the shoulders muscles.", + "primaryMuscles": [ + "shoulders" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Pike Push-Up", + "muscleGroup": "Shoulders", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=jz_Vr4JbUjc", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A shoulders exercise using bodyweight only targeting the shoulders muscles.", + "primaryMuscles": [ + "shoulders" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Resistance Band Lateral Raise", + "muscleGroup": "Shoulders", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=eX5Ad5GIQyQ", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Resistance Band Shoulder Press", + "muscleGroup": "Shoulders", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=3KFECHGL9vM", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press weight from shoulder level to overhead, emphasizing all three deltoid heads.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Keep core tight", + "Press directly overhead", + "Lower to ear level", + "Don't flare elbows too wide" + ], + "commonMistakes": [ + "Arching the back", + "Using leg drive (unless push press)", + "Not going through full range" + ] + }, + { + "name": "Resistance Band Face Pull", + "muscleGroup": "Shoulders", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=OIMUU2Q-upU", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a rope attachment toward your face at a high cable, externally rotating shoulders.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids", + "rotator cuff" + ], + "formCues": [ + "Set cable at face height", + "Pull toward face, spreading rope wide", + "Externally rotate at the end", + "Squeeze shoulder blades", + "Pause at peak contraction" + ], + "commonMistakes": [ + "Using too much weight", + "Not externally rotating", + "Pulling to chest instead of face", + "Leaning back excessively" + ] + }, + { + "name": "Cable Rear Delt Fly", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=bkejPHrPkmA", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Cable Face Pull", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qEyoBOpvqR4", + "secondaryMuscles": [ + "rear delt", + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Pull a rope attachment toward your face at a high cable, externally rotating shoulders.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids", + "rotator cuff" + ], + "formCues": [ + "Set cable at face height", + "Pull toward face, spreading rope wide", + "Externally rotate at the end", + "Squeeze shoulder blades", + "Pause at peak contraction" + ], + "commonMistakes": [ + "Using too much weight", + "Not externally rotating", + "Pulling to chest instead of face", + "Leaning back excessively" + ] + }, + { + "name": "Incline Rear Delt Fly", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LsT-bR_zxLo", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and open arms wide with dumbbells, then bring them together in a hugging motion.", + "primaryMuscles": [ + "pectoralis major" + ], + "formCues": [ + "Maintain a slight bend in elbows throughout", + "Lower until you feel a stretch in the chest", + "Squeeze chest at the top", + "Control the weight on the way down" + ], + "commonMistakes": [ + "Straightening arms (turns into a press)", + "Going too heavy and losing form", + "Lowering too far and straining shoulders" + ] + }, + { + "name": "Resistance Band Face Pull", + "muscleGroup": "Back", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=OIMUU2Q-upU", + "secondaryMuscles": [ + "rear delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Pull a rope attachment toward your face at a high cable, externally rotating shoulders.", + "primaryMuscles": [ + "rear deltoid", + "rhomboids", + "rotator cuff" + ], + "formCues": [ + "Set cable at face height", + "Pull toward face, spreading rope wide", + "Externally rotate at the end", + "Squeeze shoulder blades", + "Pause at peak contraction" + ], + "commonMistakes": [ + "Using too much weight", + "Not externally rotating", + "Pulling to chest instead of face", + "Leaning back excessively" + ] + }, + { + "name": "Barbell Shrug", + "muscleGroup": "Shoulders", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MlqHEfydPpE", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Trap Bar Shrug", + "muscleGroup": "Shoulders", + "equipment": "trapBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MlqHEfydPpE", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold weights at your sides and elevate your shoulders straight up toward your ears.", + "primaryMuscles": [ + "upper trapezius" + ], + "formCues": [ + "Shrug straight up, not rolling", + "Hold at the top for 1-2 seconds", + "Lower with control", + "Keep arms straight" + ], + "commonMistakes": [ + "Rolling shoulders (unnecessary, can cause injury)", + "Using momentum/bouncing", + "Not holding at the top" + ] + }, + { + "name": "Barbell Upright Row", + "muscleGroup": "Back", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=jaAV-rD45I0", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Dumbbell Upright Row", + "muscleGroup": "Back", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=K0dYqPCaO14", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Cable Upright Row", + "muscleGroup": "Back", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=MVDNQt5NgKI", + "secondaryMuscles": [ + "traps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bend forward at the hips and pull a barbell toward your lower chest/upper abdomen.", + "primaryMuscles": [ + "latissimus dorsi", + "rhomboids", + "trapezius", + "rear deltoid" + ], + "formCues": [ + "Keep torso at roughly 45 degrees", + "Pull bar to lower chest/upper belly", + "Squeeze shoulder blades together", + "Lower with control", + "Brace core throughout" + ], + "commonMistakes": [ + "Using momentum and swinging torso", + "Not squeezing at the top", + "Rounding the back", + "Standing too upright" + ] + }, + { + "name": "Barbell Curl", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=N6paU6TGFWU", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Wide Grip Barbell Curl", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ez3YoWf62Eg", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Close Grip Barbell Curl", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Bh52XZDF_zM", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Barbell Drag Curl", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Fj7Pk4tA1ic", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Barbell Reverse Curl", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ypfd1kaI1AU", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Barbell Preacher Curl", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Htw-s61mOw0", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with your upper arms resting on an angled preacher bench pad.", + "primaryMuscles": [ + "biceps brachii" + ], + "formCues": [ + "Upper arms flat on pad", + "Curl to full contraction", + "Lower slowly — don't let arm snap straight", + "Keep wrists neutral" + ], + "commonMistakes": [ + "Lifting elbows off the pad", + "Letting weight drop at the bottom", + "Not going through full ROM" + ] + }, + { + "name": "EZ Bar Curl", + "muscleGroup": "Arms", + "equipment": "ezCurlBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=RkcYuubJR54", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "EZ Bar Preacher Curl", + "muscleGroup": "Arms", + "equipment": "ezCurlBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Htw-s61mOw0", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with your upper arms resting on an angled preacher bench pad.", + "primaryMuscles": [ + "biceps brachii" + ], + "formCues": [ + "Upper arms flat on pad", + "Curl to full contraction", + "Lower slowly — don't let arm snap straight", + "Keep wrists neutral" + ], + "commonMistakes": [ + "Lifting elbows off the pad", + "Letting weight drop at the bottom", + "Not going through full ROM" + ] + }, + { + "name": "EZ Bar Reverse Curl", + "muscleGroup": "Arms", + "equipment": "ezCurlBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ypfd1kaI1AU", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "EZ Bar Spider Curl", + "muscleGroup": "Arms", + "equipment": "ezCurlBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=HogDDGR_7gE", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Dumbbell Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=XE_pHwbst04", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Alternating Dumbbell Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=FHY_2t7R714", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Hammer Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=BRVDS6HVR9Q", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl dumbbells with a neutral (palms facing each other) grip to target the brachialis and forearms.", + "primaryMuscles": [ + "brachialis", + "brachioradialis", + "biceps" + ], + "formCues": [ + "Keep palms facing each other throughout", + "Elbows stay at sides", + "Squeeze at the top", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Rotating wrists during the curl", + "Moving elbows forward" + ] + }, + { + "name": "Cross Body Hammer Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qmQkt1Y-FX8", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl dumbbells with a neutral (palms facing each other) grip to target the brachialis and forearms.", + "primaryMuscles": [ + "brachialis", + "brachioradialis", + "biceps" + ], + "formCues": [ + "Keep palms facing each other throughout", + "Elbows stay at sides", + "Squeeze at the top", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Rotating wrists during the curl", + "Moving elbows forward" + ] + }, + { + "name": "Incline Dumbbell Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=XhIsIcjIbCw", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Concentration Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=cHxRJdSVIkA", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit and brace your elbow against your inner thigh while curling a dumbbell for strict bicep isolation.", + "primaryMuscles": [ + "biceps brachii" + ], + "formCues": [ + "Brace elbow on inner thigh", + "Curl with a full squeeze at top", + "Lower slowly", + "Only the forearm moves" + ], + "commonMistakes": [ + "Using shoulder to swing the weight", + "Not bracing the elbow properly", + "Partial range of motion" + ] + }, + { + "name": "Dumbbell Preacher Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vngli9UR6Hw", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with your upper arms resting on an angled preacher bench pad.", + "primaryMuscles": [ + "biceps brachii" + ], + "formCues": [ + "Upper arms flat on pad", + "Curl to full contraction", + "Lower slowly — don't let arm snap straight", + "Keep wrists neutral" + ], + "commonMistakes": [ + "Lifting elbows off the pad", + "Letting weight drop at the bottom", + "Not going through full ROM" + ] + }, + { + "name": "Zottman Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=D7bMA4WEKMI", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Dumbbell Spider Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ivS3G35bapw", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Dumbbell Reverse Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=uiH-2J85mzI", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Dumbbell Wrist Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=sKXqNO2KQp8", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rest forearms on thighs or a bench and curl a weight using only wrist flexion.", + "primaryMuscles": [ + "forearm flexors" + ], + "formCues": [ + "Rest forearms on thighs, wrists hanging over knees", + "Curl wrists up squeezing forearms", + "Lower with control", + "Full range of motion" + ], + "commonMistakes": [ + "Moving the forearms", + "Using momentum", + "Going too heavy" + ] + }, + { + "name": "Reverse Wrist Curl", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ypfd1kaI1AU", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rest forearms on thighs or a bench and curl a weight using only wrist flexion.", + "primaryMuscles": [ + "forearm flexors" + ], + "formCues": [ + "Rest forearms on thighs, wrists hanging over knees", + "Curl wrists up squeezing forearms", + "Lower with control", + "Full range of motion" + ], + "commonMistakes": [ + "Moving the forearms", + "Using momentum", + "Going too heavy" + ] + }, + { + "name": "Cable Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=CrbTqNOlFgE", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Cable Rope Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=YC9QZiti-40", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Single Arm Cable Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_Z8Afknw_Fc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": "dHandle", + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "High Cable Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=xS5FLIa8SJg", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Cable Hammer Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=YC9QZiti-40", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Curl dumbbells with a neutral (palms facing each other) grip to target the brachialis and forearms.", + "primaryMuscles": [ + "brachialis", + "brachioradialis", + "biceps" + ], + "formCues": [ + "Keep palms facing each other throughout", + "Elbows stay at sides", + "Squeeze at the top", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Rotating wrists during the curl", + "Moving elbows forward" + ] + }, + { + "name": "Cable Preacher Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=eYrAV7rnol0", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with your upper arms resting on an angled preacher bench pad.", + "primaryMuscles": [ + "biceps brachii" + ], + "formCues": [ + "Upper arms flat on pad", + "Curl to full contraction", + "Lower slowly — don't let arm snap straight", + "Keep wrists neutral" + ], + "commonMistakes": [ + "Lifting elbows off the pad", + "Letting weight drop at the bottom", + "Not going through full ROM" + ] + }, + { + "name": "Cable Reverse Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_0icSYP3PlE", + "secondaryMuscles": [ + "forearms" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Bayesian Curl", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Q3rF30Wqp6s", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Preacher Curl Machine", + "muscleGroup": "Arms", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Htw-s61mOw0", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with your upper arms resting on an angled preacher bench pad.", + "primaryMuscles": [ + "biceps brachii" + ], + "formCues": [ + "Upper arms flat on pad", + "Curl to full contraction", + "Lower slowly — don't let arm snap straight", + "Keep wrists neutral" + ], + "commonMistakes": [ + "Lifting elbows off the pad", + "Letting weight drop at the bottom", + "Not going through full ROM" + ] + }, + { + "name": "Bicep Curl Machine", + "muscleGroup": "Arms", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Htw-s61mOw0", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight upward by flexing at the elbow, targeting the biceps.", + "primaryMuscles": [ + "biceps brachii", + "brachialis" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Squeeze at the top", + "Lower with control (don't swing)", + "Full range of motion" + ], + "commonMistakes": [ + "Swinging the body for momentum", + "Moving elbows forward", + "Not controlling the negative", + "Partial reps" + ] + }, + { + "name": "Kettlebell Curl", + "muscleGroup": "Arms", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=mPiTdI_FcOY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Resistance Band Curl", + "muscleGroup": "Arms", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LLrNuMHjBsE", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Close Grip Bench Press", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4yKLxOsrGfg", + "secondaryMuscles": [ + "chest" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Bench press with a narrow grip to emphasize triceps over chest.", + "primaryMuscles": [ + "triceps brachii", + "pectoralis major" + ], + "formCues": [ + "Grip shoulder-width or slightly narrower", + "Keep elbows close to body", + "Lower to lower chest", + "Press up to full lockout" + ], + "commonMistakes": [ + "Grip too narrow (wrist strain)", + "Flaring elbows outward", + "Not locking out" + ] + }, + { + "name": "Barbell Skull Crusher", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=S0fmDR60X-o", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and lower a weight toward your forehead by bending at the elbows, then extend back up.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep upper arms perpendicular to floor", + "Lower to forehead or just behind head", + "Extend fully", + "Keep elbows from flaring" + ], + "commonMistakes": [ + "Flaring elbows wide", + "Moving upper arms", + "Lowering too fast toward face" + ] + }, + { + "name": "French Press", + "muscleGroup": "Arms", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=95rudq8ZeAc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "EZ Bar Skull Crusher", + "muscleGroup": "Arms", + "equipment": "ezCurlBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=S0fmDR60X-o", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and lower a weight toward your forehead by bending at the elbows, then extend back up.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep upper arms perpendicular to floor", + "Lower to forehead or just behind head", + "Extend fully", + "Keep elbows from flaring" + ], + "commonMistakes": [ + "Flaring elbows wide", + "Moving upper arms", + "Lowering too fast toward face" + ] + }, + { + "name": "EZ Bar Overhead Extension", + "muscleGroup": "Arms", + "equipment": "ezCurlBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5IscnabWzKg", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Overhead Dumbbell Extension", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=-Vyt2QdsR7E", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Single Arm Overhead Extension", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_w3ggqafzqU", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Dumbbell Skull Crusher", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=D1y1-sXZDA0", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and lower a weight toward your forehead by bending at the elbows, then extend back up.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep upper arms perpendicular to floor", + "Lower to forehead or just behind head", + "Extend fully", + "Keep elbows from flaring" + ], + "commonMistakes": [ + "Flaring elbows wide", + "Moving upper arms", + "Lowering too fast toward face" + ] + }, + { + "name": "Tricep Kickback", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=3Bv1n7-DN7c", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Single Arm Tricep Kickback", + "muscleGroup": "Arms", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=NIp8MdPKTaI", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Rope Pushdown", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=NvZKjiZ8NYc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "V-Bar Pushdown", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=u6sqENBsXjg", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "vBar", + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Bar Pushdown", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1FjkhpZsaxc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "straightBar", + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Single Arm Pushdown", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=VAHZcPAUwdQ", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": "dHandle", + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Overhead Rope Extension", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=b5le--KkyH0", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Overhead Cable Extension", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=b5le--KkyH0", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "straightBar", + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Reverse Grip Pushdown", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=2668NKYmls4", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Cable Kickback", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=A9aN_L4vexk", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A arms exercise on a cable machine targeting the arms muscles.", + "primaryMuscles": [ + "arms" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Cable Skull Crusher", + "muscleGroup": "Arms", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Jz7UYRoXfPk", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on a bench and lower a weight toward your forehead by bending at the elbows, then extend back up.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep upper arms perpendicular to floor", + "Lower to forehead or just behind head", + "Extend fully", + "Keep elbows from flaring" + ], + "commonMistakes": [ + "Flaring elbows wide", + "Moving upper arms", + "Lowering too fast toward face" + ] + }, + { + "name": "Tricep Extension Machine", + "muscleGroup": "Arms", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=fwzkg88Okwo", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Extend a weight overhead or behind you by straightening the elbow to target the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep upper arm stationary", + "Extend fully and squeeze", + "Lower with control", + "Keep elbow pointed up (overhead) or back" + ], + "commonMistakes": [ + "Moving the upper arm", + "Flaring elbows", + "Using momentum" + ] + }, + { + "name": "Dip Machine", + "muscleGroup": "Arms", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=08pJgqNRAl0", + "secondaryMuscles": [ + "chest" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on parallel bars and lower your body by bending elbows, then press back up.", + "primaryMuscles": [ + "pectoralis major", + "triceps", + "anterior deltoid" + ], + "formCues": [ + "Lean slightly forward for chest emphasis", + "Lower until upper arms are parallel to floor", + "Press up to full lockout", + "Keep core engaged" + ], + "commonMistakes": [ + "Going too deep and straining shoulders", + "Staying too upright (shifts to triceps only)", + "Swinging legs for momentum" + ] + }, + { + "name": "Tricep Dip", + "muscleGroup": "Arms", + "equipment": "dipStation", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=9llvBAV4RHI", + "secondaryMuscles": [ + "chest", + "front delt" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on parallel bars and lower your body by bending elbows, then press back up.", + "primaryMuscles": [ + "pectoralis major", + "triceps", + "anterior deltoid" + ], + "formCues": [ + "Lean slightly forward for chest emphasis", + "Lower until upper arms are parallel to floor", + "Press up to full lockout", + "Keep core engaged" + ], + "commonMistakes": [ + "Going too deep and straining shoulders", + "Staying too upright (shifts to triceps only)", + "Swinging legs for momentum" + ] + }, + { + "name": "Weighted Dip", + "muscleGroup": "Arms", + "equipment": "dipStation", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yN6Q1UI_xkE", + "secondaryMuscles": [ + "chest" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on parallel bars and lower your body by bending elbows, then press back up.", + "primaryMuscles": [ + "pectoralis major", + "triceps", + "anterior deltoid" + ], + "formCues": [ + "Lean slightly forward for chest emphasis", + "Lower until upper arms are parallel to floor", + "Press up to full lockout", + "Keep core engaged" + ], + "commonMistakes": [ + "Going too deep and straining shoulders", + "Staying too upright (shifts to triceps only)", + "Swinging legs for momentum" + ] + }, + { + "name": "Bench Dip", + "muscleGroup": "Arms", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4ua3MzaU0QU", + "secondaryMuscles": [ + "chest" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on parallel bars and lower your body by bending elbows, then press back up.", + "primaryMuscles": [ + "pectoralis major", + "triceps", + "anterior deltoid" + ], + "formCues": [ + "Lean slightly forward for chest emphasis", + "Lower until upper arms are parallel to floor", + "Press up to full lockout", + "Keep core engaged" + ], + "commonMistakes": [ + "Going too deep and straining shoulders", + "Staying too upright (shifts to triceps only)", + "Swinging legs for momentum" + ] + }, + { + "name": "Resistance Band Pushdown", + "muscleGroup": "Arms", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=jWMXsd8Mu3Y", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a cable attachment downward by extending the elbows to work the triceps.", + "primaryMuscles": [ + "triceps brachii" + ], + "formCues": [ + "Keep elbows pinned at your sides", + "Extend fully and squeeze", + "Control the return", + "Don't lean over the weight" + ], + "commonMistakes": [ + "Flaring elbows out", + "Using body weight to push down", + "Not fully extending" + ] + }, + { + "name": "Chest Dip", + "muscleGroup": "Arms", + "equipment": "dipStation", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=NuhXmq6x9Sk", + "secondaryMuscles": [ + "triceps" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Support yourself on parallel bars and lower your body by bending elbows, then press back up.", + "primaryMuscles": [ + "pectoralis major", + "triceps", + "anterior deltoid" + ], + "formCues": [ + "Lean slightly forward for chest emphasis", + "Lower until upper arms are parallel to floor", + "Press up to full lockout", + "Keep core engaged" + ], + "commonMistakes": [ + "Going too deep and straining shoulders", + "Staying too upright (shifts to triceps only)", + "Swinging legs for momentum" + ] + }, + { + "name": "Back Squat", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=iZTxa8NJH2g", + "secondaryMuscles": [ + "glutes", + "hamstrings", + "core" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Front Squat", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v-mQm_droHg", + "secondaryMuscles": [ + "quads", + "core" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Squat with a barbell held in front of your shoulders in a rack position.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "core" + ], + "formCues": [ + "Keep elbows high", + "Stay upright", + "Hit full depth", + "Drive up through midfoot", + "Brace core hard" + ], + "commonMistakes": [ + "Elbows dropping (bar rolls forward)", + "Leaning forward", + "Not bracing core", + "Wrist pain from poor rack position" + ] + }, + { + "name": "Overhead Squat", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=7dazkuWGOa8", + "secondaryMuscles": [ + "core", + "shoulders" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Pause Squat", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nknf16JJTZo", + "secondaryMuscles": [ + "glutes", + "core" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Box Squat", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nBc_2Jyp3tM", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Barbell Lunge", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yNSXHJKYU-s", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Barbell Reverse Lunge", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=wCU6Rdkr2Fo", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Barbell Walking Lunge", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=La8YR6wCsEE", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Barbell Step-Up", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1OS-HTTtqD8", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using a barbell targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Goblet Squat", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=lRYBbchqxtI", + "secondaryMuscles": [ + "core", + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Dumbbell Lunge", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1cS-6KsJW9g", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Dumbbell Reverse Lunge", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=J9MpoAQCjos", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Dumbbell Walking Lunge", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Pbmj6xPo-Hw", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Bulgarian Split Squat", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=uODWo4YqbT8", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Single-leg squat with your rear foot elevated on a bench behind you.", + "primaryMuscles": [ + "quadriceps", + "glutes" + ], + "formCues": [ + "Rear foot on bench, laces down", + "Lower until front thigh is parallel", + "Keep torso upright", + "Push up through front heel" + ], + "commonMistakes": [ + "Standing too close to the bench", + "Leaning forward", + "Rear knee not tracking straight" + ] + }, + { + "name": "Dumbbell Split Squat", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hiLF_pF3EJM", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Dumbbell Step-Up", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8q9LVgN2RD4", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using dumbbells targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Sumo Squat", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=7BqURseCGSU", + "secondaryMuscles": [ + "glutes", + "adductors" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Dumbbell Lateral Lunge", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vwK7vZNQwUI", + "secondaryMuscles": [ + "adductors" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Curtsy Lunge", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=cVYnf2CFO9M", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Leg Press", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nDh_BlnLCGc", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Single Leg Press", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nDh_BlnLCGc", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Leg Extension", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=uM86QE59Tgc", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Single Leg Extension", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=WwTJM8VxpvE", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit on a machine and extend your legs until straight to isolate the quadriceps.", + "primaryMuscles": [ + "quadriceps" + ], + "formCues": [ + "Adjust pad to sit above ankles", + "Extend fully and squeeze", + "Lower with control", + "Don't use momentum" + ], + "commonMistakes": [ + "Swinging weight up", + "Not fully extending", + "Using too much weight" + ] + }, + { + "name": "Hack Squat", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=g9i05umL5vc", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Squat on an angled machine with shoulders under pads to target quads.", + "primaryMuscles": [ + "quadriceps", + "glutes" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until thighs are parallel", + "Press through full foot", + "Keep back flat against pad" + ], + "commonMistakes": [ + "Not going deep enough", + "Knees caving in", + "Pushing through toes only" + ] + }, + { + "name": "V-Squat", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=lkxzvLmOlJc", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise on a machine targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Pendulum Squat", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=2vjAEIjhao8", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Smith Machine Squat", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=fC5urG2CCr8", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Smith Machine Lunge", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=H4rbpNsgyvs", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Belt Squat", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=9fEloOw3hTo", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Sissy Squat Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=DOxGMy258rM", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Kettlebell Goblet Squat", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=n7Fh6vGaJ7Y", + "secondaryMuscles": [ + "core" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Kettlebell Lunge", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=3ygYqMMoxbU", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Bodyweight Squat", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=m0GcZ24pK6k", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Bodyweight Lunge", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1cS-6KsJW9g", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Walking Lunge", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1cS-6KsJW9g", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Reverse Lunge", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5frs7_F2SrU", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Jump Squat", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=h5TmdMMtIT4", + "secondaryMuscles": [ + "glutes", + "calves" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Jump Lunge", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v4-w6r5bASs", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Step forward and lower your body until both knees are at approximately 90 degrees.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Take a controlled step forward", + "Lower back knee toward floor", + "Keep front knee over ankle", + "Push back to start through front heel" + ], + "commonMistakes": [ + "Front knee going past toes excessively", + "Torso leaning forward", + "Not going deep enough", + "Wobbling side to side" + ] + }, + { + "name": "Pistol Squat", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=bH3mRwnAN88", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Wall Sit", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=mDdLC-yKudY", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using bodyweight only targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Box Jump", + "muscleGroup": "Legs", + "equipment": "box", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=HJZh-12p6vg", + "secondaryMuscles": [ + "glutes", + "calves" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise with box targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Box Step-Up", + "muscleGroup": "Legs", + "equipment": "box", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vs87hPGdnCc", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise with box targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Sled Push", + "muscleGroup": "Legs", + "equipment": "sled", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=9XRRXaUpnLk", + "secondaryMuscles": [ + "glutes", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower and raise your body using your arms while maintaining a straight line from head to heels.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Keep body in a straight line", + "Lower chest to just above the floor", + "Push through palms evenly", + "Keep core engaged throughout" + ], + "commonMistakes": [ + "Sagging hips", + "Flaring elbows too wide", + "Not going through full range of motion", + "Head dropping forward" + ] + }, + { + "name": "Sled Drag", + "muscleGroup": "Legs", + "equipment": "sled", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Ii3q9yFAwPY", + "secondaryMuscles": [ + "hamstrings" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise with sled targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Resistance Band Squat", + "muscleGroup": "Legs", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6rE0IYlMPZA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Banded Lateral Walk", + "muscleGroup": "Legs", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5VtGyiddPh4", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Trap Bar Squat", + "muscleGroup": "Legs", + "equipment": "trapBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=JfhNkNz59lI", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower your body by bending at the knees and hips with a weight on your back, then stand back up.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet shoulder-width apart", + "Brace core before descending", + "Push knees out over toes", + "Hit at least parallel depth", + "Drive up through heels and midfoot" + ], + "commonMistakes": [ + "Knees caving inward", + "Not hitting depth", + "Leaning too far forward", + "Butt wink at the bottom", + "Rising hips before chest" + ] + }, + { + "name": "Deadlift", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vfKwjT5-86k", + "secondaryMuscles": [ + "glutes", + "hamstrings", + "core" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Sumo Deadlift", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=e7oLkRlT2CQ", + "secondaryMuscles": [ + "glutes", + "quads" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Trap Bar Deadlift", + "muscleGroup": "Legs", + "equipment": "trapBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v-SrIcAp3vM", + "secondaryMuscles": [ + "quads", + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Romanian Deadlift", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5rIqP63yWFg", + "secondaryMuscles": [ + "glutes", + "lower back" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hinge at the hips while keeping legs slightly bent to lower a weight along your thighs, targeting hamstrings and glutes.", + "primaryMuscles": [ + "hamstrings", + "glutes", + "erector spinae" + ], + "formCues": [ + "Push hips back, don't squat down", + "Keep bar close to legs", + "Feel the stretch in hamstrings", + "Squeeze glutes at the top", + "Slight knee bend, shins stay vertical" + ], + "commonMistakes": [ + "Bending knees too much (turning it into a squat)", + "Rounding the back", + "Not hinging enough at the hips" + ] + }, + { + "name": "Stiff Leg Deadlift", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=VFIKI6ihCmc", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Barbell Hip Thrust", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4z_2oHvIvkA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Barbell Glute Bridge", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=0od5lwWMGV8", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on the floor and drive hips upward to target the glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet flat, knees bent at 90 degrees", + "Drive hips up squeezing glutes", + "Hold at the top", + "Lower with control" + ], + "commonMistakes": [ + "Not squeezing glutes at top", + "Pushing through toes", + "Hyperextending the back" + ] + }, + { + "name": "Dumbbell RDL", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=vnEANU7BqqY", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using dumbbells targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Single Leg Dumbbell RDL", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=R_fJ6H3FlVw", + "secondaryMuscles": [ + "glutes", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using dumbbells targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Hip Thrust", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=mC56j1VdFfA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Dumbbell Glute Bridge", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=V-Pk0ZfoszU", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on the floor and drive hips upward to target the glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet flat, knees bent at 90 degrees", + "Drive hips up squeezing glutes", + "Hold at the top", + "Lower with control" + ], + "commonMistakes": [ + "Not squeezing glutes at top", + "Pushing through toes", + "Hyperextending the back" + ] + }, + { + "name": "Lying Leg Curl", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nMRte-HF0zY", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Seated Leg Curl", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=t9sTSr-JYSs", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Single Leg Curl", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_e9vFU9-tkc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Glute Ham Raise", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=c2pWqsHR7FU", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Hip Thrust Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tztHvSLdXLA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Hip Abduction Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=01HilwRf8m8", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Hip Adduction Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6tJjQFK_Q9U", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Glute Kickback Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=gg2pvITT1IA", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on the floor and drive hips upward to target the glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet flat, knees bent at 90 degrees", + "Drive hips up squeezing glutes", + "Hold at the top", + "Lower with control" + ], + "commonMistakes": [ + "Not squeezing glutes at top", + "Pushing through toes", + "Hyperextending the back" + ] + }, + { + "name": "Reverse Hyper", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=i9tU7_w7rvw", + "secondaryMuscles": [ + "lower back" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise on a machine targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Smith Machine Hip Thrust", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=PqOnYf6Slkg", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Cable Pull-Through", + "muscleGroup": "Legs", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=IU-ERkjTKXA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "A legs exercise on a cable machine targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Cable Glute Kickback", + "muscleGroup": "Legs", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=A9aN_L4vexk", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": "ankleStrap", + "notes": null, + "meta": {}, + "description": "Lie on the floor and drive hips upward to target the glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet flat, knees bent at 90 degrees", + "Drive hips up squeezing glutes", + "Hold at the top", + "Lower with control" + ], + "commonMistakes": [ + "Not squeezing glutes at top", + "Pushing through toes", + "Hyperextending the back" + ] + }, + { + "name": "Cable Hip Abduction", + "muscleGroup": "Legs", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=KrX9ofkeBHY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": "ankleStrap", + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Cable Romanian Deadlift", + "muscleGroup": "Legs", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=dmzFNayDw00", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Kettlebell Swing", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=aSYap2yhW8s", + "secondaryMuscles": [ + "glutes", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using a kettlebell targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Kettlebell Single Leg Deadlift", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Lu1utKnijTA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Kettlebell Sumo Deadlift", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hinonqqzatk", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lift a loaded barbell from the floor to hip level by extending hips and knees.", + "primaryMuscles": [ + "erector spinae", + "glutes", + "hamstrings", + "trapezius" + ], + "formCues": [ + "Hinge at hips, keep back flat", + "Bar stays close to body", + "Drive through heels", + "Lock out hips at the top", + "Brace your core" + ], + "commonMistakes": [ + "Rounding the lower back", + "Bar drifting away from body", + "Jerking the weight off the floor", + "Hyperextending at the top", + "Looking up too much" + ] + }, + { + "name": "Nordic Curl", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_e9vFU9-tkc", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Glute Bridge", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=OUgsJ8-Vi0E", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on the floor and drive hips upward to target the glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet flat, knees bent at 90 degrees", + "Drive hips up squeezing glutes", + "Hold at the top", + "Lower with control" + ], + "commonMistakes": [ + "Not squeezing glutes at top", + "Pushing through toes", + "Hyperextending the back" + ] + }, + { + "name": "Single Leg Glute Bridge", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=AVAXhy6pl7o", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on the floor and drive hips upward to target the glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Feet flat, knees bent at 90 degrees", + "Drive hips up squeezing glutes", + "Hold at the top", + "Lower with control" + ], + "commonMistakes": [ + "Not squeezing glutes at top", + "Pushing through toes", + "Hyperextending the back" + ] + }, + { + "name": "Donkey Kick", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=YoOlLusFMYU", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using bodyweight only targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Fire Hydrant", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=9Rrz2R2Y6AQ", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using bodyweight only targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Clamshell", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qJhOH9-2yDw", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using bodyweight only targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Resistance Band Hip Thrust", + "muscleGroup": "Legs", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6oYSPzZlwL0", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Resistance Band Kickback", + "muscleGroup": "Legs", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=A9aN_L4vexk", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise with resistanceBand targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Stability Ball Leg Curl", + "muscleGroup": "Legs", + "equipment": "stabilityBall", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=XkESHgkTdFw", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Curl a weight with an overhand (pronated) grip to target the brachioradialis and forearm extensors.", + "primaryMuscles": [ + "brachioradialis", + "forearm extensors" + ], + "formCues": [ + "Overhand grip, thumbs on top", + "Keep elbows at sides", + "Curl to full contraction", + "Lower with control" + ], + "commonMistakes": [ + "Swinging for momentum", + "Grip too narrow or wide", + "Partial range of motion" + ] + }, + { + "name": "Standing Calf Raise Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=95itLfMBG40", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Seated Calf Raise Machine", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=y2ueC0LggrI", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Leg Press Calf Raise", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1cvpm--Y-4I", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Smith Machine Calf Raise", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=wlqTemUXPXY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Donkey Calf Raise", + "muscleGroup": "Legs", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=r30EoMPSNns", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Dumbbell Calf Raise", + "muscleGroup": "Legs", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=baEXLy09Ncc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Barbell Calf Raise", + "muscleGroup": "Legs", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=baEXLy09Ncc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Bodyweight Calf Raise", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=baEXLy09Ncc", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Single Leg Calf Raise", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ElcvJ0kjt6c", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Rise up onto your toes to contract the calf muscles, then lower back down.", + "primaryMuscles": [ + "gastrocnemius", + "soleus" + ], + "formCues": [ + "Full range of motion — stretch at bottom", + "Pause at the top and squeeze", + "Control the negative", + "Keep knees straight (standing) or bent (seated)" + ], + "commonMistakes": [ + "Bouncing at the bottom", + "Partial range of motion", + "Going too fast" + ] + }, + { + "name": "Cable Woodchop", + "muscleGroup": "Core", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ZDt4MCvjMAA", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise on a cable machine targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Cable Crunch", + "muscleGroup": "Core", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=dkGwcfo9zto", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": "rope", + "notes": null, + "meta": {}, + "description": "Kneel at a cable machine and crunch downward against resistance.", + "primaryMuscles": [ + "rectus abdominis" + ], + "formCues": [ + "Hold rope behind head", + "Crunch down curling spine", + "Squeeze abs at bottom", + "Don't just hinge at hips" + ], + "commonMistakes": [ + "Sitting back into hips (hip hinge instead of crunch)", + "Pulling with arms", + "Not enough spinal flexion" + ] + }, + { + "name": "Cable Pallof Press", + "muscleGroup": "Core", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=5aZ0IhJS8O8", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Cable Twist", + "muscleGroup": "Core", + "equipment": "cable", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=yglSetVOFeA", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit with torso leaned back and rotate side to side, optionally holding a weight.", + "primaryMuscles": [ + "obliques", + "rectus abdominis" + ], + "formCues": [ + "Lean back at ~45 degrees", + "Rotate through the torso, not just arms", + "Touch weight/hands to each side", + "Keep feet off floor for more challenge" + ], + "commonMistakes": [ + "Only moving arms without rotating torso", + "Rounding the back", + "Going too fast" + ] + }, + { + "name": "Ab Crunch Machine", + "muscleGroup": "Core", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=mnRhbUB3Fjs", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on your back and curl your shoulders off the floor to contract the abs.", + "primaryMuscles": [ + "rectus abdominis" + ], + "formCues": [ + "Hands behind head lightly (don't pull neck)", + "Curl shoulders up, not just head", + "Squeeze abs at top", + "Lower with control" + ], + "commonMistakes": [ + "Pulling on neck", + "Using momentum", + "Full sit-up (hip flexors take over)" + ] + }, + { + "name": "Torso Rotation Machine", + "muscleGroup": "Core", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6aAQcnoVCns", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise on a machine targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Captain's Chair Leg Raise", + "muscleGroup": "Core", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=1XgbnXtOUvk", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "GHD Sit-Up", + "muscleGroup": "Core", + "equipment": "machine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=M_NQVwZVCf0", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise on a machine targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Plank", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v25dawSzRTM", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold a push-up position on your forearms, keeping your body in a straight line.", + "primaryMuscles": [ + "rectus abdominis", + "transverse abdominis", + "obliques" + ], + "formCues": [ + "Straight line from head to heels", + "Engage core, squeeze glutes", + "Don't let hips sag or pike", + "Breathe steadily" + ], + "commonMistakes": [ + "Hips sagging", + "Hips piking up", + "Holding breath", + "Looking up (strains neck)" + ] + }, + { + "name": "Side Plank", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=BFOyHDlY2UE", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "beginner", + "isUnilateral": true, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold a push-up position on your forearms, keeping your body in a straight line.", + "primaryMuscles": [ + "rectus abdominis", + "transverse abdominis", + "obliques" + ], + "formCues": [ + "Straight line from head to heels", + "Engage core, squeeze glutes", + "Don't let hips sag or pike", + "Breathe steadily" + ], + "commonMistakes": [ + "Hips sagging", + "Hips piking up", + "Holding breath", + "Looking up (strains neck)" + ] + }, + { + "name": "Crunch", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=eeJ_CYqSoT4", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on your back and curl your shoulders off the floor to contract the abs.", + "primaryMuscles": [ + "rectus abdominis" + ], + "formCues": [ + "Hands behind head lightly (don't pull neck)", + "Curl shoulders up, not just head", + "Squeeze abs at top", + "Lower with control" + ], + "commonMistakes": [ + "Pulling on neck", + "Using momentum", + "Full sit-up (hip flexors take over)" + ] + }, + { + "name": "Bicycle Crunch", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=PAEo-zRSanM", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on your back and curl your shoulders off the floor to contract the abs.", + "primaryMuscles": [ + "rectus abdominis" + ], + "formCues": [ + "Hands behind head lightly (don't pull neck)", + "Curl shoulders up, not just head", + "Squeeze abs at top", + "Lower with control" + ], + "commonMistakes": [ + "Pulling on neck", + "Using momentum", + "Full sit-up (hip flexors take over)" + ] + }, + { + "name": "Reverse Crunch", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=I-qRngqd2wY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on your back and curl your shoulders off the floor to contract the abs.", + "primaryMuscles": [ + "rectus abdominis" + ], + "formCues": [ + "Hands behind head lightly (don't pull neck)", + "Curl shoulders up, not just head", + "Squeeze abs at top", + "Lower with control" + ], + "commonMistakes": [ + "Pulling on neck", + "Using momentum", + "Full sit-up (hip flexors take over)" + ] + }, + { + "name": "Sit-Up", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=pCX65Mtc_Kk", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Decline Sit-Up", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=dwB9Rp_patE", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "V-Up", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nfWQihJo-Mc", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Hollow Body Hold", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Xk-JcNj6lfY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dead Bug", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Aoipu_fl3HA", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on your back and alternately extend opposite arm and leg while keeping your lower back flat.", + "primaryMuscles": [ + "rectus abdominis", + "transverse abdominis" + ], + "formCues": [ + "Press lower back into the floor", + "Extend opposite arm and leg slowly", + "Breathe out as you extend", + "Return to start with control" + ], + "commonMistakes": [ + "Lower back arching off the floor", + "Moving too fast", + "Not breathing properly" + ] + }, + { + "name": "Bird Dog", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=_1j_HWknGLg", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Mountain Climber", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hZb6jTbCLeE", + "secondaryMuscles": [ + "hip flexors" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "From a push-up position, drive knees alternately toward your chest in a running motion.", + "primaryMuscles": [ + "rectus abdominis", + "hip flexors" + ], + "formCues": [ + "Keep hips level", + "Drive knees to chest", + "Maintain push-up position", + "Brace core throughout" + ], + "commonMistakes": [ + "Hips bouncing up and down", + "Not engaging core", + "Looking up" + ] + }, + { + "name": "Flutter Kick", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=tPmybsDX8ZY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Lying Leg Raise", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=0tzBVqiDwSs", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Raise weights out to the sides until arms are parallel to the floor.", + "primaryMuscles": [ + "lateral deltoid" + ], + "formCues": [ + "Slight bend in elbows", + "Lead with elbows, not hands", + "Raise to shoulder height", + "Control the negative", + "Slight forward lean helps isolation" + ], + "commonMistakes": [ + "Swinging weights up", + "Raising too high (traps take over)", + "Using straight arms", + "Shrugging shoulders" + ] + }, + { + "name": "Russian Twist", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=-BzNffL_6YE", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit with torso leaned back and rotate side to side, optionally holding a weight.", + "primaryMuscles": [ + "obliques", + "rectus abdominis" + ], + "formCues": [ + "Lean back at ~45 degrees", + "Rotate through the torso, not just arms", + "Touch weight/hands to each side", + "Keep feet off floor for more challenge" + ], + "commonMistakes": [ + "Only moving arms without rotating torso", + "Rounding the back", + "Going too fast" + ] + }, + { + "name": "Heel Touch", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=i-BBrCVNT9A", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Toe Touch", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=C2-bGR0k7kI", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Windshield Wiper", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=CZS3m8zBVSA", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dragon Flag", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=pvz7k5gO-DE", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "L-Sit", + "muscleGroup": "Core", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=XN7qnqooLC8", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using bodyweight only targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Hanging Leg Raise", + "muscleGroup": "Core", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=2n4UqRIJyk4", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hang from a bar and raise your legs to work the lower abs.", + "primaryMuscles": [ + "rectus abdominis", + "hip flexors" + ], + "formCues": [ + "Start from dead hang", + "Raise legs with control", + "Curl pelvis up at the top", + "Lower slowly, don't swing" + ], + "commonMistakes": [ + "Swinging for momentum", + "Not curling pelvis (just hip flexion)", + "Dropping legs on the way down" + ] + }, + { + "name": "Hanging Knee Raise", + "muscleGroup": "Core", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=2n4UqRIJyk4", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hang from a bar and raise your legs to work the lower abs.", + "primaryMuscles": [ + "rectus abdominis", + "hip flexors" + ], + "formCues": [ + "Start from dead hang", + "Raise legs with control", + "Curl pelvis up at the top", + "Lower slowly, don't swing" + ], + "commonMistakes": [ + "Swinging for momentum", + "Not curling pelvis (just hip flexion)", + "Dropping legs on the way down" + ] + }, + { + "name": "Toes to Bar", + "muscleGroup": "Core", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=kjYuPnmMjfo", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise with pullUpBar targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dead Hang", + "muscleGroup": "Core", + "equipment": "pullUpBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=cIcKUdhn9e0", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": "Also great for grip and shoulder health", + "meta": {}, + "description": "Lie on your back and alternately extend opposite arm and leg while keeping your lower back flat.", + "primaryMuscles": [ + "rectus abdominis", + "transverse abdominis" + ], + "formCues": [ + "Press lower back into the floor", + "Extend opposite arm and leg slowly", + "Breathe out as you extend", + "Return to start with control" + ], + "commonMistakes": [ + "Lower back arching off the floor", + "Moving too fast", + "Not breathing properly" + ] + }, + { + "name": "Kneeling Ab Rollout", + "muscleGroup": "Core", + "equipment": "abWheel", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ndc391RFNUM", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Kneel and roll an ab wheel forward, extending your body, then pull back to the start.", + "primaryMuscles": [ + "rectus abdominis", + "obliques" + ], + "formCues": [ + "Start on knees", + "Roll forward with control", + "Extend as far as you can maintain form", + "Pull back using abs, not hips" + ], + "commonMistakes": [ + "Sagging lower back", + "Not engaging core throughout", + "Going too far and losing control" + ] + }, + { + "name": "Standing Ab Rollout", + "muscleGroup": "Core", + "equipment": "abWheel", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=PEVQ-GWQjyc", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Kneel and roll an ab wheel forward, extending your body, then pull back to the start.", + "primaryMuscles": [ + "rectus abdominis", + "obliques" + ], + "formCues": [ + "Start on knees", + "Roll forward with control", + "Extend as far as you can maintain form", + "Pull back using abs, not hips" + ], + "commonMistakes": [ + "Sagging lower back", + "Not engaging core throughout", + "Going too far and losing control" + ] + }, + { + "name": "Medicine Ball Slam", + "muscleGroup": "Core", + "equipment": "medicineBall", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6vXHh-Lhb2o", + "secondaryMuscles": [ + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise with medicineBall targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Medicine Ball Russian Twist", + "muscleGroup": "Core", + "equipment": "medicineBall", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=99d2aPkgmDA", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Sit with torso leaned back and rotate side to side, optionally holding a weight.", + "primaryMuscles": [ + "obliques", + "rectus abdominis" + ], + "formCues": [ + "Lean back at ~45 degrees", + "Rotate through the torso, not just arms", + "Touch weight/hands to each side", + "Keep feet off floor for more challenge" + ], + "commonMistakes": [ + "Only moving arms without rotating torso", + "Rounding the back", + "Going too fast" + ] + }, + { + "name": "Medicine Ball Woodchop", + "muscleGroup": "Core", + "equipment": "medicineBall", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4cBqkcUSA_E", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise with medicineBall targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Side Bend", + "muscleGroup": "Core", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=hFhN7K8VpMs", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using dumbbells targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Woodchop", + "muscleGroup": "Core", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8u6lYQBpFiQ", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise using dumbbells targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Weighted Sit-Up", + "muscleGroup": "Core", + "equipment": "plate", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=kZvSaq192cg", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A core exercise with plate targeting the core muscles.", + "primaryMuscles": [ + "core" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Weighted Plank", + "muscleGroup": "Core", + "equipment": "plate", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=QYdpP6KXYf8", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Hold a push-up position on your forearms, keeping your body in a straight line.", + "primaryMuscles": [ + "rectus abdominis", + "transverse abdominis", + "obliques" + ], + "formCues": [ + "Straight line from head to heels", + "Engage core, squeeze glutes", + "Don't let hips sag or pike", + "Breathe steadily" + ], + "commonMistakes": [ + "Hips sagging", + "Hips piking up", + "Holding breath", + "Looking up (strains neck)" + ] + }, + { + "name": "Stability Ball Crunch", + "muscleGroup": "Core", + "equipment": "stabilityBall", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=8NGXHJ2bEzE", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lie on your back and curl your shoulders off the floor to contract the abs.", + "primaryMuscles": [ + "rectus abdominis" + ], + "formCues": [ + "Hands behind head lightly (don't pull neck)", + "Curl shoulders up, not just head", + "Squeeze abs at top", + "Lower with control" + ], + "commonMistakes": [ + "Pulling on neck", + "Using momentum", + "Full sit-up (hip flexors take over)" + ] + }, + { + "name": "Resistance Band Pallof Press", + "muscleGroup": "Core", + "equipment": "resistanceBand", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=JEjKGg7dVVA", + "secondaryMuscles": [ + "obliques" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Power Clean", + "muscleGroup": "Other", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=E2z5zK5V-MM", + "secondaryMuscles": [ + "traps", + "quads" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a barbell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Hang Clean", + "muscleGroup": "Other", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=frSZNeT0FIk", + "secondaryMuscles": [ + "traps", + "quads" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a barbell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Clean and Jerk", + "muscleGroup": "Other", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=E2z5zK5V-MM", + "secondaryMuscles": [ + "quads", + "shoulders" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a barbell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Snatch", + "muscleGroup": "Other", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=eZvy51ucmOc", + "secondaryMuscles": [ + "quads", + "shoulders" + ], + "difficulty": "advanced", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a barbell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Barbell Thruster", + "muscleGroup": "Other", + "equipment": "barbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=Zvt5-mugUco", + "secondaryMuscles": [ + "quads", + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a barbell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Thruster", + "muscleGroup": "Other", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=qnOikHllwWc", + "secondaryMuscles": [ + "quads", + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using dumbbells targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Snatch", + "muscleGroup": "Other", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=0yd9443Y-fg", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using dumbbells targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Dumbbell Clean and Press", + "muscleGroup": "Other", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=sZ4XMWn8bAU", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Push a weighted platform away from you using your legs while seated on a machine.", + "primaryMuscles": [ + "quadriceps", + "glutes", + "hamstrings" + ], + "formCues": [ + "Place feet shoulder-width on platform", + "Lower until knees are at 90 degrees", + "Press through full foot", + "Don't lock knees at top", + "Keep lower back against pad" + ], + "commonMistakes": [ + "Going too deep (lower back lifts off pad)", + "Locking out knees", + "Placing feet too high or low" + ] + }, + { + "name": "Kettlebell Clean", + "muscleGroup": "Other", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=fRdlDRkAT-w", + "secondaryMuscles": [ + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a kettlebell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Kettlebell Snatch", + "muscleGroup": "Other", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=EFd4FXvpK9Y", + "secondaryMuscles": [], + "difficulty": "advanced", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a kettlebell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Kettlebell Thruster", + "muscleGroup": "Other", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=2qRbPR72N4Y", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a kettlebell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Farmer's Walk", + "muscleGroup": "Other", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=nqGfgIVteoM", + "secondaryMuscles": [ + "grip", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using dumbbells targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Kettlebell Farmer's Walk", + "muscleGroup": "Other", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=4d-4gKn_lKk", + "secondaryMuscles": [ + "grip", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using a kettlebell targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Trap Bar Farmer's Walk", + "muscleGroup": "Other", + "equipment": "trapBar", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=WEZdzYWaBHE", + "secondaryMuscles": [ + "grip", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with trapBar targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Overhead Carry", + "muscleGroup": "Other", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=dpG2fBuhql0", + "secondaryMuscles": [ + "core", + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Press a weight from shoulder level overhead to full arm extension while standing or seated.", + "primaryMuscles": [ + "anterior deltoid", + "lateral deltoid", + "triceps" + ], + "formCues": [ + "Brace core and squeeze glutes", + "Press bar straight up, moving head out of the way", + "Lock out overhead", + "Lower to chin/upper chest level" + ], + "commonMistakes": [ + "Excessive back lean", + "Not locking out", + "Pressing in front of the body instead of overhead", + "Flaring ribs" + ] + }, + { + "name": "Suitcase Carry", + "muscleGroup": "Other", + "equipment": "dumbbell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v8O0kNuvp_k", + "secondaryMuscles": [ + "obliques", + "grip" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using dumbbells targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Battle Rope Slams", + "muscleGroup": "Other", + "equipment": "battleRopes", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=X5g7P_M8Wo4", + "secondaryMuscles": [ + "core", + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with battleRopes targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Battle Rope Waves", + "muscleGroup": "Other", + "equipment": "battleRopes", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=zw0OMi00X5g", + "secondaryMuscles": [ + "core", + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with battleRopes targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Prowler Push", + "muscleGroup": "Other", + "equipment": "sled", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=3KWK7SIdPz4", + "secondaryMuscles": [ + "quads", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Lower and raise your body using your arms while maintaining a straight line from head to heels.", + "primaryMuscles": [ + "pectoralis major", + "anterior deltoid", + "triceps" + ], + "formCues": [ + "Keep body in a straight line", + "Lower chest to just above the floor", + "Push through palms evenly", + "Keep core engaged throughout" + ], + "commonMistakes": [ + "Sagging hips", + "Flaring elbows too wide", + "Not going through full range of motion", + "Head dropping forward" + ] + }, + { + "name": "Burpee", + "muscleGroup": "Other", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=G2hv_NYhM-A", + "secondaryMuscles": [ + "chest", + "quads", + "core" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using bodyweight only targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Bear Crawl", + "muscleGroup": "Other", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=LCVMqEmgglo", + "secondaryMuscles": [ + "core", + "shoulders" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using bodyweight only targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Inchworm", + "muscleGroup": "Other", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=VSp0z7Mp5IU", + "secondaryMuscles": [ + "core", + "hamstrings" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise using bodyweight only targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Treadmill Run", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=EZ7OkjYGhdY", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Incline Walk", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=JGzA6FPzZS8", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Stairmaster", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=ww3YV_N6U_U", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Elliptical", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=YWfswVvOaiI", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Stationary Bike", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=XugMoMDxyhM", + "secondaryMuscles": [], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": false, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Rowing Machine", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=6_eLpWiNijE", + "secondaryMuscles": [ + "back", + "legs" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Assault Bike", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=RPY7HTGfOiU", + "secondaryMuscles": [], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Ski Erg", + "muscleGroup": "Other", + "equipment": "cardioMachine", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=P7qpoJmX91I", + "secondaryMuscles": [ + "core", + "back" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A other exercise with cardioMachine targeting the other muscles.", + "primaryMuscles": [ + "other" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Bodyweight Hip Thrust", + "muscleGroup": "Legs", + "equipment": "bodyweight", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=RR0oZhhUVWo", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "beginner", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "Drive hips upward against resistance with upper back on a bench to target glutes.", + "primaryMuscles": [ + "glutes", + "hamstrings" + ], + "formCues": [ + "Upper back on bench, feet flat on floor", + "Drive through heels", + "Squeeze glutes hard at the top", + "Chin tucked, look forward at top", + "Full hip extension" + ], + "commonMistakes": [ + "Hyperextending lower back", + "Not squeezing at the top", + "Feet too close or far from body", + "Pushing through toes" + ] + }, + { + "name": "Kettlebell RDL", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=v69WkYRcdJA", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": false, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using a kettlebell targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + }, + { + "name": "Single Leg Kettlebell RDL", + "muscleGroup": "Legs", + "equipment": "kettlebell", + "coachMode": null, + "videoURL": "https://www.youtube.com/watch?v=s32cCgmRV3I", + "secondaryMuscles": [ + "glutes" + ], + "difficulty": "intermediate", + "isUnilateral": true, + "isCompound": true, + "cableAttachment": null, + "notes": null, + "meta": {}, + "description": "A legs exercise using a kettlebell targeting the legs muscles.", + "primaryMuscles": [ + "legs" + ], + "formCues": [], + "commonMistakes": [] + } + ] +} \ No newline at end of file diff --git a/App Core/Resources/Swift Code/mAICoachApp.swift b/App Core/Resources/Swift Code/mAICoachApp.swift index 31d4d51..cbac02f 100644 --- a/App Core/Resources/Swift Code/mAICoachApp.swift +++ b/App Core/Resources/Swift Code/mAICoachApp.swift @@ -10,8 +10,17 @@ import SwiftUI @main struct MAICoachApp: App { @StateObject private var session = AuthSession() + @StateObject private var workoutStore = WorkoutStore() + @StateObject private var restTimer = RestTimerManager() + @StateObject private var tutorial = TutorialManager() + var body: some Scene { - WindowGroup { RootView().environmentObject(session) } + WindowGroup { + RootView() + .environmentObject(session) + .environmentObject(workoutStore) + .environmentObject(restTimer) + .environmentObject(tutorial) + } } } - diff --git a/App Core/mAICoach.xcodeproj/project.pbxproj b/App Core/mAICoach.xcodeproj/project.pbxproj index c16b358..93e65cb 100644 --- a/App Core/mAICoach.xcodeproj/project.pbxproj +++ b/App Core/mAICoach.xcodeproj/project.pbxproj @@ -7,16 +7,24 @@ objects = { /* Begin PBXBuildFile section */ + 173FCE3910153B9B7FF501A6 /* TemplateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D28688DBCC5E78909BC0B1 /* TemplateDetailView.swift */; }; + 18A38538BA4E2DF86CF17550 /* RestTimerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C649F3B0D3D40CAA1DCF8A /* RestTimerManager.swift */; }; + 20EC682CE0D802B65B4E16D5 /* SupersetBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9CEE506BA6FD4DB17CDE87F /* SupersetBlockView.swift */; }; + 2F33E79D3CBA18E1641B532A /* WorkoutBuilderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF9B8BB040E3A79354CC068 /* WorkoutBuilderView.swift */; }; 3424B5C8F0A225F444807078 /* Pods_mAICoach.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2CD2DD440060C0AF0078CD6 /* Pods_mAICoach.framework */; }; + 4E751254D1F2F5424A43CDDD /* ExerciseStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6979FD7A9B845CFAA0B5A1D2 /* ExerciseStatsView.swift */; }; + 5337C697DD141D31F8E1EAB3 /* PersonalRecordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF5BF65DAE615F51211CD3F /* PersonalRecordsView.swift */; }; + 5E9CC310756A99E7C712AAB2 /* DataBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC9CF97A775F8DE16977B0C /* DataBackupManager.swift */; }; + 727AD3C6E80695B982DB0285 /* TemplatePickerForDayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF0F6B9F6E864F7F4873CA5 /* TemplatePickerForDayView.swift */; }; + 76852B61C6D97D09B7E1D9AF /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 754847BA6E45CCAE4E4B5415 /* StatsView.swift */; }; + 7737542D6A594608CCBC8CF6 /* AllTemplatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAFB83DC371B60213FA24BF3 /* AllTemplatesView.swift */; }; 813188FF2ED76BD6008DA8D4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188FC2ED76BD6008DA8D4 /* RootView.swift */; }; - 813189012ED76BD6008DA8D4 /* BigRectButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F12ED76BD6008DA8D4 /* BigRectButton.swift */; }; 813189022ED76BD6008DA8D4 /* BootScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F22ED76BD6008DA8D4 /* BootScreen.swift */; }; 813189032ED76BD6008DA8D4 /* PoseOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188FB2ED76BD6008DA8D4 /* PoseOverlay.swift */; }; 813189042ED76BD6008DA8D4 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F72ED76BD6008DA8D4 /* HomeView.swift */; }; 813189052ED76BD6008DA8D4 /* BenchSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F02ED76BD6008DA8D4 /* BenchSessionView.swift */; }; 813189062ED76BD6008DA8D4 /* mAICoachApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F92ED76BD6008DA8D4 /* mAICoachApp.swift */; }; 813189072ED76BD6008DA8D4 /* BenchInferenceEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188EF2ED76BD6008DA8D4 /* BenchInferenceEngine.swift */; }; - 813189082ED76BD6008DA8D4 /* CoachView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F52ED76BD6008DA8D4 /* CoachView.swift */; }; 813189092ED76BD6008DA8D4 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188FD2ED76BD6008DA8D4 /* SignInView.swift */; }; 8131890A2ED76BD6008DA8D4 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188F32ED76BD6008DA8D4 /* CameraController.swift */; }; 8131890B2ED76BD6008DA8D4 /* AuthSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813188EE2ED76BD6008DA8D4 /* AuthSession.swift */; }; @@ -27,27 +35,50 @@ 8140DC002F49B000000ADB39 /* Audio in Resources */ = {isa = PBXBuildFile; fileRef = 8140DBFF2F49B000000ADB39 /* Audio */; }; 8140DC112F49C000000ADB39 /* pose_landmarker_full.task in Resources */ = {isa = PBXBuildFile; fileRef = 8140DC102F49C000000ADB39 /* pose_landmarker_full.task */; }; 81CB95C92E9D53FD0080FCBC /* pose_landmarker_lite.task in Resources */ = {isa = PBXBuildFile; fileRef = 81CB95C82E9D53FD0080FCBC /* pose_landmarker_lite.task */; }; + 8991BD489515DEFB4921758A /* WorkoutStatsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ADEBEE1A4D991327F60529 /* WorkoutStatsEngine.swift */; }; + 8D8F2230638FEC2C48C51D83 /* WorkoutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21B3F72463F3E9E56171565 /* WorkoutDetailView.swift */; }; + A1CAT00012FB0000011111A /* ExerciseCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1CAT00002FB0000011111A /* ExerciseCatalog.swift */; }; + A1HST00012FC0000033333A /* ExerciseHistorySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1HST00002FC0000033333A /* ExerciseHistorySheet.swift */; }; + A1NEW00012FA6B000111AAA /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW00002FA6B000111AAA /* DesignTokens.swift */; }; + A1NEW00032FA6B000111AAA /* WorkoutModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW00022FA6B000111AAA /* WorkoutModel.swift */; }; + A1NEW00052FA6B000111AAA /* ExerciseCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW00042FA6B000111AAA /* ExerciseCardView.swift */; }; + A1NEW00072FA6B000111AAA /* WorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW00062FA6B000111AAA /* WorkoutView.swift */; }; + A1NEW00092FA6B000111AAA /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW00082FA6B000111AAA /* MainTabView.swift */; }; + A1NEW000B2FA6B000111AAA /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW000A2FA6B000111AAA /* ProfileView.swift */; }; + A1NEW000D2FA6B000111AAA /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1NEW000C2FA6B000111AAA /* WelcomeView.swift */; }; + A1PKR00012FB0000022222A /* ExercisePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1PKR00002FB0000022222A /* ExercisePickerView.swift */; }; + A1WHS00012FC0000044444A /* WorkoutHistorySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1WHS00002FC0000044444A /* WorkoutHistorySheet.swift */; }; + A1WTP00012FC0000055555A /* WorkoutTemplatePickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1WTP00002FC0000055555A /* WorkoutTemplatePickerSheet.swift */; }; B10F6EED2FA3A2D800C1F7D1 /* demo_bench.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = B10F6EEC2FA3A2D800C1F7D1 /* demo_bench.mp4 */; }; B10F6EEF2FA3A2E200C1F7D1 /* bench_mlp_v1_weights.json in Resources */ = {isa = PBXBuildFile; fileRef = B10F6EEE2FA3A2E200C1F7D1 /* bench_mlp_v1_weights.json */; }; B10F6EF12FA3A2E300C1F7D1 /* bench_mlp_v2_weights.json in Resources */ = {isa = PBXBuildFile; fileRef = B10F6EF02FA3A2E300C1F7D1 /* bench_mlp_v2_weights.json */; }; B1HEAVY302FA4C0000AAA555 /* pose_landmarker_heavy.task in Resources */ = {isa = PBXBuildFile; fileRef = B1HEAVY2F2FA4C0000AAA555 /* pose_landmarker_heavy.task */; }; B1HEAVY312FA4C0000AAA555 /* DemoPlayerPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1NEWPIPE2FA4C111AAA555 /* DemoPlayerPipeline.swift */; }; - B1WORK012FA5A0000BBB666 /* MyWorkoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1WORK002FA5A0000BBB666 /* MyWorkoutsView.swift */; }; + C8148C7EFDD30EE9B9B2DA52 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D60E91BF2878AAE066AB20 /* TutorialManager.swift */; }; + C881C7591663EB96A34253E9 /* TutorialOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96018D29A458719C98517427 /* TutorialOverlay.swift */; }; D465426C852A021F6EB4CE4B /* AudioCoach.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A5464EA3C213A4106C992A /* AudioCoach.swift */; }; + D70D4739F09A404089EEF1C3 /* exercise_library.json in Resources */ = {isa = PBXBuildFile; fileRef = D70D4739F09A404089EEF1C4 /* exercise_library.json */; }; + DCED5075D97675555480635C /* ConfettiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2863B9520C07B63843633C20 /* ConfettiView.swift */; }; + FFA2E2E532E4C523E54E7CB1 /* ArchivedTemplatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4455FAA5858093E635FF33 /* ArchivedTemplatesView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 12786BB56F7A98F3058D101D /* Pods-mAICoach.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mAICoach.debug.xcconfig"; path = "Target Support Files/Pods-mAICoach/Pods-mAICoach.debug.xcconfig"; sourceTree = ""; }; + 18ADEBEE1A4D991327F60529 /* WorkoutStatsEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutStatsEngine.swift; sourceTree = ""; }; + 2863B9520C07B63843633C20 /* ConfettiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfettiView.swift; sourceTree = ""; }; + 3EC9CF97A775F8DE16977B0C /* DataBackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBackupManager.swift; sourceTree = ""; }; + 4EF9B8BB040E3A79354CC068 /* WorkoutBuilderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutBuilderView.swift; sourceTree = ""; }; + 5AF5BF65DAE615F51211CD3F /* PersonalRecordsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PersonalRecordsView.swift; sourceTree = ""; }; + 6979FD7A9B845CFAA0B5A1D2 /* ExerciseStatsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExerciseStatsView.swift; sourceTree = ""; }; + 754847BA6E45CCAE4E4B5415 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; 813188EB2ED769A9008DA8D4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 813188ED2ED76BD6008DA8D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 813188EE2ED76BD6008DA8D4 /* AuthSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthSession.swift; sourceTree = ""; }; 813188EF2ED76BD6008DA8D4 /* BenchInferenceEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchInferenceEngine.swift; sourceTree = ""; }; 813188F02ED76BD6008DA8D4 /* BenchSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchSessionView.swift; sourceTree = ""; }; - 813188F12ED76BD6008DA8D4 /* BigRectButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigRectButton.swift; sourceTree = ""; }; 813188F22ED76BD6008DA8D4 /* BootScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootScreen.swift; sourceTree = ""; }; 813188F32ED76BD6008DA8D4 /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = ""; }; 813188F42ED76BD6008DA8D4 /* CameraPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; - 813188F52ED76BD6008DA8D4 /* CoachView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachView.swift; sourceTree = ""; }; 813188F72ED76BD6008DA8D4 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 813188F92ED76BD6008DA8D4 /* mAICoachApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mAICoachApp.swift; sourceTree = ""; }; 813188FA2ED76BD6008DA8D4 /* PoseLandmarkerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoseLandmarkerService.swift; sourceTree = ""; }; @@ -60,14 +91,35 @@ 81CB957C2E9C904A0080FCBC /* mAICoach.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mAICoach.app; sourceTree = BUILT_PRODUCTS_DIR; }; 81CB95C82E9D53FD0080FCBC /* pose_landmarker_lite.task */ = {isa = PBXFileReference; lastKnownFileType = file; path = pose_landmarker_lite.task; sourceTree = ""; }; 86A5464EA3C213A4106C992A /* AudioCoach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AudioCoach.swift; path = "Resources/Swift Code/AudioCoach.swift"; sourceTree = SOURCE_ROOT; }; + 94D60E91BF2878AAE066AB20 /* TutorialManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TutorialManager.swift; path = "Resources/Swift Code/TutorialManager.swift"; sourceTree = SOURCE_ROOT; }; + 96018D29A458719C98517427 /* TutorialOverlay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TutorialOverlay.swift; path = "Resources/Swift Code/TutorialOverlay.swift"; sourceTree = SOURCE_ROOT; }; + A1CAT00002FB0000011111A /* ExerciseCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseCatalog.swift; sourceTree = ""; }; + A1HST00002FC0000033333A /* ExerciseHistorySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseHistorySheet.swift; sourceTree = ""; }; + A1NEW00002FA6B000111AAA /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = ""; }; + A1NEW00022FA6B000111AAA /* WorkoutModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutModel.swift; sourceTree = ""; }; + A1NEW00042FA6B000111AAA /* ExerciseCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseCardView.swift; sourceTree = ""; }; + A1NEW00062FA6B000111AAA /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = ""; }; + A1NEW00082FA6B000111AAA /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; + A1NEW000A2FA6B000111AAA /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + A1NEW000C2FA6B000111AAA /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; + A1PKR00002FB0000022222A /* ExercisePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExercisePickerView.swift; sourceTree = ""; }; + A1WHS00002FC0000044444A /* WorkoutHistorySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutHistorySheet.swift; sourceTree = ""; }; + A1WTP00002FC0000055555A /* WorkoutTemplatePickerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutTemplatePickerSheet.swift; sourceTree = ""; }; B10F6EEC2FA3A2D800C1F7D1 /* demo_bench.mp4 */ = {isa = PBXFileReference; lastKnownFileType = video; path = demo_bench.mp4; sourceTree = ""; }; B10F6EEE2FA3A2E200C1F7D1 /* bench_mlp_v1_weights.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bench_mlp_v1_weights.json; sourceTree = ""; }; B10F6EF02FA3A2E300C1F7D1 /* bench_mlp_v2_weights.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bench_mlp_v2_weights.json; sourceTree = ""; }; B1HEAVY2F2FA4C0000AAA555 /* pose_landmarker_heavy.task */ = {isa = PBXFileReference; lastKnownFileType = file; path = pose_landmarker_heavy.task; sourceTree = ""; }; B1NEWPIPE2FA4C111AAA555 /* DemoPlayerPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoPlayerPipeline.swift; sourceTree = ""; }; - B1WORK002FA5A0000BBB666 /* MyWorkoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyWorkoutsView.swift; sourceTree = ""; }; + D21B3F72463F3E9E56171565 /* WorkoutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutDetailView.swift; sourceTree = ""; }; + D70D4739F09A404089EEF1C4 /* exercise_library.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = exercise_library.json; sourceTree = ""; }; + DAFB83DC371B60213FA24BF3 /* AllTemplatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllTemplatesView.swift; sourceTree = ""; }; + DCF0F6B9F6E864F7F4873CA5 /* TemplatePickerForDayView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TemplatePickerForDayView.swift; sourceTree = ""; }; E36E9DDCC48592ADDAD18110 /* Pods-mAICoach.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mAICoach.release.xcconfig"; path = "Target Support Files/Pods-mAICoach/Pods-mAICoach.release.xcconfig"; sourceTree = ""; }; + F2C649F3B0D3D40CAA1DCF8A /* RestTimerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RestTimerManager.swift; sourceTree = ""; }; F2CD2DD440060C0AF0078CD6 /* Pods_mAICoach.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_mAICoach.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F6D28688DBCC5E78909BC0B1 /* TemplateDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateDetailView.swift; sourceTree = ""; }; + F9CEE506BA6FD4DB17CDE87F /* SupersetBlockView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SupersetBlockView.swift; sourceTree = ""; }; + FD4455FAA5858093E635FF33 /* ArchivedTemplatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchivedTemplatesView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,6 +142,13 @@ name = Frameworks; sourceTree = ""; }; + 6DA7191338A8E1A854E75183 /* Swift Code */ = { + isa = PBXGroup; + children = ( + ); + name = "Swift Code"; + sourceTree = ""; + }; 813188FE2ED76BD6008DA8D4 /* Swift Code */ = { isa = PBXGroup; children = ( @@ -97,19 +156,43 @@ 813188EE2ED76BD6008DA8D4 /* AuthSession.swift */, 813188EF2ED76BD6008DA8D4 /* BenchInferenceEngine.swift */, 813188F02ED76BD6008DA8D4 /* BenchSessionView.swift */, - 813188F12ED76BD6008DA8D4 /* BigRectButton.swift */, 813188F22ED76BD6008DA8D4 /* BootScreen.swift */, 813188F32ED76BD6008DA8D4 /* CameraController.swift */, 813188F42ED76BD6008DA8D4 /* CameraPreview.swift */, - 813188F52ED76BD6008DA8D4 /* CoachView.swift */, B1NEWPIPE2FA4C111AAA555 /* DemoPlayerPipeline.swift */, + A1NEW00002FA6B000111AAA /* DesignTokens.swift */, + A1NEW00042FA6B000111AAA /* ExerciseCardView.swift */, + A1HST00002FC0000033333A /* ExerciseHistorySheet.swift */, + A1WHS00002FC0000044444A /* WorkoutHistorySheet.swift */, + A1WTP00002FC0000055555A /* WorkoutTemplatePickerSheet.swift */, + A1CAT00002FB0000011111A /* ExerciseCatalog.swift */, + D70D4739F09A404089EEF1C4 /* exercise_library.json */, + A1PKR00002FB0000022222A /* ExercisePickerView.swift */, 813188F72ED76BD6008DA8D4 /* HomeView.swift */, + DAFB83DC371B60213FA24BF3 /* AllTemplatesView.swift */, + 2863B9520C07B63843633C20 /* ConfettiView.swift */, + 754847BA6E45CCAE4E4B5415 /* StatsView.swift */, + 18ADEBEE1A4D991327F60529 /* WorkoutStatsEngine.swift */, + 3EC9CF97A775F8DE16977B0C /* DataBackupManager.swift */, + FD4455FAA5858093E635FF33 /* ArchivedTemplatesView.swift */, + F6D28688DBCC5E78909BC0B1 /* TemplateDetailView.swift */, + 4EF9B8BB040E3A79354CC068 /* WorkoutBuilderView.swift */, + D21B3F72463F3E9E56171565 /* WorkoutDetailView.swift */, + A1NEW00082FA6B000111AAA /* MainTabView.swift */, 813188F92ED76BD6008DA8D4 /* mAICoachApp.swift */, - B1WORK002FA5A0000BBB666 /* MyWorkoutsView.swift */, + A1NEW000A2FA6B000111AAA /* ProfileView.swift */, 813188FA2ED76BD6008DA8D4 /* PoseLandmarkerService.swift */, 813188FB2ED76BD6008DA8D4 /* PoseOverlay.swift */, 813188FC2ED76BD6008DA8D4 /* RootView.swift */, 813188FD2ED76BD6008DA8D4 /* SignInView.swift */, + A1NEW000C2FA6B000111AAA /* WelcomeView.swift */, + A1NEW00022FA6B000111AAA /* WorkoutModel.swift */, + A1NEW00062FA6B000111AAA /* WorkoutView.swift */, + DCF0F6B9F6E864F7F4873CA5 /* TemplatePickerForDayView.swift */, + 6979FD7A9B845CFAA0B5A1D2 /* ExerciseStatsView.swift */, + F2C649F3B0D3D40CAA1DCF8A /* RestTimerManager.swift */, + 5AF5BF65DAE615F51211CD3F /* PersonalRecordsView.swift */, + F9CEE506BA6FD4DB17CDE87F /* SupersetBlockView.swift */, ); path = "Swift Code"; sourceTree = ""; @@ -124,6 +207,8 @@ E5E17A5711B60D1EE82A126E /* Pods */, 3ED2ACFF6A2830E8BBF7C64A /* Frameworks */, 86A5464EA3C213A4106C992A /* AudioCoach.swift */, + 6DA7191338A8E1A854E75183 /* Swift Code */, + A579A8FA8B172BC6CE0B56F9 /* mAICoach */, ); sourceTree = ""; }; @@ -150,6 +235,15 @@ path = Resources; sourceTree = ""; }; + A579A8FA8B172BC6CE0B56F9 /* mAICoach */ = { + isa = PBXGroup; + children = ( + 94D60E91BF2878AAE066AB20 /* TutorialManager.swift */, + 96018D29A458719C98517427 /* TutorialOverlay.swift */, + ); + name = mAICoach; + sourceTree = ""; + }; E5E17A5711B60D1EE82A126E /* Pods */ = { isa = PBXGroup; children = ( @@ -227,6 +321,7 @@ B10F6EEF2FA3A2E200C1F7D1 /* bench_mlp_v1_weights.json in Resources */, B10F6EF12FA3A2E300C1F7D1 /* bench_mlp_v2_weights.json in Resources */, B1HEAVY302FA4C0000AAA555 /* pose_landmarker_heavy.task in Resources */, + D70D4739F09A404089EEF1C3 /* exercise_library.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -263,23 +358,48 @@ buildActionMask = 2147483647; files = ( 813188FF2ED76BD6008DA8D4 /* RootView.swift in Sources */, - 813189012ED76BD6008DA8D4 /* BigRectButton.swift in Sources */, 813189022ED76BD6008DA8D4 /* BootScreen.swift in Sources */, 813189032ED76BD6008DA8D4 /* PoseOverlay.swift in Sources */, 813189042ED76BD6008DA8D4 /* HomeView.swift in Sources */, + 7737542D6A594608CCBC8CF6 /* AllTemplatesView.swift in Sources */, + DCED5075D97675555480635C /* ConfettiView.swift in Sources */, + 76852B61C6D97D09B7E1D9AF /* StatsView.swift in Sources */, + 8991BD489515DEFB4921758A /* WorkoutStatsEngine.swift in Sources */, + 5E9CC310756A99E7C712AAB2 /* DataBackupManager.swift in Sources */, + FFA2E2E532E4C523E54E7CB1 /* ArchivedTemplatesView.swift in Sources */, + 173FCE3910153B9B7FF501A6 /* TemplateDetailView.swift in Sources */, + 2F33E79D3CBA18E1641B532A /* WorkoutBuilderView.swift in Sources */, + 8D8F2230638FEC2C48C51D83 /* WorkoutDetailView.swift in Sources */, 8140DB2A2F49A028000ADB39 /* SettingsView.swift in Sources */, 813189052ED76BD6008DA8D4 /* BenchSessionView.swift in Sources */, 813189062ED76BD6008DA8D4 /* mAICoachApp.swift in Sources */, 813189072ED76BD6008DA8D4 /* BenchInferenceEngine.swift in Sources */, - 813189082ED76BD6008DA8D4 /* CoachView.swift in Sources */, - B1WORK012FA5A0000BBB666 /* MyWorkoutsView.swift in Sources */, 813189092ED76BD6008DA8D4 /* SignInView.swift in Sources */, + A1NEW00012FA6B000111AAA /* DesignTokens.swift in Sources */, + A1NEW00032FA6B000111AAA /* WorkoutModel.swift in Sources */, + A1NEW00052FA6B000111AAA /* ExerciseCardView.swift in Sources */, + A1HST00012FC0000033333A /* ExerciseHistorySheet.swift in Sources */, + A1WHS00012FC0000044444A /* WorkoutHistorySheet.swift in Sources */, + A1WTP00012FC0000055555A /* WorkoutTemplatePickerSheet.swift in Sources */, + A1NEW00072FA6B000111AAA /* WorkoutView.swift in Sources */, + A1CAT00012FB0000011111A /* ExerciseCatalog.swift in Sources */, + A1PKR00012FB0000022222A /* ExercisePickerView.swift in Sources */, + A1NEW00092FA6B000111AAA /* MainTabView.swift in Sources */, + A1NEW000B2FA6B000111AAA /* ProfileView.swift in Sources */, + A1NEW000D2FA6B000111AAA /* WelcomeView.swift in Sources */, 8131890A2ED76BD6008DA8D4 /* CameraController.swift in Sources */, 8131890B2ED76BD6008DA8D4 /* AuthSession.swift in Sources */, 8131890C2ED76BD6008DA8D4 /* PoseLandmarkerService.swift in Sources */, 8131890D2ED76BD6008DA8D4 /* CameraPreview.swift in Sources */, B1HEAVY312FA4C0000AAA555 /* DemoPlayerPipeline.swift in Sources */, D465426C852A021F6EB4CE4B /* AudioCoach.swift in Sources */, + 727AD3C6E80695B982DB0285 /* TemplatePickerForDayView.swift in Sources */, + 4E751254D1F2F5424A43CDDD /* ExerciseStatsView.swift in Sources */, + 18A38538BA4E2DF86CF17550 /* RestTimerManager.swift in Sources */, + 5337C697DD141D31F8E1EAB3 /* PersonalRecordsView.swift in Sources */, + 20EC682CE0D802B65B4E16D5 /* SupersetBlockView.swift in Sources */, + C8148C7EFDD30EE9B9B2DA52 /* TutorialManager.swift in Sources */, + C881C7591663EB96A34253E9 /* TutorialOverlay.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -413,7 +533,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4L337QC4AD; + DEVELOPMENT_TEAM = C52C8L39GC; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Info.plist; @@ -451,7 +571,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4L337QC4AD; + DEVELOPMENT_TEAM = C52C8L39GC; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Info.plist; diff --git a/Dev_tools/run_batch.py b/Dev_tools/run_batch.py index 8aaaa40..64da9b5 100644 --- a/Dev_tools/run_batch.py +++ b/Dev_tools/run_batch.py @@ -12,7 +12,6 @@ from pathlib import Path from collections import Counter -import numpy as np import torch from tqdm import tqdm @@ -20,8 +19,8 @@ ROOT = Path(__file__).parent.parent.resolve() sys.path.append(str(ROOT)) -from core.training import BenchMLP # type: ignore -from Dev_tools.run_model import ( +from core.training import BenchMLP # type: ignore # noqa: E402 +from Dev_tools.run_model import ( # noqa: E402 load_scaler, build_feature_vector_from_json, DEFAULT_MODEL, diff --git a/Dev_tools/run_model.py b/Dev_tools/run_model.py index abf178a..50b3a1d 100644 --- a/Dev_tools/run_model.py +++ b/Dev_tools/run_model.py @@ -30,8 +30,8 @@ ROOT = Path(__file__).parent.resolve() sys.path.append(str(ROOT)) -from core.training import BenchMLP # type: ignore -from core.metrics import metrics_to_features # type: ignore +from core.training import BenchMLP # type: ignore # noqa: E402 +from core.metrics import metrics_to_features # type: ignore # noqa: E402 DEFAULT_MODEL = ROOT / "models" / "bench_mlp_v1_model.pt" diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..31ab3e9 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,132 @@ +# mAI Coach — Features + +All currently implemented and working features as of Feb 2026. + +--- + +## 🏠 Home Screen + +- **Calendar View** — Monthly calendar with workout dots on days with activity. Tap a day to see that day's workout(s). +- **Day Workout Cards** — Shows completed workouts for the selected day with exercise count. Tap to open full workout detail. +- **Add Workout** — Button to add a workout to the selected day. Options: "Add from Template" (pick an existing template) or "Create New" (opens workout builder). +- **Template Shortcuts** — Favorite templates displayed as quick-access cards on the home screen. Configurable count (0–10) in Settings. +- **See All Templates** — Link at the bottom of template shortcuts, navigates to the full template management view. + +--- + +## 📋 Templates + +- **All Templates View** — Full list with Favorites section (reorderable) and All Templates section (alphabetical). +- **Star Favorites** — Tap star icon to favorite/unfavorite. Favorites show on the home screen. Max limit with warning if exceeded. +- **Edit Mode** — Reorder favorites with drag handles. Red minus delete buttons appear on every template. +- **Delete / Archive** — Confirmation dialog with Cancel, Archive (soft delete), or Delete Permanently options. +- **Archived Templates** — Archive folder icon in toolbar during edit mode. Archived templates view with "Restore" button on each. +- **Template Detail** — Full exercise list with ExerciseCardView. Edit mode with add/remove exercises. +- **Template Creator** — Create new templates with name, exercises from catalog. +- **Auto-Computed Tags** — Gold tag pills automatically derived from exercises' muscle groups (Chest, Back, Legs, etc.). Update live as exercises are added/removed. + +--- + +## 🏋️ Workouts + +- **Workout Builder** — Create workouts from the calendar. Name field with validation (red warning if empty). Exercise picker. Save-as-template dialog. +- **Workout Detail** — Unified with template behavior: ExerciseCardView cards, Edit/Done toggle, Add Exercise button, auto-computed tags, save changes. +- **Start This Workout** — Button on workout detail to begin a live session. +- **Resume Workout** — Resume a previously started workout from where you left off. +- **Save as Template** — Button on workouts that aren't already templates. +- **Exercise Cards** — Weight, reps, RPE, set tracking per exercise. Completion checkmarks per set. Notes field. mAI Coach button for supported exercises. Info button. +- **Exercise Reordering** — In edit mode, use Move ▲/▼ buttons to rearrange exercises within a workout. +- **Fill from Last Session** — Auto-fill weight/reps from the most recent workout with the same name. +- **Workout Notes** — Expandable text field below tags for overall workout-level instructions or notes. + +--- + +## 🔄 Supersets + +- **Superset Blocks** — Group multiple exercises into a visually distinct gold-accented card with a label/name at the top. +- **Superset Label** — Editable title for each superset block (e.g., "Burnout Superset", "Arm Finisher"). +- **Superset Notes** — Instructions or notes field below the label for coaching cues. +- **Add Exercise Inside** — "+ Add Exercise" button within the superset to add more exercises to the group. +- **Reorder Inside Superset** — Move ▲/▼ buttons to rearrange exercises within a superset block. +- **Delete from Superset** — Red minus button to remove individual exercises from the superset. +- **Add Superset** — "+ Add Superset" button (gold outline with stack icon) appears in edit mode below "+ Add Exercise". + +--- + +## ⏱️ Rest Timer + +- **Auto-Start** — Rest timer automatically triggers when a set is marked complete. +- **Configurable Duration** — Default 90-second rest timer. +- **Skip Button** — X button to dismiss the timer early. +- **Visual Timer Bar** — Bottom bar showing countdown with circular progress indicator. + +--- + +## 💪 Exercise System + +- **Exercise Catalog** — Comprehensive catalog of 100+ exercises organized by muscle group (Chest, Back, Shoulders, Legs, Biceps, Triceps, Core, Full Body). Stored in `exercise_library.json`. +- **Equipment Types** — Barbell, Dumbbell, Cable, Machine, Bodyweight, Smith Machine, EZ Curl Bar, Trap Bar, Kettlebell, Resistance Band, Cardio Machine, Sled, Battle Ropes, Ab Wheel, Pull-Up Bar, Dip Station, Landmine, Box, and Other. +- **Equipment Icons** — Custom PNG icons for each equipment type displayed in the exercise picker. +- **Muscle Group Tagging** — Each exercise carries its muscle group. Workouts/templates inherit tags automatically from their exercises. +- **Exercise Picker** — Searchable, grouped by muscle group. Used in both workout builder, template editing, and superset editing. + +--- + +## 📊 Stats & Progress + +- **Stats Dashboard** — Accessible from Profile → "Stats & Progress". +- **Quick Stats Grid** — Total workouts, days in gym, total sets, total reps, volume lifted (weight × reps), weekly streak. +- **Favorite Exercise** — Most-used exercise with its PR displayed. +- **PR Board** — Top 10 personal records sorted by weight, with dates and rep counts. +- **Exercise Progress** — Mini bar charts showing weight trend over time for top exercises. Session count and PR info per exercise. +- **Exercise Stats View** — Per-exercise detail view showing historical performance, best sets, volume over time. + +--- + +## 🏆 Personal Records + +- **Auto-Detection** — PRs automatically detected based on heaviest weight lifted per exercise across all completed workouts. +- **Trophy Badges** — Gold trophy icon + PR weight shown on exercise cards (e.g., `🏆 225`). Toggle on/off in Settings → Workout → PR Badges. +- **Congrats Alert** — When checking off a set that beats a PR, a "🏆 New Personal Record!" alert lists the exercises and weights. +- **Confetti Effect** — On-screen confetti animation when a PR is hit. +- **Personal Records View** — Dedicated view showing all PRs across all exercises, accessible from profile. + +--- + +## 🤖 mAI Coach (AI Coaching) + +- **Bench Press Coaching** — Live camera-based form coaching for bench press using on-device ML. +- **Coach Modes** — Live session and demo session modes. +- **Coach Voice** — Male/female voice selection in Settings. +- **Dev Data Toggle** — Show/hide debug inference data during sessions. + +--- + +## ⚙️ Settings & Profile + +- **Profile Card** — User name and email display. +- **Coach Voice** — Male or female coach voice toggle. +- **Appearance** — Light/Dark mode toggle. +- **Template Shortcuts** — Stepper to set max home screen favorite templates (0–10). +- **PR Badges** — Toggle trophy icons on exercise cards. +- **Dev Data** — Toggle debug info during coaching sessions. +- **Account Section** — Privacy Policy, Terms of Service, Sign Out. + +--- + +## 💾 Data & Persistence + +- **Local Storage** — All workouts saved as JSON (`workouts.json`). Settings via UserDefaults. +- **Unified Backup** — `DataBackupManager` auto-saves all user data (workouts, settings, favorites, archived) as `user_backup.json` on every data change. Ready for future cloud sync. +- **Backward Compatibility** — All models use backward-compatible Codable decoding for safe data migration. Old `exercises` arrays auto-migrate to `items` (supporting supersets). +- **Admin Demo Data** — 16 weeks of realistic PPL workout history with supersets, deload weeks, workout notes, and progressive overload for testing and demos. + +--- + +## 🎨 Design System + +- **AppColors** — Full dark/light color palette: backgrounds, text hierarchy, accent gold, borders. +- **AppFonts** — System rounded fonts at consistent sizes: display, headline, body, subhead, footnote. +- **AppSpacing** — Consistent padding, card radius, button radius tokens. +- **Card Style** — Reusable `.cardStyle()` modifier for consistent card appearance. +- **Gold Accent** — Signature bronze gold accent color throughout the UI. diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md index 3ccd72a..aa61f6c 100644 --- a/PRIVACY_POLICY.md +++ b/PRIVACY_POLICY.md @@ -49,7 +49,7 @@ We may update this policy from time to time. Changes will be posted to this page If you have questions about this privacy policy, please contact: **Travis Whitney** -📧 twhit229@mtroyal.ca +📧 whitnetr@oregonstate.edu --- diff --git a/README.md b/README.md index 74488a3..5598850 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### Real-time motion capture for strength training. -mAI_Coach is a research prototype that brings advanced computer vision to the weight room. By running Google's MediaPipe pose detection models directly on your iPhone, it provides instant feedback on your form—no cloud uploads, no latency, just results. +mAI Coach is a research prototype that brings advanced computer vision to the weight room. By running Google's MediaPipe pose detection models directly on your iPhone, it provides instant feedback on your form—no cloud uploads, no latency, just results. Accompanying the app is a powerful desktop suite for data curation, helping researchers label video datasets and fine-tune models to match real-world conditions. @@ -12,7 +12,10 @@ Accompanying the app is a powerful desktop suite for data curation, helping rese - **Live Form Coaching**: Real-time skeletal overlay with audio coaching cues for grip width, bar tilt, lockout, and depth issues. - **Wrist-Y Rep Detection**: Automatic rep counting using streaming peak detection on wrist landmarks—87% accuracy within ±1 rep. - **Centering Guide**: Dynamic calibration overlay that auto-dismisses when you're properly positioned. -- **Workout Tracking** *(coming soon)*: Custom workout plans, AI-generated routines, and progress tracking. +- **Full Workout Tracking**: Custom workout plans, templates, exercise reordering, and real-time progress tracking. +- **Superset Support**: Group exercises into labeled superset blocks with instructions/notes. +- **Rest Timer**: Automatic rest timer triggered on set completion with configurable durations. +- **Personal Records**: Auto-detected PRs with trophy badges, confetti, and a dedicated PR board. - **Research-Grade Tooling**: A unified PySide6 desktop application for annotating video, trimming clips, and training custom classification models. ## Repository Structure @@ -22,7 +25,8 @@ Accompanying the app is a powerful desktop suite for data curation, helping rese | **[`App Core/`](App%20Core/README.md)** | **The iOS Application.** Xcode workspace, SwiftUI views, and MediaPipe inference. | | **[`Dev_tools/`](Dev_tools/README.md)** | **The Researcher's Toolkit.** Python/Qt app for labeling, visualization, and model training. | | `tests/` | Automated test suite for the Python tools. | -| `scripts/` | Helper scripts for environment setup and launching tools. | +| `temp/` | Scratch files (gitignored). | +| `archive/` | Archived files (gitignored). | ## Quick Start diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md new file mode 100644 index 0000000..a5febd0 --- /dev/null +++ b/STYLE_GUIDE.md @@ -0,0 +1,580 @@ +# mAI Coach — UI Style Guide + +> **Version**: 3.0 +> **Last Updated**: February 28, 2026 +> **SwiftUI Tokens**: `DesignTokens.swift` — `AppColors`, `AppFonts`, `AppSpacing` +> **Authors**: Travis Whitney + +This document defines the visual design system for the mAI Coach iOS app. All new screens, components, and features **must** follow these guidelines to maintain consistency across the app. + +--- + +## 1. Brand Identity + +### Philosophy +mAI Coach is a premium AI-powered fitness coaching app. The design should feel: +- **Premium** — like a high-end gym, not a free fitness tracker +- **Confident** — bold typography, strong contrast, decisive layout +- **Focused** — minimal clutter, one action per screen when possible +- **Trustworthy** — clean, professional, nothing flashy or gimmicky + +### Accent Color: Bronze Gold +Our signature accent color is a **bronze gold**, inspired by achievement and strength (gold medals, trophies, premium gym equipment). Like Spotify uses green, we use gold **sparingly** — for calls to action, active states, and important highlights. It should never be overwhelming. + +--- + +## 2. Color System + +### 2.1 Core Palette + +| Token | Light Mode | Dark Mode | Usage | +|-------|-----------|-----------|-------| +| `accent` | `#ad8c04` | `#c9a30a` | Primary buttons, active nav items, key icons | +| `accentText` | `#8a7003` | `#c9a30a` | Small text that needs to be gold (passes WCAG AA) | +| `accentMuted` | `rgba(173,140,4, 0.12)` | `rgba(201,163,10, 0.15)` | Subtle gold backgrounds, highlights | +| `accentHover` | `#8a7003` | `#d4ad0d` | Hover/pressed state for gold elements | + +### 2.2 Backgrounds + +| Token | Light Mode | Dark Mode | Usage | +|-------|-----------|-----------|-------| +| `bgPrimary` | `#FFFFFF` | `#0A0A0F` | Main screen background | +| `bgSecondary` | `#F5F5F5` | `#141420` | Cards, grouped sections, modals | +| `bgTertiary` | `#EBEBEB` | `#1E1E2C` | Nested cards, input fields | +| `bgElevated` | `#FFFFFF` | `#1A1A28` | Elevated surfaces (sheets, popovers) | + +### 2.3 Text + +| Token | Light Mode | Dark Mode | Usage | +|-------|-----------|-----------|-------| +| `textPrimary` | `#1A1A1A` | `#F0F0F0` | Headlines, body text | +| `textSecondary` | `#6B6B6B` | `#8A8A8A` | Subtitles, descriptions, timestamps | +| `textTertiary` | `#999999` | `#555555` | Placeholders, disabled text | +| `textInverse` | `#FFFFFF` | `#FFFFFF` | Text on dark/accent buttons | + +### 2.4 Borders & Dividers + +| Token | Light Mode | Dark Mode | Usage | +|-------|-----------|-----------|-------| +| `border` | `#E0E0E0` | `#2A2A34` | Card borders, input outlines | +| `divider` | `#F0F0F0` | `#1E1E28` | List separators, section dividers | + +### 2.5 Semantic Colors + +| Token | Light Mode | Dark Mode | Usage | +|-------|-----------|-----------|-------| +| `success` | `#2D9A3F` | `#34C759` | Good form feedback, completed states | +| `warning` | `#CC8800` | `#FF9F0A` | Caution, needs attention | +| `error` | `#D32F2F` | `#FF453A` | Bad form, errors, destructive actions | +| `info` | `#2563EB` | `#5A9CFF` | Informational, tips | + +### 2.6 Color Usage Rules + +> [!IMPORTANT] +> **The gold accent is used SPARINGLY.** If it's everywhere, it loses its impact. + +**DO use gold for:** +- Primary call-to-action buttons (e.g., "Start Workout", "Coach me!") +- Active navigation tab/icon +- Progress indicators and achievement badges +- Important notification dots +- Active toggle switches + +**DO NOT use gold for:** +- Large background fills +- Every button — secondary actions should be neutral +- Body text (except links/labels with the darker `accentText` variant) +- Decorative purposes without meaning + +--- + +## 3. Typography + +### 3.1 Font Family + +| Platform | Font | +|----------|------| +| iOS (SwiftUI) | `.system(.rounded)` — SF Rounded | +| Web Prototype | `'Inter', -apple-system, sans-serif` | + +SF Rounded gives the app a friendly, modern feel while staying readable. Use it consistently — do not mix with other fonts. + +### 3.2 Type Scale + +| Style Name | Size | Weight | Line Height | Usage | +|-----------|------|--------|------------|-------| +| `displayLarge` | 34pt | `.bold` (800) | 1.2 | Large title (top of screen) | +| `displaySmall` | 28pt | `.bold` (700) | 1.2 | Section headers | +| `headline` | 22pt | `.semibold` (600) | 1.3 | Card titles, major labels | +| `title` | 20pt | `.semibold` (600) | 1.3 | Screen subtitles | +| `body` | 17pt | `.regular` (400) | 1.5 | Body text, list items | +| `bodyBold` | 17pt | `.semibold` (600) | 1.5 | Emphasized body text | +| `callout` | 16pt | `.regular` (400) | 1.4 | Secondary descriptions | +| `subhead` | 15pt | `.regular` (400) | 1.4 | Metadata, timestamps | +| `footnote` | 13pt | `.regular` (400) | 1.3 | Form labels, captions | +| `caption` | 11pt | `.medium` (500) | 1.2 | Badges, tags, micro-labels | + +### 3.3 Typography Rules + +- **One `displayLarge` per screen** — this is the screen title +- **Never use ALL CAPS for body text** — only for `caption` tags/badges +- **Minimum text size**: 13pt for anything the user needs to read +- **Line length**: aim for 60–75 characters per line maximum +- **Letter spacing**: default for all sizes except `caption` (+0.5pt) + +--- + +## 4. Spacing & Layout + +### 4.1 Spacing Scale + +Use multiples of **4pt** for all spacing. The core spacing tokens are: + +| Token | Value | Usage | +|-------|-------|-------| +| `xs` | 4pt | Tight gaps (icon-to-text inline) | +| `sm` | 8pt | Between related elements | +| `md` | 12pt | Component internal padding | +| `lg` | 16pt | Between components, screen padding | +| `xl` | 24pt | Between sections | +| `2xl` | 32pt | Major section separators | +| `3xl` | 48pt | Top/bottom breathing room | + +### 4.2 Screen Layout Rules + +- **Horizontal padding**: `16pt` on both sides (matches iOS standard) +- **Safe area**: always respect safe area insets (especially bottom on newer iPhones) +- **Cards**: `16pt` internal padding, `12pt` border radius +- **Vertical rhythm**: `24pt` between major sections, `12pt` between items within a section + +### 4.3 Grid + +- Single column layout for all screens (phone-width app) +- Full-width cards and buttons within the `16pt` margins +- Center logos and standalone graphics horizontally + +--- + +## 5. Components + +### 5.1 Buttons + +#### Primary Button (Gold Accent) +Used for the **one main action** on each screen ("Start Workout", "Finish Workout", "Sign in"). + +``` +Background: accent (#ad8c04 / #c9a30a) +Text: #000000, 17pt, semibold +Corner: 12pt radius +Padding: 14pt vertical +Disabled: accent at 40% opacity, text at 60% opacity +``` + +#### Secondary Button (Muted Gold) +Used for alternative actions ("Continue without signing in", "Add Exercise"). + +``` +Background: accentMuted +Text: accentText, 17pt, semibold +Corner: 12pt radius +Padding: 14pt vertical +``` + +#### Ghost/Text Button +Used for tertiary actions ("Create account", links). + +``` +Background: transparent +Text: accentText, 17pt, semibold +``` + +#### Dashed Add Button +Used for "+ Add Set", "+ Add Exercise" within workout context. + +``` +Border: 1pt dashed, accent or border color +Background: accentMuted (exercise-level) or transparent (set-level) +Text: accentText or textSecondary, 13–15pt semibold +Corner: 12pt radius +``` + +### 5.2 Navigation Bar + +``` +Background: bgPrimary (with blur if scrolled) +Title: textPrimary, 17pt semibold (inline) or 34pt bold (large) +Back button: accent color, "‹ Back" +Trailing: textSecondary icons, 20pt +Divider: 1pt divider color (shows on scroll) +``` + +### 5.3 Cards + +``` +Background: bgSecondary +Border: 1pt border color (optional) +Corner: 12pt radius +Padding: 16pt +Shadow: none (dark mode) / 0 1px 3px rgba(0,0,0,0.06) (light mode) +``` + +### 5.4 Form Inputs + +``` +Background: bgTertiary +Border: 1pt border color +Corner: 10pt radius +Padding: 12pt 16pt +Text: textPrimary, 17pt +Placeholder: textTertiary +Focus: border changes to accent +``` + +### 5.5 Toggle Switch + +``` +Off: bgTertiary track, white knob +On: accent track, white knob +Size: 51 × 31pt (iOS standard) +``` + +### 5.6 Segmented Control + +``` +Background: bgTertiary +Active segment: bgPrimary with shadow (light) / bgElevated (dark) +Text: textPrimary, 13pt semibold +Corner: 8pt outer, 7pt segments +``` + +### 5.7 HUD Pills (Session Screen) + +``` +Background: rgba(0,0,0,0.6) with backdrop blur +Text: #FFFFFF, 14pt, semibold +Corner: 20pt radius (fully rounded) +Border: success/warning/error at 50% opacity +Padding: 8pt 14pt +``` + +### 5.8 Tab Bar + +Full-width frosted glass bar (Spotify-style), **not floating**. + +``` +Background: systemChromeMaterial blur + systemBackground at 75% opacity +Shadow: none (clean edge) +Icons: system default (inactive) / accent gold (active) +Labels: system default / accent gold, system size +Tint: accentGold +Height: 49pt (iOS standard) + safe area +Appearance: UITabBarAppearance with configureWithDefaultBackground() +``` + +### 5.9 Exercise Card + +Used in the Workout tab for each exercise. + +``` +Container: cardStyle (bgSecondary, 12pt radius, 1pt border) +Padding: 14pt +Header: exercise name (bodyBold) + optional mAI Coach button +Grid: SET | LBS | REPS | ✓ columns + Set label: textSecondary, subhead, 36pt wide + Inputs: bgTertiary, 1pt border, centered number pad + Check btn: 36×36pt, accent gold when complete, bgTertiary when not +Add Set: dashed border button (see 5.1) +Notes: footnote TextField, bgTertiary background +``` + +### 5.10 mAI Coach Button + +Gold-accent button shown on exercises that have camera coaching models. + +``` +Background: accentMuted +Border: 1pt accent +Text: accent, 11pt, bold (.rounded) +Icon: checkmark.circle.fill, 12pt, accent +Corner: 8pt radius +Padding: 5pt 8pt +Layout: .fixedSize() — never compresses +``` + +### 5.11 Calendar + +Used on the Home tab for month view. + +``` +Container: cardStyle +Month header: bodyBold centered, chevron nav buttons in accent gold +Day labels: caption bold, textTertiary, 7-column grid +Day cells: 40pt height + Today: 32×32 Circle fill accent gold, bold black text + Selected: 32×32 Circle stroke accent gold + accentMuted fill + Has workout: 4×4 accent gold dot below number + Default: medium weight, textPrimary +``` + +### 5.12 Stat Card + +``` +Layout: centered VStack in cardStyle +Value: 32pt heavy .rounded, accent gold +Label: caption, textSecondary +Padding: 16pt +``` + +--- + +## 6. Iconography + +### System Icons +Use **SF Symbols** (Apple's built-in icon set) consistently: +- **Weight**: `.semibold` for navigation, `.regular` for content +- **Size**: 20pt for nav bar, 24pt for in-content, 60pt for empty states +- **Color**: follows the element it's in (gold for active, textSecondary for inactive) + +### App Logo +- Available in `AppLogo.imageset` as vector PDF +- Use `.renderingMode(.template)` when you need to tint it (e.g., white on dark boot screen) +- Use `.renderingMode(.original)` when displaying the gold-colored version +- Standard sizes: 140pt (boot screen), 120pt (home screen), 100pt (sub-screens) + +--- + +## 7. Motion & Animation + +### Principles +- **Purposeful** — every animation should communicate something (transition, state change, feedback) +- **Quick** — keep durations under 0.4s for interactions, up to 0.6s for page transitions +- **Natural** — use spring/easeOut curves, never linear + +### Standard Durations + +| Type | Duration | Curve | +|------|----------|-------| +| Button press | 0.15s | `easeOut` | +| Screen transition | 0.35s | `spring(response: 0.5, dampingFraction: 0.85)` | +| Element fade-in | 0.3s | `easeOut` | +| Toggle/switch | 0.2s | `easeInOut` | +| Boot screen sequence | 0.6s staggered | `spring` + `easeOut` | + +### Rules +- **No bouncing** — spring animations should be dampened (0.7–0.85 fraction) +- **Stagger** lists and multiple elements (50–100ms delay between each) +- **No animation** on scroll-triggered content unless specifically designed + +--- + +## 8. Dark Mode Rules + +### Switching Behavior +- Default to **Light** mode on first launch +- User can choose **Light / Dark / System** in Profile → Appearance +- Stored via `@AppStorage(SettingsKeys.appearance)` ("light", "dark", "system") +- Applied via `.preferredColorScheme()` on `RootView` + +### Design Principles +- **Dark mode is NOT just inverted light mode** — surfaces layer up in brightness +- Background layers: `bgPrimary` (darkest) → `bgSecondary` → `bgTertiary` → `bgElevated` (lightest) +- **Shadows are invisible** in dark mode — use subtle borders or elevation through brightness instead +- Gold accent is **slightly brighter** in dark mode (`#c9a30a`) for visibility +- All text must maintain **WCAG AA contrast** (4.5:1 for body, 3:1 for large text) + +### Elevation Hierarchy (Dark Mode) + +``` +Level 0 — bgPrimary (#0A0A0F) — screen background +Level 1 — bgSecondary (#141420) — cards, grouped sections +Level 2 — bgTertiary (#1E1E2C) — inputs, nested elements +Level 3 — bgElevated (#1A1A28) — sheets, popovers, modals +``` + +--- + +## 9. Accessibility + +### Minimum Requirements +- All interactive elements: **44 × 44pt** minimum tap target +- All text: **minimum 13pt** size +- Color contrast: **WCAG AA** (4.5:1 body text, 3:1 large text/UI elements) +- **Never use color alone** to convey meaning — pair with icons or text (e.g., ✅ + green, ⚠️ + yellow) + +### VoiceOver +- All buttons must have accessibility labels +- Images must have `accessibilityLabel` descriptions +- Navigation must be logical top-to-bottom, left-to-right + +--- + +## 10. App Architecture + +### Navigation Flow + +``` +mAICoachApp + └─ RootView + ├─ (signedOut) → WelcomeView + │ ├─ Sign In → SignInView (sheet) + │ ├─ Continue as Guest + │ └─ Create Account → SignInView (sheet) + └─ (signedIn/guest) → MainTabView + ├─ Tab 0: HomeView (house.fill) + │ ├─ Calendar + │ ├─ Day Workouts → WorkoutDetailView + │ │ ├─ Tags, Workout Notes + │ │ ├─ ExerciseCardView (standalone) + │ │ ├─ SupersetBlockView (grouped) + │ │ │ └─ ExerciseCardView (inside superset) + │ │ ├─ + Add Exercise / + Add Superset + │ │ ├─ Fill from Last Session + │ │ ├─ Start/Resume/Finish Workout + │ │ └─ Rest Timer Bar + │ ├─ My Templates → AllTemplatesView + │ │ └─ Template Detail → WorkoutDetailView + │ └─ Workout Stats → ExerciseStatsView + ├─ Tab 1: WorkoutView (dumbbell.fill) + │ ├─ Empty state → "New Workout" + │ └─ Active → WorkoutDetailView + │ └─ mAI Coach btn → BenchSessionView + └─ Tab 2: ProfileView (person.fill) + ├─ Account Card + ├─ Stats & Progress → PersonalRecordsView + ├─ Settings (voice, appearance, PR badges, dev data) + └─ Account (privacy, sign out) +``` + +### Screen Structure (Tab Screen) + +``` +┌──────────────────────────┐ +│ Status Bar │ +├──────────────────────────┤ +│ Screen Title (34pt) │ +│ (displayLarge, leading) │ +├──────────────────────────┤ +│ │ +│ ScrollView │ +│ (16pt horizontal pad) │ +│ │ +│ ┌──────────────────┐ │ +│ │ Card / Section │ │ +│ └──────────────────┘ │ +│ │ +│ ┌──────────────────┐ │ +│ │ Card / Section │ │ +│ └──────────────────┘ │ +│ │ +├──────────────────────────┤ +│ Tab Bar (frosted glass) │ +│ [Home] [Workout] [Profile]│ +└──────────────────────────┘ +``` + +### Boot / Launch Screen Structure + +``` +┌──────────────────────────┐ +│ │ +│ Dark gradient bg │ +│ │ +│ [ Logo 140pt ] │ +│ "mAI Coach" │ +│ "AI-Powered Form Coach" │ +│ │ +│ [spinner] │ +│ │ +└──────────────────────────┘ +``` + +--- + +## 11. File Naming & Organization + +### Asset Naming Convention +``` +feature_element_variant.extension + +Examples: + app_icon_1024.png + bench_setup_overlay.png + coach_voice_male.mp3 +``` + +### SwiftUI File Naming +``` +FeatureView.swift — screen-level views (HomeView, WorkoutView, ProfileView) +FeatureCardView.swift — reusable card components (ExerciseCardView) +FeatureModel.swift — data models + store (WorkoutModel) +FeatureService.swift — business logic / API calls +DesignTokens.swift — centralized color/font/spacing tokens (AppColors, AppFonts, AppSpacing) +``` + +### Current File Inventory + +| File | Role | +|------|------| +| `DesignTokens.swift` | Color, typography, spacing constants + `CardStyle` modifier | +| `WorkoutModel.swift` | `Workout`, `WorkoutItem`, `SupersetGroup`, `Exercise`, `ExerciseSet` types + `WorkoutStore` | +| `WorkoutStatsEngine.swift` | Stats computation: totals, PRs, exercise trends, streaks | +| `RestTimerManager.swift` | Observable rest timer with auto-start on set completion | +| `ExerciseCatalog.swift` | Equipment types enum, exercise catalog loader | +| `MainTabView.swift` | 3-tab `TabView` with frosted glass bar | +| `HomeView.swift` | Calendar, day workouts, templates, stats | +| `WorkoutView.swift` | Active workout tracker | +| `WorkoutDetailView.swift` | Workout/template detail: exercise cards, supersets, edit mode, notes | +| `WorkoutBuilderView.swift` | Create new workouts | +| `AllTemplatesView.swift` | Template management: favorites, archive, reorder | +| `ExerciseCardView.swift` | Exercise card with sets grid + mAI Coach button | +| `SupersetBlockView.swift` | Superset group block with label, notes, inner exercises | +| `ExercisePickerView.swift` | Searchable exercise catalog picker | +| `ExerciseStatsView.swift` | Per-exercise historical stats | +| `PersonalRecordsView.swift` | All-time PR board | +| `ConfettiView.swift` | PR celebration confetti animation | +| `ProfileView.swift` | Account card + settings | +| `SettingsView.swift` | User preferences | +| `WelcomeView.swift` | Signed-out landing screen | +| `RootView.swift` | Boot → welcome or tab view router | +| `BootScreen.swift` | Animated launch screen | +| `SignInView.swift` | Email/password auth form | +| `AuthSession.swift` | Authentication manager | +| `BenchSessionView.swift` | Live/demo camera coaching session | +| `mAICoachApp.swift` | App entry, injects `AuthSession` + `WorkoutStore` | + +--- + +## 12. Quick Reference Card + +``` +┌─────────────────────────────────────────┐ +│ mAI Coach │ +│ Design Tokens │ +├─────────────────────────────────────────┤ +│ │ +│ GOLD ACCENT #ad8c04 / #c9a30a │ +│ GOLD TEXT #8a7003 / #c9a30a │ +│ GOLD MUTED 12% op / 15% op │ +│ │ +│ BACKGROUND #FFFFFF / #0A0A0F │ +│ SURFACE #F5F5F5 / #141420 │ +│ TEXT PRIMARY #1A1A1A / #F0F0F0 │ +│ TEXT SECONDARY #6B6B6B / #8A8A8A │ +│ BORDER #E0E0E0 / #2A2A34 │ +│ │ +│ FONT SF Rounded │ +│ CORNER RADIUS 12pt (cards/btns) │ +│ SCREEN PADDING 16pt horizontal │ +│ SPACING BASE 4pt multiples │ +│ │ +│ SUCCESS #2D9A3F / #34C759 │ +│ WARNING #CC8800 / #FF9F0A │ +│ ERROR #D32F2F / #FF453A │ +│ │ +└─────────────────────────────────────────┘ + LEFT = Light / RIGHT = Dark +``` + +--- + +*This is a living document. Update it whenever new patterns are established.*