From ef5586546c908dfa189e3147b60be33b5a7c8622 Mon Sep 17 00:00:00 2001
From: Michael Kaufmann <5276337+wulfland@users.noreply.github.com>
Date: Wed, 11 Feb 2026 09:15:38 +0100
Subject: [PATCH 1/5] Enhance workout session management and UI interactions
- Clear stale active workouts when starting a new split
- Implement clickable split cards with visual feedback for workout actions
- Update workout session auto-start logic to handle stale workouts
- Refactor workout starting logic to streamline exercise loading
---
.../mesocycles/MesocycleDashboard.tsx | 2 +
.../mesocycles/SplitProgressTracker.css | 51 ++++++++++++++-
.../mesocycles/SplitProgressTracker.tsx | 56 ++++++++++++-----
src/components/workouts/WorkoutSession.tsx | 17 ++++-
src/hooks/useWorkoutSession.ts | 62 +------------------
5 files changed, 110 insertions(+), 78 deletions(-)
diff --git a/src/components/mesocycles/MesocycleDashboard.tsx b/src/components/mesocycles/MesocycleDashboard.tsx
index de43f06..3fcc293 100644
--- a/src/components/mesocycles/MesocycleDashboard.tsx
+++ b/src/components/mesocycles/MesocycleDashboard.tsx
@@ -117,6 +117,8 @@ export default function MesocycleDashboard({
};
const handleStartWorkout = (splitDayId: string) => {
+ // Clear any stale active workout so the new split takes priority
+ localStorage.removeItem('activeWorkout');
// Store the selected split day ID in localStorage for the workout session to pick up
localStorage.setItem('selectedSplitDayId', splitDayId);
// Navigate to workout page
diff --git a/src/components/mesocycles/SplitProgressTracker.css b/src/components/mesocycles/SplitProgressTracker.css
index 05604f9..a711bed 100644
--- a/src/components/mesocycles/SplitProgressTracker.css
+++ b/src/components/mesocycles/SplitProgressTracker.css
@@ -191,7 +191,56 @@
margin: 0;
}
-/* Start Workout Button */
+/* Clickable Split Cards */
+.split-card.clickable {
+ cursor: pointer;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ font-family: inherit;
+ width: 100%;
+}
+
+.split-card.clickable:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.split-card.clickable:active {
+ transform: translateY(0);
+ box-shadow: none;
+}
+
+.split-card.in-progress {
+ background: #fffbeb;
+ border-color: #f59e0b;
+ box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
+}
+
+.split-card.in-progress .split-status {
+ color: #d97706;
+ font-size: 0.875rem;
+}
+
+.split-card-action {
+ margin-top: 0.5rem;
+ font-size: 0.75rem;
+ color: #9ca3af;
+ font-weight: 500;
+}
+
+.split-card.next .split-card-action {
+ color: #3b82f6;
+}
+
+.split-card.in-progress .split-card-action {
+ color: #d97706;
+}
+
+.split-card.completed .split-card-action {
+ color: #10b981;
+}
+
+/* Start Workout Button (legacy, kept for compatibility) */
.start-workout-btn {
width: 100%;
margin-top: 1rem;
diff --git a/src/components/mesocycles/SplitProgressTracker.tsx b/src/components/mesocycles/SplitProgressTracker.tsx
index b66be54..8a60346 100644
--- a/src/components/mesocycles/SplitProgressTracker.tsx
+++ b/src/components/mesocycles/SplitProgressTracker.tsx
@@ -141,16 +141,41 @@ export default function SplitProgressTracker({
{splitStatus.map((info) => {
const isNext = !allCompleted && info.splitDay.id === nextSplit?.id;
+ const isInProgress =
+ activeWorkout &&
+ !activeWorkout.completed &&
+ activeWorkout.splitDayId === info.splitDay.id;
+
+ const handleCardClick = () => {
+ if (isInProgress && onResumeWorkout) {
+ // Resume the active workout for this split
+ onResumeWorkout();
+ } else if (onStartWorkout) {
+ // Start (or re-do) this split day
+ onStartWorkout(info.splitDay.id);
+ }
+ };
return (
-
{info.splitDay.name}
- {info.completed ? '✓' : isNext ? '★ Next' : ''}
+ {isInProgress
+ ? '🏋️ In Progress'
+ : info.completed
+ ? '✓'
+ : isNext
+ ? '★ Next'
+ : ''}
{info.completedDate && (
@@ -164,28 +189,29 @@ export default function SplitProgressTracker({
{info.splitDay.exercises.length !== 1 ? 's' : ''}
)}
-
+
+ {isInProgress
+ ? 'Tap to resume'
+ : info.completed
+ ? 'Tap to redo'
+ : 'Tap to start'}
+
+
);
})}
- {/* Action Button */}
- {allCompleted ? (
+ {/* Completion Message */}
+ {allCompleted && (
🎉
All done this week!
- You can repeat splits or start next week's training
+ Tap any split above to repeat it or start next week's
+ training
- ) : nextSplit && onStartWorkout ? (
-
- ) : null}
+ )}
{/* Deload Week Message */}
diff --git a/src/components/workouts/WorkoutSession.tsx b/src/components/workouts/WorkoutSession.tsx
index acb593b..b5fb522 100644
--- a/src/components/workouts/WorkoutSession.tsx
+++ b/src/components/workouts/WorkoutSession.tsx
@@ -105,9 +105,16 @@ export default function WorkoutSession({ onNavigate }: WorkoutSessionProps) {
// Auto-start workout if coming from mesocycle dashboard with a selected split
useEffect(() => {
const selectedSplitDayId = localStorage.getItem('selectedSplitDayId');
- if (selectedSplitDayId && activeMesocycle && !isActive) {
+ if (selectedSplitDayId && activeMesocycle) {
// Clear immediately to prevent re-triggering
localStorage.removeItem('selectedSplitDayId');
+
+ // If there's already an active (but stale) workout, cancel it first
+ // so we start fresh with the user's explicitly chosen split
+ if (isActive) {
+ cancelWorkout();
+ }
+
// Auto-start the workout with the selected split
startWorkoutFromSplit(activeMesocycle.id, selectedSplitDayId).catch(
(error) => {
@@ -116,7 +123,13 @@ export default function WorkoutSession({ onNavigate }: WorkoutSessionProps) {
}
);
}
- }, [activeMesocycle, isActive, startWorkoutFromSplit, showToast]);
+ }, [
+ activeMesocycle,
+ isActive,
+ startWorkoutFromSplit,
+ cancelWorkout,
+ showToast,
+ ]);
// Get all unique muscle groups from workout exercises
const workoutMuscleGroups = useMemo(() => {
diff --git a/src/hooks/useWorkoutSession.ts b/src/hooks/useWorkoutSession.ts
index 344a67b..e46e07d 100644
--- a/src/hooks/useWorkoutSession.ts
+++ b/src/hooks/useWorkoutSession.ts
@@ -9,7 +9,6 @@ import type {
WorkoutExercise,
WorkoutSet,
WorkoutFeedback,
- MesocycleSplitDay,
} from '../types/models';
import {
createWorkout,
@@ -20,7 +19,6 @@ import {
createEmptySet,
getPreviousPerformance,
startWorkoutFromSplit as startWorkoutFromSplitService,
- getActiveMesocycle,
} from '../db/service';
interface UseWorkoutSessionReturn {
@@ -102,31 +100,10 @@ export function useWorkoutSession(): UseWorkoutSessionReturn {
};
}, [workout, isActive]);
- const startWorkout = useCallback(async () => {
- // Check if there's a selected split day from the dashboard
- const selectedSplitDayId = localStorage.getItem('selectedSplitDayId');
- let splitDayId: string | undefined;
- let splitDay: MesocycleSplitDay | undefined;
-
- if (selectedSplitDayId) {
- // Get the active mesocycle to find the split day
- const activeMesocycle = await getActiveMesocycle();
- if (activeMesocycle) {
- splitDay = activeMesocycle.splitDays.find(
- (sd: MesocycleSplitDay) => sd.id === selectedSplitDayId
- );
- if (splitDay) {
- splitDayId = selectedSplitDayId;
- }
- }
- // Clear the selected split day from localStorage
- localStorage.removeItem('selectedSplitDayId');
- }
-
+ const startWorkout = useCallback(() => {
const newWorkout: Workout = {
- id: 'temp-workout-' + crypto.randomUUID(), // Temporary ID until saved to DB
+ id: 'temp-workout-' + crypto.randomUUID(),
date: new Date(),
- splitDayId,
exercises: [],
notes: undefined,
completed: false,
@@ -138,41 +115,6 @@ export function useWorkoutSession(): UseWorkoutSessionReturn {
setWorkout(newWorkout);
setIsActive(true);
setCurrentExerciseIndex(0);
-
- // If we have a split day, auto-load its exercises
- if (splitDay && splitDay.exercises.length > 0) {
- // We'll load exercises asynchronously after the workout is created
- const exercisesWithSets: WorkoutExercise[] = [];
-
- for (const mesocycleExercise of splitDay.exercises) {
- // Get previous performance for this exercise
- const previousPerformance = await getPreviousPerformance(
- mesocycleExercise.exerciseId
- );
-
- // Create sets based on mesocycle configuration
- const sets: WorkoutSet[] = [];
- for (let i = 0; i < mesocycleExercise.targetSets; i++) {
- const previousSet =
- previousPerformance?.sets && previousPerformance.sets[i];
- sets.push(
- createEmptySet(mesocycleExercise.exerciseId, i + 1, previousSet)
- );
- }
-
- exercisesWithSets.push({
- exerciseId: mesocycleExercise.exerciseId,
- sets,
- notes: mesocycleExercise.notes,
- });
- }
-
- // Update workout with exercises
- setWorkout({
- ...newWorkout,
- exercises: exercisesWithSets,
- });
- }
}, []);
const startWorkoutFromSplit = useCallback(
From 2555a6360c7c1c91f8b414a5d76206c824629b3a Mon Sep 17 00:00:00 2001
From: Michael Kaufmann <5276337+wulfland@users.noreply.github.com>
Date: Wed, 11 Feb 2026 09:24:15 +0100
Subject: [PATCH 2/5] Update src/components/mesocycles/SplitProgressTracker.css
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/components/mesocycles/SplitProgressTracker.css | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/components/mesocycles/SplitProgressTracker.css b/src/components/mesocycles/SplitProgressTracker.css
index a711bed..fef35dd 100644
--- a/src/components/mesocycles/SplitProgressTracker.css
+++ b/src/components/mesocycles/SplitProgressTracker.css
@@ -210,6 +210,10 @@
box-shadow: none;
}
+.split-card.clickable:focus-visible {
+ outline: 2px solid #3b82f6;
+ outline-offset: 2px;
+}
.split-card.in-progress {
background: #fffbeb;
border-color: #f59e0b;
From 10f910ad98fab16d71ca22ef93b501c40076efdb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 08:24:42 +0000
Subject: [PATCH 3/5] Initial plan
From 82c2578d8ed9a9d611c0088923c14c5429fcc690 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 08:27:13 +0000
Subject: [PATCH 4/5] Address PR review comments - fix infinite loop, add
button resets, clarify text, add safety checks
Co-authored-by: wulfland <5276337+wulfland@users.noreply.github.com>
---
.../mesocycles/SplitProgressTracker.css | 10 ++++++++++
.../mesocycles/SplitProgressTracker.tsx | 17 +++++++++--------
src/components/workouts/WorkoutSession.tsx | 11 +++++++++--
3 files changed, 28 insertions(+), 10 deletions(-)
diff --git a/src/components/mesocycles/SplitProgressTracker.css b/src/components/mesocycles/SplitProgressTracker.css
index fef35dd..08b7b57 100644
--- a/src/components/mesocycles/SplitProgressTracker.css
+++ b/src/components/mesocycles/SplitProgressTracker.css
@@ -198,6 +198,16 @@
-webkit-tap-highlight-color: transparent;
font-family: inherit;
width: 100%;
+ /* Reset default button styles so clickable cards render consistently */
+ border: none;
+ background: none;
+ padding: 0;
+ color: inherit;
+ text-align: inherit;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ outline: none;
}
.split-card.clickable:hover {
diff --git a/src/components/mesocycles/SplitProgressTracker.tsx b/src/components/mesocycles/SplitProgressTracker.tsx
index 8a60346..92109f1 100644
--- a/src/components/mesocycles/SplitProgressTracker.tsx
+++ b/src/components/mesocycles/SplitProgressTracker.tsx
@@ -147,12 +147,16 @@ export default function SplitProgressTracker({
activeWorkout.splitDayId === info.splitDay.id;
const handleCardClick = () => {
- if (isInProgress && onResumeWorkout) {
- // Resume the active workout for this split
- onResumeWorkout();
+ if (isInProgress) {
+ if (onResumeWorkout) {
+ onResumeWorkout();
+ } else {
+ console.warn('Resume workout callback not provided');
+ }
} else if (onStartWorkout) {
- // Start (or re-do) this split day
onStartWorkout(info.splitDay.id);
+ } else {
+ console.warn('Start workout callback not provided');
}
};
@@ -206,10 +210,7 @@ export default function SplitProgressTracker({
🎉
All done this week!
-
- Tap any split above to repeat it or start next week's
- training
-
+
Tap any split above to redo it
)}
diff --git a/src/components/workouts/WorkoutSession.tsx b/src/components/workouts/WorkoutSession.tsx
index b5fb522..5f671e7 100644
--- a/src/components/workouts/WorkoutSession.tsx
+++ b/src/components/workouts/WorkoutSession.tsx
@@ -3,7 +3,7 @@
* Handles active workout logging and tracking
*/
-import { useState, useMemo, useEffect } from 'react';
+import { useState, useMemo, useEffect, useRef } from 'react';
import { useWorkoutSession } from '../../hooks/useWorkoutSession';
import {
useExercises,
@@ -103,9 +103,16 @@ export default function WorkoutSession({ onNavigate }: WorkoutSessionProps) {
}, [activeMesocycle, completedWorkouts]);
// Auto-start workout if coming from mesocycle dashboard with a selected split
+ const autoStartProcessedRef = useRef(false);
useEffect(() => {
const selectedSplitDayId = localStorage.getItem('selectedSplitDayId');
- if (selectedSplitDayId && activeMesocycle) {
+ if (
+ selectedSplitDayId &&
+ activeMesocycle &&
+ !autoStartProcessedRef.current
+ ) {
+ // Mark as processed to prevent re-execution
+ autoStartProcessedRef.current = true;
// Clear immediately to prevent re-triggering
localStorage.removeItem('selectedSplitDayId');
From 5c4193ec546adb5ef86ef908f7ce9bfa3fa19c45 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Feb 2026 08:28:46 +0000
Subject: [PATCH 5/5] Improve warning messages to be more specific about impact
Co-authored-by: wulfland <5276337+wulfland@users.noreply.github.com>
---
src/components/mesocycles/SplitProgressTracker.tsx | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/components/mesocycles/SplitProgressTracker.tsx b/src/components/mesocycles/SplitProgressTracker.tsx
index 92109f1..fd2792e 100644
--- a/src/components/mesocycles/SplitProgressTracker.tsx
+++ b/src/components/mesocycles/SplitProgressTracker.tsx
@@ -151,12 +151,16 @@ export default function SplitProgressTracker({
if (onResumeWorkout) {
onResumeWorkout();
} else {
- console.warn('Resume workout callback not provided');
+ console.warn(
+ 'Resume workout callback not provided - workout cannot be resumed'
+ );
}
} else if (onStartWorkout) {
onStartWorkout(info.splitDay.id);
} else {
- console.warn('Start workout callback not provided');
+ console.warn(
+ 'Start workout callback not provided - workout cannot be started'
+ );
}
};