Side attribute editable for Unilateral exercises
This commit is contained in:
@@ -4,6 +4,8 @@ import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
|
||||
import { t } from '../services/i18n';
|
||||
import { formatSetMetrics } from '../utils/setFormatting';
|
||||
import { useSession } from '../context/SessionContext';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { getExercises } from '../services/storage';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Modal } from './ui/Modal';
|
||||
@@ -15,11 +17,20 @@ interface HistoryProps {
|
||||
|
||||
const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
const { sessions, updateSession, deleteSession } = useSession();
|
||||
const { currentUser } = useAuth();
|
||||
const userId = currentUser?.id || '';
|
||||
const [exercises, setExercises] = useState<import('../types').ExerciseDef[]>([]);
|
||||
|
||||
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
|
||||
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!userId) return;
|
||||
getExercises(userId).then(exs => setExercises(exs));
|
||||
}, [userId]);
|
||||
|
||||
|
||||
const calculateSessionWork = (session: WorkoutSession) => {
|
||||
const bw = session.userBodyWeight || 70;
|
||||
@@ -455,25 +466,36 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
)}
|
||||
</div>
|
||||
{/* Side Selector - Full width on mobile, 1 col on desktop if space */}
|
||||
{set.side && (
|
||||
<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', 'RIGHT', 'ALTERNATELY'] as const).map((sideOption) => (
|
||||
<button
|
||||
key={sideOption}
|
||||
onClick={() => handleUpdateSet(set.id, 'side', sideOption)}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{t(sideOption.toLowerCase() as any, lang).slice(0, 3)}
|
||||
</button>
|
||||
))}
|
||||
{(() => {
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,8 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
||||
setEditDistance,
|
||||
editHeight,
|
||||
setEditHeight,
|
||||
editSide,
|
||||
setEditSide,
|
||||
handleCancelEdit,
|
||||
handleSaveEdit,
|
||||
handleEditSet,
|
||||
@@ -243,6 +245,39 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
||||
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>
|
||||
) : (
|
||||
|
||||
@@ -101,24 +101,27 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
|
||||
<div className="flex items-center gap-2 bg-surface-container rounded-full p-1">
|
||||
<button
|
||||
onClick={() => setUnilateralSide('LEFT')}
|
||||
title={t('left', lang)}
|
||||
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'LEFT' ? 'bg-primary-container text-on-primary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
{t('left', lang)}
|
||||
L
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUnilateralSide('ALTERNATELY')}
|
||||
title={t('alternately', lang)}
|
||||
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'ALTERNATELY' ? 'bg-tertiary-container text-on-tertiary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
{t('alternately', lang) || 'Alternately'}
|
||||
A
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUnilateralSide('RIGHT')}
|
||||
title={t('right', lang)}
|
||||
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'RIGHT' ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
|
||||
}`}
|
||||
>
|
||||
{t('right', lang)}
|
||||
R
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -143,6 +143,39 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
|
||||
</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>
|
||||
|
||||
@@ -226,6 +226,7 @@ export const useTracker = (props: any) => { // Props ignored/removed
|
||||
editDuration: form.editDuration, setEditDuration: form.setEditDuration,
|
||||
editDistance: form.editDistance, setEditDistance: form.setEditDistance,
|
||||
editHeight: form.editHeight, setEditHeight: form.setEditHeight,
|
||||
editSide: form.editSide, setEditSide: form.setEditSide,
|
||||
|
||||
isSporadicMode, setIsSporadicMode,
|
||||
sporadicSuccess,
|
||||
|
||||
Reference in New Issue
Block a user