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.
224 lines
6.7 KiB
TypeScript
224 lines
6.7 KiB
TypeScript
/**
|
|
* 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],
|
|
},
|
|
})
|