diff --git a/app/collection/[id].tsx b/app/collection/[id].tsx
index 85d855b..8359d27 100644
--- a/app/collection/[id].tsx
+++ b/app/collection/[id].tsx
@@ -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 (
-
+
Collection not found
@@ -83,7 +83,7 @@ export default function CollectionDetailScreen() {
{/* Header */}
-
+
{collection.title}
@@ -138,7 +138,7 @@ export default function CollectionDetailScreen() {
onPress={() => handleWorkoutPress(workout.id)}
>
-
+
@@ -147,7 +147,7 @@ export default function CollectionDetailScreen() {
{t('durationLevel', {
duration: workout.duration,
- level: t(`levels.${workout.level.toLowerCase()}`),
+ level: t(`levels.${(workout.level ?? 'Beginner').toLowerCase()}`),
})}
@@ -155,14 +155,14 @@ export default function CollectionDetailScreen() {
{t('units.calUnit', { count: workout.calories })}
-
+
))}
{workouts.length === 0 && (
-
+
No workouts in this collection
diff --git a/app/workout/[id].tsx b/app/workout/[id].tsx
index 7ae8039..ea2bc7f 100644
--- a/app/workout/[id].tsx
+++ b/app/workout/[id].tsx
@@ -3,18 +3,17 @@
* Clean modal with workout info
*/
-import { useState, useEffect, useMemo } from 'react'
+import { useEffect, useMemo } from 'react'
import { View, Text as RNText, StyleSheet, ScrollView, Pressable } from 'react-native'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { BlurView } from 'expo-blur'
-import Ionicons from '@expo/vector-icons/Ionicons'
+import { Icon } from '@/src/shared/components/Icon'
import { useTranslation } from 'react-i18next'
-import { Host, Button, HStack } from '@expo/ui/swift-ui'
-import { glassEffect, padding } from '@expo/ui/swift-ui/modifiers'
import { useHaptics } from '@/src/shared/hooks'
import { usePurchases } from '@/src/shared/hooks/usePurchases'
+import { useUserStore } from '@/src/shared/stores'
import { track } from '@/src/shared/services/analytics'
import { canAccessWorkout } from '@/src/shared/services/access'
import { getWorkoutById } from '@/src/shared/data'
@@ -37,7 +36,8 @@ export default function WorkoutDetailScreen() {
const haptics = useHaptics()
const { t } = useTranslation()
const { id } = useLocalSearchParams<{ id: string }>()
- const [isSaved, setIsSaved] = useState(false)
+ const savedWorkouts = useUserStore((s) => s.savedWorkouts)
+ const toggleSavedWorkout = useUserStore((s) => s.toggleSavedWorkout)
const { isPremium } = usePurchases()
const colors = useThemeColors()
@@ -66,6 +66,7 @@ export default function WorkoutDetailScreen() {
)
}
+ const isSaved = savedWorkouts.includes(workout.id.toString())
const isLocked = !canAccessWorkout(workout.id, isPremium)
const handleStartWorkout = () => {
@@ -84,10 +85,11 @@ export default function WorkoutDetailScreen() {
const toggleSave = () => {
haptics.selection()
- setIsSaved(!isSaved)
+ toggleSavedWorkout(workout.id.toString())
}
- const repeatCount = Math.max(1, Math.floor(workout.rounds / workout.exercises.length))
+ const exerciseCount = workout.exercises?.length || 1
+ const repeatCount = Math.max(1, Math.floor((workout.rounds || exerciseCount) / exerciseCount))
return (
@@ -97,26 +99,16 @@ export default function WorkoutDetailScreen() {
{workout.title}
- {/* SwiftUI glass button */}
-
-
-
-
-
-
-
+ {/* Save button */}
+
+
+
+
+
{/* Content */}
@@ -136,17 +128,17 @@ export default function WorkoutDetailScreen() {
{/* Quick stats */}
-
+
- {t(`levels.${workout.level.toLowerCase()}`)}
+ {t(`levels.${(workout.level ?? 'Beginner').toLowerCase()}`)}
-
+
{t('units.minUnit', { count: workout.duration })}
-
+
{t('units.calUnit', { count: workout.calories })}
@@ -156,7 +148,7 @@ export default function WorkoutDetailScreen() {
{t('screens:workout.whatYoullNeed')}
{workout.equipment.map((item, index) => (
-
+
{item}
))}
@@ -178,7 +170,7 @@ export default function WorkoutDetailScreen() {
))}
-
+
{t('screens:workout.repeatRounds', { count: repeatCount })}
@@ -191,7 +183,7 @@ export default function WorkoutDetailScreen() {
{t('screens:workout.music')}
-
+
{t('screens:workout.musicMix', { vibe: musicVibeLabel })}
@@ -214,7 +206,7 @@ export default function WorkoutDetailScreen() {
onPress={handleStartWorkout}
>
{isLocked && (
-
+
)}
{isLocked ? t('screens:workout.unlockWithPremium') : t('screens:workout.startWorkout')}
@@ -260,9 +252,17 @@ function createStyles(colors: ThemeColors) {
color: colors.text.primary,
marginRight: SPACING[3],
},
- glassButtonContainer: {
- width: 44,
- height: 44,
+ saveButton: {
+ width: LAYOUT.TOUCH_TARGET,
+ height: LAYOUT.TOUCH_TARGET,
+ borderRadius: LAYOUT.TOUCH_TARGET / 2,
+ overflow: 'hidden',
+ },
+ saveButtonBlur: {
+ width: LAYOUT.TOUCH_TARGET,
+ height: LAYOUT.TOUCH_TARGET,
+ alignItems: 'center',
+ justifyContent: 'center',
},
// Video Preview
diff --git a/app/workout/category/[id].tsx b/app/workout/category/[id].tsx
index 506609d..5753b5f 100644
--- a/app/workout/category/[id].tsx
+++ b/app/workout/category/[id].tsx
@@ -7,7 +7,7 @@ import { useState, useMemo } from 'react'
import { View, StyleSheet, ScrollView, Pressable, Text as RNText } from 'react-native'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
-import Ionicons from '@expo/vector-icons/Ionicons'
+import { Icon } from '@/src/shared/components/Icon'
import {
Host,
Picker,
@@ -80,7 +80,7 @@ export default function CategoryDetailScreen() {
{/* Header */}
-
+
{categoryLabel}
@@ -122,24 +122,24 @@ export default function CategoryDetailScreen() {
onPress={() => handleWorkoutPress(workout.id)}
>
-
+
{workout.title}
- {t('durationLevel', { duration: workout.duration, level: t(`levels.${workout.level.toLowerCase()}`) })}
+ {t('durationLevel', { duration: workout.duration, level: t(`levels.${(workout.level ?? 'Beginner').toLowerCase()}`) })}
{t('units.calUnit', { count: workout.calories })}
-
+
))}
{translatedWorkouts.length === 0 && (
-
+
No workouts found
diff --git a/src/shared/data/programs.ts b/src/shared/data/programs.ts
index 90ba70c..d6bbc15 100644
--- a/src/shared/data/programs.ts
+++ b/src/shared/data/programs.ts
@@ -5,6 +5,7 @@
*/
import { Program, Assessment, ProgramId, WeekNumber, Week } from '../types/program'
+import type { WorkoutLevel, WorkoutCategory, MusicVibe } from '../types/workout'
type ProgramWorkoutInput = {
id: string
@@ -18,6 +19,13 @@ type ProgramWorkoutInput = {
tips: string[]
}
+/** Derive difficulty level from the week number in a progressive program */
+function getLevelFromWeek(week: number): WorkoutLevel {
+ if (week <= 2) return 'Beginner'
+ if (week === 3) return 'Intermediate'
+ return 'Advanced'
+}
+
// Helper to create exercises with consistent structure
const createExercise = (name: string, modification?: string, progression?: string) => ({
name,
@@ -1571,13 +1579,24 @@ export const ASSESSMENT_WORKOUT: Assessment = {
// PROGRAM BUILDER
// ═══════════════════════════════════════════════════════════════════════════
-function buildProgramWorkouts(inputs: ProgramWorkoutInput[]): any[] {
+function buildProgramWorkouts(inputs: ProgramWorkoutInput[], category: WorkoutCategory): any[] {
return inputs.map((input) => ({
...input,
exercises: input.exercises.map((name) =>
createExercise(name)
),
- duration: 4,
+ duration: 4 as const,
+ // Workout-compatible fields so screens don't crash
+ level: getLevelFromWeek(input.week),
+ category,
+ trainerId: '',
+ calories: 50,
+ rounds: input.exercises.length,
+ prepTime: 10,
+ workTime: 20,
+ restTime: 10,
+ musicVibe: 'electronic' as MusicVibe,
+ isFeatured: false,
}))
}
@@ -1661,7 +1680,7 @@ export const PROGRAMS: Record = {
optional: ['Wall', 'Elevated surface'],
},
focusAreas: ['Shoulders', 'Chest', 'Back', 'Arms', 'Posture'],
- weeks: buildWeeks(buildProgramWorkouts(upperBodyWorkouts)),
+ weeks: buildWeeks(buildProgramWorkouts(upperBodyWorkouts, 'upper-body')),
},
'lower-body': {
id: 'lower-body',
@@ -1675,7 +1694,7 @@ export const PROGRAMS: Record = {
optional: ['Step or bench', 'Wall'],
},
focusAreas: ['Legs', 'Glutes', 'Hips', 'Calves', 'Knee Health'],
- weeks: buildWeeks(buildProgramWorkouts(lowerBodyWorkouts)),
+ weeks: buildWeeks(buildProgramWorkouts(lowerBodyWorkouts, 'lower-body')),
},
'full-body': {
id: 'full-body',
@@ -1689,14 +1708,14 @@ export const PROGRAMS: Record = {
optional: ['Wall', 'Elevated surface'],
},
focusAreas: ['Total Body', 'Core', 'Cardio', 'Functional Fitness'],
- weeks: buildWeeks(buildProgramWorkouts(fullBodyWorkouts)),
+ weeks: buildWeeks(buildProgramWorkouts(fullBodyWorkouts, 'full-body')),
},
}
// Export individual arrays for convenience
-export const UPPER_BODY_WORKOUTS = buildProgramWorkouts(upperBodyWorkouts)
-export const LOWER_BODY_WORKOUTS = buildProgramWorkouts(lowerBodyWorkouts)
-export const FULL_BODY_WORKOUTS = buildProgramWorkouts(fullBodyWorkouts)
+export const UPPER_BODY_WORKOUTS = buildProgramWorkouts(upperBodyWorkouts, 'upper-body')
+export const LOWER_BODY_WORKOUTS = buildProgramWorkouts(lowerBodyWorkouts, 'lower-body')
+export const FULL_BODY_WORKOUTS = buildProgramWorkouts(fullBodyWorkouts, 'full-body')
// Export all workouts as flat array for player compatibility
export const ALL_PROGRAM_WORKOUTS = [