WellNuo/app/(auth)/enter-name.tsx
Sergei d453126c89 feat: Room location picker + robster credentials
- Backend: Update Legacy API credentials to robster/rob2
- Frontend: ROOM_LOCATIONS with icons and legacyCode mapping
- Device Settings: Modal picker for room selection
- api.ts: Bidirectional conversion (code ↔ name)
- Various UI/UX improvements across screens

PRD-DEPLOYMENT.md completed (Score: 9/10)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 15:22:40 -08:00

245 lines
6.2 KiB
TypeScript

import React, { useState, useCallback, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableOpacity,
} from 'react-native';
import { router, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { ErrorMessage } from '@/components/ui/ErrorMessage';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { api } from '@/services/api';
export default function EnterNameScreen() {
const params = useLocalSearchParams<{ email: string; inviteCode: string }>();
const email = params.email || '';
const inviteCode = params.inviteCode || '';
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleContinue = useCallback(async () => {
setError(null);
const trimmedFirstName = firstName.trim();
const trimmedLastName = lastName.trim();
if (!trimmedFirstName) {
setError('Please enter your first name');
return;
}
setIsLoading(true);
try {
// Update profile with name via API
const response = await api.updateProfile({
firstName: trimmedFirstName,
lastName: trimmedLastName || undefined,
});
if (!response.ok) {
throw new Error(response.error?.message || 'Failed to update profile');
}
// Navigate to add loved one screen (onboarding step 1)
router.replace({
pathname: '/(auth)/add-loved-one',
params: {
email,
inviteCode,
},
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save name');
} finally {
setIsLoading(false);
}
}, [firstName, lastName, email, inviteCode]);
const handleSkip = () => {
// Skip entering name and go to add loved one
router.replace({
pathname: '/(auth)/add-loved-one',
params: {
email,
inviteCode,
},
});
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Skip Button */}
<View style={styles.headerRow}>
<View style={{ width: 44 }} />
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
<Text style={styles.skipText}>Skip</Text>
</TouchableOpacity>
</View>
{/* Icon */}
<View style={styles.iconContainer}>
<View style={styles.iconCircle}>
<Ionicons name="person" size={48} color={AppColors.primary} />
</View>
</View>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>What's your name?</Text>
<Text style={styles.subtitle}>
This helps us personalize your experience
</Text>
</View>
{/* Error Message */}
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
{/* Form */}
<View style={styles.form}>
<Input
label="First Name"
placeholder="Enter your first name"
leftIcon="person-outline"
value={firstName}
onChangeText={(text) => {
setFirstName(text);
setError(null);
}}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
returnKeyType="next"
/>
<Input
label="Last Name (optional)"
placeholder="Enter your last name"
leftIcon="person-outline"
value={lastName}
onChangeText={setLastName}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
onSubmitEditing={handleContinue}
returnKeyType="done"
/>
</View>
{/* Continue Button */}
<View style={styles.buttonContainer}>
<Button
title="Continue"
onPress={handleContinue}
loading={isLoading}
fullWidth
size="lg"
/>
</View>
{/* Info */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
You can update your name later in settings
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
paddingBottom: Spacing.xl,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: Spacing.xl,
},
skipButton: {
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
},
skipText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
fontWeight: '500',
},
iconContainer: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
iconCircle: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: `${AppColors.primary}15`,
justifyContent: 'center',
alignItems: 'center',
},
header: {
alignItems: 'center',
marginBottom: Spacing.xl,
},
title: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
marginBottom: Spacing.md,
textAlign: 'center',
},
subtitle: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
},
form: {
gap: Spacing.md,
},
buttonContainer: {
marginTop: Spacing.xl,
},
infoContainer: {
alignItems: 'center',
marginTop: Spacing.lg,
paddingHorizontal: Spacing.md,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
textAlign: 'center',
},
});