Skip to main content
The ProgramScreen component is the primary interface for navigating the Rippler program. It displays a week selector and a list of workout days for the selected week, showing completion status and main lift targets.

Overview

This screen serves as the hub for the entire training program, allowing users to:
  • Select different weeks (1-12) in the program
  • View all workout days for the selected week
  • See completion progress for each workout
  • Navigate to individual workout sessions
  • View calculated main lift weights based on goal settings
Location: ~/workspace/source/client/screens/ProgramScreen.tsx

Key Features

Week Navigation

Interactive week selector for navigating all 12 weeks

Progress Tracking

Visual indicators showing completed sets for each workout

Pull to Refresh

Refresh workout data and completion status

Main Lift Preview

Display calculated weight for the primary lift of each day

State Management

const [selectedWeek, setSelectedWeek] = useState(1);
const [loggedWorkouts, setLoggedWorkouts] = useState<LoggedWorkout[]>([]);
const [goalWeights, setGoalWeights] = useState<Record<string, number>>({});
const [refreshing, setRefreshing] = useState(false);

const totalWeeks = Object.keys(ripplerProgram.weeks).length;
const weekWorkouts = ripplerProgram.weeks[String(selectedWeek)] || [];

Data Loading

The screen loads multiple data sources in parallel:
const loadData = async () => {
  const [logged, currentWeek, goals] = await Promise.all([
    getLoggedWorkouts(),
    getCurrentWeek(),
    getGoalWeights(),
  ]);
  setLoggedWorkouts(logged);
  setSelectedWeek(currentWeek);
  setGoalWeights(goals);
};
useEffect(() => {
  loadData();
}, []);
Loads data when component mounts.

Week Selection

const handleWeekChange = async (week: number) => {
  setSelectedWeek(week);
  await setCurrentWeek(week);
};
The selected week is both stored in component state for immediate UI updates and persisted to storage for cross-session continuity.
The current week is saved to AsyncStorage, so users return to the same week when reopening the app.

Progress Calculation

Each workout’s progress is calculated from logged data:
const getWorkoutProgress = (workout: WorkoutDay) => {
  const logged = loggedWorkouts.find(
    (w) => w.week === workout.week && w.day === workout.day
  );
  if (!logged) return { isCompleted: false, completedSets: 0, totalSets: 0 };

  let completedSets = 0;
  let totalSets = 0;

  logged.exercises.forEach((ex) => {
    ex.sets.forEach((set) => {
      totalSets++;
      if (set.completed) completedSets++;
    });
  });

  return {
    isCompleted: logged.completed,
    completedSets,
    totalSets,
  };
};
Progress is calculated by counting completed sets vs total sets across all exercises in the workout.

Main Lift Weight Calculation

const getMainLiftWeight = (workout: WorkoutDay): number | string | undefined => {
  const mainLift = workout.exercises.find((e) => e.tier === "T1");
  if (!mainLift) return undefined;

  const mainLiftIndex = workout.exercises.findIndex((e) => e.tier === "T1");
  const mergedGoals = { ...getDefaultGoals(), ...goalWeights };
  const calculatedWeight = calculateTargetWeight(
    mainLift.exercise,
    workout.week,
    workout.day,
    mainLiftIndex,
    mergedGoals
  );

  return calculatedWeight !== null ? calculatedWeight : mainLift.weight;
};
This function:
  1. Finds the T1 (main) lift for the workout
  2. Merges saved goal weights with defaults
  3. Calculates the target weight based on the program’s percentage scheme
  4. Falls back to the static weight if calculation fails
const handleDayPress = (workout: WorkoutDay) => {
  navigation.navigate("Workout", {
    week: workout.week,
    day: workout.day,
  });
};
Tapping a workout card navigates to the WorkoutScreen with week and day parameters.

UI Structure

