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