Files
tabatago/app/complete/[id].tsx
Millian Lamiaux 5888aac08e 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.
2026-04-21 21:50:48 +02:00

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' },
})
}