Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/mesocycles/MesocycleDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 64 additions & 1 deletion src/components/mesocycles/SplitProgressTracker.css
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,70 @@
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%;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button elements should have default styles reset to maintain the card appearance. The .split-card.clickable class should include properties to reset button defaults such as 'border: none;' and 'background: none;' to ensure the button inherits the .split-card styling properly. Currently, the button might display with default browser button styling that could interfere with the card appearance.

Suggested change
width: 100%;
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;

Copilot uses AI. Check for mistakes.
/* 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 {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.split-card.clickable:active {
transform: translateY(0);
box-shadow: none;
}

Comment thread
wulfland marked this conversation as resolved.
.split-card.clickable:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.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;
Expand Down
65 changes: 48 additions & 17 deletions src/components/mesocycles/SplitProgressTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,49 @@ export default function SplitProgressTracker({
<div className="split-cards">
{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) {
if (onResumeWorkout) {
onResumeWorkout();
} else {
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 - workout cannot be started'
);
}
};
Comment on lines +149 to +165
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleCardClick function has a potential issue: when isInProgress is true but onResumeWorkout is undefined, the button click does nothing. Similarly, when isInProgress is false but onStartWorkout is undefined, the button click does nothing. Consider adding a fallback or disabling the button when the required callbacks are not provided.

For example:

const handleCardClick = () => {
  if (isInProgress) {
    if (onResumeWorkout) {
      onResumeWorkout();
    } else {
      console.warn('Resume workout callback not provided');
    }
  } else if (onStartWorkout) {
    onStartWorkout(info.splitDay.id);
  } else {
    console.warn('Start workout callback not provided');
  }
};

Alternatively, disable the button when callbacks are missing:

disabled={isInProgress ? !onResumeWorkout : !onStartWorkout}

Copilot uses AI. Check for mistakes.

return (
<div
<button
type="button"
key={info.splitDay.id}
className={`split-card ${info.completed ? 'completed' : ''} ${isNext ? 'next' : ''}`}
className={`split-card clickable ${info.completed ? 'completed' : ''} ${isNext ? 'next' : ''} ${isInProgress ? 'in-progress' : ''}`}
onClick={handleCardClick}
aria-label={`${
isInProgress ? 'Resume' : info.completed ? 'Redo' : 'Start'
} ${info.splitDay.name}`}
>
<div className="split-card-header">
<span className="split-name">{info.splitDay.name}</span>
<span className="split-status">
{info.completed ? '✓' : isNext ? '★ Next' : ''}
{isInProgress
? '🏋️ In Progress'
: info.completed
? '✓'
: isNext
? '★ Next'
: ''}
</span>
</div>
{info.completedDate && (
Expand All @@ -164,28 +197,26 @@ export default function SplitProgressTracker({
{info.splitDay.exercises.length !== 1 ? 's' : ''}
</div>
)}
</div>
<div className="split-card-action">
{isInProgress
? 'Tap to resume'
: info.completed
? 'Tap to redo'
: 'Tap to start'}
</div>
</button>
);
})}
</div>

{/* Action Button */}
{allCompleted ? (
{/* Completion Message */}
{allCompleted && (
<div className="completion-message">
<span className="completion-icon">🎉</span>
<p className="completion-text">All done this week!</p>
<p className="completion-subtext">
You can repeat splits or start next week&apos;s training
</p>
<p className="completion-subtext">Tap any split above to redo it</p>
</div>
) : nextSplit && onStartWorkout ? (
<button
className="btn btn-primary start-workout-btn"
onClick={() => onStartWorkout(nextSplit.id)}
>
Start Next Workout
</button>
) : null}
)}
</div>

{/* Deload Week Message */}
Expand Down
26 changes: 23 additions & 3 deletions src/components/workouts/WorkoutSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -103,11 +103,25 @@ 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 && !isActive) {
if (
selectedSplitDayId &&
activeMesocycle &&
!autoStartProcessedRef.current
) {
// Mark as processed to prevent re-execution
autoStartProcessedRef.current = true;
// 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) => {
Expand All @@ -116,7 +130,13 @@ export default function WorkoutSession({ onNavigate }: WorkoutSessionProps) {
}
);
}
}, [activeMesocycle, isActive, startWorkoutFromSplit, showToast]);
}, [
activeMesocycle,
isActive,
startWorkoutFromSplit,
cancelWorkout,
showToast,
]);
Comment on lines 107 to +139
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useEffect has a potential infinite loop issue. When isActive is true and cancelWorkout is called on line 115, it changes the isActive state to false. Since isActive is a dependency in the array on line 128, this triggers the effect to run again. Although the selectedSplitDayId is removed from localStorage on line 110 (preventing further iterations), this causes an unnecessary re-execution of the effect.

To fix this, consider one of these approaches:

  1. Remove isActive from the dependency array and use a ref to check its current value
  2. Move the cancelWorkout call outside the effect by checking isActive before setting selectedSplitDayId in MesocycleDashboard
  3. Use a separate flag to track whether auto-start has already been triggered for this selectedSplitDayId

Copilot uses AI. Check for mistakes.

// Get all unique muscle groups from workout exercises
const workoutMuscleGroups = useMemo(() => {
Expand Down
62 changes: 2 additions & 60 deletions src/hooks/useWorkoutSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type {
WorkoutExercise,
WorkoutSet,
WorkoutFeedback,
MesocycleSplitDay,
} from '../types/models';
import {
createWorkout,
Expand All @@ -20,7 +19,6 @@ import {
createEmptySet,
getPreviousPerformance,
startWorkoutFromSplit as startWorkoutFromSplitService,
getActiveMesocycle,
} from '../db/service';

interface UseWorkoutSessionReturn {
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down