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 ? (
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 */}
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}
+
- )}
-
+
+ )}
);
}
diff --git a/src/db/schema.test.ts b/src/db/schema.test.ts
index 8a31ab5..e101dd3 100644
--- a/src/db/schema.test.ts
+++ b/src/db/schema.test.ts
@@ -239,6 +239,41 @@ 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).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')",
+ );
+ 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 5', async () => {
+ const row = await db.getFirstAsync<{ version: number }>(
+ 'SELECT MAX(version) as version FROM schema_version',
+ );
+ expect(row!.version).toBe(5);
});
});
diff --git a/src/db/schema.ts b/src/db/schema.ts
index f794135..163041a 100644
--- a/src/db/schema.ts
+++ b/src/db/schema.ts
@@ -138,10 +138,15 @@ const MIGRATIONS: Migration[] = [
{
version: 4,
up: `
- -- Optional notes per workout set
ALTER TABLE workout_sets ADD COLUMN notes TEXT;
`,
},
+ {
+ version: 5,
+ up: `
+ ALTER TABLE exercises ADD COLUMN notes TEXT;
+ `,
+ },
];
export async function runMigrations(db: SQLiteDatabase): Promise {
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 }));
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 6e7481a..f2a9a77 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -225,6 +225,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 0b8dace..2fe86e4 100644
--- a/src/i18n/es.ts
+++ b/src/i18n/es.ts
@@ -233,6 +233,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',
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 34885c8..263b73c 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;
}
@@ -71,7 +72,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(
@@ -92,6 +93,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,
}));
@@ -184,13 +186,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,
);
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/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 19899cf..e88993f 100644
--- a/src/repositories/workout.repo.ts
+++ b/src/repositories/workout.repo.ts
@@ -35,6 +35,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;
}
@@ -257,6 +258,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
@@ -279,6 +281,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: [],
diff --git a/src/types/index.ts b/src/types/index.ts
index 81eeedd..a5cad9b 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;
}
@@ -150,6 +151,7 @@ export interface ExerciseExport {
muscleGroups?: string[];
illustration: string | null;
restSeconds: number;
+ notes?: string | null;
createdAt: string;
}