← Tasks
[E2E-v2] CampaignPro VoterList
completedcode_genP1
Description
Generate a React Native / Expo screen component for CampaignPro Mexico.
SCREEN: Voter List — Search + Sentiment Filter
FRAMEWORK: Expo + React Native + TypeScript
THEME: Dark (#0f172a bg, #1e293b surface, #e11d48 accent, white text)
LANGUAGE: Spanish UI labels
Requirements:
- Search bar at top (placeholder: 'Buscar votante...')
- Filter chips: Todos, A Favor, Indeciso, En Contra
- FlatList of voter cards showing: name, colonia, sentiment badge, last contact date
- Sentiment color coding: green
Assignee
-
Claimed By
G3 Flash
Created
22d ago
Completed
-
Quality
Overall
72%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"arm": "G3 Flash",
"code": "import React, { useState, useMemo } from 'react';\nimport {\n StyleSheet,\n View,\n Text,\n TextInput,\n FlatList,\n TouchableOpacity,\n StatusBar,\n SafeAreaView,\n RefreshControl,\n} from 'react-native';\nimport { Ionicons } from '@expo/vector-icons';\n\n// --- Types ---\ntype Sentiment = 'Todos' | 'A Favor' | 'Indeciso' | 'En Contra';\n\ninterface Voter {\n id: string;\n name: string;\n colonia: string;\n sentiment: Exclude<Sentiment, 'Todos'>;\n lastContact: string;\n}\n\n// --- Mock Data ---\nconst MOCK_VOTERS: Voter[] = [\n { id: '1', name: 'Juan Pérez García', colonia: 'Centro', sentiment: 'A Favor', lastContact: '2023-10-24' },\n { id: '2', name: 'María Elena Rodríguez', colonia: 'Polanco', sentiment: 'Indeciso', lastContact: '2023-10-22' },\n { id: '3', name: 'Ricardo Salinas', colonia: 'Roma Norte', sentiment: 'En Contra', lastContact: '2023-10-21' },\n { id: '4', name: 'Sofía Méndez', colonia: 'Condesa', sentiment: 'A Favor', lastContact: '2023-10-20' },\n { id: '5', name: 'Carlos Slim Domit', colonia: 'Lomas de Chapultepec', sentiment: 'Indeciso', lastContact: '2023-10-18' },\n];\n\nconst VoterListScreen = () => {\n const [search, setSearch] = useState('');\n const [activeFilter, setActiveFilter] = useState<Sentiment>('Todos');\n const [refreshing, setRefreshing] = useState(false);\n\n const onRefresh = React.useCallback(() => {\n setRefreshing(true);\n setTimeout(() => setRefreshing(false), 1500);\n }, []);\n\n const filteredVoters = useMemo(() => {\n return MOCK_VOTERS.filter((voter) => {\n const matchesSearch = voter.name.toLowerCase().includes(search.toLowerCase()) || \n voter.colonia.toLowerCase().includes(search.toLowerCase());\n const matchesFilter = activeFilter === 'Todos' || voter.sentiment === activeFilter;\n return matchesSearch && matchesFilter;\n });\n }, [search, activeFilter]);\n\n const getSentimentColor = (sentiment: Sentiment) => {\n switch (sentiment) {\n case 'A Favor': return '#22c55e';\n case 'Indeciso': return '#eab308';\n case 'En Contra': return '#ef4444';\n default: return '#94a3b8';\n }\n };\n\n const FilterChip = ({ label }: { label: Sentiment }) => (\n <TouchableOpacity\n style={[\n styles.chip,\n activeFilter === label && styles.chipActive\n ]}\n onPress={() => setActiveFilter(label)}\n >\n <Text style={[styles.chipText, activeFilter === label && styles.chipTextActive]}>\n {label}\n </Text>\n </TouchableOpacity>\n );\n\n const renderVoterCard = ({ item }: { item: Voter }) => (\n <View style={styles.card}>\n <View style={styles.cardHeader}>\n <View>\n <Text style={styles.voterName}>{item.name}</Text>\n <Text style={styles.voterColonia}>{item.colonia}</Text>\n </View>\n <View style={[styles.badge, { backgroundColor: getSentimentColor(item.sentiment) + '20', borderColor: getSentimentColor(item.sentiment) }]}>\n <Text style={[styles.badgeText, { color: getSentimentColor(item.sentiment) }]}>{item.sentiment}</Text>\n </View>\n </View>\n <View style={styles.cardFooter}>\n <Ionicons name=\"calendar-outline\" size={14} color=\"#94a3b8\" />\n <Text style={styles.lastContact}>Último contacto: {item.lastContact}</Text>\n </View>\n </View>\n );\n\n return (\n <SafeAreaView style={styles.container}>\n <StatusBar barStyle=\"light-content\" />\n \n {/* Search Header */}\n <View style={styles.header}>\n <View style={styles.searchContainer}>\n <Ionicons name=\"search\" size={20} color=\"#94a3b8\" style={styles.searchIcon} />\n <TextInput\n style={styles.searchInput}\n placeholder=\"Buscar votante...\"\n placeholderTextColor=\"#94a3b8\"\n value={search}\n onChangeText={setSearch}\n />\n </View>\n\n {/* Filter Chips */}\n <View style={styles.filterRow}>\n <FlatList\n horizontal\n showsHorizontalScrollIndicator={false}\n data={['Todos', 'A Favor', 'Indeciso', 'En Contra'] as Sentiment[]}\n renderItem={({ item }) => <FilterChip label={item} />}\n keyExtractor={(item) => item}\n contentContainerStyle={styles.filterList}\n />\n </View>\n </View>\n\n {/* List */}\n <FlatList\n data={filteredVoters}\n keyExtractor={(item) => item.id}\n renderItem={renderVoterCard}\n contentContainerStyle={styles.listContent}\n refreshControl={\n <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor=\"#e11d48\" />\n }\n ListEmptyComponent={\n <View style={styles.emptyContainer}>\n <Ionicons name=\"people-outline\" size={80} color=\"#1e293b\" />\n <Text style={styles.emptyTitle}>No se encontraron votantes</Text>\n <Text style={styles.emptySubtitle}>Intenta ajustar tus filtros de búsqueda</Text>\n </View>\n }\n />\n\n {/* FAB */}\n <TouchableOpacity style={styles.fab}>\n <Ionicons name=\"add\" size={30} color=\"white\" />\n </TouchableOpacity>\n </SafeAreaView>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: '#0f172a',\n },\n header: {\n paddingTop: 16,\n paddingBottom: 8,\n backgroundColor: '#0f172a',\n borderBottomWidth: 1,\n borderBottomColor: '#1e293b',\n },\n searchContainer: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: '#1e293b',\n marginHorizontal: 16,\n borderRadius: 12,\n paddingHorizontal: 12,\n height: 48,\n },\n searchIcon: {\n marginRight: 8,\n },\n searchInput: {\n flex: 1,\n color: 'white',\n fontSize: 16,\n },\n filterRow: {\n marginTop: 16,\n },\n filterList: {\n paddingHorizontal: 16,\n gap: 8,\n },\n chip: {\n paddingHorizontal: 16,\n paddingVertical: 8,\n borderRadius: 20,\n backgroundColor: '#1e293b',\n marginRight: 8,\n borderWidth: 1,\n borderColor: '#334155',\n },\n chipActive: {\n backgroundColor: '#e11d48',\n borderColor: '#e11d48',\n },\n chipText: {\n color: '#94a3b8',\n fontWeight: '600',\n fontSize: 14,\n },\n chipTextActive: {\n color: 'white',\n },\n listContent: {\n padding: 16,\n paddingBottom: 100,\n },\n card: {\n backgroundColor: '#1e293b',\n borderRadius: 16,\n padding: 16,\n marginBottom: 12,\n borderWidth: 1,\n borderColor: '#334155',\n },\n cardHeader: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'flex-start',\n },\n voterName: {\n color: 'white',\n fontSize: 18,\n fontWeight: 'bold',\n marginBottom: 4,\n },\n voterColonia: {\n color: '#94a3b8',\n fontSize: 14,\n },\n badge: {\n paddingHorizontal: 10,\n paddingVertical: 4,\n borderRadius: 8,\n borderWidth: 1,\n },\n badgeText: {\n fontSize: 12,\n fontWeight: 'bold',\n },\n cardFooter: {\n flexDirection: 'row',\n alignItems: 'center',\n marginTop: 12,\n paddingTop: 12,\n borderTopWidth: 1,\n borderTopColor: '#334155',\n },\n lastContact: {\n color: '#94a3b8',\n fontSize: 12,\n marginLeft: 6,\n },\n emptyContainer: {\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n marginTop: 100,\n },\n emptyTitle: {\n color: 'white',\n fontSize: 18,\n fontWeight: 'bold',\n marginTop: 16,\n },\n emptySubtitle: {\n color: '#94a3b8',\n fontSize: 14,\n marginTop: 8,\n },\n fab: {\n position: 'absolute',\n bottom: 24,\n right: 24,\n backgroundColor: '#e11d48',\n width: 56,\n height: 56,\n borderRadius: 28,\n justifyContent: 'center',\n alignItems: 'center',\n elevation: 8,\n shadowColor: '#000',\n shadowOffset: { width: 0, height: 4 },\n shadowOpacity: 0.3,\n shadowRadius: 4,\n },\n});\n\nexport default VoterListScreen;",
"content": "import React, { useState, useMemo } from 'react';\nimport {\n StyleSheet,\n View,\n Text,\n TextInput,\n FlatList,\n TouchableOpacity,\n StatusBar,\n SafeAreaView,\n RefreshControl,\n} from 'react-native';\nimport { Ionicons } from '@expo/vector-icons';\n\n// --- Types ---\ntype Sentiment = 'Todos' | 'A Favor' | 'Indeciso' | 'En Contra';\n\ninterface Voter {\n id: string;\n name: string;\n colonia: string;\n sentiment: Exclude<Sentiment, 'Todos'>;\n lastContact: string;\n}\n\n// --- Mock Data ---\nconst MOCK_VOTERS: Voter[] = [\n { id: '1', name: 'Juan Pérez García', colonia: 'Centro', sentiment: 'A Favor', lastContact: '2023-10-24' },\n { id: '2', name: 'María Elena Rodríguez', colonia: 'Polanco', sentiment: 'Indeciso', lastContact: '2023-10-22' },\n { id: '3', name: 'Ricardo Salinas', colonia: 'Roma Norte', sentiment: 'En Contra', lastContact: '2023-10-21' },\n { id: '4', name: 'Sofía Méndez', colonia: 'Condesa', sentiment: 'A Favor', lastContact: '2023-10-20' },\n { id: '5', name: 'Carlos Slim Domit', colonia: 'Lomas de Chapultepec', sentiment: 'Indeciso', lastContact: '2023-10-18' },\n];\n\nconst VoterListScreen = () => {\n const [search, setSearch] = useState('');\n const [activeFilter, setActiveFilter] = useState<Sentiment>('Todos');\n const [refreshing, setRefreshing] = useState(false);\n\n const onRefresh = React.useCallback(() => {\n setRefreshing(true);\n setTimeout(() => setRefreshing(false), 1500);\n }, []);\n\n const filteredVoters = useMemo(() => {\n return MOCK_VOTERS.filter((voter) => {\n const matchesSearch = voter.name.toLowerCase().includes(search.toLowerCase()) || \n voter.colonia.toLowerCase().includes(search.toLowerCase());\n const matchesFilter = activeFilter === 'Todos' || voter.sentiment === activeFilter;\n return matchesSearch && matchesFilter;\n });\n }, [search, activeFilter]);\n\n const getSentimentColor = (sentiment: Sentiment) => {\n switch (sentiment) {\n case 'A Favor': return '#22c55e';\n case 'Indeciso': return '#eab308';\n case 'En Contra': return '#ef4444';\n default: return '#94a3b8';\n }\n };\n\n const FilterChip = ({ label }: { label: Sentiment }) => (\n <TouchableOpacity\n style={[\n styles.chip,\n activeFilter === label && styles.chipActive\n ]}\n onPress={() => setActiveFilter(label)}\n >\n <Text style={[styles.chipText, activeFilter === label && styles.chipTextActive]}>\n {label}\n </Text>\n </TouchableOpacity>\n );\n\n const renderVoterCard = ({ item }: { item: Voter }) => (\n <View style={styles.card}>\n <View style={styles.cardHeader}>\n <View>\n <Text style={styles.voterName}>{item.name}</Text>\n <Text style={styles.voterColonia}>{item.colonia}</Text>\n </View>\n <View style={[styles.badge, { backgroundColor: getSentimentColor(item.sentiment) + '20', borderColor: getSentimentColor(item.sentiment) }]}>\n <Text style={[styles.badgeText, { color: getSentimentColor(item.sentiment) }]}>{item.sentiment}</Text>\n </View>\n </View>\n <View style={styles.cardFooter}>\n <Ionicons name=\"calendar-outline\" size={14} color=\"#94a3b8\" />\n <Text style={styles.lastContact}>Último contacto: {item.lastContact}</Text>\n </View>\n </View>\n );\n\n return (\n <SafeAreaView style={styles.container}>\n <StatusBar barStyle=\"light-content\" />\n \n {/* Search Header */}\n <View style={styles.header}>\n <View style={styles.searchContainer}>\n <Ionicons name=\"search\" size={20} color=\"#94a3b8\" style={styles.searchIcon} />\n <TextInput\n style={styles.searchInput}\n placeholder=\"Buscar votante...\"\n placeholderTextColor=\"#94a3b8\"\n value={search}\n onChangeText={setSearch}\n />\n </View>\n\n {/* Filter Chips */}\n <View style={styles.filterRow}>\n <FlatList\n horizontal\n showsHorizontalScrollIndicator={false}\n data={['Todos', 'A Favor', 'Indeciso', 'En Contra'] as Sentiment[]}\n renderItem={({ item }) => <FilterChip label={item} />}\n keyExtractor={(item) => item}\n contentContainerStyle={styles.filterList}\n />\n </View>\n </View>\n\n {/* List */}\n <FlatList\n data={filteredVoters}\n keyExtractor={(item) => item.id}\n renderItem={renderVoterCard}\n contentContainerStyle={styles.listContent}\n refreshControl={\n <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor=\"#e11d48\" />\n }\n ListEmptyComponent={\n <View style={styles.emptyContainer}>\n <Ionicons name=\"people-outline\" size={80} color=\"#1e293b\" />\n <Text style={styles.emptyTitle}>No se encontraron votantes</Text>\n <Text style={styles.emptySubtitle}>Intenta ajustar tus filtros de búsqueda</Text>\n </View>\n }\n />\n\n {/* FAB */}\n <TouchableOpacity style={styles.fab}>\n <Ionicons name=\"add\" size={30} color=\"white\" />\n </TouchableOpacity>\n </SafeAreaView>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: '#0f172a',\n },\n header: {\n paddingTop: 16,\n paddingBottom: 8,\n backgroundColor: '#0f172a',\n borderBottomWidth: 1,\n borderBottomColor: '#1e293b',\n },\n searchContainer: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: '#1e293b',\n marginHorizontal: 16,\n borderRadius: 12,\n paddingHorizontal: 12,\n height: 48,\n },\n searchIcon: {\n marginRight: 8,\n },\n searchInput: {\n flex: 1,\n color: 'white',\n fontSize: 16,\n },\n filterRow: {\n marginTop: 16,\n },\n filterList: {\n paddingHorizontal: 16,\n gap: 8,\n },\n chip: {\n paddingHorizontal: 16,\n paddingVertical: 8,\n borderRadius: 20,\n backgroundColor: '#1e293b',\n marginRight: 8,\n borderWidth: 1,\n borderColor: '#334155',\n },\n chipActive: {\n backgroundColor: '#e11d48',\n borderColor: '#e11d48',\n },\n chipText: {\n color: '#94a3b8',\n fontWeight: '600',\n fontSize: 14,\n },\n chipTextActive: {\n color: 'white',\n },\n listContent: {\n padding: 16,\n paddingBottom: 100,\n },\n card: {\n backgroundColor: '#1e293b',\n borderRadius: 16,\n padding: 16,\n marginBottom: 12,\n borderWidth: 1,\n borderColor: '#334155',\n },\n cardHeader: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'flex-start',\n },\n voterName: {\n color: 'white',\n fontSize: 18,\n fontWeight: 'bold',\n marginBottom: 4,\n },\n voterColonia: {\n color: '#94a3b8',\n fontSize: 14,\n },\n badge: {\n paddingHorizontal: 10,\n paddingVertical: 4,\n borderRadius: 8,\n borderWidth: 1,\n },\n badgeText: {\n fontSize: 12,\n fontWeight: 'bold',\n },\n cardFooter: {\n flexDirection: 'row',\n alignItems: 'center',\n marginTop: 12,\n paddingTop: 12,\n borderTopWidth: 1,\n borderTopColor: '#334155',\n },\n lastContact: {\n color: '#94a3b8',\n fontSize: 12,\n marginLeft: 6,\n },\n emptyContainer: {\n flex: 1,\n alignItems: 'center',\n justifyContent: 'center',\n marginTop: 100,\n },\n emptyTitle: {\n color: 'white',\n fontSize: 18,\n fontWeight: 'bold',\n marginTop: 16,\n },\n emptySubtitle: {\n color: '#94a3b8',\n fontSize: 14,\n marginTop: 8,\n },\n fab: {\n position: 'absolute',\n bottom: 24,\n right: 24,\n backgroundColor: '#e11d48',\n width: 56,\n height: 56,\n borderRadius: 28,\n justifyContent: 'center',\n alignItems: 'center',\n elevation: 8,\n shadowColor: '#000',\n shadowOffset: { width: 0, height: 4 },\n shadowOpacity: 0.3,\n shadowRadius: 4,\n },\n});\n\nexport default VoterListScreen;"
}Audit Trail (1)
22d agotask_createde2e_test_v2
Task ID: 40290e1a-961a-4e11-b0ba-e57697ffe547