Files
gymflow/src/components/Tracker/ActiveSessionView.tsx

378 lines
18 KiB
TypeScript

import React from 'react';
import { MoreVertical, X, CheckSquare, ChevronUp, ChevronDown, Scale, Dumbbell, Plus, Activity, Timer as TimerIcon, ArrowRight, ArrowUp, CheckCircle, Edit, Trash2 } from 'lucide-react';
import { ExerciseType, Language, WorkoutSet } from '../../types';
import { t } from '../../services/i18n';
import FilledInput from '../FilledInput';
import ExerciseModal from '../ExerciseModal';
import { useTracker } from './useTracker';
import SetLogger from './SetLogger';
import { formatSetMetrics } from '../../utils/setFormatting';
import { useAuth } from '../../context/AuthContext';
import { api } from '../../services/api';
import RestTimerFAB from '../ui/RestTimerFAB';
import EditSetModal from '../EditSetModal';
interface ActiveSessionViewProps {
tracker: ReturnType<typeof useTracker>;
activeSession: any; // Using any to avoid strict type issues with the complex session object for now, but ideally should be WorkoutSession
lang: Language;
onSessionEnd: () => void;
onSessionQuit: () => void;
onRemoveSet: (setId: string) => void;
}
const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSession, lang, onSessionEnd, onSessionQuit, onRemoveSet }) => {
const {
elapsedTime,
showFinishConfirm,
setShowFinishConfirm,
showQuitConfirm,
setShowQuitConfirm,
showMenu,
setShowMenu,
activePlan, // This comes from useTracker props but we might need to pass it explicitly if not in hook return
currentStepIndex,
showPlanList,
setShowPlanList,
jumpToStep,
searchQuery,
setSearchQuery,
setShowSuggestions,
showSuggestions,
filteredExercises,
setSelectedExercise,
selectedExercise,
weight,
setWeight,
reps,
setReps,
duration,
setDuration,
distance,
setDistance,
height,
setHeight,
handleAddSet,
editingSetId,
editWeight,
setEditWeight,
editReps,
setEditReps,
editDuration,
setEditDuration,
editDistance,
setEditDistance,
editHeight,
setEditHeight,
editSide,
setEditSide,
handleCancelEdit,
handleSaveEdit,
handleEditSet,
isCreating,
setIsCreating,
handleCreateExercise,
exercises
} = tracker;
const { currentUser, updateUser } = useAuth();
// Timer Logic is now managed in useTracker to persist across re-renders/step changes
const { timer } = tracker;
const [editingSet, setEditingSet] = React.useState<WorkoutSet | null>(null);
const handleSaveSetFromModal = async (updatedSet: WorkoutSet) => {
if (tracker.updateSet) {
tracker.updateSet(updatedSet);
}
setEditingSet(null);
};
const handleLogSet = async () => {
await handleAddSet();
// Determine next rest time
let nextTime = currentUser?.profile?.restTimerDefault || 120;
if (activePlan) {
// Logic: activePlan set just added. We are moving to next step?
// Tracker's handleAddSet calls addSet -> which calls ActiveWorkoutContext's addSet -> which increments currentStepIndex (logic inside context)
// But state update might be async or we might need to look at current index before update?
// Usually we want the rest time AFTER the set we just did.
// The user just configured the set for the *current* step index.
// So we look at activePlan.steps[currentStepIndex].restTime.
// BUT, if the user just finished step 0, and step 0 says "Rest 60s", then we rest 60s.
// If fallback, use default.
// Note: currentStepIndex might update immediately or after render.
// In a real app, we might get the next set's target time? No, rest is usually associated with the fatigue of the set just done.
// Requirement: "rest time after this set".
// So we use currentStepIndex (which likely points to the set we just logged, assuming UI hasn't advanced yet?
// Actually, handleAddSet likely appends set. Context might auto-advance.
// Let's assume we use the restTime of the step that matches the set just logged.
const currentStep = activePlan.steps[currentStepIndex];
if (currentStep && currentStep.restTimeSeconds) {
nextTime = currentStep.restTimeSeconds;
}
}
if (timer.status !== 'RUNNING') {
timer.reset(nextTime);
// timer.start(); // Removed per user request: disable auto-start
}
};
const handleDurationChange = async (newVal: number) => {
// Update user profile
try {
await api.patch('/auth/profile', { restTimerDefault: newVal });
if (currentUser) {
updateUser({
...currentUser,
profile: { ...currentUser.profile, restTimerDefault: newVal }
});
}
} catch (e) {
console.error("Failed to update default timer", e);
}
};
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;
return (
<div className="flex flex-col h-full max-h-full overflow-hidden relative bg-surface">
<div className="px-4 py-3 bg-surface-container shadow-elevation-1 z-20 flex justify-between items-center">
<div className="flex flex-col">
<h2 className="text-title-medium text-on-surface flex items-center gap-2 font-medium">
<span className="w-2 h-2 rounded-full bg-error animate-pulse" />
{activePlan ? activePlan.name : t('free_workout', lang)}
</h2>
<span className="text-xs text-on-surface-variant font-mono mt-0.5 flex items-center gap-2">
<span className="bg-surface-container-high px-1.5 py-0.5 rounded text-on-surface font-bold">{elapsedTime}</span>
{activeSession.userBodyWeight ? `${activeSession.userBodyWeight}kg` : ''}
</span>
</div>
<div className="flex items-center gap-2 relative">
<button
onClick={() => setShowFinishConfirm(true)}
className="px-5 py-2 rounded-full bg-error-container text-on-error-container text-sm font-medium hover:opacity-90 transition-opacity"
>
{t('finish', lang)}
</button>
<button
onClick={() => setShowMenu(!showMenu)}
className="p-2 rounded-full bg-surface-container-high text-on-surface hover:bg-surface-container-highest transition-colors"
aria-label="Options"
>
<MoreVertical size={20} />
</button>
{showMenu && (
<>
<div
className="fixed inset-0 z-30"
onClick={() => setShowMenu(false)}
/>
<div className="absolute right-0 top-full mt-2 bg-surface-container rounded-xl shadow-elevation-3 overflow-hidden z-40 min-w-[200px]">
<button
onClick={() => {
setShowMenu(false);
setShowQuitConfirm(true);
}}
className="w-full px-4 py-3 text-left text-error hover:bg-error-container/20 transition-colors flex items-center gap-2"
>
<X size={18} />
{t('quit_no_save', lang)}
</button>
</div>
</>
)}
</div>
</div>
{activePlan && (
<div className="bg-surface-container-low border-b border-outline-variant">
<button
onClick={() => setShowPlanList(!showPlanList)}
className="w-full px-4 py-3 flex justify-between items-center"
>
<div className="flex flex-col items-start">
{isPlanFinished ? (
<div className="flex items-center gap-2 text-primary">
<CheckSquare size={18} />
<span className="font-bold">{t('plan_completed', lang)}</span>
</div>
) : (
<>
<span className="text-[10px] text-primary font-medium tracking-wider">{t('step', lang)} {currentStepIndex + 1} {t('of', lang)} {activePlan.steps.length}</span>
<div className="font-medium text-on-surface flex items-center gap-2">
{activePlan.steps[currentStepIndex].exerciseName}
{activePlan.steps[currentStepIndex].isWeighted && <Scale size={12} className="text-primary" />}
</div>
</>
)}
</div>
{showPlanList ? <ChevronUp size={20} className="text-on-surface-variant" /> : <ChevronDown size={20} className="text-on-surface-variant" />}
</button>
{showPlanList && (
<div className="max-h-48 overflow-y-auto bg-surface-container-high p-2 space-y-1 animate-in slide-in-from-top-2">
{activePlan.steps.map((step, idx) => (
<button
key={step.id}
onClick={() => jumpToStep(idx)}
className={`w-full text-left px-4 py-3 rounded-full text-sm flex items-center justify-between transition-colors ${idx === currentStepIndex
? 'bg-primary-container text-on-primary-container font-medium'
: idx < currentStepIndex
? 'text-on-surface-variant opacity-50'
: 'text-on-surface hover:bg-white/5'
}`}
>
<span>{idx + 1}. {step.exerciseName}</span>
{step.isWeighted && <Scale size={14} />}
</button>
))}
</div>
)}
</div>
)}
<div className="flex-1 overflow-y-auto p-4 pb-32 space-y-6">
<SetLogger
tracker={tracker}
lang={lang}
onLogSet={handleLogSet}
/>
{activeSession.sets.length > 0 && (
<div className="pt-4">
<h3 className="text-sm text-primary font-medium px-2 mb-3 tracking-wide">{t('history_section', lang)}</h3>
<div className="flex flex-col gap-2">
{[...activeSession.sets].reverse().map((set: WorkoutSet, idx: number) => {
const setNumber = activeSession.sets.length - idx;
return (
<div key={set.id} className="flex justify-between items-center p-4 bg-surface-container rounded-xl shadow-elevation-1 animate-in fade-in slide-in-from-bottom-2">
<div className="flex items-center gap-4 flex-1">
<div className="w-8 h-8 rounded-full bg-secondary-container text-on-secondary-container flex items-center justify-center text-xs font-bold">
{setNumber}
</div>
<div>
<div className="text-base font-medium text-on-surface">{set.exerciseName}{set.side && <span className="ml-2 text-xs font-medium text-on-surface-variant">{t(set.side.toLowerCase() as any, lang)}</span>}</div>
<div className="text-sm text-on-surface-variant">
{formatSetMetrics(set, lang)}
</div>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setEditingSet(set)}
className="p-2 text-on-surface-variant hover:text-primary hover:bg-primary-container/20 rounded-full transition-colors"
aria-label={t('edit', lang)}
>
<Edit size={20} />
</button>
<button
onClick={() => onRemoveSet(set.id)}
className="p-2 text-on-surface-variant hover:text-error hover:bg-error-container/10 rounded-full transition-colors"
aria-label={t('delete', lang)}
>
<Trash2 size={20} />
</button>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
{isCreating && (
<ExerciseModal
isOpen={isCreating}
onClose={() => setIsCreating(false)}
onSave={handleCreateExercise}
lang={lang}
existingExercises={exercises}
initialName={tracker.searchQuery}
/>
)}
{/* Finish Confirmation Dialog */}
{showFinishConfirm && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-6 backdrop-blur-sm">
<div className="w-full max-w-sm bg-surface-container rounded-[28px] p-6 shadow-elevation-3">
<h3 className="text-2xl font-normal text-on-surface mb-2">{t('finish_confirm_title', lang)}</h3>
<p className="text-on-surface-variant text-sm mb-8">{t('finish_confirm_msg', lang)}</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowFinishConfirm(false)}
className="px-6 py-2.5 rounded-full text-primary font-medium hover:bg-white/5"
>
{t('cancel', lang)}
</button>
<button
onClick={() => {
setShowFinishConfirm(false);
onSessionEnd();
}}
className="px-6 py-2.5 rounded-full bg-green-600 text-white font-medium hover:bg-green-700"
>
{t('confirm', lang)}
</button>
</div>
</div>
</div>
)}
{/* Quit Without Saving Confirmation Dialog */}
{showQuitConfirm && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-6 backdrop-blur-sm">
<div className="w-full max-w-sm bg-surface-container rounded-[28px] p-6 shadow-elevation-3">
<h3 className="text-2xl font-normal text-error mb-2">{t('quit_confirm_title', lang)}</h3>
<p className="text-on-surface-variant text-sm mb-8">{t('quit_confirm_msg', lang)}</p>
<div className="flex justify-end gap-2">
<button
onClick={() => setShowQuitConfirm(false)}
className="px-6 py-2.5 rounded-full text-primary font-medium hover:bg-white/5"
>
{t('cancel', lang)}
</button>
<button
onClick={() => {
setShowQuitConfirm(false);
onSessionQuit();
}}
className="px-6 py-2.5 rounded-full bg-green-600 text-white font-medium hover:bg-green-700"
>
{t('confirm', lang)}
</button>
</div>
</div>
</div>
)}
{/* Edit Set Modal */}
{editingSet && (
<EditSetModal
isOpen={!!editingSet}
onClose={() => setEditingSet(null)}
set={editingSet}
exerciseDef={tracker.exercises.find(e => e.id === editingSet.exerciseId) || tracker.exercises.find(e => e.name === editingSet.exerciseName)}
onSave={handleSaveSetFromModal}
lang={lang}
/>
)}
<RestTimerFAB timer={timer} onDurationChange={handleDurationChange} />
</div>
);
};
export default ActiveSessionView;