/** * Workout Program Detail Screen * Shows Warmup → 3 Tabatas → Stretch preview, CTA to player. */ import { useEffect, useState } from 'react' import { View, Text, StyleSheet, ScrollView, Pressable, ActivityIndicator } from 'react-native' import { Stack, useRouter, useLocalSearchParams } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useTranslation } from 'react-i18next' import { Icon } from '@/src/shared/components/Icon' import { fetchProgramById, buildWorkoutProgramId } from '@/src/shared/data/workoutPrograms' import type { WorkoutProgram } from '@/src/shared/types/workoutProgram' import { BODY_ZONE_META, LEVEL_META } from '@/src/shared/types/workoutProgram' import { useUserStore } from '@/src/shared/stores/userStore' import { TYPOGRAPHY } from '@/src/shared/constants/typography' import { SPACING } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' import { TEXT, NAVY, GREEN, BORDER_COLORS, DARK } from '@/src/shared/constants/colors' import { withOpacity } from '@/src/shared/utils/color' const FALLBACK_ACCENT = '#FF6B35' export default function WorkoutProgramDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>() const router = useRouter() const insets = useSafeAreaInsets() const { t } = useTranslation() const [program, setProgram] = useState(null) const [loading, setLoading] = useState(true) const isPremium = useUserStore(s => s.profile.subscription) !== 'free' useEffect(() => { let cancelled = false setLoading(true) fetchProgramById(id) .then(p => { if (!cancelled) setProgram(p) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [id]) if (loading) { return ( ) } if (!program) { return ( {t('screens:program.notFound')} ) } const accent = program.accentColor ?? BODY_ZONE_META[program.bodyZone].color ?? FALLBACK_ACCENT const level = LEVEL_META[program.level] const zone = BODY_ZONE_META[program.bodyZone] const canAccess = program.isFree || isPremium const handleStart = () => { if (!canAccess) { router.push('/paywall') return } router.push(`/player/${buildWorkoutProgramId(program.id)}`) } const warmupMinutes = Math.round(program.warmup.totalDuration / 60) const stretchMinutes = Math.round(program.stretch.totalDuration / 60) return ( {/* Hero */} {program.title} {program.description && {program.description}} {level.label} {zone.label} {!program.isFree && ( {t('screens:home.premiumBadge')} )} {/* Warmup */}
{program.warmup.exercises.map((ex, i) => ( ))}
{/* Tabatas */} {program.tabatas.map((tabata, i) => (
))} {/* Stretch */}
{program.stretch.exercises.map((ex, i) => ( ))}
{canAccess ? t('screens:program.startSession') : t('screens:program.unlockPremium')}
) } function Stat({ value, label }: { value: number; label: string }) { return ( {value} {label} ) } function Section({ title, subtitle, accent, children, }: { title: string subtitle: string accent: string children: React.ReactNode }) { return ( {title} {subtitle} {children} ) } function Row({ label, detail }: { label: string; detail: string }) { return ( {label} {detail} ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: NAVY[900] }, center: { alignItems: 'center', justifyContent: 'center' }, scroll: { flex: 1 }, errorText: { color: TEXT.SECONDARY, ...TYPOGRAPHY.BODY }, hero: { padding: SPACING[6], alignItems: 'center' }, iconCircle: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', marginBottom: SPACING[3], }, title: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, textAlign: 'center' }, description: { ...TYPOGRAPHY.BODY, color: TEXT.SECONDARY, textAlign: 'center', marginTop: SPACING[2], lineHeight: 22, }, badgeRow: { flexDirection: 'row', gap: SPACING[2], marginTop: SPACING[3], flexWrap: 'wrap', justifyContent: 'center' }, badge: { paddingHorizontal: SPACING[2], paddingVertical: 3, borderRadius: RADIUS.SM, borderWidth: 1 }, badgeText: { ...TYPOGRAPHY.LABEL }, statsRow: { flexDirection: 'row', marginTop: SPACING[6], gap: SPACING[8] }, statItem: { alignItems: 'center' }, statValue: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY }, statLabel: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 }, section: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] }, sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2], marginBottom: SPACING[3] }, sectionDot: { width: 8, height: 8, borderRadius: 4 }, sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, flex: 1 }, sectionSubtitle: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY }, sectionBody: { backgroundColor: NAVY[800], borderRadius: RADIUS.MD, borderWidth: 1, borderColor: BORDER_COLORS.DIM, overflow: 'hidden', }, row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: SPACING[3], paddingVertical: SPACING[3], borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: BORDER_COLORS.DIM, gap: SPACING[3], }, rowLabel: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY, flex: 1 }, rowDetail: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY }, ctaContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: SPACING[5], paddingTop: SPACING[3], backgroundColor: DARK.SCRIM, borderTopWidth: 1, borderTopColor: BORDER_COLORS.DIM, }, ctaButton: { height: 52, borderRadius: RADIUS.MD, alignItems: 'center', justifyContent: 'center' }, ctaText: { ...TYPOGRAPHY.BUTTON_MEDIUM, color: NAVY[900], letterSpacing: 0.5 }, })