Replace workouts tab with explore tab connected to Supabase

- Rename workouts.tsx to explore.tsx with new functionality
- Add horizontal scrolling collections section with gradient cards
- Add featured workouts section
- Implement filtering by category (All, Full Body, Upper Body, Lower Body, Core, Cardio)
- Implement filtering by level (All Levels, Beginner, Intermediate, Advanced)
- Implement filtering by equipment (All, No Equipment, Band, Dumbbells, Mat)
- Add clear filters button when filters are active
- Add loading states with ActivityIndicator
- Add empty state for no results
- Update tab label from "Workouts" to "Explore"
- Add explore translations for en, fr, de, es
This commit is contained in:
Millian Lamiaux
2026-03-23 21:27:19 +01:00
parent 197324188c
commit 8703c484e8
7 changed files with 1120 additions and 446 deletions

View File

@@ -1,7 +1,7 @@
/**
* TabataFit Tab Layout
* Native iOS tabs with liquid glass effect
* 5 tabs: Home, Workouts, Activity, Browse, Profile
* 4 tabs: Home, Workouts, Activity, Profile
* Redirects to onboarding if not completed
*/
@@ -28,9 +28,9 @@ export default function TabLayout() {
<Label>{t('tabs.home')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="workouts">
<NativeTabs.Trigger name="explore">
<Icon sf={{ default: 'flame', selected: 'flame.fill' }} />
<Label>{t('tabs.workouts')}</Label>
<Label>{t('tabs.explore')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="activity">
@@ -38,11 +38,6 @@ export default function TabLayout() {
<Label>{t('tabs.activity')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="browse">
<Icon sf={{ default: 'square.grid.2x2', selected: 'square.grid.2x2.fill' }} />
<Label>{t('tabs.browse')}</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="profile">
<Icon sf={{ default: 'person', selected: 'person.fill' }} />
<Label>{t('tabs.profile')}</Label>

549
app/(tabs)/explore.tsx Normal file
View File

@@ -0,0 +1,549 @@
import { useState, useMemo } from 'react'
import { View, StyleSheet, ScrollView, Pressable, Dimensions, ActivityIndicator, FlatList } from 'react-native'
import { useRouter } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { BlurView } from 'expo-blur'
import { LinearGradient } from 'expo-linear-gradient'
import Ionicons from '@expo/vector-icons/Ionicons'
import { useTranslation } from 'react-i18next'
import { useHaptics } from '@/src/shared/hooks'
import { useWorkouts, useCollections, useFeaturedWorkouts, useTrainers } from '@/src/shared/hooks/useSupabaseData'
import { StyledText } from '@/src/shared/components/StyledText'
import { useThemeColors, BRAND } 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 { Workout, WorkoutCategory, WorkoutLevel } from '@/src/shared/types'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
const CARD_WIDTH = (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2
const COLLECTION_CARD_WIDTH = SCREEN_WIDTH * 0.7
type FilterState = {
category: WorkoutCategory | 'all'
level: WorkoutLevel | 'all'
equipment: string | 'all'
}
const CATEGORY_ICONS: Record<WorkoutCategory | 'all', string> = {
all: 'grid-outline',
'full-body': 'body-outline',
'upper-body': 'barbell-outline',
'lower-body': 'footsteps-outline',
core: 'fitness-outline',
cardio: 'heart-outline',
}
const CATEGORY_COLORS: Record<WorkoutCategory | 'all', string> = {
all: BRAND.PRIMARY,
'full-body': '#5AC8FA',
'upper-body': '#FF6B35',
'lower-body': '#30D158',
core: '#FF9500',
cardio: '#FF3B30',
}
const CATEGORY_TRANSLATION_KEYS: Record<WorkoutCategory | 'all', string> = {
all: 'all',
'full-body': 'fullBody',
'upper-body': 'upperBody',
'lower-body': 'lowerBody',
core: 'core',
cardio: 'cardio',
}
function CollectionCard({
title,
description,
icon,
gradient,
workoutCount,
onPress,
}: {
title: string
description: string
icon: string
gradient?: [string, string]
workoutCount: number
onPress: () => void
}) {
const colors = useThemeColors()
const haptics = useHaptics()
const gradientColors = gradient || [BRAND.PRIMARY, '#FF3B30']
return (
<Pressable
style={styles.collectionCard}
onPress={() => {
haptics.buttonTap()
onPress()
}}
>
<LinearGradient
colors={gradientColors}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<View style={styles.collectionOverlay}>
<View style={styles.collectionIcon}>
<Ionicons name={icon as any} size={28} color="#FFFFFF" />
</View>
<StyledText size={18} weight="bold" color="#FFFFFF" style={{ marginTop: SPACING[3] }}>
{title}
</StyledText>
<StyledText size={13} color="rgba(255,255,255,0.8)" numberOfLines={2} style={{ marginTop: SPACING[1] }}>
{description}
</StyledText>
<StyledText size={12} color="rgba(255,255,255,0.6)" style={{ marginTop: SPACING[2] }}>
{workoutCount} workouts
</StyledText>
</View>
</Pressable>
)
}
function WorkoutCard({
workout,
onPress,
}: {
workout: Workout
onPress: () => void
}) {
const colors = useThemeColors()
const categoryColor = CATEGORY_COLORS[workout.category]
return (
<Pressable
style={[styles.workoutCard, { borderColor: colors.border.glassLight }]}
onPress={onPress}
>
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
<LinearGradient
colors={[categoryColor + '20', 'transparent']}
style={StyleSheet.absoluteFill}
/>
<View style={[styles.durationBadge, { backgroundColor: categoryColor + '30' }]}>
<Ionicons name="time-outline" size={10} color={categoryColor} />
<StyledText size={11} weight="semibold" color={categoryColor} style={{ marginLeft: 3 }}>
{workout.duration} min
</StyledText>
</View>
<View style={styles.playArea}>
<View style={[styles.playCircle, { backgroundColor: categoryColor + '20' }]}>
<Ionicons name="play" size={18} color={categoryColor} />
</View>
</View>
<View style={styles.workoutInfo}>
<StyledText size={14} weight="semibold" color={colors.text.primary} numberOfLines={2}>
{workout.title}
</StyledText>
<StyledText size={11} color={colors.text.tertiary} style={{ marginTop: 2 }}>
{workout.level} · {workout.calories} cal
</StyledText>
</View>
</Pressable>
)
}
function FilterPill({
label,
isSelected,
color,
onPress,
}: {
label: string
isSelected: boolean
color: string
onPress: () => void
}) {
const colors = useThemeColors()
const haptics = useHaptics()
return (
<Pressable
style={[
styles.filterPill,
{
backgroundColor: isSelected ? color + '20' : colors.glass.base.backgroundColor,
borderColor: isSelected ? color : colors.border.glass,
},
]}
onPress={() => {
haptics.selection()
onPress()
}}
>
<StyledText
size={13}
weight={isSelected ? 'semibold' : 'medium'}
color={isSelected ? color : colors.text.secondary}
>
{label}
</StyledText>
</Pressable>
)
}
function LoadingSkeleton() {
const colors = useThemeColors()
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={BRAND.PRIMARY} />
<StyledText size={15} color={colors.text.secondary} style={{ marginTop: SPACING[3] }}>
Loading workouts...
</StyledText>
</View>
)
}
export default function ExploreScreen() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()
const router = useRouter()
const haptics = useHaptics()
const colors = useThemeColors()
const [filters, setFilters] = useState<FilterState>({
category: 'all',
level: 'all',
equipment: 'all',
})
const { data: workoutsData, isLoading: workoutsLoading } = useWorkouts()
const { data: collectionsData, isLoading: collectionsLoading } = useCollections()
const { data: featuredData } = useFeaturedWorkouts()
const workouts = workoutsData || []
const collections = collectionsData || []
const featured = featuredData || []
const filteredWorkouts = useMemo(() => {
return workouts.filter((workout) => {
if (filters.category !== 'all' && workout.category !== filters.category) return false
if (filters.level !== 'all' && workout.level !== filters.level) return false
if (filters.equipment !== 'all') {
if (filters.equipment === 'none') {
return workout.equipment.length === 0
}
return workout.equipment.some((e) => e.toLowerCase().includes(filters.equipment.toLowerCase()))
}
return true
})
}, [workouts, filters])
const categories: (WorkoutCategory | 'all')[] = ['all', 'full-body', 'upper-body', 'lower-body', 'core', 'cardio']
const levels: (WorkoutLevel | 'all')[] = ['all', 'Beginner', 'Intermediate', 'Advanced']
const equipmentOptions = ['all', 'none', 'band', 'dumbbells', 'mat']
const handleWorkoutPress = (id: string) => {
haptics.buttonTap()
router.push(`/workout/${id}`)
}
const handleCollectionPress = (id: string) => {
haptics.buttonTap()
router.push(`/collection/${id}` as any)
}
const clearFilters = () => {
haptics.selection()
setFilters({ category: 'all', level: 'all', equipment: 'all' })
}
const hasActiveFilters = filters.category !== 'all' || filters.level !== 'all' || filters.equipment !== 'all'
if (workoutsLoading || collectionsLoading) {
return (
<View style={[styles.container, { backgroundColor: colors.bg.base, paddingTop: insets.top }]}>
<LoadingSkeleton />
</View>
)
}
return (
<View style={[styles.container, { backgroundColor: colors.bg.base, paddingTop: insets.top }]}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
showsVerticalScrollIndicator={false}
>
<View style={styles.header}>
<StyledText size={34} weight="bold" color={colors.text.primary}>
{t('screens:explore.title')}
</StyledText>
<StyledText size={15} color={colors.text.tertiary}>
{t('screens:explore.workoutsCount', { count: workouts.length })}
</StyledText>
</View>
{collections.length > 0 && (
<View style={styles.collectionsSection}>
<StyledText size={22} weight="bold" color={colors.text.primary}>
{t('screens:explore.collections')}
</StyledText>
<FlatList
horizontal
data={collections}
keyExtractor={(item) => item.id}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.collectionsList}
renderItem={({ item }) => (
<CollectionCard
title={item.title}
description={item.description}
icon={item.icon}
gradient={item.gradient}
workoutCount={item.workoutIds.length}
onPress={() => handleCollectionPress(item.id)}
/>
)}
/>
</View>
)}
{featured.length > 0 && (
<View style={styles.section}>
<StyledText size={22} weight="bold" color={colors.text.primary}>
{t('screens:explore.featured')}
</StyledText>
<View style={styles.workoutsGrid}>
{featured.slice(0, 4).map((workout) => (
<WorkoutCard
key={workout.id}
workout={workout}
onPress={() => handleWorkoutPress(workout.id)}
/>
))}
</View>
</View>
)}
<View style={styles.filtersSection}>
<View style={styles.filterHeader}>
<StyledText size={22} weight="bold" color={colors.text.primary}>
{t('screens:explore.allWorkouts')}
</StyledText>
{hasActiveFilters && (
<Pressable onPress={clearFilters}>
<StyledText size={14} weight="medium" color={BRAND.PRIMARY}>
{t('screens:explore.clearFilters')}
</StyledText>
</Pressable>
)}
</View>
<View style={styles.filterRow}>
<StyledText size={12} weight="semibold" color={colors.text.tertiary}>
{t('screens:explore.filterCategory')}
</StyledText>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
{categories.map((cat) => (
<FilterPill
key={cat}
label={t(`common:categories.${CATEGORY_TRANSLATION_KEYS[cat]}`)}
isSelected={filters.category === cat}
color={CATEGORY_COLORS[cat]}
onPress={() => setFilters((f) => ({ ...f, category: cat }))}
/>
))}
</ScrollView>
<View style={[styles.filterRow, { marginTop: SPACING[3] }]}>
<StyledText size={12} weight="semibold" color={colors.text.tertiary}>
{t('screens:explore.filterLevel')}
</StyledText>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
{levels.map((level) => (
<FilterPill
key={level}
label={level === 'all' ? 'All Levels' : level}
isSelected={filters.level === level}
color={BRAND.PRIMARY}
onPress={() => setFilters((f) => ({ ...f, level }))}
/>
))}
</ScrollView>
<View style={[styles.filterRow, { marginTop: SPACING[3] }]}>
<StyledText size={12} weight="semibold" color={colors.text.tertiary}>
{t('screens:explore.filterEquipment')}
</StyledText>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll}>
{equipmentOptions.map((eq) => (
<FilterPill
key={eq}
label={eq === 'all' ? 'All Equipment' : t(`screens:explore.equipmentOptions.${eq}`)}
isSelected={filters.equipment === eq}
color={BRAND.PRIMARY}
onPress={() => setFilters((f) => ({ ...f, equipment: eq }))}
/>
))}
</ScrollView>
</View>
{filteredWorkouts.length > 0 ? (
<View style={styles.workoutsGrid}>
{filteredWorkouts.map((workout) => (
<WorkoutCard
key={workout.id}
workout={workout}
onPress={() => handleWorkoutPress(workout.id)}
/>
))}
</View>
) : (
<View style={styles.noResults}>
<Ionicons name="search-outline" size={48} color={colors.text.tertiary} />
<StyledText size={17} weight="semibold" color={colors.text.secondary} style={{ marginTop: SPACING[3] }}>
{t('screens:explore.noResults')}
</StyledText>
<StyledText size={14} color={colors.text.tertiary} style={{ marginTop: SPACING[1] }}>
{t('screens:explore.tryAdjustingFilters')}
</StyledText>
</View>
)}
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingHorizontal: LAYOUT.SCREEN_PADDING,
},
header: {
marginBottom: SPACING[6],
},
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
collectionsSection: {
marginBottom: SPACING[6],
},
collectionsList: {
marginTop: SPACING[4],
paddingRight: LAYOUT.SCREEN_PADDING,
},
collectionCard: {
width: COLLECTION_CARD_WIDTH,
height: 180,
borderRadius: RADIUS.XL,
marginRight: SPACING[3],
overflow: 'hidden',
},
collectionOverlay: {
flex: 1,
padding: SPACING[5],
backgroundColor: 'rgba(0,0,0,0.3)',
},
collectionIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: 'rgba(255,255,255,0.2)',
alignItems: 'center',
justifyContent: 'center',
},
section: {
marginBottom: SPACING[6],
},
filtersSection: {
marginBottom: SPACING[6],
},
filterHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING[4],
},
filterRow: {
marginBottom: SPACING[2],
},
filterScroll: {
marginHorizontal: -LAYOUT.SCREEN_PADDING,
paddingHorizontal: LAYOUT.SCREEN_PADDING,
},
filterPill: {
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[2],
borderRadius: RADIUS.FULL,
marginRight: SPACING[2],
borderWidth: 1,
borderCurve: 'continuous',
},
workoutsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: SPACING[3],
},
workoutCard: {
width: CARD_WIDTH,
height: 200,
borderRadius: RADIUS.GLASS_CARD,
overflow: 'hidden',
borderWidth: 1,
borderCurve: 'continuous',
},
durationBadge: {
position: 'absolute',
top: SPACING[3],
right: SPACING[3],
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: SPACING[2],
paddingVertical: 3,
borderRadius: RADIUS.SM,
},
playArea: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 70,
alignItems: 'center',
justifyContent: 'center',
},
playCircle: {
width: 44,
height: 44,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
},
workoutInfo: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: SPACING[3],
paddingTop: SPACING[2],
},
noResults: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: SPACING[10],
},
})

