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:
@@ -8,10 +8,8 @@ import {
|
||||
View,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Pressable,
|
||||
Switch,
|
||||
Text as RNText,
|
||||
TextStyle,
|
||||
} from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import * as Linking from 'expo-linking'
|
||||
@@ -22,41 +20,12 @@ import { useUserStore, useActivityStore } from '@/src/shared/stores'
|
||||
import { requestNotificationPermissions, usePurchases } from '@/src/shared/hooks'
|
||||
import { useThemeColors, BRAND } from '@/src/shared/theme'
|
||||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||||
import { StyledText } from '@/src/shared/components/StyledText'
|
||||
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
|
||||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||||
import { DataDeletionModal } from '@/src/shared/components/DataDeletionModal'
|
||||
import { deleteSyncedData } from '@/src/shared/services/sync'
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// STYLED TEXT COMPONENT
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
interface TextProps {
|
||||
children: React.ReactNode
|
||||
style?: TextStyle
|
||||
size?: number
|
||||
weight?: 'normal' | 'bold' | '600' | '700' | '800' | '900'
|
||||
color?: string
|
||||
center?: boolean
|
||||
}
|
||||
|
||||
function Text({ children, style, size, weight, color, center }: TextProps) {
|
||||
const colors = useThemeColors()
|
||||
return (
|
||||
<RNText
|
||||
style={[
|
||||
{
|
||||
fontSize: size ?? 17,
|
||||
fontWeight: weight ?? 'normal',
|
||||
color: color ?? colors.text.primary,
|
||||
textAlign: center ? 'center' : 'left',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</RNText>
|
||||
)
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COMPONENT: PROFILE SCREEN
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -150,24 +119,24 @@ export default function ProfileScreen() {
|
||||
<View style={styles.headerContainer}>
|
||||
{/* Avatar with gradient background */}
|
||||
<View style={styles.avatarContainer}>
|
||||
<Text size={48} weight="bold" color="#FFFFFF">
|
||||
<StyledText size={48} weight="bold" color="#FFFFFF">
|
||||
{avatarInitial}
|
||||
</Text>
|
||||
</StyledText>
|
||||
</View>
|
||||
|
||||
{/* Name & Plan */}
|
||||
<View style={styles.nameContainer}>
|
||||
<Text size={22} weight="600" center>
|
||||
<StyledText size={22} weight="semibold" style={{ textAlign: 'center' }}>
|
||||
{profile.name || t('profile.guest')}
|
||||
</Text>
|
||||
</StyledText>
|
||||
<View style={styles.planContainer}>
|
||||
<Text size={15} color={isPremium ? BRAND.PRIMARY : colors.text.tertiary}>
|
||||
<StyledText size={15} color={isPremium ? BRAND.PRIMARY : colors.text.tertiary}>
|
||||
{planLabel}
|
||||
</Text>
|
||||
</StyledText>
|
||||
{isPremium && (
|
||||
<Text size={12} color={BRAND.PRIMARY}>
|
||||
<StyledText size={12} color={BRAND.PRIMARY}>
|
||||
✓
|
||||
</Text>
|
||||
</StyledText>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
@@ -175,28 +144,28 @@ export default function ProfileScreen() {
|
||||
{/* Stats Row */}
|
||||
<View style={styles.statsContainer}>
|
||||
<View style={styles.statItem}>
|
||||
<Text size={20} weight="bold" color={BRAND.PRIMARY} center>
|
||||
<StyledText size={20} weight="bold" color={BRAND.PRIMARY} style={{ textAlign: 'center' }}>
|
||||
🔥 {stats.workouts}
|
||||
</Text>
|
||||
<Text size={12} color={colors.text.tertiary} center>
|
||||
</StyledText>
|
||||
<StyledText size={12} color={colors.text.tertiary} style={{ textAlign: 'center' }}>
|
||||
{t('profile.statsWorkouts')}
|
||||
</Text>
|
||||
</StyledText>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text size={20} weight="bold" color={BRAND.PRIMARY} center>
|
||||
<StyledText size={20} weight="bold" color={BRAND.PRIMARY} style={{ textAlign: 'center' }}>
|
||||
📅 {stats.streak}
|
||||
</Text>
|
||||
<Text size={12} color={colors.text.tertiary} center>
|
||||
</StyledText>
|
||||
<StyledText size={12} color={colors.text.tertiary} style={{ textAlign: 'center' }}>
|
||||
{t('profile.statsStreak')}
|
||||
</Text>
|
||||
</StyledText>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text size={20} weight="bold" color={BRAND.PRIMARY} center>
|
||||
<StyledText size={20} weight="bold" color={BRAND.PRIMARY} style={{ textAlign: 'center' }}>
|
||||
⚡️ {Math.round(stats.calories / 1000)}k
|
||||
</Text>
|
||||
<Text size={12} color={colors.text.tertiary} center>
|
||||
</StyledText>
|
||||
<StyledText size={12} color={colors.text.tertiary} style={{ textAlign: 'center' }}>
|
||||
{t('profile.statsCalories')}
|
||||
</Text>
|
||||
</StyledText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -207,32 +176,32 @@ export default function ProfileScreen() {
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
{!isPremium && (
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity
|
||||
<Pressable
|
||||
style={styles.premiumContainer}
|
||||
onPress={() => router.push('/paywall')}
|
||||
>
|
||||
<View style={styles.premiumContent}>
|
||||
<Text size={17} weight="600" color={BRAND.PRIMARY}>
|
||||
<StyledText size={17} weight="semibold" color={BRAND.PRIMARY}>
|
||||
✨ {t('profile.upgradeTitle')}
|
||||
</Text>
|
||||
<Text size={15} color={colors.text.tertiary} style={{ marginTop: 4 }}>
|
||||
</StyledText>
|
||||
<StyledText size={15} color={colors.text.tertiary} style={{ marginTop: SPACING[1] }}>
|
||||
{t('profile.upgradeDescription')}
|
||||
</Text>
|
||||
</StyledText>
|
||||
</View>
|
||||
<Text size={15} color={BRAND.PRIMARY} style={{ marginTop: 12 }}>
|
||||
<StyledText size={15} color={BRAND.PRIMARY} style={{ marginTop: SPACING[3] }}>
|
||||
{t('profile.learnMore')} →
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</StyledText>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
WORKOUT SETTINGS
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<Text style={styles.sectionHeader}>{t('profile.sectionWorkout')}</Text>
|
||||
<StyledText style={styles.sectionHeader}>{t('profile.sectionWorkout')}</StyledText>
|
||||
<View style={styles.section}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel}>{t('profile.hapticFeedback')}</Text>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.hapticFeedback')}</StyledText>
|
||||
<Switch
|
||||
value={settings.haptics}
|
||||
onValueChange={(v) => updateSettings({ haptics: v })}
|
||||
@@ -241,7 +210,7 @@ export default function ProfileScreen() {
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel}>{t('profile.soundEffects')}</Text>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.soundEffects')}</StyledText>
|
||||
<Switch
|
||||
value={settings.soundEffects}
|
||||
onValueChange={(v) => updateSettings({ soundEffects: v })}
|
||||
@@ -250,7 +219,7 @@ export default function ProfileScreen() {
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.row, styles.rowLast]}>
|
||||
<Text style={styles.rowLabel}>{t('profile.voiceCoaching')}</Text>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.voiceCoaching')}</StyledText>
|
||||
<Switch
|
||||
value={settings.voiceCoaching}
|
||||
onValueChange={(v) => updateSettings({ voiceCoaching: v })}
|
||||
@@ -263,10 +232,10 @@ export default function ProfileScreen() {
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
NOTIFICATIONS
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<Text style={styles.sectionHeader}>{t('profile.sectionNotifications')}</Text>
|
||||
<StyledText style={styles.sectionHeader}>{t('profile.sectionNotifications')}</StyledText>
|
||||
<View style={styles.section}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel}>{t('profile.dailyReminders')}</Text>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.dailyReminders')}</StyledText>
|
||||
<Switch
|
||||
value={settings.reminders}
|
||||
onValueChange={handleReminderToggle}
|
||||
@@ -276,8 +245,8 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
{settings.reminders && (
|
||||
<View style={styles.rowTime}>
|
||||
<Text style={styles.rowLabel}>{t('profile.reminderTime')}</Text>
|
||||
<Text style={styles.rowValue}>{settings.reminderTime}</Text>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.reminderTime')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>{settings.reminderTime}</StyledText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -287,18 +256,18 @@ export default function ProfileScreen() {
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
{isPremium && (
|
||||
<>
|
||||
<Text style={styles.sectionHeader}>Personalization</Text>
|
||||
<StyledText style={styles.sectionHeader}>{t('profile.sectionPersonalization')}</StyledText>
|
||||
<View style={styles.section}>
|
||||
<View style={[styles.row, styles.rowLast]}>
|
||||
<Text style={styles.rowLabel}>
|
||||
{profile.syncStatus === 'synced' ? 'Personalization Enabled' : 'Generic Programs'}
|
||||
</Text>
|
||||
<Text
|
||||
<StyledText style={styles.rowLabel}>
|
||||
{profile.syncStatus === 'synced' ? t('profile.personalizationEnabled') : t('profile.personalizationDisabled')}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
size={14}
|
||||
color={profile.syncStatus === 'synced' ? BRAND.SUCCESS : colors.text.tertiary}
|
||||
>
|
||||
{profile.syncStatus === 'synced' ? '✓' : '○'}
|
||||
</Text>
|
||||
</StyledText>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
@@ -307,28 +276,28 @@ export default function ProfileScreen() {
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
ABOUT
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<Text style={styles.sectionHeader}>{t('profile.sectionAbout')}</Text>
|
||||
<StyledText style={styles.sectionHeader}>{t('profile.sectionAbout')}</StyledText>
|
||||
<View style={styles.section}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel}>{t('profile.version')}</Text>
|
||||
<Text style={styles.rowValue}>{appVersion}</Text>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.version')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>{appVersion}</StyledText>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.row} onPress={handleRateApp}>
|
||||
<Text style={styles.rowLabel}>{t('profile.rateApp')}</Text>
|
||||
<Text style={styles.rowValue}>›</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.row} onPress={handleContactUs}>
|
||||
<Text style={styles.rowLabel}>{t('profile.contactUs')}</Text>
|
||||
<Text style={styles.rowValue}>›</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.row} onPress={handleFAQ}>
|
||||
<Text style={styles.rowLabel}>{t('profile.faq')}</Text>
|
||||
<Text style={styles.rowValue}>›</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.row, styles.rowLast]} onPress={handlePrivacyPolicy}>
|
||||
<Text style={styles.rowLabel}>{t('profile.privacyPolicy')}</Text>
|
||||
<Text style={styles.rowValue}>›</Text>
|
||||
</TouchableOpacity>
|
||||
<Pressable style={styles.row} onPress={handleRateApp}>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.rateApp')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>›</StyledText>
|
||||
</Pressable>
|
||||
<Pressable style={styles.row} onPress={handleContactUs}>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.contactUs')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>›</StyledText>
|
||||
</Pressable>
|
||||
<Pressable style={styles.row} onPress={handleFAQ}>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.faq')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>›</StyledText>
|
||||
</Pressable>
|
||||
<Pressable style={[styles.row, styles.rowLast]} onPress={handlePrivacyPolicy}>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.privacyPolicy')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>›</StyledText>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
@@ -336,12 +305,12 @@ export default function ProfileScreen() {
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
{isPremium && (
|
||||
<>
|
||||
<Text style={styles.sectionHeader}>{t('profile.sectionAccount')}</Text>
|
||||
<StyledText style={styles.sectionHeader}>{t('profile.sectionAccount')}</StyledText>
|
||||
<View style={styles.section}>
|
||||
<TouchableOpacity style={[styles.row, styles.rowLast]} onPress={handleRestore}>
|
||||
<Text style={styles.rowLabel}>{t('profile.restorePurchases')}</Text>
|
||||
<Text style={styles.rowValue}>›</Text>
|
||||
</TouchableOpacity>
|
||||
<Pressable style={[styles.row, styles.rowLast]} onPress={handleRestore}>
|
||||
<StyledText style={styles.rowLabel}>{t('profile.restorePurchases')}</StyledText>
|
||||
<StyledText style={styles.rowValue}>›</StyledText>
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -350,9 +319,9 @@ export default function ProfileScreen() {
|
||||
SIGN OUT
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
<View style={[styles.section, styles.signOutSection]}>
|
||||
<TouchableOpacity style={styles.button} onPress={handleSignOut}>
|
||||
<Text style={styles.destructive}>{t('profile.signOut')}</Text>
|
||||
</TouchableOpacity>
|
||||
<Pressable style={styles.button} onPress={handleSignOut}>
|
||||
<StyledText style={styles.destructive}>{t('profile.signOut')}</StyledText>
|
||||
</Pressable>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -383,10 +352,10 @@ function createStyles(colors: ThemeColors) {
|
||||
flexGrow: 1,
|
||||
},
|
||||
section: {
|
||||
marginHorizontal: 16,
|
||||
marginTop: 20,
|
||||
marginHorizontal: SPACING[4],
|
||||
marginTop: SPACING[5],
|
||||
backgroundColor: colors.bg.surface,
|
||||
borderRadius: 10,
|
||||
borderRadius: RADIUS.MD,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
sectionHeader: {
|
||||
@@ -394,14 +363,14 @@ function createStyles(colors: ThemeColors) {
|
||||
fontWeight: '600',
|
||||
color: colors.text.tertiary,
|
||||
textTransform: 'uppercase',
|
||||
marginLeft: 32,
|
||||
marginTop: 20,
|
||||
marginBottom: 8,
|
||||
marginLeft: SPACING[8],
|
||||
marginTop: SPACING[5],
|
||||
marginBottom: SPACING[2],
|
||||
},
|
||||
headerContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: SPACING[6],
|
||||
paddingHorizontal: SPACING[4],
|
||||
},
|
||||
avatarContainer: {
|
||||
width: 90,
|
||||
@@ -410,44 +379,40 @@ function createStyles(colors: ThemeColors) {
|
||||
backgroundColor: BRAND.PRIMARY,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
shadowColor: BRAND.PRIMARY,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 20,
|
||||
elevation: 10,
|
||||
boxShadow: `0 4px 20px ${BRAND.PRIMARY}80`,
|
||||
},
|
||||
nameContainer: {
|
||||
marginTop: 16,
|
||||
marginTop: SPACING[4],
|
||||
alignItems: 'center',
|
||||
},
|
||||
planContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
gap: 4,
|
||||
marginTop: SPACING[1],
|
||||
gap: SPACING[1],
|
||||
},
|
||||
statsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 16,
|
||||
gap: 32,
|
||||
marginTop: SPACING[4],
|
||||
gap: SPACING[8],
|
||||
},
|
||||
statItem: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
premiumContainer: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: SPACING[4],
|
||||
paddingHorizontal: SPACING[4],
|
||||
},
|
||||
premiumContent: {
|
||||
gap: 4,
|
||||
gap: SPACING[1],
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: SPACING[3],
|
||||
paddingHorizontal: SPACING[4],
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.border.glassLight,
|
||||
},
|
||||
@@ -466,13 +431,13 @@ function createStyles(colors: ThemeColors) {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: SPACING[3],
|
||||
paddingHorizontal: SPACING[4],
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: colors.border.glassLight,
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 14,
|
||||
paddingVertical: SPACING[3] + 2,
|
||||
alignItems: 'center',
|
||||
},
|
||||
destructive: {
|
||||
@@ -480,7 +445,7 @@ function createStyles(colors: ThemeColors) {
|
||||
color: BRAND.DANGER,
|
||||
},
|
||||
signOutSection: {
|
||||
marginTop: 20,
|
||||
marginTop: SPACING[5],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user