normalize mobile auth and profile ui to theme components

This commit is contained in:
dimitar committed 2026-03-29 20:07:33 +02:00
1 parent 7ada05da6a
commit ed14c57749
7 files changed
+557 -615

No files matched your search

+177 -253
View File
@@ -1,269 +1,193 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
TextInput,
Alert,
Platform,
} from 'react-native';
import { useRouter, Stack } from 'expo-router';
import { useUser } from '@clerk/clerk-expo';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from "react-native";
import { Stack, useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { useUser } from "@clerk/clerk-expo";
import { useTheme } from "../contexts/ThemeContext";
import { MinimalCard } from "../components/MinimalCard";
import { Input } from "../components/Input";
import { MinimalButton } from "../components/MinimalButton";
export default function PersonalDetailsScreen() {
const router = useRouter();
const { user } = useUser();
const [loading, setLoading] = useState(false);
const router = useRouter();
const { user } = useUser();
const { colors, typography } = useTheme();
const [loading, setLoading] = useState(false);
// Initialize with current user data
const [formData, setFormData] = useState({
firstName: user?.firstName || '',
lastName: user?.lastName || '',
email: user?.primaryEmailAddress?.emailAddress || '',
phone: user?.primaryPhoneNumber?.phoneNumber || '',
});
const [formData, setFormData] = useState({
firstName: user?.firstName || "",
lastName: user?.lastName || "",
email: user?.primaryEmailAddress?.emailAddress || "",
phone: user?.primaryPhoneNumber?.phoneNumber || "",
});
const handleSave = async () => {
setLoading(true);
try {
// Update user profile via Clerk
await user?.update({
firstName: formData.firstName,
lastName: formData.lastName,
});
const updateField = (field: "firstName" | "lastName", value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
Alert.alert('Success', 'Personal details updated successfully', [
{ text: 'OK', onPress: () => router.back() },
]);
} catch (error) {
console.error('Error updating personal details:', error);
Alert.alert('Error', 'Failed to update personal details. Please try again.');
} finally {
setLoading(false);
}
};
const handleSave = async () => {
setLoading(true);
try {
await user?.update({
firstName: formData.firstName,
lastName: formData.lastName,
});
const updateField = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
Alert.alert("Success", "Personal details updated successfully", [
{ text: "OK", onPress: () => router.back() },
]);
} catch {
Alert.alert(
"Error",
"Failed to update personal details. Please try again.",
);
} finally {
setLoading(false);
}
};
return (
<>
<Stack.Screen options={{ headerShown: false }} />
<View style={styles.container}>
{/* Header */}
<LinearGradient
colors={theme.gradients.primary}
style={styles.header}
>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color="#fff" />
</TouchableOpacity>
<Text style={styles.headerTitle}>Personal Details</Text>
<View style={{ width: 40 }} />
</LinearGradient>
return (
<>
<Stack.Screen options={{ headerShown: false }} />
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View
style={[styles.header, { borderBottomColor: colors.borderLight }]}
>
<TouchableOpacity
style={[
styles.backButton,
{ backgroundColor: colors.surfaceElevated },
]}
onPress={() => router.back()}
activeOpacity={0.8}
>
<Ionicons name="arrow-back" size={20} color={colors.textPrimary} />
</TouchableOpacity>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Personal Details
</Text>
<View style={styles.backButtonPlaceholder} />
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* First Name */}
<View style={styles.field}>
<Text style={styles.label}>First Name *</Text>
<View style={styles.inputContainer}>
<Ionicons name="person-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
<TextInput
style={styles.input}
value={formData.firstName}
onChangeText={(value) => updateField('firstName', value)}
placeholder="Enter first name"
placeholderTextColor={theme.colors.gray400}
/>
</View>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<MinimalCard variant="elevated" style={styles.card}>
<Input
label="First Name"
value={formData.firstName}
onChangeText={(value) => updateField("firstName", value)}
placeholder="Enter first name"
/>
{/* Last Name */}
<View style={styles.field}>
<Text style={styles.label}>Last Name *</Text>
<View style={styles.inputContainer}>
<Ionicons name="person-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
<TextInput
style={styles.input}
value={formData.lastName}
onChangeText={(value) => updateField('lastName', value)}
placeholder="Enter last name"
placeholderTextColor={theme.colors.gray400}
/>
</View>
</View>
<Input
label="Last Name"
value={formData.lastName}
onChangeText={(value) => updateField("lastName", value)}
placeholder="Enter last name"
/>
{/* Email (Read-only) */}
<View style={styles.field}>
<Text style={styles.label}>Email</Text>
<View style={[styles.inputContainer, styles.disabledInput]}>
<Ionicons name="mail-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
<TextInput
style={[styles.input, styles.disabledText]}
value={formData.email}
editable={false}
placeholderTextColor={theme.colors.gray400}
/>
<Ionicons name="lock-closed-outline" size={16} color={theme.colors.gray400} />
</View>
<Text style={styles.helperText}>Email cannot be changed here</Text>
</View>
{/* Phone (Read-only for now) */}
<View style={styles.field}>
<Text style={styles.label}>Phone Number</Text>
<View style={[styles.inputContainer, styles.disabledInput]}>
<Ionicons name="call-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
<TextInput
style={[styles.input, styles.disabledText]}
value={formData.phone || 'Not set'}
editable={false}
placeholderTextColor={theme.colors.gray400}
/>
<Ionicons name="lock-closed-outline" size={16} color={theme.colors.gray400} />
</View>
<Text style={styles.helperText}>Phone number cannot be changed here</Text>
</View>
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.saveButton, loading && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={loading}
>
<LinearGradient
colors={theme.gradients.primary}
style={styles.saveButtonGradient}
>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.saveButtonText}>
{loading ? 'Saving...' : 'Save Changes'}
</Text>
</LinearGradient>
</TouchableOpacity>
</View>
<View style={styles.readOnlyField}>
<Text style={[typography.h4, { color: colors.textPrimary }]}>
Email
</Text>
<Text style={[typography.body, { color: colors.textSecondary }]}>
{formData.email || "Not set"}
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Read-only in app settings
</Text>
</View>
</>
);
<View style={styles.readOnlyField}>
<Text style={[typography.h4, { color: colors.textPrimary }]}>
Phone Number
</Text>
<Text style={[typography.body, { color: colors.textSecondary }]}>
{formData.phone || "Not set"}
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Read-only in app settings
</Text>
</View>
</MinimalCard>
<MinimalButton
title="Save Changes"
onPress={handleSave}
loading={loading}
disabled={loading}
fullWidth
size="lg"
style={{ marginTop: 16 }}
/>
{loading ? (
<View style={styles.loadingRow}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
) : null}
</ScrollView>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: Platform.OS === 'ios' ? 60 : 40,
paddingBottom: 20,
paddingHorizontal: 20,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
},
headerTitle: {
fontSize: theme.typography.fontSize['2xl'],
fontWeight: theme.typography.fontWeight.bold,
color: '#fff',
},
content: {
flex: 1,
},
scrollContent: {
padding: 20,
paddingBottom: 100,
},
field: {
marginBottom: 24,
},
label: {
fontSize: theme.typography.fontSize.sm,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray700,
marginBottom: 8,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.gray200,
paddingHorizontal: 16,
...theme.shadows.subtle,
},
inputIcon: {
marginRight: 12,
},
input: {
flex: 1,
paddingVertical: 16,
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray900,
},
disabledInput: {
backgroundColor: theme.colors.gray50,
},
disabledText: {
color: theme.colors.gray500,
},
helperText: {
fontSize: theme.typography.fontSize.xs,
color: theme.colors.gray500,
marginTop: 6,
marginLeft: 4,
},
footer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 20,
paddingBottom: Platform.OS === 'ios' ? 40 : 20,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: theme.colors.gray100,
...theme.shadows.medium,
},
saveButton: {
borderRadius: theme.borderRadius.lg,
overflow: 'hidden',
},
saveButtonGradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 16,
gap: 8,
},
saveButtonDisabled: {
opacity: 0.6,
},
saveButtonText: {
fontSize: theme.typography.fontSize.base,
fontWeight: theme.typography.fontWeight.bold,
color: '#fff',
},
container: {
flex: 1,
},
header: {
paddingTop: 56,
paddingBottom: 12,
paddingHorizontal: 20,
borderBottomWidth: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
backButton: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
},
backButtonPlaceholder: {
width: 36,
height: 36,
},
content: {
flex: 1,
},
scrollContent: {
padding: 20,
paddingBottom: 80,
},
card: {
borderRadius: 16,
},
readOnlyField: {
marginTop: 8,
marginBottom: 12,
gap: 4,
},
loadingRow: {
marginTop: 16,
alignItems: "center",
},
});