import { useEffect, useRef, useState } from 'react'; import { Alert, Text, TextInput, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { Ionicons } from '@expo/vector-icons'; import { useColors } from '../../../lib/theme'; import { apiFetch } from '../../../lib/api'; import { invalidateMe, useMe } from '../../../hooks/useMe'; import { OnboardingShell } from '../OnboardingShell'; import { LyraBubble } from '../LyraBubble'; import { CTABar } from '../CTABar'; type CheckResult = | { available: true } | { available: false; reason: 'too_short' | 'too_long' | 'profanity' | 'taken' }; export function NicknameSlide({ onNext, current, total, }: { onNext: () => void; current: number; total: number; }) { const { t } = useTranslation(); const colors = useColors(); const { me } = useMe(); const [nickname, setNickname] = useState(me?.nickname ?? ''); const [saving, setSaving] = useState(false); const [checking, setChecking] = useState(false); const [checkResult, setCheckResult] = useState(null); const inputRef = useRef(null); const debounceTimerRef = useRef | null>(null); const checkSeqRef = useRef(0); const trimmed = nickname.trim(); const valid = checkResult?.available === true; // Debounced live-check während User tippt useEffect(() => { if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); if (trimmed.length === 0) { setCheckResult(null); setChecking(false); return; } if (trimmed.length < 3) { setCheckResult({ available: false, reason: 'too_short' }); setChecking(false); return; } setChecking(true); debounceTimerRef.current = setTimeout(async () => { const seq = ++checkSeqRef.current; try { const res = await apiFetch( `/api/profile/check-nickname?nickname=${encodeURIComponent(trimmed)}`, ); // Race-guard — verwirf veraltete Antworten if (seq !== checkSeqRef.current) return; setCheckResult(res); } catch { // Network-Error: nicht blocken, lass Server-Side bei Save validieren if (seq === checkSeqRef.current) setCheckResult({ available: true }); } finally { if (seq === checkSeqRef.current) setChecking(false); } }, 450); return () => { if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); }; }, [trimmed]); async function save() { if (!valid || saving) return; setSaving(true); try { await apiFetch('/api/auth/me', { method: 'PATCH', body: { nickname: trimmed }, }); await apiFetch('/api/profile/me/onboarding-step', { method: 'PATCH', body: { step: 'account' }, }).catch(() => {}); invalidateMe(); onNext(); } catch (e: unknown) { Alert.alert( t('common.error'), e instanceof Error ? e.message : t('common.unknown_error'), ); } finally { setSaving(false); } } // Visual-state für Border + Hint const borderColor = checkResult && !checkResult.available ? colors.error : valid ? colors.success : 'transparent'; return ( } > {t('onboarding.nickname.label')} {/* Status-Indicator rechts im Input */} {trimmed.length === 0 ? null : checking ? ( ) : valid ? ( ) : ( )} {/* Hint / Error-Text */} {checkResult && !checkResult.available ? t(`onboarding.nickname.error_${checkResult.reason}`) : t('onboarding.nickname.hint')} ); }