@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
33import { Alert , Pressable , ScrollView , StyleSheet , View } from 'react-native' ;
44import { useSafeAreaInsets } from 'react-native-safe-area-context' ;
55import { MaterialIcons } from '@expo/vector-icons' ;
6+ import * as Haptics from 'expo-haptics' ;
67import { HudCard } from '../src/components/HudCard' ;
78import { HudText } from '../src/components/HudText' ;
89import { MargiButton } from '../src/components/MargiButton' ;
@@ -18,32 +19,137 @@ import {
1819} from '../src/lib/storage' ;
1920import { tokens } from '../src/theme/tokens' ;
2021
21- /** Stitch emergency contacts card — Primary Contact (ICE 1) with name, relationship, phone. */
22+ const MAX_ICE = 5 ; // ICE 1 (primary) + ICE 2–5 (extras)
23+
24+ const ICE_LABELS : Record < number , string > = {
25+ 1 : 'Primary Contact (ICE 1)' ,
26+ 2 : 'ICE 2 — Backup Contact' ,
27+ 3 : 'ICE 3 — Tertiary Contact' ,
28+ 4 : 'ICE 4' ,
29+ 5 : 'ICE 5' ,
30+ } ;
31+
32+ /** Single ICE contact form card */
33+ function IceContactCard ( {
34+ index,
35+ contact,
36+ onChange,
37+ onDelete,
38+ canDelete,
39+ } : {
40+ index : number ; // 1-based ICE number
41+ contact : EmergencyContact ;
42+ onChange : ( c : EmergencyContact ) => void ;
43+ onDelete : ( ) => void ;
44+ canDelete : boolean ;
45+ } ) {
46+ return (
47+ < HudCard style = { styles . card } >
48+ < View style = { styles . cardHead } >
49+ { /* ICE badge */ }
50+ < View style = { styles . iceBadge } >
51+ < HudText variant = "mono" style = { styles . iceBadgeText } >
52+ ICE { index }
53+ </ HudText >
54+ </ View >
55+ < HudText variant = "bodyMd" style = { styles . iceTitle } >
56+ { ICE_LABELS [ index ] ?? `ICE ${ index } ` }
57+ </ HudText >
58+ { canDelete && (
59+ < Pressable
60+ onPress = { ( ) => {
61+ Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) . catch ( ( ) => undefined ) ;
62+ onDelete ( ) ;
63+ } }
64+ style = { styles . deleteBtn }
65+ accessibilityLabel = { `Remove ICE ${ index } ` }
66+ >
67+ < MaterialIcons name = "delete-outline" size = { 22 } color = { tokens . outline } />
68+ </ Pressable >
69+ ) }
70+ </ View >
71+
72+ < HudText variant = "mono" style = { styles . label } > Full Name</ HudText >
73+ < MargiInput
74+ value = { contact . fullName }
75+ onChangeText = { ( t ) => onChange ( { ...contact , fullName : t } ) }
76+ placeholder = "e.g. Sarah Jenkins"
77+ />
78+
79+ < HudText variant = "mono" style = { styles . label } > Relationship</ HudText >
80+ < MargiInput
81+ value = { contact . relationship }
82+ onChangeText = { ( t ) => onChange ( { ...contact , relationship : t } ) }
83+ placeholder = "e.g. Spouse, Parent, Sibling"
84+ />
85+
86+ < HudText variant = "mono" style = { styles . label } > Phone Number</ HudText >
87+ < MargiInput
88+ value = { contact . phone }
89+ onChangeText = { ( t ) => onChange ( { ...contact , phone : t } ) }
90+ placeholder = "+91 98765 43210"
91+ keyboardType = "phone-pad"
92+ />
93+ </ HudCard >
94+ ) ;
95+ }
96+
97+ /** Emergency Contacts screen — supports ICE 1 through ICE 5 */
2298export default function EmergencyContactsScreen ( ) {
2399 const insets = useSafeAreaInsets ( ) ;
24100 const { profile, updateMedical } = useApp ( ) ;
25- const [ contact , setContact ] = useState < EmergencyContact > ( ( ) => {
101+ const userEdited = useRef ( false ) ;
102+
103+ // ICE 1 — primary contact
104+ const [ primary , setPrimary ] = useState < EmergencyContact > ( ( ) => {
26105 const m = normalizeMedical ( { ...defaultMedical ( ) , ...profile . medical } ) ;
27106 return m . primaryContact ?? defaultEmergencyContact ( ) ;
28107 } ) ;
29- const userEdited = useRef ( false ) ;
108+
109+ // ICE 2–5 — extra contacts (stored as array, max 4)
110+ const [ extras , setExtras ] = useState < EmergencyContact [ ] > ( ( ) => {
111+ const m = normalizeMedical ( { ...defaultMedical ( ) , ...profile . medical } ) ;
112+ return m . iceContacts ?? [ ] ;
113+ } ) ;
30114
31115 useEffect ( ( ) => {
32116 if ( userEdited . current || ! profile . medical ) return ;
33117 const m = normalizeMedical ( profile . medical ) ;
34- setContact ( m . primaryContact ?? defaultEmergencyContact ( ) ) ;
118+ setPrimary ( m . primaryContact ?? defaultEmergencyContact ( ) ) ;
119+ setExtras ( m . iceContacts ?? [ ] ) ;
35120 } , [ profile . medical ] ) ;
36121
122+ const totalContacts = 1 + extras . length ; // ICE 1 + extras
123+ const canAddMore = totalContacts < MAX_ICE ;
124+
125+ const addContact = ( ) => {
126+ if ( ! canAddMore ) return ;
127+ Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium ) . catch ( ( ) => undefined ) ;
128+ userEdited . current = true ;
129+ setExtras ( ( prev ) => [ ...prev , defaultEmergencyContact ( ) ] ) ;
130+ } ;
131+
132+ const updateExtra = ( i : number , c : EmergencyContact ) => {
133+ userEdited . current = true ;
134+ setExtras ( ( prev ) => prev . map ( ( x , idx ) => ( idx === i ? c : x ) ) ) ;
135+ } ;
136+
137+ const deleteExtra = ( i : number ) => {
138+ userEdited . current = true ;
139+ setExtras ( ( prev ) => prev . filter ( ( _ , idx ) => idx !== i ) ) ;
140+ } ;
141+
37142 const save = async ( ) => {
38- if ( ! contact . fullName . trim ( ) || ! contact . phone . trim ( ) ) {
39- Alert . alert ( 'Required fields' , 'Enter the full name and phone number for your primary contact.' ) ;
143+ if ( ! primary . fullName . trim ( ) || ! primary . phone . trim ( ) ) {
144+ Alert . alert ( 'Required fields' , 'Enter the full name and phone number for your primary contact (ICE 1) .' ) ;
40145 return ;
41146 }
42147 const m = normalizeMedical ( {
43148 ...defaultMedical ( ) ,
44149 ...profile . medical ,
45- primaryContact : contact ,
46- emergencyContact : formatIceLine ( contact ) ,
150+ primaryContact : primary ,
151+ emergencyContact : formatIceLine ( primary ) ,
152+ iceContacts : extras ,
47153 } ) ;
48154 userEdited . current = true ;
49155 await updateMedical ( m ) ;
@@ -57,73 +163,72 @@ export default function EmergencyContactsScreen() {
57163 contentContainerStyle = { [ styles . scroll , { paddingBottom : insets . bottom + 32 } ] }
58164 keyboardShouldPersistTaps = "handled"
59165 >
166+ { /* Section header */ }
60167 < View style = { styles . headerRow } >
61168 < MaterialIcons name = "contact-emergency" size = { 24 } color = { tokens . primary } />
62169 < HudText variant = "headlineMd" style = { styles . title } >
63170 Emergency Contacts
64171 </ HudText >
65- < Pressable
66- onPress = { ( ) =>
67- setContact ( {
68- fullName : '' ,
69- relationship : '' ,
70- phone : '' ,
71- } )
72- }
73- style = { styles . addBtn }
74- >
75- < MaterialIcons name = "add" size = { 18 } color = { tokens . primary } />
76- < HudText variant = "mono" style = { styles . addLabel } >
77- Add
172+ { /* Contact count badge */ }
173+ < View style = { styles . countBadge } >
174+ < HudText variant = "mono" style = { styles . countText } >
175+ { totalContacts } /{ MAX_ICE }
78176 </ HudText >
79- </ Pressable >
177+ </ View >
80178 </ View >
81179
82- < HudCard >
83- < View style = { styles . cardHead } >
84- < HudText variant = "bodyMd" style = { styles . iceTitle } >
85- Primary Contact (ICE 1)
86- </ HudText >
87- < Pressable onPress = { ( ) => setContact ( defaultEmergencyContact ( ) ) } >
88- < MaterialIcons name = "delete-outline" size = { 22 } color = { tokens . outline } />
89- </ Pressable >
90- </ View >
91- < HudText variant = "mono" style = { styles . label } >
92- Full Name
93- </ HudText >
94- < MargiInput
95- value = { contact . fullName }
96- onChangeText = { ( t ) => {
97- userEdited . current = true ;
98- setContact ( ( c ) => ( { ...c , fullName : t } ) ) ;
99- } }
100- placeholder = "Sarah Jenkins"
101- />
102- < HudText variant = "mono" style = { styles . label } >
103- Relationship
104- </ HudText >
105- < MargiInput
106- value = { contact . relationship }
107- onChangeText = { ( t ) => {
108- userEdited . current = true ;
109- setContact ( ( c ) => ( { ...c , relationship : t } ) ) ;
110- } }
111- placeholder = "Spouse"
112- />
113- < HudText variant = "mono" style = { styles . label } >
114- Phone Number
115- </ HudText >
116- < MargiInput
117- value = { contact . phone }
118- onChangeText = { ( t ) => {
119- userEdited . current = true ;
120- setContact ( ( c ) => ( { ...c , phone : t } ) ) ;
121- } }
122- placeholder = "+91 98765 43210"
123- keyboardType = "phone-pad"
180+ < HudText variant = "bodySm" style = { styles . subtitle } >
181+ In Case of Emergency contacts are notified automatically when SOS is triggered.
182+ </ HudText >
183+
184+ { /* ICE 1 — always shown, cannot be deleted */ }
185+ < IceContactCard
186+ index = { 1 }
187+ contact = { primary }
188+ onChange = { ( c ) => {
189+ userEdited . current = true ;
190+ setPrimary ( c ) ;
191+ } }
192+ onDelete = { ( ) => { /* primary cannot be deleted */ } }
193+ canDelete = { false }
194+ />
195+
196+ { /* ICE 2–5 — dynamically added extras */ }
197+ { extras . map ( ( c , i ) => (
198+ < IceContactCard
199+ key = { i }
200+ index = { i + 2 }
201+ contact = { c }
202+ onChange = { ( updated ) => updateExtra ( i , updated ) }
203+ onDelete = { ( ) => deleteExtra ( i ) }
204+ canDelete
124205 />
125- </ HudCard >
206+ ) ) }
126207
208+ { /* Add contact button — hidden when at ICE 5 */ }
209+ { canAddMore && (
210+ < Pressable
211+ onPress = { addContact }
212+ style = { ( { pressed } ) => [ styles . addBtn , pressed && styles . addBtnPressed ] }
213+ accessibilityRole = "button"
214+ accessibilityLabel = "Add emergency contact"
215+ >
216+ < View style = { styles . addIconCircle } >
217+ < MaterialIcons name = "add" size = { 22 } color = { tokens . onPrimary } />
218+ </ View >
219+ < View style = { styles . addTextBlock } >
220+ < HudText variant = "mono" style = { styles . addLabel } >
221+ Add ICE { totalContacts + 1 }
222+ </ HudText >
223+ < HudText variant = "bodySm" style = { styles . addSub } >
224+ { MAX_ICE - totalContacts } slot{ MAX_ICE - totalContacts !== 1 ? 's' : '' } remaining
225+ </ HudText >
226+ </ View >
227+ < MaterialIcons name = "chevron-right" size = { 20 } color = { tokens . onSurfaceVariant } />
228+ </ Pressable >
229+ ) }
230+
231+ { /* Actions */ }
127232 < View style = { styles . actions } >
128233 < MargiButton label = "Cancel" variant = "ghost" onPress = { ( ) => router . back ( ) } style = { { flex : 1 } } />
129234 < MargiButton label = "Save & continue" onPress = { save } large style = { { flex : 1 } } />
@@ -136,17 +241,97 @@ export default function EmergencyContactsScreen() {
136241const styles = StyleSheet . create ( {
137242 root : { flex : 1 , backgroundColor : tokens . background } ,
138243 scroll : { padding : 20 , gap : 14 } ,
244+
139245 headerRow : { flexDirection : 'row' , alignItems : 'center' , gap : 10 } ,
140246 title : { flex : 1 , color : tokens . primary , fontFamily : 'PublicSans_700Bold' } ,
141- addBtn : { flexDirection : 'row' , alignItems : 'center' , gap : 4 , padding : 6 } ,
142- addLabel : { color : tokens . primary , fontSize : 12 } ,
247+
248+ countBadge : {
249+ backgroundColor : tokens . primaryFixed ,
250+ borderRadius : 99 ,
251+ paddingHorizontal : 10 ,
252+ paddingVertical : 3 ,
253+ borderWidth : 1 ,
254+ borderColor : tokens . primaryContainer ,
255+ } ,
256+ countText : {
257+ fontSize : 10 ,
258+ color : tokens . primary ,
259+ fontFamily : 'PublicSans_700Bold' ,
260+ letterSpacing : 0.4 ,
261+ } ,
262+
263+ subtitle : {
264+ color : tokens . onSurfaceVariant ,
265+ lineHeight : 20 ,
266+ marginBottom : 4 ,
267+ } ,
268+
269+ card : { gap : 0 } ,
270+
143271 cardHead : {
144272 flexDirection : 'row' ,
145- justifyContent : 'space-between' ,
146273 alignItems : 'center' ,
147- marginBottom : 8 ,
274+ gap : 10 ,
275+ marginBottom : 10 ,
276+ } ,
277+ iceBadge : {
278+ backgroundColor : tokens . primary ,
279+ borderRadius : 6 ,
280+ paddingHorizontal : 8 ,
281+ paddingVertical : 3 ,
282+ } ,
283+ iceBadgeText : {
284+ fontSize : 9 ,
285+ color : tokens . onPrimary ,
286+ fontFamily : 'PublicSans_700Bold' ,
287+ letterSpacing : 0.8 ,
148288 } ,
149- iceTitle : { color : tokens . primary , fontFamily : 'PublicSans_700Bold' } ,
289+ iceTitle : {
290+ flex : 1 ,
291+ color : tokens . primary ,
292+ fontFamily : 'PublicSans_700Bold' ,
293+ } ,
294+ deleteBtn : {
295+ padding : 4 ,
296+ } ,
297+
150298 label : { color : tokens . primary , fontSize : 11 , marginTop : 10 , marginBottom : 4 } ,
299+
300+ /* Add contact button */
301+ addBtn : {
302+ flexDirection : 'row' ,
303+ alignItems : 'center' ,
304+ gap : 14 ,
305+ backgroundColor : tokens . surface ,
306+ borderRadius : tokens . radius . card ,
307+ borderWidth : 1.5 ,
308+ borderColor : tokens . primary ,
309+ borderStyle : 'dashed' ,
310+ padding : 16 ,
311+ } ,
312+ addBtnPressed : {
313+ backgroundColor : tokens . primaryFixed ,
314+ } ,
315+ addIconCircle : {
316+ width : 40 ,
317+ height : 40 ,
318+ borderRadius : 20 ,
319+ backgroundColor : tokens . primary ,
320+ alignItems : 'center' ,
321+ justifyContent : 'center' ,
322+ } ,
323+ addTextBlock : { flex : 1 } ,
324+ addLabel : {
325+ color : tokens . primary ,
326+ fontSize : 13 ,
327+ fontFamily : 'PublicSans_700Bold' ,
328+ letterSpacing : 0.3 ,
329+ } ,
330+ addSub : {
331+ color : tokens . onSurfaceVariant ,
332+ fontSize : 11 ,
333+ marginTop : 2 ,
334+ } ,
335+
151336 actions : { flexDirection : 'row' , gap : 12 , marginTop : 8 } ,
152337} ) ;
0 commit comments