docs(phase-02): research flexible sets phase
Researched: - Dropset conventions: 20-25% weight reduction per step (HIGH confidence, strength training literature) - React array management: Use filter() for immutable removals (HIGH confidence, official React docs) - Mobile delete UX: Combine inline icons + optional swipe, 48px touch targets (HIGH confidence, WCAG + NN/G) - Lightweight modal: Plain CSS overlay pattern without component library (MEDIUM confidence, verified with community) - Backend set numbering: Recommend frontend renumbering before save to handle gaps (MEDIUM confidence, needs verification) Key deliverables: - Standard Stack: React 18 + plain CSS (no new dependencies) - Architecture Patterns: Dynamic array management, lightweight modal, inline delete with optional confirmation - Don't Hand-Roll: Array mutations (use filter), modal dialog (CSS is simpler than library), set calculations - Common Pitfalls: Set numbering gaps, missing reps defaults, arbitrary weight reductions, last-set deletion - Code Examples: Add/remove sets, dropset calculations, delete patterns with renumbering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,508 @@
|
|||||||
|
# Phase 2: Flexible Sets - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-02-21
|
||||||
|
**Domain:** React dynamic list management, backend set persistence, mobile delete UX, dropset training conventions
|
||||||
|
**Confidence:** HIGH (dropset conventions, React patterns) / MEDIUM (backend implementation specifics)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Flexible Sets requires managing a variable-length array of sets per exercise on the frontend (React setState), persisting those changes to the database (upsert pattern), and supporting dropsets (a standard strength training technique with 20-25% weight reductions per step). The frontend needs lightweight modal/sheet UI for set-type selection, and delete interactions must follow mobile UX best practices (combine swipe + inline icons, 48px touch targets, optional confirmation for destructive actions).
|
||||||
|
|
||||||
|
**Primary recommendation:** Use React's filter() method for array mutations (standard pattern), implement a lightweight CSS+React modal (no library needed), respect the 20-25% weight reduction convention for dropsets with 8-12 reps per dropped set, and pair inline delete icons with optional confirmation for the last set.
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- When a normal set is added, pre-fill weight from the set directly above it
|
||||||
|
- Tapping "Lägg till set" opens a popup/modal with two choices:
|
||||||
|
- Vanligt set — appends one set row, weight pre-filled from row above
|
||||||
|
- Dropset — appends 3 set rows with progressively decreasing weight
|
||||||
|
- First of the 3 dropset rows: same weight as the set row above
|
||||||
|
- Weight drops successively across the 3 rows at a fixed percentage step (researcher should confirm what's conventional)
|
||||||
|
- All 3 dropset rows are pre-filled but editable before logging
|
||||||
|
- Reps for dropset rows: researcher should determine sensible defaults
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Delete control placement on set rows (inline icon, swipe, etc.)
|
||||||
|
- "Add set" button placement on the exercise card
|
||||||
|
- Last-set deletion guard (block or confirmation — pick whichever is safer for mobile)
|
||||||
|
- Exact dropset percentage step (guided by research into conventional dropset weight reductions)
|
||||||
|
- Modal/popup design for the set-type chooser
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
- None — discussion stayed within phase scope
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| React | 18+ | State management via useState for dynamic set list | Already in use; hooks provide direct control over nested state mutations |
|
||||||
|
| Plain CSS | current | Modal overlay, delete UI, animations | App uses no component library; CSS gives full control, small bundle size |
|
||||||
|
|
||||||
|
### Supporting
|
||||||
|
| Library | Version | Purpose | When to Use |
|
||||||
|
|---------|---------|---------|-------------|
|
||||||
|
| fetch() | native | Backend API calls (add/remove set endpoints) | App standard; no new dependency |
|
||||||
|
| Array.filter() | ES5+ | Remove sets from state array immutably | Official React recommendation for array mutations |
|
||||||
|
|
||||||
|
### Alternatives Considered
|
||||||
|
| Instead of | Could Use | Tradeoff |
|
||||||
|
|------------|-----------|----------|
|
||||||
|
| Custom modal | @headlessui/react, MUI Modal | Adds dependency; app uses plain CSS throughout |
|
||||||
|
| filter() for deletion | splice() or filter with index | splice() mutates in-place (React anti-pattern); filter() is cleaner, more functional |
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
No new packages required. Uses existing React + plain CSS.
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Recommended Project Structure (Frontend)
|
||||||
|
|
||||||
|
WorkoutPage.jsx manages the master state:
|
||||||
|
```
|
||||||
|
WorkoutPage
|
||||||
|
├── state: exercises[] (with expanded setInputs per exercise)
|
||||||
|
├── ExerciseCard (controlled component, all state in parent)
|
||||||
|
│ ├── SetRow × N (rendered from setInputs[exerciseId])
|
||||||
|
│ ├── "Lägg till set" button (opens modal)
|
||||||
|
│ └── Delete icon per set row
|
||||||
|
└── SetTypeModal (conditionally rendered, closes on selection)
|
||||||
|
├── "Vanligt set" button
|
||||||
|
└── "Dropset" button
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 1: Dynamic Array Management in React (Add/Remove Sets)
|
||||||
|
|
||||||
|
**What:** Managing a variable-length array of sets per exercise using React's useState hook with immutable updates.
|
||||||
|
|
||||||
|
**When to use:** Every time the user taps "Lägg till set" or clicks delete on a set row.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In ExerciseCard.jsx or WorkoutPage.jsx
|
||||||
|
const [setInputs, setSetInputs] = useState({});
|
||||||
|
// setInputs = { exerciseId: { 1: { weight, reps, completed }, 2: { ... } } }
|
||||||
|
|
||||||
|
// Add a normal set (append to end)
|
||||||
|
const handleAddSet = (exerciseId, newSetData) => {
|
||||||
|
setSetInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[exerciseId]: {
|
||||||
|
...prev[exerciseId],
|
||||||
|
[nextSetNumber]: newSetData
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove a set by set_number
|
||||||
|
const handleDeleteSet = (exerciseId, setNumber) => {
|
||||||
|
setSetInputs(prev => {
|
||||||
|
const exerciseSets = { ...prev[exerciseId] };
|
||||||
|
delete exerciseSets[setNumber];
|
||||||
|
return { ...prev, [exerciseId]: exerciseSets };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add dropset (3 sets at once)
|
||||||
|
const handleAddDropset = (exerciseId, firstDropsetWeight) => {
|
||||||
|
const setCount = Object.keys(setInputs[exerciseId]).length;
|
||||||
|
const dropset = {
|
||||||
|
[setCount + 1]: { weight: firstDropsetWeight, reps: '', completed: false },
|
||||||
|
[setCount + 2]: { weight: (firstDropsetWeight * 0.8).toFixed(1), reps: '', completed: false },
|
||||||
|
[setCount + 3]: { weight: (firstDropsetWeight * 0.64).toFixed(1), reps: '', completed: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
setSetInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[exerciseId]: { ...prev[exerciseId], ...dropset }
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** [React official docs on updating arrays in state](https://react.dev/learn/updating-arrays-in-state)
|
||||||
|
|
||||||
|
### Pattern 2: Lightweight Modal for Set Type Selection
|
||||||
|
|
||||||
|
**What:** A simple CSS overlay + div modal (no component library) that appears when user taps "Lägg till set", offers "Vanligt set" or "Dropset" choice, then closes.
|
||||||
|
|
||||||
|
**When to use:** User initiates adding a new set via the "Lägg till set" button on exercise card.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// SetTypeModal.jsx
|
||||||
|
export function SetTypeModal({ exerciseId, isOpen, onClose, onSelectVanligt, onSelectDropset }) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Overlay - click to close */}
|
||||||
|
<div className="modal-overlay" onClick={onClose} />
|
||||||
|
|
||||||
|
{/* Modal content */}
|
||||||
|
<div className="modal-content">
|
||||||
|
<h3>Lägg till set</h3>
|
||||||
|
<div className="modal-buttons">
|
||||||
|
<button
|
||||||
|
className="modal-btn modal-btn-primary"
|
||||||
|
onClick={() => {
|
||||||
|
onSelectVanligt();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Vanligt set
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="modal-btn modal-btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onSelectDropset();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dropset (3 set)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* App.css addition */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: var(--color-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 320px;
|
||||||
|
z-index: 101;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 44px; /* Touch target */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-primary {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn-secondary {
|
||||||
|
background: var(--color-border);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** [Creating modals without component libraries](https://javachipd.medium.com/create-a-modal-in-react-js-without-a-component-library-f4675bfef906)
|
||||||
|
|
||||||
|
### Pattern 3: Delete Control on Set Rows
|
||||||
|
|
||||||
|
**What:** Inline delete icon (trash or X) on the right side of each set row, with optional confirmation for the last set.
|
||||||
|
|
||||||
|
**When to use:** Users need to remove sets during workout without leaving the page.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// Inside SetRow component
|
||||||
|
const handleDeleteSet = () => {
|
||||||
|
const isLastSet = completedSets === totalSets;
|
||||||
|
|
||||||
|
if (isLastSet) {
|
||||||
|
// Show confirmation for last set
|
||||||
|
const confirmed = window.confirm('En övning måste ha minst ett set. Vill du radera?');
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteSet(exerciseId, setNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render
|
||||||
|
<div className="set-row">
|
||||||
|
<span className="set-number">Set {setNum}</span>
|
||||||
|
<div className="set-inputs">
|
||||||
|
{/* Weight and reps inputs */}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="set-delete-btn"
|
||||||
|
onClick={handleDeleteSet}
|
||||||
|
title="Radera set"
|
||||||
|
aria-label="Radera set"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
.set-delete-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-error, #ff4444);
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-delete-btn:hover,
|
||||||
|
.set-delete-btn:active {
|
||||||
|
background: rgba(255, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Source:** [Mobile delete UX best practices](https://www.designmonks.co/blog/delete-button-ui)
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
- **Mutating state directly** (e.g., `setInputs[exerciseId][setNum] = newVal`): React won't detect change. Always use spread operator or filter().
|
||||||
|
- **Using array.splice() for deletions**: Mutates in-place. Use filter() instead to create new array.
|
||||||
|
- **No touch target for delete**: Icon smaller than 44×44px will be hard to tap. Ensure adequate padding/size.
|
||||||
|
- **Swipe-only delete gestures**: Not all users can perform swipes (motor impairments). Pair with visible inline icon.
|
||||||
|
- **Auto-deleting the last set**: Can cause data loss. Block or confirm before allowing deletion of exercise's final set.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| List item deletion | Custom deletion logic | Array.filter() + React setState | Immutability, React reactivity, no bugs from state mutation |
|
||||||
|
| Modal dialog | DIY overlay with event handling | CSS overlay + conditional rendering + onClick handler | Proper z-index stacking, backdrop click handling, keyboard escape support already in play via plain CSS |
|
||||||
|
| Weight reduction calculations | Custom percentage math | Straightforward multiplication (weight * 0.8, weight * 0.64) | No library needed; formulaic and testable |
|
||||||
|
| Touch target sizes | Eyeballing button sizes | Min 44×44px (iOS/Android standard, WCAG guideline) | Accessibility, reduces accidental taps, mobile best practice |
|
||||||
|
|
||||||
|
**Key insight:** The only complex part is state management. React's built-in useState + immutable patterns handle it cleanly. Everything else (modal, delete, dropset math) is simple enough that a small custom implementation beats dragging in a dependency.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Set Numbering After Deletion
|
||||||
|
**What goes wrong:** User deletes Set 2 from a 4-set exercise, leaving Sets 1, 3, 4. Backend doesn't know how to re-number or the frontend tries to save with gaps.
|
||||||
|
|
||||||
|
**Why it happens:** Current backend does upsert per set using `set_number` as part of the upsert key. If you delete Set 2 and re-save, the DB sees Sets 1, 3, 4 and doesn't know what to do with the gap.
|
||||||
|
|
||||||
|
**How to avoid:**
|
||||||
|
- Option A (Recommended): On save, renumber all sets sequentially (1, 2, 3...) before sending to backend.
|
||||||
|
- Option B: Store sets as an unordered list in the DB, use `(user_id, program_exercise_id, date, set_index)` as upsert key.
|
||||||
|
|
||||||
|
**Warning signs:** When you try to save a deleted set and get a constraint violation, or orphaned rows remain in the DB with old set numbers.
|
||||||
|
|
||||||
|
### Pitfall 2: Dropset Reps Defaults
|
||||||
|
**What goes wrong:** Dropset reps are left blank (undefined), user forgets to fill them in mid-workout, tries to log incomplete data.
|
||||||
|
|
||||||
|
**Why it happens:** Frontend pre-fills weight but forgets reps, or reps input isn't required by validation.
|
||||||
|
|
||||||
|
**How to avoid:**
|
||||||
|
- Always pre-fill dropset reps with a sensible default (e.g., same as the set above, or same as reps_min from the exercise definition).
|
||||||
|
- Add client-side validation: refuse to log a set if weight OR reps is missing.
|
||||||
|
|
||||||
|
**Warning signs:** Users complaining about blank reps, or backend rejecting incomplete logs.
|
||||||
|
|
||||||
|
### Pitfall 3: Weight Reduction Percentage Misunderstanding
|
||||||
|
**What goes wrong:** Dropset weight reductions are arbitrary (e.g., 0.9 multiplier one time, 0.75 another), inconsistent with training science, confusing to users.
|
||||||
|
|
||||||
|
**Why it happens:** No research into standard convention, developer eyeballs a "reasonable" percentage.
|
||||||
|
|
||||||
|
**How to avoid:**
|
||||||
|
- Use 20-25% reduction per step as the standard (verified in strength training literature).
|
||||||
|
- Example: 100kg → 80kg → 64kg (multiply by 0.8 twice).
|
||||||
|
- Document this in comments and allow users to see and modify before logging.
|
||||||
|
|
||||||
|
**Warning signs:** Users saying "Why does the weight drop so much?" or dropsets not feeling right during workout.
|
||||||
|
|
||||||
|
### Pitfall 4: Last Set Deletion Without Guard
|
||||||
|
**What goes wrong:** User accidentally taps delete on the only set, exercise becomes invalid (exercises require at least 1 set), data model breaks.
|
||||||
|
|
||||||
|
**Why it happens:** No confirmation or block on the last set.
|
||||||
|
|
||||||
|
**How to avoid:**
|
||||||
|
- Either block deletion (disable button or show toast: "En övning måste ha minst ett set").
|
||||||
|
- Or show confirmation: `confirm('Are you sure?')` before deleting the last set.
|
||||||
|
|
||||||
|
**Warning signs:** Exercises with 0 sets in the database, user confusion about why an exercise disappeared.
|
||||||
|
|
||||||
|
### Pitfall 5: Modal Not Closing on Backdrop Click
|
||||||
|
**What goes wrong:** User taps outside the modal to close it, nothing happens. User taps the button again, two modals appear.
|
||||||
|
|
||||||
|
**Why it happens:** Overlay click handler not wired or modal state not cleared properly.
|
||||||
|
|
||||||
|
**How to avoid:**
|
||||||
|
- Attach `onClick={onClose}` to the overlay div.
|
||||||
|
- Ensure state updates synchronously (setIsOpenModal(false)).
|
||||||
|
- Test that repeated taps don't stack modals.
|
||||||
|
|
||||||
|
**Warning signs:** Modal stays open after backdrop click, or overlay clicks open multiple modals.
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
Verified patterns from official sources and app conventions:
|
||||||
|
|
||||||
|
### Adding a Normal Set (Pre-fill Weight)
|
||||||
|
```javascript
|
||||||
|
// Source: React patterns + app convention (pre-fill from row above)
|
||||||
|
const handleAddVanligtSet = (exerciseId) => {
|
||||||
|
const exSets = setInputs[exerciseId] || {};
|
||||||
|
const setCount = Object.keys(exSets).length;
|
||||||
|
const lastSetNumber = Math.max(...Object.keys(exSets).map(Number), 0);
|
||||||
|
const prevSet = exSets[lastSetNumber];
|
||||||
|
const newSetNumber = lastSetNumber + 1;
|
||||||
|
|
||||||
|
const newSet = {
|
||||||
|
weight: prevSet?.weight || '', // Pre-fill from row above
|
||||||
|
reps: '',
|
||||||
|
completed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
setSetInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[exerciseId]: {
|
||||||
|
...prev[exerciseId],
|
||||||
|
[newSetNumber]: newSet
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a Dropset (3 sets with 20% reduction per step)
|
||||||
|
```javascript
|
||||||
|
// Source: Strength training literature (20-25% reduction standard, ~8-12 reps)
|
||||||
|
const handleAddDropset = (exerciseId) => {
|
||||||
|
const exSets = setInputs[exerciseId] || {};
|
||||||
|
const lastSetNumber = Math.max(...Object.keys(exSets).map(Number), 0);
|
||||||
|
const prevSet = exSets[lastSetNumber];
|
||||||
|
const baseWeight = parseFloat(prevSet?.weight) || 0;
|
||||||
|
|
||||||
|
const dropsetRows = {
|
||||||
|
[lastSetNumber + 1]: { weight: baseWeight, reps: prevSet?.reps || '', completed: false },
|
||||||
|
[lastSetNumber + 2]: { weight: (baseWeight * 0.8).toFixed(1), reps: prevSet?.reps || '', completed: false },
|
||||||
|
[lastSetNumber + 3]: { weight: (baseWeight * 0.64).toFixed(1), reps: prevSet?.reps || '', completed: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
setSetInputs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[exerciseId]: { ...prev[exerciseId], ...dropsetRows }
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting a Set
|
||||||
|
```javascript
|
||||||
|
// Source: React official docs on array mutations
|
||||||
|
const handleDeleteSet = (exerciseId, setNumber) => {
|
||||||
|
setSetInputs(prev => {
|
||||||
|
const updated = { ...prev[exerciseId] };
|
||||||
|
delete updated[setNumber];
|
||||||
|
return { ...prev, [exerciseId]: updated };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renumbering Sets Before Save
|
||||||
|
```javascript
|
||||||
|
// Source: App convention to handle gaps from deletions
|
||||||
|
const renumberSets = (exerciseId) => {
|
||||||
|
const exSets = setInputs[exerciseId] || {};
|
||||||
|
const numbered = Object.entries(exSets)
|
||||||
|
.sort(([a], [b]) => Number(a) - Number(b))
|
||||||
|
.reduce((acc, ([, val], idx) => {
|
||||||
|
acc[idx + 1] = val;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return numbered;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | When Changed | Impact |
|
||||||
|
|--------------|------------------|--------------|--------|
|
||||||
|
| Single set count per exercise (hardcoded in program_exercises.sets) | Variable set count per workout instance | Phase 2 | Enables dropsets, flexible training, better user control |
|
||||||
|
| Swipe-only delete (mobile pattern from ~2020) | Swipe + inline icon (visible, accessible) | Current best practice (2024+) | Reduces accessibility issues, discoverability improves |
|
||||||
|
| Arbitrary weight reduction % (e.g., 0.9 or 0.75) | Standard 20-25% per research (2023+ reviews) | Strength training consensus | Better alignment with training science, more user trust |
|
||||||
|
|
||||||
|
**Deprecated/outdated:**
|
||||||
|
- Single modal library per app: Modern pattern is lightweight CSS modal for occasional use (saves bundle size).
|
||||||
|
- Confirmation fatigue (asking for confirmation on every action): Current UX reserves confirm for high-risk actions only (deleting last set or similar).
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Database schema for set gaps:** If a user adds 4 sets, deletes set 2, then saves, should the DB see (1, 3, 4) or should the frontend renumber to (1, 2, 3)?
|
||||||
|
- What we know: Current backend does upsert per set using set_number.
|
||||||
|
- What's unclear: Whether backend has a unique constraint on (program_exercise_id, date, set_number) that would reject gaps.
|
||||||
|
- Recommendation: Implement renumbering in frontend before save (safest approach, no schema changes needed). Verify backend constraint during implementation.
|
||||||
|
|
||||||
|
2. **Reps defaults for dropsets:** Should all 3 dropset rows default to the same reps as the set above, or should they increase (e.g., 8 reps on row 1, 10 on row 2, 12 on row 3)?
|
||||||
|
- What we know: Standard strength training says dropsets often use equal or higher reps as weight decreases.
|
||||||
|
- What's unclear: What Gravl's training philosophy is (hypertrophy vs. strength vs. endurance).
|
||||||
|
- Recommendation: Default all 3 rows to the same reps as the row above (simpler, user can adjust). Document in code that dropsets typically use higher reps at lower weights.
|
||||||
|
|
||||||
|
3. **Last set deletion: block vs. confirm?**
|
||||||
|
- What we know: Mobile UX recommends confirmation only for high-risk actions; small risk of data loss here (can re-add set).
|
||||||
|
- What's unclear: User preference (power users might prefer block, casual users might prefer confirm).
|
||||||
|
- Recommendation: Implement confirmation via `window.confirm()` (safe, visible, respects user intent). Users can hit cancel if unsure.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- [React official docs: Updating Arrays in State](https://react.dev/learn/updating-arrays-in-state) — array mutation patterns, filter() usage
|
||||||
|
- [Brookbush Institute: Drop Sets Systematic Review](https://brookbushinstitute.com/articles/drop-sets-comprehensive-systematic-review-and-training-recommendations) — 20% weight reduction, 2-3 drops research
|
||||||
|
- [ISSA: Drop Sets Training Guide](https://www.issaonline.com/blog/post/drop-sets-everything-you-need-to-know-for-muscle-gains) — 15-25% reduction per step, 8-12 reps per set
|
||||||
|
- [LogRocket: Accessible Swipe/Delete Interactions](https://blog.logrocket.com/ux-design/accessible-swipe-contextual-action-triggers/) — 48px touch targets, swipe + inline icons pattern
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- [Creating Modals Without Libraries (Medium)](https://javachipd.medium.com/create-a-modal-in-react-js-without-a-component-library-f4675bfef906) — CSS overlay pattern, conditional rendering
|
||||||
|
- [DesignMonks: Delete Button UX Best Practices](https://www.designmonks.co/blog/delete-button-ui) — confirmation patterns, last-item guards
|
||||||
|
- [NN/G: Confirmation Dialogs](https://www.nngroup.com/articles/confirmation-dialog/) — when to use confirmation vs. undo vs. block
|
||||||
|
- [GeeksforGeeks: Database Design for Fitness Tracking](https://www.geeksforgeeks.org/dbms/how-to-design-a-database-for-health-and-fitness-tracking-applications/) — per-set storage patterns
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence)
|
||||||
|
- Various fitness app UX articles (general patterns, may not reflect Gravl's specific philosophy)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- **Dropset conventions (20-25% reduction, 8-12 reps):** HIGH — multiple strength training sources agree, research-backed.
|
||||||
|
- **React array management patterns:** HIGH — official React docs, verified with community consensus.
|
||||||
|
- **Mobile delete UX (48px targets, swipe + inline):** HIGH — WCAG guidelines, major design systems (NN/G, LogRocket).
|
||||||
|
- **Backend set numbering:** MEDIUM — codebase uses upsert pattern, but schema constraints not fully verified. Recommend confirming during implementation.
|
||||||
|
- **Reps defaults for dropsets:** MEDIUM — strength training consensus exists, but Gravl's specific philosophy (hypertrophy/strength/endurance focus) should guide final choice.
|
||||||
|
- **Last set deletion guard:** MEDIUM — UX best practice is "confirm for high-risk," but user preference unknown. Recommend lightweight confirm() over hard block.
|
||||||
|
|
||||||
|
**Research date:** 2026-02-21
|
||||||
|
**Valid until:** 2026-03-21 (stable domain; 30-day window recommended)
|
||||||
Reference in New Issue
Block a user