diff --git a/app/(tabs)/settings/index.tsx b/app/(tabs)/settings/index.tsx
index 8cf6a73..d12e21a 100644
--- a/app/(tabs)/settings/index.tsx
+++ b/app/(tabs)/settings/index.tsx
@@ -25,8 +25,8 @@ const SettingsScreen = () => {
>
- Profile
- Name, photo, email
+ Profile Settings
+ Update profile information or sign out.
@@ -59,7 +59,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
},
section: {
- marginVertical: 16,
+ marginVertical: 8,
borderRadius: 10,
overflow: 'hidden',
},
diff --git a/app/(tabs)/settings/profile.tsx b/app/(tabs)/settings/profile.tsx
index 6c4dcd0..b5aa178 100644
--- a/app/(tabs)/settings/profile.tsx
+++ b/app/(tabs)/settings/profile.tsx
@@ -1,153 +1,177 @@
import React, { useState, useEffect } from 'react';
-import { StyleSheet, TouchableOpacity, Image, Alert, ActivityIndicator } from 'react-native';
-import * as ImagePicker from 'expo-image-picker';
+import { StyleSheet, Alert, ActivityIndicator, ScrollView } from 'react-native';
import { supabase } from '@/lib/supabase';
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
-import { IconSymbol } from '@/components/ui/IconSymbol';
-import Avatar from '@/components/auth/Profile_Avatar';
-import { Session } from '@supabase/supabase-js'
-import Logout_Button from '@/components/auth/Logout_Button';
+import ProfileAvatar from '@/components/auth/Profile_Avatar';
+import LogoutButton from '@/components/auth/Logout_Button';
+import { useFocusEffect } from '@react-navigation/native';
-export default function ProfileScreen() {
- const [loading, setLoading] = useState(false);
- const [fullName, setFullName] = useState('');
- const [email, setEmail] = useState('');
- const [avatar, setAvatar] = useState(null);
-
- useEffect(() => {
- fetchUserProfile();
- }, []);
+const ProfileScreen = () => {
+ const [loading, setLoading] = useState(true);
+ const [updating, setUpdating] = useState(false);
+ const [profile, setProfile] = useState({
+ full_name: '',
+ email: '',
+ avatar_url: null,
+ provider: ''
+ });
+
+ // Fetch profile when screen comes into focus
+ useFocusEffect(
+ React.useCallback(() => {
+ fetchUserProfile();
+ }, [])
+ );
const fetchUserProfile = async () => {
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
- if (user) {
- const { data, error } = await supabase
- .from('profiles')
- .select('*')
- .eq('id', user.id)
- .single();
-
- if (data) {
- setFullName(data.full_name || '');
- setEmail(data.email || '');
- setAvatar(data.avatar_url || null);
- }
+ if (!user) {
+ throw new Error('Not authenticated');
+ }
+
+ const { data, error } = await supabase
+ .from('profiles')
+ .select('*')
+ .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) {
console.error('Error fetching profile:', error);
+ Alert.alert('Error', 'Failed to load profile information');
} finally {
setLoading(false);
}
};
const updateProfile = async () => {
- setLoading(true);
+ setUpdating(true);
try {
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 = {
id: user.id,
- full_name: fullName,
- email,
+ full_name: profile.full_name.trim(),
updated_at: new Date(),
};
const { error } = await supabase
.from('profiles')
.upsert(updates);
-
+
if (error) throw error;
Alert.alert('Success', 'Profile updated successfully!');
} catch (error) {
- Alert.alert('Error updating profile', error.message);
+ Alert.alert('Error', error instanceof Error ? error.message : 'Failed to update profile');
} 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 (
+
+
+
+ );
+ }
return (
-
-
-
-
-
- Full Name
-
+
+
+ {profile.provider && (
+
+ Signed in with {profile.provider.charAt(0).toUpperCase() + profile.provider.slice(1)}
+
+ )}
+
+
+ Name
+ setProfile(prev => ({ ...prev, full_name: text }))}
+ placeholder="Enter your full name"
+ style={styles.input}
+ editable={!updating}
+ />
+
+
+
+
+
-
-
-
-
-
+
);
-}
+};
+export default ProfileScreen;
const styles = StyleSheet.create({
+ scrollContainer: {
+ flexGrow: 1,
+ },
container: {
flex: 1,
padding: 16,
alignItems: 'center',
},
- avatarContainer: {
- alignItems: 'center',
- marginTop: 20,
- marginBottom: 30,
- },
- avatar: {
- width: 120,
- height: 120,
- borderRadius: 60,
- },
- avatarPlaceholder: {
- width: 120,
- height: 120,
- borderRadius: 60,
- backgroundColor: '#E1E1E1',
+ loadingContainer: {
+ flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
- changePhotoText: {
- marginTop: 8,
- color: '#007AFF',
- fontSize: 16,
- },
formSection: {
- marginBottom: 30,
+ marginBottom: 20,
},
label: {
marginBottom: 8,
fontSize: 16,
+ fontWeight: '500',
},
input: {
fontSize: 16,
@@ -155,15 +179,24 @@ const styles = StyleSheet.create({
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 20,
+ width: '100%',
+ },
+ disabledInput: {
+ opacity: 0.7,
},
saveButton: {
borderRadius: 8,
alignItems: 'center',
+ marginBottom: 10,
},
logoutButton: {
- backgroundColor: 'red',
- marginTop: 50,
+ marginTop: 30,
borderRadius: 8,
alignItems: 'center',
},
+ providerText: {
+ marginBottom: 20,
+ fontSize: 14,
+ opacity: 0.7,
+ }
});
diff --git a/components/auth/Logout_Button.tsx b/components/auth/Logout_Button.tsx
index fd51a3a..52b5524 100644
--- a/components/auth/Logout_Button.tsx
+++ b/components/auth/Logout_Button.tsx
@@ -1,11 +1,8 @@
import { supabase } from '@/lib/supabase';
-import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
-import { Alert, StyleSheet, AppState } from 'react-native';
+import { ThemedTextButton } from '@/components/theme';
+import { Alert, StyleSheet } from 'react-native';
import React from 'react';
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)
type ThemedTextButtonProps = Omit & {
@@ -38,6 +35,8 @@ const Logout_Button: React.FC = ({
text='Logout'
width={width}
height={height}
+ textColor='white'
+ backgroundColor='red'
fontSize={fontSize}
fontWeight={fontWeight}
containerStyle={containerStyle}
@@ -48,5 +47,3 @@ const Logout_Button: React.FC = ({
);
};
export default Logout_Button;
-
-const styles = StyleSheet.create({});
diff --git a/components/auth/Profile_Avatar.tsx b/components/auth/Profile_Avatar.tsx
index be39af2..82b455d 100644
--- a/components/auth/Profile_Avatar.tsx
+++ b/components/auth/Profile_Avatar.tsx
@@ -1,124 +1,158 @@
-import { useState, useEffect } from 'react'
-import { supabase } from '@/lib/supabase'
-import { StyleSheet, Alert, Image, TouchableOpacity } from 'react-native'
-import * as ImagePicker from 'expo-image-picker'
-//import { ImageManipulator } from 'expo-image-manipulator';
-import { ThemedView, ThemedText, ThemedTextButton } from '../theme';
+import { useState, useEffect } from 'react';
+import { supabase } from '@/lib/supabase';
+import { StyleSheet, Alert, Image, TouchableOpacity, ActivityIndicator } from 'react-native';
+import * as ImagePicker from 'expo-image-picker';
+import * as FileSystem from 'expo-file-system';
+import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
+import { ThemedView, ThemedText } from '../theme';
import { IconSymbol } from '@/components/ui/IconSymbol';
-interface Props {
- size: number
- url: string | null
- onUpload: (filePath: string) => void
+interface AvatarProps {
+ size?: number;
+ url: string | null;
+ onUpload?: (filePath: string) => void;
+ disabled?: boolean;
}
-export default function Avatar({ url, size = 150, onUpload }: Props) {
- const [uploading, setUploading] = useState(false)
- const [avatarUrl, setAvatarUrl] = useState(null)
- const avatarSize = { height: size, width: size }
-
+export default function ProfileAvatar({
+ url,
+ size = 120,
+ onUpload,
+ disabled = false
+}: AvatarProps) {
+ const [uploading, setUploading] = useState(false);
+ const [avatarUrl, setAvatarUrl] = useState(null);
+
useEffect(() => {
- if (url) downloadImage(url)
- }, [url])
+ if (url) downloadImage(url);
+ }, [url]);
async function downloadImage(path: string) {
try {
- const { data, error } = await supabase.storage.from('avatars').download(path)
-
- if (error) {
- throw error
- }
-
- const fr = new FileReader()
- fr.readAsDataURL(data)
+ const { data, error } = await supabase.storage.from('avatars').download(path);
+
+ if (error) throw error;
+
+ const fr = new FileReader();
+ fr.readAsDataURL(data);
fr.onload = () => {
- setAvatarUrl(fr.result as string)
- }
+ setAvatarUrl(fr.result as string);
+ };
} catch (error) {
- if (error instanceof Error) {
- console.log('Error downloading image: ', error.message)
- }
+ console.log('Error downloading image: ', error instanceof Error ? error.message : error);
}
- };
+ }
async function uploadAvatar() {
+ if (disabled || uploading) return;
+
try {
- setUploading(true)
+ setUploading(true);
+
+ // Get current user
const { data: { user } } = await supabase.auth.getUser();
-
- const result = await ImagePicker.launchImageLibraryAsync({
- mediaTypes: ImagePicker.MediaTypeOptions.Images, // Restrict to only images
- allowsMultipleSelection: false, // Can only select one image
- allowsEditing: true, // Allows the user to crop / rotate their photo before uploading it
- 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.');
+ if (!user) throw new Error('User not authenticated');
+
+ // Request permission if needed
+ const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (!permissionResult.granted) {
+ Alert.alert('Permission Required', 'Please allow access to your photo library to upload an avatar.');
return;
}
-
- //const manipulatedImage = await ImageManipulator.manipulate(result.assets[0].uri)
- //.resize({ width: 300 })
- //.renderAsync();
-
- //const manipulateResult = await manipulateAsync(
- //result.assets[0].uri,
- //[{resize: {width: 300, height: 300}}],
- //{compress: 0.7, format: SaveFormat.JPEG}
- //);
-
- const image = result.assets[0];
- console.log('Got image', image)
-
- if (!image.uri) {
- throw new Error('No image uri!') // Realistically, this should never happen, but just in case...
+
+ // Launch image picker
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsMultipleSelection: false,
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 0.8,
+ exif: false,
+ });
+
+ if (result.canceled || !result.assets || result.assets.length === 0) {
+ return;
}
-
- const arraybuffer = await fetch(image.uri).then((res) => res.arrayBuffer())
-
- const fileExt = image.uri?.split('.').pop()?.toLowerCase() ?? 'jpeg'
- const path = `${Date.now()}.${fileExt}`
+
+ const image = result.assets[0];
+
+ // Compress and resize the image
+ const compressedImage = await manipulateAsync(
+ image.uri,
+ [{ resize: { width: 300, height: 300 } }],
+ { compress: 0.7, format: SaveFormat.JPEG }
+ );
+
+ // Get file info to check size
+ const fileInfo = await FileSystem.getInfoAsync(compressedImage.uri);
+
+ // Convert to array buffer for upload
+ 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
.from('avatars')
- .upload(path, arraybuffer, {
- contentType: image.mimeType ?? 'image/jpeg',
+ .upload(fileName, arraybuffer, {
+ 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')
- .update({ avatar_url: data?.path })
- .eq('id', user?.id);
-
- if (uploadError) {
- throw uploadError
- }
-
- onUpload(data.path)
+ .update({
+ avatar_url: data.path,
+ updated_at: new Date()
+ })
+ .eq('id', user.id);
+
+ if (updateError) throw updateError;
+
+ // 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!');
+
} catch (error) {
- if (error instanceof Error) {
- Alert.alert(error.message)
- } else {
- throw error
- }
+ Alert.alert('Error uploading avatar', error instanceof Error ? error.message : 'An unknown error occurred');
} finally {
- setUploading(false)
+ setUploading(false);
}
- };
+ }
return (
{avatarUrl ? (
-
+
) : (
-
-
+
+
)}
- Change Photo
+
+ {uploading ? (
+
+ ) : (
+
+ {disabled ? 'Avatar' : 'Change Photo'}
+
+ )}
);
}
@@ -126,18 +160,12 @@ export default function Avatar({ url, size = 150, onUpload }: Props) {
const styles = StyleSheet.create({
avatarContainer: {
alignItems: 'center',
- marginTop: 20,
- marginBottom: 30,
+ marginVertical: 20,
},
avatar: {
- width: 120,
- height: 120,
- borderRadius: 60,
+ backgroundColor: '#E1E1E1',
},
avatarPlaceholder: {
- width: 120,
- height: 120,
- borderRadius: 60,
backgroundColor: '#E1E1E1',
justifyContent: 'center',
alignItems: 'center',
@@ -147,4 +175,7 @@ const styles = StyleSheet.create({
color: '#007AFF',
fontSize: 16,
},
+ uploadingIndicator: {
+ marginTop: 8,
+ }
});
diff --git a/components/theme/buttons/ThemedButton.tsx b/components/theme/buttons/ThemedButton.tsx
index 24f0726..a3c6ee1 100644
--- a/components/theme/buttons/ThemedButton.tsx
+++ b/components/theme/buttons/ThemedButton.tsx
@@ -10,34 +10,34 @@ const DEFAULT_HEIGHT = 68;
type ThemedButtonProps = PressableProps & {
width?: DimensionValue;
height?: DimensionValue;
+ backgroundColor?: string;
containerStyle?: object;
buttonStyle?: object;
};
const ThemedButton: React.FC = ({
- width,
- height,
+ width = DEFAULT_WIDTH,
+ height = DEFAULT_HEIGHT,
+ backgroundColor = Colors[useColorScheme() ?? 'dark'].text,
children,
containerStyle,
buttonStyle,
style,
...restProps // This now includes onPress automatically
}) => {
- const scheme = useColorScheme() ?? 'dark';
-
return (
{children}
diff --git a/components/theme/buttons/ThemedTextButton.tsx b/components/theme/buttons/ThemedTextButton.tsx
index 1be37fa..8f9620b 100644
--- a/components/theme/buttons/ThemedTextButton.tsx
+++ b/components/theme/buttons/ThemedTextButton.tsx
@@ -15,6 +15,8 @@ type ThemedTextButtonProps = Omit & {
textStyle?: TextStyle;
containerStyle?: object;
buttonStyle?: object;
+ textColor?: string;
+ backgroundColor?: string;
};
const ThemedTextButton: React.FC = ({
@@ -26,9 +28,10 @@ const ThemedTextButton: React.FC = ({
textStyle,
containerStyle,
buttonStyle,
+ textColor = Colors[useColorScheme() ?? 'dark'].background,
+ backgroundColor = Colors[useColorScheme() ?? 'dark'].text,
...restProps // This includes onPress and all other Pressable props
}) => {
- const scheme = useColorScheme() ?? 'dark';
if (fontWeight === 'semibold') fontWeight = '600';
return (
@@ -37,12 +40,13 @@ const ThemedTextButton: React.FC = ({
height={height}
containerStyle={containerStyle}
buttonStyle={buttonStyle}
+ backgroundColor={backgroundColor}
{...restProps}
>