import { useState } from 'react'; import { View, Text, TextInput, Pressable, ScrollView, Image, ActivityIndicator, KeyboardAvoidingView, Platform, Alert, } from 'react-native'; import { useRouter } from 'expo-router'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; // TODO(sdk54): migrate to new expo-file-system class-based API — see Task #14 import * as FileSystem from 'expo-file-system/legacy'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../lib/theme'; import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars'; import { resolveAvatar } from '../../lib/resolveAvatar'; import { apiFetch } from '../../lib/api'; import { useMe } from '../../hooks/useMe'; export default function ProfileEditScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const { t } = useTranslation(); const colors = useColors(); const { me, reload } = useMe(); const INPUT_STYLE = { fontSize: 16, lineHeight: 22, paddingVertical: 14, paddingHorizontal: 16, color: colors.text, fontFamily: 'Nunito_400Regular', backgroundColor: colors.surfaceElevated, borderRadius: 12, } as const; const [nickname, setNickname] = useState(me?.nickname ?? ''); const [avatarId, setAvatarId] = useState(me?.avatar ?? null); const [photoUri, setPhotoUri] = useState(null); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const displayAvatar = photoUri ?? avatarId; async function pickPhoto() { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert( t('profile.edit_photo_perm_title'), t('profile.edit_photo_perm_desc'), ); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [1, 1], quality: 0.7, }); if (result.canceled || !result.assets[0]) return; const uri = result.assets[0].uri; setPhotoUri(uri); setAvatarId(null); } async function save() { if (!nickname.trim()) return; setSaving(true); try { let finalAvatar: string | null = avatarId; if (photoUri) { setUploading(true); const base64 = await FileSystem.readAsStringAsync(photoUri, { encoding: FileSystem.EncodingType.Base64, }); const ext = photoUri.toLowerCase().endsWith('.png') ? 'png' : 'jpeg'; const dataUrl = `data:image/${ext};base64,${base64}`; const res = await apiFetch<{ url: string }>('/api/avatar/upload', { method: 'POST', body: { dataUrl }, }); finalAvatar = res.url; setUploading(false); } await apiFetch('/api/auth/me', { method: 'PATCH', body: { nickname: nickname.trim(), ...(finalAvatar !== me?.avatar ? { avatar: finalAvatar } : {}), }, }); reload(); router.back(); } catch (err: any) { setUploading(false); console.error('[profile/edit] save failed:', err?.message ?? err); Alert.alert(t('common.error'), err?.message ?? t('common.unknown_error')); } finally { setSaving(false); } } const resolvedPreview = photoUri ? photoUri : resolveAvatar(avatarId, nickname || (me?.nickname ?? '')); const hasChanges = nickname.trim() !== (me?.nickname ?? '') || photoUri !== null || avatarId !== me?.avatar; return ( router.back()} hitSlop={10} style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1, marginRight: 12 })} > {t('profile.edit_title')} ({ opacity: pressed || saving || !hasChanges || !nickname.trim() ? 0.4 : 1, })} > {saving ? ( ) : ( {t('profile.edit_save')} )} {/* Avatar preview + pick-photo button */} {uploading ? ( ) : null} ({ marginTop: 12, flexDirection: 'row', alignItems: 'center', gap: 6, opacity: pressed ? 0.5 : 1, })} > {t('profile.edit_photo_cta')} {/* Preset avatars */} {t('profile.edit_preset_label').toUpperCase()} {HERO_AVATARS.map((avatar) => { const isSelected = !photoUri && avatarId === avatar.id; return ( { setAvatarId(avatar.id); setPhotoUri(null); }} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1, })} > ); })} {/* Divider */} {/* Nickname */} {t('profile.edit_nickname_label').toUpperCase()} {t('profile.edit_nickname_hint')} ); }