mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 19:27:56 +00:00
549 lines
16 KiB
TypeScript
549 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
Modal,
|
|
TouchableOpacity,
|
|
StyleSheet,
|
|
ScrollView,
|
|
Image,
|
|
ActivityIndicator,
|
|
Platform,
|
|
Alert,
|
|
} from 'react-native';
|
|
import * as ImagePicker from 'expo-image-picker';
|
|
import { KurdistanColors } from '../theme/colors';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { supabase } from '../lib/supabase';
|
|
|
|
// Cross-platform alert helper
|
|
const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void}>) => {
|
|
if (Platform.OS === 'web') {
|
|
window.alert(`${title}\n\n${message}`);
|
|
if (buttons?.[0]?.onPress) buttons[0].onPress();
|
|
} else {
|
|
Alert.alert(title, message, buttons);
|
|
}
|
|
};
|
|
|
|
// Avatar pool - Kurdish/Middle Eastern themed avatars
|
|
const AVATAR_POOL = [
|
|
{ id: 'avatar1', emoji: '👨🏻', label: 'Man 1' },
|
|
{ id: 'avatar2', emoji: '👨🏼', label: 'Man 2' },
|
|
{ id: 'avatar3', emoji: '👨🏽', label: 'Man 3' },
|
|
{ id: 'avatar4', emoji: '👨🏾', label: 'Man 4' },
|
|
{ id: 'avatar5', emoji: '👩🏻', label: 'Woman 1' },
|
|
{ id: 'avatar6', emoji: '👩🏼', label: 'Woman 2' },
|
|
{ id: 'avatar7', emoji: '👩🏽', label: 'Woman 3' },
|
|
{ id: 'avatar8', emoji: '👩🏾', label: 'Woman 4' },
|
|
{ id: 'avatar9', emoji: '🧔🏻', label: 'Beard 1' },
|
|
{ id: 'avatar10', emoji: '🧔🏼', label: 'Beard 2' },
|
|
{ id: 'avatar11', emoji: '🧔🏽', label: 'Beard 3' },
|
|
{ id: 'avatar12', emoji: '🧔🏾', label: 'Beard 4' },
|
|
{ id: 'avatar13', emoji: '👳🏻♂️', label: 'Turban 1' },
|
|
{ id: 'avatar14', emoji: '👳🏼♂️', label: 'Turban 2' },
|
|
{ id: 'avatar15', emoji: '👳🏽♂️', label: 'Turban 3' },
|
|
{ id: 'avatar16', emoji: '🧕🏻', label: 'Hijab 1' },
|
|
{ id: 'avatar17', emoji: '🧕🏼', label: 'Hijab 2' },
|
|
{ id: 'avatar18', emoji: '🧕🏽', label: 'Hijab 3' },
|
|
{ id: 'avatar19', emoji: '👴🏻', label: 'Elder 1' },
|
|
{ id: 'avatar20', emoji: '👴🏼', label: 'Elder 2' },
|
|
{ id: 'avatar21', emoji: '👵🏻', label: 'Elder Woman 1' },
|
|
{ id: 'avatar22', emoji: '👵🏼', label: 'Elder Woman 2' },
|
|
{ id: 'avatar23', emoji: '👦🏻', label: 'Boy 1' },
|
|
{ id: 'avatar24', emoji: '👦🏼', label: 'Boy 2' },
|
|
{ id: 'avatar25', emoji: '👧🏻', label: 'Girl 1' },
|
|
{ id: 'avatar26', emoji: '👧🏼', label: 'Girl 2' },
|
|
];
|
|
|
|
interface AvatarPickerModalProps {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
currentAvatar?: string;
|
|
onAvatarSelected?: (avatarUrl: string) => void;
|
|
}
|
|
|
|
const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
|
|
visible,
|
|
onClose,
|
|
currentAvatar,
|
|
onAvatarSelected,
|
|
}) => {
|
|
const { user } = useAuth();
|
|
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(currentAvatar || null);
|
|
const [uploadedImageUri, setUploadedImageUri] = useState<string | null>(null);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
const handleAvatarSelect = (avatarId: string) => {
|
|
setSelectedAvatar(avatarId);
|
|
setUploadedImageUri(null); // Clear uploaded image when selecting from pool
|
|
};
|
|
|
|
const requestPermissions = async () => {
|
|
if (Platform.OS !== 'web') {
|
|
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
if (status !== 'granted') {
|
|
showAlert(
|
|
'Permission Required',
|
|
'Sorry, we need camera roll permissions to upload your photo!'
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const handlePickImage = async () => {
|
|
const hasPermission = await requestPermissions();
|
|
if (!hasPermission) return;
|
|
|
|
try {
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
mediaTypes: 'images',
|
|
allowsEditing: true,
|
|
aspect: [1, 1],
|
|
quality: 0.8,
|
|
});
|
|
|
|
if (!result.canceled && result.assets[0]) {
|
|
setIsUploading(true);
|
|
const imageUri = result.assets[0].uri;
|
|
|
|
if (__DEV__) console.warn('[AvatarPicker] Uploading image:', imageUri);
|
|
|
|
// Upload to Supabase Storage
|
|
const uploadedUrl = await uploadImageToSupabase(imageUri);
|
|
|
|
setIsUploading(false);
|
|
|
|
if (uploadedUrl) {
|
|
if (__DEV__) console.warn('[AvatarPicker] Upload successful:', uploadedUrl);
|
|
setUploadedImageUri(uploadedUrl);
|
|
setSelectedAvatar(null); // Clear emoji selection
|
|
showAlert('Success', 'Photo uploaded successfully!');
|
|
} else {
|
|
if (__DEV__) console.error('[AvatarPicker] Upload failed: no URL returned');
|
|
showAlert('Upload Failed', 'Could not upload your photo. Please check your internet connection and try again.');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
setIsUploading(false);
|
|
if (__DEV__) console.error('[AvatarPicker] Error picking image:', error);
|
|
showAlert('Error', 'Failed to pick image. Please try again.');
|
|
}
|
|
};
|
|
|
|
const uploadImageToSupabase = async (imageUri: string): Promise<string | null> => {
|
|
if (!user) {
|
|
if (__DEV__) console.warn('[AvatarPicker] No user found');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (__DEV__) console.warn('[AvatarPicker] Starting upload for URI:', imageUri.substring(0, 50) + '...');
|
|
|
|
// Convert image URI to blob
|
|
const response = await fetch(imageUri);
|
|
const blob = await response.blob();
|
|
if (__DEV__) console.warn('[AvatarPicker] Blob created - size:', blob.size, 'bytes, type:', blob.type);
|
|
|
|
if (blob.size === 0) {
|
|
if (__DEV__) console.warn('[AvatarPicker] Blob is empty!');
|
|
return null;
|
|
}
|
|
|
|
// Get file extension from blob type or URI
|
|
let fileExt = 'jpg';
|
|
if (blob.type) {
|
|
// Extract extension from MIME type (e.g., 'image/jpeg' -> 'jpeg')
|
|
const mimeExt = blob.type.split('/')[1];
|
|
if (mimeExt && mimeExt !== 'octet-stream') {
|
|
fileExt = mimeExt === 'jpeg' ? 'jpg' : mimeExt;
|
|
}
|
|
} else if (!imageUri.startsWith('blob:') && !imageUri.startsWith('data:')) {
|
|
// Try to get extension from URI for non-blob URIs
|
|
const uriExt = imageUri.split('.').pop()?.toLowerCase();
|
|
if (uriExt && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(uriExt)) {
|
|
fileExt = uriExt;
|
|
}
|
|
}
|
|
|
|
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
|
|
const filePath = `avatars/${fileName}`;
|
|
const contentType = blob.type || `image/${fileExt}`;
|
|
|
|
if (__DEV__) console.warn('[AvatarPicker] Uploading to path:', filePath, 'contentType:', contentType);
|
|
|
|
// Upload to Supabase Storage
|
|
const { data: uploadData, error: uploadError } = await supabase.storage
|
|
.from('avatars')
|
|
.upload(filePath, blob, {
|
|
contentType: contentType,
|
|
upsert: true, // Allow overwriting if file exists
|
|
});
|
|
|
|
if (uploadError) {
|
|
if (__DEV__) console.warn('[AvatarPicker] Supabase upload error:', uploadError.message, uploadError);
|
|
// Show more specific error to user
|
|
showAlert('Upload Error', `Storage error: ${uploadError.message}`);
|
|
return null;
|
|
}
|
|
|
|
if (__DEV__) console.warn('[AvatarPicker] Upload successful:', uploadData);
|
|
|
|
// Get public URL
|
|
const { data } = supabase.storage
|
|
.from('avatars')
|
|
.getPublicUrl(filePath);
|
|
|
|
if (__DEV__) console.warn('[AvatarPicker] Public URL:', data.publicUrl);
|
|
|
|
return data.publicUrl;
|
|
} catch (error: unknown) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
if (__DEV__) console.warn('[AvatarPicker] Error uploading to Supabase:', errorMessage);
|
|
showAlert('Upload Error', `Failed to upload: ${errorMessage}`);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
const avatarToSave = uploadedImageUri || selectedAvatar;
|
|
|
|
if (!avatarToSave || !user) {
|
|
showAlert('Error', 'Please select an avatar or upload a photo');
|
|
return;
|
|
}
|
|
|
|
if (__DEV__) console.warn('[AvatarPicker] Saving avatar:', avatarToSave);
|
|
|
|
setIsSaving(true);
|
|
|
|
try {
|
|
// Update avatar in Supabase profiles table
|
|
const { data, error } = await supabase
|
|
.from('profiles')
|
|
.update({ avatar_url: avatarToSave })
|
|
.eq('id', user.id)
|
|
.select();
|
|
|
|
if (error) {
|
|
if (__DEV__) console.error('[AvatarPicker] Save error:', error);
|
|
throw error;
|
|
}
|
|
|
|
if (__DEV__) console.warn('[AvatarPicker] Avatar saved successfully:', data);
|
|
|
|
showAlert('Success', 'Avatar updated successfully!');
|
|
|
|
if (onAvatarSelected) {
|
|
onAvatarSelected(avatarToSave);
|
|
}
|
|
|
|
onClose();
|
|
} catch (error) {
|
|
if (__DEV__) console.error('[AvatarPicker] Error updating avatar:', error);
|
|
showAlert('Error', 'Failed to update avatar. Please try again.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
visible={visible}
|
|
animationType="slide"
|
|
transparent={true}
|
|
onRequestClose={onClose}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContainer}>
|
|
{/* Header */}
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>Choose Your Avatar</Text>
|
|
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
|
<Text style={styles.closeButtonText}>✕</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Upload Photo Button */}
|
|
<View style={styles.uploadSection}>
|
|
<TouchableOpacity
|
|
style={[styles.uploadButton, isUploading && styles.uploadButtonDisabled]}
|
|
onPress={handlePickImage}
|
|
disabled={isUploading}
|
|
>
|
|
{isUploading ? (
|
|
<ActivityIndicator color={KurdistanColors.spi} size="small" />
|
|
) : (
|
|
<>
|
|
<Text style={styles.uploadButtonIcon}>📷</Text>
|
|
<Text style={styles.uploadButtonText}>Upload Your Photo</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
{/* Uploaded Image Preview */}
|
|
{uploadedImageUri && (
|
|
<View style={styles.uploadedPreview}>
|
|
<Image source={{ uri: uploadedImageUri }} style={styles.uploadedImage} />
|
|
<TouchableOpacity
|
|
style={styles.removeUploadButton}
|
|
onPress={() => setUploadedImageUri(null)}
|
|
>
|
|
<Text style={styles.removeUploadText}>✕</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Divider */}
|
|
<View style={styles.divider}>
|
|
<View style={styles.dividerLine} />
|
|
<Text style={styles.dividerText}>OR CHOOSE FROM POOL</Text>
|
|
<View style={styles.dividerLine} />
|
|
</View>
|
|
|
|
{/* Avatar Grid */}
|
|
<ScrollView style={styles.avatarScroll} showsVerticalScrollIndicator={false}>
|
|
<View style={styles.avatarGrid}>
|
|
{AVATAR_POOL.map((avatar) => (
|
|
<TouchableOpacity
|
|
key={avatar.id}
|
|
style={[
|
|
styles.avatarOption,
|
|
selectedAvatar === avatar.id && styles.avatarOptionSelected,
|
|
]}
|
|
onPress={() => handleAvatarSelect(avatar.id)}
|
|
>
|
|
<Text style={styles.avatarEmoji}>{avatar.emoji}</Text>
|
|
{selectedAvatar === avatar.id && (
|
|
<View style={styles.selectedBadge}>
|
|
<Text style={styles.selectedBadgeText}>✓</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* Footer Actions */}
|
|
<View style={styles.modalFooter}>
|
|
<TouchableOpacity
|
|
style={styles.cancelButton}
|
|
onPress={onClose}
|
|
disabled={isSaving}
|
|
>
|
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
|
|
onPress={handleSave}
|
|
disabled={isSaving}
|
|
>
|
|
{isSaving ? (
|
|
<ActivityIndicator color={KurdistanColors.spi} size="small" />
|
|
) : (
|
|
<Text style={styles.saveButtonText}>Save Avatar</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
justifyContent: 'flex-end',
|
|
},
|
|
modalContainer: {
|
|
backgroundColor: KurdistanColors.spi,
|
|
borderTopLeftRadius: 24,
|
|
borderTopRightRadius: 24,
|
|
maxHeight: '80%',
|
|
paddingBottom: 20,
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
padding: 20,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#E0E0E0',
|
|
},
|
|
modalTitle: {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
color: KurdistanColors.reş,
|
|
},
|
|
closeButton: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: '#F0F0F0',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
closeButtonText: {
|
|
fontSize: 18,
|
|
color: '#666',
|
|
},
|
|
avatarScroll: {
|
|
padding: 20,
|
|
},
|
|
avatarGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-between',
|
|
},
|
|
avatarOption: {
|
|
width: '22%',
|
|
aspectRatio: 1,
|
|
backgroundColor: '#F8F9FA',
|
|
borderRadius: 16,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginBottom: 12,
|
|
borderWidth: 3,
|
|
borderColor: 'transparent',
|
|
},
|
|
avatarOptionSelected: {
|
|
borderColor: KurdistanColors.kesk,
|
|
backgroundColor: '#E8F5E9',
|
|
},
|
|
avatarEmoji: {
|
|
fontSize: 36,
|
|
},
|
|
selectedBadge: {
|
|
position: 'absolute',
|
|
top: -4,
|
|
right: -4,
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: 12,
|
|
backgroundColor: KurdistanColors.kesk,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
selectedBadgeText: {
|
|
fontSize: 14,
|
|
color: KurdistanColors.spi,
|
|
fontWeight: 'bold',
|
|
},
|
|
modalFooter: {
|
|
flexDirection: 'row',
|
|
paddingHorizontal: 20,
|
|
paddingTop: 16,
|
|
gap: 12,
|
|
},
|
|
cancelButton: {
|
|
flex: 1,
|
|
padding: 16,
|
|
borderRadius: 12,
|
|
backgroundColor: '#F0F0F0',
|
|
alignItems: 'center',
|
|
},
|
|
cancelButtonText: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: '#666',
|
|
},
|
|
saveButton: {
|
|
flex: 1,
|
|
padding: 16,
|
|
borderRadius: 12,
|
|
backgroundColor: KurdistanColors.kesk,
|
|
alignItems: 'center',
|
|
},
|
|
saveButtonDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
saveButtonText: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
color: KurdistanColors.spi,
|
|
},
|
|
uploadSection: {
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 16,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#E0E0E0',
|
|
},
|
|
uploadButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: KurdistanColors.kesk,
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 20,
|
|
borderRadius: 12,
|
|
gap: 8,
|
|
},
|
|
uploadButtonDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
uploadButtonIcon: {
|
|
fontSize: 20,
|
|
},
|
|
uploadButtonText: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
color: KurdistanColors.spi,
|
|
},
|
|
uploadedPreview: {
|
|
marginTop: 12,
|
|
alignItems: 'center',
|
|
position: 'relative',
|
|
},
|
|
uploadedImage: {
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
borderWidth: 3,
|
|
borderColor: KurdistanColors.kesk,
|
|
},
|
|
removeUploadButton: {
|
|
position: 'absolute',
|
|
top: -4,
|
|
right: '38%',
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 14,
|
|
backgroundColor: KurdistanColors.sor,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
boxShadow: '0px 2px 3px rgba(0, 0, 0, 0.3)',
|
|
elevation: 4,
|
|
},
|
|
removeUploadText: {
|
|
color: KurdistanColors.spi,
|
|
fontSize: 14,
|
|
fontWeight: 'bold',
|
|
},
|
|
divider: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 16,
|
|
},
|
|
dividerLine: {
|
|
flex: 1,
|
|
height: 1,
|
|
backgroundColor: '#E0E0E0',
|
|
},
|
|
dividerText: {
|
|
fontSize: 11,
|
|
fontWeight: '600',
|
|
color: '#999',
|
|
marginHorizontal: 12,
|
|
letterSpacing: 0.5,
|
|
},
|
|
});
|
|
|
|
export default AvatarPickerModal;
|