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.
213 lines
7.0 KiB
TypeScript
213 lines
7.0 KiB
TypeScript
/**
|
|
* TabataGo Home Screen
|
|
* Mascot + 3 stat pills + 3 body zone cards + settings button.
|
|
*/
|
|
|
|
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 { Mascot } from '@/src/shared/components/Mascot'
|
|
import { useUserStore } from '@/src/shared/stores/userStore'
|
|
import { useProgressStore } from '@/src/shared/stores/progressStore'
|
|
import { BODY_ZONE_META, type BodyZone } from '@/src/shared/types/workoutProgram'
|
|
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
|
|
import { SPACING } from '@/src/shared/constants/spacing'
|
|
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
|
import { TEXT, NAVY, GREEN, BORDER_COLORS } from '@/src/shared/constants/colors'
|
|
import { withOpacity } from '@/src/shared/utils/color'
|
|
|
|
const BODY_ZONES: BodyZone[] = ['upper-body', 'lower-body', 'full-body']
|
|
|
|
export default function HomeScreen() {
|
|
const router = useRouter()
|
|
const insets = useSafeAreaInsets()
|
|
const { t } = useTranslation()
|
|
|
|
const firstName = useUserStore(s => s.profile.name)
|
|
const streak = useProgressStore(s => s.streak.current)
|
|
const weeklyCount = useProgressStore(s => s.getWeeklyCount())
|
|
const completedCount = useProgressStore(s => s.getCompletedCount())
|
|
|
|
const nameSuffix = firstName ? `, ${firstName}` : ''
|
|
const mascotMessage = streak > 0
|
|
? t('screens:home.mascotStreak', { count: streak, name: nameSuffix })
|
|
: t('screens:home.mascotReady', { name: nameSuffix })
|
|
|
|
return (
|
|
<ScrollView
|
|
style={styles.container}
|
|
contentContainerStyle={[styles.content, { paddingTop: insets.top + SPACING[4], paddingBottom: insets.bottom + SPACING[6] }]}
|
|
>
|
|
{/* Header with settings */}
|
|
<View style={styles.header}>
|
|
<Text style={styles.brand}>TabataGo</Text>
|
|
<Pressable onPress={() => router.push('/settings')} style={styles.iconBtn} hitSlop={8}>
|
|
<Icon name="gearshape" size={22} tintColor={TEXT.PRIMARY} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
{/* Mascot */}
|
|
<View style={styles.mascotWrap}>
|
|
<Mascot message={mascotMessage} />
|
|
</View>
|
|
|
|
{/* Stats pills */}
|
|
<View style={styles.statsRow}>
|
|
<StatPill value={streak} label={t('screens:home.statsStreak')} icon="flame.fill" color="#FF6B35" />
|
|
<StatPill value={weeklyCount} label={t('screens:home.statsThisWeek')} icon="calendar" color={GREEN[500]} />
|
|
<StatPill value={completedCount} label={t('screens:home.statsCompleted')} icon="checkmark.seal.fill" color="#5AC8FA" />
|
|
</View>
|
|
|
|
{/* Body zone cards */}
|
|
<Text style={styles.sectionTitle}>{t('screens:zone.chooseYourFocus')}</Text>
|
|
<View style={styles.zoneList}>
|
|
{BODY_ZONES.map(zone => (
|
|
<ZoneCard key={zone} zone={zone} onPress={() => router.push(`/zone/${zone}`)} />
|
|
))}
|
|
</View>
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
function StatPill({
|
|
value,
|
|
label,
|
|
icon,
|
|
color,
|
|
}: {
|
|
value: number
|
|
label: string
|
|
icon: any
|
|
color: string
|
|
}) {
|
|
return (
|
|
<View style={[styles.pill, { borderColor: withOpacity(color, 0.4) }]}>
|
|
<Icon name={icon} size={16} tintColor={color} />
|
|
<Text style={styles.pillValue}>{value}</Text>
|
|
<Text style={styles.pillLabel}>{label}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
function ZoneCard({ zone, onPress }: { zone: BodyZone; onPress: () => void }) {
|
|
const meta = BODY_ZONE_META[zone]
|
|
const { t } = useTranslation()
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
style={({ pressed }) => [
|
|
styles.zoneCard,
|
|
{ borderColor: withOpacity(meta.color, 0.3) },
|
|
pressed && { opacity: 0.85, transform: [{ scale: 0.98 }] },
|
|
]}
|
|
>
|
|
{/* Colored top strip with large icon */}
|
|
<View style={[styles.zoneTopStrip, { backgroundColor: withOpacity(meta.color, 0.12) }]}>
|
|
<View style={[styles.zoneIconCircle, { backgroundColor: withOpacity(meta.color, 0.22) }]}>
|
|
<Icon name={meta.icon as any} size={34} tintColor={meta.color} />
|
|
</View>
|
|
</View>
|
|
|
|
{/* Content area */}
|
|
<View style={styles.zoneContent}>
|
|
<Text style={styles.zoneTitle}>{meta.label}</Text>
|
|
<Text style={styles.zoneDesc} numberOfLines={2}>
|
|
{t(meta.descKey)}
|
|
</Text>
|
|
|
|
{/* Bottom row: level badge + chevron */}
|
|
<View style={styles.zoneFooter}>
|
|
<View style={[styles.zoneBadge, { backgroundColor: withOpacity(meta.color, 0.15) }]}>
|
|
<Text style={[styles.zoneBadgeText, { color: meta.color }]}>
|
|
{t('screens:home.zoneLevels')}
|
|
</Text>
|
|
</View>
|
|
<Icon name="chevron.right" size={16} tintColor={TEXT.TERTIARY} />
|
|
</View>
|
|
</View>
|
|
</Pressable>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: NAVY[900] },
|
|
content: { paddingHorizontal: SPACING[5] },
|
|
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: SPACING[4],
|
|
},
|
|
brand: { ...TYPOGRAPHY.TITLE_1, color: TEXT.PRIMARY, letterSpacing: -0.5 },
|
|
iconBtn: {
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 20,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: NAVY[800],
|
|
borderWidth: 1,
|
|
borderColor: BORDER_COLORS.DIM,
|
|
},
|
|
|
|
mascotWrap: { alignItems: 'center', marginVertical: SPACING[4] },
|
|
|
|
statsRow: { flexDirection: 'row', gap: SPACING[2], marginBottom: SPACING[6] },
|
|
pill: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
paddingVertical: SPACING[3],
|
|
paddingHorizontal: SPACING[2],
|
|
borderRadius: RADIUS.MD,
|
|
borderWidth: 1,
|
|
backgroundColor: NAVY[800],
|
|
gap: 4,
|
|
},
|
|
pillValue: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY, fontVariant: ['tabular-nums'] },
|
|
pillLabel: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, textAlign: 'center' },
|
|
|
|
sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, marginBottom: SPACING[3] },
|
|
zoneList: { gap: SPACING[4] },
|
|
zoneCard: {
|
|
borderRadius: RADIUS.XL,
|
|
borderWidth: 1,
|
|
backgroundColor: NAVY[800],
|
|
overflow: 'hidden' as const,
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
|
},
|
|
zoneTopStrip: {
|
|
alignItems: 'center' as const,
|
|
justifyContent: 'center' as const,
|
|
paddingVertical: SPACING[5],
|
|
},
|
|
zoneIconCircle: {
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 36,
|
|
alignItems: 'center' as const,
|
|
justifyContent: 'center' as const,
|
|
},
|
|
zoneContent: {
|
|
padding: SPACING[4],
|
|
gap: SPACING[2],
|
|
},
|
|
zoneTitle: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
|
|
zoneDesc: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY, lineHeight: 20 },
|
|
zoneFooter: {
|
|
flexDirection: 'row' as const,
|
|
alignItems: 'center' as const,
|
|
justifyContent: 'space-between' as const,
|
|
marginTop: SPACING[1],
|
|
},
|
|
zoneBadge: {
|
|
paddingHorizontal: SPACING[3],
|
|
paddingVertical: SPACING[1],
|
|
borderRadius: RADIUS.SM,
|
|
},
|
|
zoneBadgeText: { ...TYPOGRAPHY.CAPTION_1, fontWeight: '600' as const },
|
|
})
|