FOUNDRY
C8 Platform
← Tasks

[UI-02] Campaign Selection Screen (retry 1)

completedui_genP0

Description

## UI-02: Campaign Selection Screen Section: UI Screens | Milestone: M1 | Owner: None ### Failing Tests - **campaigns_screen_file** (file_exists): File `src/screens/CampaignsScreen.tsx` must exist ### Acceptance Criteria - File `src/screens/CampaignsScreen.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-02: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago

Quality

Overall
94%
Pass
PASS
Graded By
-
Graded At
-

Cost

Input Tokens
-
Output Tokens
-
Est. USD
-

Result

{
  "cost": {
    "input_tokens": 202,
    "estimated_usd": 0.051126,
    "output_tokens": 3368
  },
  "model": "claude-sonnet-4-20250514",
  "status": "success",
  "transport": "anthropic_api",
  "latency_ms": 39932,
  "response_text": "I'll create the Campaign Selection Screen component based on the requirements and codebase conventions.\n\n```tsx\n// src/screens/CampaignsScreen.tsx\n// EN: Campaign selection screen component for displaying and managing campaigns\n// VI: Component màn hình lựa chọn chiến dịch để hiển thị và quản lý các chiến dịch\n// ES: Componente de pantalla de selección de campaña para mostrar y gestionar campañas\n\nimport React, { useState, useEffect } from 'react';\nimport { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert } from 'react-native';\nimport { SafeAreaView } from 'react-native-safe-area-context';\nimport { z } from 'zod';\n\n// EN: Zod schema for campaign data validation - SPEC: API-02\n// VI: Schema Zod để xác thực dữ liệu chiến dịch - SPEC: API-02\n// ES: Esquema Zod para validación de datos de campaña - SPEC: API-02\nconst CampaignSchema = z.object({\n  id: z.string(),\n  title: z.string(),\n  description: z.string().optional(),\n  status: z.enum(['active', 'inactive', 'completed']),\n  created_at: z.string(),\n  updated_at: z.string(),\n});\n\nconst CampaignsListSchema = z.array(CampaignSchema);\n\ntype Campaign = z.infer<typeof CampaignSchema>;\n\ninterface CampaignsScreenProps {\n  // EN: Navigation prop for screen navigation\n  // VI: Prop điều hướng cho việc chuyển màn hình\n  // ES: Prop de navegación para navegación de pantalla\n  navigation?: any;\n  // EN: Route parameters\n  // VI: Tham số đường dẫn\n  // ES: Parámetros de ruta\n  route?: any;\n}\n\nconst CampaignsScreen: React.FC<CampaignsScreenProps> = ({ navigation }) => {\n  // EN: State for campaigns list and loading status\n  // VI: State cho danh sách chiến dịch và trạng thái tải\n  // ES: Estado para lista de campañas y estado de carga\n  const [campaigns, setCampaigns] = useState<Campaign[]>([]);\n  const [loading, setLoading] = useState<boolean>(true);\n  const [error, setError] = useState<string | null>(null);\n\n  // EN: Fetch campaigns from API on component mount\n  // VI: Lấy dữ liệu chiến dịch từ API khi component được mount\n  // ES: Obtener campañas de la API al montar el componente\n  useEffect(() => {\n    fetchCampaigns();\n  }, []);\n\n  // EN: Function to fetch campaigns from API - SPEC: API-02\n  // VI: Hàm lấy dữ liệu chiến dịch từ API - SPEC: API-02\n  // ES: Función para obtener campañas de la API - SPEC: API-02\n  const fetchCampaigns = async () => {\n    try {\n      setLoading(true);\n      setError(null);\n\n      // EN: API call to fetch campaigns\n      // VI: Gọi API để lấy danh sách chiến dịch\n      // ES: Llamada a la API para obtener campañas\n      const response = await fetch('/api/campaigns', {\n        method: 'GET',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n      });\n\n      const data = await response.json();\n\n      if (!response.ok) {\n        throw new Error(data.error || 'Failed to fetch campaigns');\n      }\n\n      // EN: Validate response data with Zod schema\n      // VI: Xác thực dữ liệu phản hồi bằng schema Zod\n      // ES: Validar datos de respuesta con esquema Zod\n      const validationResult = CampaignsListSchema.safeParse(data.campaigns || []);\n\n      if (!validationResult.success) {\n        console.error('Campaign data validation failed:', validationResult.error);\n        throw new Error('Invalid campaign data format');\n      }\n\n      setCampaigns(validationResult.data);\n    } catch (err) {\n      console.error('Error fetching campaigns:', err);\n      setError(err instanceof Error ? err.message : 'Unknown error occurred');\n      \n      // EN: Show error alert to user\n      // VI: Hiển thị thông báo lỗi cho người dùng\n      // ES: Mostrar alerta de error al usuario\n      Alert.alert(\n        'Error',\n        'Failed to load campaigns. Please try again.',\n        [{ text: 'OK' }]\n      );\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  // EN: Handle campaign selection\n  // VI: Xử lý việc chọn chiến dịch\n  // ES: Manejar selección de campaña\n  const handleCampaignSelect = (campaign: Campaign) => {\n    // EN: Navigate to campaign details or perform action\n    // VI: Điều hướng đến chi tiết chiến dịch hoặc thực hiện hành động\n    // ES: Navegar a detalles de campaña o realizar acción\n    console.log('Selected campaign:', campaign.id);\n    \n    if (navigation) {\n      navigation.navigate('CampaignDetails', { campaignId: campaign.id });\n    }\n  };\n\n  // EN: Render individual campaign item\n  // VI: Render từng item chiến dịch\n  // ES: Renderizar elemento individual de campaña\n  const renderCampaignItem = ({ item }: { item: Campaign }) => (\n    <TouchableOpacity\n      style={styles.campaignItem}\n      onPress={() => handleCampaignSelect(item)}\n      activeOpacity={0.7}\n    >\n      <View style={styles.campaignContent}>\n        <Text style={styles.campaignTitle}>{item.title}</Text>\n        {item.description && (\n          <Text style={styles.campaignDescription} numberOfLines={2}>\n            {item.description}\n          </Text>\n        )}\n        <View style={styles.campaignMeta}>\n          <Text style={[styles.campaignStatus, { color: getStatusColor(item.status) }]}>\n            {item.status.toUpperCase()}\n          </Text>\n          <Text style={styles.campaignDate}>\n            {new Date(item.created_at).toLocaleDateString()}\n          </Text>\n        </View>\n      </View>\n    </TouchableOpacity>\n  );\n\n  // EN: Get color for campaign status\n  // VI: Lấy màu cho trạng thái chiến dịch\n  // ES: Obtener color para estado de campaña\n  const getStatusColor = (status: string) => {\n    switch (status) {\n      case 'active':\n        return '#4CAF50';\n      case 'inactive':\n        return '#FF9800';\n      case 'completed':\n        return '#2196F3';\n      default:\n        return '#757575';\n    }\n  };\n\n  // EN: Render loading state\n  // VI: Render trạng thái đang tải\n  // ES: Renderizar estado de carga\n  if (loading) {\n    return (\n      <SafeAreaView style={styles.container}>\n        <View style={styles.centerContent}>\n          <Text style={styles.loadingText}>Loading campaigns...</Text>\n        </View>\n      </SafeAreaView>\n    );\n  }\n\n  // EN: Render error state\n  // VI: Render trạng thái lỗi\n  // ES: Renderizar estado de error\n  if (error) {\n    return (\n      <SafeAreaView style={styles.container}>\n        <View style={styles.centerContent}>\n          <Text style={styles.errorText}>Error: {error}</Text>\n          <TouchableOpacity style={styles.retryButton} onPress={fetchCampaigns}>\n            <Text style={styles.retryButtonText}>Retry</Text>\n          </TouchableOpacity>\n        </View>\n      </SafeAreaView>\n    );\n  }\n\n  // EN: Main render\n  // VI: Render chính\n  // ES: Renderizado principal\n  return (\n    <SafeAreaView style={styles.container}>\n      <View style={styles.header}>\n        <Text style={styles.headerTitle}>Campaigns</Text>\n        <Text style={styles.headerSubtitle}>\n          {campaigns.length} campaign{campaigns.length !== 1 ? 's' : ''} available\n        </Text>\n      </View>\n\n      {campaigns.length === 0 ? (\n        <View style={styles.centerContent}>\n          <Text style={styles.emptyText}>No campaigns available</Text>\n        </View>\n      ) : (\n        <FlatList\n          data={campaigns}\n          renderItem={renderCampaignItem}\n          keyExtractor={(item) => item.id}\n          style={styles.campaignsList}\n          contentContainerStyle={styles.campaignsListContent}\n          showsVerticalScrollIndicator={false}\n          refreshing={loading}\n          onRefresh={fetchCampaigns}\n        />\n      )}\n    </SafeAreaView>\n  );\n};\n\n// EN: Styles for the campaigns screen\n// VI: Styles cho màn hình chiến dịch\n// ES: Estilos para la pantalla de campañas\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    backgroundColor: '#FFFFFF',\n  },\n  header: {\n    padding: 20,\n    borderBottomWidth: 1,\n    borderBottomColor: '#E0E0E0',\n  },\n  headerTitle: {\n    fontSize: 24,\n    fontWeight: 'bold',\n    color: '#333333',\n    marginBottom: 4,\n  },\n  headerSubtitle: {\n    fontSize: 14,\n    color: '#666666',\n  },\n  campaignsList: {\n    flex: 1,\n  },\n  campaignsListContent: {\n    padding: 16,\n  },\n  campaignItem: {\n    backgroundColor: '#FFFFFF',\n    borderRadius: 12,\n    padding: 16,\n    marginBottom: 12,\n    shadowColor: '#000000',\n    shadowOffset: {\n      width: 0,\n      height: 2,\n    },\n    shadowOpacity: 0.1,\n    shadowRadius: 4,\n    elevation: 3,\n    borderWidth: 1,\n    borderColor: '#F0F0F0',\n  },\n  campaignContent: {\n    flex: 1,\n  },\n  campaignTitle: {\n    fontSize: 18,\n    fontWeight: '600',\n    color: '#333333',\n    marginBottom: 8,\n  },\n  campaignDescription: {\n    fontSize: 14,\n    color: '#666666',\n    lineHeight: 20,\n    marginBottom: 12,\n  },\n  campaignMeta: {\n    flexDirection: 'row',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n  },\n  campaignStatus: {\n    fontSize: 12,\n    fontWeight: '600',\n    letterSpacing: 0.5,\n  },\n  campaignDate: {\n    fontSize: 12,\n    color: '#999999',\n  },\n  centerContent: {\n    flex: 1,\n    justifyContent: 'center',\n    alignItems: 'center',\n    padding: 20,\n  },\n  loadingText: {\n    fontSize: 16,\n    color: '#666666',\n  },\n  errorText: {\n    fontSize: 16,\n    color: '#F44336',\n    textAlign: 'center',\n    marginBottom: 20,\n  },\n  emptyText: {\n    fontSize: 16,\n    color: '#666666',\n    textAlign: 'center',\n  },\n  retryButton: {\n    backgroundColor: '#2196F3',\n    paddingHorizontal: 24,\n    paddingVertical: 12,\n    borderRadius: 8,\n  },\n  retryButtonText: {\n    color: '#FFFFFF',\n    fontSize: 16,\n    fontWeight: '600',\n  },\n});\n\nexport default CampaignsScreen;\n```\n\nThis implementation provides:\n\n1. **File Creation**: Creates the required `src/screens/CampaignsScreen.tsx` file\n2. **Trilingual Comments**: All major sections have EN/VI/ES comments\n3. **Zod Validation**: Module-level Zod schemas with `.safeParse()` for data validation\n4. **SPEC References**: Comments reference API-02 dependency\n5. **Error Handling**: Proper error format `{ error: 'message' }` expected from API\n6. **Campaign Management**: \n   - Fetches campaigns from API endpoint\n   - Displays campaigns in a scrollable list\n   - Handles loading, error, and empty states\n   - Campaign selection functionality\n   - Pull-to-refresh support\n7. **TypeScript**: Fully typed with proper interfaces\n8. **React Native**: Uses appropriate RN components and styling\n9. **State Management**: Proper useState and useEffect usage for data fetching\n\nThe component is ready for integration once the API-02 dependency is resolved."
}
Task ID: ea8d3ddf-e0ef-4bfd-8053-7a0a6ea5adf0