Edit modals for Sets are complete

This commit is contained in:
AG
2025-12-12 00:12:18 +02:00
parent 70ea0a0ac3
commit 87f639e320
8 changed files with 379 additions and 385 deletions

View File

@@ -0,0 +1,217 @@
import React, { useState, useEffect } from 'react';
import { Dumbbell, Activity, Percent, Timer, ArrowRight, ArrowUp, Save, Calendar, Clock } from 'lucide-react';
import { WorkoutSet, ExerciseType, ExerciseDef, Language } from '../types';
import { t } from '../services/i18n';
import { formatSetMetrics } from '../utils/setFormatting';
import { Modal } from './ui/Modal';
import { Button } from './ui/Button';
interface EditSetModalProps {
isOpen: boolean;
onClose: () => void;
set: WorkoutSet;
exerciseDef?: ExerciseDef;
onSave: (updatedSet: WorkoutSet) => Promise<void> | void;
lang: Language;
}
const EditSetModal: React.FC<EditSetModalProps> = ({
isOpen,
onClose,
set: initialSet,
exerciseDef,
onSave,
lang
}) => {
const [set, setSet] = useState<WorkoutSet>(initialSet);
// Reset state when modal opens with a new set
useEffect(() => {
setSet(initialSet);
}, [initialSet, isOpen]);
const handleUpdate = (field: keyof WorkoutSet, value: any) => {
setSet(prev => ({ ...prev, [field]: value }));
};
const handleSave = async () => {
await onSave(set);
onClose();
};
// Date/Time handling
const setDate = new Date(set.timestamp);
const dateStr = dateStrFromDate(setDate);
const timeStr = timeStrFromDate(setDate);
function dateStrFromDate(d: Date) {
// YYYY-MM-DD
const pad = (n: number) => n < 10 ? '0' + n : n;
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
function timeStrFromDate(d: Date) {
// HH:mm
const pad = (n: number) => n < 10 ? '0' + n : n;
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
const handleDateChange = (newDateStr: string) => {
// Keep time, change date
const current = new Date(set.timestamp);
const [y, m, d] = newDateStr.split('-').map(Number);
const newDate = new Date(y, m - 1, d, current.getHours(), current.getMinutes(), current.getSeconds());
handleUpdate('timestamp', newDate.getTime());
};
const handleTimeChange = (newTimeStr: string) => {
// Keep date, change time
const current = new Date(set.timestamp);
const [h, min] = newTimeStr.split(':').map(Number);
const newDate = new Date(current.getFullYear(), current.getMonth(), current.getDate(), h, min, current.getSeconds());
handleUpdate('timestamp', newDate.getTime());
};
// Determine functionality based on resolved type
// Fallback: Check feature availability based on Type String or Prop existence
const type = (exerciseDef?.type || set.type) as string;
const hasWeight = ['STRENGTH', 'BODYWEIGHT', 'STATIC', 'Strength', 'Bodyweight', 'Static'].includes(type) || set.weight !== null;
const hasReps = ['STRENGTH', 'BODYWEIGHT', 'PLYOMETRIC', 'Strength', 'Bodyweight', 'Plyometric'].includes(type) || set.reps !== null;
const hasBwPercent = ['BODYWEIGHT', 'STATIC', 'Bodyweight', 'Static'].includes(type) || set.bodyWeightPercentage !== null;
const hasTime = ['CARDIO', 'STATIC', 'Cardio', 'Static'].includes(type) || set.durationSeconds !== null;
const hasDist = ['CARDIO', 'LONG_JUMP', 'Cardio', 'Long Jump'].includes(type) || set.distanceMeters !== null;
const hasHeight = ['HIGH_JUMP', 'High Jump'].includes(type) || set.height !== null;
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={t('edit_set', lang) || 'Edit Set'}
maxWidth="sm"
>
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium text-on-surface mb-1">
{set.exerciseName}
</h3>
<p className="text-sm text-on-surface-variant">
{exerciseDef?.type || set.type}
{set.side && `${t(set.side.toLowerCase() as any, lang)}`}
</p>
</div>
{/* Date & Time */}
<div className="grid grid-cols-2 gap-4">
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant font-bold flex gap-1 items-center mb-1">
<Calendar size={10} /> {t('date', lang) || 'Date'}
</label>
<input
type="date"
value={dateStr}
onChange={(e) => handleDateChange(e.target.value)}
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
/>
</div>
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant font-bold flex gap-1 items-center mb-1">
<Clock size={10} /> {t('time', lang) || 'Time'}
</label>
<input
type="time"
value={timeStr}
onChange={(e) => handleTimeChange(e.target.value)}
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
/>
</div>
</div>
{/* Metrics */}
<div className="grid grid-cols-2 gap-3">
{hasWeight && (
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center mb-1"><Dumbbell size={10} /> {t('weight_kg', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none"
value={set.weight === 0 ? '' : (set.weight ?? '')}
onChange={(e) => handleUpdate('weight', parseFloat(e.target.value))}
placeholder="0"
/>
</div>
)}
{hasReps && (
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center mb-1"><Activity size={10} /> {t('reps', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none"
value={set.reps === 0 ? '' : (set.reps ?? '')}
onChange={(e) => handleUpdate('reps', parseInt(e.target.value))}
placeholder="0"
/>
</div>
)}
{hasBwPercent && (
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center mb-1"><Percent size={10} /> {t('body_weight_percent', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none"
value={set.bodyWeightPercentage === 0 ? '' : (set.bodyWeightPercentage ?? 100)}
onChange={(e) => handleUpdate('bodyWeightPercentage', parseFloat(e.target.value))}
placeholder="100"
/>
</div>
)}
{hasTime && (
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center mb-1"><Timer size={10} /> {t('time_sec', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none"
value={set.durationSeconds === 0 ? '' : (set.durationSeconds ?? '')}
onChange={(e) => handleUpdate('durationSeconds', parseFloat(e.target.value))}
placeholder="0"
/>
</div>
)}
{hasDist && (
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center mb-1"><ArrowRight size={10} /> {t('dist_m', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none"
value={set.distanceMeters === 0 ? '' : (set.distanceMeters ?? '')}
onChange={(e) => handleUpdate('distanceMeters', parseFloat(e.target.value))}
placeholder="0"
/>
</div>
)}
{hasHeight && (
<div className="bg-surface-container-high rounded px-3 py-2">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center mb-1"><ArrowUp size={10} /> {t('height_cm', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-lg text-on-surface focus:outline-none"
value={set.height === 0 ? '' : (set.height ?? '')}
onChange={(e) => handleUpdate('height', parseFloat(e.target.value))}
placeholder="0"
/>
</div>
)}
</div>
<div className="flex justify-end pt-4 border-t border-outline-variant">
<Button onClick={handleSave}>
<Save size={16} className="mr-2" />
{t('save', lang)}
</Button>
</div>
</div>
</Modal>
);
};
export default EditSetModal;

View File

@@ -9,6 +9,7 @@ import { getExercises } from '../services/storage';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { Modal } from './ui/Modal';
import EditSetModal from './EditSetModal';
import FilledInput from './FilledInput';
interface HistoryProps {
@@ -25,6 +26,38 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
const [editingSetInfo, setEditingSetInfo] = useState<{ sessionId: string, set: WorkoutSet } | null>(null);
const handleSaveSingleSet = async (updatedSet: WorkoutSet) => {
if (!editingSetInfo) return;
const session = sessions.find(s => s.id === editingSetInfo.sessionId);
if (session) {
const updatedSets = session.sets.map(s => s.id === updatedSet.id ? updatedSet : s);
const updatedSession = { ...session, sets: updatedSets };
try {
await updateSession(updatedSession);
setEditingSetInfo(null);
} catch (e) {
console.error("Failed to update set", e);
}
}
};
const handleSaveSetFromModal = async (updatedSet: WorkoutSet) => {
if (!editingSetInfo) return;
// If editing within the session edit modal, update local state only
if (editingSession && editingSession.id === editingSetInfo.sessionId) {
const updatedSets = editingSession.sets.map(s =>
s.id === updatedSet.id ? updatedSet : s
);
setEditingSession({ ...editingSession, sets: updatedSets });
setEditingSetInfo(null);
} else {
// Otherwise save to backend (Quick Log flow)
await handleSaveSingleSet(updatedSet);
}
};
React.useEffect(() => {
if (!userId) return;
@@ -87,13 +120,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
}
};
const handleUpdateSet = (setId: string, field: keyof WorkoutSet, value: number | string) => {
if (!editingSession) return;
const updatedSets = editingSession.sets.map(s =>
s.id === setId ? { ...s, [field]: value } : s
);
setEditingSession({ ...editingSession, sets: updatedSets });
};
const handleDeleteSet = (setId: string) => {
if (!editingSession) return;
@@ -267,7 +294,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
// Find the session this set belongs to and open edit mode
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
if (parentSession) {
setEditingSession(JSON.parse(JSON.stringify(parentSession)));
setEditingSetInfo({ sessionId: parentSession.id, set: set });
}
}}
variant="ghost"
@@ -380,122 +407,37 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
<div className="space-y-3">
<h3 className="text-sm font-medium text-primary ml-1">{t('sets_count', lang)} ({editingSession.sets.length})</h3>
{editingSession.sets.map((set, idx) => (
<div key={set.id} className="bg-surface-container-low p-3 rounded-xl border border-outline-variant/20 flex flex-col gap-3">
<div className="flex justify-between items-center border-b border-outline-variant/50 pb-2">
<div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container text-xs font-bold flex items-center justify-center">{idx + 1}</span>
<span className="font-medium text-on-surface text-sm">{set.exerciseName}{set.side && <span className="ml-2 text-xs font-medium text-on-surface-variant">{t(set.side.toLowerCase() as any, lang)}</span>}</span>
<div key={set.id} className="bg-surface-container-low p-3 rounded-xl border border-outline-variant/20 flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="w-5 h-5 rounded-full bg-secondary-container text-on-secondary-container text-[10px] font-bold flex items-center justify-center">{idx + 1}</span>
<span className="font-medium text-on-surface text-sm">{set.exerciseName}</span>
{set.side && <span className="text-xs text-on-surface-variant font-medium bg-surface-container px-1.5 py-0.5 rounded">{t(set.side.toLowerCase() as any, lang)}</span>}
</div>
<div className="text-sm text-on-surface-variant pl-7">
{formatSetMetrics(set, lang)}
</div>
</div>
<div className="flex gap-1">
<Button
onClick={() => setEditingSetInfo({ sessionId: editingSession.id, set: set })}
variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-primary"
title={t('edit', lang)}
>
<Pencil size={18} />
</Button>
<Button
onClick={() => handleDeleteSet(set.id)}
variant="ghost"
size="icon"
className="h-8 w-8 text-on-surface-variant hover:text-error hover:bg-error-container/10"
className="text-on-surface-variant hover:text-error hover:bg-error-container/10"
title={t('delete', lang)}
>
<Trash2 size={16} />
<Trash2 size={18} />
</Button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{(set.type === ExerciseType.STRENGTH || set.type === ExerciseType.BODYWEIGHT || set.type === ExerciseType.STATIC) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Dumbbell size={10} /> {t('weight_kg', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.weight === 0 ? '' : (set.weight ?? '')}
onChange={(e) => handleUpdateSet(set.id, 'weight', parseFloat(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.STRENGTH || set.type === ExerciseType.BODYWEIGHT || set.type === ExerciseType.PLYOMETRIC) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Activity size={10} /> {t('reps', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.reps === 0 ? '' : (set.reps ?? '')}
onChange={(e) => handleUpdateSet(set.id, 'reps', parseInt(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.BODYWEIGHT || set.type === ExerciseType.STATIC) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Percent size={10} /> {t('body_weight_percent', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.bodyWeightPercentage === 0 ? '' : (set.bodyWeightPercentage ?? 100)}
onChange={(e) => handleUpdateSet(set.id, 'bodyWeightPercentage', parseFloat(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.CARDIO || set.type === ExerciseType.STATIC) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Timer size={10} /> {t('time_sec', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.durationSeconds === 0 ? '' : (set.durationSeconds ?? '')}
onChange={(e) => handleUpdateSet(set.id, 'durationSeconds', parseFloat(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.CARDIO || set.type === ExerciseType.LONG_JUMP) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><ArrowRight size={10} /> {t('dist_m', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.distanceMeters === 0 ? '' : (set.distanceMeters ?? '')}
onChange={(e) => handleUpdateSet(set.id, 'distanceMeters', parseFloat(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.HIGH_JUMP) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><ArrowUp size={10} /> {t('height_cm', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.height === 0 ? '' : (set.height ?? '')}
onChange={(e) => handleUpdateSet(set.id, 'height', parseFloat(e.target.value))}
/>
</div>
)}
</div>
{/* Side Selector - Full width on mobile, 1 col on desktop if space */}
{(() => {
const exDef = exercises.find(e => e.id === set.exerciseId);
const showSide = set.side || exDef?.isUnilateral;
if (!showSide) return null;
return (
<div className="bg-surface-container-high rounded px-2 py-1 col-span-2 sm:col-span-1 border border-outline-variant/30">
<label className="text-[10px] text-on-surface-variant font-bold block mb-1">{t('unilateral', lang)}</label>
<div className="flex bg-surface-container-low rounded p-0.5">
{(['LEFT', 'ALTERNATELY', 'RIGHT'] as const).map((sideOption) => {
const labelMap: Record<string, string> = { LEFT: 'L', RIGHT: 'R', ALTERNATELY: 'A' };
return (
<button
key={sideOption}
onClick={() => handleUpdateSet(set.id, 'side', sideOption)}
title={t(sideOption.toLowerCase() as any, lang)}
className={`flex-1 text-[10px] py-1 rounded transition-colors ${set.side === sideOption
? 'bg-primary/10 text-primary font-bold'
: 'text-on-surface-variant hover:bg-surface-container'
}`}
>
{labelMap[sideOption]}
</button>
);
})}
</div>
</div>
);
})()}
</div>
))}
</div>
@@ -510,6 +452,16 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</Modal>
)
}
{editingSetInfo && (
<EditSetModal
isOpen={!!editingSetInfo}
onClose={() => setEditingSetInfo(null)}
set={editingSetInfo.set}
exerciseDef={exercises.find(e => e.id === editingSetInfo.set.exerciseId)}
onSave={handleSaveSetFromModal}
lang={lang}
/>
)}
</div >
);
};

View File

@@ -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>
);

View File

@@ -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 */}

View File

@@ -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
};

View File

@@ -9,7 +9,9 @@ interface ApiSession extends Omit<WorkoutSession, 'startTime' | 'endTime' | 'set
exercise?: {
name: string;
type: ExerciseType;
}
};
exerciseName?: string;
type?: ExerciseType;
})[];
}
@@ -30,8 +32,8 @@ export const getSessions = async (userId: string): Promise<WorkoutSession[]> =>
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
sets: session.sets.map((set) => ({
...set,
exerciseName: set.exercise?.name || 'Unknown',
type: set.exercise?.type || ExerciseType.STRENGTH
exerciseName: set.exerciseName || set.exercise?.name || 'Unknown',
type: set.type || set.exercise?.type || ExerciseType.STRENGTH
})) as WorkoutSet[]
}));
} catch {
@@ -58,8 +60,8 @@ export const getActiveSession = async (userId: string): Promise<WorkoutSession |
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
sets: session.sets.map((set) => ({
...set,
exerciseName: set.exercise?.name || 'Unknown',
type: set.exercise?.type || ExerciseType.STRENGTH
exerciseName: set.exerciseName || set.exercise?.name || 'Unknown',
type: set.type || set.exercise?.type || ExerciseType.STRENGTH
})) as WorkoutSet[]
};
} catch {