mobileapp / src /screens /profile /EditProfileScreen.tsx
Antaram Dev Bot
feat: complete ANTARAM.ORG ride-sharing app frontend
5c876be
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'>;
// ─── Main Screen ─────────────────────────────────────────────────────────────────
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>>({});
// ── Populate form from user data ───────────────────────────────────────────
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]);
// ── Validate and save ──────────────────────────────────────────────────────
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]);
// ── Loading state ──────────────────────────────────────────────────────────
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>
);
}
// ─── Styles ──────────────────────────────────────────────────────────────────────
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,
},
});