View File

@@ -1,354 +0,0 @@
/**
* TabataFit Workouts Screen
* Premium workout browser — scrollable category pills, trainers, workout grid
*/
import { useState, useRef, useMemo } from 'react'
import { View, StyleSheet, ScrollView, Pressable, Dimensions, Animated } 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 { useTranslation } from 'react-i18next'
import { useHaptics } from '@/src/shared/hooks'
import { WORKOUTS } from '@/src/shared/data'
import { useTranslatedCategories, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData'
import { StyledText } from '@/src/shared/components/StyledText'
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'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
const CARD_WIDTH = (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2
// ═══════════════════════════════════════════════════════════════════════════
// CATEGORY PILL
// ═══════════════════════════════════════════════════════════════════════════
function CategoryPill({
label,
selected,
onPress,
}: {
label: string
selected: boolean
onPress: () => void
}) {
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
return (
<Pressable
style={[styles.pill, selected && styles.pillSelected]}
onPress={onPress}
>
{selected && (
<LinearGradient
colors={GRADIENTS.CTA}
style={[StyleSheet.absoluteFill, { borderRadius: 20 }]}
/>
)}
<StyledText
size={14}
weight={selected ? 'semibold' : 'regular'}
color={selected ? '#FFFFFF' : colors.text.tertiary}
>
{label}
</StyledText>
</Pressable>
)
}
// ═══════════════════════════════════════════════════════════════════════════
// WORKOUT CARD
// ═══════════════════════════════════════════════════════════════════════════
function WorkoutCard({
title,
duration,
level,
levelLabel,
onPress,
}: {
title: string
duration: number
level: string
levelLabel: string
onPress: () => void
}) {
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
return (
<Pressable style={styles.workoutCard} onPress={onPress}>
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
{/* Subtle gradient accent at top */}
<LinearGradient
colors={[levelColor(level, colors) + '30', 'transparent']}
style={styles.cardGradient}
/>
{/* Duration badge */}
<View style={styles.durationBadge}>
<Ionicons name="time-outline" size={10} color={colors.text.secondary} />
<StyledText size={11} weight="semibold" color={colors.text.secondary} style={{ marginLeft: 3 }}>
{duration + ' min'}
</StyledText>
</View>
{/* Play button */}
<View style={styles.playArea}>
<View style={styles.playCircle}>
<Ionicons name="play" size={18} color={colors.text.primary} />
</View>
</View>
{/* Info */}
<View style={styles.workoutInfo}>
<StyledText size={14} weight="semibold" color={colors.text.primary} numberOfLines={2}>
{title}
</StyledText>
<View style={styles.levelRow}>
<View style={[styles.levelDot, { backgroundColor: levelColor(level, colors) }]} />
<StyledText size={11} color={colors.text.tertiary}>
{levelLabel}
</StyledText>
</View>
</View>
</Pressable>
)
}
function levelColor(level: string, colors: ThemeColors): string {
switch (level.toLowerCase()) {
case 'beginner': return BRAND.SUCCESS
case 'intermediate': return BRAND.SECONDARY
case 'advanced': return BRAND.DANGER
default: return colors.text.tertiary
}
}
// ═══════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ═══════════════════════════════════════════════════════════════════════════
export default function WorkoutsScreen() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()
const router = useRouter()
const haptics = useHaptics()
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
const [selectedCategory, setSelectedCategory] = useState('all')
const categories = useTranslatedCategories()
const filteredWorkouts = selectedCategory === 'all'
? WORKOUTS
: WORKOUTS.filter(w => w.category === selectedCategory)
const translatedFiltered = useTranslatedWorkouts(filteredWorkouts)
const handleWorkoutPress = (id: string) => {
haptics.buttonTap()
router.push(`/workout/${id}`)
}
const selectedLabel = categories.find(c => c.id === selectedCategory)?.label ?? t('screens:workouts.allWorkouts')
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<StyledText size={34} weight="bold" color={colors.text.primary}>
{t('screens:workouts.title')}
</StyledText>
<StyledText size={15} color={colors.text.tertiary}>
{t('screens:workouts.available', { count: WORKOUTS.length })}
</StyledText>
</View>
{/* Category Pills — horizontal scroll, no truncation */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.pillsRow}
style={styles.pillsScroll}
>
{categories.map((cat) => (
<CategoryPill
key={cat.id}
label={cat.label}
selected={selectedCategory === cat.id}
onPress={() => {
haptics.selection()
setSelectedCategory(cat.id)
}}
/>
))}
</ScrollView>
{/* Workouts Grid */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<StyledText size={20} weight="semibold" color={colors.text.primary}>
{selectedCategory === 'all' ? t('screens:workouts.allWorkouts') : selectedLabel}
</StyledText>
{selectedCategory !== 'all' && (
<Pressable onPress={() => { haptics.buttonTap(); router.push(`/workout/category/${selectedCategory}`) }}>
<StyledText size={14} color={BRAND.PRIMARY} weight="medium">{t('seeAll')}</StyledText>
</Pressable>
)}
</View>
<View style={styles.workoutsGrid}>
{translatedFiltered.map((workout) => (
<WorkoutCard
key={workout.id}
title={workout.title}
duration={workout.duration}
level={workout.level}
levelLabel={t(`levels.${workout.level.toLowerCase()}`)}
onPress={() => handleWorkoutPress(workout.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,
},
// Header
header: {
marginBottom: SPACING[4],
},
// Pills
pillsScroll: {
marginHorizontal: -LAYOUT.SCREEN_PADDING,
marginBottom: SPACING[6],
},
pillsRow: {
paddingHorizontal: LAYOUT.SCREEN_PADDING,
gap: SPACING[2],
},
pill: {
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[2],
borderRadius: 20,
backgroundColor: colors.bg.surface,
borderWidth: 1,
borderColor: colors.border.glassLight,
},
pillSelected: {
borderColor: BRAND.PRIMARY,
backgroundColor: 'transparent',
},
// Section
section: {
marginBottom: SPACING[6],
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING[4],
},
// Workouts Grid
workoutsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: SPACING[3],
},
workoutCard: {
width: CARD_WIDTH,
height: 190,
borderRadius: RADIUS.GLASS_CARD,
overflow: 'hidden',
borderWidth: 1,
borderColor: colors.bg.overlay2,
},
cardGradient: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 80,
},
durationBadge: {
position: 'absolute',
top: SPACING[3],
right: SPACING[3],
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
paddingHorizontal: SPACING[2],
paddingVertical: 3,
borderRadius: RADIUS.SM,
},
playArea: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 64,
alignItems: 'center',
justifyContent: 'center',
},
playCircle: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.border.glassStrong,
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.25)',
},
workoutInfo: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: SPACING[3],
paddingTop: SPACING[2],
},
levelRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 3,
gap: 5,
},
levelDot: {
width: 6,
height: 6,
borderRadius: 3,
},
})
}