The HistoryScreen component displays a chronological list of all logged workouts, showing completion status, statistics, and providing navigation back to individual workout details.
Overview
This screen serves as a workout journal, allowing users to:
View all logged workouts in reverse chronological order
See completion statistics (completed count vs total logged)
Review workout details (sets completed, exercises performed)
Navigate back to specific workouts to view or edit
Delete workout logs
Location: ~/workspace/source/client/screens/HistoryScreen.tsx
Key Features
Chronological List Workouts sorted by most recent first
Summary Statistics Overview of completed vs total logged workouts
Delete Functionality Remove individual workout logs
Empty State Helpful message when no workouts are logged yet
State Management
const [ workouts , setWorkouts ] = useState < LoggedWorkout []>([]);
const [ refreshing , setRefreshing ] = useState ( false );
Data Loading
const loadData = async () => {
const logged = await getLoggedWorkouts ();
const sorted = logged . sort (
( a , b ) => new Date ( b . dateLogged ). getTime () - new Date ( a . dateLogged ). getTime ()
);
setWorkouts ( sorted );
};
Initial Load
Focus Effect
Pull to Refresh
useEffect (() => {
loadData ();
}, []);
Loads all logged workouts when component mounts. useFocusEffect (
useCallback (() => {
loadData ();
}, [])
);
Reloads when screen gains focus (e.g., after completing a workout). const onRefresh = async () => {
setRefreshing ( true );
await loadData ();
setRefreshing ( false );
};
Manual refresh gesture.
Workouts are sorted in descending order by dateLogged, showing the most recent workouts first.
HistoryCard Component
Each workout is displayed in a HistoryCard with animations:
interface HistoryCardProps {
workout : LoggedWorkout ;
onPress : () => void ;
onDelete : () => void ;
}
function HistoryCard ({ workout , onPress , onDelete } : HistoryCardProps ) {
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 });
};
// Calculate stats
const completedSets = workout . exercises . reduce (
( acc , ex ) => acc + ex . sets . filter (( s ) => s . completed ). length ,
0
);
const totalSets = workout . exercises . reduce (
( acc , ex ) => acc + ex . sets . length ,
0
);
const dateLogged = new Date ( workout . dateLogged );
const formattedDate = dateLogged . toLocaleDateString ( "en-US" , {
month: "short" ,
day: "numeric" ,
});
return (
< AnimatedPressable
onPress = { onPress }
onPressIn = { handlePressIn }
onPressOut = { handlePressOut }
style = { [
styles.historyCard,
{
backgroundColor: theme.backgroundDefault,
borderLeftColor: workout.completed ? theme.success : theme.primary,
},
animatedStyle,
]}
>
{ /* Card content */ }
</AnimatedPressable>
);
}
The card uses react-native-reanimated for smooth press animations and a colored left border indicating completion status.
Card Structure
< AnimatedPressable style = { styles . historyCard } >
< View style = { styles . cardContent } >
{ /* Header */ }
< View style = { styles . cardHeader } >
< View >
< ThemedText type = "h4" >
Week { workout . week } - { workout . day }
</ ThemedText >
< ThemedText style = { styles . dateText } >
{ formattedDate }
</ ThemedText >
</ View >
{ workout . completed && (
< View style = { styles . completedBadge } >
< Feather name = "check" size = { 14 } color = "#FFFFFF" />
</ View >
) }
</ View >
{ /* Stats */ }
< View style = { styles . statsRow } >
< View style = { styles . statItem } >
< ThemedText style = { styles . statValue } >
{ completedSets } / { totalSets }
</ ThemedText >
< ThemedText style = { styles . statLabel } > Sets </ ThemedText >
</ View >
< View style = { styles . statItem } >
< ThemedText style = { styles . statValue } >
{ workout . exercises . length }
</ ThemedText >
< ThemedText style = { styles . statLabel } > Exercises </ ThemedText >
</ View >
</ View >
{ /* Exercise Preview */ }
< View style = { styles . exercisesList } >
{ workout . exercises . slice ( 0 , 2 ). map (( ex , index ) => (
< ThemedText key = { index } numberOfLines = { 1 } >
{ ex . tier } : { ex . exercise }
</ ThemedText >
)) }
</ View >
</ View >
{ /* Delete Button */ }
< Pressable
onPress = { onDelete }
style = { styles . deleteButton }
hitSlop = { 10 }
>
< Feather name = "trash-2" size = { 16 } />
</ Pressable >
</ AnimatedPressable >
Visual Indicators
The left border color indicates workout status: borderLeftColor : workout . completed ? theme . success : theme . primary
Green : Workout completed
Primary color : Workout in progress
A circular badge with a checkmark appears for completed workouts: { workout . completed && (
< View style = { [ styles . completedBadge , { backgroundColor: theme . success }] } >
< Feather name = "check" size = { 14 } color = "#FFFFFF" />
</ View >
)}
Displays “X/Y” format showing completed sets vs total sets: < ThemedText > { completedSets } / { totalSets } </ ThemedText >
< ThemedText > Sets </ ThemedText >
User Interactions
View Workout
Delete Workout
const handlePress = ( workout : LoggedWorkout ) => {
navigation . navigate ( "Workout" , {
week: workout . week ,
day: workout . day
});
};
Tapping a card navigates to the WorkoutScreen to view or continue the workout. const handleDelete = async ( id : string ) => {
await deleteLoggedWorkout ( id );
await loadData ();
};
Tapping the delete button removes the workout from storage and refreshes the list.
Deletion is immediate with no confirmation dialog. Consider adding a confirmation for better UX in production.
Summary Statistics
const completedCount = workouts . filter (( w ) => w . completed ). length ;
const totalCount = workouts . length ;
const renderHeader = () => (
< View style = {styles. header } >
< ThemedText type = "h1" > History </ ThemedText >
{ totalCount > 0 && (
< View style = {styles. summaryRow } >
< View style = {styles. summaryCard } >
< ThemedText style = {{ color : theme . success }} >
{ completedCount }
</ ThemedText >
< ThemedText > Completed </ ThemedText >
</ View >
< View style = {styles. summaryCard } >
< ThemedText style = {{ color : theme . primary }} >
{ totalCount }
</ ThemedText >
< ThemedText > Total Logged </ ThemedText >
</ View >
</ View >
)}
</ View >
);
Summary cards only appear when there are logged workouts, preventing visual clutter on empty state.
Empty State
const renderEmpty = () => (
< EmptyState
image = { require ( "../../assets/images/empty-history.png" )}
title = "No Workouts Logged"
message = "Start tracking your workouts in the Program tab to see your progress here."
/>
);
The empty state provides:
A visual illustration
Clear messaging
Guidance on what to do next
FlatList Implementation
< FlatList
style = { [ styles . container , { backgroundColor: theme . backgroundRoot }] }
contentContainerStyle = { [
{
paddingTop: headerHeight + Spacing . xl ,
paddingBottom: tabBarHeight + Spacing . xl ,
paddingHorizontal: Spacing . lg ,
},
workouts . length === 0 ? styles . emptyContainer : undefined ,
] }
scrollIndicatorInsets = { { bottom: insets . bottom } }
data = { workouts }
renderItem = { renderItem }
keyExtractor = { ( item ) => item . id }
ListHeaderComponent = { totalCount > 0 ? renderHeader : undefined }
ListEmptyComponent = { renderEmpty }
refreshControl = {
< RefreshControl refreshing = { refreshing } onRefresh = { onRefresh } />
}
/>
The emptyContainer style is conditionally applied to center the empty state vertically.
const dateLogged = new Date ( workout . dateLogged );
const formattedDate = dateLogged . toLocaleDateString ( "en-US" , {
month: "short" ,
day: "numeric" ,
});
// Example output: "Jan 15"
Dates are formatted to be concise while remaining clear and readable.
Animation Details
const AnimatedPressable = Animated . createAnimatedComponent ( Pressable );
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 });
};
The card scales down slightly when pressed, providing tactile feedback using react-native-reanimated’s spring animation.
Data Flow
Exercise Preview
< View style = {styles. exercisesList } >
{ workout . exercises . slice ( 0 , 2 ). map (( ex , index ) => (
< ThemedText
key = { index }
style = { [styles.exerciseText, { color: theme.textSecondary }]}
numberOfLines={1}
>
{ex.tier}: {ex. exercise }
</ ThemedText >
))}
</ View >
Only the first 2 exercises are shown to provide a preview without overwhelming the card.
Layout Considerations
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 }}
Best Practices
Reverse Chronological Order
Always show most recent workouts first for quick access
Reload on Focus
Use useFocusEffect to ensure the list updates when returning from other screens
Provide Empty State
Guide users on what to do when no data exists
Visual Feedback
Use animations and color coding to communicate workout status
Summary Stats
Give users a quick overview of their training consistency
FlatList : Efficient rendering with virtualization
Memoized Calculations : Stats calculated once per card
Conditional Rendering : Summary header only renders when needed
Limited Preview : Only 2 exercises shown per card
WorkoutScreen - Destination when tapping a history card
Storage API - Functions for retrieving and managing logged workouts