Skip to content

Commit e02786c

Browse files
author
bndct-devops
committed
fix: workout view scroll, exercise order, sticky rest timer, mobile search UX
- Natural page scroll (minHeight: 100dvh) replaces broken flex overflow-y - Header + rest timer wrapped in sticky block so both pin to top - Auto-select last-worked exercise on re-open (restores log button) - Workout detail sets ordered by log time, not exercise_id - Exercise picker search input moved to bottom (above keyboard on mobile) - orderedExIds used in ActiveWorkoutView completion summary too
1 parent 105e2dd commit e02786c

2 files changed

Lines changed: 51 additions & 30 deletions

File tree

frontend/src/ActiveWorkoutView.jsx

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
4545
const trimmed = draftName.trim()
4646
if (trimmed && trimmed !== workout.name) onRename?.(workout.id, trimmed)
4747
}
48-
const [selectedExId, setSelectedExId] = React.useState(null)
48+
// Auto-select the last-worked exercise so the log row is ready on re-open
49+
const [selectedExId, setSelectedExId] = React.useState(() => {
50+
if (sessionSets.length > 0) return sessionSets[sessionSets.length - 1].exercise_id
51+
return null
52+
})
4953
const [showExPicker, setShowExPicker] = React.useState(false)
5054
const [exSearch, setExSearch] = React.useState('')
5155
const [reps, setReps] = React.useState('')
@@ -58,6 +62,7 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
5862
const [restRunning, setRestRunning] = React.useState(false)
5963
const restEndRef = React.useRef(null)
6064
const swipeTouchRef = React.useRef(null)
65+
const exSearchInputRef = React.useRef(null)
6166
const [logPulseActive, setLogPulseActive] = React.useState(false)
6267
const [prFlashExId, setPrFlashExId] = React.useState(null)
6368

@@ -239,6 +244,14 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
239244
}
240245
}, [_activeSessionCount])
241246

