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:
223
app/settings.tsx
Normal file
223
app/settings.tsx
Normal 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],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user