| import React, { useState, useEffect, useCallback } from 'react'; |
| import { View, StyleSheet, ScrollView } from 'react-native'; |
| import { |
| Appbar, |
| Text, |
| Surface, |
| Avatar, |
| TextInput, |
| Divider, |
| Button, |
| Snackbar, |
| RadioButton, |
| ProgressBar, |
| IconButton, |
| } from 'react-native-paper'; |
| import { SafeAreaView } from 'react-native-safe-area-context'; |
| import { useNavigation } from '@react-navigation/native'; |
| import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; |
| import type { ProfileStackParamList } from '../../navigation/types'; |
| import { useCurrentUser } from '../../hooks/useCurrentUser'; |
| import { customTheme } from '../../theme'; |
| import { isValidPhone, isValidDisplayName, isValidVehicleNumber } from '../../utils/validators'; |
| import type { VehicleType } from '../../types/user'; |
|
|
| type NavigationProp = NativeStackNavigationProp<ProfileStackParamList, 'EditProfile'>; |
|
|
| |
|
|
| export default function EditProfileScreen() { |
| const navigation = useNavigation<NavigationProp>(); |
|
|
| const { user, loading, updateProfile } = useCurrentUser(); |
|
|
| const [displayName, setDisplayName] = useState(''); |
| const [phone, setPhone] = useState(''); |
| const [email] = useState(''); |
| const [vehicleType, setVehicleType] = useState<VehicleType>('auto'); |
| const [vehicleNumber, setVehicleNumber] = useState(''); |
| const [licenseNumber, setLicenseNumber] = useState(''); |
| const [saving, setSaving] = useState(false); |
| const [snackbarVisible, setSnackbarVisible] = useState(false); |
| const [errors, setErrors] = useState<Record<string, string>>({}); |
|
|
| |
|
|
| useEffect(() => { |
| if (user) { |
| setDisplayName(user.fullName ?? ''); |
| setPhone(user.phone ?? ''); |
| setVehicleType(((user as Record<string, unknown>).vehicleType as VehicleType) ?? 'auto'); |
| setVehicleNumber((user as Record<string, unknown>).vehicleNumber as string ?? ''); |
| setLicenseNumber((user as Record<string, unknown>).licenseNumber as string ?? ''); |
| } |
| }, [user]); |
|
|
| |
|
|
| const handleSave = useCallback(async () => { |
| const newErrors: Record<string, string> = {}; |
|
|
| if (!isValidDisplayName(displayName)) { |
| newErrors.displayName = 'Enter a valid name (2-50 characters)'; |
| } |
| if (phone && !isValidPhone(phone)) { |
| newErrors.phone = 'Enter a valid Indian phone number'; |
| } |
| if (vehicleNumber && !isValidVehicleNumber(vehicleNumber)) { |
| newErrors.vehicleNumber = 'Enter a valid vehicle number'; |
| } |
|
|
| if (Object.keys(newErrors).length > 0) { |
| setErrors(newErrors); |
| return; |
| } |
|
|
| setErrors({}); |
| setSaving(true); |
|
|
| try { |
| await updateProfile({ |
| fullName: displayName, |
| phone: phone || null, |
| vehicleType, |
| vehicleNumber: vehicleNumber || null, |
| licenseNumber: licenseNumber || null, |
| }); |
| setSnackbarVisible(true); |
| setTimeout(() => navigation.goBack(), 800); |
| } catch (err) { |
| console.error('[EditProfile] Save failed:', err); |
| } finally { |
| setSaving(false); |
| } |
| }, [displayName, phone, vehicleType, vehicleNumber, licenseNumber, updateProfile, navigation]); |
|
|
| |
|
|
| if (loading && !user) { |
| return ( |
| <SafeAreaView style={styles.container} edges={['top']}> |
| <Appbar.Header> |
| <Appbar.BackAction onPress={navigation.goBack} /> |
| <Appbar.Content title="Edit Profile" /> |
| </Appbar.Header> |
| <View style={styles.center}> |
| <ProgressBar indeterminate visible /> |
| </View> |
| </SafeAreaView> |
| ); |
| } |
|
|
| return ( |
| <SafeAreaView style={styles.container} edges={['top']}> |
| <Appbar.Header> |
| <Appbar.BackAction onPress={navigation.goBack} /> |
| <Appbar.Content title="Edit Profile" /> |
| <Appbar.Action |
| icon="check" |
| onPress={handleSave} |
| disabled={saving} |
| /> |
| </Appbar.Header> |
| |
| <ScrollView |
| contentContainerStyle={styles.scrollContent} |
| showsVerticalScrollIndicator={false} |
| keyboardShouldPersistTaps="handled" |
| > |
| {/* Avatar with camera overlay */} |
| <View style={styles.avatarContainer}> |
| {user?.avatarUrl ? ( |
| <Avatar.Image size={96} source={{ uri: user.avatarUrl }} /> |
| ) : ( |
| <Avatar.Text |
| size={96} |
| label={(user?.fullName ?? 'U').charAt(0).toUpperCase()} |
| style={{ backgroundColor: customTheme.colors.brand.primary }} |
| labelStyle={{ fontSize: 36 }} |
| /> |
| )} |
| <IconButton |
| icon="camera" |
| size={24} |
| style={styles.cameraButton} |
| iconColor="#FFFFFF" |
| containerColor={customTheme.colors.brand.primary} |
| onPress={() => { |
| // TODO: Image picker integration |
| }} |
| /> |
| </View> |
| |
| {/* Display Name */} |
| <TextInput |
| mode="outlined" |
| label="Display Name" |
| value={displayName} |
| onChangeText={setDisplayName} |
| error={!!errors.displayName} |
| style={styles.input} |
| /> |
| {errors.displayName && ( |
| <Text variant="bodySmall" style={styles.errorText}> |
| {errors.displayName} |
| </Text> |
| )} |
| |
| {/* Phone */} |
| <TextInput |
| mode="outlined" |
| label="Phone" |
| value={phone} |
| onChangeText={setPhone} |
| keyboardType="phone-pad" |
| placeholder="+91" |
| error={!!errors.phone} |
| style={styles.input} |
| left={<TextInput.Affix text="+91" />} |
| /> |
| {errors.phone && ( |
| <Text variant="bodySmall" style={styles.errorText}> |
| {errors.phone} |
| </Text> |
| )} |
| |
| {/* Email (readonly) */} |
| <TextInput |
| mode="outlined" |
| label="Email" |
| value={user?.email ?? ''} |
| editable={false} |
| style={styles.input} |
| disabled |
| left={<TextInput.Icon icon="lock" />} |
| /> |
| |
| {/* Vehicle Details Section */} |
| <Divider style={styles.sectionDivider} /> |
| <Text variant="labelLarge" style={styles.sectionLabel}> |
| VEHICLE DETAILS |
| </Text> |
| |
| {/* Vehicle Type */} |
| <RadioButton.Group |
| value={vehicleType} |
| onValueChange={(v) => setVehicleType(v as VehicleType)} |
| > |
| <View style={styles.radioRow}> |
| <View style={styles.radioItem}> |
| <RadioButton value="auto" color={customTheme.colors.brand.primary} /> |
| <Text variant="bodyMedium">Auto</Text> |
| </View> |
| <View style={styles.radioItem}> |
| <RadioButton value="car" color={customTheme.colors.brand.primary} /> |
| <Text variant="bodyMedium">Car</Text> |
| </View> |
| <View style={styles.radioItem}> |
| <RadioButton value="bike" color={customTheme.colors.brand.primary} /> |
| <Text variant="bodyMedium">Bike</Text> |
| </View> |
| </View> |
| </RadioButton.Group> |
| |
| {/* Vehicle Number */} |
| <TextInput |
| mode="outlined" |
| label="Vehicle Number" |
| value={vehicleNumber} |
| onChangeText={setVehicleNumber} |
| autoCapitalize="characters" |
| placeholder="KA-01-AB-1234" |
| error={!!errors.vehicleNumber} |
| style={styles.input} |
| /> |
| {errors.vehicleNumber && ( |
| <Text variant="bodySmall" style={styles.errorText}> |
| {errors.vehicleNumber} |
| </Text> |
| )} |
| |
| {/* License Number */} |
| <TextInput |
| mode="outlined" |
| label="License Number" |
| value={licenseNumber} |
| onChangeText={setLicenseNumber} |
| autoCapitalize="characters" |
| style={styles.input} |
| /> |
| |
| {/* Save Button */} |
| <Button |
| mode="contained" |
| onPress={handleSave} |
| loading={saving} |
| disabled={saving} |
| style={styles.saveButton} |
| > |
| Save Changes |
| </Button> |
| </ScrollView> |
| |
| <Snackbar |
| visible={snackbarVisible} |
| onDismiss={() => setSnackbarVisible(false)} |
| duration={800} |
| > |
| Profile updated! |
| </Snackbar> |
| </SafeAreaView> |
| ); |
| } |
|
|
| |
|
|
| const styles = StyleSheet.create({ |
| container: { |
| flex: 1, |
| backgroundColor: customTheme.colors.brand.background, |
| }, |
| center: { |
| flex: 1, |
| justifyContent: 'center', |
| alignItems: 'center', |
| }, |
| scrollContent: { |
| paddingHorizontal: 16, |
| paddingBottom: 32, |
| }, |
| avatarContainer: { |
| alignItems: 'center', |
| marginTop: 16, |
| marginBottom: 24, |
| }, |
| cameraButton: { |
| position: 'absolute', |
| bottom: 0, |
| right: 0, |
| }, |
| input: { |
| marginBottom: 4, |
| }, |
| errorText: { |
| color: customTheme.colors.brand.error, |
| marginBottom: 8, |
| marginLeft: 16, |
| }, |
| sectionDivider: { |
| marginVertical: 16, |
| }, |
| sectionLabel: { |
| fontWeight: '700', |
| textTransform: 'uppercase', |
| color: customTheme.colors.brand.primaryDark, |
| marginBottom: 8, |
| }, |
| radioRow: { |
| flexDirection: 'row', |
| justifyContent: 'space-around', |
| marginBottom: 16, |
| }, |
| radioItem: { |
| flexDirection: 'row', |
| alignItems: 'center', |
| }, |
| saveButton: { |
| marginTop: 24, |
| borderRadius: 24, |
| paddingVertical: 4, |
| }, |
| }); |
|
|