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