- Replace browse tab with Supabase-connected explore tab with filters - Add React Query for data fetching with loading states - Add 3 structured programs with weekly progression - Add Supabase anonymous auth sync service - Add PostHog analytics with screen tracking and events - Add comprehensive test strategy (Vitest + Maestro E2E) - Add RevenueCat subscription system with DEV simulation - Add i18n translations for new screens (EN/FR/DE/ES) - Add data deletion modal, sync consent modal - Add assessment screen and program routes - Add GitHub Actions CI workflow - Update activity store with sync integration
729 lines
23 KiB
TypeScript
729 lines
23 KiB
TypeScript
/**
|
||
* TabataFit Home Screen - 3 Program Design
|
||
* Premium Apple Fitness+ inspired layout
|
||
*/
|
||
|
||
import { View, StyleSheet, ScrollView, Pressable, Animated } from 'react-native'
|
||
import { useRouter } from 'expo-router'
|
||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||
import { LinearGradient } from 'expo-linear-gradient'
|
||
import { BlurView } from 'expo-blur'
|
||
import Ionicons from '@expo/vector-icons/Ionicons'
|
||
|
||
import { useMemo, useRef, useCallback } from 'react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { useHaptics } from '@/src/shared/hooks'
|
||
import { useUserStore, useProgramStore, useActivityStore, getWeeklyActivity } from '@/src/shared/stores'
|
||
import { PROGRAMS, ASSESSMENT_WORKOUT } from '@/src/shared/data/programs'
|
||
import { StyledText } from '@/src/shared/components/StyledText'
|
||
|
||
import { useThemeColors, BRAND } from '@/src/shared/theme'
|
||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
|
||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||
import type { ProgramId } from '@/src/shared/types'
|
||
|
||
const FONTS = {
|
||
LARGE_TITLE: 34,
|
||
TITLE: 28,
|
||
TITLE_2: 22,
|
||
HEADLINE: 17,
|
||
BODY: 16,
|
||
CAPTION: 13,
|
||
}
|
||
|
||
// Program metadata for display
|
||
const PROGRAM_META: Record<ProgramId, { icon: keyof typeof Ionicons.glyphMap; gradient: [string, string]; accent: string }> = {
|
||
'upper-body': {
|
||
icon: 'barbell-outline',
|
||
gradient: ['#FF6B35', '#FF3B30'],
|
||
accent: '#FF6B35',
|
||
},
|
||
'lower-body': {
|
||
icon: 'footsteps-outline',
|
||
gradient: ['#30D158', '#28A745'],
|
||
accent: '#30D158',
|
||
},
|
||
'full-body': {
|
||
icon: 'flame-outline',
|
||
gradient: ['#5AC8FA', '#007AFF'],
|
||
accent: '#5AC8FA',
|
||
},
|
||
}
|
||
|
||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// PROGRAM CARD
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function ProgramCard({
|
||
programId,
|
||
onPress,
|
||
}: {
|
||
programId: ProgramId
|
||
onPress: () => void
|
||
}) {
|
||
const { t } = useTranslation('screens')
|
||
const haptics = useHaptics()
|
||
const colors = useThemeColors()
|
||
const styles = useMemo(() => createStyles(colors), [colors])
|
||
const program = PROGRAMS[programId]
|
||
const meta = PROGRAM_META[programId]
|
||
const programStatus = useProgramStore((s) => s.getProgramStatus(programId))
|
||
const completion = useProgramStore((s) => s.getProgramCompletion(programId))
|
||
|
||
// Press animation
|
||
const scaleValue = useRef(new Animated.Value(1)).current
|
||
const handlePressIn = useCallback(() => {
|
||
Animated.spring(scaleValue, {
|
||
toValue: 0.97,
|
||
useNativeDriver: true,
|
||
speed: 50,
|
||
bounciness: 4,
|
||
}).start()
|
||
}, [scaleValue])
|
||
const handlePressOut = useCallback(() => {
|
||
Animated.spring(scaleValue, {
|
||
toValue: 1,
|
||
useNativeDriver: true,
|
||
speed: 30,
|
||
bounciness: 6,
|
||
}).start()
|
||
}, [scaleValue])
|
||
|
||
const statusText = {
|
||
'not-started': t('programs.status.notStarted'),
|
||
'in-progress': `${completion}% ${t('programs.status.complete')}`,
|
||
'completed': t('programs.status.completed'),
|
||
}[programStatus]
|
||
|
||
const handlePress = () => {
|
||
haptics.buttonTap()
|
||
onPress()
|
||
}
|
||
|
||
return (
|
||
<View
|
||
style={styles.programCard}
|
||
testID={`program-card-${programId}`}
|
||
>
|
||
{/* Glass Background */}
|
||
<BlurView
|
||
intensity={colors.glass.blurMedium}
|
||
tint={colors.glass.blurTint}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
|
||
{/* Color Gradient Overlay */}
|
||
<LinearGradient
|
||
colors={[meta.gradient[0] + '40', meta.gradient[1] + '18', 'transparent']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
|
||
{/* Top Accent Line */}
|
||
<LinearGradient
|
||
colors={meta.gradient}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 0 }}
|
||
style={styles.accentLine}
|
||
/>
|
||
|
||
<View style={styles.programCardContent}>
|
||
{/* Icon + Title Row */}
|
||
<View style={styles.programCardHeader}>
|
||
{/* Gradient Icon Circle */}
|
||
<View style={styles.programIconWrapper}>
|
||
<LinearGradient
|
||
colors={meta.gradient}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={styles.programIconGradient}
|
||
/>
|
||
<View style={styles.programIconInner}>
|
||
<Ionicons name={meta.icon} size={24} color="#FFFFFF" />
|
||
</View>
|
||
</View>
|
||
<View style={styles.programHeaderText}>
|
||
<View style={styles.programTitleRow}>
|
||
<StyledText size={FONTS.HEADLINE} weight="bold" color={colors.text.primary} style={{ flex: 1 }}>
|
||
{t(`content:programs.${program.id}.title`)}
|
||
</StyledText>
|
||
{programStatus !== 'not-started' && (
|
||
<View style={[styles.statusBadge, { backgroundColor: meta.accent + '20', borderColor: meta.accent + '35' }]}>
|
||
<StyledText size={11} weight="semibold" color={meta.accent}>
|
||
{statusText}
|
||
</StyledText>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<StyledText size={FONTS.CAPTION} color={colors.text.secondary} numberOfLines={2} style={{ lineHeight: 18 }}>
|
||
{t(`content:programs.${program.id}.description`)}
|
||
</StyledText>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Progress Bar (if started) */}
|
||
{programStatus !== 'not-started' && (
|
||
<View style={styles.progressContainer}>
|
||
<View style={styles.progressBar}>
|
||
<View style={styles.progressFillWrapper}>
|
||
<LinearGradient
|
||
colors={meta.gradient}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 0 }}
|
||
style={[
|
||
styles.progressFill,
|
||
{ width: `${Math.max(completion, 2)}%` },
|
||
]}
|
||
/>
|
||
</View>
|
||
</View>
|
||
<StyledText size={11} color={colors.text.tertiary}>
|
||
{programStatus === 'completed'
|
||
? t('programs.allWorkoutsComplete')
|
||
: `${completion}% ${t('programs.complete')}`
|
||
}
|
||
</StyledText>
|
||
</View>
|
||
)}
|
||
|
||
{/* Stats — inline text, not chips */}
|
||
<StyledText size={12} color={colors.text.tertiary} style={styles.programMeta}>
|
||
{program.durationWeeks} {t('programs.weeks')} · {program.workoutsPerWeek}×{t('programs.perWeek')} · {program.totalWorkouts} {t('programs.workouts')}
|
||
</StyledText>
|
||
|
||
{/* Premium CTA Button — only interactive element */}
|
||
<AnimatedPressable
|
||
style={[
|
||
styles.ctaButtonWrapper,
|
||
{ transform: [{ scale: scaleValue }] },
|
||
]}
|
||
onPress={handlePress}
|
||
onPressIn={handlePressIn}
|
||
onPressOut={handlePressOut}
|
||
testID={`program-${programId}-cta`}
|
||
>
|
||
<LinearGradient
|
||
colors={meta.gradient}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 0 }}
|
||
style={styles.ctaButton}
|
||
>
|
||
<StyledText size={15} weight="semibold" color="#FFFFFF">
|
||
{programStatus === 'not-started'
|
||
? t('programs.startProgram')
|
||
: programStatus === 'completed'
|
||
? t('programs.restart')
|
||
: t('programs.continue')
|
||
}
|
||
</StyledText>
|
||
<Ionicons
|
||
name={programStatus === 'completed' ? 'refresh' : 'arrow-forward'}
|
||
size={17}
|
||
color="#FFFFFF"
|
||
style={styles.ctaIcon}
|
||
/>
|
||
</LinearGradient>
|
||
</AnimatedPressable>
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// QUICK STATS ROW
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function QuickStats() {
|
||
const { t } = useTranslation('screens')
|
||
const colors = useThemeColors()
|
||
const styles = useMemo(() => createStyles(colors), [colors])
|
||
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' as const, value: streak.current, label: t('home.statsStreak'), color: BRAND.PRIMARY },
|
||
{ icon: 'calendar-outline' as const, value: `${thisWeekCount}/7`, label: t('home.statsThisWeek'), color: '#5AC8FA' },
|
||
{ icon: 'time-outline' as const, value: totalMinutes, label: t('home.statsMinutes'), color: '#30D158' },
|
||
]
|
||
|
||
return (
|
||
<View style={styles.quickStatsRow}>
|
||
{stats.map((stat) => (
|
||
<View key={stat.label} style={styles.quickStatPill}>
|
||
<BlurView
|
||
intensity={colors.glass.blurLight}
|
||
tint={colors.glass.blurTint}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
<Ionicons name={stat.icon} size={16} color={stat.color} />
|
||
<StyledText size={17} weight="bold" color={colors.text.primary} style={{ fontVariant: ['tabular-nums'] }}>
|
||
{String(stat.value)}
|
||
</StyledText>
|
||
<StyledText size={11} color={colors.text.tertiary}>
|
||
{stat.label}
|
||
</StyledText>
|
||
</View>
|
||
))}
|
||
</View>
|
||
)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// ASSESSMENT CARD
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function AssessmentCard({ onPress }: { onPress: () => void }) {
|
||
const { t } = useTranslation('screens')
|
||
const haptics = useHaptics()
|
||
const colors = useThemeColors()
|
||
const styles = useMemo(() => createStyles(colors), [colors])
|
||
const isCompleted = useProgramStore((s) => s.assessment.isCompleted)
|
||
|
||
if (isCompleted) return null
|
||
|
||
const handlePress = () => {
|
||
haptics.buttonTap()
|
||
onPress()
|
||
}
|
||
|
||
return (
|
||
<Pressable
|
||
style={styles.assessmentCard}
|
||
onPress={handlePress}
|
||
testID="assessment-card"
|
||
>
|
||
{/* Glass Background */}
|
||
<BlurView
|
||
intensity={colors.glass.blurMedium}
|
||
tint={colors.glass.blurTint}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
{/* Subtle brand gradient overlay */}
|
||
<LinearGradient
|
||
colors={[`${BRAND.PRIMARY}18`, 'transparent']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
|
||
<View style={styles.assessmentContent}>
|
||
{/* Gradient Icon Circle */}
|
||
<View style={styles.assessmentIconCircle}>
|
||
<LinearGradient
|
||
colors={[BRAND.PRIMARY, '#FF3B30']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
<View style={styles.assessmentIconInner}>
|
||
<Ionicons name="clipboard-outline" size={22} color="#FFFFFF" />
|
||
</View>
|
||
</View>
|
||
<View style={styles.assessmentText}>
|
||
<StyledText size={FONTS.HEADLINE} weight="semibold" color={colors.text.primary}>
|
||
{t('assessment.title')}
|
||
</StyledText>
|
||
<StyledText size={FONTS.CAPTION} color={colors.text.secondary} style={{ marginTop: 2 }}>
|
||
{ASSESSMENT_WORKOUT.duration} {t('assessment.duration')} · {ASSESSMENT_WORKOUT.exercises.length} {t('assessment.movements')}
|
||
</StyledText>
|
||
</View>
|
||
<View style={styles.assessmentArrow}>
|
||
<Ionicons name="arrow-forward" size={16} color={BRAND.PRIMARY} />
|
||
</View>
|
||
</View>
|
||
</Pressable>
|
||
)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// MAIN SCREEN
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
export default function HomeScreen() {
|
||
const { t } = useTranslation('screens')
|
||
const insets = useSafeAreaInsets()
|
||
const router = useRouter()
|
||
const colors = useThemeColors()
|
||
const styles = useMemo(() => createStyles(colors), [colors])
|
||
const haptics = useHaptics()
|
||
const userName = useUserStore((s) => s.profile.name)
|
||
const selectedProgram = useProgramStore((s) => s.selectedProgramId)
|
||
const changeProgram = useProgramStore((s) => s.changeProgram)
|
||
const streak = useActivityStore((s) => s.streak)
|
||
|
||
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')
|
||
})()
|
||
|
||
const handleProgramPress = (programId: ProgramId) => {
|
||
// Navigate to program detail
|
||
router.push(`/program/${programId}` as any)
|
||
}
|
||
|
||
const handleAssessmentPress = () => {
|
||
router.push('/assessment' as any)
|
||
}
|
||
|
||
const handleSwitchProgram = () => {
|
||
haptics.buttonTap()
|
||
changeProgram(null as any)
|
||
}
|
||
|
||
const programOrder: ProgramId[] = ['upper-body', 'lower-body', 'full-body']
|
||
|
||
return (
|
||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||
{/* Ambient gradient glow at top */}
|
||
<LinearGradient
|
||
colors={['rgba(255, 107, 53, 0.06)', 'rgba(255, 107, 53, 0.02)', 'transparent']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0.5, y: 1 }}
|
||
style={styles.ambientGlow}
|
||
/>
|
||
|
||
<ScrollView
|
||
style={styles.scrollView}
|
||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* Hero Section */}
|
||
<View style={styles.heroSection}>
|
||
<View style={styles.heroGreetingRow}>
|
||
<StyledText size={FONTS.BODY} color={colors.text.tertiary}>
|
||
{greeting}
|
||
</StyledText>
|
||
{/* Inline streak badge */}
|
||
{streak.current > 0 && (
|
||
<View style={styles.streakBadge}>
|
||
<Ionicons name="flame" size={13} color={BRAND.PRIMARY} />
|
||
<StyledText size={12} weight="bold" color={BRAND.PRIMARY} style={{ fontVariant: ['tabular-nums'] }}>
|
||
{streak.current}
|
||
</StyledText>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<StyledText size={FONTS.LARGE_TITLE} weight="bold" color={colors.text.primary} style={styles.heroName}>
|
||
{userName}
|
||
</StyledText>
|
||
<StyledText size={FONTS.CAPTION} color={colors.text.secondary} style={styles.heroSubtitle}>
|
||
{selectedProgram
|
||
? t('home.continueYourJourney')
|
||
: t('home.chooseYourPath')
|
||
}
|
||
</StyledText>
|
||
</View>
|
||
|
||
{/* Quick Stats Row */}
|
||
<QuickStats />
|
||
|
||
{/* Assessment Card (if not completed) */}
|
||
<AssessmentCard onPress={handleAssessmentPress} />
|
||
|
||
{/* Program Cards */}
|
||
<View style={styles.programsSection}>
|
||
<View style={styles.sectionHeader}>
|
||
<StyledText size={FONTS.TITLE} weight="bold" color={colors.text.primary}>
|
||
{t('home.yourPrograms')}
|
||
</StyledText>
|
||
<StyledText size={FONTS.CAPTION} color={colors.text.tertiary} style={styles.sectionSubtitle}>
|
||
{t('home.programsSubtitle')}
|
||
</StyledText>
|
||
</View>
|
||
|
||
{programOrder.map((programId) => (
|
||
<ProgramCard
|
||
key={programId}
|
||
programId={programId}
|
||
onPress={() => handleProgramPress(programId)}
|
||
/>
|
||
))}
|
||
</View>
|
||
|
||
{/* Switch Program Option (if has progress) */}
|
||
{selectedProgram && (
|
||
<Pressable
|
||
style={styles.switchProgramButton}
|
||
onPress={handleSwitchProgram}
|
||
>
|
||
<BlurView
|
||
intensity={colors.glass.blurLight}
|
||
tint={colors.glass.blurTint}
|
||
style={StyleSheet.absoluteFill}
|
||
/>
|
||
<Ionicons name="shuffle-outline" size={16} color={colors.text.secondary} />
|
||
<StyledText size={14} weight="medium" color={colors.text.secondary}>
|
||
{t('home.switchProgram')}
|
||
</StyledText>
|
||
</Pressable>
|
||
)}
|
||
</ScrollView>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// STYLES
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function createStyles(colors: ThemeColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: colors.bg.base,
|
||
},
|
||
scrollView: {
|
||
flex: 1,
|
||
},
|
||
scrollContent: {
|
||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||
},
|
||
|
||
// Ambient gradient glow
|
||
ambientGlow: {
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
width: 300,
|
||
height: 300,
|
||
borderRadius: 150,
|
||
},
|
||
|
||
// Hero Section
|
||
heroSection: {
|
||
marginTop: SPACING[4],
|
||
marginBottom: SPACING[7],
|
||
},
|
||
heroGreetingRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
streakBadge: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: SPACING[1],
|
||
paddingHorizontal: SPACING[3],
|
||
paddingVertical: SPACING[1],
|
||
borderRadius: RADIUS.FULL,
|
||
backgroundColor: `${BRAND.PRIMARY}15`,
|
||
borderWidth: 1,
|
||
borderColor: `${BRAND.PRIMARY}30`,
|
||
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.GLASS_CARD,
|
||
overflow: 'hidden',
|
||
borderWidth: 1,
|
||
borderColor: colors.border.glass,
|
||
borderCurve: 'continuous',
|
||
gap: SPACING[1],
|
||
backgroundColor: colors.glass.base.backgroundColor,
|
||
},
|
||
|
||
// Assessment Card
|
||
assessmentCard: {
|
||
borderRadius: RADIUS.GLASS_CARD,
|
||
overflow: 'hidden',
|
||
padding: SPACING[5],
|
||
marginBottom: SPACING[8],
|
||
borderWidth: 1,
|
||
borderColor: colors.border.glassStrong,
|
||
borderCurve: 'continuous',
|
||
backgroundColor: colors.glass.base.backgroundColor,
|
||
},
|
||
assessmentContent: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
assessmentIconCircle: {
|
||
width: 44,
|
||
height: 44,
|
||
borderRadius: 22,
|
||
overflow: 'hidden',
|
||
borderCurve: 'continuous',
|
||
marginRight: SPACING[4],
|
||
},
|
||
assessmentIconInner: {
|
||
...StyleSheet.absoluteFillObject,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
assessmentText: {
|
||
flex: 1,
|
||
},
|
||
assessmentArrow: {
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 16,
|
||
backgroundColor: `${BRAND.PRIMARY}18`,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
borderCurve: 'continuous',
|
||
},
|
||
|
||
// Programs Section
|
||
programsSection: {
|
||
marginTop: SPACING[2],
|
||
},
|
||
sectionHeader: {
|
||
marginBottom: SPACING[6],
|
||
},
|
||
sectionSubtitle: {
|
||
marginTop: SPACING[1],
|
||
},
|
||
|
||
// Program Card
|
||
programCard: {
|
||
borderRadius: RADIUS.XL,
|
||
marginBottom: SPACING[6],
|
||
overflow: 'hidden',
|
||
borderWidth: 1,
|
||
borderColor: colors.border.glassStrong,
|
||
borderCurve: 'continuous',
|
||
backgroundColor: colors.glass.base.backgroundColor,
|
||
},
|
||
accentLine: {
|
||
height: 2,
|
||
width: '100%',
|
||
},
|
||
programCardContent: {
|
||
padding: SPACING[5],
|
||
paddingRight: SPACING[6],
|
||
},
|
||
programCardHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-start',
|
||
gap: SPACING[4],
|
||
marginBottom: SPACING[4],
|
||
},
|
||
// Gradient icon circle
|
||
programIconWrapper: {
|
||
width: 48,
|
||
height: 48,
|
||
borderRadius: 24,
|
||
overflow: 'hidden',
|
||
borderCurve: 'continuous',
|
||
},
|
||
programIconGradient: {
|
||
...StyleSheet.absoluteFillObject,
|
||
},
|
||
programIconInner: {
|
||
...StyleSheet.absoluteFillObject,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
programHeaderText: {
|
||
flex: 1,
|
||
paddingBottom: SPACING[1],
|
||
},
|
||
programTitleRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: SPACING[2],
|
||
marginBottom: SPACING[1],
|
||
},
|
||
statusBadge: {
|
||
paddingHorizontal: SPACING[2],
|
||
paddingVertical: 2,
|
||
borderRadius: RADIUS.FULL,
|
||
borderWidth: 1,
|
||
},
|
||
programTitle: {
|
||
marginBottom: SPACING[1],
|
||
},
|
||
programDescription: {
|
||
marginBottom: SPACING[4],
|
||
lineHeight: 20,
|
||
},
|
||
|
||
// Progress
|
||
progressContainer: {
|
||
marginBottom: SPACING[4],
|
||
},
|
||
progressBar: {
|
||
height: 8,
|
||
borderRadius: 4,
|
||
marginBottom: SPACING[2],
|
||
overflow: 'hidden',
|
||
backgroundColor: colors.glass.inset.backgroundColor,
|
||
borderCurve: 'continuous',
|
||
},
|
||
progressFillWrapper: {
|
||
flex: 1,
|
||
},
|
||
progressFill: {
|
||
height: '100%',
|
||
borderRadius: 4,
|
||
borderCurve: 'continuous',
|
||
},
|
||
|
||
// Stats as inline meta text
|
||
programMeta: {
|
||
marginBottom: SPACING[4],
|
||
},
|
||
|
||
// Premium CTA Button
|
||
ctaButtonWrapper: {
|
||
borderRadius: RADIUS.LG,
|
||
overflow: 'hidden',
|
||
borderCurve: 'continuous',
|
||
},
|
||
ctaButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingVertical: SPACING[4],
|
||
paddingHorizontal: SPACING[5],
|
||
borderRadius: RADIUS.LG,
|
||
borderCurve: 'continuous',
|
||
},
|
||
ctaIcon: {
|
||
marginLeft: SPACING[2],
|
||
},
|
||
|
||
// Switch Program — glass pill
|
||
switchProgramButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
alignSelf: 'center',
|
||
gap: SPACING[2],
|
||
paddingVertical: SPACING[3],
|
||
paddingHorizontal: SPACING[6],
|
||
marginTop: SPACING[2],
|
||
borderRadius: RADIUS.FULL,
|
||
borderWidth: 1,
|
||
borderColor: colors.border.glass,
|
||
borderCurve: 'continuous',
|
||
overflow: 'hidden',
|
||
backgroundColor: colors.glass.base.backgroundColor,
|
||
},
|
||
})
|
||
}
|