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

@@ -15,7 +15,7 @@ import {
} from 'react-native'
import { useRouter } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import Ionicons from '@expo/vector-icons/Ionicons'
import { Icon } from '@/src/shared/components/Icon'
import { Alert } from 'react-native'
import { useTranslation } from 'react-i18next'
@@ -85,7 +85,7 @@ function ProblemScreen({ onNext }: { onNext: () => void }) {
marginBottom: SPACING[8],
}}
>
<Ionicons name="time" size={80} color={BRAND.PRIMARY} />
<Icon name="clock.fill" size={80} color={BRAND.PRIMARY} />
</Animated.View>
<Animated.View style={{ opacity: textOpacity, alignItems: 'center' }}>
@@ -136,10 +136,10 @@ function ProblemScreen({ onNext }: { onNext: () => void }) {
// ═══════════════════════════════════════════════════════════════════════════
const BARRIERS = [
{ id: 'no-time', labelKey: 'onboarding.empathy.noTime' as const, icon: 'time-outline' as const },
{ id: 'low-motivation', labelKey: 'onboarding.empathy.lowMotivation' as const, icon: 'battery-dead-outline' as const },
{ id: 'no-knowledge', labelKey: 'onboarding.empathy.noKnowledge' as const, icon: 'help-circle-outline' as const },
{ id: 'no-gym', labelKey: 'onboarding.empathy.noGym' as const, icon: 'home-outline' as const },
{ id: 'no-time', labelKey: 'onboarding.empathy.noTime' as const, icon: 'clock' as const },
{ id: 'low-motivation', labelKey: 'onboarding.empathy.lowMotivation' as const, icon: 'battery.0percent' as const },
{ id: 'no-knowledge', labelKey: 'onboarding.empathy.noKnowledge' as const, icon: 'questionmark.circle' as const },
{ id: 'no-gym', labelKey: 'onboarding.empathy.noGym' as const, icon: 'house' as const },
]
function EmpathyScreen({
@@ -187,7 +187,7 @@ function EmpathyScreen({
]}
onPress={() => toggleBarrier(item.id)}
>
<Ionicons
<Icon
name={item.icon}
size={28}
color={selected ? BRAND.PRIMARY : colors.text.tertiary}
@@ -373,10 +373,10 @@ function SolutionScreen({ onNext }: { onNext: () => void }) {
// ═══════════════════════════════════════════════════════════════════════════
const WOW_FEATURES = [
{ icon: 'timer-outline' as const, iconColor: BRAND.PRIMARY, titleKey: 'onboarding.wow.card1Title', subtitleKey: 'onboarding.wow.card1Subtitle' },
{ icon: 'barbell-outline' as const, iconColor: PHASE.REST, titleKey: 'onboarding.wow.card2Title', subtitleKey: 'onboarding.wow.card2Subtitle' },
{ icon: 'mic-outline' as const, iconColor: PHASE.PREP, titleKey: 'onboarding.wow.card3Title', subtitleKey: 'onboarding.wow.card3Subtitle' },
{ icon: 'trending-up-outline' as const, iconColor: PHASE.COMPLETE, titleKey: 'onboarding.wow.card4Title', subtitleKey: 'onboarding.wow.card4Subtitle' },
{ icon: 'timer' as const, iconColor: BRAND.PRIMARY, titleKey: 'onboarding.wow.card1Title', subtitleKey: 'onboarding.wow.card1Subtitle' },
{ icon: 'dumbbell' as const, iconColor: PHASE.REST, titleKey: 'onboarding.wow.card2Title', subtitleKey: 'onboarding.wow.card2Subtitle' },
{ icon: 'mic' as const, iconColor: PHASE.PREP, titleKey: 'onboarding.wow.card3Title', subtitleKey: 'onboarding.wow.card3Subtitle' },
{ icon: 'arrow.up.right' as const, iconColor: PHASE.COMPLETE, titleKey: 'onboarding.wow.card4Title', subtitleKey: 'onboarding.wow.card4Subtitle' },
] as const
function WowScreen({ onNext }: { onNext: () => void }) {
@@ -453,7 +453,7 @@ function WowScreen({ onNext }: { onNext: () => void }) {
]}
>
<View style={[wowStyles.iconCircle, { backgroundColor: `${feature.iconColor}26` }]}>
<Ionicons name={feature.icon} size={22} color={feature.iconColor} />
<Icon name={feature.icon} size={22} color={feature.iconColor} />
</View>
<View style={wowStyles.textCol}>
<StyledText size={17} weight="semibold" color={colors.text.primary}>
@@ -822,7 +822,7 @@ function PaywallScreen({
key={featureKey}
style={[styles.featureRow, { opacity: featureAnims[i] }]}
>
<Ionicons name="checkmark-circle" size={22} color={BRAND.SUCCESS} />
<Icon name="checkmark.circle.fill" size={22} color={BRAND.SUCCESS} />
<StyledText
size={16}
color={colors.text.primary}
@@ -1036,6 +1036,15 @@ export default function OnboardingScreen() {
setStep(next)
}, [step, barriers, name, level, goal, frequency])
const prevStep = useCallback(() => {
if (step > 1) {
const prev = step - 1
stepStartTime.current = Date.now()
track('onboarding_step_back', { from_step: step, to_step: prev })
setStep(prev)
}
}, [step])
const renderStep = () => {
switch (step) {
case 1:
@@ -1079,7 +1088,7 @@ export default function OnboardingScreen() {
}
return (
<OnboardingStep step={step} totalSteps={TOTAL_STEPS}>
<OnboardingStep step={step} totalSteps={TOTAL_STEPS} onBack={prevStep}>
{renderStep()}
</OnboardingStep>
)