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.
182 lines
6.6 KiB
TypeScript
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 },
|
|
})
|
|
}
|