feat: migrate icons to SF Symbols, refactor explore tab, add collections/programs data layer

- Replace all Ionicons with native SF Symbols via expo-symbols SymbolView
- Create reusable Icon wrapper component (src/shared/components/Icon.tsx)
- Remove @expo/vector-icons and lucide-react dependencies
- Refactor explore tab with filters, search, and category browsing
- Add collections and programs data with Supabase integration
- Add explore filter store and filter sheet
- Update i18n strings (en, de, es, fr) for new explore features
- Update test mocks and remove stale snapshots
- Add user fitness level to user store and types
This commit is contained in:
Millian Lamiaux
2026-03-25 23:28:51 +01:00
parent f11eb6b9ae
commit b833198e9d
42 changed files with 2006 additions and 1594 deletions

View File

@@ -8,7 +8,7 @@ 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 { Icon, type IconName } from '@/src/shared/components/Icon'
import { useMemo, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@@ -23,6 +23,11 @@ import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import type { ProgramId } from '@/src/shared/types'
// Feature flags — disable incomplete features
const FEATURE_FLAGS = {
ASSESSMENT_ENABLED: false, // Assessment player not yet implemented
}
const FONTS = {
LARGE_TITLE: 34,
TITLE: 28,
@@ -33,19 +38,19 @@ const FONTS = {
}
// Program metadata for display
const PROGRAM_META: Record<ProgramId, { icon: keyof typeof Ionicons.glyphMap; gradient: [string, string]; accent: string }> = {
const PROGRAM_META: Record<ProgramId, { icon: IconName; gradient: [string, string]; accent: string }> = {
'upper-body': {
icon: 'barbell-outline',
icon: 'dumbbell',
gradient: ['#FF6B35', '#FF3B30'],
accent: '#FF6B35',
},
'lower-body': {
icon: 'footsteps-outline',
icon: 'figure.walk',
gradient: ['#30D158', '#28A745'],
accent: '#30D158',
},
'full-body': {
icon: 'flame-outline',
icon: 'flame',
gradient: ['#5AC8FA', '#007AFF'],
accent: '#5AC8FA',
},
@@ -143,7 +148,7 @@ function ProgramCard({
style={styles.programIconGradient}
/>
<View style={styles.programIconInner}>
<Ionicons name={meta.icon} size={24} color="#FFFFFF" />
<Icon name={meta.icon} size={24} tintColor="#FFFFFF" />
</View>
</View>
<View style={styles.programHeaderText}>
@@ -220,10 +225,10 @@ function ProgramCard({
: t('programs.continue')
}
</StyledText>
<Ionicons
name={programStatus === 'completed' ? 'refresh' : 'arrow-forward'}
<Icon
name={programStatus === 'completed' ? 'arrow.clockwise' : 'arrow.right'}
size={17}
color="#FFFFFF"
tintColor="#FFFFFF"
style={styles.ctaIcon}
/>
</LinearGradient>
@@ -248,9 +253,9 @@ function QuickStats() {
const totalMinutes = useMemo(() => history.reduce((sum, r) => sum + r.durationMinutes, 0), [history])
const stats = [
{ icon: 'flame' as const, value: streak.current, label: t('home.statsStreak'), color: BRAND.PRIMARY },
{ icon: 'calendar-outline' as const, value: `${thisWeekCount}/7`, label: t('home.statsThisWeek'), color: '#5AC8FA' },
{ icon: 'time-outline' as const, value: totalMinutes, label: t('home.statsMinutes'), color: '#30D158' },
{ icon: 'flame.fill' as const, value: streak.current, label: t('home.statsStreak'), color: BRAND.PRIMARY },
{ icon: 'calendar' as const, value: `${thisWeekCount}/7`, label: t('home.statsThisWeek'), color: '#5AC8FA' },
{ icon: 'clock' as const, value: totalMinutes, label: t('home.statsMinutes'), color: '#30D158' },
]
return (
@@ -262,7 +267,7 @@ function QuickStats() {
tint={colors.glass.blurTint}
style={StyleSheet.absoluteFill}
/>
<Ionicons name={stat.icon} size={16} color={stat.color} />
<Icon name={stat.icon} size={16} tintColor={stat.color} />
<StyledText size={17} weight="bold" color={colors.text.primary} style={{ fontVariant: ['tabular-nums'] }}>
{String(stat.value)}
</StyledText>
@@ -323,7 +328,7 @@ function AssessmentCard({ onPress }: { onPress: () => void }) {
style={StyleSheet.absoluteFill}
/>
<View style={styles.assessmentIconInner}>
<Ionicons name="clipboard-outline" size={22} color="#FFFFFF" />
<Icon name="clipboard" size={22} tintColor="#FFFFFF" />
</View>
</View>
<View style={styles.assessmentText}>
@@ -335,7 +340,7 @@ function AssessmentCard({ onPress }: { onPress: () => void }) {
</StyledText>
</View>
<View style={styles.assessmentArrow}>
<Ionicons name="arrow-forward" size={16} color={BRAND.PRIMARY} />
<Icon name="arrow.right" size={16} tintColor={BRAND.PRIMARY} />
</View>
</View>
</Pressable>
@@ -405,7 +410,7 @@ export default function HomeScreen() {
{/* Inline streak badge */}
{streak.current > 0 && (
<View style={styles.streakBadge}>
<Ionicons name="flame" size={13} color={BRAND.PRIMARY} />
<Icon name="flame.fill" size={13} tintColor={BRAND.PRIMARY} />
<StyledText size={12} weight="bold" color={BRAND.PRIMARY} style={{ fontVariant: ['tabular-nums'] }}>
{streak.current}
</StyledText>
@@ -426,8 +431,10 @@ export default function HomeScreen() {
{/* Quick Stats Row */}
<QuickStats />
{/* Assessment Card (if not completed) */}
<AssessmentCard onPress={handleAssessmentPress} />
{/* Assessment Card (if not completed and feature enabled) */}
{FEATURE_FLAGS.ASSESSMENT_ENABLED && (
<AssessmentCard onPress={handleAssessmentPress} />
)}
{/* Program Cards */}
<View style={styles.programsSection}>
@@ -460,7 +467,7 @@ export default function HomeScreen() {
tint={colors.glass.blurTint}
style={StyleSheet.absoluteFill}
/>
<Ionicons name="shuffle-outline" size={16} color={colors.text.secondary} />
<Icon name="shuffle" size={16} tintColor={colors.text.secondary} />
<StyledText size={14} weight="medium" color={colors.text.secondary}>
{t('home.switchProgram')}
</StyledText>