initial
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user