I got pfps working basicallly
This commit is contained in:
parent
50d3d69dbd
commit
f9655424db
@ -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
0
assets/fonts/SpaceMono-Regular.ttf
Executable file → Normal 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,
|
||||
},
|
||||
});
|
150
components/auth/Profile_Avatar.tsx
Normal file
150
components/auth/Profile_Avatar.tsx
Normal 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
13
package-lock.json
generated
@ -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",
|
||||
|
@ -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
0
scripts/files_to_clipboard
Executable file → Normal file
Loading…
x
Reference in New Issue
Block a user