import { useState } from 'react'; import { View, Text, TextInput, TouchableOpacity, ScrollView, ActivityIndicator, Alert, } from 'react-native'; import { Image } from 'expo-image'; 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'; import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen'; 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; } // iOS-native square crop UI — pan/zoom built in. Output is the user's // actual selection, not the original image (which is what the old // AvatarCropSheet returned). The avatar is rendered with borderRadius // everywhere it's shown, so square output reads as a circle visually. const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [1, 1], quality: 0.85, }); if (result.canceled || !result.assets[0]) return; setPhotoUri(result.assets[0].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); } } // Avatar-Preview: Dicebear-Seed muss STABIL bleiben während User den Nickname tippt // — sonst wechselt das Bild bei jedem Keystroke. Daher den gespeicherten `me?.nickname` // als Seed nehmen (nicht den live-getippten lokalen `nickname`-State). const resolvedPreview = photoUri ? photoUri : resolveAvatar(avatarId, me?.nickname ?? ''); const hasChanges = nickname.trim() !== (me?.nickname ?? '') || photoUri !== null || avatarId !== me?.avatar; return ( router.back()} hitSlop={10} activeOpacity={0.5} style={{ marginRight: 12 }} > {t('profile.edit_title')} {saving ? ( ) : ( {t('profile.edit_save')} )} {/* Avatar preview + pick-photo button */} {uploading ? ( ) : null} {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); }} activeOpacity={0.7} > ); })} {/* Divider */} {/* Nickname */} {t('profile.edit_nickname_label').toUpperCase()} { if (!saving && hasChanges && nickname.trim()) save(); }} /> {t('profile.edit_nickname_hint')} ); }