refactor screens, navigation & player for new architecture
Simplify Home, Activity, Profile, Complete, Player, and Program screens to work with the new Supabase-driven data layer. Update root and tab layouts. Add Settings, Terms, and Zone routes. Add OfflineBanner component and progressStore. Update all player sub-components to use the refreshed design system tokens.
This commit is contained in:
@@ -1,204 +1,43 @@
|
||||
/**
|
||||
* TabataFit Workout Complete Screen
|
||||
* Celebration with real data from activity store
|
||||
* Dark Medical design system — navy, green, no glass
|
||||
* Celebration + stats driven by progressStore.
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text as RNText,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
Animated,
|
||||
Dimensions,
|
||||
} from 'react-native'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { Icon, type IconName } from '@/src/shared/components/Icon'
|
||||
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
|
||||
import { useHaptics } from '@/src/shared/hooks'
|
||||
import { useActivityStore, useUserStore } from '@/src/shared/stores'
|
||||
import { getWorkoutById, getPopularWorkouts, getTrainerById, getWorkoutAccentColor } from '@/src/shared/data'
|
||||
import { useTranslatedWorkout, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData'
|
||||
import { SyncConsentModal } from '@/src/shared/components/SyncConsentModal'
|
||||
import { useProgressStore } from '@/src/shared/stores'
|
||||
import { NativeButton } from '@/src/shared/components/native'
|
||||
import { enableSync } from '@/src/shared/services/sync'
|
||||
import type { WorkoutSessionData } from '@/src/shared/types'
|
||||
|
||||
import { useThemeColors } from '@/src/shared/theme'
|
||||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||||
import { TYPOGRAPHY, FONT_FAMILY } from '@/src/shared/constants/typography'
|
||||
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
|
||||
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
|
||||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||||
import { SPRING, EASE } from '@/src/shared/constants/animations'
|
||||
import { SPRING } from '@/src/shared/constants/animations'
|
||||
import { GREEN, NAVY, TEXT, BORDER_COLORS } from '@/src/shared/constants/colors'
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window')
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// BUTTON COMPONENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function SecondaryButton({
|
||||
onPress,
|
||||
children,
|
||||
icon,
|
||||
}: {
|
||||
onPress: () => void
|
||||
children: React.ReactNode
|
||||
icon?: IconName
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current
|
||||
|
||||
const handlePressIn = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 0.97,
|
||||
useNativeDriver: true,
|
||||
...SPRING.SNAPPY,
|
||||
}).start()
|
||||
}
|
||||
|
||||
const handlePressOut = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
...SPRING.SNAPPY,
|
||||
}).start()
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Animated.View style={[styles.secondaryButton, { transform: [{ scale: scaleAnim }] }]}>
|
||||
{icon && <Icon name={icon} size={18} tintColor={TEXT.PRIMARY} style={styles.buttonIcon} />}
|
||||
<RNText style={styles.secondaryButtonText}>{children}</RNText>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
function PrimaryButton({
|
||||
onPress,
|
||||
children,
|
||||
}: {
|
||||
onPress: () => void
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current
|
||||
|
||||
const handlePressIn = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 0.97,
|
||||
useNativeDriver: true,
|
||||
...SPRING.SNAPPY,
|
||||
}).start()
|
||||
}
|
||||
|
||||
const handlePressOut = () => {
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
...SPRING.SNAPPY,
|
||||
}).start()
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.primaryButton,
|
||||
{ backgroundColor: GREEN['500'], transform: [{ scale: scaleAnim }] },
|
||||
]}
|
||||
>
|
||||
<RNText style={[styles.primaryButtonText, { color: NAVY['900'] }]}>
|
||||
{children}
|
||||
</RNText>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COMPONENTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function CelebrationRings() {
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const ring1Anim = useRef(new Animated.Value(0)).current
|
||||
const ring2Anim = useRef(new Animated.Value(0)).current
|
||||
const ring3Anim = useRef(new Animated.Value(0)).current
|
||||
|
||||
useEffect(() => {
|
||||
Animated.stagger(200, [
|
||||
Animated.spring(ring1Anim, {
|
||||
toValue: 1,
|
||||
...SPRING.BOUNCY,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(ring2Anim, {
|
||||
toValue: 1,
|
||||
...SPRING.BOUNCY,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(ring3Anim, {
|
||||
toValue: 1,
|
||||
...SPRING.BOUNCY,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={styles.ringsContainer}>
|
||||
<Animated.View
|
||||
style={[styles.ring, styles.ring1, { transform: [{ scale: ring1Anim }], opacity: ring1Anim }]}
|
||||
>
|
||||
<RNText style={styles.ringEmoji}>🔥</RNText>
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={[styles.ring, styles.ring2, { transform: [{ scale: ring2Anim }], opacity: ring2Anim }]}
|
||||
>
|
||||
<RNText style={styles.ringEmoji}>💪</RNText>
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={[styles.ring, styles.ring3, { transform: [{ scale: ring3Anim }], opacity: ring3Anim }]}
|
||||
>
|
||||
<RNText style={styles.ringEmoji}>⚡</RNText>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
value,
|
||||
label,
|
||||
icon,
|
||||
accentColor,
|
||||
delay = 0,
|
||||
}: {
|
||||
value: string | number
|
||||
label: string
|
||||
icon: IconName
|
||||
accentColor: string
|
||||
delay?: number
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
@@ -208,58 +47,19 @@ function StatCard({
|
||||
useEffect(() => {
|
||||
Animated.sequence([
|
||||
Animated.delay(delay),
|
||||
Animated.spring(scaleAnim, {
|
||||
toValue: 1,
|
||||
...SPRING.BOUNCY,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(scaleAnim, { toValue: 1, ...SPRING.BOUNCY, useNativeDriver: true }),
|
||||
]).start()
|
||||
}, [delay])
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.statCard, { transform: [{ scale: scaleAnim }] }]}>
|
||||
<Icon name={icon} size={24} tintColor={GREEN['500']} />
|
||||
<RNText style={styles.statValue}>{value}</RNText>
|
||||
<RNText selectable style={styles.statValue}>{value}</RNText>
|
||||
<RNText style={styles.statLabel}>{label}</RNText>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
function BurnBarResult({ percentile, accentColor }: { percentile: number; accentColor: string }) {
|
||||
const { t } = useTranslation()
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const barAnim = useRef(new Animated.Value(0)).current
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(barAnim, {
|
||||
toValue: percentile,
|
||||
duration: 1000,
|
||||
easing: EASE.EASE_OUT,
|
||||
useNativeDriver: false,
|
||||
}).start()
|
||||
}, [percentile])
|
||||
|
||||
const barWidth = barAnim.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: ['0%', '100%'],
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={styles.burnBarContainer}>
|
||||
<RNText style={styles.burnBarTitle}>{t('screens:complete.burnBar')}</RNText>
|
||||
<RNText style={[styles.burnBarResult, { color: GREEN['500'] }]}>{t('screens:complete.burnBarResult', { percentile })}</RNText>
|
||||
<View style={styles.burnBarTrack}>
|
||||
<Animated.View style={[styles.burnBarFill, { width: barWidth, backgroundColor: GREEN['500'] }]} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// MAIN SCREEN
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export default function WorkoutCompleteScreen() {
|
||||
const insets = useSafeAreaInsets()
|
||||
const router = useRouter()
|
||||
@@ -270,30 +70,17 @@ export default function WorkoutCompleteScreen() {
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
|
||||
const rawWorkout = getWorkoutById(id ?? '1')
|
||||
const workout = useTranslatedWorkout(rawWorkout)
|
||||
const trainer = rawWorkout ? getTrainerById(rawWorkout.trainerId) : undefined
|
||||
const trainerColor = getWorkoutAccentColor(id ?? '1')
|
||||
const streak = useActivityStore((s) => s.streak)
|
||||
const history = useActivityStore((s) => s.history)
|
||||
const recentWorkouts = history.slice(0, 1)
|
||||
const history = useProgressStore((s) => s.history)
|
||||
const streak = useProgressStore((s) => s.streak)
|
||||
const weeklyCount = useProgressStore((s) => s.getWeeklyCount())
|
||||
|
||||
// Sync consent modal state
|
||||
const [showSyncPrompt, setShowSyncPrompt] = useState(false)
|
||||
const { profile, setSyncStatus } = useUserStore()
|
||||
|
||||
// Get the most recent result for this workout
|
||||
const latestResult = recentWorkouts[0]
|
||||
const resultCalories = latestResult?.calories ?? workout?.calories ?? 45
|
||||
const resultMinutes = latestResult?.durationMinutes ?? workout?.duration ?? 4
|
||||
|
||||
// Recommended workouts (different from current)
|
||||
const rawRecommended = getPopularWorkouts(4).filter(w => w.id !== id).slice(0, 3)
|
||||
const recommended = useTranslatedWorkouts(rawRecommended)
|
||||
// Latest session (the one we just completed)
|
||||
const latest = history[0]
|
||||
const resultMinutes = latest ? Math.round(latest.durationSeconds / 60) : 0
|
||||
|
||||
const handleGoHome = () => {
|
||||
haptics.buttonTap()
|
||||
router.replace('/(tabs)')
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
const handleShare = async () => {
|
||||
@@ -301,96 +88,35 @@ export default function WorkoutCompleteScreen() {
|
||||
const isAvailable = await Sharing.isAvailableAsync()
|
||||
if (isAvailable) {
|
||||
await Sharing.shareAsync('https://tabatafit.app', {
|
||||
dialogTitle: t('screens:complete.shareText', { title: workout?.title ?? 'a workout', calories: resultCalories, duration: resultMinutes }),
|
||||
dialogTitle: t('screens:complete.shareTitle', { minutes: resultMinutes }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleWorkoutPress = (workoutId: string) => {
|
||||
haptics.buttonTap()
|
||||
router.push(`/workout/${workoutId}`)
|
||||
}
|
||||
|
||||
// Fire celebration haptic on mount
|
||||
useEffect(() => {
|
||||
haptics.workoutComplete()
|
||||
}, [])
|
||||
|
||||
// Check if we should show sync prompt (after first workout for premium users)
|
||||
useEffect(() => {
|
||||
if (profile.syncStatus === 'prompt-pending') {
|
||||
// Wait a moment for the user to see their results first
|
||||
const timer = setTimeout(() => {
|
||||
setShowSyncPrompt(true)
|
||||
}, 1500)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [profile.syncStatus])
|
||||
|
||||
const handleSyncAccept = async () => {
|
||||
setShowSyncPrompt(false)
|
||||
|
||||
// Prepare data for sync
|
||||
const profileData = {
|
||||
name: profile.name,
|
||||
fitnessLevel: profile.fitnessLevel,
|
||||
goal: profile.goal,
|
||||
weeklyFrequency: profile.weeklyFrequency,
|
||||
barriers: profile.barriers,
|
||||
onboardingCompletedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
// Get all workout history for retroactive sync
|
||||
const workoutHistory: WorkoutSessionData[] = history.map((w) => ({
|
||||
workoutId: w.workoutId,
|
||||
completedAt: new Date(w.completedAt).toISOString(),
|
||||
durationSeconds: w.durationMinutes * 60,
|
||||
caloriesBurned: w.calories,
|
||||
}))
|
||||
|
||||
// Enable sync
|
||||
const result = await enableSync(profileData, workoutHistory)
|
||||
|
||||
if (result.success) {
|
||||
setSyncStatus('synced', result.userId || null)
|
||||
} else {
|
||||
// Show error - sync failed
|
||||
setSyncStatus('never-synced')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSyncDecline = () => {
|
||||
setShowSyncPrompt(false)
|
||||
setSyncStatus('never-synced') // Reset so we don't ask again
|
||||
}
|
||||
|
||||
// Simulate percentile
|
||||
const burnBarPercentile = Math.min(95, Math.max(40, Math.round((resultCalories / (workout?.calories ?? 45)) * 70)))
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
|
||||
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 120 }]}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Celebration */}
|
||||
<View style={styles.celebrationSection}>
|
||||
<RNText style={styles.celebrationEmoji}>🎉</RNText>
|
||||
<RNText style={styles.celebrationTitle}>{t('screens:complete.title')}</RNText>
|
||||
<CelebrationRings />
|
||||
</View>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<View style={styles.statsGrid}>
|
||||
<StatCard value={resultCalories} label={t('screens:complete.caloriesLabel')} icon="flame.fill" accentColor={GREEN['500']} delay={100} />
|
||||
<StatCard value={resultMinutes} label={t('screens:complete.minutesLabel')} icon="clock.fill" accentColor={GREEN['500']} delay={200} />
|
||||
<StatCard value="100%" label={t('screens:complete.completeLabel')} icon="checkmark.circle.fill" accentColor={GREEN['500']} delay={300} />
|
||||
<StatCard value={resultMinutes} label={t('screens:complete.minutesLabel')} icon="clock.fill" delay={100} />
|
||||
<StatCard value={streak.current} label={t('screens:home.statsStreak')} icon="flame.fill" delay={200} />
|
||||
<StatCard value={weeklyCount} label={t('screens:complete.thisWeek')} icon="calendar" delay={300} />
|
||||
</View>
|
||||
|
||||
{/* Burn Bar */}
|
||||
<BurnBarResult percentile={burnBarPercentile} accentColor={GREEN['500']} />
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Streak */}
|
||||
@@ -399,45 +125,27 @@ export default function WorkoutCompleteScreen() {
|
||||
<Icon name="flame.fill" size={32} tintColor={GREEN['500']} />
|
||||
</View>
|
||||
<View style={styles.streakInfo}>
|
||||
<RNText style={styles.streakTitle}>{t('screens:complete.streakTitle', { count: streak.current })}</RNText>
|
||||
<RNText style={styles.streakSubtitle}>{t('screens:complete.streakSubtitle')}</RNText>
|
||||
<RNText selectable style={styles.streakTitle}>
|
||||
{t('screens:complete.streakDays', { count: streak.current })}
|
||||
</RNText>
|
||||
<RNText style={styles.streakSubtitle}>
|
||||
{t('screens:complete.streakRecord', { count: streak.longest })}
|
||||
</RNText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Share Button */}
|
||||
{/* Share */}
|
||||
<View style={styles.shareSection}>
|
||||
<NativeButton
|
||||
variant="ghost"
|
||||
title={t('screens:complete.shareWorkout')}
|
||||
title={t('screens:complete.share')}
|
||||
systemImage="square.and.arrow.up"
|
||||
onPress={handleShare}
|
||||
fullWidth
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Recommended */}
|
||||
<View style={styles.recommendedSection}>
|
||||
<RNText style={styles.recommendedTitle}>{t('screens:complete.recommendedNext')}</RNText>
|
||||
<View style={styles.recommendedGrid}>
|
||||
{recommended.map((w) => (
|
||||
<Pressable
|
||||
key={w.id}
|
||||
onPress={() => handleWorkoutPress(w.id)}
|
||||
style={styles.recommendedCard}
|
||||
>
|
||||
<View style={[styles.recommendedThumb, { backgroundColor: GREEN.DIM }]}>
|
||||
<Icon name="flame.fill" size={24} tintColor={GREEN['500']} />
|
||||
</View>
|
||||
<RNText style={styles.recommendedTitleText} numberOfLines={1}>{w.title}</RNText>
|
||||
<RNText style={styles.recommendedDurationText}>{t('units.minUnit', { count: w.duration })}</RNText>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Fixed Bottom Button */}
|
||||
@@ -452,260 +160,52 @@ export default function WorkoutCompleteScreen() {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Sync Consent Modal */}
|
||||
<SyncConsentModal
|
||||
visible={showSyncPrompt}
|
||||
onAccept={handleSyncAccept}
|
||||
onDecline={handleSyncDecline}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// STYLES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function createStyles(colors: ThemeColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bg.base,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||||
},
|
||||
container: { flex: 1, backgroundColor: colors.bg.base },
|
||||
scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING },
|
||||
|
||||
// Buttons
|
||||
secondaryButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: SPACING[3],
|
||||
paddingHorizontal: SPACING[4],
|
||||
borderRadius: RADIUS.MD,
|
||||
borderWidth: 1,
|
||||
borderColor: BORDER_COLORS.DIM,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
secondaryButtonText: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: TEXT.PRIMARY,
|
||||
fontFamily: FONT_FAMILY.SANS_SEMIBOLD,
|
||||
},
|
||||
primaryButton: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: SPACING[4],
|
||||
paddingHorizontal: SPACING[6],
|
||||
borderRadius: RADIUS.MD,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
primaryButtonText: {
|
||||
...TYPOGRAPHY.HEADLINE,
|
||||
fontFamily: FONT_FAMILY.SANS_BOLD,
|
||||
},
|
||||
buttonIcon: {
|
||||
marginRight: SPACING[2],
|
||||
},
|
||||
celebrationSection: { alignItems: 'center', paddingVertical: SPACING[8] },
|
||||
celebrationEmoji: { fontSize: 64, marginBottom: SPACING[4] },
|
||||
celebrationTitle: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, letterSpacing: 1 },
|
||||
|
||||
// Celebration
|
||||
celebrationSection: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: SPACING[8],
|
||||
},
|
||||
celebrationEmoji: {
|
||||
fontSize: 64,
|
||||
marginBottom: SPACING[4],
|
||||
},
|
||||
celebrationTitle: {
|
||||
...TYPOGRAPHY.TITLE_1,
|
||||
color: TEXT.PRIMARY,
|
||||
letterSpacing: 2,
|
||||
},
|
||||
ringsContainer: {
|
||||
flexDirection: 'row',
|
||||
marginTop: SPACING[6],
|
||||
gap: SPACING[4],
|
||||
},
|
||||
ring: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 2,
|
||||
},
|
||||
ring1: {
|
||||
borderColor: GREEN['500'],
|
||||
backgroundColor: GREEN.DIM,
|
||||
},
|
||||
ring2: {
|
||||
borderColor: GREEN['500'],
|
||||
backgroundColor: GREEN.DIM,
|
||||
},
|
||||
ring3: {
|
||||
borderColor: GREEN['500'],
|
||||
backgroundColor: GREEN.DIM,
|
||||
},
|
||||
ringEmoji: {
|
||||
fontSize: 28,
|
||||
},
|
||||
|
||||
// Stats Grid
|
||||
statsGrid: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
statsGrid: { flexDirection: 'row', gap: SPACING[3], marginBottom: SPACING[6] },
|
||||
statCard: {
|
||||
width: (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[4]) / 3,
|
||||
flex: 1,
|
||||
padding: SPACING[3],
|
||||
borderRadius: RADIUS.LG,
|
||||
backgroundColor: colors.surface.default.backgroundColor,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.surface.default.borderColor,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
statValue: {
|
||||
...TYPOGRAPHY.TITLE_1,
|
||||
color: TEXT.PRIMARY,
|
||||
marginTop: SPACING[2],
|
||||
},
|
||||
statLabel: {
|
||||
...TYPOGRAPHY.CAPTION_2,
|
||||
color: TEXT.TERTIARY,
|
||||
marginTop: SPACING[1],
|
||||
borderCurve: 'continuous',
|
||||
},
|
||||
statValue: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, marginTop: SPACING[2], fontVariant: ['tabular-nums'] },
|
||||
statLabel: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, marginTop: SPACING[1] },
|
||||
|
||||
// Burn Bar
|
||||
burnBarContainer: {
|
||||
marginBottom: SPACING[6],
|
||||
},
|
||||
burnBarTitle: {
|
||||
...TYPOGRAPHY.HEADLINE,
|
||||
color: TEXT.TERTIARY,
|
||||
},
|
||||
burnBarResult: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
marginTop: SPACING[1],
|
||||
marginBottom: SPACING[3],
|
||||
},
|
||||
burnBarTrack: {
|
||||
height: 8,
|
||||
backgroundColor: colors.bg.surface,
|
||||
borderRadius: RADIUS.SM,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
burnBarFill: {
|
||||
height: '100%',
|
||||
borderRadius: RADIUS.SM,
|
||||
},
|
||||
divider: { height: 1, backgroundColor: BORDER_COLORS.DIM, marginVertical: SPACING[2] },
|
||||
|
||||
// Divider
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: BORDER_COLORS.DIM,
|
||||
marginVertical: SPACING[2],
|
||||
},
|
||||
streakSection: { flexDirection: 'row', alignItems: 'center', paddingVertical: SPACING[4], gap: SPACING[4] },
|
||||
streakBadge: { width: 64, height: 64, borderRadius: RADIUS.FULL, alignItems: 'center', justifyContent: 'center' },
|
||||
streakInfo: { flex: 1 },
|
||||
streakTitle: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
|
||||
streakSubtitle: { ...TYPOGRAPHY.BODY, color: TEXT.TERTIARY, marginTop: SPACING[1] },
|
||||
|
||||
// Streak
|
||||
streakSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: SPACING[4],
|
||||
gap: SPACING[4],
|
||||
},
|
||||
streakBadge: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: RADIUS.FULL,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
streakInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
streakTitle: {
|
||||
...TYPOGRAPHY.TITLE_2,
|
||||
color: TEXT.PRIMARY,
|
||||
},
|
||||
streakSubtitle: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: TEXT.TERTIARY,
|
||||
marginTop: SPACING[1],
|
||||
},
|
||||
shareSection: { paddingVertical: SPACING[4], alignItems: 'center' },
|
||||
|
||||
// Share
|
||||
shareSection: {
|
||||
paddingVertical: SPACING[4],
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
// Recommended
|
||||
recommendedSection: {
|
||||
paddingVertical: SPACING[4],
|
||||
},
|
||||
recommendedTitle: {
|
||||
...TYPOGRAPHY.HEADLINE,
|
||||
color: TEXT.PRIMARY,
|
||||
marginBottom: SPACING[4],
|
||||
},
|
||||
recommendedGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: SPACING[3],
|
||||
},
|
||||
recommendedCard: {
|
||||
flex: 1,
|
||||
padding: SPACING[3],
|
||||
borderRadius: RADIUS.LG,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.surface.default.borderColor,
|
||||
backgroundColor: colors.surface.default.backgroundColor,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
recommendedThumb: {
|
||||
width: '100%',
|
||||
aspectRatio: 1,
|
||||
borderRadius: RADIUS.MD,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: SPACING[2],
|
||||
overflow: 'hidden',
|
||||
},
|
||||
recommendedInitial: {
|
||||
...TYPOGRAPHY.TITLE_1,
|
||||
color: TEXT.PRIMARY,
|
||||
},
|
||||
recommendedTitleText: {
|
||||
...TYPOGRAPHY.CARD_TITLE,
|
||||
color: TEXT.PRIMARY,
|
||||
},
|
||||
recommendedDurationText: {
|
||||
...TYPOGRAPHY.CARD_METADATA,
|
||||
color: TEXT.TERTIARY,
|
||||
},
|
||||
|
||||
// Bottom Bar
|
||||
bottomBar: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0, left: 0, right: 0,
|
||||
paddingHorizontal: LAYOUT.SCREEN_PADDING,
|
||||
paddingTop: SPACING[4],
|
||||
backgroundColor: colors.bg.base,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: BORDER_COLORS.DIM,
|
||||
},
|
||||
homeButtonContainer: {
|
||||
height: 56,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
homeButtonContainer: { height: 56, justifyContent: 'center' },
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user