FOUNDRY
C8 Platform
← 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