Edit modals for Sets are complete
This commit is contained in:
@@ -10,6 +10,8 @@ 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>;
|
||||
@@ -79,6 +81,15 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
||||
// 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();
|
||||
|
||||
@@ -244,145 +255,34 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
||||
<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)"
|
||||
/>
|
||||
)}
|
||||
{(() => {
|
||||
const exDef = exercises.find(e => e.name === set.exerciseName); // Best effort matching by name since set might not have exerciseId deeply populated in some contexts, but id is safer.
|
||||
// Actually set has exerciseId usually. Let's try to match by ID if possible, else name.
|
||||
// But wait, ActiveSession sets might not have exerciseId if created ad-hoc? No, they should.
|
||||
// Let's assume we can look up by name if id missing, or just check set.side presence.
|
||||
// Detailed look: The session object has sets.
|
||||
// Ideally check exDef.isUnilateral.
|
||||
const isUnilateral = set.side || (exercises.find(e => e.name === set.exerciseName)?.isUnilateral);
|
||||
|
||||
if (isUnilateral) {
|
||||
return (
|
||||
<div className="col-span-2 flex bg-surface-container-high rounded p-0.5">
|
||||
{(['LEFT', 'ALTERNATELY', 'RIGHT'] as const).map((side) => {
|
||||
const labelMap: Record<string, string> = { LEFT: 'L', RIGHT: 'R', ALTERNATELY: 'A' };
|
||||
return (
|
||||
<button
|
||||
key={side}
|
||||
onClick={() => setEditSide(side)}
|
||||
title={t(side.toLowerCase() as any, lang)}
|
||||
className={`flex-1 text-[10px] py-1 rounded transition-colors ${editSide === side
|
||||
? 'bg-primary/10 text-primary font-bold'
|
||||
: 'text-on-surface-variant hover:bg-surface-container'
|
||||
}`}
|
||||
>
|
||||
{labelMap[side]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</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 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>
|
||||
<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"
|
||||
aria-label={t('cancel', lang)}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(set)}
|
||||
className="p-2 text-primary hover:bg-primary-container/20 rounded-full transition-colors"
|
||||
aria-label={t('save', lang)}
|
||||
>
|
||||
<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"
|
||||
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 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>
|
||||
);
|
||||
@@ -456,6 +356,18 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CheckCircle, Plus, Pencil, Trash2, X, Save } from 'lucide-react';
|
||||
import { Language, WorkoutSet } from '../../types';
|
||||
import { t } from '../../services/i18n';
|
||||
import ExerciseModal from '../ExerciseModal';
|
||||
import EditSetModal from '../EditSetModal';
|
||||
import { useTracker } from './useTracker';
|
||||
import SetLogger from './SetLogger';
|
||||
import { formatSetMetrics } from '../../utils/setFormatting';
|
||||
@@ -157,139 +158,35 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
|
||||
|
||||
{/* Edit Set Modal */}
|
||||
{editingSetId && editingSet && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-surface-container w-full max-w-md rounded-[28px] p-6 shadow-elevation-3 max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-normal text-on-surface">{t('edit', lang)}</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingSetId(null);
|
||||
setEditingSet(null);
|
||||
}}
|
||||
className="p-2 hover:bg-surface-container-high rounded-full transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{/* Side Selector */}
|
||||
{(() => {
|
||||
const exDef = exercises.find(e => e.name === editingSet.exerciseName);
|
||||
const isUnilateral = editingSet.side || exDef?.isUnilateral;
|
||||
|
||||
if (isUnilateral) {
|
||||
return (
|
||||
<div className="bg-surface-container-high rounded-lg p-2">
|
||||
<label className="text-sm text-on-surface-variant block mb-2">{t('unilateral', lang)}</label>
|
||||
<div className="flex bg-surface-container rounded p-0.5">
|
||||
{(['LEFT', 'ALTERNATELY', 'RIGHT'] as const).map((side) => {
|
||||
const labelMap: Record<string, string> = { LEFT: 'L', RIGHT: 'R', ALTERNATELY: 'A' };
|
||||
return (
|
||||
<button
|
||||
key={side}
|
||||
onClick={() => setEditingSet({ ...editingSet, side })}
|
||||
title={t(side.toLowerCase() as any, lang)}
|
||||
className={`flex-1 text-xs py-2 rounded transition-colors ${editingSet.side === side
|
||||
? 'bg-primary/10 text-primary font-bold'
|
||||
: 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
{labelMap[side]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{(editingSet.type === 'STRENGTH' || editingSet.type === 'BODYWEIGHT') && (
|
||||
<>
|
||||
<div>
|
||||
<label className="text-sm text-on-surface-variant">{t('weight_kg', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={editingSet.weight || ''}
|
||||
onChange={(e) => setEditingSet({ ...editingSet, weight: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-on-surface-variant">{t('reps', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editingSet.reps || ''}
|
||||
onChange={(e) => setEditingSet({ ...editingSet, reps: parseInt(e.target.value) || 0 })}
|
||||
className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(editingSet.type === 'CARDIO' || editingSet.type === 'STATIC') && (
|
||||
<div>
|
||||
<label className="text-sm text-on-surface-variant">{t('time_sec', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editingSet.durationSeconds || ''}
|
||||
onChange={(e) => setEditingSet({ ...editingSet, durationSeconds: parseInt(e.target.value) || 0 })}
|
||||
className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{editingSet.type === 'CARDIO' && (
|
||||
<div>
|
||||
<label className="text-sm text-on-surface-variant">{t('dist_m', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={editingSet.distanceMeters || ''}
|
||||
onChange={(e) => setEditingSet({ ...editingSet, distanceMeters: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingSetId(null);
|
||||
setEditingSet(null);
|
||||
}}
|
||||
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5"
|
||||
>
|
||||
{t('cancel', lang)}
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/active/set/${editingSetId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify(editingSet)
|
||||
});
|
||||
if (response.ok) {
|
||||
await loadQuickLogSession();
|
||||
setEditingSetId(null);
|
||||
setEditingSet(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update set:', error);
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 rounded-full bg-primary text-on-primary font-medium flex items-center gap-2"
|
||||
>
|
||||
<Save size={18} />
|
||||
{t('save', lang)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditSetModal
|
||||
isOpen={!!editingSetId}
|
||||
onClose={() => {
|
||||
setEditingSetId(null);
|
||||
setEditingSet(null);
|
||||
}}
|
||||
set={editingSet}
|
||||
exerciseDef={exercises.find(e => e.id === editingSet.exerciseId || e.name === editingSet.exerciseName)}
|
||||
onSave={async (updatedSet) => {
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/active/set/${editingSetId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify(updatedSet)
|
||||
});
|
||||
if (response.ok) {
|
||||
await loadQuickLogSession();
|
||||
setEditingSetId(null);
|
||||
setEditingSet(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update set:', error);
|
||||
}
|
||||
}}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
|
||||
@@ -263,6 +263,7 @@ export const useTracker = (props: any) => { // Props ignored/removed
|
||||
onSessionEnd: endSession,
|
||||
onSessionQuit: quitSession,
|
||||
onRemoveSet: removeSet,
|
||||
updateSet: handleUpdateSetWrapper,
|
||||
activeSession, // Need this in view
|
||||
timer // Expose timer to views
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user