638 lines
18 KiB
TypeScript
638 lines
18 KiB
TypeScript
/*
|
|
* File: EditProfileScreen.tsx
|
|
* Description: Edit profile screen for updating user information
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
Alert,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
} from 'react-native';
|
|
import Icon from 'react-native-vector-icons/Feather';
|
|
import { theme } from '../../../theme/theme';
|
|
import { SettingsHeader } from '../components/SettingsHeader';
|
|
import { useAppSelector, useAppDispatch } from '../../../store/hooks';
|
|
import {
|
|
selectUserFirstName,
|
|
selectUserLastName,
|
|
selectUserDisplayName,
|
|
selectUserEmail,
|
|
selectUser,
|
|
} from '../../Auth/redux/authSelectors';
|
|
import { updateUserProfileAsync } from '../../Auth/redux/authActions';
|
|
|
|
/**
|
|
* EditProfileScreenProps Interface
|
|
*
|
|
* Purpose: Defines the props required by the EditProfileScreen component
|
|
*
|
|
* Props:
|
|
* - navigation: React Navigation object for screen navigation
|
|
*/
|
|
interface EditProfileScreenProps {
|
|
navigation: any;
|
|
}
|
|
|
|
/**
|
|
* FormData Interface
|
|
*
|
|
* Purpose: Defines the structure of the profile form data
|
|
*/
|
|
interface FormData {
|
|
firstName: string;
|
|
lastName: string;
|
|
displayName: string;
|
|
}
|
|
|
|
/**
|
|
* FormErrors Interface
|
|
*
|
|
* Purpose: Defines the structure of form validation errors
|
|
*/
|
|
interface FormErrors {
|
|
firstName?: string;
|
|
lastName?: string;
|
|
displayName?: string;
|
|
}
|
|
|
|
/**
|
|
* EditProfileScreen Component
|
|
*
|
|
* Purpose: Allows users to edit their profile information including first name,
|
|
* last name, and display name with proper validation
|
|
*
|
|
* Features:
|
|
* 1. Pre-populated form with current user data
|
|
* 2. Real-time validation
|
|
* 3. Form submission with error handling
|
|
* 4. Clean and intuitive user interface
|
|
*/
|
|
export const EditProfileScreen: React.FC<EditProfileScreenProps> = ({
|
|
navigation,
|
|
}) => {
|
|
// ============================================================================
|
|
// REDUX STATE
|
|
// ============================================================================
|
|
|
|
const dispatch = useAppDispatch();
|
|
const user = useAppSelector(selectUser);
|
|
const currentFirstName = useAppSelector(selectUserFirstName);
|
|
const currentLastName = useAppSelector(selectUserLastName);
|
|
const currentDisplayName = useAppSelector(selectUserDisplayName);
|
|
const currentEmail = useAppSelector(selectUserEmail);
|
|
|
|
// ============================================================================
|
|
// LOCAL STATE
|
|
// ============================================================================
|
|
|
|
const [formData, setFormData] = useState<FormData>({
|
|
firstName: '',
|
|
lastName: '',
|
|
displayName: '',
|
|
});
|
|
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [hasChanges, setHasChanges] = useState(false);
|
|
|
|
// ============================================================================
|
|
// EFFECTS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Initialize form data with current user information
|
|
*/
|
|
useEffect(() => {
|
|
setFormData({
|
|
firstName: currentFirstName || '',
|
|
lastName: currentLastName || '',
|
|
displayName: currentDisplayName || '',
|
|
});
|
|
}, [currentFirstName, currentLastName, currentDisplayName]);
|
|
|
|
/**
|
|
* Check if form has unsaved changes
|
|
*/
|
|
useEffect(() => {
|
|
const hasUnsavedChanges =
|
|
formData.firstName !== currentFirstName ||
|
|
formData.lastName !== currentLastName ||
|
|
formData.displayName !== currentDisplayName;
|
|
|
|
setHasChanges(hasUnsavedChanges);
|
|
}, [formData, currentFirstName, currentLastName, currentDisplayName]);
|
|
|
|
// ============================================================================
|
|
// VALIDATION FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* validateField Function
|
|
*
|
|
* Purpose: Validate individual form fields
|
|
*
|
|
* @param field - Field name to validate
|
|
* @param value - Field value to validate
|
|
* @returns Validation error message or undefined
|
|
*/
|
|
const validateField = (field: keyof FormData, value: string): string | undefined => {
|
|
switch (field) {
|
|
case 'firstName':
|
|
if (!value.trim()) {
|
|
return 'First name is required';
|
|
}
|
|
if (value.trim().length < 2) {
|
|
return 'First name must be at least 2 characters';
|
|
}
|
|
if (value.trim().length > 50) {
|
|
return 'First name must be less than 50 characters';
|
|
}
|
|
if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) {
|
|
return 'First name can only contain letters, spaces, hyphens, and apostrophes';
|
|
}
|
|
break;
|
|
|
|
case 'lastName':
|
|
if (!value.trim()) {
|
|
return 'Last name is required';
|
|
}
|
|
if (value.trim().length < 1) {
|
|
return 'Last name must be at least 1 character';
|
|
}
|
|
if (value.trim().length > 50) {
|
|
return 'Last name must be less than 50 characters';
|
|
}
|
|
if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) {
|
|
return 'Last name can only contain letters, spaces, hyphens, and apostrophes';
|
|
}
|
|
break;
|
|
|
|
case 'displayName':
|
|
if (!value.trim()) {
|
|
return 'Display name is required';
|
|
}
|
|
if (value.trim().length < 2) {
|
|
return 'Display name must be at least 2 characters';
|
|
}
|
|
if (value.trim().length > 30) {
|
|
return 'Display name must be less than 30 characters';
|
|
}
|
|
break;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* validateForm Function
|
|
*
|
|
* Purpose: Validate entire form and return validation errors
|
|
*
|
|
* @returns Object containing validation errors
|
|
*/
|
|
const validateForm = (): FormErrors => {
|
|
const newErrors: FormErrors = {};
|
|
|
|
Object.keys(formData).forEach((field) => {
|
|
const key = field as keyof FormData;
|
|
const error = validateField(key, formData[key]);
|
|
if (error) {
|
|
newErrors[key] = error;
|
|
}
|
|
});
|
|
|
|
return newErrors;
|
|
};
|
|
|
|
// ============================================================================
|
|
// EVENT HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* handleInputChange Function
|
|
*
|
|
* Purpose: Handle input field changes and clear field-specific errors
|
|
*
|
|
* @param field - Field name that changed
|
|
* @param value - New field value
|
|
*/
|
|
const handleInputChange = (field: keyof FormData, value: string) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
|
|
// Clear field-specific error when user starts typing
|
|
if (errors[field]) {
|
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* handleInputBlur Function
|
|
*
|
|
* Purpose: Validate field when user leaves the input
|
|
*
|
|
* @param field - Field name to validate
|
|
*/
|
|
const handleInputBlur = (field: keyof FormData) => {
|
|
const error = validateField(field, formData[field]);
|
|
setErrors(prev => ({ ...prev, [field]: error }));
|
|
};
|
|
|
|
/**
|
|
* handleSubmit Function
|
|
*
|
|
* Purpose: Handle form submission with validation and API call
|
|
*/
|
|
const handleSubmit = async () => {
|
|
// Validate form
|
|
const validationErrors = validateForm();
|
|
if (Object.keys(validationErrors).length > 0) {
|
|
setErrors(validationErrors);
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
// Dispatch update action
|
|
await dispatch(updateUserProfileAsync({
|
|
first_name: formData.firstName.trim(),
|
|
last_name: formData.lastName.trim(),
|
|
})).unwrap();
|
|
|
|
// Navigate back after successful profile update
|
|
navigation.goBack();
|
|
} catch (error: any) {
|
|
// Handle error - toast notification is already shown in the thunk
|
|
console.error('Profile update error:', error);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* handleCancel Function
|
|
*
|
|
* Purpose: Handle cancel action with unsaved changes warning
|
|
*/
|
|
const handleCancel = () => {
|
|
if (hasChanges) {
|
|
Alert.alert(
|
|
'Unsaved Changes',
|
|
'You have unsaved changes. Are you sure you want to leave?',
|
|
[
|
|
{
|
|
text: 'Cancel',
|
|
style: 'cancel',
|
|
},
|
|
{
|
|
text: 'Leave',
|
|
style: 'destructive',
|
|
onPress: () => navigation.goBack(),
|
|
},
|
|
]
|
|
);
|
|
} else {
|
|
navigation.goBack();
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// MAIN RENDER
|
|
// ============================================================================
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={styles.container}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
>
|
|
{/* Header with back button */}
|
|
<SettingsHeader
|
|
title="Edit Profile"
|
|
showBackButton={true}
|
|
onBackPress={handleCancel}
|
|
/>
|
|
|
|
{/* Scrollable form content */}
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
keyboardShouldPersistTaps="handled"
|
|
>
|
|
{/* Current email display (read-only) */}
|
|
<View style={styles.infoSection}>
|
|
<Text style={styles.sectionTitle}>Account Information</Text>
|
|
<View style={styles.readOnlyField}>
|
|
<Text style={styles.readOnlyLabel}>Email Address</Text>
|
|
<View style={styles.emailContainer}>
|
|
<Text style={styles.readOnlyValue}>{currentEmail}</Text>
|
|
<View style={styles.lockIcon}>
|
|
<Icon name="lock" size={16} color={theme.colors.textMuted} />
|
|
</View>
|
|
</View>
|
|
<Text style={styles.readOnlyHint}>Email address cannot be changed</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Profile form section */}
|
|
<View style={styles.infoSection}>
|
|
<Text style={styles.sectionTitle}>Personal Information</Text>
|
|
|
|
{/* First Name Input */}
|
|
<View style={styles.inputContainer}>
|
|
<Text style={styles.inputLabel}>First Name *</Text>
|
|
<TextInput
|
|
style={[
|
|
styles.textInput,
|
|
errors.firstName ? styles.inputError : null,
|
|
]}
|
|
value={formData.firstName}
|
|
onChangeText={(value) => handleInputChange('firstName', value)}
|
|
onBlur={() => handleInputBlur('firstName')}
|
|
placeholder="Enter your first name"
|
|
placeholderTextColor={theme.colors.textMuted}
|
|
autoCapitalize="words"
|
|
autoCorrect={false}
|
|
maxLength={50}
|
|
/>
|
|
{errors.firstName && (
|
|
<Text style={styles.errorText}>{errors.firstName}</Text>
|
|
)}
|
|
</View>
|
|
|
|
{/* Last Name Input */}
|
|
<View style={styles.inputContainer}>
|
|
<Text style={styles.inputLabel}>Last Name *</Text>
|
|
<TextInput
|
|
style={[
|
|
styles.textInput,
|
|
errors.lastName ? styles.inputError : null,
|
|
]}
|
|
value={formData.lastName}
|
|
onChangeText={(value) => handleInputChange('lastName', value)}
|
|
onBlur={() => handleInputBlur('lastName')}
|
|
placeholder="Enter your last name"
|
|
placeholderTextColor={theme.colors.textMuted}
|
|
autoCapitalize="words"
|
|
autoCorrect={false}
|
|
maxLength={50}
|
|
/>
|
|
{errors.lastName && (
|
|
<Text style={styles.errorText}>{errors.lastName}</Text>
|
|
)}
|
|
</View>
|
|
|
|
{/* Display Name Input */}
|
|
<View style={styles.inputContainer}>
|
|
<Text style={styles.inputLabel}>Display Name *</Text>
|
|
<TextInput
|
|
style={[
|
|
styles.textInput,
|
|
errors.displayName ? styles.inputError : null,
|
|
]}
|
|
value={formData.displayName}
|
|
onChangeText={(value) => handleInputChange('displayName', value)}
|
|
onBlur={() => handleInputBlur('displayName')}
|
|
placeholder="Enter your display name"
|
|
placeholderTextColor={theme.colors.textMuted}
|
|
autoCapitalize="words"
|
|
autoCorrect={false}
|
|
maxLength={30}
|
|
/>
|
|
{errors.displayName && (
|
|
<Text style={styles.errorText}>{errors.displayName}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Action buttons */}
|
|
<View style={styles.buttonContainer}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.submitButton,
|
|
(!hasChanges || isSubmitting) && styles.submitButtonDisabled,
|
|
]}
|
|
onPress={handleSubmit}
|
|
disabled={!hasChanges || isSubmitting}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={[
|
|
styles.submitButtonText,
|
|
(!hasChanges || isSubmitting) && styles.submitButtonTextDisabled,
|
|
]}>
|
|
{isSubmitting ? 'Updating...' : 'Update Profile'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={styles.cancelButton}
|
|
onPress={handleCancel}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Bottom spacing for tab bar */}
|
|
<View style={styles.bottomSpacing} />
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// STYLES SECTION
|
|
// ============================================================================
|
|
|
|
const styles = StyleSheet.create({
|
|
// Main container
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
|
|
// Scroll view styling
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
|
|
// Scroll content styling
|
|
scrollContent: {
|
|
paddingHorizontal: theme.spacing.md,
|
|
},
|
|
|
|
// Bottom spacing for tab bar
|
|
bottomSpacing: {
|
|
height: theme.spacing.xl,
|
|
},
|
|
|
|
// Information sections
|
|
infoSection: {
|
|
backgroundColor: theme.colors.background,
|
|
borderRadius: theme.borderRadius.medium,
|
|
padding: theme.spacing.md,
|
|
marginBottom: theme.spacing.md,
|
|
...theme.shadows.primary,
|
|
},
|
|
|
|
sectionTitle: {
|
|
fontSize: theme.typography.fontSize.displaySmall,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
color: theme.colors.textPrimary,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
|
|
// Read-only field styling
|
|
readOnlyField: {
|
|
paddingVertical: theme.spacing.sm,
|
|
borderBottomColor: theme.colors.border,
|
|
borderBottomWidth: 1,
|
|
},
|
|
|
|
readOnlyLabel: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textMuted,
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
|
|
emailContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
|
|
readOnlyValue: {
|
|
fontSize: theme.typography.fontSize.bodyMedium,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textPrimary,
|
|
flex: 1,
|
|
},
|
|
|
|
lockIcon: {
|
|
padding: theme.spacing.xs,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderRadius: theme.borderRadius.small,
|
|
},
|
|
|
|
readOnlyHint: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textMuted,
|
|
fontStyle: 'italic',
|
|
},
|
|
|
|
// Input container styling
|
|
inputContainer: {
|
|
marginBottom: theme.spacing.sm,
|
|
backgroundColor: theme.colors.background,
|
|
padding: theme.spacing.sm,
|
|
borderRadius: theme.borderRadius.small,
|
|
},
|
|
|
|
inputLabel: {
|
|
fontSize: theme.typography.fontSize.bodyMedium,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textPrimary,
|
|
marginBottom: theme.spacing.sm,
|
|
fontWeight: '600',
|
|
},
|
|
|
|
textInput: {
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.medium,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: theme.spacing.md,
|
|
fontSize: theme.typography.fontSize.bodyMedium,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textPrimary,
|
|
backgroundColor: theme.colors.background,
|
|
minHeight: 48,
|
|
},
|
|
|
|
inputError: {
|
|
borderColor: theme.colors.error,
|
|
backgroundColor: theme.colors.error + '10',
|
|
},
|
|
|
|
errorText: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.error,
|
|
marginTop: theme.spacing.sm,
|
|
marginLeft: theme.spacing.xs,
|
|
paddingHorizontal: theme.spacing.sm,
|
|
paddingVertical: theme.spacing.xs,
|
|
backgroundColor: theme.colors.error + '10',
|
|
borderRadius: theme.borderRadius.small,
|
|
alignSelf: 'flex-start',
|
|
},
|
|
|
|
// Button container
|
|
buttonContainer: {
|
|
marginTop: theme.spacing.lg,
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
|
|
submitButton: {
|
|
backgroundColor: theme.colors.primary,
|
|
borderRadius: theme.borderRadius.medium,
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.md,
|
|
...theme.shadows.primary,
|
|
},
|
|
|
|
submitButtonDisabled: {
|
|
backgroundColor: theme.colors.border,
|
|
shadowColor: 'transparent',
|
|
shadowOffset: { width: 0, height: 0 },
|
|
shadowOpacity: 0,
|
|
shadowRadius: 0,
|
|
elevation: 0,
|
|
},
|
|
|
|
submitButtonText: {
|
|
color: theme.colors.background,
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
},
|
|
|
|
submitButtonTextDisabled: {
|
|
color: theme.colors.textMuted,
|
|
},
|
|
|
|
cancelButton: {
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.medium,
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
alignItems: 'center',
|
|
},
|
|
|
|
cancelButtonText: {
|
|
color: theme.colors.textSecondary,
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
});
|
|
|
|
/*
|
|
* End of File: EditProfileScreen.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|