173 lines
5.8 KiB
TypeScript
173 lines
5.8 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
import { Image } from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { api, LocalFile } from '@/api';
|
|
import { useAuth } from '@/auth/AuthContext';
|
|
import { useT } from '@/i18n';
|
|
import { AppBar } from '@/components/AppBar';
|
|
import { Avatar, Button, Field, TextField } from '@/components/ui';
|
|
import { pickImageFromLibrary, takePhotoWithCamera } from '@/lib/pickImage';
|
|
import { colors, fontSize, spacing } from '@/theme';
|
|
|
|
export default function EditProfileScreen() {
|
|
const { user, applyUser } = useAuth();
|
|
const router = useRouter();
|
|
const t = useT();
|
|
|
|
const [firstName, setFirstName] = useState('');
|
|
const [lastName, setLastName] = useState('');
|
|
const [phone, setPhone] = useState('');
|
|
const [address, setAddress] = useState('');
|
|
const [avatar, setAvatar] = useState<LocalFile | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
setFirstName(user.firstName ?? '');
|
|
setLastName(user.lastName ?? '');
|
|
setPhone(user.phone ?? '');
|
|
setAddress(user.address ?? '');
|
|
}, [user]);
|
|
|
|
const save = useMutation({
|
|
mutationFn: async () => {
|
|
const updated = await api.auth.updateProfile({
|
|
firstName: firstName.trim() || null,
|
|
lastName: lastName.trim() || null,
|
|
phone: phone.trim() || null,
|
|
address: address.trim() || null,
|
|
});
|
|
// The avatar endpoint answers last, so its user object is the newer one.
|
|
if (avatar) {
|
|
try {
|
|
return await api.auth.uploadAvatar(avatar);
|
|
} catch {
|
|
Alert.alert(t('addProperty.photoFailed'));
|
|
}
|
|
}
|
|
return updated;
|
|
},
|
|
onSuccess: (updated) => {
|
|
applyUser(updated);
|
|
router.back();
|
|
},
|
|
onError: (e: any) => Alert.alert(t('editProfile.failed'), e?.message ?? t('common.tryAgain')),
|
|
});
|
|
|
|
function pickAvatar() {
|
|
Alert.alert(t('editProfile.photo'), undefined, [
|
|
{
|
|
text: t('addProperty.takePhoto'),
|
|
onPress: async () => {
|
|
const picked = await takePhotoWithCamera({ allowsEditing: true, aspect: [1, 1], fallbackName: 'avatar' });
|
|
if (picked) setAvatar(picked);
|
|
},
|
|
},
|
|
{
|
|
text: t('addProperty.fromGallery'),
|
|
onPress: async () => {
|
|
const picked = await pickImageFromLibrary({ allowsEditing: true, aspect: [1, 1], fallbackName: 'avatar' });
|
|
if (picked) setAvatar(picked);
|
|
},
|
|
},
|
|
{ text: t('common.cancel'), style: 'cancel' },
|
|
]);
|
|
}
|
|
|
|
const initials = ((firstName[0] ?? '') + (lastName[0] ?? '')).toUpperCase() || 'U';
|
|
const avatarUri = avatar?.uri ?? user?.avatar ?? null;
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={{ flex: 1, backgroundColor: colors.bgApp }}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<AppBar title={t('editProfile.title')} back />
|
|
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
|
|
<Pressable style={styles.avatarWrap} onPress={pickAvatar}>
|
|
{avatarUri ? (
|
|
<Image source={{ uri: avatarUri }} style={styles.avatarImage} />
|
|
) : (
|
|
<Avatar initials={initials} size={92} />
|
|
)}
|
|
<View style={styles.avatarBadge}>
|
|
<Ionicons name="camera" size={14} color={colors.onDark} />
|
|
</View>
|
|
</Pressable>
|
|
<Text style={styles.avatarHint}>{t('editProfile.photo')}</Text>
|
|
|
|
<View style={{ flexDirection: 'row', gap: spacing[3] }}>
|
|
<Field label={t('register.firstName')} style={{ flex: 1 }}>
|
|
<TextField value={firstName} onChangeText={setFirstName} autoCapitalize="words" />
|
|
</Field>
|
|
<Field label={t('register.lastName')} style={{ flex: 1 }}>
|
|
<TextField value={lastName} onChangeText={setLastName} autoCapitalize="words" />
|
|
</Field>
|
|
</View>
|
|
|
|
<Field label={t('login.email')}>
|
|
<TextField value={user?.email ?? ''} editable={false} style={{ color: colors.textMuted }} />
|
|
<Text style={styles.hint}>{t('editProfile.emailReadOnly')}</Text>
|
|
</Field>
|
|
|
|
<Field label={t('editProfile.phone')} optional>
|
|
<TextField
|
|
icon="call-outline"
|
|
value={phone}
|
|
onChangeText={setPhone}
|
|
keyboardType="phone-pad"
|
|
textContentType="telephoneNumber"
|
|
/>
|
|
</Field>
|
|
|
|
<Field label={t('editProfile.address')} optional>
|
|
<TextField
|
|
icon="location-outline"
|
|
value={address}
|
|
onChangeText={setAddress}
|
|
autoCapitalize="words"
|
|
/>
|
|
</Field>
|
|
|
|
<Button
|
|
title={t('editProfile.save')}
|
|
icon="checkmark"
|
|
size="lg"
|
|
block
|
|
loading={save.isPending}
|
|
onPress={() => save.mutate()}
|
|
/>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
body: { padding: spacing[4], paddingBottom: spacing[9] },
|
|
avatarWrap: { alignSelf: 'center', marginBottom: spacing[2] },
|
|
avatarImage: { width: 92, height: 92, borderRadius: 46, backgroundColor: colors.surfaceSunken },
|
|
avatarBadge: {
|
|
position: 'absolute',
|
|
right: -2,
|
|
bottom: -2,
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: 15,
|
|
backgroundColor: colors.teal600,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 2,
|
|
borderColor: colors.bgApp,
|
|
},
|
|
avatarHint: {
|
|
textAlign: 'center',
|
|
fontSize: fontSize.sm,
|
|
fontWeight: '600',
|
|
color: colors.teal700,
|
|
marginBottom: spacing[6],
|
|
},
|
|
hint: { fontSize: fontSize.xs, color: colors.textMuted, marginTop: 6 },
|
|
});
|