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

@@ -3,11 +3,13 @@
* Premium stats dashboard — streak, rings, weekly chart, history
*/
import { View, StyleSheet, ScrollView, Dimensions } from 'react-native'
import { View, StyleSheet, ScrollView, Dimensions, Pressable } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useRouter } from 'expo-router'
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 Svg, { Circle } from 'react-native-svg'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -45,39 +47,32 @@ function StatRing({
const progress = Math.min(value / max, 1)
const strokeDashoffset = circumference * (1 - progress)
// We'll use a View-based ring since SVG isn't available
// Use border trick for a circular progress indicator
return (
<View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
<Svg width={size} height={size}>
{/* Track */}
<View
style={{
position: 'absolute',
width: size,
height: size,
borderRadius: size / 2,
borderWidth: strokeWidth,
borderColor: colors.bg.overlay2,
}}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={colors.bg.overlay2}
strokeWidth={strokeWidth}
fill="none"
/>
{/* Fill — simplified: show a colored ring proportional to progress */}
<View
style={{
position: 'absolute',
width: size,
height: size,
borderRadius: size / 2,
borderWidth: strokeWidth,
borderColor: color,
borderTopColor: progress > 0.25 ? color : 'transparent',
borderRightColor: progress > 0.5 ? color : 'transparent',
borderBottomColor: progress > 0.75 ? color : 'transparent',
borderLeftColor: progress > 0 ? color : 'transparent',
transform: [{ rotate: '-90deg' }],
opacity: progress > 0 ? 1 : 0.3,
}}
{/* Progress */}
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${size / 2} ${size / 2})`}
opacity={progress > 0 ? 1 : 0.3}
/>
</View>
</Svg>
)
}
@@ -96,7 +91,7 @@ function StatCard({
value: number
max: number
color: string
icon: keyof typeof Ionicons.glyphMap
icon: IconName
}) {
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
@@ -113,7 +108,7 @@ function StatCard({
{label}
</StyledText>
</View>
<Ionicons name={icon} size={18} color={color} />
<Icon name={icon} size={18} tintColor={color} />
</View>
</View>
)
@@ -155,6 +150,42 @@ function WeeklyBar({
)
}
// ═══════════════════════════════════════════════════════════════════════════
// EMPTY STATE
// ═══════════════════════════════════════════════════════════════════════════
function EmptyState({ onStartWorkout }: { onStartWorkout: () => void }) {
const { t } = useTranslation()
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
return (
<View style={styles.emptyState}>
<View style={styles.emptyIconCircle}>
<Icon name="flame" size={48} tintColor={BRAND.PRIMARY} />
</View>
<StyledText size={22} weight="bold" color={colors.text.primary} style={styles.emptyTitle}>
{t('screens:activity.emptyTitle')}
</StyledText>
<StyledText size={15} color={colors.text.tertiary} style={styles.emptySubtitle}>
{t('screens:activity.emptySubtitle')}
</StyledText>
<Pressable style={styles.emptyCtaButton} onPress={onStartWorkout}>
<LinearGradient
colors={GRADIENTS.CTA}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<Icon name="play.fill" size={18} tintColor="#FFFFFF" style={{ marginRight: SPACING[2] }} />
<StyledText size={16} weight="semibold" color="#FFFFFF">
{t('screens:activity.startFirstWorkout')}
</StyledText>
</Pressable>
</View>
)
}
// ═══════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ═══════════════════════════════════════════════════════════════════════════
@@ -164,6 +195,7 @@ const DAY_KEYS = ['days.sun', 'days.mon', 'days.tue', 'days.wed', 'days.thu', 'd
export default function ActivityScreen() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()
const router = useRouter()
const colors = useThemeColors()
const styles = useMemo(() => createStyles(colors), [colors])
const streak = useActivityStore((s) => s.streak)
@@ -216,6 +248,11 @@ export default function ActivityScreen() {
{t('screens:activity.title')}
</StyledText>
{/* Empty state when no history */}
{history.length === 0 ? (
<EmptyState onStartWorkout={() => router.push('/(tabs)/explore' as any)} />
) : (
<>
{/* Streak Banner */}
<View style={styles.streakBanner}>
<LinearGradient
@@ -226,7 +263,7 @@ export default function ActivityScreen() {
/>
<View style={styles.streakRow}>
<View style={styles.streakIconWrap}>
<Ionicons name="flame" size={28} color="#FFFFFF" />
<Icon name="flame.fill" size={28} tintColor="#FFFFFF" />
</View>
<View style={{ flex: 1 }}>
<StyledText size={28} weight="bold" color="#FFFFFF">
@@ -254,28 +291,28 @@ export default function ActivityScreen() {
value={totalWorkouts}
max={100}
color={BRAND.PRIMARY}
icon="barbell-outline"
icon="dumbbell"
/>
<StatCard
label={t('screens:activity.minutes')}
value={totalMinutes}
max={300}
color={PHASE.REST}
icon="time-outline"
icon="clock"
/>
<StatCard
label={t('screens:activity.calories')}
value={totalCalories}
max={5000}
color={BRAND.SECONDARY}
icon="flash-outline"
icon="bolt"
/>
<StatCard
label={t('screens:activity.bestStreak')}
value={streak.longest}
max={30}
color={BRAND.SUCCESS}
icon="trending-up-outline"
icon="arrow.up.right"
/>
</View>
@@ -358,10 +395,10 @@ export default function ActivityScreen() {
: { backgroundColor: 'rgba(255, 255, 255, 0.04)' },
]}
>
<Ionicons
name={a.unlocked ? 'trophy' : 'lock-closed'}
<Icon
name={a.unlocked ? 'trophy.fill' : 'lock.fill'}
size={22}
color={a.unlocked ? BRAND.PRIMARY : colors.text.hint}
tintColor={a.unlocked ? BRAND.PRIMARY : colors.text.hint}
/>
</View>
<StyledText
@@ -377,6 +414,8 @@ export default function ActivityScreen() {
))}
</View>
</View>
</>
)}
</ScrollView>
</View>
)
@@ -549,5 +588,41 @@ function createStyles(colors: ThemeColors) {
alignItems: 'center',
justifyContent: 'center',
},
// Empty State
emptyState: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: SPACING[10],
paddingHorizontal: SPACING[6],
},
emptyIconCircle: {
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: `${BRAND.PRIMARY}15`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: SPACING[6],
},
emptyTitle: {
textAlign: 'center' as const,
marginBottom: SPACING[2],
},
emptySubtitle: {
textAlign: 'center' as const,
lineHeight: 22,
marginBottom: SPACING[8],
},
emptyCtaButton: {
flexDirection: 'row' as const,
alignItems: 'center' as const,
justifyContent: 'center' as const,
height: 52,
paddingHorizontal: SPACING[8],
borderRadius: RADIUS.LG,
overflow: 'hidden' as const,
},
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ import { useRouter } 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 { useMemo, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@@ -23,6 +23,11 @@ import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import type { ProgramId } from '@/src/shared/types'
// Feature flags — disable incomplete features
const FEATURE_FLAGS = {
ASSESSMENT_ENABLED: false, // Assessment player not yet implemented
}
const FONTS = {
LARGE_TITLE: 34,
TITLE: 28,
@@ -33,19 +38,19 @@ const FONTS = {
}
// Program metadata for display
const PROGRAM_META: Record<ProgramId, { icon: keyof typeof Ionicons.glyphMap; gradient: [string, string]; accent: string }> = {
const PROGRAM_META: Record<ProgramId, { icon: IconName; gradient: [string, string]; accent: string }> = {
'upper-body': {
icon: 'barbell-outline',
icon: 'dumbbell',
gradient: ['#FF6B35', '#FF3B30'],
accent: '#FF6B35',
},
'lower-body': {
icon: 'footsteps-outline',
icon: 'figure.walk',
gradient: ['#30D158', '#28A745'],
accent: '#30D158',
},
'full-body': {
icon: 'flame-outline',
icon: 'flame',
gradient: ['#5AC8FA', '#007AFF'],
accent: '#5AC8FA',
},
@@ -143,7 +148,7 @@ function ProgramCard({
style={styles.programIconGradient}
/>
<View style={styles.programIconInner}>
<Ionicons name={meta.icon} size={24} color="#FFFFFF" />
<Icon name={meta.icon} size={24} tintColor="#FFFFFF" />
</View>
</View>
<View style={styles.programHeaderText}>
@@ -220,10 +225,10 @@ function ProgramCard({
: t('programs.continue')
}
</StyledText>
<Ionicons
name={programStatus === 'completed' ? 'refresh' : 'arrow-forward'}
<Icon
name={programStatus === 'completed' ? 'arrow.clockwise' : 'arrow.right'}
size={17}
color="#FFFFFF"
tintColor="#FFFFFF"
style={styles.ctaIcon}
/>
</LinearGradient>
@@ -248,9 +253,9 @@ function QuickStats() {
const totalMinutes = useMemo(() => history.reduce((sum, r) => sum + r.durationMinutes, 0), [history])
const stats = [
{ icon: 'flame' as const, value: streak.current, label: t('home.statsStreak'), color: BRAND.PRIMARY },
{ icon: 'calendar-outline' as const, value: `${thisWeekCount}/7`, label: t('home.statsThisWeek'), color: '#5AC8FA' },
{ icon: 'time-outline' as const, value: totalMinutes, label: t('home.statsMinutes'), color: '#30D158' },
{ icon: 'flame.fill' as const, value: streak.current, label: t('home.statsStreak'), color: BRAND.PRIMARY },
{ icon: 'calendar' as const, value: `${thisWeekCount}/7`, label: t('home.statsThisWeek'), color: '#5AC8FA' },
{ icon: 'clock' as const, value: totalMinutes, label: t('home.statsMinutes'), color: '#30D158' },
]
return (
@@ -262,7 +267,7 @@ function QuickStats() {
tint={colors.glass.blurTint}
style={StyleSheet.absoluteFill}
/>
<Ionicons name={stat.icon} size={16} color={stat.color} />
<Icon name={stat.icon} size={16} tintColor={stat.color} />
<StyledText size={17} weight="bold" color={colors.text.primary} style={{ fontVariant: ['tabular-nums'] }}>
{String(stat.value)}
</StyledText>
@@ -323,7 +328,7 @@ function AssessmentCard({ onPress }: { onPress: () => void }) {
style={StyleSheet.absoluteFill}
/>
<View style={styles.assessmentIconInner}>
<Ionicons name="clipboard-outline" size={22} color="#FFFFFF" />
<Icon name="clipboard" size={22} tintColor="#FFFFFF" />
</View>
</View>
<View style={styles.assessmentText}>
@@ -335,7 +340,7 @@ function AssessmentCard({ onPress }: { onPress: () => void }) {
</StyledText>
</View>
<View style={styles.assessmentArrow}>
<Ionicons name="arrow-forward" size={16} color={BRAND.PRIMARY} />
<Icon name="arrow.right" size={16} tintColor={BRAND.PRIMARY} />
</View>
</View>
</Pressable>
@@ -405,7 +410,7 @@ export default function HomeScreen() {
{/* Inline streak badge */}
{streak.current > 0 && (
<View style={styles.streakBadge}>
<Ionicons name="flame" size={13} color={BRAND.PRIMARY} />
<Icon name="flame.fill" size={13} tintColor={BRAND.PRIMARY} />
<StyledText size={12} weight="bold" color={BRAND.PRIMARY} style={{ fontVariant: ['tabular-nums'] }}>
{streak.current}
</StyledText>
@@ -426,8 +431,10 @@ export default function HomeScreen() {
{/* Quick Stats Row */}
<QuickStats />
{/* Assessment Card (if not completed) */}
<AssessmentCard onPress={handleAssessmentPress} />
{/* Assessment Card (if not completed and feature enabled) */}
{FEATURE_FLAGS.ASSESSMENT_ENABLED && (
<AssessmentCard onPress={handleAssessmentPress} />
)}
{/* Program Cards */}
<View style={styles.programsSection}>
@@ -460,7 +467,7 @@ export default function HomeScreen() {
tint={colors.glass.blurTint}
style={StyleSheet.absoluteFill}
/>
<Ionicons name="shuffle-outline" size={16} color={colors.text.secondary} />
<Icon name="shuffle" size={16} tintColor={colors.text.secondary} />
<StyledText size={14} weight="medium" color={colors.text.secondary}>
{t('home.switchProgram')}
</StyledText>

View File

@@ -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],
},
})
}