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' + ); } };