/** * TabataFit Workout Complete Screen * Celebration with real data from activity store */ import { useRef, useEffect, useMemo, useState } 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 { LinearGradient } from 'expo-linear-gradient' import { BlurView } from 'expo-blur' import { Icon, type IconName } from '@/src/shared/components/Icon' import * as Sharing from 'expo-sharing' import { useTranslation } from 'react-i18next' import { useHaptics } from '@/src/shared/hooks' import { useActivityStore, useUserStore } from '@/src/shared/stores' import { getWorkoutById, getPopularWorkouts } from '@/src/shared/data' import { useTranslatedWorkout, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData' import { SyncConsentModal } from '@/src/shared/components/SyncConsentModal' import { enableSync } from '@/src/shared/services/sync' import type { WorkoutSessionData } from '@/src/shared/types' import { useThemeColors, BRAND } from '@/src/shared/theme' import type { ThemeColors } from '@/src/shared/theme/types' 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' 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 ( {icon && } {children} ) } 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 ( {children} ) } // ═══════════════════════════════════════════════════════════════════════════ // 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 ( 🔥 💪 ) } function StatCard({ value, label, icon, delay = 0, }: { value: string | number label: string icon: IconName delay?: number }) { const colors = useThemeColors() const styles = useMemo(() => createStyles(colors), [colors]) const scaleAnim = useRef(new Animated.Value(0)).current useEffect(() => { Animated.sequence([ Animated.delay(delay), Animated.spring(scaleAnim, { toValue: 1, ...SPRING.BOUNCY, useNativeDriver: true, }), ]).start() }, [delay]) return ( {value} {label} ) } function BurnBarResult({ percentile }: { percentile: number }) { 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 ( {t('screens:complete.burnBar')} {t('screens:complete.burnBarResult', { percentile })} ) } // ═══════════════════════════════════════════════════════════════════════════ // MAIN SCREEN // ═══════════════════════════════════════════════════════════════════════════ export default function WorkoutCompleteScreen() { const insets = useSafeAreaInsets() const router = useRouter() const haptics = useHaptics() const { t } = useTranslation() const { id } = useLocalSearchParams<{ id: string }>() const colors = useThemeColors() const styles = useMemo(() => createStyles(colors), [colors]) const rawWorkout = getWorkoutById(id ?? '1') const workout = useTranslatedWorkout(rawWorkout) const streak = useActivityStore((s) => s.streak) const history = useActivityStore((s) => s.history) const recentWorkouts = history.slice(0, 1) // 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) const handleGoHome = () => { haptics.buttonTap() router.replace('/(tabs)') } const handleShare = async () => { haptics.selection() 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 }), }) } } 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 ( {/* Celebration */} 🎉 {t('screens:complete.title')} {/* Stats Grid */} {/* Burn Bar */} {/* Streak */} {t('screens:complete.streakTitle', { count: streak.current })} {t('screens:complete.streakSubtitle')} {/* Share Button */} {t('screens:complete.shareWorkout')} {/* Recommended */} {t('screens:complete.recommendedNext')} {recommended.map((w) => ( handleWorkoutPress(w.id)} style={styles.recommendedCard} > {w.title} {t('units.minUnit', { count: w.duration })} ))} {/* Fixed Bottom Button */} {t('screens:complete.backToHome')} {/* Sync Consent Modal */} ) } // ═══════════════════════════════════════════════════════════════════════════ // STYLES // ═══════════════════════════════════════════════════════════════════════════ function createStyles(colors: ThemeColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg.base, }, scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING, }, // Buttons secondaryButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: SPACING[3], paddingHorizontal: SPACING[4], borderRadius: RADIUS.LG, borderWidth: 1, borderColor: colors.border.glassStrong, backgroundColor: 'transparent', }, secondaryButtonText: { ...TYPOGRAPHY.BODY, color: colors.text.primary, fontWeight: '600', }, primaryButton: { alignItems: 'center', justifyContent: 'center', paddingVertical: SPACING[4], paddingHorizontal: SPACING[6], borderRadius: RADIUS.LG, overflow: 'hidden', }, primaryButtonText: { ...TYPOGRAPHY.HEADLINE, color: '#FFFFFF', fontWeight: '700', }, buttonIcon: { marginRight: SPACING[2], }, // Celebration celebrationSection: { alignItems: 'center', paddingVertical: SPACING[8], }, celebrationEmoji: { fontSize: 64, marginBottom: SPACING[4], }, celebrationTitle: { ...TYPOGRAPHY.TITLE_1, color: colors.text.primary, letterSpacing: 2, }, ringsContainer: { flexDirection: 'row', marginTop: SPACING[6], gap: SPACING[4], }, ring: { width: 64, height: 64, borderRadius: 32, backgroundColor: colors.border.glass, alignItems: 'center', justifyContent: 'center', borderWidth: 2, borderColor: colors.border.glassStrong, }, ring1: { borderColor: BRAND.PRIMARY, backgroundColor: 'rgba(255, 107, 53, 0.15)', }, ring2: { borderColor: '#30D158', backgroundColor: 'rgba(48, 209, 88, 0.15)', }, ring3: { borderColor: '#5AC8FA', backgroundColor: 'rgba(90, 200, 250, 0.15)', }, ringEmoji: { fontSize: 28, }, // Stats Grid statsGrid: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: SPACING[6], }, statCard: { width: (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[4]) / 3, padding: SPACING[3], borderRadius: RADIUS.LG, alignItems: 'center', borderWidth: 1, borderColor: colors.border.glass, overflow: 'hidden', }, statValue: { ...TYPOGRAPHY.TITLE_1, color: colors.text.primary, marginTop: SPACING[2], }, statLabel: { ...TYPOGRAPHY.CAPTION_2, color: colors.text.tertiary, marginTop: SPACING[1], }, // Burn Bar burnBarContainer: { marginBottom: SPACING[6], }, burnBarTitle: { ...TYPOGRAPHY.HEADLINE, color: colors.text.tertiary, }, burnBarResult: { ...TYPOGRAPHY.BODY, color: BRAND.PRIMARY, marginTop: SPACING[1], marginBottom: SPACING[3], }, burnBarTrack: { height: 8, backgroundColor: colors.bg.surface, borderRadius: 4, overflow: 'hidden', }, burnBarFill: { height: '100%', backgroundColor: BRAND.PRIMARY, borderRadius: 4, }, // Divider divider: { height: 1, backgroundColor: colors.border.glass, marginVertical: SPACING[2], }, // Streak streakSection: { flexDirection: 'row', alignItems: 'center', paddingVertical: SPACING[4], gap: SPACING[4], }, streakBadge: { width: 64, height: 64, borderRadius: 32, backgroundColor: 'rgba(255, 107, 53, 0.15)', alignItems: 'center', justifyContent: 'center', }, streakInfo: { flex: 1, }, streakTitle: { ...TYPOGRAPHY.TITLE_2, color: colors.text.primary, }, streakSubtitle: { ...TYPOGRAPHY.BODY, color: colors.text.tertiary, marginTop: SPACING[1], }, // Share shareSection: { paddingVertical: SPACING[4], alignItems: 'center', }, // Recommended recommendedSection: { paddingVertical: SPACING[4], }, recommendedTitle: { ...TYPOGRAPHY.HEADLINE, color: colors.text.primary, marginBottom: SPACING[4], }, recommendedGrid: { flexDirection: 'row', gap: SPACING[3], }, recommendedCard: { flex: 1, padding: SPACING[3], borderRadius: RADIUS.LG, borderWidth: 1, borderColor: colors.border.glass, overflow: 'hidden', }, recommendedThumb: { width: '100%', aspectRatio: 1, borderRadius: RADIUS.MD, alignItems: 'center', justifyContent: 'center', marginBottom: SPACING[2], overflow: 'hidden', }, recommendedInitial: { ...TYPOGRAPHY.TITLE_1, color: colors.text.primary, }, recommendedTitleText: { ...TYPOGRAPHY.CARD_TITLE, color: colors.text.primary, }, recommendedDurationText: { ...TYPOGRAPHY.CARD_METADATA, color: colors.text.tertiary, }, // Bottom Bar bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: LAYOUT.SCREEN_PADDING, paddingTop: SPACING[4], borderTopWidth: 1, borderTopColor: colors.border.glass, }, homeButtonContainer: { height: 56, justifyContent: 'center', }, }) }