← 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