247+
// Focus search input when exercise picker opens (autoFocus is unreliable on iOS Safari)
248+
React.useEffect(() => {
249+
if (showExPicker) {
250+
const t = setTimeout(() => exSearchInputRef.current?.focus(), 80)
251+
return () => clearTimeout(t)
252+
}
253+
}, [showExPicker])
254+
242255
async function handleLogSet() {
243256
if (!selectedExId || (!reps && !weight)) return
244257
const weightKg = weight ? parseWeight(weight, unit) : null
@@ -282,7 +295,7 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
282295

283296
return (
284297
<div className={[liquidGlass ? 'liquid-glass' : '', animationsEnabled ? '' : 'no-anim'].filter(Boolean).join(' ') || undefined}
285-
style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', background: 'var(--bg)' }}
298+
style={{ minHeight: '100dvh', background: 'var(--bg)' }}
286299
onTouchStart={e => { swipeTouchRef.current = { x: e.touches[0].clientX, y: e.touches[0].clientY } }}
287300
onTouchEnd={e => {
288301
if (!swipeTouchRef.current) return
@@ -291,7 +304,8 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
291304
swipeTouchRef.current = null
292305
if (dx > 80 && Math.abs(dx) > Math.abs(dy) * 1.5) onExit?.()
293306
}}>
294-
<div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg)', borderBottom: '1px solid var(--border)', padding: 'calc(14px + env(safe-area-inset-top)) 16px 12px', display: 'flex', alignItems: 'flex-start', gap: 12 }}>
307+
<div style={{ position: 'sticky', top: 0, zIndex: 10 }}>
308+
<div style={{ background: 'var(--bg)', borderBottom: '1px solid var(--border)', padding: 'calc(14px + env(safe-area-inset-top)) 16px 12px', display: 'flex', alignItems: 'flex-start', gap: 12 }}>
295309
<div style={{ flex: 1, minWidth: 0 }}>
296310
{editingName ? (
297311
<input
@@ -323,22 +337,7 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
323337
style={{ background: sessionSets.length === 0 ? 'color-mix(in srgb, var(--danger) 12%, transparent)' : 'var(--bg-secondary)', border: `1px solid ${sessionSets.length === 0 ? 'color-mix(in srgb, var(--danger) 35%, transparent)' : 'var(--border)'}`, color: sessionSets.length === 0 ? 'var(--danger)' : 'var(--text)', borderRadius: 8, padding: 0, width: 36, height: 36, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><IconX size={16} /></button>
324338
</div>
325339

326-
{/* Cancel confirmation modal */}
327-
{cancelConfirm && (
328-
<div className="glass-overlay" style={{ position: 'fixed', inset: 0, zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
329-
<div className="card glass-panel" style={{ width: '100%', maxWidth: 360 }}>
330-
<p className="section-heading" style={{ marginTop: 0 }}>Cancel workout?</p>
331-
<p className="muted" style={{ marginBottom: 20 }}>This will delete the workout and all logged sets. This cannot be undone.</p>
332-
<div className="row">
333-
<button onClick={() => setCancelConfirm(false)} style={{ flex: 1 }}>Keep going</button>
334-
<button onClick={() => { setCancelConfirm(false); onCancel(workout.id) }}
335-
style={{ flex: 1, background: 'var(--danger)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 0', cursor: 'pointer', fontWeight: 600 }}>Yes, cancel</button>
336-
</div>
337-
</div>
338-
</div>
339-
)}
340-
341-
{/* Rest timer */}
340+
{/* Rest timer — sticky with header */}
342341
{restLeft !== null && (
343342
<div style={{ background: 'var(--accent)', color: '#fff', padding: '18px 16px 14px', textAlign: 'center' }}>
344343
<div style={{ fontSize: '0.75rem', fontWeight: 700, letterSpacing: '0.12em', opacity: 0.85, marginBottom: 4, textTransform: 'uppercase' }}>Rest</div>
@@ -374,9 +373,25 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
374373
</div>
375374
</div>
376375
)}
376+
</div>{/* end sticky wrapper */}
377377

378-
{/* Main scroll area */}
379-
<div style={{ flex: 1, overflowY: 'auto', padding: '16px 16px 32px', display: 'flex', flexDirection: 'column', gap: 12 }}>
378+
{/* Cancel confirmation modal */}
379+
{cancelConfirm && (
380+
<div className="glass-overlay" style={{ position: 'fixed', inset: 0, zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
381+
<div className="card glass-panel" style={{ width: '100%', maxWidth: 360 }}>
382+
<p className="section-heading" style={{ marginTop: 0 }}>Cancel workout?</p>
383+
<p className="muted" style={{ marginBottom: 20 }}>This will delete the workout and all logged sets. This cannot be undone.</p>
384+
<div className="row">
385+
<button onClick={() => setCancelConfirm(false)} style={{ flex: 1 }}>Keep going</button>
386+
<button onClick={() => { setCancelConfirm(false); onCancel(workout.id) }}
387+
style={{ flex: 1, background: 'var(--danger)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 0', cursor: 'pointer', fontWeight: 600 }}>Yes, cancel</button>
388+
</div>
389+
</div>
390+
</div>
391+
)}
392+
393+
{/* Main content */}
394+
<div style={{ padding: '16px 16px 32px', display: 'flex', flexDirection: 'column', gap: 12 }}>
380395

381396
{workout.status === 'in_progress' && (
382397
<>
@@ -604,7 +619,8 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
604619
<div style={{ fontWeight: 700, fontSize: '1.1rem', marginBottom: 4 }}>Workout complete</div>
605620
<div style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{setCount} set{setCount !== 1 ? 's' : ''} · {workoutTimer}</div>
606621
</div>
607-
{Object.entries(setsByExercise).map(([exId, sets]) => {
622+
{orderedExIds.map(exId => {
623+
const sets = setsByExercise[exId] || []
608624
const ex = exercises.find(e => e.id == exId)
609625
return (
610626
<div key={exId} style={{ marginBottom: 16 }}>
@@ -702,7 +718,7 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
702718
<div style={{ position: 'fixed', inset: 0, zIndex: 100, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}
703719
onClick={() => { setShowExPicker(false); setExSearch('') }}>
704720
<div className="glass-overlay" style={{ position: 'absolute', inset: 0 }} />
705-
<div className="glass-panel" style={{ position: 'relative', borderRadius: '20px 20px 0 0', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}
721+
<div className="glass-panel" style={{ position: 'relative', borderRadius: '20px 20px 0 0', maxHeight: '85dvh', display: 'flex', flexDirection: 'column' }}
706722
onClick={e => e.stopPropagation()}>
707723
<div style={{ textAlign: 'center', padding: '10px 0 0' }}>
708724
<div style={{ width: 36, height: 4, borderRadius: 2, background: 'var(--border)', display: 'inline-block' }} />
@@ -712,11 +728,7 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
712728
<button onClick={() => { setShowExPicker(false); setExSearch('') }}
713729
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--muted)', padding: 4, display: 'flex', alignItems: 'center' }}><IconX size={18} /></button>
714730
</div>
715-
<div style={{ padding: '0 16px 8px' }}>
716-
<input autoFocus placeholder="Search exercises…" value={exSearch}
717-
onChange={e => setExSearch(e.target.value)} style={{ margin: 0 }} />
718-
</div>
719-
<div style={{ flex: 1, overflowY: 'auto', padding: '0 16px 24px' }}>
731+
<div style={{ flex: 1, overflowY: 'auto', padding: '0 16px 8px' }}>
720732
{groupKeys.length === 0 && <div className="muted small" style={{ padding: '16px 0' }}>No exercises found</div>}
721733
{groupKeys.map(group => (
722734
<div key={group}>
@@ -733,6 +745,11 @@ export default function ActiveWorkoutView({ workout, exercises, sessionSets, onF
733745
</div>
734746
))}
735747
</div>
748+
{/* Search input at bottom so it sits just above the keyboard */}
749+
<div style={{ padding: '8px 16px calc(8px + env(safe-area-inset-bottom))', borderTop: '1px solid var(--border)' }}>
750+
<input ref={exSearchInputRef} placeholder="Search exercises…" value={exSearch}
751+
onChange={e => setExSearch(e.target.value)} style={{ margin: 0 }} />
752+
</div>
736753
</div>
737754
</div>
738755
)}

frontend/src/App.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,10 @@ function WorkoutDetailSheet({ workout, detail, exercises, unit, onClose, onDelet
17951795
const byEx = sets.reduce((acc, s) => {
17961796
acc[s.exercise_id] = acc[s.exercise_id] || []; acc[s.exercise_id].push(s); return acc
17971797
}, {})
1798+
// Preserve the order exercises were first logged (Object.entries on numeric keys sorts by id, not log order)
1799+
const exerciseOrder = []
1800+
const _seenEx = new Set()
1801+
sets.forEach(s => { if (!_seenEx.has(s.exercise_id)) { _seenEx.add(s.exercise_id); exerciseOrder.push(s.exercise_id) } })
17981802

17991803
// Stats
18001804
const totalSets = sets.length
@@ -1897,9 +1901,9 @@ function WorkoutDetailSheet({ workout, detail, exercises, unit, onClose, onDelet
18971901

18981902
{/* Sets body */}
18991903
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 18px 32px' }}>
1900-
{Object.entries(byEx).map(([exId, exSets]) => {
1904+
{exerciseOrder.map(exId => {
1905+
const exSets = byEx[exId] || []
19011906
const ex = exercises.find(e => e.id == exId)
1902-
const partIdx = BODY_PART_ORDER.indexOf(ex?.body_part)
19031907
const color = COLORS[muscleEntries.findIndex(([p]) => p === (ex?.body_part || 'Other')) % COLORS.length]
19041908
return (
19051909
<div key={exId} style={{ marginBottom: 20 }}>

0 commit comments

Comments
 (0)