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

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