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.
180 lines
6.3 KiB
TypeScript
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' },
|
|
})
|
|
}
|