I got pfps working basicallly

This commit is contained in:
Gabriel Brown 2025-03-10 23:58:00 -05:00
parent 50d3d69dbd
commit f9655424db
7 changed files with 183 additions and 155 deletions

View File

@ -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 (
<ThemedView style={styles.container}>
<TouchableOpacity style={styles.avatarContainer}>
{avatar ? (
<Image source={{ uri: avatar }} style={styles.avatar} />
) : (
<ThemedView style={styles.avatarPlaceholder}>
<IconSymbol name="person.fill" size={50} color="#999" />
</ThemedView>
)}
<ThemedText style={styles.changePhotoText}>Change Photo</ThemedText>
</TouchableOpacity>
<Avatar
size={50}
url={avatar}
onUpload={updateProfile}
/>
<ThemedView style={styles.formSection}>
<ThemedText style={styles.label}>Full Name</ThemedText>
@ -92,28 +90,18 @@ export default function ProfileScreen() {
style={styles.input}
/>
<ThemedText style={styles.label}>Email</ThemedText>
<ThemedTextInput
value={email}
onChangeText={setEmail}
placeholder="Enter your email"
keyboardType="email-address"
autoCapitalize="none"
style={styles.input}
/>
</ThemedView>
<TouchableOpacity
style={styles.saveButton}
<ThemedTextButton
text='Save Changes'
onPress={updateProfile}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText style={styles.saveButtonText}>Save Changes</ThemedText>
)}
</TouchableOpacity>
fontSize={18}
fontWeight='semibold'
width='90%'
style={styles.saveButton}
/>
</ThemedView>
);
}
@ -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,
},
});

0
assets/fonts/SpaceMono-Regular.ttf Executable file → Normal file
View File

View File

@ -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 (
<View style={styles.container}>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Input label='Email' value={session?.user?.email} disabled />
</View>
<View style={styles.verticallySpaced}>
<Input label='Username' value={username || ''} onChangeText={(text) => setUsername(text)} />
</View>
<View style={styles.verticallySpaced}>
<Input label='Website' value={website || ''} onChangeText={(text) => setWebsite(text)} />
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button
title={loading ? 'Loading ...' : 'Update'}
onPress={() => updateProfile({ username, website, avatar_url: avatarUrl })}
disabled={loading}
/>
</View>
<View style={styles.verticallySpaced}>
<Button title='Sign Out' onPress={() => supabase.auth.signOut()} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 40,
padding: 12,
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: 'stretch',
},
mt20: {
marginTop: 20,
},
});

View File

@ -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<string | null>(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 (
<TouchableOpacity
onPress={uploadAvatar}
style={styles.avatarContainer}
>
{avatarUrl ? (
<Image source={{ uri: avatarUrl }} style={styles.avatar} />
) : (
<ThemedView style={styles.avatarPlaceholder}>
<IconSymbol name="person.fill" size={50} color="#999" />
</ThemedView>
)}
<ThemedText style={styles.changePhotoText}>Change Photo</ThemedText>
</TouchableOpacity>
);
}
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,
},
});

13
package-lock.json generated
View File

@ -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",

View File

@ -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",

0
scripts/files_to_clipboard Executable file → Normal file
View File