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 );
};
Initial Load
Focus Effect
Pull to Refresh
useEffect (() => {
loadData ();
}, []);
Loads data when component mounts. useFocusEffect (
useCallback (() => {
loadData ();
}, [])
);
Reloads data when screen gains focus (e.g., returning from WorkoutScreen). const onRefresh = async () => {
setRefreshing ( true );
await loadData ();
setRefreshing ( false );
};
Manual refresh triggered by user gesture.
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:
Finds the T1 (main) lift for the workout
Merges saved goal weights with defaults
Calculates the target weight based on the program’s percentage scheme
Falls back to the static weight if calculation fails
Navigation
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 ,
} }
/>
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.
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
Auto-refresh on Focus
Data automatically refreshes when returning from WorkoutScreen, showing updated progress
Pull to Refresh
Manual refresh gesture for users who want to ensure data is up-to-date
Persistent Week Selection
Selected week persists across app sessions
Visual Progress Indicators
Clear visual feedback on workout completion status
Target Weight Display
Users see calculated main lift weights before starting workout