refactor screens, navigation & player for new architecture

Simplify Home, Activity, Profile, Complete, Player, and Program screens
to work with the new Supabase-driven data layer. Update root and tab
layouts. Add Settings, Terms, and Zone routes. Add OfflineBanner
component and progressStore. Update all player sub-components to use
the refreshed design system tokens.
This commit is contained in:
Millian Lamiaux
2026-04-21 21:50:48 +02:00
parent 04b83fc419
commit 5888aac08e
26 changed files with 1836 additions and 2772 deletions

223
app/settings.tsx Normal file
View File

@@ -0,0 +1,223 @@
/**
* Settings FormSheet
* Profile, preferences, premium, legal, reset progress.
*/
import { View, Text, StyleSheet, ScrollView, Pressable, Switch, Alert } from 'react-native'
import { useRouter } from 'expo-router'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import { Icon, type IconName } from '@/src/shared/components/Icon'
import { useUserStore } from '@/src/shared/stores/userStore'
import { useProgressStore } from '@/src/shared/stores/progressStore'
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
import { SPACING } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import { TEXT, NAVY, GREEN, BORDER_COLORS } from '@/src/shared/constants/colors'
export default function SettingsScreen() {
const router = useRouter()
const insets = useSafeAreaInsets()
const { t } = useTranslation()
const profile = useUserStore(s => s.profile)
const settings = useUserStore(s => s.settings)
const updateSettings = useUserStore(s => s.updateSettings)
const resetProgress = useProgressStore(s => s.resetProgress)
const isPremium = profile.subscription !== 'free'
const handleResetProgress = () => {
Alert.alert(
t('screens:settings.resetTitle'),
t('screens:settings.resetMessage'),
[
{ text: t('common:cancel'), style: 'cancel' },
{
text: t('screens:settings.resetConfirm'),
style: 'destructive',
onPress: () => {
resetProgress()
},
},
],
)
}
return (
<ScrollView
style={styles.container}
contentContainerStyle={{ padding: SPACING[5], paddingBottom: insets.bottom + SPACING[8] }}
>
<Text style={styles.header}>{t('screens:settings.title')}</Text>
{/* Profile */}
<Section title={t('screens:settings.sectionProfile')}>
<Row label={t('screens:settings.name')} value={profile.name || '—'} />
<Row
label={t('screens:settings.subscription')}
value={isPremium ? t('screens:settings.premium') : t('screens:settings.free')}
accent={isPremium ? GREEN[500] : undefined}
/>
{!isPremium && (
<LinkRow
icon="sparkles"
label={t('screens:settings.upgradePremium')}
onPress={() => router.push('/paywall')}
accent={GREEN[500]}
/>
)}
</Section>
{/* Preferences */}
<Section title={t('screens:settings.sectionPrefs')}>
<SwitchRow
label={t('screens:settings.haptics')}
value={settings.haptics}
onChange={v => updateSettings({ haptics: v })}
/>
<SwitchRow
label={t('screens:settings.soundEffects')}
value={settings.soundEffects}
onChange={v => updateSettings({ soundEffects: v })}
/>
<SwitchRow
label={t('screens:settings.voiceCoaching')}
value={settings.voiceCoaching}
onChange={v => updateSettings({ voiceCoaching: v })}
/>
<SwitchRow
label={t('screens:settings.music')}
value={settings.musicEnabled}
onChange={v => updateSettings({ musicEnabled: v })}
/>
</Section>
{/* Legal */}
<Section title={t('screens:settings.sectionLegal')}>
<LinkRow icon="doc.text" label={t('screens:settings.terms')} onPress={() => router.push('/terms')} />
<LinkRow icon="lock.shield" label={t('screens:settings.privacy')} onPress={() => router.push('/privacy')} />
</Section>
{/* Danger */}
<Section title={t('screens:settings.sectionData')}>
<Pressable onPress={handleResetProgress} style={styles.dangerRow}>
<Icon name="trash" size={18} tintColor="#FF453A" />
<Text style={styles.dangerText}>{t('screens:settings.resetProgress')}</Text>
</Pressable>
</Section>
<Text style={styles.version}>{t('screens:settings.version')}</Text>
</ScrollView>
)
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.sectionBody}>{children}</View>
</View>
)
}
function Row({ label, value, accent }: { label: string; value: string; accent?: string }) {
return (
<View style={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
<Text style={[styles.rowValue, accent && { color: accent }]}>{value}</Text>
</View>
)
}
function SwitchRow({
label,
value,
onChange,
}: {
label: string
value: boolean
onChange: (value: boolean) => void
}) {
return (
<View style={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
<Switch value={value} onValueChange={onChange} trackColor={{ true: GREEN[500] }} />
</View>
)
}
function LinkRow({
icon,
label,
onPress,
accent,
}: {
icon: IconName
label: string
onPress: () => void
accent?: string
}) {
return (
<Pressable onPress={onPress} style={styles.row}>
<View style={styles.linkLeft}>
<Icon name={icon} size={18} tintColor={accent ?? TEXT.SECONDARY} />
<Text style={[styles.rowLabel, accent && { color: accent, fontWeight: '600' }]}>{label}</Text>
</View>
<Icon name="chevron.right" size={16} tintColor={TEXT.TERTIARY} />
</Pressable>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: NAVY[900] },
header: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, marginBottom: SPACING[4] },
section: { marginBottom: SPACING[6] },
sectionTitle: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: SPACING[2],
paddingHorizontal: SPACING[2],
},
sectionBody: {
backgroundColor: NAVY[800],
borderRadius: RADIUS.MD,
borderWidth: 1,
borderColor: BORDER_COLORS.DIM,
overflow: 'hidden',
},
row: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[3],
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: BORDER_COLORS.DIM,
gap: SPACING[3],
},
rowLabel: { ...TYPOGRAPHY.BODY, color: TEXT.PRIMARY },
rowValue: { ...TYPOGRAPHY.BODY, color: TEXT.SECONDARY },
linkLeft: { flexDirection: 'row', alignItems: 'center', gap: SPACING[3] },
dangerRow: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[3],
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[3],
},
dangerText: { ...TYPOGRAPHY.BODY, color: '#FF453A' },
version: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
textAlign: 'center',
marginTop: SPACING[4],
},
})