Skip to main content
The ExercisesScreen component provides a management interface for custom exercises. Users can add, edit, and delete exercises, with support for notes about form, cues, or variations.

Overview

This screen allows users to:
  • View all custom exercises in alphabetical order
  • Add new exercises to their personal library
  • Edit existing exercise names and notes
  • Delete exercises from the library
  • Track the total count of exercises
Location: ~/workspace/source/client/screens/ExercisesScreen.tsx

Key Features

Exercise Library

Alphabetically sorted list of all custom exercises

CRUD Operations

Full create, read, update, and delete functionality

Exercise Notes

Optional notes for form cues, tips, or variations

Floating Action Button

Quick access to add new exercises

State Management

const [exercises, setExercises] = useState<Exercise[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [editingExercise, setEditingExercise] = useState<Exercise | null>(null);
const [exerciseName, setExerciseName] = useState("");
const [exerciseNotes, setExerciseNotes] = useState("");

Exercise Type

interface Exercise {
  id: string;
  name: string;
  notes?: string;
}

Data Loading

const loadData = async () => {
  const loaded = await getExercises();
  setExercises(loaded.sort((a, b) => a.name.localeCompare(b.name)));
};
useEffect(() => {
  loadData();
}, []);
Exercises are automatically sorted alphabetically using localeCompare for consistent ordering.

ExerciseRow Component

interface ExerciseRowProps {
  exercise: Exercise;
  onEdit: () => void;
  onDelete: () => void;
}

function ExerciseRow({ exercise, onEdit, onDelete }: ExerciseRowProps) {
  const { theme } = useTheme();
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const handlePressIn = () => {
    scale.value = withSpring(0.98, { damping: 15 });
  };

  const handlePressOut = () => {
    scale.value = withSpring(1, { damping: 15 });
  };

  return (
    <AnimatedPressable
      onPress={onEdit}
      onPressIn={handlePressIn}
      onPressOut={handlePressOut}
      style={[styles.exerciseRow, animatedStyle]}
    >
      <View style={styles.exerciseInfo}>
        <ThemedText type="body" style={styles.exerciseName}>
          {exercise.name}
        </ThemedText>
        {exercise.notes && (
          <ThemedText
            style={[styles.exerciseNotes, { color: theme.textSecondary }]}
            numberOfLines={1}
          >
            {exercise.notes}
          </ThemedText>
        )}
      </View>
      <View style={styles.exerciseActions}>
        <Pressable onPress={onEdit} style={styles.actionButton}>
          <Feather name="edit-2" size={16} color={theme.primary} />
        </Pressable>
        <Pressable onPress={onDelete} style={styles.actionButton}>
          <Feather name="trash-2" size={16} color={theme.textSecondary} />
        </Pressable>
      </View>
    </AnimatedPressable>
  );
}
Each row uses react-native-reanimated for smooth press animations and includes dedicated edit/delete buttons.

CRUD Operations

const openAddModal = () => {
  setEditingExercise(null);
  setExerciseName("");
  setExerciseNotes("");
  setModalVisible(true);
};

const handleSave = async () => {
  if (!exerciseName.trim()) return;

  if (Platform.OS !== "web") {
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
  }

  await addExercise(exerciseName.trim(), exerciseNotes.trim() || undefined);
  
  setModalVisible(false);
  await loadData();
};
Deletion is immediate with no confirmation. Consider adding a confirmation dialog for better UX.

Add/Edit Modal

The modal serves both add and edit functionality:
<Modal
  visible={modalVisible}
  animationType="slide"
  presentationStyle="pageSheet"
  onRequestClose={() => setModalVisible(false)}
>
  <View style={styles.modalContainer}>
    {/* Header with Cancel/Save */}
    <View style={styles.modalHeader}>
      <Pressable onPress={() => setModalVisible(false)}>
        <ThemedText style={{ color: theme.primary }}>Cancel</ThemedText>
      </Pressable>
      <ThemedText type="h4">
        {editingExercise ? "Edit Exercise" : "Add Exercise"}
      </ThemedText>
      <Pressable onPress={handleSave}>
        <ThemedText
          style={{
            color: exerciseName.trim() ? theme.primary : theme.disabled,
          }}
        >
          Save
        </ThemedText>
      </Pressable>
    </View>

    <View style={styles.modalContent}>
      {/* Exercise Name Input */}
      <View style={styles.inputGroup}>
        <ThemedText style={styles.inputLabel}>Exercise Name</ThemedText>
        <TextInput
          style={styles.input}
          value={exerciseName}
          onChangeText={setExerciseName}
          placeholder="e.g., Bench Press"
          autoFocus
          testID="exercise-name-input"
        />
      </View>

      {/* Notes Input */}
      <View style={styles.inputGroup}>
        <ThemedText style={styles.inputLabel}>Notes (optional)</ThemedText>
        <TextInput
          style={[styles.input, styles.textArea]}
          value={exerciseNotes}
          onChangeText={setExerciseNotes}
          placeholder="Add notes about form, cues, etc."
          multiline
          numberOfLines={3}
          textAlignVertical="top"
          testID="exercise-notes-input"
        />
      </View>
    </View>
  </View>
</Modal>
The modal title and behavior change based on whether editingExercise is null (add mode) or populated (edit mode).

Floating Action Button (FAB)

<View
  style={[
    styles.fabContainer,
    { bottom: tabBarHeight + Spacing.lg },
  ]}
>
  <Pressable
    onPress={openAddModal}
    style={[styles.fab, { backgroundColor: theme.primary }]}
  >
    <Feather name="plus" size={24} color="#FFFFFF" />
  </>
</View>
The FAB is positioned above the tab bar for easy access:
const fabContainer: ViewStyle = {
  position: "absolute",
  right: Spacing.lg,
};

const fab: ViewStyle = {
  width: 56,
  height: 56,
  borderRadius: 28,
  alignItems: "center",
  justifyContent: "center",
  shadowColor: "#000",
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: 0.15,
  shadowRadius: 4,
  elevation: 4,
};

Header with Count

const renderHeader = () => (
  <View style={styles.header}>
    <ThemedText type="h1">Exercises</ThemedText>
    <ThemedText style={[styles.subtitle, { color: theme.textSecondary }]}>
      {exercises.length} exercises in your library
    </ThemedText>
  </View>
);
Displays the total count of exercises in the user’s library.

FlatList Implementation

<FlatList
  style={styles.list}
  contentContainerStyle={{
    paddingTop: headerHeight + Spacing.xl,
    paddingBottom: tabBarHeight + Spacing["4xl"],
    paddingHorizontal: Spacing.lg,
  }}
  scrollIndicatorInsets={{ bottom: insets.bottom }}
  data={exercises}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  ListHeaderComponent={renderHeader}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
  }
