← Tasks
[UI-14] User Profile Screen (retry 1)
completedui_genP0
Description
## UI-14: User Profile Screen
Section: UI Screens | Milestone: M1 | Owner: None
### Failing Tests
- **profile_screen_file** (file_exists): File `src/screens/ProfileScreen.tsx` must exist
### Acceptance Criteria
- File `src/screens/ProfileScreen.tsx` must exist
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- API-01: FAIL
- API-03: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
74%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 202,
"estimated_usd": 0.064086,
"output_tokens": 4232
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 44600,
"response_text": "I'll create the User Profile Screen component following the codebase conventions.\n\n```tsx\n// src/screens/ProfileScreen.tsx\n// EN: User profile screen component for displaying and editing user information\n// VI: Màn hình hồ sơ người dùng để hiển thị và chỉnh sửa thông tin người dùng\n// ES: Pantalla de perfil de usuario para mostrar y editar información del usuario\n// SPEC: UI-14 - User Profile Screen\n\nimport React, { useState, useEffect } from 'react';\nimport { View, Text, StyleSheet, ScrollView, TouchableOpacity, TextInput, Alert, Image } from 'react-native';\nimport { z } from 'zod';\n\n// EN: User profile validation schema\n// VI: Schema xác thực hồ sơ người dùng\n// ES: Schema de validación del perfil de usuario\nconst ProfileSchema = z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n username: z.string().min(3).max(50),\n first_name: z.string().min(1).max(100),\n last_name: z.string().min(1).max(100),\n bio: z.string().max(500).optional(),\n avatar_url: z.string().url().optional(),\n created_at: z.string(),\n updated_at: z.string(),\n});\n\ntype UserProfile = z.infer<typeof ProfileSchema>;\n\ninterface ProfileScreenProps {\n userId: string;\n onLogout?: () => void;\n onUpdateProfile?: (profile: Partial<UserProfile>) => Promise<void>;\n}\n\nconst ProfileScreen: React.FC<ProfileScreenProps> = ({\n userId,\n onLogout,\n onUpdateProfile,\n}) => {\n const [profile, setProfile] = useState<UserProfile | null>(null);\n const [isEditing, setIsEditing] = useState(false);\n const [isLoading, setIsLoading] = useState(true);\n const [editForm, setEditForm] = useState({\n username: '',\n first_name: '',\n last_name: '',\n bio: '',\n });\n\n useEffect(() => {\n loadUserProfile();\n }, [userId]);\n\n // EN: Load user profile data from API\n // VI: Tải dữ liệu hồ sơ người dùng từ API\n // ES: Cargar datos del perfil de usuario desde API\n const loadUserProfile = async () => {\n try {\n setIsLoading(true);\n \n // TODO: Replace with actual API call when API-01 is implemented\n const response = await fetch(`/api/users/${userId}`);\n const data = await response.json();\n \n if (response.ok) {\n const result = ProfileSchema.safeParse(data);\n if (result.success) {\n setProfile(result.data);\n setEditForm({\n username: result.data.username,\n first_name: result.data.first_name,\n last_name: result.data.last_name,\n bio: result.data.bio || '',\n });\n } else {\n throw new Error('Invalid profile data format');\n }\n } else {\n throw new Error(data.error || 'Failed to load profile');\n }\n } catch (error) {\n console.error('Profile loading error:', error);\n Alert.alert(\n 'Error', // EN: Error\n 'Failed to load profile. Please try again.', // VI: Không thể tải hồ sơ. Vui lòng thử lại. // ES: Error al cargar perfil. Inténtalo de nuevo.\n );\n } finally {\n setIsLoading(false);\n }\n };\n\n // EN: Handle profile update\n // VI: Xử lý cập nhật hồ sơ\n // ES: Manejar actualización de perfil\n const handleSaveProfile = async () => {\n try {\n if (onUpdateProfile) {\n await onUpdateProfile(editForm);\n setProfile(prev => prev ? { ...prev, ...editForm } : null);\n setIsEditing(false);\n Alert.alert(\n 'Success', // EN: Success\n 'Profile updated successfully!', // VI: Cập nhật hồ sơ thành công! // ES: ¡Perfil actualizado exitosamente!\n );\n }\n } catch (error) {\n console.error('Profile update error:', error);\n Alert.alert(\n 'Error', // EN: Error\n 'Failed to update profile. Please try again.', // VI: Không thể cập nhật hồ sơ. Vui lòng thử lại. // ES: Error al actualizar perfil. Inténtalo de nuevo.\n );\n }\n };\n\n // EN: Handle logout action\n // VI: Xử lý hành động đăng xuất\n // ES: Manejar acción de cerrar sesión\n const handleLogout = () => {\n Alert.alert(\n 'Logout', // EN: Logout // VI: Đăng xuất // ES: Cerrar sesión\n 'Are you sure you want to logout?', // VI: Bạn có chắc muốn đăng xuất? // ES: ¿Estás seguro de que quieres cerrar sesión?\n [\n { text: 'Cancel', style: 'cancel' }, // VI: Hủy // ES: Cancelar\n { \n text: 'Logout', // VI: Đăng xuất // ES: Cerrar sesión\n style: 'destructive',\n onPress: onLogout,\n },\n ],\n );\n };\n\n if (isLoading) {\n return (\n <View style={styles.centerContainer}>\n <Text style={styles.loadingText}>\n Loading profile... {/* VI: Đang tải hồ sơ... */} {/* ES: Cargando perfil... */}\n </Text>\n </View>\n );\n }\n\n if (!profile) {\n return (\n <View style={styles.centerContainer}>\n <Text style={styles.errorText}>\n Profile not found {/* VI: Không tìm thấy hồ sơ */} {/* ES: Perfil no encontrado */}\n </Text>\n <TouchableOpacity style={styles.retryButton} onPress={loadUserProfile}>\n <Text style={styles.retryButtonText}>\n Retry {/* VI: Thử lại */} {/* ES: Reintentar */}\n </Text>\n </TouchableOpacity>\n </View>\n );\n }\n\n return (\n <ScrollView style={styles.container}>\n <View style={styles.header}>\n {profile.avatar_url ? (\n <Image source={{ uri: profile.avatar_url }} style={styles.avatar} />\n ) : (\n <View style={styles.avatarPlaceholder}>\n <Text style={styles.avatarText}>\n {profile.first_name[0]?.toUpperCase()}{profile.last_name[0]?.toUpperCase()}\n </Text>\n </View>\n )}\n \n <View style={styles.headerActions}>\n <TouchableOpacity\n style={styles.editButton}\n onPress={() => setIsEditing(!isEditing)}\n >\n <Text style={styles.editButtonText}>\n {isEditing ? 'Cancel' : 'Edit'} {/* VI: Hủy : Chỉnh sửa */} {/* ES: Cancelar : Editar */}\n </Text>\n </TouchableOpacity>\n \n <TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>\n <Text style={styles.logoutButtonText}>\n Logout {/* VI: Đăng xuất */} {/* ES: Cerrar sesión */}\n </Text>\n </TouchableOpacity>\n </View>\n </View>\n\n <View style={styles.content}>\n <View style={styles.field}>\n <Text style={styles.label}>\n Email {/* VI: Email */} {/* ES: Correo electrónico */}\n </Text>\n <Text style={styles.value}>{profile.email}</Text>\n </View>\n\n <View style={styles.field}>\n <Text style={styles.label}>\n Username {/* VI: Tên người dùng */} {/* ES: Nombre de usuario */}\n </Text>\n {isEditing ? (\n <TextInput\n style={styles.input}\n value={editForm.username}\n onChangeText={(text) => setEditForm(prev => ({ ...prev, username: text }))}\n placeholder=\"Enter username\" // VI: Nhập tên người dùng // ES: Ingresa nombre de usuario\n />\n ) : (\n <Text style={styles.value}>{profile.username}</Text>\n )}\n </View>\n\n <View style={styles.field}>\n <Text style={styles.label}>\n First Name {/* VI: Tên */} {/* ES: Nombre */}\n </Text>\n {isEditing ? (\n <TextInput\n style={styles.input}\n value={editForm.first_name}\n onChangeText={(text) => setEditForm(prev => ({ ...prev, first_name: text }))}\n placeholder=\"Enter first name\" // VI: Nhập tên // ES: Ingresa nombre\n />\n ) : (\n <Text style={styles.value}>{profile.first_name}</Text>\n )}\n </View>\n\n <View style={styles.field}>\n <Text style={styles.label}>\n Last Name {/* VI: Họ */} {/* ES: Apellido */}\n </Text>\n {isEditing ? (\n <TextInput\n style={styles.input}\n value={editForm.last_name}\n onChangeText={(text) => setEditForm(prev => ({ ...prev, last_name: text }))}\n placeholder=\"Enter last name\" // VI: Nhập họ // ES: Ingresa apellido\n />\n ) : (\n <Text style={styles.value}>{profile.last_name}</Text>\n )}\n </View>\n\n <View style={styles.field}>\n <Text style={styles.label}>\n Bio {/* VI: Tiểu sử */} {/* ES: Biografía */}\n </Text>\n {isEditing ? (\n <TextInput\n style={[styles.input, styles.textArea]}\n value={editForm.bio}\n onChangeText={(text) => setEditForm(prev => ({ ...prev, bio: text }))}\n placeholder=\"Tell us about yourself...\" // VI: Nói về bản thân bạn... // ES: Cuéntanos sobre ti...\n multiline\n numberOfLines={4}\n />\n ) : (\n <Text style={styles.value}>\n {profile.bio || 'No bio available'} {/* VI: Không có tiểu sử */} {/* ES: Sin biografía disponible */}\n </Text>\n )}\n </View>\n\n {isEditing && (\n <TouchableOpacity style={styles.saveButton} onPress={handleSaveProfile}>\n <Text style={styles.saveButtonText}>\n Save Changes {/* VI: Lưu thay đổi */} {/* ES: Guardar cambios */}\n </Text>\n </TouchableOpacity>\n )}\n\n <View style={styles.metadata}>\n <Text style={styles.metadataText}>\n Member since: {new Date(profile.created_at).toLocaleDateString()}\n {/* VI: Thành viên từ: */} {/* ES: Miembro desde: */}\n </Text>\n </View>\n </View>\n </ScrollView>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: '#f8f9fa',\n },\n centerContainer: {\n flex: 1,\n justifyContent: 'center',\n alignItems: 'center',\n padding: 20,\n },\n header: {\n flexDirection: 'row',\n alignItems: 'center',\n padding: 20,\n backgroundColor: '#fff',\n borderBottomWidth: 1,\n borderBottomColor: '#e9ecef',\n },\n avatar: {\n width: 80,\n height: 80,\n borderRadius: 40,\n },\n avatarPlaceholder: {\n width: 80,\n height: 80,\n borderRadius: 40,\n backgroundColor: '#007bff',\n justifyContent: 'center',\n alignItems: 'center',\n },\n avatarText: {\n color: '#fff',\n fontSize: 24,\n fontWeight: 'bold',\n },\n headerActions: {\n flex: 1,\n flexDirection: 'row',\n justifyContent: 'flex-end',\n gap: 10,\n },\n editButton: {\n backgroundColor: '#007bff',\n paddingHorizontal: 16,\n paddingVertical: 8,\n borderRadius: 6,\n },\n editButtonText: {\n color: '#fff',\n fontSize: 14,\n fontWeight: '500',\n },\n logoutButton: {\n backgroundColor: '#dc3545',\n paddingHorizontal: 16,\n paddingVertical: 8,\n borderRadius: 6,\n },\n logoutButtonText: {\n color: '#fff',\n fontSize: 14,\n fontWeight: '500',\n },\n content: {\n padding: 20,\n },\n field: {\n marginBottom: 20,\n },\n label: {\n fontSize: 16,\n fontWeight: '600',\n color: '#495057',\n marginBottom: 8,\n },\n value: {\n fontSize: 16,\n color: '#212529',\n backgroundColor: '#fff',\n padding: 12,\n borderRadius: 6,\n borderWidth: 1,\n borderColor: '#e9ecef',\n },\n input: {\n fontSize: 16,\n color: '#212529',\n backgroundColor: '#fff',\n padding: 12,\n borderRadius: 6,\n borderWidth: 1,\n borderColor: '#ced4da',\n },\n textArea: {\n height: 100,\n textAlignVertical: 'top',\n },\n saveButton: {\n backgroundColor: '#28a745',\n padding: 16,\n borderRadius: 6,\n alignItems: 'center',\n marginTop: 20,\n },\n saveButtonText: {\n color: '#fff',\n fontSize: 16,\n fontWeight: '600',\n },\n retryButton: {\n backgroundColor: '#007bff',\n paddingHorizontal: 20,\n paddingVertical: 12,\n borderRadius: 6,\n marginTop: 16,\n },\n retryButtonText: {\n color: '#fff',\n fontSize: 16,\n fontWeight: '500',\n },\n loadingText: {\n fontSize: 16,\n color: '#6c757d',\n },\n errorText: {\n fontSize: 16,\n color: '#dc3545',\n textAlign: 'center',\n },\n metadata: {\n marginTop: 30,\n paddingTop: 20,\n borderTopWidth: 1,\n borderTopColor: '#e9ecef',\n },\n metadataText: {\n fontSize: 14,\n color: '#6c757d',\n textAlign: 'center',\n },\n});\n\nexport default ProfileScreen;\n```\n\nThis ProfileScreen component includes:\n\n1. **File Structure**: Created at `src/screens/ProfileScreen.tsx` as required\n2. **Trilingual Comments**: English, Vietnamese, and Spanish comments throughout\n3. **Zod Validation**: Module-level schema with `.safeParse()` validation\n4. **User Profile Features**:\n - Display user information (email, username, name, bio, avatar)\n - Edit mode for updating profile information\n - Avatar display with fallback initials\n - Logout functionality\n - Member since date display\n5. **Error Handling**: Proper error handling with user-friendly messages\n6. **Loading States**: Loading indicator and error states\n7. **Responsive Design**: Clean, mobile-friendly interface\n8. **TypeScript**: Fully typed with proper interfaces\n\nThe component is ready for integration once the dependent APIs (API-01 and API-03) are implemented."
}Task ID: 87c3ee6c-cab3-4998-8a5f-749019677ad9