mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-25 02:57:56 +00:00
refactor(mobile): Remove i18n, expand core screens, update plan
BREAKING: Removed multi-language support (i18n) - will be re-added later Changes: - Removed i18n system (6 language files, LanguageContext) - Expanded WalletScreen, SettingsScreen, SwapScreen with more features - Added KurdistanSun component, HEZ/PEZ token icons - Added EditProfileScreen, WalletSetupScreen - Added button e2e tests (Profile, Settings, Wallet) - Updated plan: honest assessment - 42 nav buttons with mock data - Fixed terminology: Polkadot→Pezkuwi, Substrate→Bizinikiwi Reality check: UI complete with mock data, converting to production one-by-one
This commit is contained in:
@@ -16,6 +16,16 @@ 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 {
|
||||
showAlert(title, message, buttons);
|
||||
}
|
||||
};
|
||||
|
||||
// Avatar pool - Kurdish/Middle Eastern themed avatars
|
||||
const AVATAR_POOL = [
|
||||
{ id: 'avatar1', emoji: '👨🏻', label: 'Man 1' },
|
||||
@@ -74,7 +84,7 @@ const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
|
||||
if (Platform.OS !== 'web') {
|
||||
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
Alert.alert(
|
||||
showAlert(
|
||||
'Permission Required',
|
||||
'Sorry, we need camera roll permissions to upload your photo!'
|
||||
);
|
||||
@@ -111,63 +121,88 @@ const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
|
||||
if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadedUrl);
|
||||
setUploadedImageUri(uploadedUrl);
|
||||
setSelectedAvatar(null); // Clear emoji selection
|
||||
Alert.alert('Success', 'Photo uploaded successfully!');
|
||||
showAlert('Success', 'Photo uploaded successfully!');
|
||||
} else {
|
||||
if (__DEV__) console.error('[AvatarPicker] Upload failed: no URL returned');
|
||||
Alert.alert('Upload Failed', 'Could not upload your photo. Please check your internet connection and try again.');
|
||||
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);
|
||||
Alert.alert('Error', 'Failed to pick image. Please try again.');
|
||||
showAlert('Error', 'Failed to pick image. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const uploadImageToSupabase = async (imageUri: string): Promise<string | null> => {
|
||||
if (!user) {
|
||||
if (__DEV__) console.error('[AvatarPicker] No user found');
|
||||
console.error('[AvatarPicker] No user found');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (__DEV__) console.log('[AvatarPicker] Fetching image blob...');
|
||||
// Convert image URI to blob for web, or use file for native
|
||||
console.log('[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.log('[AvatarPicker] Blob size:', blob.size, 'bytes');
|
||||
console.log('[AvatarPicker] Blob created - size:', blob.size, 'bytes, type:', blob.type);
|
||||
|
||||
// Generate unique filename
|
||||
const fileExt = imageUri.split('.').pop()?.toLowerCase() || 'jpg';
|
||||
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
|
||||
const filePath = `avatars/${fileName}`;
|
||||
if (__DEV__) console.log('[AvatarPicker] Uploading to:', filePath);
|
||||
|
||||
// Upload to Supabase Storage
|
||||
const { data: uploadData, error: uploadError } = await supabase.storage
|
||||
.from('profiles')
|
||||
.upload(filePath, blob, {
|
||||
contentType: `image/${fileExt}`,
|
||||
upsert: false,
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
if (__DEV__) console.error('[AvatarPicker] Upload error:', uploadError);
|
||||
if (blob.size === 0) {
|
||||
console.error('[AvatarPicker] Blob is empty!');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (__DEV__) console.log('[AvatarPicker] Upload successful:', uploadData);
|
||||
// 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}`;
|
||||
|
||||
console.log('[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) {
|
||||
console.error('[AvatarPicker] Supabase upload error:', uploadError.message, uploadError);
|
||||
// Show more specific error to user
|
||||
showAlert('Upload Error', `Storage error: ${uploadError.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[AvatarPicker] Upload successful:', uploadData);
|
||||
|
||||
// Get public URL
|
||||
const { data } = supabase.storage
|
||||
.from('profiles')
|
||||
.from('avatars')
|
||||
.getPublicUrl(filePath);
|
||||
|
||||
if (__DEV__) console.log('[AvatarPicker] Public URL:', data.publicUrl);
|
||||
console.log('[AvatarPicker] Public URL:', data.publicUrl);
|
||||
|
||||
return data.publicUrl;
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('[AvatarPicker] Error uploading to Supabase:', error);
|
||||
} catch (error: any) {
|
||||
console.error('[AvatarPicker] Error uploading to Supabase:', error?.message || error);
|
||||
showAlert('Upload Error', `Failed to upload: ${error?.message || 'Unknown error'}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -176,7 +211,7 @@ const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
|
||||
const avatarToSave = uploadedImageUri || selectedAvatar;
|
||||
|
||||
if (!avatarToSave || !user) {
|
||||
Alert.alert('Error', 'Please select an avatar or upload a photo');
|
||||
showAlert('Error', 'Please select an avatar or upload a photo');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,7 +234,7 @@ const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
|
||||
|
||||
if (__DEV__) console.log('[AvatarPicker] Avatar saved successfully:', data);
|
||||
|
||||
Alert.alert('Success', 'Avatar updated successfully!');
|
||||
showAlert('Success', 'Avatar updated successfully!');
|
||||
|
||||
if (onAvatarSelected) {
|
||||
onAvatarSelected(avatarToSave);
|
||||
@@ -208,7 +243,7 @@ const AvatarPickerModal: React.FC<AvatarPickerModalProps> = ({
|
||||
onClose();
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('[AvatarPicker] Error updating avatar:', error);
|
||||
Alert.alert('Error', 'Failed to update avatar. Please try again.');
|
||||
showAlert('Error', 'Failed to update avatar. Please try again.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, Animated, Easing, StyleSheet } from 'react-native';
|
||||
import Svg, { Circle, Line, Defs, RadialGradient, Stop } from 'react-native-svg';
|
||||
|
||||
interface KurdistanSunProps {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const AnimatedView = Animated.View;
|
||||
|
||||
export const KurdistanSun: React.FC<KurdistanSunProps> = ({ size = 200 }) => {
|
||||
// Animation values
|
||||
const greenHaloRotation = useRef(new Animated.Value(0)).current;
|
||||
const redHaloRotation = useRef(new Animated.Value(0)).current;
|
||||
const yellowHaloRotation = useRef(new Animated.Value(0)).current;
|
||||
const raysPulse = useRef(new Animated.Value(1)).current;
|
||||
const glowPulse = useRef(new Animated.Value(0.6)).current;
|
||||
|
||||
useEffect(() => {
|
||||
// Green halo rotation (3s, clockwise)
|
||||
Animated.loop(
|
||||
Animated.timing(greenHaloRotation, {
|
||||
toValue: 1,
|
||||
duration: 3000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
|
||||
// Red halo rotation (2.5s, counter-clockwise)
|
||||
Animated.loop(
|
||||
Animated.timing(redHaloRotation, {
|
||||
toValue: -1,
|
||||
duration: 2500,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
|
||||
// Yellow halo rotation (2s, clockwise)
|
||||
Animated.loop(
|
||||
Animated.timing(yellowHaloRotation, {
|
||||
toValue: 1,
|
||||
duration: 2000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
|
||||
// Rays pulse animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(raysPulse, {
|
||||
toValue: 0.7,
|
||||
duration: 1000,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(raysPulse, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
|
||||
// Glow pulse animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(glowPulse, {
|
||||
toValue: 0.3,
|
||||
duration: 1000,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(glowPulse, {
|
||||
toValue: 0.6,
|
||||
duration: 1000,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
}, []);
|
||||
|
||||
const greenSpin = greenHaloRotation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
const redSpin = redHaloRotation.interpolate({
|
||||
inputRange: [-1, 0],
|
||||
outputRange: ['-360deg', '0deg'],
|
||||
});
|
||||
|
||||
const yellowSpin = yellowHaloRotation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
const haloSize = size * 0.9;
|
||||
const borderWidth = size * 0.02;
|
||||
|
||||
// Generate 21 rays for Kurdistan flag
|
||||
const rays = Array.from({ length: 21 }).map((_, i) => {
|
||||
const angle = (i * 360) / 21;
|
||||
return (
|
||||
<Line
|
||||
key={i}
|
||||
x1="100"
|
||||
y1="100"
|
||||
x2="100"
|
||||
y2="20"
|
||||
stroke="rgba(255, 255, 255, 0.9)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(${angle} 100 100)`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { width: size, height: size }]}>
|
||||
{/* Rotating colored halos */}
|
||||
<View style={styles.halosContainer}>
|
||||
{/* Green halo (outermost) */}
|
||||
<AnimatedView
|
||||
style={[
|
||||
styles.halo,
|
||||
{
|
||||
width: haloSize,
|
||||
height: haloSize,
|
||||
borderWidth: borderWidth,
|
||||
borderTopColor: '#00FF00',
|
||||
borderBottomColor: '#00FF00',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
transform: [{ rotate: greenSpin }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* Red halo (middle) */}
|
||||
<AnimatedView
|
||||
style={[
|
||||
styles.halo,
|
||||
{
|
||||
width: haloSize * 0.8,
|
||||
height: haloSize * 0.8,
|
||||
borderWidth: borderWidth,
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
borderLeftColor: '#FF0000',
|
||||
borderRightColor: '#FF0000',
|
||||
transform: [{ rotate: redSpin }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* Yellow halo (inner) */}
|
||||
<AnimatedView
|
||||
style={[
|
||||
styles.halo,
|
||||
{
|
||||
width: haloSize * 0.6,
|
||||
height: haloSize * 0.6,
|
||||
borderWidth: borderWidth,
|
||||
borderTopColor: '#FFD700',
|
||||
borderBottomColor: '#FFD700',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
transform: [{ rotate: yellowSpin }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Kurdistan Sun SVG with 21 rays */}
|
||||
<AnimatedView style={[styles.svgContainer, { opacity: raysPulse }]}>
|
||||
<Svg width={size} height={size} viewBox="0 0 200 200">
|
||||
<Defs>
|
||||
<RadialGradient id="sunGradient" cx="50%" cy="50%" r="50%">
|
||||
<Stop offset="0%" stopColor="rgba(255, 255, 255, 0.8)" />
|
||||
<Stop offset="100%" stopColor="rgba(255, 255, 255, 0.2)" />
|
||||
</RadialGradient>
|
||||
</Defs>
|
||||
|
||||
{/* Sun rays (21 rays for Kurdistan flag) */}
|
||||
{rays}
|
||||
|
||||
{/* Central white circle */}
|
||||
<Circle cx="100" cy="100" r="35" fill="white" />
|
||||
|
||||
{/* Inner glow */}
|
||||
<Circle cx="100" cy="100" r="35" fill="url(#sunGradient)" />
|
||||
</Svg>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
halosContainer: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
halo: {
|
||||
position: 'absolute',
|
||||
borderRadius: 1000,
|
||||
},
|
||||
svgContainer: {
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default KurdistanSun;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
export { default as HezTokenLogo } from './HezTokenLogo';
|
||||
export { default as PezTokenLogo } from './PezTokenLogo';
|
||||
Reference in New Issue
Block a user