diff --git a/app/(tabs)/settings/profile.tsx b/app/(tabs)/settings/profile.tsx
index dee08eb..ba7812f 100644
--- a/app/(tabs)/settings/profile.tsx
+++ b/app/(tabs)/settings/profile.tsx
@@ -2,8 +2,10 @@ import React, { useState, useEffect } from 'react';
import { StyleSheet, TouchableOpacity, Image, Alert, ActivityIndicator } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { supabase } from '@/lib/supabase';
-import { ThemedView, ThemedText, ThemedTextInput } from '@/components/theme';
+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'
export default function ProfileScreen() {
const [loading, setLoading] = useState(false);
@@ -72,16 +74,12 @@ export default function ProfileScreen() {
return (
-
- {avatar ? (
-
- ) : (
-
-
-
- )}
- Change Photo
-
+
+
Full Name
@@ -92,28 +90,18 @@ export default function ProfileScreen() {
style={styles.input}
/>
- Email
-
-
- {loading ? (
-
- ) : (
- Save Changes
- )}
-
+ fontSize={18}
+ fontWeight='semibold'
+ width='90%'
+ style={styles.saveButton}
+ />
+
);
}
@@ -122,6 +110,7 @@ const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
+ alignItems: 'center',
},
avatarContainer: {
alignItems: 'center',
@@ -161,13 +150,8 @@ const styles = StyleSheet.create({
marginBottom: 20,
},
saveButton: {
- backgroundColor: '#007AFF',
paddingVertical: 14,
borderRadius: 8,
alignItems: 'center',
},
- saveButtonText: {
- color: '#fff',
- fontSize: 16,
- },
});
diff --git a/assets/fonts/SpaceMono-Regular.ttf b/assets/fonts/SpaceMono-Regular.ttf
old mode 100755
new mode 100644
diff --git a/components/Account.tsx b/components/Account.tsx
deleted file mode 100644
index 1604c1b..0000000
--- a/components/Account.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { useState, useEffect } from 'react';
-import { supabase } from '../lib/supabase';
-import { StyleSheet, View, Alert } from 'react-native';
-import { Button, Input } from '@rneui/themed';
-import { Session } from '@supabase/supabase-js';
-
-export default function Account({ session }: { session: Session }) {
- const [loading, setLoading] = useState(true);
- const [username, setUsername] = useState('');
- const [website, setWebsite] = useState('');
- const [avatarUrl, setAvatarUrl] = useState('');
-
- useEffect(() => {
- if (session) getProfile();
- }, [session]);
-
- async function getProfile() {
- try {
- setLoading(true);
- if (!session?.user) throw new Error('No user on the session!');
-
- const { data, error, status } = await supabase
- .from('profiles')
- .select(`username, website, avatar_url`)
- .eq('id', session?.user.id)
- .single();
- if (error && status !== 406) {
- throw error;
- }
-
- if (data) {
- setUsername(data.username);
- setWebsite(data.website);
- setAvatarUrl(data.avatar_url);
- }
- } catch (error) {
- if (error instanceof Error) {
- Alert.alert(error.message);
- }
- } finally {
- setLoading(false);
- }
- }
-
- async function updateProfile({
- username,
- website,
- avatar_url,
- }: {
- username: string;
- website: string;
- avatar_url: string;
- }) {
- try {
- setLoading(true);
- if (!session?.user) throw new Error('No user on the session!');
-
- const updates = {
- id: session?.user.id,
- username,
- website,
- avatar_url,
- updated_at: new Date(),
- };
-
- const { error } = await supabase.from('profiles').upsert(updates);
-
- if (error) {
- throw error;
- }
- } catch (error) {
- if (error instanceof Error) {
- Alert.alert(error.message);
- }
- } finally {
- setLoading(false);
- }
- }
-
- return (
-
-
-
-
-
- setUsername(text)} />
-
-
- setWebsite(text)} />
-
-
-
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- marginTop: 40,
- padding: 12,
- },
- verticallySpaced: {
- paddingTop: 4,
- paddingBottom: 4,
- alignSelf: 'stretch',
- },
- mt20: {
- marginTop: 20,
- },
-});
diff --git a/components/auth/Profile_Avatar.tsx b/components/auth/Profile_Avatar.tsx
new file mode 100644
index 0000000..be39af2
--- /dev/null
+++ b/components/auth/Profile_Avatar.tsx
@@ -0,0 +1,150 @@
+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 { IconSymbol } from '@/components/ui/IconSymbol';
+
+interface Props {
+ size: number
+ url: string | null
+ onUpload: (filePath: string) => void
+}
+
+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 }
+
+ useEffect(() => {
+ 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)
+ fr.onload = () => {
+ setAvatarUrl(fr.result as string)
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ console.log('Error downloading image: ', error.message)
+ }
+ }
+ };
+
+ async function uploadAvatar() {
+ try {
+ setUploading(true)
+ 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.');
+ 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...
+ }
+
+ const arraybuffer = await fetch(image.uri).then((res) => res.arrayBuffer())
+
+ const fileExt = image.uri?.split('.').pop()?.toLowerCase() ?? 'jpeg'
+ const path = `${Date.now()}.${fileExt}`
+ const { data, error: uploadError } = await supabase.storage
+ .from('avatars')
+ .upload(path, arraybuffer, {
+ contentType: image.mimeType ?? 'image/jpeg',
+ });
+ const { data: updateData, error: updateError } = await supabase
+ .from('profiles')
+ .update({ avatar_url: data?.path })
+ .eq('id', user?.id);
+
+ if (uploadError) {
+ throw uploadError
+ }
+
+ onUpload(data.path)
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert(error.message)
+ } else {
+ throw error
+ }
+ } finally {
+ setUploading(false)
+ }
+ };
+
+ return (
+
+ {avatarUrl ? (
+
+ ) : (
+
+
+
+ )}
+ Change Photo
+
+ );
+}
+
+const styles = StyleSheet.create({
+ avatarContainer: {
+ alignItems: 'center',
+ marginTop: 20,
+ marginBottom: 30,
+ },
+ avatar: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ },
+ avatarPlaceholder: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ backgroundColor: '#E1E1E1',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ changePhotoText: {
+ marginTop: 8,
+ color: '#007AFF',
+ fontSize: 16,
+ },
+});
diff --git a/package-lock.json b/package-lock.json
index a3cd810..8c8e5f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"expo-font": "~13.0.3",
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.6",
+ "expo-image-manipulator": "~13.0.6",
"expo-image-picker": "~16.0.6",
"expo-insights": "~0.8.2",
"expo-linking": "~7.0.5",
@@ -8467,6 +8468,18 @@
"expo": "*"
}
},
+ "node_modules/expo-image-manipulator": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-13.0.6.tgz",
+ "integrity": "sha512-Rz8Kcfx1xYm0AsIDi6zfKYUDnwCP8edgYXWb00KAkzOF8bDxwzTrnvESWhCiveM4IB3fojjLpNeENME34p3bzA==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-image-loader": "~5.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-image-picker": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.0.6.tgz",
diff --git a/package.json b/package.json
index d7b3762..5e71a06 100644
--- a/package.json
+++ b/package.json
@@ -64,7 +64,8 @@
"react-native-svg-transformer": "^1.5.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
- "expo-image-picker": "~16.0.6"
+ "expo-image-picker": "~16.0.6",
+ "expo-image-manipulator": "~13.0.6"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/scripts/files_to_clipboard b/scripts/files_to_clipboard
old mode 100755
new mode 100644