384 lines
23 KiB
TypeScript
384 lines
23 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';
|
|
|
|
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,
|
|
handleCancelEdit,
|
|
handleSaveEdit,
|
|
handleEditSet,
|
|
isCreating,
|
|
setIsCreating,
|
|
handleCreateExercise,
|
|
exercises
|
|
} = tracker;
|
|
|
|
|
|
|
|
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"
|
|
>
|
|
<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={handleAddSet}
|
|
/>
|
|
|
|
{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;
|
|
const isEditing = editingSetId === set.id;
|
|
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>
|
|
{isEditing ? (
|
|
<div className="flex-1">
|
|
<div className="text-base font-medium text-on-surface mb-2">{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="grid grid-cols-2 gap-2">
|
|
{set.weight !== undefined && (
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
value={editWeight}
|
|
onChange={(e) => setEditWeight(e.target.value)}
|
|
className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none"
|
|
placeholder="Weight (kg)"
|
|
/>
|
|
)}
|
|
{set.reps !== undefined && (
|
|
<input
|
|
type="number"
|
|
value={editReps}
|
|
onChange={(e) => setEditReps(e.target.value)}
|
|
className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none"
|
|
placeholder="Reps"
|
|
/>
|
|
)}
|
|
{set.durationSeconds !== undefined && (
|
|
<input
|
|
type="number"
|
|
value={editDuration}
|
|
onChange={(e) => setEditDuration(e.target.value)}
|
|
className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none"
|
|
placeholder="Duration (s)"
|
|
/>
|
|
)}
|
|
{set.distanceMeters !== undefined && (
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
value={editDistance}
|
|
onChange={(e) => setEditDistance(e.target.value)}
|
|
className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none"
|
|
placeholder="Distance (m)"
|
|
/>
|
|
)}
|
|
{set.height !== undefined && (
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
value={editHeight}
|
|
onChange={(e) => setEditHeight(e.target.value)}
|
|
className="px-2 py-1 bg-surface-container-high rounded text-sm text-on-surface border border-outline-variant focus:border-primary focus:outline-none"
|
|
placeholder="Height (cm)"
|
|
/>
|
|
)}
|
|
</div>
|
|
</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">
|
|
{set.type === ExerciseType.STRENGTH &&
|
|
`${set.weight ? `${set.weight}kg` : ''} ${set.reps ? `x ${set.reps}` : ''}`.trim()
|
|
}
|
|
{set.type === ExerciseType.BODYWEIGHT &&
|
|
`${set.weight ? `${set.weight}kg` : ''} ${set.reps ? `x ${set.reps}` : ''}`.trim()
|
|
}
|
|
{set.type === ExerciseType.CARDIO &&
|
|
`${set.durationSeconds ? `${set.durationSeconds}s` : ''} ${set.distanceMeters ? `/ ${set.distanceMeters}m` : ''}`.trim()
|
|
}
|
|
{set.type === ExerciseType.STATIC &&
|
|
`${set.durationSeconds ? `${set.durationSeconds}s` : ''}`.trim()
|
|
}
|
|
{set.type === ExerciseType.HIGH_JUMP &&
|
|
`${set.height ? `${set.height}cm` : ''}`.trim()
|
|
}
|
|
{set.type === ExerciseType.LONG_JUMP &&
|
|
`${set.distanceMeters ? `${set.distanceMeters}m` : ''}`.trim()
|
|
}
|
|
{set.type === ExerciseType.PLYOMETRIC &&
|
|
`${set.reps ? `x ${set.reps}` : ''}`.trim()
|
|
}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{isEditing ? (
|
|
<>
|
|
<button
|
|
onClick={handleCancelEdit}
|
|
className="p-2 text-on-surface-variant hover:text-on-surface hover:bg-surface-container-high rounded-full transition-colors"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
<button
|
|
onClick={() => handleSaveEdit(set)}
|
|
className="p-2 text-primary hover:bg-primary-container/20 rounded-full transition-colors"
|
|
>
|
|
<CheckCircle size={20} />
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<button
|
|
onClick={() => handleEditSet(set)}
|
|
className="p-2 text-on-surface-variant hover:text-primary hover:bg-primary-container/20 rounded-full transition-colors"
|
|
>
|
|
<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"
|
|
>
|
|
<Trash2 size={20} />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{isCreating && (
|
|
<ExerciseModal
|
|
isOpen={isCreating}
|
|
onClose={() => setIsCreating(false)}
|
|
onSave={handleCreateExercise}
|
|
lang={lang}
|
|
existingExercises={exercises}
|
|
/>
|
|
)}
|
|
|
|
{/* 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>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ActiveSessionView;
|