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

@@ -9,15 +9,15 @@ import {
StyleSheet,
ScrollView,
Pressable,
Text,
} from 'react-native'
import { useRouter } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { LinearGradient } from 'expo-linear-gradient'
import Ionicons from '@expo/vector-icons/Ionicons'
import { Icon, type IconName } from '@/src/shared/components/Icon'
import { useTranslation } from 'react-i18next'
import { useHaptics, usePurchases } from '@/src/shared/hooks'
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 } from '@/src/shared/constants/spacing'
@@ -27,13 +27,13 @@ import { RADIUS } from '@/src/shared/constants/borderRadius'
// FEATURES LIST
// ═══════════════════════════════════════════════════════════════════════════
const PREMIUM_FEATURES = [
{ icon: 'musical-notes', key: 'music' },
const PREMIUM_FEATURES: { icon: IconName; key: string }[] = [
{ icon: 'music.note.list', key: 'music' },
{ icon: 'infinity', key: 'workouts' },
{ icon: 'stats-chart', key: 'stats' },
{ icon: 'flame', key: 'calories' },
{ icon: 'notifications', key: 'reminders' },
{ icon: 'close-circle', key: 'ads' },
{ icon: 'chart.bar.fill', key: 'stats' },
{ icon: 'flame.fill', key: 'calories' },
{ icon: 'bell.fill', key: 'reminders' },
{ icon: 'xmark.circle.fill', key: 'ads' },
]
// ═══════════════════════════════════════════════════════════════════════════
@@ -93,23 +93,23 @@ function PlanCard({
>
{savings && (
<View style={styles.savingsBadge}>
<Text style={styles.savingsText}>{savings}</Text>
<StyledText size={10} weight="bold" color={colors.text.primary}>{savings}</StyledText>
</View>
)}
<View style={styles.planInfo}>
<Text style={[styles.planTitle, { color: colors.text.primary }]}>
<StyledText size={16} weight="semibold" color={colors.text.primary}>
{title}
</Text>
<Text style={[styles.planPeriod, { color: colors.text.tertiary }]}>
</StyledText>
<StyledText size={13} color={colors.text.tertiary} style={{ marginTop: 2 }}>
{period}
</Text>
</StyledText>
</View>
<Text style={[styles.planPrice, { color: BRAND.PRIMARY }]}>
<StyledText size={20} weight="bold" color={BRAND.PRIMARY}>
{price}
</Text>
</StyledText>
{isSelected && (
<View style={styles.checkmark}>
<Ionicons name="checkmark-circle" size={24} color={BRAND.PRIMARY} />
<Icon name="checkmark.circle.fill" size={24} color={BRAND.PRIMARY} />
</View>
)}
</Pressable>
@@ -196,7 +196,7 @@ export default function PaywallScreen() {
<View style={[styles.container, { paddingTop: insets.top }]}>
{/* Close Button */}
<Pressable style={[styles.closeButton, { top: insets.top + SPACING[2] }]} onPress={handleClose}>
<Ionicons name="close" size={28} color={colors.text.secondary} />
<Icon name="xmark" size={28} color={colors.text.secondary} />
</Pressable>
<ScrollView
@@ -209,8 +209,12 @@ export default function PaywallScreen() {
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>TabataFit+</Text>
<Text style={styles.subtitle}>{t('paywall.subtitle')}</Text>
<StyledText size={32} weight="bold" color={colors.text.primary} style={{ textAlign: 'center' }}>
TabataFit+
</StyledText>
<StyledText size={16} color={colors.text.secondary} style={{ textAlign: 'center', marginTop: SPACING[2] }}>
{t('paywall.subtitle')}
</StyledText>
</View>
{/* Features Grid */}
@@ -218,11 +222,11 @@ export default function PaywallScreen() {
{PREMIUM_FEATURES.map((feature) => (
<View key={feature.key} style={styles.featureItem}>
<View style={[styles.featureIcon, { backgroundColor: colors.glass.tinted.backgroundColor }]}>
<Ionicons name={feature.icon as any} size={22} color={BRAND.PRIMARY} />
<Icon name={feature.icon} size={22} color={BRAND.PRIMARY} />
</View>
<Text style={[styles.featureText, { color: colors.text.secondary }]}>
<StyledText size={13} color={colors.text.secondary} style={{ textAlign: 'center' }}>
{t(`paywall.features.${feature.key}`)}
</Text>
</StyledText>
</View>
))}
</View>
@@ -252,9 +256,9 @@ export default function PaywallScreen() {
{/* Price Note */}
{selectedPlan === 'annual' && (
<Text style={[styles.priceNote, { color: colors.text.tertiary }]}>
<StyledText size={13} color={colors.text.tertiary} style={{ textAlign: 'center', marginTop: SPACING[3] }}>
{t('paywall.equivalent', { price: annualMonthlyEquivalent })}
</Text>
</StyledText>
)}
{/* CTA Button */}
@@ -269,23 +273,23 @@ export default function PaywallScreen() {
end={{ x: 1, y: 1 }}
style={styles.ctaGradient}
>
<Text style={[styles.ctaText, { color: colors.text.primary }]}>
{isLoading ? t('paywall.processing') : t('paywall.subscribe')}
</Text>
<StyledText size={17} weight="semibold" color="#FFFFFF">
{isLoading ? t('paywall.processing') : t('paywall.trialCta')}
</StyledText>
</LinearGradient>
</Pressable>
{/* Restore & Terms */}
<View style={styles.footer}>
<Pressable onPress={handleRestore}>
<Text style={[styles.restoreText, { color: colors.text.tertiary }]}>
<StyledText size={14} color={colors.text.tertiary}>
{t('paywall.restore')}
</Text>
</StyledText>
</Pressable>
<Text style={[styles.termsText, { color: colors.text.tertiary }]}>
<StyledText size={11} color={colors.text.tertiary} style={{ textAlign: 'center', lineHeight: 18, paddingHorizontal: SPACING[4] }}>
{t('paywall.terms')}
</Text>
</StyledText>
</View>
</ScrollView>
</View>