docs: add phase 3 design polish planning, update progress
This commit is contained in:
@@ -91,3 +91,11 @@ None - no external service configuration required.
|
||||
---
|
||||
*Phase: 01-input-ux*
|
||||
*Completed: 2026-02-16*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: frontend/src/pages/WorkoutPage.jsx
|
||||
- FOUND: frontend/src/App.css
|
||||
- FOUND: .planning/phases/01-input-ux/01-02-SUMMARY.md
|
||||
- FOUND commit: 18ecf06 (Task 1 — stepper integration)
|
||||
- FOUND commit: cb6f41c (docs — summary + state)
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
---
|
||||
phase: 02-flexible-sets
|
||||
plan: "01"
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- frontend/src/pages/WorkoutPage.jsx
|
||||
- frontend/src/App.css
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every exercise card shows a 'Lägg till set' button"
|
||||
- "Tapping 'Lägg till set' opens a modal with two choices: Vanligt set and Dropset"
|
||||
- "Choosing Vanligt set appends one set row pre-filled with weight from the row above (same reps as row above)"
|
||||
- "Choosing Dropset appends 3 set rows: first at same weight as row above, second at ~75-80% of that, third at ~55-60% of that, each with 10 reps pre-filled"
|
||||
- "Every set row has an inline trash icon button that removes that row"
|
||||
- "Tapping delete on the last remaining set is blocked (button disabled or no-op)"
|
||||
- "Set numbers display correctly after adds and deletions (Set 1, Set 2, Set 3...)"
|
||||
artifacts:
|
||||
- path: "frontend/src/pages/WorkoutPage.jsx"
|
||||
provides: "ExerciseCard with dynamic set array, add-set modal, delete-set button, onDeleteSet prop (stub)"
|
||||
contains: "setList"
|
||||
- path: "frontend/src/App.css"
|
||||
provides: "Modal overlay CSS, add-set button CSS, delete-set button CSS"
|
||||
contains: ".set-type-modal"
|
||||
key_links:
|
||||
- from: "ExerciseCard setList state"
|
||||
to: "set rows rendered"
|
||||
via: "setList.map() instead of Array.from({ length: exercise.sets })"
|
||||
pattern: "setList\\.map"
|
||||
- from: "Trash icon button"
|
||||
to: "setList filter"
|
||||
via: "handleDeleteSet removes index from setList array"
|
||||
pattern: "handleDeleteSet"
|
||||
- from: "'Lägg till set' button"
|
||||
to: "modal open state"
|
||||
via: "setShowAddModal(true)"
|
||||
pattern: "showAddModal"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Refactor ExerciseCard to support a dynamic, variable-length set list. Add UI for adding sets (via modal with Vanligt set / Dropset choice) and deleting sets (inline trash icon, last-set guard).
|
||||
|
||||
Purpose: Core Phase 2 feature — flexible set count entirely in the frontend. The backend already persists individual sets via upsert; adding/deleting on the frontend just means the next save will include the correct set_number sequence.
|
||||
|
||||
Output: ExerciseCard with dynamic set array state, set-type chooser modal, delete-set button on each row.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/intense/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/intense/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02-flexible-sets/02-CONTEXT.md
|
||||
@.planning/phases/02-flexible-sets/02-RESEARCH.md
|
||||
@frontend/src/pages/WorkoutPage.jsx
|
||||
@frontend/src/App.css
|
||||
@frontend/src/components/Icons.jsx
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Refactor ExerciseCard to dynamic set array + add-set modal + delete-set button</name>
|
||||
<files>frontend/src/pages/WorkoutPage.jsx</files>
|
||||
<action>
|
||||
Replace the fixed-length set rendering in ExerciseCard with a dynamic `setList` array in state. Each element in the array is an object `{ weight, reps, completed }` (indexed by position, not by set_number — set_number is derived as index+1 when logging).
|
||||
|
||||
**State refactor (ExerciseCard):**
|
||||
|
||||
Replace:
|
||||
```js
|
||||
const [setInputs, setSetInputs] = useState({})
|
||||
```
|
||||
With:
|
||||
```js
|
||||
const [setList, setSetList] = useState([])
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
```
|
||||
|
||||
Update the `useEffect` that initializes state. Build `setList` as an ordered array instead of a keyed object:
|
||||
```js
|
||||
useEffect(() => {
|
||||
const initial = []
|
||||
for (let i = 1; i <= exercise.sets; i++) {
|
||||
const existingLog = logs.find(l => l.set_number === i)
|
||||
initial.push({
|
||||
weight: existingLog?.weight?.toString() || progression?.suggestedWeight?.toString() || '',
|
||||
reps: existingLog?.reps?.toString() || '',
|
||||
completed: existingLog?.completed || false
|
||||
})
|
||||
}
|
||||
setSetList(initial)
|
||||
}, [exercise, logs, progression])
|
||||
```
|
||||
|
||||
**handleInputChange** — update to use array index:
|
||||
```js
|
||||
const handleInputChange = (idx, field, value) => {
|
||||
setSetList(prev => prev.map((s, i) => i === idx ? { ...s, [field]: value } : s))
|
||||
}
|
||||
```
|
||||
|
||||
**handleComplete** — update to use array index, pass set_number as idx+1 to onLogSet:
|
||||
```js
|
||||
const handleComplete = (idx) => {
|
||||
const input = setList[idx]
|
||||
const newCompleted = !input.completed
|
||||
setSetList(prev => prev.map((s, i) => i === idx ? { ...s, completed: newCompleted } : s))
|
||||
onLogSet(exercise.id, idx + 1, input.weight, input.reps, newCompleted)
|
||||
}
|
||||
```
|
||||
|
||||
**handleAddNormal** — append one set pre-filled from the last row:
|
||||
```js
|
||||
const handleAddNormal = () => {
|
||||
const last = setList[setList.length - 1] || { weight: '', reps: '' }
|
||||
setSetList(prev => [...prev, { weight: last.weight, reps: last.reps, completed: false }])
|
||||
setShowAddModal(false)
|
||||
}
|
||||
```
|
||||
|
||||
**handleAddDropset** — append 3 sets with 20% weight reduction per step, 10 reps each:
|
||||
```js
|
||||
const handleAddDropset = () => {
|
||||
const last = setList[setList.length - 1] || { weight: '0', reps: '10' }
|
||||
const baseWeight = parseFloat(last.weight) || 0
|
||||
const drop1 = Math.round((baseWeight * 0.80) / 2.5) * 2.5
|
||||
const drop2 = Math.round((baseWeight * 0.60) / 2.5) * 2.5
|
||||
const newSets = [
|
||||
{ weight: last.weight, reps: '10', completed: false },
|
||||
{ weight: drop1.toString(), reps: '10', completed: false },
|
||||
{ weight: drop2.toString(), reps: '10', completed: false },
|
||||
]
|
||||
setSetList(prev => [...prev, ...newSets])
|
||||
setShowAddModal(false)
|
||||
}
|
||||
```
|
||||
Note: Dropset weight steps use research-confirmed 20% drops per step (80% then 60% of base), rounded to nearest 2.5kg per the app's progression convention.
|
||||
|
||||
**handleDeleteSet** — remove by index, guard against last set:
|
||||
```js
|
||||
const handleDeleteSet = (idx) => {
|
||||
if (setList.length <= 1) return // last-set guard: block deletion
|
||||
setSetList(prev => prev.filter((_, i) => i !== idx))
|
||||
if (onDeleteSet) onDeleteSet(exercise.id, idx + 1)
|
||||
}
|
||||
```
|
||||
|
||||
**completedSets count** — update to use setList:
|
||||
```js
|
||||
const completedSets = setList.filter(s => s.completed).length
|
||||
```
|
||||
|
||||
**ExerciseCard props** — add `onDeleteSet` prop (optional, will be wired in plan 02):
|
||||
```js
|
||||
function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSet, onDeleteSet }) {
|
||||
```
|
||||
|
||||
**Render update — set rows:**
|
||||
|
||||
Replace the `Array.from({ length: exercise.sets }, ...)` map with `setList.map((input, idx) => ...)`:
|
||||
```jsx
|
||||
{setList.map((input, idx) => (
|
||||
<div key={idx} className={`set-row ${input.completed ? 'completed' : ''}`}>
|
||||
<span className="set-number">Set {idx + 1}</span>
|
||||
<div className="set-inputs">
|
||||
<WeightInput
|
||||
value={input.weight}
|
||||
onChange={(val) => handleInputChange(idx, 'weight', val)}
|
||||
/>
|
||||
<span className="input-separator">×</span>
|
||||
<RepsInput
|
||||
value={input.reps}
|
||||
onChange={(val) => handleInputChange(idx, 'reps', val)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={`delete-set-btn ${setList.length <= 1 ? 'disabled' : ''}`}
|
||||
onClick={() => handleDeleteSet(idx)}
|
||||
disabled={setList.length <= 1}
|
||||
aria-label={`Ta bort set ${idx + 1}`}
|
||||
>
|
||||
<Icon name="trash" size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={`complete-btn ${input.completed ? 'done' : ''}`}
|
||||
onClick={() => handleComplete(idx)}
|
||||
>
|
||||
{input.completed ? <Icon name="check" size={18} /> : ''}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
```
|
||||
|
||||
**Render update — below sets list, add "Lägg till set" button and modal:**
|
||||
```jsx
|
||||
<button
|
||||
className="add-set-btn"
|
||||
onClick={() => setShowAddModal(true)}
|
||||
>
|
||||
+ Lägg till set
|
||||
</button>
|
||||
|
||||
{showAddModal && (
|
||||
<div className="set-type-modal-overlay" onClick={() => setShowAddModal(false)}>
|
||||
<div className="set-type-modal" onClick={e => e.stopPropagation()}>
|
||||
<h3>Välj settyp</h3>
|
||||
<button className="set-type-option" onClick={handleAddNormal}>
|
||||
<strong>Vanligt set</strong>
|
||||
<span>Lägg till ett set</span>
|
||||
</button>
|
||||
<button className="set-type-option dropset" onClick={handleAddDropset}>
|
||||
<strong>Dropset</strong>
|
||||
<span>3 set med viktnedtrappning (20% per steg)</span>
|
||||
</button>
|
||||
<button className="set-type-cancel" onClick={() => setShowAddModal(false)}>
|
||||
Avbryt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
Place the modal JSX inside `{expanded && (...)}` after the `.sets-list` div but before closing `.exercise-body`.
|
||||
|
||||
**Progress badge in exercise header** — update to use `setList.length` instead of `exercise.sets`:
|
||||
```jsx
|
||||
<span className={`progress-badge ${completedSets === setList.length ? 'complete' : ''}`}>
|
||||
{completedSets}/{setList.length}
|
||||
</span>
|
||||
```
|
||||
|
||||
Also update the `exercise-card` class condition:
|
||||
```jsx
|
||||
className={`exercise-card ${expanded ? 'expanded' : ''} ${completedSets === setList.length && setList.length > 0 ? 'all-done' : ''}`}
|
||||
```
|
||||
|
||||
Check Icons.jsx for a "trash" icon — if none exists, add it (an SVG trash can outline, consistent with the existing Icon component pattern). Check the file before assuming it exists.
|
||||
</action>
|
||||
<verify>
|
||||
Open frontend dev server (`npm run dev` in frontend/) and load a workout. Verify:
|
||||
1. Set rows render correctly with existing set count
|
||||
2. "Lägg till set" button is visible below set list
|
||||
3. Tapping it opens modal with two choices
|
||||
4. "Vanligt set" adds one row, weight pre-filled from row above
|
||||
5. "Dropset" adds 3 rows with progressively lower weights
|
||||
6. Trash icon appears on each row; clicking removes the row
|
||||
7. Trash icon on the only remaining set is disabled (cannot delete)
|
||||
8. Set numbers re-number correctly after deletion (Set 1, Set 2, ...)
|
||||
</verify>
|
||||
<done>
|
||||
ExerciseCard renders from a dynamic setList array. Add-set modal works for both Vanligt set and Dropset. Delete button removes rows with last-set guard enforced. Set numbering is always sequential from 1.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add CSS for modal overlay, add-set button, and delete-set button</name>
|
||||
<files>frontend/src/App.css</files>
|
||||
<action>
|
||||
Add the following CSS blocks to App.css. Append after the existing stepper CSS section.
|
||||
|
||||
**Add-set button** — sits below the sets-list, full width, secondary style:
|
||||
```css
|
||||
/* Add set button */
|
||||
.add-set-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.add-set-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
```
|
||||
|
||||
**Delete set button** — inline on the set row, between inputs and complete-btn:
|
||||
```css
|
||||
/* Delete set button */
|
||||
.delete-set-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
min-height: 44px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.delete-set-btn:hover:not(:disabled) {
|
||||
color: #e53e3e;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-set-btn:disabled,
|
||||
.delete-set-btn.disabled {
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
```
|
||||
|
||||
**Set type modal** — CSS overlay + card, dark theme consistent:
|
||||
```css
|
||||
/* Set type modal */
|
||||
.set-type-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.set-type-modal {
|
||||
background: var(--surface);
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 1.5rem 1rem 2rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.set-type-modal h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 0.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.set-type-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.2rem;
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--surface-2, rgba(255,255,255,0.05));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.set-type-option strong {
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.set-type-option span {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.set-type-option:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.set-type-option.dropset strong {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.set-type-cancel {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
```
|
||||
|
||||
Note: Use `var(--surface)`, `var(--border)`, `var(--text-primary)`, `var(--text-secondary)`, `var(--accent)` — all existing CSS custom properties from the dark theme. Verify property names against existing App.css before writing.
|
||||
</action>
|
||||
<verify>
|
||||
Check in browser that:
|
||||
1. "Lägg till set" button renders with dashed border, no background
|
||||
2. Trash icon on set rows is subtle (low opacity), turns red on hover
|
||||
3. Modal slides up from bottom as a sheet (bottom-anchored overlay)
|
||||
4. Modal has the two option cards and a cancel button
|
||||
5. All touch targets are at least 44px tall
|
||||
</verify>
|
||||
<done>
|
||||
All new interactive elements styled with dark theme variables. Modal is a bottom sheet at max-width 600px. Delete button is subtle but discoverable. All touch targets ≥ 44px.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Run `npm run build` in frontend/ — build must pass with no errors.
|
||||
|
||||
In the dev server, open a workout and test:
|
||||
- Add normal set: weight copies from row above, reps copy from row above, set number increments
|
||||
- Add dropset: 3 rows appear, weights drop at 20% intervals (rounded to 2.5kg), reps default to 10
|
||||
- Delete middle set: remaining rows renumber correctly
|
||||
- Delete when only 1 set remains: button disabled, no row removed
|
||||
- Modal dismisses on overlay click and on "Avbryt"
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
ExerciseCard renders from dynamic setList. Both add-set paths work (Vanligt set, Dropset). Delete works with last-set guard. Build passes. All new buttons meet 44px touch target. CSS uses only existing theme variables.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-flexible-sets/02-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,220 @@
|
||||
---
|
||||
phase: 02-flexible-sets
|
||||
plan: "02"
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["02-01"]
|
||||
files_modified:
|
||||
- backend/src/index.js
|
||||
- frontend/src/App.jsx
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Deleting a set row that was previously logged removes it from the database"
|
||||
- "Adding and logging sets beyond the original program count persists to the database"
|
||||
- "After saving/refreshing, the dynamic set count reflects what was logged (no phantom sets, no missing sets)"
|
||||
- "The backend DELETE endpoint returns 200 on success and 404 if the log row did not exist"
|
||||
artifacts:
|
||||
- path: "backend/src/index.js"
|
||||
provides: "DELETE /api/logs endpoint accepting user_id, program_exercise_id, date, set_number"
|
||||
contains: "DELETE.*workout_logs"
|
||||
- path: "frontend/src/App.jsx"
|
||||
provides: "deleteLog function that calls DELETE /api/logs; passed to WorkoutPage as onDeleteSet"
|
||||
contains: "deleteLog"
|
||||
key_links:
|
||||
- from: "ExerciseCard handleDeleteSet"
|
||||
to: "App.jsx deleteLog"
|
||||
via: "onDeleteSet prop through WorkoutPage"
|
||||
pattern: "onDeleteSet"
|
||||
- from: "App.jsx deleteLog"
|
||||
to: "DELETE /api/logs"
|
||||
via: "fetch with method DELETE"
|
||||
pattern: "method.*DELETE"
|
||||
- from: "DELETE /api/logs"
|
||||
to: "workout_logs table"
|
||||
via: "DELETE FROM workout_logs WHERE user_id=$1 AND program_exercise_id=$2 AND date=$3 AND set_number=$4"
|
||||
pattern: "DELETE FROM workout_logs"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add a DELETE /api/logs backend endpoint and wire it through App.jsx so that when a user removes a set row that was already logged, the database record is deleted.
|
||||
|
||||
Purpose: Without this, deleted set rows leave orphan rows in workout_logs, causing ghost sets to reappear on next load. The frontend dynamic state from plan 01 is correct per-session, but only persists with proper backend deletion.
|
||||
|
||||
Output: Backend DELETE endpoint + App.jsx deleteLog function + WorkoutPage onDeleteSet prop wired to ExerciseCard.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/intense/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/intense/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/02-flexible-sets/02-CONTEXT.md
|
||||
@.planning/phases/02-flexible-sets/02-01-SUMMARY.md
|
||||
@backend/src/index.js
|
||||
@frontend/src/App.jsx
|
||||
@frontend/src/pages/WorkoutPage.jsx
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add DELETE /api/logs endpoint to backend</name>
|
||||
<files>backend/src/index.js</files>
|
||||
<action>
|
||||
Add a DELETE endpoint that removes a specific set log row. Place it directly after the existing POST /api/logs route (around line 329).
|
||||
|
||||
```js
|
||||
// Delete a specific set log
|
||||
app.delete('/api/logs', async (req, res) => {
|
||||
try {
|
||||
const { user_id, program_exercise_id, date, set_number } = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
'DELETE FROM workout_logs WHERE user_id = $1 AND program_exercise_id = $2 AND date = $3 AND set_number = $4 RETURNING id',
|
||||
[user_id, program_exercise_id, date, set_number]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Log not found' });
|
||||
}
|
||||
|
||||
res.json({ deleted: result.rows[0].id });
|
||||
} catch (err) {
|
||||
console.error('Error deleting log:', err);
|
||||
res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
No auth middleware on this endpoint — consistent with the existing POST /api/logs which also has no auth middleware (user_id comes from the request body). Do not add authMiddleware unless the existing POST /api/logs has it (it does not).
|
||||
</action>
|
||||
<verify>
|
||||
Start backend (`npm start` in backend/) and run:
|
||||
```
|
||||
curl -X DELETE http://localhost:3001/api/logs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id":1,"program_exercise_id":1,"date":"2026-02-21","set_number":1}'
|
||||
```
|
||||
Returns `{"deleted": N}` if row existed, or `{"error":"Log not found"}` with 404 if not. Server does not crash on missing body fields (returns 404 or 500 gracefully).
|
||||
</verify>
|
||||
<done>
|
||||
DELETE /api/logs endpoint exists, deletes the matching workout_logs row by composite key (user_id, program_exercise_id, date, set_number), returns 200+id on success, 404 if not found.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire deleteLog through App.jsx and WorkoutPage to ExerciseCard</name>
|
||||
<files>frontend/src/App.jsx, frontend/src/pages/WorkoutPage.jsx</files>
|
||||
<action>
|
||||
**In App.jsx:**
|
||||
|
||||
Add a `deleteLog` function alongside the existing `logSet` function:
|
||||
|
||||
```js
|
||||
const deleteLog = async (programExerciseId, setNumber) => {
|
||||
try {
|
||||
await fetch(`${API_URL}/logs`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
program_exercise_id: programExerciseId,
|
||||
date: today,
|
||||
set_number: setNumber
|
||||
})
|
||||
})
|
||||
// Remove from local logs state
|
||||
setLogs(prev => ({
|
||||
...prev,
|
||||
[programExerciseId]: (prev[programExerciseId] || []).filter(l => l.set_number !== setNumber)
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('Failed to delete log:', err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pass `deleteLog` to `WorkoutPage` as `onDeleteSet`:
|
||||
```jsx
|
||||
<WorkoutPage
|
||||
day={selectedDay}
|
||||
week={currentWeek}
|
||||
logs={logs}
|
||||
onLogSet={logSet}
|
||||
onDeleteSet={deleteLog}
|
||||
onBack={() => setView('dashboard')}
|
||||
fetchProgression={fetchProgression}
|
||||
/>
|
||||
```
|
||||
|
||||
**In WorkoutPage.jsx:**
|
||||
|
||||
Update the `WorkoutPage` function signature to accept `onDeleteSet`:
|
||||
```js
|
||||
function WorkoutPage({ day, week, logs, onLogSet, onDeleteSet, onBack, fetchProgression }) {
|
||||
```
|
||||
|
||||
Pass `onDeleteSet` through to each `ExerciseCard`:
|
||||
```jsx
|
||||
<ExerciseCard
|
||||
key={exercise.id || idx}
|
||||
exercise={exercise}
|
||||
logs={logs[exercise.id] || []}
|
||||
progression={progressions[exercise.id]}
|
||||
expanded={expandedExercise === exercise.id}
|
||||
onToggle={() => setExpandedExercise(
|
||||
expandedExercise === exercise.id ? null : exercise.id
|
||||
)}
|
||||
onLogSet={onLogSet}
|
||||
onDeleteSet={onDeleteSet}
|
||||
/>
|
||||
```
|
||||
|
||||
The ExerciseCard already has `onDeleteSet` in its prop signature and calls it in `handleDeleteSet` from plan 01. No changes needed inside ExerciseCard itself — just confirm `onDeleteSet` is being passed through correctly.
|
||||
|
||||
**Behavior when delete is called:**
|
||||
- If the set being deleted has `completed: true` in setList (was logged to DB), `deleteLog` fires and removes the DB row
|
||||
- If the set is new and not yet logged (e.g. user added a set but didn't complete it), `onDeleteSet` is still called but the DELETE request will return 404 — the catch block silently ignores this (the row didn't exist; no harm done)
|
||||
|
||||
This means the `handleDeleteSet` in ExerciseCard should always call `onDeleteSet` regardless of whether the set was completed — the backend handles the "not found" case gracefully.
|
||||
</action>
|
||||
<verify>
|
||||
In the dev server:
|
||||
1. Start a workout, complete set 1 of an exercise (logs it to DB)
|
||||
2. Verify it's logged: `curl "http://localhost:3001/api/logs?user_id=1&program_exercise_id=1&date=TODAY"`
|
||||
3. Delete set 1 row using the trash icon
|
||||
4. Verify it's gone from DB: repeat the curl — set_number=1 should no longer appear
|
||||
5. Add a new set (Vanligt set), complete it — verify it appears in DB as the next set_number
|
||||
6. Reload the workout — no ghost sets, count matches what was logged
|
||||
</verify>
|
||||
<done>
|
||||
deleteLog in App.jsx calls DELETE /api/logs. WorkoutPage passes onDeleteSet to ExerciseCard. Deleting a completed set removes it from the database and from local logs state. Non-logged sets delete silently without error.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Run `npm run build` in frontend/ — must pass with no errors.
|
||||
|
||||
Full flow test:
|
||||
1. Open a workout
|
||||
2. Add 2 extra sets to the first exercise (Vanligt set)
|
||||
3. Complete all sets — verify they all persist in DB
|
||||
4. Delete the middle set — verify DB row removed, UI renumbers
|
||||
5. Save workout (navigate back to dashboard)
|
||||
6. Re-open same workout — set count matches what was logged, no ghost rows
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
DELETE /api/logs endpoint exists in backend. App.jsx has deleteLog function. WorkoutPage passes onDeleteSet to ExerciseCard. Deleting a logged set removes it from DB. Adding and completing new sets saves beyond original program set count. Frontend build passes.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-flexible-sets/02-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Phase 3: Design Polish & MVP
|
||||
|
||||
**Started:** 2026-02-26
|
||||
**Goal:** Enterprise-quality look while maintaining MVP functionality
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Current app looks like a "cheap template copy" - needs professional polish while keeping core functionality intact.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Polish, don't rebuild** - Improve visual quality without breaking working features
|
||||
- **Enterprise feel** - Clean, sophisticated, not template-like
|
||||
- **Subtle animations** - Smooth transitions, not flashy
|
||||
- **Consistent spacing** - Professional rhythm and breathing room
|
||||
- **Better typography** - More hierarchy contrast
|
||||
|
||||
## Phase Plans
|
||||
|
||||
### 03-01: Login/Onboarding Polish
|
||||
- Auth pages visual upgrade
|
||||
- Better branding presence
|
||||
- Smoother form interactions
|
||||
|
||||
### 03-02: Dashboard Polish
|
||||
- Header/brand refinement
|
||||
- Card improvements
|
||||
- Better visual hierarchy
|
||||
|
||||
### 03-03: Workout Experience Polish
|
||||
- Exercise cards refinement
|
||||
- Set logging UX
|
||||
- Progress indicators
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] App feels cohesive and professional
|
||||
- [ ] No "template" visual artifacts
|
||||
- [ ] Consistent spacing/sizing
|
||||
- [ ] Better typography hierarchy
|
||||
- [ ] Core flow (login → workout) works smoothly
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- New features (only visual polish)
|
||||
- Backend changes
|
||||
- Database migrations
|
||||
Reference in New Issue
Block a user