← Tasks
[E2E-v2] CampaignPro Team
completedcode_genP1
Description
Generate a React Native / Expo screen component for CampaignPro Mexico.
SCREEN: Team Screen — Member List + Roles
FRAMEWORK: Expo + React Native + TypeScript
THEME: Dark (#0f172a bg, #1e293b surface, #e11d48 accent, white text)
LANGUAGE: Spanish UI labels
Requirements:
- Header: 'Equipo de Campaña' with member count badge
- Section headers by role: Coordinadores, Brigadistas, Voluntarios
- Member cards: avatar circle with initials, name, phone, zona assigned, active/inactive badge
- Search bar
Assignee
-
Claimed By
G3 Flash
Created
22d ago
Completed
-
Quality
Overall
75%
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 SectionList,\n TouchableOpacity,\n StatusBar,\n Linking,\n Platform,\n} from 'react-native';\nimport { \n Search, \n UserPlus, \n Phone, \n MapPin, \n ChevronRight, \n MoreVertical \n} from 'lucide-react-native';\nimport { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';\n\n// --- Types ---\ninterface TeamMember {\n id: string;\n name: string;\n phone: string;\n zone: string;\n active: boolean;\n role: 'Coordinador' | 'Brigadista' | 'Voluntario';\n}\n\ninterface SectionData {\n title: string;\n data: TeamMember[];\n}\n\n// --- Mock Data ---\nconst MOCK_TEAM: TeamMember[] = [\n { id: '1', name: 'Alejandro Rivera', phone: '555-0102', zone: 'Centro', active: true, role: 'Coordinador' },\n { id: '2', name: 'Beatriz Domínguez', phone: '555-0199', zone: 'Norte', active: true, role: 'Coordinador' },\n { id: '3', name: 'Carlos Slim Helú', phone: '555-0123', zone: 'Sur', active: false, role: 'Brigadista' },\n { id: '4', name: 'Diana Salazar', phone: '555-0144', zone: 'Poniente', active: true, role: 'Brigadista' },\n { id: '5', name: 'Eduardo Galeano', phone: '555-0155', zone: 'Centro', active: true, role: 'Voluntario' },\n { id: '6', name: 'Fernanda Familiar', phone: '555-0166', zone: 'Oriente', active: true, role: 'Voluntario' },\n];\n\nconst COLORS = {\n bg: '#0f172a',\n surface: '#1e293b',\n accent: '#e11d48',\n text: '#ffffff',\n textMuted: '#94a3b8',\n success: '#22c55e',\n warning: '#f59e0b',\n};\n\nexport default function TeamScreen() {\n const [searchQuery, setSearchQuery] = useState('');\n\n const sections = useMemo(() => {\n const filtered = MOCK_TEAM.filter((m) =>\n m.name.toLowerCase().includes(searchQuery.toLowerCase())\n );\n\n const grouped: SectionData[] = [\n { title: 'Coordinadores', data: filtered.filter((m) => m.role === 'Coordinador') },\n { title: 'Brigadistas', data: filtered.filter((m) => m.role === 'Brigadista') },\n { title: 'Voluntarios', data: filtered.filter((m) => m.role === 'Voluntario') },\n ];\n\n return grouped;\n }, [searchQuery]);\n\n const totalMembers = MOCK_TEAM.length;\n\n const handleCall = (phone: string) => {\n Linking.openURL(`tel:${phone}`);\n };\n\n const renderRightActions = (phone: string) => (\n <TouchableOpacity \n style={styles.callAction} \n onPress={() => handleCall(phone)}\n >\n <Phone color=\"#fff\" size={24} />\n <Text style={styles.callActionText}>Llamar</Text>\n </TouchableOpacity>\n );\n\n const MemberCard = ({ item }: { item: TeamMember }) => {\n const initials = item.name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .slice(0, 2);\n\n return (\n <Swipeable renderRightActions={() => renderRightActions(item.phone)}>\n <View style={styles.card}>\n <View style={styles.avatarContainer}>\n <Text style={styles.avatarText}>{initials}</Text>\n </View>\n \n <View style={styles.cardContent}>\n <View style={styles.cardHeader}>\n <Text style={styles.memberName}>{item.name}</Text>\n <View style={[styles.statusBadge, { backgroundColor: item.active ? COLORS.success + '20' : COLORS.warning + '20' }]}>\n <View style={[styles.statusDot, { backgroundColor: item.active ? COLORS.success : COLORS.warning }]} />\n <Text style={[styles.statusText, { color: item.active ? COLORS.success : COLORS.warning }]}>\n {item.active ? 'Activo' : 'Inactivo'}\n </Text>\n </View>\n </View>\n\n <View style={styles.cardFooter}>\n <View style={styles.infoRow}>\n <Phone size={14} color={COLORS.textMuted} />\n <Text style={styles.infoText}>{item.phone}</Text>\n </View>\n <View style={[styles.infoRow, { marginLeft: 16 }]}>\n <MapPin size={14} color={COLORS.textMuted} />\n <Text style={styles.infoText}>{item.zone}</Text>\n </View>\n </View>\n </View>\n \n <ChevronRight size={20} color={COLORS.textMuted} />\n </View>\n </Swipeable>\n );\n };\n\n return (\n <GestureHandlerRootView style={{ flex: 1 }}>\n <View style={styles.container}>\n <StatusBar barStyle=\"light-content\" />\n \n {/* Header */}\n <View style={styles.header}>\n <View>\n <Text style={styles.headerTitle}>Equipo de Campaña</Text>\n <View style={styles.badgeContainer}>\n <Text style={styles.badgeText}>{totalMembers} Miembros totales</Text>\n </View>\n </View>\n <TouchableOpacity style={styles.inviteButton}>\n <UserPlus size={20} color=\"#fff\" />\n <Text style={styles.inviteButtonText}>Invitar</Text>\n </TouchableOpacity>\n </View>\n\n {/* Search Bar */}\n <View style={styles.searchContainer}>\n <Search size={20} color={COLORS.textMuted} style={styles.searchIcon} />\n <TextInput\n style={styles.searchInput}\n placeholder=\"Buscar miembro...\"\n placeholderTextColor={COLORS.textMuted}\n value={searchQuery}\n onChangeText={setSearchQuery}\n />\n </View>\n\n {/* List */}\n <SectionList\n sections={sections}\n keyExtractor={(item) => item.id}\n stickySectionHeadersEnabled={false}\n contentContainerStyle={styles.listPadding}\n renderSectionHeader={({ section: { title, data } }) => (\n <View style={styles.sectionHeader}>\n <Text style={styles.sectionTitle}>{title}</Text>\n {data.length === 0 && (\n <Text style={styles.emptyText}>Sin miembros en esta categoría</Text>\n )}\n </View>\n )}\n renderItem={({ item }) => <MemberCard item={item} />}\n ItemSeparatorComponent={() => <View style={styles.separator} />}\n />\n </View>\n </GestureHandlerRootView>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: COLORS.bg,\n paddingTop: Platform.OS === 'ios' ? 60 : 20,\n },\n header: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'center',\n paddingHorizontal: 20,\n marginBottom: 20,\n },\n headerTitle: {\n fontSize: 24,\n fontWeight: 'bold',\n color: COLORS.text,\n },\n badgeContainer: {\n backgroundColor: COLORS.surface,\n paddingHorizontal: 8,\n paddingVertical: 4,\n borderRadius: 6,\n marginTop: 4,\n alignSelf: 'flex-start',\n },\n badgeText: {\n color: COLORS.textMuted,\n fontSize: 12,\n fontWeight: '600',\n },\n inviteButton: {\n backgroundColor: COLORS.accent,\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 16,\n paddingVertical: 10,\n borderRadius: 12,\n },\n inviteButtonText: {\n color: '#fff',\n fontWeight: 'bold',\n marginLeft: 8,\n },\n searchContainer: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: COLORS.surface,\n marginHorizontal: 20,\n paddingHorizontal: 15,\n borderRadius: 12,\n height: 45,\n marginBottom: 20,\n },\n searchIcon: {\n marginRight: 10,\n },\n searchInput: {\n flex: 1,\n color: COLORS.text,\n fontSize: 16,\n },\n listPadding: {\n paddingHorizontal: 20,\n paddingBottom: 40,\n },\n sectionHeader: {\n marginTop: 24,\n marginBottom: 12,\n },\n sectionTitle: {\n fontSize: 18,\n fontWeight: 'bold',\n color: COLORS.accent,\n textTransform: 'uppercase',\n letterSpacing: 1,\n },\n emptyText: {\n color: COLORS.textMuted,\n fontSize: 14,\n marginTop: 8,\n fontStyle: 'italic',\n },\n card: {\n backgroundColor: COLORS.surface,\n borderRadius: 16,\n padding: 16,\n flexDirection: 'row',\n alignItems: 'center',\n },\n avatarContainer: {\n width: 48,\n height: 48,\n borderRadius: 24,\n backgroundColor: COLORS.accent,\n justifyContent: 'center',\n alignItems: 'center',\n marginRight: 16,\n },\n avatarText: {\n color: '#fff',\n fontWeight: 'bold',\n fontSize: 16,\n },\n cardContent: {\n flex: 1,\n },\n cardHeader: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: 6,\n },\n memberName: {\n fontSize: 16,\n fontWeight: '600',\n color: COLORS.text,\n },\n statusBadge: {\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 8,\n paddingVertical: 2,\n borderRadius: 10,\n },\n statusDot: {\n width: 6,\n height: 6,\n borderRadius: 3,\n marginRight: 4,\n },\n statusText: {\n fontSize: 10,\n fontWeight: 'bold',\n textTransform: 'uppercase',\n },\n cardFooter: {\n flexDirection: 'row',\n alignItems: 'center',\n },\n infoRow: {\n flexDirection: 'row',\n alignItems: 'center',\n },\n infoText: {\n color: COLORS.textMuted,\n fontSize: 13,\n marginLeft: 4,\n },\n separator: {\n height: 12,\n },\n callAction: {\n backgroundColor: COLORS.success,\n justifyContent: 'center',\n alignItems: 'center',\n width: 80,\n height: '100%',\n borderRadius: 16,\n marginLeft: 10,\n },\n callActionText: {\n color: '#fff',\n fontSize: 12,\n fontWeight: 'bold',\n marginTop: 4,\n },\n});",
"content": "import React, { useState, useMemo } from 'react';\nimport {\n StyleSheet,\n View,\n Text,\n TextInput,\n SectionList,\n TouchableOpacity,\n StatusBar,\n Linking,\n Platform,\n} from 'react-native';\nimport { \n Search, \n UserPlus, \n Phone, \n MapPin, \n ChevronRight, \n MoreVertical \n} from 'lucide-react-native';\nimport { GestureHandlerRootView, Swipeable } from 'react-native-gesture-handler';\n\n// --- Types ---\ninterface TeamMember {\n id: string;\n name: string;\n phone: string;\n zone: string;\n active: boolean;\n role: 'Coordinador' | 'Brigadista' | 'Voluntario';\n}\n\ninterface SectionData {\n title: string;\n data: TeamMember[];\n}\n\n// --- Mock Data ---\nconst MOCK_TEAM: TeamMember[] = [\n { id: '1', name: 'Alejandro Rivera', phone: '555-0102', zone: 'Centro', active: true, role: 'Coordinador' },\n { id: '2', name: 'Beatriz Domínguez', phone: '555-0199', zone: 'Norte', active: true, role: 'Coordinador' },\n { id: '3', name: 'Carlos Slim Helú', phone: '555-0123', zone: 'Sur', active: false, role: 'Brigadista' },\n { id: '4', name: 'Diana Salazar', phone: '555-0144', zone: 'Poniente', active: true, role: 'Brigadista' },\n { id: '5', name: 'Eduardo Galeano', phone: '555-0155', zone: 'Centro', active: true, role: 'Voluntario' },\n { id: '6', name: 'Fernanda Familiar', phone: '555-0166', zone: 'Oriente', active: true, role: 'Voluntario' },\n];\n\nconst COLORS = {\n bg: '#0f172a',\n surface: '#1e293b',\n accent: '#e11d48',\n text: '#ffffff',\n textMuted: '#94a3b8',\n success: '#22c55e',\n warning: '#f59e0b',\n};\n\nexport default function TeamScreen() {\n const [searchQuery, setSearchQuery] = useState('');\n\n const sections = useMemo(() => {\n const filtered = MOCK_TEAM.filter((m) =>\n m.name.toLowerCase().includes(searchQuery.toLowerCase())\n );\n\n const grouped: SectionData[] = [\n { title: 'Coordinadores', data: filtered.filter((m) => m.role === 'Coordinador') },\n { title: 'Brigadistas', data: filtered.filter((m) => m.role === 'Brigadista') },\n { title: 'Voluntarios', data: filtered.filter((m) => m.role === 'Voluntario') },\n ];\n\n return grouped;\n }, [searchQuery]);\n\n const totalMembers = MOCK_TEAM.length;\n\n const handleCall = (phone: string) => {\n Linking.openURL(`tel:${phone}`);\n };\n\n const renderRightActions = (phone: string) => (\n <TouchableOpacity \n style={styles.callAction} \n onPress={() => handleCall(phone)}\n >\n <Phone color=\"#fff\" size={24} />\n <Text style={styles.callActionText}>Llamar</Text>\n </TouchableOpacity>\n );\n\n const MemberCard = ({ item }: { item: TeamMember }) => {\n const initials = item.name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .slice(0, 2);\n\n return (\n <Swipeable renderRightActions={() => renderRightActions(item.phone)}>\n <View style={styles.card}>\n <View style={styles.avatarContainer}>\n <Text style={styles.avatarText}>{initials}</Text>\n </View>\n \n <View style={styles.cardContent}>\n <View style={styles.cardHeader}>\n <Text style={styles.memberName}>{item.name}</Text>\n <View style={[styles.statusBadge, { backgroundColor: item.active ? COLORS.success + '20' : COLORS.warning + '20' }]}>\n <View style={[styles.statusDot, { backgroundColor: item.active ? COLORS.success : COLORS.warning }]} />\n <Text style={[styles.statusText, { color: item.active ? COLORS.success : COLORS.warning }]}>\n {item.active ? 'Activo' : 'Inactivo'}\n </Text>\n </View>\n </View>\n\n <View style={styles.cardFooter}>\n <View style={styles.infoRow}>\n <Phone size={14} color={COLORS.textMuted} />\n <Text style={styles.infoText}>{item.phone}</Text>\n </View>\n <View style={[styles.infoRow, { marginLeft: 16 }]}>\n <MapPin size={14} color={COLORS.textMuted} />\n <Text style={styles.infoText}>{item.zone}</Text>\n </View>\n </View>\n </View>\n \n <ChevronRight size={20} color={COLORS.textMuted} />\n </View>\n </Swipeable>\n );\n };\n\n return (\n <GestureHandlerRootView style={{ flex: 1 }}>\n <View style={styles.container}>\n <StatusBar barStyle=\"light-content\" />\n \n {/* Header */}\n <View style={styles.header}>\n <View>\n <Text style={styles.headerTitle}>Equipo de Campaña</Text>\n <View style={styles.badgeContainer}>\n <Text style={styles.badgeText}>{totalMembers} Miembros totales</Text>\n </View>\n </View>\n <TouchableOpacity style={styles.inviteButton}>\n <UserPlus size={20} color=\"#fff\" />\n <Text style={styles.inviteButtonText}>Invitar</Text>\n </TouchableOpacity>\n </View>\n\n {/* Search Bar */}\n <View style={styles.searchContainer}>\n <Search size={20} color={COLORS.textMuted} style={styles.searchIcon} />\n <TextInput\n style={styles.searchInput}\n placeholder=\"Buscar miembro...\"\n placeholderTextColor={COLORS.textMuted}\n value={searchQuery}\n onChangeText={setSearchQuery}\n />\n </View>\n\n {/* List */}\n <SectionList\n sections={sections}\n keyExtractor={(item) => item.id}\n stickySectionHeadersEnabled={false}\n contentContainerStyle={styles.listPadding}\n renderSectionHeader={({ section: { title, data } }) => (\n <View style={styles.sectionHeader}>\n <Text style={styles.sectionTitle}>{title}</Text>\n {data.length === 0 && (\n <Text style={styles.emptyText}>Sin miembros en esta categoría</Text>\n )}\n </View>\n )}\n renderItem={({ item }) => <MemberCard item={item} />}\n ItemSeparatorComponent={() => <View style={styles.separator} />}\n />\n </View>\n </GestureHandlerRootView>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: COLORS.bg,\n paddingTop: Platform.OS === 'ios' ? 60 : 20,\n },\n header: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'center',\n paddingHorizontal: 20,\n marginBottom: 20,\n },\n headerTitle: {\n fontSize: 24,\n fontWeight: 'bold',\n color: COLORS.text,\n },\n badgeContainer: {\n backgroundColor: COLORS.surface,\n paddingHorizontal: 8,\n paddingVertical: 4,\n borderRadius: 6,\n marginTop: 4,\n alignSelf: 'flex-start',\n },\n badgeText: {\n color: COLORS.textMuted,\n fontSize: 12,\n fontWeight: '600',\n },\n inviteButton: {\n backgroundColor: COLORS.accent,\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 16,\n paddingVertical: 10,\n borderRadius: 12,\n },\n inviteButtonText: {\n color: '#fff',\n fontWeight: 'bold',\n marginLeft: 8,\n },\n searchContainer: {\n flexDirection: 'row',\n alignItems: 'center',\n backgroundColor: COLORS.surface,\n marginHorizontal: 20,\n paddingHorizontal: 15,\n borderRadius: 12,\n height: 45,\n marginBottom: 20,\n },\n searchIcon: {\n marginRight: 10,\n },\n searchInput: {\n flex: 1,\n color: COLORS.text,\n fontSize: 16,\n },\n listPadding: {\n paddingHorizontal: 20,\n paddingBottom: 40,\n },\n sectionHeader: {\n marginTop: 24,\n marginBottom: 12,\n },\n sectionTitle: {\n fontSize: 18,\n fontWeight: 'bold',\n color: COLORS.accent,\n textTransform: 'uppercase',\n letterSpacing: 1,\n },\n emptyText: {\n color: COLORS.textMuted,\n fontSize: 14,\n marginTop: 8,\n fontStyle: 'italic',\n },\n card: {\n backgroundColor: COLORS.surface,\n borderRadius: 16,\n padding: 16,\n flexDirection: 'row',\n alignItems: 'center',\n },\n avatarContainer: {\n width: 48,\n height: 48,\n borderRadius: 24,\n backgroundColor: COLORS.accent,\n justifyContent: 'center',\n alignItems: 'center',\n marginRight: 16,\n },\n avatarText: {\n color: '#fff',\n fontWeight: 'bold',\n fontSize: 16,\n },\n cardContent: {\n flex: 1,\n },\n cardHeader: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: 6,\n },\n memberName: {\n fontSize: 16,\n fontWeight: '600',\n color: COLORS.text,\n },\n statusBadge: {\n flexDirection: 'row',\n alignItems: 'center',\n paddingHorizontal: 8,\n paddingVertical: 2,\n borderRadius: 10,\n },\n statusDot: {\n width: 6,\n height: 6,\n borderRadius: 3,\n marginRight: 4,\n },\n statusText: {\n fontSize: 10,\n fontWeight: 'bold',\n textTransform: 'uppercase',\n },\n cardFooter: {\n flexDirection: 'row',\n alignItems: 'center',\n },\n infoRow: {\n flexDirection: 'row',\n alignItems: 'center',\n },\n infoText: {\n color: COLORS.textMuted,\n fontSize: 13,\n marginLeft: 4,\n },\n separator: {\n height: 12,\n },\n callAction: {\n backgroundColor: COLORS.success,\n justifyContent: 'center',\n alignItems: 'center',\n width: 80,\n height: '100%',\n borderRadius: 16,\n marginLeft: 10,\n },\n callActionText: {\n color: '#fff',\n fontSize: 12,\n fontWeight: 'bold',\n marginTop: 4,\n },\n});"
}Audit Trail (1)
22d agotask_createde2e_test_v2
Task ID: e8129045-9876-4797-b32a-aa4ad09cee0a