fix: add missing Workout fields to program workouts and guard against undefined level

Program workouts built by buildProgramWorkouts() were missing level,
rounds, calories, and other Workout-interface fields, causing
workout.level.toLowerCase() to crash on the detail, collection, and
category screens. Added derived defaults (level from week number,
category from program id, standard Tabata timings) and defensive
fallbacks with ?? 'Beginner' at all call sites. Also fixed a potential
division-by-zero when exercises array is empty.
This commit is contained in:
Millian Lamiaux
2026-03-25 23:28:47 +01:00
parent 4fa8be600c
commit f11eb6b9ae
4 changed files with 78 additions and 59 deletions

View File

@@ -8,7 +8,7 @@ import { View, StyleSheet, ScrollView, Pressable } from 'react-native'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LinearGradient } from 'expo-linear-gradient'
import Ionicons from '@expo/vector-icons/Ionicons'
import { Icon } from '@/src/shared/components/Icon'
import { useTranslation } from 'react-i18next'
import { useHaptics } from '@/src/shared/hooks'
@@ -70,7 +70,7 @@ export default function CollectionDetailScreen() {
if (!collection) {
return (
<View style={[styles.container, styles.centered, { paddingTop: insets.top }]}>
<Ionicons name="folder-open-outline" size={48} color={colors.text.tertiary} />
<Icon name="folder" size={48} color={colors.text.tertiary} />
<StyledText size={17} color={colors.text.tertiary} style={{ marginTop: SPACING[3] }}>
Collection not found
</StyledText>
@@ -83,7 +83,7 @@ export default function CollectionDetailScreen() {
{/* Header */}
<View style={styles.header}>
<Pressable testID="collection-back-button" onPress={handleBack} style={styles.backButton}>
<Ionicons name="chevron-back" size={24} color={colors.text.primary} />
<Icon name="chevron.left" size={24} color={colors.text.primary} />
</Pressable>
<StyledText size={22} weight="bold" color={colors.text.primary} numberOfLines={1} style={{ flex: 1, textAlign: 'center' }}>
{collection.title}
@@ -138,7 +138,7 @@ export default function CollectionDetailScreen() {
onPress={() => handleWorkoutPress(workout.id)}
>
<View style={[styles.workoutAvatar, { backgroundColor: BRAND.PRIMARY }]}>
<Ionicons name="flame" size={20} color="#FFFFFF" />
<Icon name="flame.fill" size={20} color="#FFFFFF" />
</View>
<View style={styles.workoutInfo}>
<StyledText size={17} weight="semibold" color={colors.text.primary}>
@@ -147,7 +147,7 @@ export default function CollectionDetailScreen() {
<StyledText size={13} color={colors.text.tertiary}>
{t('durationLevel', {
duration: workout.duration,
level: t(`levels.${workout.level.toLowerCase()}`),
level: t(`levels.${(workout.level ?? 'Beginner').toLowerCase()}`),
})}
</StyledText>
</View>
@@ -155,14 +155,14 @@ export default function CollectionDetailScreen() {
<StyledText size={13} color={BRAND.PRIMARY}>
{t('units.calUnit', { count: workout.calories })}
</StyledText>
<Ionicons name="chevron-forward" size={16} color={colors.text.tertiary} />
<Icon name="chevron.right" size={16} color={colors.text.tertiary} />
</View>
</Pressable>
))}
{workouts.length === 0 && (
<View style={styles.emptyState}>
<Ionicons name="barbell-outline" size={48} color={colors.text.tertiary} />
<Icon name="dumbbell" size={48} color={colors.text.tertiary} />
<StyledText size={17} color={colors.text.tertiary} style={{ marginTop: SPACING[3] }}>
No workouts in this collection
</StyledText>