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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user