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 = [