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

@@ -17,7 +17,7 @@ import { useRouter, useLocalSearchParams } 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 * as Sharing from 'expo-sharing'
import { useTranslation } from 'react-i18next'
@@ -50,7 +50,7 @@ function SecondaryButton({
}: {
onPress: () => void
children: React.ReactNode
icon?: keyof typeof Ionicons.glyphMap
icon?: IconName
}) {
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
@@ -80,7 +80,7 @@ function SecondaryButton({
style={{ width: '100%' }}
>
<Animated.View style={[styles.secondaryButton, { transform: [{ scale: scaleAnim }] }]}>
{icon && <Ionicons name={icon} size={18} color={colors.text.primary} style={styles.buttonIcon} />}
{icon && <Icon name={icon} size={18} tintColor={colors.text.primary} style={styles.buttonIcon} />}
<RNText style={styles.secondaryButtonText}>{children}</RNText>
</Animated.View>
</Pressable>
@@ -194,7 +194,7 @@ function StatCard({
}: {
value: string | number
label: string
icon: keyof typeof Ionicons.glyphMap
icon: IconName
delay?: number
}) {
const colors = useThemeColors()
@@ -215,7 +215,7 @@ function StatCard({
return (
<Animated.View style={[styles.statCard, { transform: [{ scale: scaleAnim }] }]}>
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
<Ionicons name={icon} size={24} color={BRAND.PRIMARY} />
<Icon name={icon} size={24} tintColor={BRAND.PRIMARY} />
<RNText style={styles.statValue}>{value}</RNText>
<RNText style={styles.statLabel}>{label}</RNText>
</Animated.View>
@@ -306,6 +306,11 @@ export default function WorkoutCompleteScreen() {
router.push(`/workout/${workoutId}`)
}
// Fire celebration haptic on mount
useEffect(() => {
haptics.workoutComplete()
}, [])
// Check if we should show sync prompt (after first workout for premium users)
useEffect(() => {
if (profile.syncStatus === 'prompt-pending') {
@@ -373,9 +378,9 @@ export default function WorkoutCompleteScreen() {
{/* Stats Grid */}
<View style={styles.statsGrid}>
<StatCard value={resultCalories} label={t('screens:complete.caloriesLabel')} icon="flame" delay={100} />
<StatCard value={resultMinutes} label={t('screens:complete.minutesLabel')} icon="time" delay={200} />
<StatCard value="100%" label={t('screens:complete.completeLabel')} icon="checkmark-circle" delay={300} />
<StatCard value={resultCalories} label={t('screens:complete.caloriesLabel')} icon="flame.fill" delay={100} />
<StatCard value={resultMinutes} label={t('screens:complete.minutesLabel')} icon="clock.fill" delay={200} />
<StatCard value="100%" label={t('screens:complete.completeLabel')} icon="checkmark.circle.fill" delay={300} />
</View>
{/* Burn Bar */}
@@ -386,7 +391,7 @@ export default function WorkoutCompleteScreen() {
{/* Streak */}
<View style={styles.streakSection}>
<View style={styles.streakBadge}>
<Ionicons name="flame" size={32} color={BRAND.PRIMARY} />
<Icon name="flame.fill" size={32} tintColor={BRAND.PRIMARY} />
</View>
<View style={styles.streakInfo}>
<RNText style={styles.streakTitle}>{t('screens:complete.streakTitle', { count: streak.current })}</RNText>
@@ -398,7 +403,7 @@ export default function WorkoutCompleteScreen() {
{/* Share Button */}
<View style={styles.shareSection}>
<SecondaryButton onPress={handleShare} icon="share-outline">
<SecondaryButton onPress={handleShare} icon="square.and.arrow.up">
{t('screens:complete.shareWorkout')}
</SecondaryButton>
</View>
@@ -421,7 +426,7 @@ export default function WorkoutCompleteScreen() {
colors={[BRAND.PRIMARY, BRAND.PRIMARY_LIGHT]}
style={StyleSheet.absoluteFill}
/>
<Ionicons name="flame" size={24} color="#FFFFFF" />
<Icon name="flame.fill" size={24} tintColor="#FFFFFF" />
</View>
<RNText style={styles.recommendedTitleText} numberOfLines={1}>{w.title}</RNText>
<RNText style={styles.recommendedDurationText}>{t('units.minUnit', { count: w.duration })}</RNText>