
SectionList
SectionList Básico
typescript1import { SectionList, Text, View, StyleSheet } from "react-native"; 2 3interface Contact { 4 id: string; 5 name: string; 6 phone: string; 7} 8 9interface ContactSection { 10 title: string; 11 data: Contact[]; 12} 13 14const CONTACTS: ContactSection[] = [ 15 { 16 title: "A", 17 data: [ 18 { id: "1", name: "Ana García", phone: "123-456" }, 19 { id: "2", name: "Antonio López", phone: "123-457" }, 20 ], 21 }, 22 { 23 title: "B", 24 data: [{ id: "3", name: "Beatriz Ruiz", phone: "123-458" }], 25 }, 26 { 27 title: "C", 28 data: [ 29 { id: "4", name: "Carlos Martín", phone: "123-459" }, 30 { id: "5", name: "Carmen Pérez", phone: "123-460" }, 31 ], 32 }, 33]; 34 35function ContactsList() { 36 const renderContact = ({ item }: { item: Contact }) => ( 37 <View style={styles.contactItem}> 38 <Text style={styles.contactName}>{item.name}</Text> 39 <Text style={styles.contactPhone}>{item.phone}</Text> 40 </View> 41 ); 42 43 const renderSectionHeader = ({ section }: { section: ContactSection }) => ( 44 <View style={styles.sectionHeader}> 45 <Text style={styles.sectionHeaderText}>{section.title}</Text> 46 </View> 47 ); 48 49 return ( 50 <SectionList 51 sections={CONTACTS} 52 renderItem={renderContact} 53 renderSectionHeader={renderSectionHeader} 54 keyExtractor={(item) => item.id} 55 stickySectionHeadersEnabled={true} 56 /> 57 ); 58} 59 60const styles = StyleSheet.create({ 61 contactItem: { 62 padding: 16, 63 backgroundColor: "white", 64 borderBottomWidth: 1, 65 borderBottomColor: "#eee", 66 }, 67 contactName: { 68 fontSize: 16, 69 fontWeight: "600", 70 marginBottom: 4, 71 }, 72 contactPhone: { 73 fontSize: 14, 74 color: "#666", 75 }, 76 sectionHeader: { 77 backgroundColor: "#f5f5f5", 78 padding: 8, 79 paddingLeft: 16, 80 }, 81 sectionHeaderText: { 82 fontSize: 14, 83 fontWeight: "bold", 84 color: "#666", 85 }, 86});
Proyecto de la Semana: App de Contactos
typescript1import { useState } from "react"; 2import { 3 SectionList, 4 TextInput, 5 View, 6 Text, 7 TouchableOpacity, 8 StyleSheet, 9 SafeAreaView, 10 Alert, 11 Linking, 12} from "react-native"; 13 14interface Contact { 15 id: string; 16 name: string; 17 phone: string; 18 email: string; 19} 20 21interface Section { 22 title: string; 23 data: Contact[]; 24} 25 26export default function ContactsApp() { 27 const [contacts, setContacts] = useState<Contact[]>([ 28 { 29 id: "1", 30 name: "Ana García", 31 phone: "123-456-7890", 32 email: "ana@example.com", 33 }, 34 { 35 id: "2", 36 name: "Antonio López", 37 phone: "123-456-7891", 38 email: "antonio@example.com", 39 }, 40 { 41 id: "3", 42 name: "Beatriz Ruiz", 43 phone: "123-456-7892", 44 email: "beatriz@example.com", 45 }, 46 { 47 id: "4", 48 name: "Carlos Martín", 49 phone: "123-456-7893", 50 email: "carlos@example.com", 51 }, 52 ]); 53 const [searchQuery, setSearchQuery] = useState(""); 54 55 // Filtrar contactos 56 const filteredContacts = contacts.filter((contact) => 57 contact.name.toLowerCase().includes(searchQuery.toLowerCase()) 58 ); 59 60 // Agrupar por letra 61 const sections: Section[] = filteredContacts 62 .reduce((acc, contact) => { 63 const firstLetter = contact.name[0].toUpperCase(); 64 const section = acc.find((s) => s.title === firstLetter); 65 66 if (section) { 67 section.data.push(contact); 68 } else { 69 acc.push({ title: firstLetter, data: [contact] }); 70 } 71 72 return acc; 73 }, [] as Section[]) 74 .sort((a, b) => a.title.localeCompare(b.title)); 75 76 const handleCall = (phone: string) => { 77 Linking.openURL(`tel:${phone}`); 78 }; 79 80 const handleEmail = (email: string) => { 81 Linking.openURL(`mailto:${email}`); 82 }; 83 84 const renderContact = ({ item }: { item: Contact }) => ( 85 <View style={styles.contactCard}> 86 <View style={styles.avatar}> 87 <Text style={styles.avatarText}> 88 {item.name 89 .split(" ") 90 .map((n) => n[0]) 91 .join("") 92 .toUpperCase()} 93 </Text> 94 </View> 95 96 <View style={styles.contactInfo}> 97 <Text style={styles.contactName}>{item.name}</Text> 98 <Text style={styles.contactPhone}>{item.phone}</Text> 99 <Text style={styles.contactEmail}>{item.email}</Text> 100 </View> 101 102 <View style={styles.actions}> 103 <TouchableOpacity 104 style={styles.actionButton} 105 onPress={() => handleCall(item.phone)} 106 > 107 <Text style={styles.actionText}>📱</Text> 108 </TouchableOpacity> 109 <TouchableOpacity 110 style={styles.actionButton} 111 onPress={() => handleEmail(item.email)} 112 > 113 <Text style={styles.actionText}>✉️</Text> 114 </TouchableOpacity> 115 </View> 116 </View> 117 ); 118 119 const renderSectionHeader = ({ section }: { section: Section }) => ( 120 <View style={styles.sectionHeader}> 121 <Text style={styles.sectionHeaderText}>{section.title}</Text> 122 </View> 123 ); 124 125 return ( 126 <SafeAreaView style={styles.container}> 127 <View style={styles.header}> 128 <Text style={styles.headerTitle}>Contactos</Text> 129 <Text style={styles.headerSubtitle}> 130 {filteredContacts.length} contacto 131 {filteredContacts.length !== 1 ? "s" : ""} 132 </Text> 133 </View> 134 135 <View style={styles.searchContainer}> 136 <TextInput 137 style={styles.searchInput} 138 placeholder="Buscar contactos..." 139 value={searchQuery} 140 onChangeText={setSearchQuery} 141 /> 142 </View> 143 144 <SectionList 145 sections={sections} 146 renderItem={renderContact} 147 renderSectionHeader={renderSectionHeader} 148 keyExtractor={(item) => item.id} 149 stickySectionHeadersEnabled={true} 150 ListEmptyComponent={ 151 <View style={styles.emptyContainer}> 152 <Text style={styles.emptyText}>No se encontraron contactos</Text> 153 </View> 154 } 155 /> 156 </SafeAreaView> 157 ); 158} 159 160const styles = StyleSheet.create({ 161 container: { 162 flex: 1, 163 backgroundColor: "#f5f5f5", 164 }, 165 header: { 166 backgroundColor: "white", 167 padding: 20, 168 borderBottomWidth: 1, 169 borderBottomColor: "#eee", 170 }, 171 headerTitle: { 172 fontSize: 32, 173 fontWeight: "bold", 174 marginBottom: 4, 175 }, 176 headerSubtitle: { 177 fontSize: 14, 178 color: "#666", 179 }, 180 searchContainer: { 181 padding: 16, 182 backgroundColor: "white", 183 borderBottomWidth: 1, 184 borderBottomColor: "#eee", 185 }, 186 searchInput: { 187 backgroundColor: "#f0f0f0", 188 padding: 12, 189 borderRadius: 8, 190 fontSize: 16, 191 }, 192 contactCard: { 193 flexDirection: "row", 194 alignItems: "center", 195 backgroundColor: "white", 196 padding: 16, 197 borderBottomWidth: 1, 198 borderBottomColor: "#eee", 199 }, 200 avatar: { 201 width: 50, 202 height: 50, 203 borderRadius: 25, 204 backgroundColor: "#007AFF", 205 justifyContent: "center", 206 alignItems: "center", 207 }, 208 avatarText: { 209 color: "white", 210 fontSize: 18, 211 fontWeight: "bold", 212 }, 213 contactInfo: { 214 flex: 1, 215 marginLeft: 12, 216 }, 217 contactName: { 218 fontSize: 16, 219 fontWeight: "600", 220 marginBottom: 2, 221 }, 222 contactPhone: { 223 fontSize: 14, 224 color: "#666", 225 marginBottom: 2, 226 }, 227 contactEmail: { 228 fontSize: 12, 229 color: "#999", 230 }, 231 actions: { 232 flexDirection: "row", 233 gap: 8, 234 }, 235 actionButton: { 236 width: 40, 237 height: 40, 238 justifyContent: "center", 239 alignItems: "center", 240 backgroundColor: "#f0f0f0", 241 borderRadius: 20, 242 }, 243 actionText: { 244 fontSize: 18, 245 }, 246 sectionHeader: { 247 backgroundColor: "#f5f5f5", 248 padding: 8, 249 paddingLeft: 16, 250 }, 251 sectionHeaderText: { 252 fontSize: 14, 253 fontWeight: "bold", 254 color: "#666", 255 }, 256 emptyContainer: { 257 padding: 40, 258 alignItems: "center", 259 }, 260 emptyText: { 261 fontSize: 16, 262 color: "#999", 263 }, 264});