/** * TabataFit Home Screen — Body Zone Workout Programs * Programs organized by Upper Body, Lower Body, Full Body * Dark Medical design system — navy backgrounds, green actions, no glass */ import { View, StyleSheet, ScrollView, Pressable } from 'react-native' import { useRouter } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Icon, type IconName } from '@/src/shared/components/Icon' import { useMemo, useState, useEffect, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useHaptics } from '@/src/shared/hooks' import { useUserStore, useActivityStore, getWeeklyActivity } from '@/src/shared/stores' import { useWorkoutProgramStore } from '@/src/shared/stores' import { StyledText } from '@/src/shared/components/StyledText' import { Mascot } from '@/src/shared/components/Mascot' import { useThemeColors } from '@/src/shared/theme' import { BRAND, GREEN, TEXT, NAVY, BORDER_COLORS } from '@/src/shared/constants/colors' import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' import { fetchAllPrograms, buildWorkoutProgramId } from '@/src/shared/data/workoutPrograms' import type { WorkoutProgram, BodyZone } from '@/src/shared/types/workoutProgram' import { BODY_ZONE_META } from '@/src/shared/types/workoutProgram' // Feature flags — disable incomplete features const FEATURE_FLAGS = { ASSESSMENT_ENABLED: false, // Assessment player not yet implemented } /** Body zone order for display */ const BODY_ZONE_ORDER: BodyZone[] = ['upper-body', 'lower-body', 'full-body'] const AnimatedPressable = Pressable // ═══════════════════════════════════════════════════════════════════════════ // BODY ZONE CARD (clickable, navigates to detail) // ═══════════════════════════════════════════════════════════════════════════ function BodyZoneCard({ bodyZone, programCount, }: { bodyZone: BodyZone programCount: number }) { const router = useRouter() const haptics = useHaptics() const colors = useThemeColors() const meta = BODY_ZONE_META[bodyZone] const handlePress = () => { haptics.buttonTap() router.push(`/workout/body-zone/${bodyZone}` as any) } return ( [ styles.bodyZoneCard, { backgroundColor: colors.surface.default.backgroundColor, borderColor: colors.border.dim, opacity: pressed ? 0.85 : 1, }, ]} testID={`zone-card-${bodyZone}`} > {meta.label} {programCount} programme{programCount !== 1 ? 's' : ''} ) } // ═══════════════════════════════════════════════════════════════════════════ // CONTINUE SESSION CARD — adapted for workout programs // ═══════════════════════════════════════════════════════════════════════════ function ContinueSessionCard({ programs }: { programs: WorkoutProgram[] }) { const { t } = useTranslation('screens') const router = useRouter() const haptics = useHaptics() const colors = useThemeColors() const recommended = useWorkoutProgramStore( useCallback((s) => s.getRecommendedNext(programs), [programs]) ) if (!recommended) return null const zoneMeta = BODY_ZONE_META[recommended.bodyZone] const accentColor = recommended.accentColor ?? zoneMeta.color const handlePress = () => { haptics.buttonTap() router.push(`/workout/${buildWorkoutProgramId(recommended.id)}` as any) } return ( {t('home.recommendedNext')} {recommended.title} {zoneMeta.label} · {recommended.estimatedDuration} min · ~{recommended.estimatedCalories} kcal ) } // ═══════════════════════════════════════════════════════════════════════════ // QUICK STATS ROW // ═══════════════════════════════════════════════════════════════════════════ function QuickStats() { const { t } = useTranslation('screens') const colors = useThemeColors() const streak = useActivityStore((s) => s.streak) const history = useActivityStore((s) => s.history) const weeklyActivity = useMemo(() => getWeeklyActivity(history), [history]) const thisWeekCount = weeklyActivity.filter((d) => d.completed).length const totalMinutes = useMemo(() => history.reduce((sum, r) => sum + r.durationMinutes, 0), [history]) const stats = [ { icon: 'flame.fill' as const, value: streak.current, label: t('home.statsStreak'), color: GREEN['500'] }, { icon: 'calendar' as const, value: `${thisWeekCount}/7`, label: t('home.statsThisWeek'), color: BRAND.INFO }, { icon: 'clock' as const, value: totalMinutes, label: t('home.statsMinutes'), color: GREEN['500'] }, ] return ( {stats.map((stat) => ( {String(stat.value)} {stat.label} ))} ) } // ═══════════════════════════════════════════════════════════════════════════ // KINE LINK CARD (bottom link to physio programs) // ═══════════════════════════════════════════════════════════════════════════ function TabataLinkCard() { const { t } = useTranslation('screens') const router = useRouter() const haptics = useHaptics() const colors = useThemeColors() const handlePress = () => { haptics.buttonTap() router.push('/program/debutant' as any) } return ( [ styles.tabataLinkCard, { backgroundColor: colors.surface.default.backgroundColor, borderColor: colors.border.dim, opacity: pressed ? 0.85 : 1, }, ]} > {t('home.tabataPrograms')} {t('home.tabataProgramsSubtitle')} ) } // ═══════════════════════════════════════════════════════════════════════════ // MAIN SCREEN // ═══════════════════════════════════════════════════════════════════════════ export default function HomeScreen() { const { t } = useTranslation('screens') const insets = useSafeAreaInsets() const router = useRouter() const colors = useThemeColors() const userName = useUserStore((s) => s.profile.name) const streak = useActivityStore((s) => s.streak) // Fetch workout programs const [programs, setPrograms] = useState([]) useEffect(() => { fetchAllPrograms().then(setPrograms) }, []) // Group programs by body zone const programsByZone = useMemo(() => { const grouped: Record = { 'upper-body': [], 'lower-body': [], 'full-body': [], } for (const program of programs) { if (grouped[program.bodyZone]) { grouped[program.bodyZone].push(program) } } return grouped }, [programs]) const greeting = (() => { const hour = new Date().getHours() if (hour < 12) return t('common:greetings.morning') if (hour < 18) return t('common:greetings.afternoon') return t('common:greetings.evening') })() return ( {/* Hero Section */} {greeting} {/* Inline streak badge */} {streak.current > 0 && ( {streak.current} )} {userName} {t('home.programsByZone')} {/* Quick Stats Row */} {/* Continue Session (if in progress) */} {/* Body Zone Cards */} {BODY_ZONE_ORDER.map((zone) => ( ))} {/* Tabata Programs Link */} ) } // ═══════════════════════════════════════════════════════════════════════════ // STYLES // ═══════════════════════════════════════════════════════════════════════════ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: NAVY[900], }, scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING, }, // Hero Section heroSection: { marginTop: SPACING[4], marginBottom: SPACING[7], }, heroRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', }, heroTextContent: { flex: 1, }, heroGreetingRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, streakBadge: { flexDirection: 'row', alignItems: 'center', gap: SPACING[1], paddingHorizontal: SPACING[3], paddingVertical: SPACING[1], borderRadius: RADIUS.PILL, backgroundColor: GREEN.DIM, borderWidth: 1, borderColor: GREEN.BORDER, borderCurve: 'continuous', }, heroName: { marginTop: SPACING[1], }, heroSubtitle: { marginTop: SPACING[2], }, // Quick Stats Row quickStatsRow: { flexDirection: 'row', gap: SPACING[3], marginBottom: SPACING[7], }, quickStatPill: { flex: 1, alignItems: 'center', paddingVertical: SPACING[4], borderRadius: RADIUS.LG, borderWidth: 1, borderColor: BORDER_COLORS.DIM, borderCurve: 'continuous', gap: SPACING[1], backgroundColor: NAVY[800], }, // Continue Session Card continueCard: { borderRadius: RADIUS.XL, marginBottom: SPACING[7], overflow: 'hidden', borderWidth: 1, borderCurve: 'continuous', backgroundColor: NAVY[800], }, continueAccentLine: { height: 3, width: '100%', }, continueContent: { padding: SPACING[5], }, continueHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: SPACING[3], }, // Body Zone Card bodyZoneCard: { borderRadius: RADIUS.XL, borderWidth: 1, borderCurve: 'continuous', marginBottom: SPACING[3], }, bodyZoneCardInner: { flexDirection: 'row', alignItems: 'center', padding: SPACING[4], gap: SPACING[3], }, bodyZoneCardIcon: { width: 44, height: 44, borderRadius: RADIUS.FULL, borderWidth: 1.5, borderCurve: 'continuous', backgroundColor: NAVY[800], alignItems: 'center', justifyContent: 'center', }, bodyZoneCardInfo: { flex: 1, }, // Tabata Link Card tabataLinkCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: SPACING[4], borderRadius: RADIUS.XL, borderWidth: 1, borderCurve: 'continuous', marginBottom: SPACING[6], }, tabataLinkLeft: { flexDirection: 'row', alignItems: 'center', flex: 1, gap: SPACING[3], }, tabataLinkIcon: { width: 44, height: 44, borderRadius: RADIUS.LG, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center', }, tabataLinkText: { flex: 1, }, })