Squash merge feat/expo-app: app-expo, .cursor, workflows, package.json, .husky; remove app-android, app-ios, react-app
This commit is contained in:
296
app-expo/src/app/(tabs)/profile.tsx
Normal file
296
app-expo/src/app/(tabs)/profile.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import { router } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Pressable, ScrollView, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { LucideIcon } from 'lucide-react-native';
|
||||
import {
|
||||
ChevronRight,
|
||||
Download,
|
||||
Globe,
|
||||
HelpCircle,
|
||||
Info,
|
||||
LogOut,
|
||||
MessageCircle,
|
||||
Moon,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Type,
|
||||
User,
|
||||
} from 'lucide-react-native';
|
||||
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import { useAppSettings } from '@/hooks/use-app-settings';
|
||||
import { useSession, useLogout } from '@/features/auth/hooks';
|
||||
import { useCurrentPlan } from '@/features/profile/hooks';
|
||||
|
||||
function SectionCard({
|
||||
title,
|
||||
icon: SectionIcon,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<View className="overflow-hidden rounded-xl border border-border bg-card">
|
||||
<View className="flex-row items-center gap-3 border-b border-border px-4 py-3">
|
||||
<Icon as={SectionIcon} className="text-primary" size={24} />
|
||||
<Text variant="large" className="text-foreground">
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function RowButton({
|
||||
icon: RowIcon,
|
||||
label,
|
||||
onPress,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
onPress?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between px-4 py-3.5 active:bg-muted"
|
||||
onPress={onPress}
|
||||
>
|
||||
<View className="flex-row items-center gap-4">
|
||||
<Icon as={RowIcon} className="text-muted-foreground" size={20} />
|
||||
<Text className="font-medium text-foreground">{label}</Text>
|
||||
</View>
|
||||
<Icon as={ChevronRight} className="text-muted-foreground" size={20} />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function LanguageRow({
|
||||
label,
|
||||
description,
|
||||
currentLabel,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
description: string;
|
||||
currentLabel: string;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-border px-4 py-3.5 active:bg-muted"
|
||||
onPress={onPress}
|
||||
>
|
||||
<View className="flex-row items-center gap-4">
|
||||
<Icon as={Globe} className="text-muted-foreground" size={20} />
|
||||
<View>
|
||||
<Text className="font-medium text-foreground">{label}</Text>
|
||||
<Text className="text-sm text-muted-foreground">{description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Text className="text-sm text-muted-foreground">{currentLabel}</Text>
|
||||
<Icon as={ChevronRight} className="text-muted-foreground" size={20} />
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingRow({
|
||||
icon: SettingIcon,
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
onValueChange,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
description: string;
|
||||
value: boolean;
|
||||
onValueChange: (value: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<View className="flex-row items-center justify-between border-b border-border px-4 py-3.5 last:border-b-0">
|
||||
<View className="flex-row items-center gap-4">
|
||||
<Icon as={SettingIcon} className="text-muted-foreground" size={20} />
|
||||
<View>
|
||||
<Text className="font-medium text-foreground">{label}</Text>
|
||||
<Text className="text-sm text-muted-foreground">{description}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch checked={value} onCheckedChange={onValueChange} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function planDisplayName(
|
||||
subscriptionType: string | undefined,
|
||||
planName?: string,
|
||||
) {
|
||||
if (planName) return planName;
|
||||
switch (subscriptionType) {
|
||||
case 'free':
|
||||
return 'Free';
|
||||
case 'pro':
|
||||
case 'premium':
|
||||
return 'Pro';
|
||||
case 'pro_plus':
|
||||
return 'Pro+';
|
||||
default:
|
||||
return 'Free';
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const { t } = useTranslation('profile');
|
||||
const { t: tApp } = useTranslation('app');
|
||||
const { user } = useSession();
|
||||
const logout = useLogout();
|
||||
const { data: currentPlan } = useCurrentPlan();
|
||||
const {
|
||||
ready: settingsReady,
|
||||
language,
|
||||
hasLanguageOverride,
|
||||
languageOptions,
|
||||
themeName,
|
||||
themeOptions,
|
||||
largeText,
|
||||
changeLargeText,
|
||||
darkMode,
|
||||
changeDarkMode,
|
||||
} = useAppSettings();
|
||||
|
||||
const tierLabel =
|
||||
currentPlan?.plan_name ?? planDisplayName(user?.subscription_type);
|
||||
const currentLanguageLabel =
|
||||
hasLanguageOverride && language
|
||||
? (languageOptions.find((o) => o.code === language)?.label ?? language)
|
||||
: tApp('languages.system');
|
||||
const currentThemeLabel =
|
||||
themeOptions.find((o) => o.value === themeName)?.label ??
|
||||
tApp('theme.default');
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
className="flex-1 bg-background"
|
||||
contentContainerStyle={{ paddingBottom: 32, gap: 16, padding: 16 }}
|
||||
>
|
||||
{/* Profile header */}
|
||||
<View className="items-center gap-4 py-8">
|
||||
<View className="relative">
|
||||
<View
|
||||
className="h-24 w-24 items-center justify-center rounded-full bg-muted"
|
||||
style={{ borderCurve: 'continuous' }}
|
||||
>
|
||||
<Icon as={User} className="text-muted-foreground" size={40} />
|
||||
</View>
|
||||
<Pressable
|
||||
className="absolute bottom-0 right-0 h-9 w-9 items-center justify-center rounded-full border-2 border-background bg-primary"
|
||||
style={{ borderCurve: 'continuous' }}
|
||||
accessibilityLabel={t('editAvatar')}
|
||||
onPress={() => router.push('/(main)/personal-info')}
|
||||
>
|
||||
<Icon as={Pencil} className="text-primary-foreground" size={16} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View className="items-center gap-1">
|
||||
<Text variant="h3" className="text-foreground">
|
||||
{user?.nickname ?? t('userNamePlaceholder')}
|
||||
</Text>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
{t('userTier', { tier: tierLabel })}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* App Experience */}
|
||||
<SectionCard title={t('appExperience.title')} icon={Type}>
|
||||
{settingsReady && (
|
||||
<LanguageRow
|
||||
label={t('appExperience.language')}
|
||||
description={t('appExperience.languageDesc')}
|
||||
currentLabel={currentLanguageLabel}
|
||||
onPress={() => router.push('/(main)/language')}
|
||||
/>
|
||||
)}
|
||||
{settingsReady && (
|
||||
<LanguageRow
|
||||
label={t('appExperience.theme')}
|
||||
description={t('appExperience.themeDesc')}
|
||||
currentLabel={currentThemeLabel}
|
||||
onPress={() => router.push('/(main)/theme')}
|
||||
/>
|
||||
)}
|
||||
<SettingRow
|
||||
icon={Type}
|
||||
label={t('appExperience.largeText')}
|
||||
description={t('appExperience.largeTextDesc')}
|
||||
value={largeText}
|
||||
onValueChange={changeLargeText}
|
||||
/>
|
||||
<SettingRow
|
||||
icon={Moon}
|
||||
label={t('appExperience.nightMode')}
|
||||
description={t('appExperience.nightModeDesc')}
|
||||
value={darkMode}
|
||||
onValueChange={changeDarkMode}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
{/* Data & Privacy */}
|
||||
<SectionCard title={t('dataPrivacy.title')} icon={Download}>
|
||||
<RowButton
|
||||
icon={Download}
|
||||
label={t('dataPrivacy.exportAll')}
|
||||
onPress={() => router.push('/(main)/export-data')}
|
||||
/>
|
||||
<RowButton
|
||||
icon={Trash2}
|
||||
label={t('dataPrivacy.deleteAll')}
|
||||
onPress={() => router.push('/(main)/delete-data')}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
{/* Help & Support */}
|
||||
<SectionCard title={t('helpSupport.title')} icon={HelpCircle}>
|
||||
<RowButton
|
||||
icon={HelpCircle}
|
||||
label={t('helpSupport.faq')}
|
||||
onPress={() => router.push('/(main)/faq')}
|
||||
/>
|
||||
<RowButton
|
||||
icon={MessageCircle}
|
||||
label={t('helpSupport.feedback')}
|
||||
onPress={() => router.push('/(main)/feedback')}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
{/* About */}
|
||||
<SectionCard title={t('about.title')} icon={Info}>
|
||||
<RowButton
|
||||
icon={Info}
|
||||
label={t('about.aboutUs')}
|
||||
onPress={() => router.push('/(main)/about')}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
{/* Sign out */}
|
||||
<Pressable
|
||||
className="mt-6 flex-row items-center justify-center gap-2 rounded-xl bg-destructive/10 py-6 active:bg-destructive/20"
|
||||
onPress={() => logout.mutate()}
|
||||
disabled={logout.isPending}
|
||||
>
|
||||
<Icon as={LogOut} className="text-destructive" size={20} />
|
||||
<Text className="font-semibold text-destructive">
|
||||
{logout.isPending ? t('signingOut') : t('signOut')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user