tech-tracker-expo/components/auth/Profile_Avatar.tsx

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,
}
});