/>
Extra bottom padding (4xl) ensures the last item is not obscured by the FAB.

Storage Layer

Exercises are persisted using AsyncStorage:
// lib/storage.ts
export const getExercises = async (): Promise<Exercise[]> => {
  const data = await AsyncStorage.getItem(EXERCISES_KEY);
  return data ? JSON.parse(data) : [];
};

export const addExercise = async (
  name: string,
  notes?: string
): Promise<void> => {
  const exercises = await getExercises();
  const newExercise: Exercise = {
    id: Date.now().toString(),
    name,
    notes,
  };
  exercises.push(newExercise);
  await AsyncStorage.setItem(EXERCISES_KEY, JSON.stringify(exercises));
};

export const updateExercise = async (
  id: string,
  updates: Partial<Exercise>
): Promise<void> => {
  const exercises = await getExercises();
  const index = exercises.findIndex((e) => e.id === id);
  if (index !== -1) {
    exercises[index] = { ...exercises[index], ...updates };
    await AsyncStorage.setItem(EXERCISES_KEY, JSON.stringify(exercises));
  }
};

export const deleteExercise = async (id: string): Promise<void> => {
  const exercises = await getExercises();
  const filtered = exercises.filter((e) => e.id !== id);
  await AsyncStorage.setItem(EXERCISES_KEY, JSON.stringify(filtered));
};

Data Flow

Validation

const handleSave = async () => {
  if (!exerciseName.trim()) return;
  // ... save logic
};
The save button is disabled (via opacity/color) when the exercise name is empty:
<ThemedText
  style={{
    color: exerciseName.trim() ? theme.primary : theme.disabled,
  }}
>
  Save
</ThemedText>

Haptic Feedback

if (Platform.OS !== "web") {
  // Light feedback for save
  Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
  
  // Medium feedback for delete
  Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
Provides tactile feedback on mobile devices for important actions.

Use Cases

Users can track variations of standard exercises:
  • “Bench Press (Close Grip)”
  • “Squat (Pause)”
  • “Deadlift (Deficit)”
With notes about technique differences.
Store mental cues for proper form:
  • Exercise: “Bench Press”
  • Notes: “Retract scapula, drive through floor, touch chest”
Track equipment-specific exercises:
  • “Dumbbell Bench Press”
  • “Cable Flyes”
  • “Machine Leg Press”
Add niche or sport-specific movements not in the default program.

Best Practices

1

Alphabetical Sorting

Always sort exercises alphabetically for easy navigation
2

Trim Input

Trim whitespace from names and notes before saving
3

Optional Notes

Make notes optional with clear placeholder text
4

Haptic Feedback

Provide tactile feedback for actions on mobile
5

Auto-focus Name

Auto-focus the name input when opening the modal
6

Test IDs

Include testID props for automated testing

Layout Considerations

const insets = useSafeAreaInsets();
const headerHeight = useHeaderHeight();
const tabBarHeight = useBottomTabBarHeight();

contentContainerStyle={{
  paddingTop: headerHeight + Spacing.xl,
  paddingBottom: tabBarHeight + Spacing["4xl"], // Extra space for FAB
  paddingHorizontal: Spacing.lg,
}}
The FAB overlays content, so ensure adequate bottom padding on the FlatList to prevent the last item from being obscured.

Future Enhancements

  • Search/Filter: Add search bar for large exercise libraries
  • Categories: Group exercises by type (upper body, lower body, etc.)
  • Usage Stats: Show how many times each exercise has been logged
  • Import/Export: Share exercise libraries between users
  • Images: Attach reference images or videos

Build docs developers (and LLMs) love