All tests fixed. Deployment on NAS prepared
This commit is contained in:
@@ -95,10 +95,37 @@ const EditSetModal: React.FC<EditSetModalProps> = ({
|
||||
<h3 className="text-lg font-medium text-on-surface mb-1">
|
||||
{set.exerciseName}
|
||||
</h3>
|
||||
<p className="text-sm text-on-surface-variant">
|
||||
<p className="text-sm text-on-surface-variant mb-2">
|
||||
{exerciseDef?.type || set.type}
|
||||
{set.side && ` • ${t(set.side.toLowerCase() as any, lang)}`}
|
||||
</p>
|
||||
{set.side && (
|
||||
<div className="flex items-center gap-2 bg-surface-container rounded-full p-1 w-fit">
|
||||
<button
|
||||
onClick={() => handleUpdate('side', 'LEFT')}
|
||||
title={t('left', lang)}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${set.side === 'LEFT' ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
L
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdate('side', 'ALTERNATELY')}
|
||||
title={t('alternately', lang)}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${set.side === 'ALTERNATELY' ? 'bg-tertiary-container text-on-tertiary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
A
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdate('side', 'RIGHT')}
|
||||
title={t('right', lang)}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${set.side === 'RIGHT' ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
R
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date & Time */}
|
||||
|
||||
@@ -318,6 +318,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-on-surface-variant hover:text-primary"
|
||||
aria-label={t('edit', lang)}
|
||||
>
|
||||
<Pencil size={24} />
|
||||
</Button>
|
||||
@@ -332,6 +333,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-error hover:text-error"
|
||||
aria-label={t('delete', lang)}
|
||||
>
|
||||
<Trash2 size={24} />
|
||||
</Button>
|
||||
|
||||
@@ -123,7 +123,7 @@ const SortablePlanStep: React.FC<SortablePlanStepProps> = ({ step, index, toggle
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} {...attributes}>
|
||||
<div ref={setNodeRef} style={style} {...attributes} data-testid="plan-exercise-item">
|
||||
<Card
|
||||
className={`flex items-center gap-3 transition-all hover:bg-surface-container-high ${isDragging ? 'bg-surface-container-high shadow-elevation-3' : ''}`}
|
||||
>
|
||||
@@ -666,35 +666,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
</SideSheet>
|
||||
|
||||
|
||||
<SideSheet
|
||||
isOpen={showExerciseSelector}
|
||||
onClose={() => setShowExerciseSelector(false)}
|
||||
title={t('select_exercise', lang)}
|
||||
width="lg"
|
||||
>
|
||||
<div className="flex flex-col h-[60vh]">
|
||||
<div className="flex justify-end mb-2">
|
||||
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" className="text-primary hover:bg-primary-container/20 flex gap-2">
|
||||
<Plus size={18} /> {t('create_exercise', lang)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto -mx-6 px-6">
|
||||
{availableExercises
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(ex => (
|
||||
<button
|
||||
key={ex.id}
|
||||
onClick={() => addStep(ex)}
|
||||
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between group"
|
||||
>
|
||||
<span className="group-hover:text-primary transition-colors">{ex.name}</span>
|
||||
<span className="text-xs bg-secondary-container text-on-secondary-container px-2 py-1 rounded-full">{ex.type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SideSheet>
|
||||
|
||||
|
||||
<ExerciseModal
|
||||
isOpen={isCreatingExercise}
|
||||
|
||||
@@ -35,18 +35,26 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
|
||||
let initialDuration = defaultTime;
|
||||
|
||||
if (savedState) {
|
||||
initialDuration = savedState.duration || defaultTime;
|
||||
initialStatus = savedState.status;
|
||||
initialTimeLeft = savedState.timeLeft;
|
||||
// Only restore if running OR if the context (defaultTime) matches
|
||||
// This prevents carrying over timer state between different plan steps or sessions
|
||||
// where the default time is different.
|
||||
const contextMatch = savedState.defaultTimeSnapshot === defaultTime;
|
||||
const isRunning = savedState.status === 'RUNNING';
|
||||
|
||||
if (initialStatus === 'RUNNING' && savedState.endTime) {
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, Math.ceil((savedState.endTime - now) / 1000));
|
||||
if (remaining > 0) {
|
||||
initialTimeLeft = remaining;
|
||||
} else {
|
||||
initialStatus = 'FINISHED'; // It finished while we were away
|
||||
initialTimeLeft = 0;
|
||||
if (isRunning || contextMatch) {
|
||||
initialDuration = savedState.duration || defaultTime;
|
||||
initialStatus = savedState.status;
|
||||
initialTimeLeft = savedState.timeLeft;
|
||||
|
||||
if (initialStatus === 'RUNNING' && savedState.endTime) {
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, Math.ceil((savedState.endTime - now) / 1000));
|
||||
if (remaining > 0) {
|
||||
initialTimeLeft = remaining;
|
||||
} else {
|
||||
initialStatus = 'FINISHED'; // It finished while we were away
|
||||
initialTimeLeft = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,10 +98,11 @@ export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
|
||||
status,
|
||||
timeLeft,
|
||||
duration,
|
||||
endTime: endTimeRef.current
|
||||
endTime: endTimeRef.current,
|
||||
defaultTimeSnapshot: defaultTime // Save context
|
||||
};
|
||||
localStorage.setItem('gymflow_rest_timer', JSON.stringify(stateToSave));
|
||||
}, [status, timeLeft, duration]);
|
||||
}, [status, timeLeft, duration, defaultTime]);
|
||||
|
||||
// Update internal duration when defaultTime changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -125,7 +125,7 @@ const translations = {
|
||||
my_plans: 'My Plans',
|
||||
no_plans_yet: 'No workout plans yet.',
|
||||
ask_ai_to_create: 'Ask your AI coach to create one',
|
||||
create_manually: 'Create one manually',
|
||||
create_manually: 'Manually',
|
||||
create_with_ai: 'With AI',
|
||||
ai_plan_prompt_title: 'Create Plan with AI',
|
||||
ai_plan_prompt_placeholder: 'Any specific requirements? (optional)',
|
||||
|
||||
@@ -32,6 +32,7 @@ export const getSessions = async (userId: string): Promise<WorkoutSession[]> =>
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set) => ({
|
||||
...set,
|
||||
timestamp: new Date(set.timestamp).getTime(),
|
||||
exerciseName: set.exerciseName || set.exercise?.name || 'Unknown',
|
||||
type: set.type || set.exercise?.type || ExerciseType.STRENGTH
|
||||
})) as WorkoutSet[]
|
||||
@@ -60,6 +61,7 @@ export const getActiveSession = async (userId: string): Promise<WorkoutSession |
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set) => ({
|
||||
...set,
|
||||
timestamp: new Date(set.timestamp).getTime(),
|
||||
exerciseName: set.exerciseName || set.exercise?.name || 'Unknown',
|
||||
type: set.type || set.exercise?.type || ExerciseType.STRENGTH
|
||||
})) as WorkoutSet[]
|
||||
|
||||
Reference in New Issue
Block a user