Files
tabatago/app/(tabs)/index.tsx
Millian Lamiaux 197324188c feat: update Home screen to use React Query with loading states
- Replace static data with React Query hooks
- Add loading skeletons for all data sections
- Show shimmer effect while data is loading
- Handle empty and error states gracefully
2026-03-17 14:29:27 +01:00

326 lines
11 KiB
TypeScript

/**
* TabataFit Home Screen - Premium Redesign
* React Native UI with glassmorphism
*/
import { View, StyleSheet, ScrollView, Pressable, Dimensions, Text as RNText } from 'react-native'
import { useRouter } from 'expo-router'
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, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHaptics, useFeaturedWorkouts, usePopularWorkouts, useCollections } from '@/src/shared/hooks'
import { useUserStore, useActivityStore } from '@/src/shared/stores'
import { StyledText } from '@/src/shared/components/StyledText'
import { WorkoutCard } from '@/src/shared/components/WorkoutCard'
import { CollectionCard } from '@/src/shared/components/CollectionCard'
import { WorkoutCardSkeleton, CollectionCardSkeleton, StatsCardSkeleton } from '@/src/shared/components/loading/Skeleton'
import { useThemeColors, BRAND, 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'
import type { WorkoutCategory } from '@/src/shared/types'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
const FONTS = {
LARGE_TITLE: 34,
TITLE: 28,
TITLE_2: 22,
HEADLINE: 17,
SUBHEADLINE: 15,
CAPTION_1: 12,
CAPTION_2: 11,
}
const CATEGORIES: { id: WorkoutCategory | 'all'; key: string }[] = [
{ id: 'all', key: 'all' },
{ id: 'full-body', key: 'fullBody' },
{ id: 'core', key: 'core' },
{ id: 'upper-body', key: 'upperBody' },
{ id: 'lower-body', key: 'lowerBody' },
{ id: 'cardio', key: 'cardio' },
]
// ═══════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ═══════════════════════════════════════════════════════════════════════════
export default function HomeScreen() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()
const router = useRouter()
const haptics = useHaptics()
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
const userName = useUserStore((s) => s.profile.name)
const history = useActivityStore((s) => s.history)
const recentWorkouts = useMemo(() => history.slice(0, 3), [history])
const [selectedCategory, setSelectedCategory] = useState<WorkoutCategory | 'all'>('all')
// React Query hooks for live data
const { data: featuredWorkouts = [], isLoading: isLoadingFeatured } = useFeaturedWorkouts()
const { data: popularWorkouts = [], isLoading: isLoadingPopular } = usePopularWorkouts(6)
const { data: collections = [], isLoading: isLoadingCollections } = useCollections()
const featured = featuredWorkouts[0]
const filteredWorkouts = useMemo(() => {
if (selectedCategory === 'all') return popularWorkouts
return popularWorkouts.filter((w) => w.category === selectedCategory)
}, [popularWorkouts, selectedCategory])
const greeting = (() => {
const hour = new Date().getHours()
if (hour < 12) return t('greetings.morning')
if (hour < 18) return t('greetings.afternoon')
return t('greetings.evening')
})()
const handleWorkoutPress = (id: string) => {
haptics.buttonTap()
router.push(`/workout/${id}`)
}
const handleCollectionPress = (id: string) => {
haptics.buttonTap()
router.push(`/collection/${id}`)
}
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
showsVerticalScrollIndicator={false}
>
{/* Hero Section */}
<View style={styles.heroSection}>
<StyledText size={FONTS.SUBHEADLINE} color={colors.text.tertiary}>
{greeting}
</StyledText>
<View style={styles.heroHeader}>
<StyledText
size={FONTS.LARGE_TITLE}
weight="bold"
color={colors.text.primary}
style={styles.heroTitle}
>
{userName}
</StyledText>
<Pressable style={styles.profileButton}>
<Ionicons name="person-circle-outline" size={40} color={colors.text.primary} />
</Pressable>
</View>
</View>
{/* Featured Workout */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<StyledText size={FONTS.TITLE_2} weight="semibold" color={colors.text.primary}>
{t('screens:home.featured')}
</StyledText>
</View>
{isLoadingFeatured ? (
<WorkoutCardSkeleton />
) : featured ? (
<WorkoutCard
workout={featured}
variant="featured"
onPress={() => handleWorkoutPress(featured.id)}
/>
) : null}
</View>
{/* Category Filter */}
<View style={styles.section}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.categoriesScroll}
>
{CATEGORIES.map((cat) => (
<Pressable
key={cat.id}
style={[
styles.categoryChip,
selectedCategory === cat.id && styles.categoryChipActive,
]}
onPress={() => {
haptics.buttonTap()
setSelectedCategory(cat.id)
}}
>
{selectedCategory === cat.id && (
<BlurView
intensity={colors.glass.blurMedium}
tint={colors.glass.blurTint}
style={StyleSheet.absoluteFill}
/>
)}
<StyledText
size={14}
weight={selectedCategory === cat.id ? 'semibold' : 'medium'}
color={selectedCategory === cat.id ? colors.text.primary : colors.text.secondary}
>
{t(`categories.${cat.key}`)}
</StyledText>
</Pressable>
))}
</ScrollView>
</View>
{/* Popular Workouts - Horizontal */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<StyledText size={FONTS.TITLE_2} weight="semibold" color={colors.text.primary}>
{t('screens:home.popularThisWeek')}
</StyledText>
<Pressable hitSlop={8}>
<StyledText size={FONTS.SUBHEADLINE} color={BRAND.PRIMARY} weight="medium">
{t('seeAll')}
</StyledText>
</Pressable>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.workoutsScroll}
>
{isLoadingPopular ? (
// Loading skeletons
<>
<WorkoutCardSkeleton />
<WorkoutCardSkeleton />
<WorkoutCardSkeleton />
</>
) : (
filteredWorkouts.map((workout: any) => (
<WorkoutCard
key={workout.id}
workout={workout}
variant="horizontal"
onPress={() => handleWorkoutPress(workout.id)}
/>
))
)}
</ScrollView>
</View>
{/* Collections Grid */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<StyledText size={FONTS.TITLE_2} weight="semibold" color={colors.text.primary}>
{t('screens:home.collections')}
</StyledText>
</View>
<View style={styles.collectionsGrid}>
{isLoadingCollections ? (
// Loading skeletons for collections
<>
<CollectionCardSkeleton />
<CollectionCardSkeleton />
</>
) : (
collections.map((collection) => (
<CollectionCard
key={collection.id}
collection={collection}
onPress={() => handleCollectionPress(collection.id)}
/>
))
)}
</View>
</View>
</ScrollView>
</View>
)
}
// ═══════════════════════════════════════════════════════════════════════════
// STYLES
// ═══════════════════════════════════════════════════════════════════════════
function createStyles(colors: ThemeColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.bg.base,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingHorizontal: LAYOUT.SCREEN_PADDING,
},
// Hero Section
heroSection: {
marginBottom: SPACING[6],
},
heroHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: SPACING[2],
},
heroTitle: {
flex: 1,
marginRight: SPACING[3],
},
profileButton: {
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
// Sections
section: {
marginBottom: SPACING[8],
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING[4],
},
// Categories
categoriesScroll: {
gap: SPACING[2],
paddingRight: SPACING[4],
},
categoryChip: {
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[2],
borderRadius: RADIUS.FULL,
overflow: 'hidden',
borderWidth: 1,
borderColor: colors.border.glass,
},
categoryChipActive: {
borderColor: BRAND.PRIMARY,
backgroundColor: `${BRAND.PRIMARY}30`,
},
// Workouts Scroll
workoutsScroll: {
gap: SPACING[3],
paddingRight: SPACING[4],
},
// Collections Grid
collectionsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: SPACING[3],
},
})
}