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

180 lines
6.3 KiB
TypeScript

/**
* TabataGo Activity Tab
* Streak, weekly sessions, program history — driven by progressStore.
*/
import { useMemo } from 'react'
import { View, Text, StyleSheet, ScrollView } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { Icon } from '@/src/shared/components/Icon'
import { useProgressStore } from '@/src/shared/stores/progressStore'
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 ActivityScreen() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()
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())
const completedCount = useProgressStore(s => s.getCompletedCount())
const totalMinutes = useMemo(
() => history.reduce((sum, s) => sum + Math.round(s.durationSeconds / 60), 0),
[history],
)
return (
<ScrollView
style={styles.container}
contentContainerStyle={[styles.content, { paddingTop: insets.top + SPACING[4], paddingBottom: insets.bottom + SPACING[6] }]}
contentInsetAdjustmentBehavior="automatic"
>
<Text style={styles.title}>{t('screens:tabs.progression')}</Text>
{/* Streak hero */}
<View style={styles.streakHero}>
<Icon name="flame.fill" size={32} tintColor={GREEN[500]} />
<Text selectable style={styles.streakCount}>{streak.current}</Text>
<Text style={styles.streakLabel}>{t('screens:activity.dayStreak')}</Text>
<Text style={styles.streakRecord}>
{t('screens:activity.longest')}: {streak.longest}
</Text>
</View>
{/* Stats grid */}
<View style={styles.grid}>
<StatCard
icon="checkmark.circle.fill"
value={completedCount}
label={t('screens:programs.completed')}
color={GREEN[500]}
/>
<StatCard
icon="calendar"
value={weeklyCount}
label={t('screens:activity.thisWeek')}
color="#5AC8FA"
/>
<StatCard
icon="clock.fill"
value={totalMinutes}
label={t('screens:player.minutes')}
color="#FF6B35"
/>
</View>
{/* Recent history */}
{history.length > 0 && (
<View style={styles.historySection}>
<Text style={styles.sectionTitle}>{t('screens:activity.recent')}</Text>
{history.slice(0, 10).map((session, i) => (
<View key={i} style={styles.historyRow}>
<Icon name="checkmark.circle.fill" size={18} tintColor={GREEN[500]} />
<View style={{ flex: 1 }}>
<Text style={styles.historyTitle} numberOfLines={1}>{session.programId}</Text>
<Text style={styles.historyMeta}>
{Math.round(session.durationSeconds / 60)} min
{' · '}
{new Date(session.completedAt).toLocaleDateString()}
</Text>
</View>
</View>
))}
</View>
)}
{history.length === 0 && (
<View style={styles.emptyState}>
<Text style={styles.emptyTitle}>{t('screens:activity.emptyTitle')}</Text>
<Text style={styles.emptySubtitle}>{t('screens:activity.emptySubtitle')}</Text>
</View>
)}
</ScrollView>
)
}
function StatCard({ icon, value, label, color }: { icon: any; value: number; label: string; color: string }) {
return (
<View style={cardStyles.card}>
<Icon name={icon} size={22} tintColor={color} />
<Text selectable style={cardStyles.value}>{value}</Text>
<Text style={cardStyles.label}>{label}</Text>
</View>
)
}
const cardStyles = StyleSheet.create({
card: {
flex: 1,
alignItems: 'center',
padding: SPACING[3],
borderRadius: RADIUS.LG,
backgroundColor: NAVY[800],
borderWidth: 1,
borderColor: BORDER_COLORS.DIM,
gap: SPACING[1],
borderCurve: 'continuous',
},
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 },
title: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, marginBottom: SPACING[5] },
streakHero: {
alignItems: 'center',
paddingVertical: SPACING[6],
marginBottom: SPACING[4],
backgroundColor: NAVY[800],
borderRadius: RADIUS.XL,
borderWidth: 1,
borderColor: BORDER_COLORS.DIM,
gap: SPACING[1],
},
streakCount: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, fontSize: 56, fontVariant: ['tabular-nums'] },
streakLabel: { ...TYPOGRAPHY.HEADLINE, color: TEXT.SECONDARY },
streakRecord: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1] },
grid: { flexDirection: 'row', gap: SPACING[3], marginBottom: SPACING[6] },
historySection: { gap: SPACING[2] },
sectionTitle: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: SPACING[1],
},
historyRow: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[3],
padding: SPACING[3],
backgroundColor: colors.surface.default.backgroundColor,
borderRadius: RADIUS.MD,
borderWidth: 1,
borderColor: colors.surface.default.borderColor,
},
historyTitle: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY },
historyMeta: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 },
emptyState: { alignItems: 'center', marginTop: SPACING[12], gap: SPACING[2] },
emptyTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY },
emptySubtitle: { ...TYPOGRAPHY.BODY, color: TEXT.TERTIARY, textAlign: 'center' },
})
}