Files
tabatago/app/(tabs)/profile.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

182 lines
6.6 KiB
TypeScript

/**
* TabataGo Profile Tab
* User info, subscription status, quick stats. Settings via form sheet.
*/
import { useMemo } from 'react'
import { View, Text, StyleSheet, ScrollView, Pressable } from 'react-native'
import { useRouter } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { Icon } from '@/src/shared/components/Icon'
import { useUserStore } from '@/src/shared/stores/userStore'
import { useProgressStore } from '@/src/shared/stores/progressStore'
import { usePurchases } from '@/src/shared/hooks'
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 { GREEN, NAVY, TEXT, BORDER_COLORS } from '@/src/shared/constants/colors'
export default function ProfileScreen() {
const { t } = useTranslation()
const router = useRouter()
const insets = useSafeAreaInsets()
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
const profile = useUserStore(s => s.profile)
const { isPremium } = usePurchases()
const completedCount = useProgressStore(s => s.getCompletedCount())
const streak = useProgressStore(s => s.streak)
const weeklyCount = useProgressStore(s => s.getWeeklyCount())
const avatarLetter = profile.name?.[0]?.toUpperCase() || '?'
return (
<ScrollView
style={styles.container}
contentContainerStyle={[styles.content, { paddingTop: insets.top + SPACING[4], paddingBottom: insets.bottom + SPACING[6] }]}
contentInsetAdjustmentBehavior="automatic"
>
{/* Avatar + name */}
<View style={styles.profileHeader}>
<View style={styles.avatar}>
<Text style={styles.avatarLetter}>{avatarLetter}</Text>
</View>
<View style={{ flex: 1 }}>
<Text style={styles.name}>{profile.name || t('screens:profile.guest')}</Text>
<View style={[styles.planBadge, { borderColor: isPremium ? GREEN[500] : BORDER_COLORS.DIM }]}>
<Text style={[styles.planText, { color: isPremium ? GREEN[500] : TEXT.TERTIARY }]}>
{isPremium ? t('screens:settings.premium') : t('screens:settings.free')}
</Text>
</View>
</View>
<Pressable onPress={() => router.push('/settings')} hitSlop={8}>
<Icon name="gearshape.fill" size={22} tintColor={TEXT.TERTIARY} />
</Pressable>
</View>
{/* Stats row */}
<View style={styles.statsRow}>
<StatPill value={streak.current} label={t('screens:home.statsStreak')} icon="flame.fill" color="#FF6B35" />
<StatPill value={weeklyCount} label={t('screens:activity.thisWeek')} icon="calendar" color="#5AC8FA" />
<StatPill value={completedCount} label={t('screens:home.statsCompleted')} icon="checkmark.seal.fill" color={GREEN[500]} />
</View>
{/* Upgrade banner (free users) */}
{!isPremium && (
<Pressable
style={[styles.upgradeBanner, { borderColor: GREEN[500] }]}
onPress={() => router.push('/paywall')}
>
<Icon name="sparkles" size={20} tintColor={GREEN[500]} />
<View style={{ flex: 1 }}>
<Text style={styles.upgradeTitle}>{t('screens:profile.upgradeTitle')}</Text>
<Text style={styles.upgradeDesc}>{t('screens:profile.upgradeDescription')}</Text>
</View>
<Icon name="chevron.right" size={16} tintColor={TEXT.TERTIARY} />
</Pressable>
)}
{/* Settings link */}
<Pressable style={styles.settingsRow} onPress={() => router.push('/settings')}>
<Icon name="gearshape" size={20} tintColor={TEXT.SECONDARY} />
<Text style={styles.settingsLabel}>{t('screens:settings.title')}</Text>
<Icon name="chevron.right" size={16} tintColor={TEXT.TERTIARY} />
</Pressable>
</ScrollView>
)
}
function StatPill({ value, label, icon, color }: { value: number; label: string; icon: any; color: string }) {
return (
<View style={pillStyles.pill}>
<Icon name={icon} size={18} tintColor={color} />
<Text selectable style={pillStyles.value}>{value}</Text>
<Text style={pillStyles.label}>{label}</Text>
</View>
)
}
const pillStyles = StyleSheet.create({
pill: {
flex: 1,
alignItems: 'center',
paddingVertical: SPACING[3],
borderRadius: RADIUS.MD,
borderWidth: 1,
backgroundColor: NAVY[800],
borderColor: BORDER_COLORS.DIM,
gap: 4,
},
value: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
label: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, textAlign: 'center' },
})
function createStyles(colors: ThemeColors) {
return StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bg.base },
content: { paddingHorizontal: LAYOUT.SCREEN_PADDING },
profileHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[3],
marginBottom: SPACING[5],
},
avatar: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: NAVY[700] ?? NAVY[800],
alignItems: 'center',
justifyContent: 'center',
borderWidth: 2,
borderColor: BORDER_COLORS.DIM,
},
avatarLetter: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY },
name: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
planBadge: {
alignSelf: 'flex-start',
marginTop: 4,
paddingHorizontal: SPACING[2],
paddingVertical: 2,
borderRadius: RADIUS.SM,
borderWidth: 1,
},
planText: { ...TYPOGRAPHY.CAPTION_2, fontWeight: '600' },
statsRow: { flexDirection: 'row', gap: SPACING[2], marginBottom: SPACING[5] },
upgradeBanner: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[3],
padding: SPACING[4],
borderRadius: RADIUS.LG,
borderWidth: 1,
backgroundColor: colors.surface.default.backgroundColor,
marginBottom: SPACING[3],
borderCurve: 'continuous',
},
upgradeTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY },
upgradeDesc: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.SECONDARY, marginTop: 2 },
settingsRow: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[3],
padding: SPACING[4],
borderRadius: RADIUS.LG,
borderWidth: 1,
borderColor: BORDER_COLORS.DIM,
backgroundColor: colors.surface.default.backgroundColor,
borderCurve: 'continuous',
},
settingsLabel: { ...TYPOGRAPHY.BODY, color: TEXT.PRIMARY, flex: 1 },
})
}