/** * TabataFit Program Detail Screen * Clean scrollable layout — native header, Apple Fitness+ style */ import React, { useEffect, useRef } from 'react' import { View, Text as RNText, StyleSheet, ScrollView, Pressable, Animated, } from 'react-native' import { Stack, useRouter, useLocalSearchParams } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Icon } from '@/src/shared/components/Icon' import { useTranslation } from 'react-i18next' import { useHaptics } from '@/src/shared/hooks' import { useProgramStore } from '@/src/shared/stores' import { PROGRAMS } from '@/src/shared/data/programs' import { track } from '@/src/shared/services/analytics' import { useThemeColors, BRAND } from '@/src/shared/theme' import { TYPOGRAPHY } from '@/src/shared/constants/typography' import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' import { SPRING } from '@/src/shared/constants/animations' import type { ProgramId } from '@/src/shared/types' import type { IconName } from '@/src/shared/components/Icon' // Per-program accent colors (matches home screen cards) const PROGRAM_ACCENT: Record = { 'upper-body': { color: '#FF6B35', icon: 'dumbbell' }, 'lower-body': { color: '#30D158', icon: 'figure.walk' }, 'full-body': { color: '#5AC8FA', icon: 'flame' }, } export default function ProgramDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>() const programId = id as ProgramId const { t } = useTranslation('screens') const insets = useSafeAreaInsets() const router = useRouter() const haptics = useHaptics() const colors = useThemeColors() const isDark = colors.colorScheme === 'dark' const program = PROGRAMS[programId] const accent = PROGRAM_ACCENT[programId] ?? PROGRAM_ACCENT['full-body'] const selectProgram = useProgramStore((s) => s.selectProgram) const progress = useProgramStore((s) => s.programsProgress[programId]) const isWeekUnlocked = useProgramStore((s) => s.isWeekUnlocked) const getCurrentWorkout = useProgramStore((s) => s.getCurrentWorkout) const completion = useProgramStore((s) => s.getProgramCompletion(programId)) // CTA entrance animation const ctaAnim = useRef(new Animated.Value(0)).current useEffect(() => { Animated.sequence([ Animated.delay(300), Animated.spring(ctaAnim, { toValue: 1, ...SPRING.GENTLE, useNativeDriver: true, }), ]).start() }, []) useEffect(() => { if (program) { track('program_detail_viewed', { program_id: programId, program_title: program.title, }) } }, [programId]) if (!program) { return ( <> {t('programs.notFound', { defaultValue: 'Program not found' })} ) } const handleStartProgram = () => { haptics.phaseChange() selectProgram(programId) const currentWorkout = getCurrentWorkout(programId) if (currentWorkout) { router.push(`/workout/${currentWorkout.id}`) } } const handleWorkoutPress = (workoutId: string) => { haptics.buttonTap() router.push(`/workout/${workoutId}`) } const hasStarted = progress.completedWorkoutIds.length > 0 const ctaBg = isDark ? '#FFFFFF' : '#000000' const ctaTextColor = isDark ? '#000000' : '#FFFFFF' const ctaLabel = hasStarted ? progress.isProgramCompleted ? t('programs.restartProgram') : t('programs.continueTraining') : t('programs.startProgram') return ( <> {/* Icon + Title */} {program.title} {program.durationWeeks} {t('programs.weeks')} · {program.totalWorkouts} {t('programs.workouts')} {/* Description */} {program.description} {/* Stats Card */} {program.durationWeeks} {t('programs.weeks')} {program.totalWorkouts} {t('programs.workouts')} 4 {t('programs.minutes')} {/* Equipment & Focus */} {program.equipment.required.length > 0 && ( <> {t('programs.equipment')} {program.equipment.required.map((item) => ( {item} ))} {program.equipment.optional.map((item) => ( {item} {t('programs.optional')} ))} )} {t('programs.focusAreas')} {program.focusAreas.map((area) => ( {area} ))} {/* Separator */} {/* Progress (if started) */} {hasStarted && ( {t('programs.yourProgress')} {completion}% {progress.completedWorkoutIds.length} {t('programs.of')} {program.totalWorkouts} {t('programs.workoutsComplete')} )} {/* Training Plan */} {t('programs.trainingPlan')} {program.weeks.map((week) => { const isUnlocked = isWeekUnlocked(programId, week.weekNumber) const isCurrentWeek = progress.currentWeek === week.weekNumber const weekCompletion = week.workouts.filter((w) => progress.completedWorkoutIds.includes(w.id) ).length return ( {/* Week Header */} {week.title} {!isUnlocked && ( )} {isCurrentWeek && isUnlocked && ( {t('programs.current')} )} {week.description} {weekCompletion > 0 && ( {weekCompletion}/{week.workouts.length} {t('programs.complete')} )} {/* Week Workouts */} {isUnlocked && week.workouts.map((workout, index) => { const isCompleted = progress.completedWorkoutIds.includes(workout.id) const isWorkoutLocked = !isCompleted && index > 0 && !progress.completedWorkoutIds.includes(week.workouts[index - 1].id) && week.weekNumber === progress.currentWeek return ( [ s.workoutRow, isWorkoutLocked && { opacity: 0.4 }, pressed && !isWorkoutLocked && { opacity: 0.6 }, ]} onPress={() => !isWorkoutLocked && handleWorkoutPress(workout.id)} disabled={isWorkoutLocked} > {isCompleted ? ( ) : isWorkoutLocked ? ( ) : ( {index + 1} )} {workout.title} {workout.exercises.length} {t('programs.exercises')} · {workout.duration} {t('programs.min')} {!isWorkoutLocked && !isCompleted && ( )} ) })} ) })} {/* CTA */} [ s.ctaButton, { backgroundColor: ctaBg }, pressed && { opacity: 0.85, transform: [{ scale: 0.98 }] }, ]} onPress={handleStartProgram} > {ctaLabel} ) } // ─── Styles ────────────────────────────────────────────────────────────────── const s = StyleSheet.create({ container: { flex: 1, }, centered: { alignItems: 'center', justifyContent: 'center', }, scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING, paddingTop: SPACING[2], }, // Title titleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING[3], marginBottom: SPACING[3], }, programIcon: { width: 44, height: 44, borderRadius: RADIUS.MD, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center', }, titleContent: { flex: 1, }, title: { ...TYPOGRAPHY.TITLE_1, }, subtitle: { ...TYPOGRAPHY.SUBHEADLINE, marginTop: 2, }, // Description description: { ...TYPOGRAPHY.BODY, lineHeight: 24, marginBottom: SPACING[5], }, // Card card: { borderRadius: RADIUS.LG, borderCurve: 'continuous', overflow: 'hidden', padding: SPACING[4], }, // Stats statsRow: { flexDirection: 'row', alignItems: 'center', }, statItem: { flex: 1, alignItems: 'center', gap: 2, }, statValue: { ...TYPOGRAPHY.TITLE_1, fontVariant: ['tabular-nums'], }, statLabel: { ...TYPOGRAPHY.CAPTION_2, textTransform: 'uppercase' as const, letterSpacing: 0.5, }, statDivider: { width: StyleSheet.hairlineWidth, height: 32, }, // Tags tagsSection: { marginTop: SPACING[5], marginBottom: SPACING[5], }, tagSectionLabel: { ...TYPOGRAPHY.FOOTNOTE, fontWeight: '600', marginBottom: SPACING[2], }, tagRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING[2], }, tag: { paddingHorizontal: SPACING[3], paddingVertical: SPACING[1], borderRadius: RADIUS.FULL, }, tagText: { ...TYPOGRAPHY.CAPTION_1, }, // Separator separator: { height: StyleSheet.hairlineWidth, marginBottom: SPACING[5], }, // Progress progressHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: SPACING[3], }, progressTrack: { height: 6, borderRadius: 3, overflow: 'hidden', }, progressFill: { height: '100%', borderRadius: 3, }, // Section title sectionTitle: { ...TYPOGRAPHY.TITLE_2, marginBottom: SPACING[4], }, // Week header weekHeader: { marginBottom: SPACING[1], }, weekTitleRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2], }, currentBadge: { paddingHorizontal: SPACING[2], paddingVertical: 2, borderRadius: RADIUS.SM, }, currentBadgeText: { ...TYPOGRAPHY.CAPTION_2, fontWeight: '600', }, // Workout row workoutSep: { height: StyleSheet.hairlineWidth, marginLeft: SPACING[4] + 28, }, workoutRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: SPACING[3], gap: SPACING[3], }, workoutIcon: { width: 28, alignItems: 'center', justifyContent: 'center', }, workoutIndex: { ...TYPOGRAPHY.SUBHEADLINE, fontVariant: ['tabular-nums'], fontWeight: '600', }, workoutInfo: { flex: 1, gap: 2, }, // Bottom bar bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: LAYOUT.SCREEN_PADDING, paddingTop: SPACING[3], }, ctaButton: { height: 54, borderRadius: RADIUS.MD, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', }, ctaText: { ...TYPOGRAPHY.BUTTON_LARGE, }, })