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 { StyleSheet, TouchableOpacity, Image, Alert, ActivityIndicator } from 'react-native';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
import { supabase } from '@/lib/supabase';
|
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 { IconSymbol } from '@/components/ui/IconSymbol';
|
||||||
|
import Avatar from '@/components/auth/Profile_Avatar';
|
||||||
|
import { Session } from '@supabase/supabase-js'
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -72,16 +74,12 @@ export default function ProfileScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<TouchableOpacity style={styles.avatarContainer}>
|
|
||||||
{avatar ? (
|
<Avatar
|
||||||
<Image source={{ uri: avatar }} style={styles.avatar} />
|
size={50}
|
||||||
) : (
|
url={avatar}
|
||||||
<ThemedView style={styles.avatarPlaceholder}>
|
onUpload={updateProfile}
|
||||||
<IconSymbol name="person.fill" size={50} color="#999" />
|
/>
|
||||||
</ThemedView>
|
|
||||||
)}
|
|
||||||
<ThemedText style={styles.changePhotoText}>Change Photo</ThemedText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<ThemedView style={styles.formSection}>
|
<ThemedView style={styles.formSection}>
|
||||||
<ThemedText style={styles.label}>Full Name</ThemedText>
|
<ThemedText style={styles.label}>Full Name</ThemedText>
|
||||||
@ -92,28 +90,18 @@ export default function ProfileScreen() {
|
|||||||
style={styles.input}
|
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>
|
</ThemedView>
|
||||||
|
|
||||||
<TouchableOpacity
|
<ThemedTextButton
|
||||||
style={styles.saveButton}
|
text='Save Changes'
|
||||||
onPress={updateProfile}
|
onPress={updateProfile}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
fontSize={18}
|
||||||
{loading ? (
|
fontWeight='semibold'
|
||||||
<ActivityIndicator color="#fff" />
|
width='90%'
|
||||||
) : (
|
style={styles.saveButton}
|
||||||
<ThemedText style={styles.saveButtonText}>Save Changes</ThemedText>
|
/>
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -122,6 +110,7 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -161,13 +150,8 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
backgroundColor: '#007AFF',
|
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
alignItems: 'center',
|
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-font": "~13.0.3",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.6",
|
"expo-image": "~2.0.6",
|
||||||
|
"expo-image-manipulator": "~13.0.6",
|
||||||
"expo-image-picker": "~16.0.6",
|
"expo-image-picker": "~16.0.6",
|
||||||
"expo-insights": "~0.8.2",
|
"expo-insights": "~0.8.2",
|
||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.0.5",
|
||||||
@ -8467,6 +8468,18 @@
|
|||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-image-picker": {
|
||||||
"version": "16.0.6",
|
"version": "16.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-16.0.6.tgz",
|
"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-svg-transformer": "^1.5.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.5",
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@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