<FlatList
  data={weekWorkouts}
  renderItem={renderItem}
  ListHeaderComponent={renderHeader}
  refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
  contentContainerStyle={{
    paddingTop: headerHeight + Spacing.xl,
    paddingBottom: tabBarHeight + Spacing.xl,
    paddingHorizontal: Spacing.lg,
  }}
/>

Header Component

const renderHeader = () => (
  <View style={styles.headerContainer}>
    <ThemedText type="h1" style={styles.title}>
      The Rippler
    </ThemedText>
    <ThemedText style={[styles.subtitle, { color: theme.textSecondary }]}>
      12-Week Strength Program
    </ThemedText>
    <WeekSelector
      totalWeeks={totalWeeks}
      currentWeek={selectedWeek}
      onWeekChange={handleWeekChange}
    />
    <ThemedText type="h4" style={styles.weekTitle}>
      Week {selectedWeek}
    </ThemedText>
  </View>
);

Day Card Rendering

const renderItem = ({ item }: { item: WorkoutDay }) => {
  const progress = getWorkoutProgress(item);
  const mainLiftWeight = getMainLiftWeight(item);
  
  return (
    <DayCard
      workout={item}
      onPress={() => handleDayPress(item)}
      isCompleted={progress.isCompleted}
      completedSets={progress.completedSets}
      totalSets={progress.totalSets}
      mainLiftWeight={mainLiftWeight}
    />
  );
};

Data Flow

Component Integrations

The WeekSelector provides a horizontal scrollable list of week numbers:
<WeekSelector
  totalWeeks={12}
  currentWeek={selectedWeek}
  onWeekChange={handleWeekChange}
/>
Users can quickly jump to any week in the program.
DayCard displays a workout day with all relevant information:
<DayCard
  workout={item}
  onPress={() => handleDayPress(item)}
  isCompleted={progress.isCompleted}
  completedSets={progress.completedSets}
  totalSets={progress.totalSets}
  mainLiftWeight={mainLiftWeight}
/>
Shows:
  • Day name (e.g., “Upper A”, “Lower B”)
  • Exercise list preview
  • Progress indicator (X/Y sets completed)
  • Main lift target weight
  • Completion badge
The Rippler program data is structured by week and day:
// data/rippler-program.ts
export const ripplerProgram = {
  weeks: {
    "1": [
      {
        week: 1,
        day: "Upper A",
        exercises: [
          { tier: "T1", exercise: "Bench Press", weight: 225, reps: 5, sets: 5 },
          { tier: "T2", exercise: "Close Grip Bench", weight: 180, reps: 8, sets: 3 },
          // ...
        ]
      },
      // ...
    ],
    "2": [ /* ... */ ],
    // ... weeks 3-12
  }
};

Layout Considerations

The screen uses navigation hooks for proper spacing around device UI elements:
const insets = useSafeAreaInsets();
const headerHeight = useHeaderHeight();
const tabBarHeight = useBottomTabBarHeight();

contentContainerStyle={{
  paddingTop: headerHeight + Spacing.xl,
  paddingBottom: tabBarHeight + Spacing.xl,
  paddingHorizontal: Spacing.lg,
}}
scrollIndicatorInsets={{ bottom: insets.bottom }}
Always account for header and tab bar heights to prevent content from being obscured by navigation elements.

Performance Optimization

  • Parallel Data Loading: Uses Promise.all to load multiple data sources simultaneously
  • FlatList: Efficient rendering of workout days with built-in virtualization
  • Memoized Callbacks: useFocusEffect with proper dependency arrays
  • Conditional Rendering: Only calculates progress and weights for visible items

User Experience Features

1

Auto-refresh on Focus

Data automatically refreshes when returning from WorkoutScreen, showing updated progress
2

Pull to Refresh

Manual refresh gesture for users who want to ensure data is up-to-date
3

Persistent Week Selection

Selected week persists across app sessions
4

Visual Progress Indicators

Clear visual feedback on workout completion status
5

Target Weight Display

Users see calculated main lift weights before starting workout

Build docs developers (and LLMs) love