More cleanup. More robust themed compononents
This commit is contained in:
parent
86d1df3558
commit
cfcf118275
@ -25,8 +25,8 @@ const SettingsScreen = () => {
|
|||||||
>
|
>
|
||||||
<IconSymbol name="person.fill" size={24} color="#007AFF" style={styles.icon} />
|
<IconSymbol name="person.fill" size={24} color="#007AFF" style={styles.icon} />
|
||||||
<ThemedView style={styles.settingContent}>
|
<ThemedView style={styles.settingContent}>
|
||||||
<ThemedText style={styles.settingTitle}>Profile</ThemedText>
|
<ThemedText style={styles.settingTitle}>Profile Settings</ThemedText>
|
||||||
<ThemedText style={styles.settingSubtitle}>Name, photo, email</ThemedText>
|
<ThemedText style={styles.settingSubtitle}>Update profile information or sign out.</ThemedText>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
<IconSymbol name="chevron.right" size={20} color="#C7C7CC" />
|
<IconSymbol name="chevron.right" size={20} color="#C7C7CC" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -59,7 +59,7 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
marginVertical: 16,
|
marginVertical: 8,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
@ -1,59 +1,77 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { StyleSheet, TouchableOpacity, Image, Alert, ActivityIndicator } from 'react-native';
|
import { StyleSheet, Alert, ActivityIndicator, ScrollView } from 'react-native';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
|
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import ProfileAvatar from '@/components/auth/Profile_Avatar';
|
||||||
import Avatar from '@/components/auth/Profile_Avatar';
|
import LogoutButton from '@/components/auth/Logout_Button';
|
||||||
import { Session } from '@supabase/supabase-js'
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import Logout_Button from '@/components/auth/Logout_Button';
|
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
const ProfileScreen = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [fullName, setFullName] = useState('');
|
const [updating, setUpdating] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [profile, setProfile] = useState({
|
||||||
const [avatar, setAvatar] = useState(null);
|
full_name: '',
|
||||||
|
email: '',
|
||||||
|
avatar_url: null,
|
||||||
|
provider: ''
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Fetch profile when screen comes into focus
|
||||||
fetchUserProfile();
|
useFocusEffect(
|
||||||
}, []);
|
React.useCallback(() => {
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (user) {
|
if (!user) {
|
||||||
const { data, error } = await supabase
|
throw new Error('Not authenticated');
|
||||||
.from('profiles')
|
}
|
||||||
.select('*')
|
|
||||||
.eq('id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (data) {
|
const { data, error } = await supabase
|
||||||
setFullName(data.full_name || '');
|
.from('profiles')
|
||||||
setEmail(data.email || '');
|
.select('*')
|
||||||
setAvatar(data.avatar_url || null);
|
.eq('id', user.id)
|
||||||
}
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setProfile({
|
||||||
|
full_name: data.full_name || '',
|
||||||
|
email: data.email || '',
|
||||||
|
avatar_url: data.avatar_url,
|
||||||
|
provider: data.provider || ''
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching profile:', error);
|
console.error('Error fetching profile:', error);
|
||||||
|
Alert.alert('Error', 'Failed to load profile information');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProfile = async () => {
|
const updateProfile = async () => {
|
||||||
setLoading(true);
|
setUpdating(true);
|
||||||
try {
|
try {
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!user) throw new Error('User not found');
|
if (!user) throw new Error('Not authenticated');
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!profile.full_name.trim()) {
|
||||||
|
Alert.alert('Error', 'Please enter your full name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
full_name: fullName,
|
full_name: profile.full_name.trim(),
|
||||||
email,
|
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,89 +83,95 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
Alert.alert('Success', 'Profile updated successfully!');
|
Alert.alert('Success', 'Profile updated successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Alert.alert('Error updating profile', error.message);
|
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to update profile');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setUpdating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add image picking functionality here (similar to previous example)
|
const handleAvatarUpload = () => {
|
||||||
|
// Refresh profile data after avatar upload
|
||||||
|
fetchUserProfile();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ThemedView style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.container}>
|
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||||
|
<ThemedView style={styles.container}>
|
||||||
<Avatar
|
<ProfileAvatar
|
||||||
size={50}
|
url={profile.avatar_url}
|
||||||
url={avatar}
|
size={120}
|
||||||
onUpload={updateProfile}
|
onUpload={handleAvatarUpload}
|
||||||
/>
|
disabled={updating}
|
||||||
|
|
||||||
<ThemedView style={styles.formSection}>
|
|
||||||
<ThemedText style={styles.label}>Full Name</ThemedText>
|
|
||||||
<ThemedTextInput
|
|
||||||
value={fullName}
|
|
||||||
onChangeText={setFullName}
|
|
||||||
placeholder="Enter your full name"
|
|
||||||
style={styles.input}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{profile.provider && (
|
||||||
|
<ThemedText style={styles.providerText}>
|
||||||
|
Signed in with {profile.provider.charAt(0).toUpperCase() + profile.provider.slice(1)}
|
||||||
|
</ThemedText>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ThemedView style={styles.formSection}>
|
||||||
|
<ThemedText type='title' style={styles.label}>Name</ThemedText>
|
||||||
|
<ThemedTextInput
|
||||||
|
value={profile.full_name}
|
||||||
|
onChangeText={(text) => setProfile(prev => ({ ...prev, full_name: text }))}
|
||||||
|
placeholder="Enter your full name"
|
||||||
|
style={styles.input}
|
||||||
|
editable={!updating}
|
||||||
|
/>
|
||||||
|
</ThemedView>
|
||||||
|
|
||||||
|
<ThemedTextButton
|
||||||
|
text={updating ? 'Saving...' : 'Save Changes'}
|
||||||
|
onPress={updateProfile}
|
||||||
|
disabled={updating || !profile.full_name.trim()}
|
||||||
|
fontSize={18}
|
||||||
|
fontWeight='semibold'
|
||||||
|
width='90%'
|
||||||
|
style={styles.saveButton}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LogoutButton
|
||||||
|
fontSize={18}
|
||||||
|
fontWeight='semibold'
|
||||||
|
width='90%'
|
||||||
|
style={styles.logoutButton}
|
||||||
|
/>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
|
</ScrollView>
|
||||||
<ThemedTextButton
|
|
||||||
text='Save Changes'
|
|
||||||
onPress={updateProfile}
|
|
||||||
disabled={loading}
|
|
||||||
fontSize={18}
|
|
||||||
fontWeight='semibold'
|
|
||||||
width='90%'
|
|
||||||
style={styles.saveButton}
|
|
||||||
/>
|
|
||||||
<Logout_Button
|
|
||||||
fontSize={18}
|
|
||||||
fontWeight='semibold'
|
|
||||||
width='90%'
|
|
||||||
style={styles.logoutButton}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</ThemedView>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default ProfileScreen;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
scrollContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
avatarContainer: {
|
loadingContainer: {
|
||||||
alignItems: 'center',
|
flex: 1,
|
||||||
marginTop: 20,
|
|
||||||
marginBottom: 30,
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
},
|
|
||||||
avatarPlaceholder: {
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
backgroundColor: '#E1E1E1',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
changePhotoText: {
|
|
||||||
marginTop: 8,
|
|
||||||
color: '#007AFF',
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
formSection: {
|
formSection: {
|
||||||
marginBottom: 30,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@ -155,15 +179,24 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
disabledInput: {
|
||||||
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
logoutButton: {
|
logoutButton: {
|
||||||
backgroundColor: 'red',
|
marginTop: 30,
|
||||||
marginTop: 50,
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
providerText: {
|
||||||
|
marginBottom: 20,
|
||||||
|
fontSize: 14,
|
||||||
|
opacity: 0.7,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
|
import { ThemedTextButton } from '@/components/theme';
|
||||||
import { Alert, StyleSheet, AppState } from 'react-native';
|
import { Alert, StyleSheet } from 'react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextStyle, PressableProps, DimensionValue } from 'react-native';
|
import { TextStyle, PressableProps, DimensionValue } from 'react-native';
|
||||||
import ThemedButton from '@/components/theme/buttons/ThemedButton';
|
|
||||||
import { Colors } from '@/constants/Colors';
|
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
||||||
|
|
||||||
// Extend ThemedButton props (which already extends PressableProps)
|
// Extend ThemedButton props (which already extends PressableProps)
|
||||||
type ThemedTextButtonProps = Omit<PressableProps, 'children'> & {
|
type ThemedTextButtonProps = Omit<PressableProps, 'children'> & {
|
||||||
@ -38,6 +35,8 @@ const Logout_Button: React.FC<ThemedTextButtonProps> = ({
|
|||||||
text='Logout'
|
text='Logout'
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
textColor='white'
|
||||||
|
backgroundColor='red'
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
fontWeight={fontWeight}
|
fontWeight={fontWeight}
|
||||||
containerStyle={containerStyle}
|
containerStyle={containerStyle}
|
||||||
@ -48,5 +47,3 @@ const Logout_Button: React.FC<ThemedTextButtonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Logout_Button;
|
export default Logout_Button;
|
||||||
|
|
||||||
const styles = StyleSheet.create({});
|
|
||||||
|
@ -1,124 +1,158 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react';
|
||||||
import { supabase } from '@/lib/supabase'
|
import { supabase } from '@/lib/supabase';
|
||||||
import { StyleSheet, Alert, Image, TouchableOpacity } from 'react-native'
|
import { StyleSheet, Alert, Image, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||||
import * as ImagePicker from 'expo-image-picker'
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
//import { ImageManipulator } from 'expo-image-manipulator';
|
import * as FileSystem from 'expo-file-system';
|
||||||
import { ThemedView, ThemedText, ThemedTextButton } from '../theme';
|
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
|
||||||
|
import { ThemedView, ThemedText } from '../theme';
|
||||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
|
|
||||||
interface Props {
|
interface AvatarProps {
|
||||||
size: number
|
size?: number;
|
||||||
url: string | null
|
url: string | null;
|
||||||
onUpload: (filePath: string) => void
|
onUpload?: (filePath: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Avatar({ url, size = 150, onUpload }: Props) {
|
export default function ProfileAvatar({
|
||||||
const [uploading, setUploading] = useState(false)
|
url,
|
||||||
const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
|
size = 120,
|
||||||
const avatarSize = { height: size, width: size }
|
onUpload,
|
||||||
|
disabled = false
|
||||||
|
}: AvatarProps) {
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (url) downloadImage(url)
|
if (url) downloadImage(url);
|
||||||
}, [url])
|
}, [url]);
|
||||||
|
|
||||||
async function downloadImage(path: string) {
|
async function downloadImage(path: string) {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.storage.from('avatars').download(path)
|
const { data, error } = await supabase.storage.from('avatars').download(path);
|
||||||
|
|
||||||
if (error) {
|
if (error) throw error;
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
const fr = new FileReader()
|
const fr = new FileReader();
|
||||||
fr.readAsDataURL(data)
|
fr.readAsDataURL(data);
|
||||||
fr.onload = () => {
|
fr.onload = () => {
|
||||||
setAvatarUrl(fr.result as string)
|
setAvatarUrl(fr.result as string);
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
console.log('Error downloading image: ', error instanceof Error ? error.message : error);
|
||||||
console.log('Error downloading image: ', error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
async function uploadAvatar() {
|
async function uploadAvatar() {
|
||||||
|
if (disabled || uploading) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setUploading(true)
|
setUploading(true);
|
||||||
|
|
||||||
|
// Get current user
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
if (!user) throw new Error('User not authenticated');
|
||||||
|
|
||||||
const result = await ImagePicker.launchImageLibraryAsync({
|
// Request permission if needed
|
||||||
mediaTypes: ImagePicker.MediaTypeOptions.Images, // Restrict to only images
|
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
allowsMultipleSelection: false, // Can only select one image
|
if (!permissionResult.granted) {
|
||||||
allowsEditing: true, // Allows the user to crop / rotate their photo before uploading it
|
Alert.alert('Permission Required', 'Please allow access to your photo library to upload an avatar.');
|
||||||
quality: 1,
|
|
||||||
exif: false, // We don't want nor need that data.
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.canceled || !result.assets || result.assets.length === 0) {
|
|
||||||
console.log('User cancelled image picker.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//const manipulatedImage = await ImageManipulator.manipulate(result.assets[0].uri)
|
// Launch image picker
|
||||||
//.resize({ width: 300 })
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
//.renderAsync();
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
allowsMultipleSelection: false,
|
||||||
|
allowsEditing: true,
|
||||||
|
aspect: [1, 1],
|
||||||
|
quality: 0.8,
|
||||||
|
exif: false,
|
||||||
|
});
|
||||||
|
|
||||||
//const manipulateResult = await manipulateAsync(
|
if (result.canceled || !result.assets || result.assets.length === 0) {
|
||||||
//result.assets[0].uri,
|
return;
|
||||||
//[{resize: {width: 300, height: 300}}],
|
}
|
||||||
//{compress: 0.7, format: SaveFormat.JPEG}
|
|
||||||
//);
|
|
||||||
|
|
||||||
const image = result.assets[0];
|
const image = result.assets[0];
|
||||||
console.log('Got image', image)
|
|
||||||
|
|
||||||
if (!image.uri) {
|
// Compress and resize the image
|
||||||
throw new Error('No image uri!') // Realistically, this should never happen, but just in case...
|
const compressedImage = await manipulateAsync(
|
||||||
}
|
image.uri,
|
||||||
|
[{ resize: { width: 300, height: 300 } }],
|
||||||
|
{ compress: 0.7, format: SaveFormat.JPEG }
|
||||||
|
);
|
||||||
|
|
||||||
const arraybuffer = await fetch(image.uri).then((res) => res.arrayBuffer())
|
// Get file info to check size
|
||||||
|
const fileInfo = await FileSystem.getInfoAsync(compressedImage.uri);
|
||||||
|
|
||||||
const fileExt = image.uri?.split('.').pop()?.toLowerCase() ?? 'jpeg'
|
// Convert to array buffer for upload
|
||||||
const path = `${Date.now()}.${fileExt}`
|
const arraybuffer = await fetch(compressedImage.uri).then((res) => res.arrayBuffer());
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
const fileExt = compressedImage.uri.split('.').pop()?.toLowerCase() ?? 'jpg';
|
||||||
|
const fileName = `${user.id}_${Date.now()}.${fileExt}`;
|
||||||
|
|
||||||
|
// Upload to Supabase Storage
|
||||||
const { data, error: uploadError } = await supabase.storage
|
const { data, error: uploadError } = await supabase.storage
|
||||||
.from('avatars')
|
.from('avatars')
|
||||||
.upload(path, arraybuffer, {
|
.upload(fileName, arraybuffer, {
|
||||||
contentType: image.mimeType ?? 'image/jpeg',
|
contentType: `image/${fileExt}`,
|
||||||
|
upsert: true,
|
||||||
});
|
});
|
||||||
const { data: updateData, error: updateError } = await supabase
|
|
||||||
|
if (uploadError) throw uploadError;
|
||||||
|
|
||||||
|
// Update user profile with new avatar URL
|
||||||
|
const { error: updateError } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.update({ avatar_url: data?.path })
|
.update({
|
||||||
.eq('id', user?.id);
|
avatar_url: data.path,
|
||||||
|
updated_at: new Date()
|
||||||
|
})
|
||||||
|
.eq('id', user.id);
|
||||||
|
|
||||||
if (uploadError) {
|
if (updateError) throw updateError;
|
||||||
throw uploadError
|
|
||||||
}
|
// Set the new avatar URL
|
||||||
|
setAvatarUrl(compressedImage.uri);
|
||||||
|
|
||||||
|
// Call the onUpload callback if provided
|
||||||
|
if (onUpload) onUpload(data.path);
|
||||||
|
|
||||||
|
Alert.alert('Success', 'Avatar updated successfully!');
|
||||||
|
|
||||||
onUpload(data.path)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
Alert.alert('Error uploading avatar', error instanceof Error ? error.message : 'An unknown error occurred');
|
||||||
Alert.alert(error.message)
|
|
||||||
} else {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false)
|
setUploading(false);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={uploadAvatar}
|
onPress={uploadAvatar}
|
||||||
style={styles.avatarContainer}
|
style={[styles.avatarContainer, { opacity: disabled ? 0.7 : 1 }]}
|
||||||
|
disabled={disabled || uploading}
|
||||||
>
|
>
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<Image source={{ uri: avatarUrl }} style={styles.avatar} />
|
<Image
|
||||||
|
source={{ uri: avatarUrl }}
|
||||||
|
style={[styles.avatar, { width: size, height: size, borderRadius: size / 2 }]}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ThemedView style={styles.avatarPlaceholder}>
|
<ThemedView style={[styles.avatarPlaceholder, { width: size, height: size, borderRadius: size / 2 }]}>
|
||||||
<IconSymbol name="person.fill" size={50} color="#999" />
|
<IconSymbol name="person.fill" size={size / 2.5} color="#999" />
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
)}
|
)}
|
||||||
<ThemedText style={styles.changePhotoText}>Change Photo</ThemedText>
|
|
||||||
|
{uploading ? (
|
||||||
|
<ActivityIndicator style={styles.uploadingIndicator} size="small" color="#007AFF" />
|
||||||
|
) : (
|
||||||
|
<ThemedText style={styles.changePhotoText}>
|
||||||
|
{disabled ? 'Avatar' : 'Change Photo'}
|
||||||
|
</ThemedText>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -126,18 +160,12 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 20,
|
marginVertical: 20,
|
||||||
marginBottom: 30,
|
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
width: 120,
|
backgroundColor: '#E1E1E1',
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
},
|
},
|
||||||
avatarPlaceholder: {
|
avatarPlaceholder: {
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
borderRadius: 60,
|
|
||||||
backgroundColor: '#E1E1E1',
|
backgroundColor: '#E1E1E1',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -147,4 +175,7 @@ const styles = StyleSheet.create({
|
|||||||
color: '#007AFF',
|
color: '#007AFF',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
|
uploadingIndicator: {
|
||||||
|
marginTop: 8,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
@ -10,34 +10,34 @@ const DEFAULT_HEIGHT = 68;
|
|||||||
type ThemedButtonProps = PressableProps & {
|
type ThemedButtonProps = PressableProps & {
|
||||||
width?: DimensionValue;
|
width?: DimensionValue;
|
||||||
height?: DimensionValue;
|
height?: DimensionValue;
|
||||||
|
backgroundColor?: string;
|
||||||
containerStyle?: object;
|
containerStyle?: object;
|
||||||
buttonStyle?: object;
|
buttonStyle?: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemedButton: React.FC<ThemedButtonProps> = ({
|
const ThemedButton: React.FC<ThemedButtonProps> = ({
|
||||||
width,
|
width = DEFAULT_WIDTH,
|
||||||
height,
|
height = DEFAULT_HEIGHT,
|
||||||
|
backgroundColor = Colors[useColorScheme() ?? 'dark'].text,
|
||||||
children,
|
children,
|
||||||
containerStyle,
|
containerStyle,
|
||||||
buttonStyle,
|
buttonStyle,
|
||||||
style,
|
style,
|
||||||
...restProps // This now includes onPress automatically
|
...restProps // This now includes onPress automatically
|
||||||
}) => {
|
}) => {
|
||||||
const scheme = useColorScheme() ?? 'dark';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView
|
<ThemedView
|
||||||
style={[
|
style={[
|
||||||
styles.buttonContainer,
|
styles.buttonContainer,
|
||||||
{
|
{
|
||||||
width: width ?? DEFAULT_WIDTH,
|
width,
|
||||||
height: height ?? DEFAULT_HEIGHT,
|
height,
|
||||||
},
|
},
|
||||||
containerStyle,
|
containerStyle,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.button, { backgroundColor: Colors[scheme].text }, buttonStyle, style]}
|
style={[styles.button, { backgroundColor }, buttonStyle, style]}
|
||||||
{...restProps} // This passes onPress and all other Pressable props
|
{...restProps} // This passes onPress and all other Pressable props
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -15,6 +15,8 @@ type ThemedTextButtonProps = Omit<PressableProps, 'children'> & {
|
|||||||
textStyle?: TextStyle;
|
textStyle?: TextStyle;
|
||||||
containerStyle?: object;
|
containerStyle?: object;
|
||||||
buttonStyle?: object;
|
buttonStyle?: object;
|
||||||
|
textColor?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemedTextButton: React.FC<ThemedTextButtonProps> = ({
|
const ThemedTextButton: React.FC<ThemedTextButtonProps> = ({
|
||||||
@ -26,9 +28,10 @@ const ThemedTextButton: React.FC<ThemedTextButtonProps> = ({
|
|||||||
textStyle,
|
textStyle,
|
||||||
containerStyle,
|
containerStyle,
|
||||||
buttonStyle,
|
buttonStyle,
|
||||||
|
textColor = Colors[useColorScheme() ?? 'dark'].background,
|
||||||
|
backgroundColor = Colors[useColorScheme() ?? 'dark'].text,
|
||||||
...restProps // This includes onPress and all other Pressable props
|
...restProps // This includes onPress and all other Pressable props
|
||||||
}) => {
|
}) => {
|
||||||
const scheme = useColorScheme() ?? 'dark';
|
|
||||||
if (fontWeight === 'semibold') fontWeight = '600';
|
if (fontWeight === 'semibold') fontWeight = '600';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -37,12 +40,13 @@ const ThemedTextButton: React.FC<ThemedTextButtonProps> = ({
|
|||||||
height={height}
|
height={height}
|
||||||
containerStyle={containerStyle}
|
containerStyle={containerStyle}
|
||||||
buttonStyle={buttonStyle}
|
buttonStyle={buttonStyle}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<ThemedText
|
<ThemedText
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
color: Colors[scheme].background,
|
color: textColor,
|
||||||
fontSize,
|
fontSize,
|
||||||
lineHeight: fontSize * 1.5,
|
lineHeight: fontSize * 1.5,
|
||||||
fontWeight,
|
fontWeight,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user