/** * TabataFit Activity Screen * Premium stats dashboard — streak, rings, weekly chart, history */ import { View, StyleSheet, ScrollView, Dimensions } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { LinearGradient } from 'expo-linear-gradient' import { BlurView } from 'expo-blur' import Ionicons from '@expo/vector-icons/Ionicons' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useActivityStore, getWeeklyActivity } from '@/src/shared/stores' import { getWorkoutById } from '@/src/shared/data' import { ACHIEVEMENTS } from '@/src/shared/data' import { StyledText } from '@/src/shared/components/StyledText' import { useThemeColors, BRAND, PHASE, GRADIENTS } from '@/src/shared/theme' import type { ThemeColors } from '@/src/shared/theme/types' import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' const { width: SCREEN_WIDTH } = Dimensions.get('window') // ═══════════════════════════════════════════════════════════════════════════ // STAT RING — Custom circular progress (pure RN, no SwiftUI) // ═══════════════════════════════════════════════════════════════════════════ function StatRing({ value, max, color, size = 64, }: { value: number max: number color: string size?: number }) { const colors = useThemeColors() const strokeWidth = 5 const radius = (size - strokeWidth) / 2 const circumference = 2 * Math.PI * radius const progress = Math.min(value / max, 1) const strokeDashoffset = circumference * (1 - progress) // We'll use a View-based ring since SVG isn't available // Use border trick for a circular progress indicator return ( {/* Track */} {/* Fill — simplified: show a colored ring proportional to progress */} 0.25 ? color : 'transparent', borderRightColor: progress > 0.5 ? color : 'transparent', borderBottomColor: progress > 0.75 ? color : 'transparent', borderLeftColor: progress > 0 ? color : 'transparent', transform: [{ rotate: '-90deg' }], opacity: progress > 0 ? 1 : 0.3, }} /> ) } // ═══════════════════════════════════════════════════════════════════════════ // STAT CARD // ═══════════════════════════════════════════════════════════════════════════ function StatCard({ label, value, max, color, icon, }: { label: string value: number max: number color: string icon: keyof typeof Ionicons.glyphMap }) { const colors = useThemeColors() const styles = useMemo(() => createStyles(colors), [colors]) return ( {String(value)} {label} ) } // ═══════════════════════════════════════════════════════════════════════════ // WEEKLY BAR // ═══════════════════════════════════════════════════════════════════════════ function WeeklyBar({ day, completed, isToday, }: { day: string completed: boolean isToday: boolean }) { const colors = useThemeColors() const styles = useMemo(() => createStyles(colors), [colors]) return ( {completed && ( )} {day} ) } // ═══════════════════════════════════════════════════════════════════════════ // MAIN SCREEN // ═══════════════════════════════════════════════════════════════════════════ const DAY_KEYS = ['days.sun', 'days.mon', 'days.tue', 'days.wed', 'days.thu', 'days.fri', 'days.sat'] as const export default function ActivityScreen() { const { t } = useTranslation() const insets = useSafeAreaInsets() const colors = useThemeColors() const styles = useMemo(() => createStyles(colors), [colors]) const streak = useActivityStore((s) => s.streak) const history = useActivityStore((s) => s.history) const totalWorkouts = history.length const totalMinutes = useMemo(() => history.reduce((sum, r) => sum + r.durationMinutes, 0), [history]) const totalCalories = useMemo(() => history.reduce((sum, r) => sum + r.calories, 0), [history]) const recentWorkouts = useMemo(() => history.slice(0, 5), [history]) const weeklyActivity = useMemo(() => getWeeklyActivity(history), [history]) const today = new Date().getDay() // 0=Sun // Check achievements const unlockedAchievements = ACHIEVEMENTS.filter(a => { switch (a.type) { case 'workouts': return totalWorkouts >= a.requirement case 'streak': return streak.longest >= a.requirement case 'minutes': return totalMinutes >= a.requirement case 'calories': return totalCalories >= a.requirement default: return false } }) const displayAchievements = ACHIEVEMENTS.slice(0, 4).map(a => ({ ...a, unlocked: unlockedAchievements.some(u => u.id === a.id), })) const formatDate = (timestamp: number) => { const now = Date.now() const diff = now - timestamp if (diff < 86400000) return t('screens:activity.today') if (diff < 172800000) return t('screens:activity.yesterday') return t('screens:activity.daysAgo', { count: Math.floor(diff / 86400000) }) } return ( {/* Header */} {t('screens:activity.title')} {/* Streak Banner */} {String(streak.current || 0)} {t('screens:activity.dayStreak')} {t('screens:activity.longest')} {String(streak.longest)} {/* Stats Grid — 2x2 */} {/* This Week */} {t('screens:activity.thisWeek')} {weeklyActivity.map((d, i) => ( ))} {t('screens:activity.ofDays', { completed: weeklyActivity.filter(d => d.completed).length })} {/* Recent Workouts */} {recentWorkouts.length > 0 && ( {t('screens:activity.recent')} {recentWorkouts.map((result, idx) => { const workout = getWorkoutById(result.workoutId) const workoutTitle = workout ? t(`content:workouts.${workout.id}`, { defaultValue: workout.title }) : t('screens:activity.workouts') return ( {workoutTitle} {formatDate(result.completedAt) + ' \u00B7 ' + t('units.minUnit', { count: result.durationMinutes })} {t('units.calUnit', { count: result.calories })} {idx < recentWorkouts.length - 1 && } ) })} )} {/* Achievements */} {t('screens:activity.achievements')} {displayAchievements.map((a) => ( {t(`content:achievements.${a.id}.title`, { defaultValue: a.title })} ))} ) } // ═══════════════════════════════════════════════════════════════════════════ // STYLES // ═══════════════════════════════════════════════════════════════════════════ const CARD_HALF = (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2 function createStyles(colors: ThemeColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg.base, }, scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: LAYOUT.SCREEN_PADDING, }, // Streak streakBanner: { borderRadius: RADIUS.GLASS_CARD, overflow: 'hidden', marginBottom: SPACING[5], ...colors.shadow.md, }, streakRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING[5], paddingVertical: SPACING[5], gap: SPACING[4], }, streakIconWrap: { width: 48, height: 48, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.15)', alignItems: 'center', justifyContent: 'center', }, streakMeta: { alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.1)', paddingHorizontal: SPACING[4], paddingVertical: SPACING[2], borderRadius: RADIUS.MD, }, // Stats 2x2 statsGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING[3], marginBottom: SPACING[6], }, statCard: { width: CARD_HALF, borderRadius: RADIUS.LG, overflow: 'hidden', borderWidth: 1, borderColor: colors.bg.overlay2, }, statCardInner: { flexDirection: 'row', alignItems: 'center', padding: SPACING[4], }, // Section section: { marginBottom: SPACING[6], }, // Weekly weekCard: { borderRadius: RADIUS.LG, overflow: 'hidden', borderWidth: 1, borderColor: colors.bg.overlay2, paddingTop: SPACING[5], paddingBottom: SPACING[4], }, weekBarsRow: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'flex-end', paddingHorizontal: SPACING[4], height: 100, }, weekBarColumn: { alignItems: 'center', flex: 1, gap: SPACING[2], }, weekBar: { width: 24, height: 60, borderRadius: 4, backgroundColor: colors.border.glassLight, overflow: 'hidden', }, weekBarFilled: { backgroundColor: BRAND.PRIMARY, }, weekSummary: { alignItems: 'center', marginTop: SPACING[3], paddingTop: SPACING[3], borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.bg.overlay2, marginHorizontal: SPACING[4], }, // Recent recentCard: { borderRadius: RADIUS.LG, overflow: 'hidden', borderWidth: 1, borderColor: colors.bg.overlay2, paddingVertical: SPACING[2], }, recentRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: SPACING[4], paddingVertical: SPACING[3], }, recentDot: { width: 24, alignItems: 'center', marginRight: SPACING[3], }, dot: { width: 8, height: 8, borderRadius: 4, }, recentDivider: { height: StyleSheet.hairlineWidth, backgroundColor: colors.border.glassLight, marginLeft: SPACING[4] + 24 + SPACING[3], }, // Achievements achievementsRow: { flexDirection: 'row', gap: SPACING[3], }, achievementCard: { flex: 1, aspectRatio: 0.9, borderRadius: RADIUS.LG, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: colors.bg.overlay2, overflow: 'hidden', paddingHorizontal: SPACING[1], }, achievementIcon: { width: 44, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center', }, }) }