185 lines
5.3 KiB
TypeScript
185 lines
5.3 KiB
TypeScript
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 AvatarProps {
|
|
size?: number;
|
|
url: string | null;
|
|
onUpload?: (filePath: string) => void;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export default function ProfileAvatar({
|
|
url,
|
|
size = 120,
|
|
onUpload,
|
|
disabled = false
|
|
}: AvatarProps) {
|
|
const [uploading, setUploading] = useState(false);
|
|
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
|
|
|
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) {
|
|
console.log('Error downloading image: ', error instanceof Error ? error.message : error);
|
|
}
|
|
}
|
|
|
|
async function uploadAvatar() {
|
|
if (disabled || uploading) return;
|
|
|
|
try {
|
|
setUploading(true);
|
|
|
|
// Get current user
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
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;
|
|
}
|
|
|
|
// 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 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(fileName, arraybuffer, {
|
|
contentType: `image/${fileExt}`,
|
|
upsert: true,
|
|
});
|
|
|
|
if (uploadError) throw uploadError;
|
|
|
|
// Update user profile with new avatar URL
|
|
const { error: updateError } = await supabase
|
|
.from('profiles')
|
|
.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) {
|
|
Alert.alert('Error uploading avatar', error instanceof Error ? error.message : 'An unknown error occurred');
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={uploadAvatar}
|
|
style={[styles.avatarContainer, { opacity: disabled ? 0.7 : 1 }]}
|
|
disabled={disabled || uploading}
|
|
>
|
|
{avatarUrl ? (
|
|
<Image
|
|
source={{ uri: avatarUrl }}
|
|
style={[styles.avatar, { width: size, height: size, borderRadius: size / 2 }]}
|
|
/>
|
|
) : (
|
|
<ThemedView style={[styles.avatarPlaceholder, { width: size, height: size, borderRadius: size / 2 }]}>
|
|
<IconSymbol name="person.fill" size={size / 2.5} color="#999" />
|
|
</ThemedView>
|
|
)}
|
|
|
|
{uploading ? (
|
|
<ActivityIndicator style={styles.uploadingIndicator} size="small" color="#007AFF" />
|
|
) : (
|
|
disabled ? (
|
|
<ThemedView />
|
|
) : (
|
|
<ThemedText style={styles.changePhotoText}>
|
|
Change Photo
|
|
</ThemedText>
|
|
)
|
|
)}
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
avatarContainer: {
|
|
alignItems: 'center',
|
|
},
|
|
avatar: {
|
|
backgroundColor: '#E1E1E1',
|
|
},
|
|
avatarPlaceholder: {
|
|
backgroundColor: '#E1E1E1',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
changePhotoText: {
|
|
marginTop: 8,
|
|
color: '#007AFF',
|
|
fontSize: 16,
|
|
},
|
|
uploadingIndicator: {
|
|
marginTop: 8,
|
|
}
|
|
});
|