From 35fa946ad7347a444b406abd60fb35731625527f Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:56:39 +0100 Subject: [PATCH 01/10] feat(db): add migration #4 for exercise notes column Add notes TEXT column to exercises table via ALTER TABLE. --- src/db/schema.test.ts | 37 ++++++++++++++++++++++++++++++++++++- src/db/schema.ts | 6 ++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/db/schema.test.ts b/src/db/schema.test.ts index e0df192..e8a66f5 100644 --- a/src/db/schema.test.ts +++ b/src/db/schema.test.ts @@ -196,6 +196,41 @@ describe('Migration #3', () => { const row = await db.getFirstAsync<{ version: number }>( 'SELECT MAX(version) as version FROM schema_version', ); - expect(row!.version).toBe(3); + expect(row!.version).toBeGreaterThanOrEqual(3); + }); +}); + +describe('Migration #4', () => { + let db: SQLiteDatabase; + + beforeEach(async () => { + db = await createTestDatabase(); + }); + + it('should add notes column to exercises', async () => { + await db.runAsync( + "INSERT INTO exercises (name, type, muscle_group, notes) VALUES ('Test', 'weights', 'chest', 'Keep elbows in')", + ); + const row = await db.getFirstAsync<{ notes: string }>( + "SELECT notes FROM exercises WHERE name = 'Test'", + ); + expect(row!.notes).toBe('Keep elbows in'); + }); + + it('should default notes to null', async () => { + await db.runAsync( + "INSERT INTO exercises (name, type, muscle_group) VALUES ('NoNotes', 'weights', 'legs')", + ); + const row = await db.getFirstAsync<{ notes: string | null }>( + "SELECT notes FROM exercises WHERE name = 'NoNotes'", + ); + expect(row!.notes).toBeNull(); + }); + + it('should record schema version 4', async () => { + const row = await db.getFirstAsync<{ version: number }>( + 'SELECT MAX(version) as version FROM schema_version', + ); + expect(row!.version).toBe(4); }); }); diff --git a/src/db/schema.ts b/src/db/schema.ts index 66d82f0..cef4f0a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -135,6 +135,12 @@ const MIGRATIONS: Migration[] = [ ); `, }, + { + version: 4, + up: ` + ALTER TABLE exercises ADD COLUMN notes TEXT; + `, + }, ]; export async function runMigrations(db: SQLiteDatabase): Promise { From 684c4ec3f7135fefa0eff5c7a0ea6b1e4407c726 Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:56:48 +0100 Subject: [PATCH 02/10] feat(exercise): add notes field to Exercise type and repository Include notes in Exercise interface, ExerciseExport, CreateExerciseData, UpdateExerciseData. Update ExerciseRepository create, update, getAll, getById. Add 7 tests covering create, update, clear, and backward compatibility. --- src/repositories/exercise.repo.test.ts | 99 ++++++++++++++++++++++++++ src/repositories/exercise.repo.ts | 13 +++- src/types/index.ts | 2 + 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/repositories/exercise.repo.test.ts b/src/repositories/exercise.repo.test.ts index 2baf3e4..22e3846 100644 --- a/src/repositories/exercise.repo.test.ts +++ b/src/repositories/exercise.repo.test.ts @@ -382,6 +382,105 @@ describe('ExerciseRepository', () => { expect(sets).toHaveLength(0); }); + // --- Notes tests --- + + it('should create an exercise with notes', async () => { + const exercise = await repo.create({ + name: 'Bench Press', + type: 'weights', + muscleGroup: 'chest', + notes: 'Keep elbows at 45 degrees', + }); + + expect(exercise.notes).toBe('Keep elbows at 45 degrees'); + }); + + it('should default notes to null when not provided', async () => { + const exercise = await repo.create({ + name: 'Squat', + type: 'weights', + muscleGroup: 'legs', + }); + + expect(exercise.notes).toBeNull(); + }); + + it('should return notes in getById', async () => { + const created = await repo.create({ + name: 'Deadlift', + type: 'weights', + muscleGroup: 'back', + notes: 'Hinge at hips, neutral spine', + }); + + const exercise = await repo.getById(created.id); + + expect(exercise!.notes).toBe('Hinge at hips, neutral spine'); + }); + + it('should return notes in getAll', async () => { + await repo.create({ + name: 'Bench Press', + type: 'weights', + muscleGroup: 'chest', + notes: 'Retract scapula', + }); + await repo.create({ + name: 'Squat', + type: 'weights', + muscleGroup: 'legs', + }); + + const exercises = await repo.getAll(); + const bench = exercises.find((e) => e.name === 'Bench Press')!; + const squat = exercises.find((e) => e.name === 'Squat')!; + + expect(bench.notes).toBe('Retract scapula'); + expect(squat.notes).toBeNull(); + }); + + it('should update notes', async () => { + const created = await repo.create({ + name: 'OHP', + type: 'weights', + muscleGroup: 'shoulders', + }); + + await repo.update(created.id, { notes: 'Brace core, press overhead' }); + + const updated = await repo.getById(created.id); + expect(updated!.notes).toBe('Brace core, press overhead'); + }); + + it('should clear notes by setting to null', async () => { + const created = await repo.create({ + name: 'OHP', + type: 'weights', + muscleGroup: 'shoulders', + notes: 'Some notes', + }); + + await repo.update(created.id, { notes: null }); + + const updated = await repo.getById(created.id); + expect(updated!.notes).toBeNull(); + }); + + it('should not affect notes when updating other fields', async () => { + const created = await repo.create({ + name: 'Press', + type: 'weights', + muscleGroup: 'chest', + notes: 'Keep tight', + }); + + await repo.update(created.id, { name: 'Bench Press' }); + + const updated = await repo.getById(created.id); + expect(updated!.name).toBe('Bench Press'); + expect(updated!.notes).toBe('Keep tight'); + }); + it('should return empty muscleGroups array as single primary group fallback', async () => { // Insert directly into DB without pivot data to test fallback await db.runAsync( diff --git a/src/repositories/exercise.repo.ts b/src/repositories/exercise.repo.ts index 9a1d4ba..08f99c4 100644 --- a/src/repositories/exercise.repo.ts +++ b/src/repositories/exercise.repo.ts @@ -9,6 +9,7 @@ export interface CreateExerciseData { illustration?: string | null; restSeconds?: number; isPredefined?: boolean; + notes?: string | null; } export interface UpdateExerciseData { @@ -18,6 +19,7 @@ export interface UpdateExerciseData { muscleGroups?: MuscleGroup[]; illustration?: string | null; restSeconds?: number; + notes?: string | null; } interface ExerciseRow { @@ -28,6 +30,7 @@ interface ExerciseRow { is_predefined: number; illustration: string | null; rest_seconds: number; + notes: string | null; created_at: string; } @@ -47,6 +50,7 @@ function rowToExercise(row: ExerciseRow, muscleGroups?: MuscleGroup[]): Exercise isPredefined: row.is_predefined === 1, illustration: row.illustration, restSeconds: row.rest_seconds, + notes: row.notes, createdAt: row.created_at, }; } @@ -59,14 +63,15 @@ export class ExerciseRepository { await this.db.withTransactionAsync(async () => { const result = await this.db.runAsync( - `INSERT INTO exercises (name, type, muscle_group, illustration, rest_seconds, is_predefined) - VALUES (?, ?, ?, ?, ?, ?)`, + `INSERT INTO exercises (name, type, muscle_group, illustration, rest_seconds, is_predefined, notes) + VALUES (?, ?, ?, ?, ?, ?, ?)`, data.name, data.type, data.muscleGroup, data.illustration ?? null, data.restSeconds ?? 90, data.isPredefined ? 1 : 0, + data.notes ?? null, ); const exerciseId = result.lastInsertRowId; @@ -186,6 +191,10 @@ export class ExerciseRepository { fields.push('rest_seconds = ?'); values.push(data.restSeconds); } + if (data.notes !== undefined) { + fields.push('notes = ?'); + values.push(data.notes); + } if (fields.length > 0) { values.push(id); diff --git a/src/types/index.ts b/src/types/index.ts index 850a190..c281e45 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -13,6 +13,7 @@ export interface Exercise { isPredefined: boolean; illustration: string | null; restSeconds: number; + notes: string | null; createdAt: string; } @@ -149,6 +150,7 @@ export interface ExerciseExport { muscleGroups?: string[]; illustration: string | null; restSeconds: number; + notes?: string | null; createdAt: string; } From 9ace21091a8fa154be61a8401566baa80e689ff2 Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:56:56 +0100 Subject: [PATCH 03/10] feat(backup): include exercise notes in export and import --- src/repositories/backup.repo.test.ts | 1 + src/repositories/backup.repo.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/repositories/backup.repo.test.ts b/src/repositories/backup.repo.test.ts index ebb7744..ea0e4f7 100644 --- a/src/repositories/backup.repo.test.ts +++ b/src/repositories/backup.repo.test.ts @@ -388,6 +388,7 @@ describe('BackupRepository', () => { 'back', null, 120, + null, '2026-01-01T00:00:00.000Z', ); expect(db.getFirstAsync).toHaveBeenCalledWith( diff --git a/src/repositories/backup.repo.ts b/src/repositories/backup.repo.ts index c75d0ee..f3940e0 100644 --- a/src/repositories/backup.repo.ts +++ b/src/repositories/backup.repo.ts @@ -14,6 +14,7 @@ interface ExerciseRow { muscle_group: string; illustration: string | null; rest_seconds: number; + notes: string | null; created_at: string; } @@ -70,7 +71,7 @@ export class BackupRepository { async exportData(): Promise { // Exercises const exerciseRows = await this.db.getAllAsync( - 'SELECT id, name, type, muscle_group, illustration, rest_seconds, created_at FROM exercises ORDER BY id ASC', + 'SELECT id, name, type, muscle_group, illustration, rest_seconds, notes, created_at FROM exercises ORDER BY id ASC', ); // Muscle groups from pivot table const muscleGroupRows = await this.db.getAllAsync( @@ -91,6 +92,7 @@ export class BackupRepository { muscleGroups: groupsByExercise.get(row.id) ?? [row.muscle_group], illustration: row.illustration, restSeconds: row.rest_seconds, + notes: row.notes, createdAt: row.created_at, })); @@ -182,13 +184,14 @@ export class BackupRepository { // After each upsert, resolve the real local ID. for (const ex of backup.exercises) { await this.db.runAsync( - `INSERT OR IGNORE INTO exercises (name, type, muscle_group, illustration, rest_seconds, created_at) - VALUES (?, ?, ?, ?, ?, ?)`, + `INSERT OR IGNORE INTO exercises (name, type, muscle_group, illustration, rest_seconds, notes, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, ex.name, ex.type, ex.muscleGroup, ex.illustration ?? null, ex.restSeconds, + ex.notes ?? null, ex.createdAt, ); From fbf85fd12fb48746e2f4ee40c4ba252cd3ff2276 Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:57:16 +0100 Subject: [PATCH 04/10] feat(repo): include exercise notes in routine and workout queries --- src/repositories/routine.repo.ts | 5 +++++ src/repositories/workout.repo.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/repositories/routine.repo.ts b/src/repositories/routine.repo.ts index 9b6e443..ccc39a1 100644 --- a/src/repositories/routine.repo.ts +++ b/src/repositories/routine.repo.ts @@ -27,6 +27,7 @@ interface RoutineExerciseJoinRow { e_is_predefined: number; e_illustration: string | null; e_rest_seconds: number; + e_notes: string | null; e_created_at: string; } @@ -87,6 +88,7 @@ export class RoutineRepository { e.is_predefined as e_is_predefined, e.illustration as e_illustration, e.rest_seconds as e_rest_seconds, + e.notes as e_notes, e.created_at as e_created_at FROM routine_exercises re JOIN exercises e ON e.id = re.exercise_id @@ -117,6 +119,7 @@ export class RoutineRepository { isPredefined: row.e_is_predefined === 1, illustration: row.e_illustration, restSeconds: row.e_rest_seconds, + notes: row.e_notes, createdAt: row.e_created_at, }, })), @@ -216,6 +219,7 @@ export class RoutineRepository { e.is_predefined as e_is_predefined, e.illustration as e_illustration, e.rest_seconds as e_rest_seconds, + e.notes as e_notes, e.created_at as e_created_at FROM routine_exercises re JOIN exercises e ON e.id = re.exercise_id @@ -242,6 +246,7 @@ export class RoutineRepository { isPredefined: row.e_is_predefined === 1, illustration: row.e_illustration, restSeconds: row.e_rest_seconds, + notes: row.e_notes, createdAt: row.e_created_at, }, }); diff --git a/src/repositories/workout.repo.ts b/src/repositories/workout.repo.ts index b4889d4..b4fee74 100644 --- a/src/repositories/workout.repo.ts +++ b/src/repositories/workout.repo.ts @@ -34,6 +34,7 @@ interface WorkoutSetJoinRow extends WorkoutSetRow { e_is_predefined: number; e_illustration: string | null; e_rest_seconds: number; + e_notes: string | null; e_created_at: string; } @@ -248,6 +249,7 @@ export class WorkoutRepository { e.is_predefined as e_is_predefined, e.illustration as e_illustration, e.rest_seconds as e_rest_seconds, + e.notes as e_notes, e.created_at as e_created_at FROM workout_sets ws JOIN exercises e ON e.id = ws.exercise_id @@ -270,6 +272,7 @@ export class WorkoutRepository { isPredefined: row.e_is_predefined === 1, illustration: row.e_illustration, restSeconds: row.e_rest_seconds, + notes: row.e_notes, createdAt: row.e_created_at, }, sets: [], From fd8beb8dd7642136078552b6f6998db0467a4d0d Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:57:25 +0100 Subject: [PATCH 05/10] feat(i18n): add exercise notes translation keys (EN/ES) --- src/i18n/en.ts | 5 +++++ src/i18n/es.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/i18n/en.ts b/src/i18n/en.ts index d048098..52aec2d 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -223,6 +223,11 @@ export const en = { 'exercise.restTimeEdit': 'Edit rest time', 'exercise.restTimeSave': 'Save', + // Exercise — notes + 'exercise.notes': 'Notes', + 'exercise.notesPlaceholder': 'Form tips, cues, personal notes...', + 'exercise.addNotes': 'Add notes', + // Body — charts 'body.chartWeight': 'Weight', 'body.chartBodyFat': 'Body Fat', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 4246a5f..751fd1f 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -231,6 +231,11 @@ export const es: Record = { 'exercise.restTimeEdit': 'Editar tiempo de descanso', 'exercise.restTimeSave': 'Guardar', + // Exercise — notes + 'exercise.notes': 'Notas', + 'exercise.notesPlaceholder': 'Tips de forma, cues, notas personales...', + 'exercise.addNotes': 'A\u00F1adir notas', + // Body — charts 'body.chartWeight': 'Peso', 'body.chartBodyFat': 'Grasa Corporal', From c3505a107321dd6cfccec291a5bb7a194a458a15 Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:57:30 +0100 Subject: [PATCH 06/10] feat(hooks): add notes to RoutineExerciseItem in useRoutineForm --- src/hooks/useRoutineForm.test.ts | 2 ++ src/hooks/useRoutineForm.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/hooks/useRoutineForm.test.ts b/src/hooks/useRoutineForm.test.ts index 7e9b0a0..68ac76b 100644 --- a/src/hooks/useRoutineForm.test.ts +++ b/src/hooks/useRoutineForm.test.ts @@ -12,6 +12,7 @@ function makeExercise(overrides: Partial = {}): Exercise { isPredefined: true, illustration: 'bench-press', restSeconds: 90, + notes: null, createdAt: '2026-01-01T00:00:00', ...overrides, }; @@ -23,6 +24,7 @@ function makeExerciseItem(overrides: Partial = {}): Routine name: 'Bench Press', illustration: 'bench-press', muscleGroup: 'chest', + notes: null, ...overrides, }; } diff --git a/src/hooks/useRoutineForm.ts b/src/hooks/useRoutineForm.ts index bdb39e1..3ea6631 100644 --- a/src/hooks/useRoutineForm.ts +++ b/src/hooks/useRoutineForm.ts @@ -6,6 +6,7 @@ export interface RoutineExerciseItem { name: string; illustration: string | null; muscleGroup: string; + notes: string | null; } interface FormErrors { @@ -29,6 +30,7 @@ export function useRoutineForm( name: exercise.name, illustration: exercise.illustration, muscleGroup: exercise.muscleGroup, + notes: exercise.notes, }, ]); setErrors((prev) => ({ ...prev, exercises: undefined })); From 2ee3024243f18e1338549f03935e17da23509349 Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:57:43 +0100 Subject: [PATCH 07/10] feat(ui): add notes field to exercise create and edit screens Multiline TextInput with 500 char limit. Optional field, saved as null when empty. --- app/exercise/create.tsx | 17 ++++++++++++++++- app/exercise/edit/[id].tsx | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/exercise/create.tsx b/app/exercise/create.tsx index ae59363..027ee94 100644 --- a/app/exercise/create.tsx +++ b/app/exercise/create.tsx @@ -66,6 +66,7 @@ export default function CreateExerciseScreen() { const [name, setName] = useState(''); const [type, setType] = useState(''); + const [notes, setNotes] = useState(''); const [selectedMuscleGroups, setSelectedMuscleGroups] = useState([]); const [muscleGroupModalVisible, setMuscleGroupModalVisible] = useState(false); const [errors, setErrors] = useState({}); @@ -102,6 +103,7 @@ export default function CreateExerciseScreen() { muscleGroup: groups[0], muscleGroups: groups, illustration: null, + notes: notes.trim() || null, }); router.back(); } catch (error) { @@ -109,7 +111,7 @@ export default function CreateExerciseScreen() { Alert.alert('Error', 'Could not save exercise. Please try again.'); setSaving(false); } - }, [name, type, selectedMuscleGroups, router]); + }, [name, type, notes, selectedMuscleGroups, router]); return ( @@ -371,6 +373,19 @@ export default function CreateExerciseScreen() { + + {/* Notes */} + {/* Save button */} diff --git a/app/exercise/edit/[id].tsx b/app/exercise/edit/[id].tsx index 4e42100..022375e 100644 --- a/app/exercise/edit/[id].tsx +++ b/app/exercise/edit/[id].tsx @@ -71,6 +71,7 @@ export default function EditExerciseScreen() { const [notFound, setNotFound] = useState(false); const [name, setName] = useState(''); const [type, setType] = useState(''); + const [notes, setNotes] = useState(''); const [selectedMuscleGroups, setSelectedMuscleGroups] = useState([]); const [muscleGroupModalVisible, setMuscleGroupModalVisible] = useState(false); const [errors, setErrors] = useState({}); @@ -88,6 +89,7 @@ export default function EditExerciseScreen() { } setName(exercise.name); setType(exercise.type); + setNotes(exercise.notes ?? ''); setSelectedMuscleGroups(exercise.muscleGroups); } catch (error) { console.error('Failed to load exercise:', error); @@ -128,6 +130,7 @@ export default function EditExerciseScreen() { type: type as ExerciseType, muscleGroup: groups[0], muscleGroups: groups, + notes: notes.trim() || null, }); router.back(); } catch (error) { @@ -135,7 +138,7 @@ export default function EditExerciseScreen() { Alert.alert(t('common.error'), t('exercise.edit.saveError')); setSaving(false); } - }, [name, type, selectedMuscleGroups, exerciseId, router, t]); + }, [name, type, notes, selectedMuscleGroups, exerciseId, router, t]); if (loading) { return ( @@ -419,6 +422,19 @@ export default function EditExerciseScreen() { + + {/* Notes */} + {/* Save button */} From 814c3422396ea386365ae732a431483b1fccee9e Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:57:52 +0100 Subject: [PATCH 08/10] feat(ui): show exercise notes section in detail screen Display notes in a tappable card when present, or an "Add notes" link that navigates to the edit screen when absent. Uses StickyNote icon. --- app/exercise/[id].tsx | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/app/exercise/[id].tsx b/app/exercise/[id].tsx index fd77ebb..6612fcb 100644 --- a/app/exercise/[id].tsx +++ b/app/exercise/[id].tsx @@ -21,6 +21,7 @@ import { Trash2, Check, X, + StickyNote, } from 'lucide-react-native'; import { colors } from '@/constants/theme'; import { getDatabase } from '@/db/connection'; @@ -454,6 +455,78 @@ export default function ExerciseDetailScreen() { + {/* Notes section */} + {exercise.notes ? ( + router.push(`/exercise/edit/${exerciseId}`)} + accessibilityRole="button" + accessibilityLabel={t('exercise.notes')} + > + {({ pressed }) => ( + + + + + {t('exercise.notes')} + + + + {exercise.notes} + + + )} + + ) : !exercise.isPredefined ? ( + router.push(`/exercise/edit/${exerciseId}`)} + accessibilityRole="button" + accessibilityLabel={t('exercise.addNotes')} + > + {({ pressed }) => ( + + + + {t('exercise.addNotes')} + + + )} + + ) : null} + {/* Stats section */} {statsLoading ? ( From 99a96f38d68f1c52817b1e86ceafb5056a4b05aa Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 13:57:59 +0100 Subject: [PATCH 09/10] feat(ui): show notes indicator in routine exercise list Display StickyNote icon next to exercises that have notes. Tapping the icon expands an inline panel showing the notes text. --- app/routine/[id].tsx | 1 + src/components/RoutineExerciseList.tsx | 212 ++++++++++++++++--------- 2 files changed, 134 insertions(+), 79 deletions(-) diff --git a/app/routine/[id].tsx b/app/routine/[id].tsx index 4aa9505..45abaf5 100644 --- a/app/routine/[id].tsx +++ b/app/routine/[id].tsx @@ -28,6 +28,7 @@ function routineToExerciseItems(routine: RoutineWithExercises): RoutineExerciseI name: re.exercise.name, illustration: re.exercise.illustration, muscleGroup: re.exercise.muscleGroup, + notes: re.exercise.notes, })); } diff --git a/src/components/RoutineExerciseList.tsx b/src/components/RoutineExerciseList.tsx index 9303c9f..cbbb711 100644 --- a/src/components/RoutineExerciseList.tsx +++ b/src/components/RoutineExerciseList.tsx @@ -1,5 +1,6 @@ +import { useState } from 'react'; import { View, Text, Pressable } from 'react-native'; -import { ChevronUp, ChevronDown, Trash2 } from 'lucide-react-native'; +import { ChevronUp, ChevronDown, Trash2, StickyNote } from 'lucide-react-native'; import { colors } from '@/constants/theme'; import { ExerciseIllustration } from './ExerciseIllustration'; import type { RoutineExerciseItem } from '@/hooks/useRoutineForm'; @@ -32,87 +33,134 @@ function RoutineExerciseRow({ onRemove: (index: number) => void; onMove: (index: number, direction: 'up' | 'down') => void; }) { + const [notesExpanded, setNotesExpanded] = useState(false); + const hasNotes = Boolean(exercise.notes); + return ( - {/* Order number */} - - {index + 1} - - - {/* Illustration */} - - - {/* Name + muscle group */} - - - {exercise.name} - + {/* Order number */} - {formatMuscleGroup(exercise.muscleGroup)} + {index + 1} - - {/* Reorder buttons */} - - onMove(index, 'up')} - disabled={isFirst} - accessibilityRole="button" - accessibilityLabel={`Move ${exercise.name} up`} - > - {({ pressed }) => ( - + + {/* Name + muscle group */} + + + - - - )} - + {exercise.name} + + {hasNotes && ( + setNotesExpanded((prev) => !prev)} + accessibilityRole="button" + accessibilityLabel={`${notesExpanded ? 'Hide' : 'Show'} notes for ${exercise.name}`} + > + {({ pressed }) => ( + + + + )} + + )} + + + {formatMuscleGroup(exercise.muscleGroup)} + + + + {/* Reorder buttons */} + + onMove(index, 'up')} + disabled={isFirst} + accessibilityRole="button" + accessibilityLabel={`Move ${exercise.name} up`} + > + {({ pressed }) => ( + + + + )} + + onMove(index, 'down')} + disabled={isLast} + accessibilityRole="button" + accessibilityLabel={`Move ${exercise.name} down`} + > + {({ pressed }) => ( + + + + )} + + + + {/* Remove button */} onMove(index, 'down')} - disabled={isLast} + onPress={() => onRemove(index)} accessibilityRole="button" - accessibilityLabel={`Move ${exercise.name} down`} + accessibilityLabel={`Remove ${exercise.name}`} > {({ pressed }) => ( - + )} - {/* Remove button */} - onRemove(index)} - accessibilityRole="button" - accessibilityLabel={`Remove ${exercise.name}`} - > - {({ pressed }) => ( + {/* Notes expandable */} + {hasNotes && notesExpanded && ( + - + + {exercise.notes} + - )} - + + )} ); } From 69b7e5b1c92c684619a349599e2310057f8ab1d6 Mon Sep 17 00:00:00 2001 From: Alvaro Torres Date: Wed, 4 Mar 2026 14:42:34 +0100 Subject: [PATCH 10/10] fix(db): migration number order --- src/db/schema.test.ts | 45 ++++++++++++++++++++++++++++++++++++++++++- src/db/schema.ts | 6 ++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/db/schema.test.ts b/src/db/schema.test.ts index e8a66f5..d4b5049 100644 --- a/src/db/schema.test.ts +++ b/src/db/schema.test.ts @@ -207,6 +207,49 @@ describe('Migration #4', () => { db = await createTestDatabase(); }); + it('should add notes column to workout_sets', async () => { + await db.runAsync( + "INSERT INTO exercises (name, type, muscle_group) VALUES ('Bench', 'weights', 'chest')", + ); + await db.runAsync('INSERT INTO workouts (routine_id) VALUES (NULL)'); + await db.runAsync( + "INSERT INTO workout_sets (workout_id, exercise_id, sort_order, notes) VALUES (1, 1, 1, 'Felt strong')", + ); + const row = await db.getFirstAsync<{ notes: string }>( + 'SELECT notes FROM workout_sets WHERE id = 1', + ); + expect(row!.notes).toBe('Felt strong'); + }); + + it('should default workout_sets notes to null', async () => { + await db.runAsync( + "INSERT INTO exercises (name, type, muscle_group) VALUES ('Bench', 'weights', 'chest')", + ); + await db.runAsync('INSERT INTO workouts (routine_id) VALUES (NULL)'); + await db.runAsync( + 'INSERT INTO workout_sets (workout_id, exercise_id, sort_order) VALUES (1, 1, 1)', + ); + const row = await db.getFirstAsync<{ notes: string | null }>( + 'SELECT notes FROM workout_sets WHERE id = 1', + ); + expect(row!.notes).toBeNull(); + }); + + it('should record schema version 4', async () => { + const row = await db.getFirstAsync<{ version: number }>( + 'SELECT MAX(version) as version FROM schema_version', + ); + expect(row!.version).toBeGreaterThanOrEqual(4); + }); +}); + +describe('Migration #5', () => { + let db: SQLiteDatabase; + + beforeEach(async () => { + db = await createTestDatabase(); + }); + it('should add notes column to exercises', async () => { await db.runAsync( "INSERT INTO exercises (name, type, muscle_group, notes) VALUES ('Test', 'weights', 'chest', 'Keep elbows in')", @@ -231,6 +274,6 @@ describe('Migration #4', () => { const row = await db.getFirstAsync<{ version: number }>( 'SELECT MAX(version) as version FROM schema_version', ); - expect(row!.version).toBe(4); + expect(row!.version).toBe(5); }); }); diff --git a/src/db/schema.ts b/src/db/schema.ts index cef4f0a..163041a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -137,6 +137,12 @@ const MIGRATIONS: Migration[] = [ }, { version: 4, + up: ` + ALTER TABLE workout_sets ADD COLUMN notes TEXT; + `, + }, + { + version: 5, up: ` ALTER TABLE exercises ADD COLUMN notes TEXT; `,