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.
212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
/**
|
|
* TabataFit Workout Complete Screen
|
|
* Celebration + stats driven by progressStore.
|
|
*/
|
|
|
|
import { useEffect, useMemo, useRef } from 'react'
|
|
import {
|
|
View,
|
|
Text as RNText,
|
|
StyleSheet,
|
|
ScrollView,
|
|
Animated,
|
|
} 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 { useTranslation } from 'react-i18next'
|
|
import * as Sharing from 'expo-sharing'
|
|
|
|
import { useHaptics } from '@/src/shared/hooks'
|
|
import { useProgressStore } from '@/src/shared/stores'
|
|
import { NativeButton } from '@/src/shared/components/native'
|
|
|
|
import { useThemeColors } 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 } from '@/src/shared/constants/animations'
|
|
import { GREEN, NAVY, TEXT, BORDER_COLORS } from '@/src/shared/constants/colors'
|
|
|
|
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 (
|
|
<Animated.View style={[styles.statCard, { transform: [{ scale: scaleAnim }] }]}>
|
|
<Icon name={icon} size={24} tintColor={GREEN['500']} />
|
|
<RNText selectable style={styles.statValue}>{value}</RNText>
|
|
<RNText style={styles.statLabel}>{label}</RNText>
|
|
</Animated.View>
|
|
)
|
|
}
|
|
|
|
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 history = useProgressStore((s) => s.history)
|
|
const streak = useProgressStore((s) => s.streak)
|
|
const weeklyCount = useProgressStore((s) => s.getWeeklyCount())
|
|
|
|
// 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('/')
|
|
}
|
|
|
|
const handleShare = async () => {
|
|
haptics.selection()
|
|
const isAvailable = await Sharing.isAvailableAsync()
|
|
if (isAvailable) {
|
|
await Sharing.shareAsync('https://tabatafit.app', {
|
|
dialogTitle: t('screens:complete.shareTitle', { minutes: resultMinutes }),
|
|
})
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
haptics.workoutComplete()
|
|
}, [])
|
|
|
|
return (
|
|
<View style={[styles.container, { paddingTop: insets.top }]}>
|
|
<ScrollView
|
|
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>
|
|
</View>
|
|
|
|
{/* Stats Grid */}
|
|
<View style={styles.statsGrid}>
|
|
<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>
|
|
|
|
<View style={styles.divider} />
|
|
|
|
{/* Streak */}
|
|
<View style={styles.streakSection}>
|
|
<View style={[styles.streakBadge, { backgroundColor: GREEN.DIM }]}>
|
|
<Icon name="flame.fill" size={32} tintColor={GREEN['500']} />
|
|
</View>
|
|
<View style={styles.streakInfo}>
|
|
<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 */}
|
|
<View style={styles.shareSection}>
|
|
<NativeButton
|
|
variant="ghost"
|
|
title={t('screens:complete.share')}
|
|
systemImage="square.and.arrow.up"
|
|
onPress={handleShare}
|
|
fullWidth
|
|
/>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Fixed Bottom Button */}
|
|
<View style={[styles.bottomBar, { paddingBottom: insets.bottom + SPACING[4] }]}>
|
|
<View style={styles.homeButtonContainer}>
|
|
<NativeButton
|
|
variant="primary"
|
|
title={t('screens:complete.backToHome')}
|
|
onPress={handleGoHome}
|
|
fullWidth
|
|
controlSize="large"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function createStyles(colors: ThemeColors) {
|
|
return StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.bg.base },
|
|
scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING },
|
|
|
|
celebrationSection: { alignItems: 'center', paddingVertical: SPACING[8] },
|
|
celebrationEmoji: { fontSize: 64, marginBottom: SPACING[4] },
|
|
celebrationTitle: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, letterSpacing: 1 },
|
|
|
|
statsGrid: { flexDirection: 'row', gap: SPACING[3], marginBottom: SPACING[6] },
|
|
statCard: {
|
|
flex: 1,
|
|
padding: SPACING[3],
|
|
borderRadius: RADIUS.LG,
|
|
backgroundColor: colors.surface.default.backgroundColor,
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
borderColor: colors.surface.default.borderColor,
|
|
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] },
|
|
|
|
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] },
|
|
|
|
shareSection: { paddingVertical: SPACING[4], alignItems: 'center' },
|
|
|
|
bottomBar: {
|
|
position: 'absolute',
|
|
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' },
|
|
})
|
|
}
|