create-ern-boilerplate 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useRef } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -13,16 +13,23 @@ import {
|
|
|
13
13
|
TextInput
|
|
14
14
|
} from 'react-native';
|
|
15
15
|
import { Link } from 'expo-router';
|
|
16
|
-
import { Mail, Lock, Eye, EyeOff, LogIn, Sparkles, TentTree, Biohazard } from 'lucide-react-native';
|
|
16
|
+
import { Mail, Lock, Eye, EyeOff, LogIn, Sparkles, TentTree, Biohazard, Fingerprint } from 'lucide-react-native';
|
|
17
|
+
import * as LocalAuthentication from 'expo-local-authentication';
|
|
18
|
+
import * as SecureStore from 'expo-secure-store';
|
|
17
19
|
import { useAuth } from '@/hooks/useAuth';
|
|
18
20
|
import { useTheme } from '@/hooks/useTheme';
|
|
19
21
|
import { Button } from '@/components/common/Button';
|
|
20
22
|
import { validation } from '@/utils/validation';
|
|
21
23
|
import { APP_CONFIG } from '@/utils/constants';
|
|
22
|
-
|
|
24
|
+
import Toast from 'react-native-toast-message';
|
|
23
25
|
|
|
24
26
|
const { width, height } = Dimensions.get('window');
|
|
25
27
|
|
|
28
|
+
// Keys untuk menyimpan credentials di SecureStore
|
|
29
|
+
const BIOMETRIC_ENABLED_KEY = 'biometric_enabled';
|
|
30
|
+
const STORED_EMAIL_KEY = 'stored_email';
|
|
31
|
+
const STORED_PASSWORD_KEY = 'stored_password';
|
|
32
|
+
|
|
26
33
|
export default function LoginScreen() {
|
|
27
34
|
const { login } = useAuth();
|
|
28
35
|
const { colors } = useTheme();
|
|
@@ -31,13 +38,20 @@ export default function LoginScreen() {
|
|
|
31
38
|
const [showPassword, setShowPassword] = useState(false);
|
|
32
39
|
const [errors, setErrors] = useState({ email: '', password: '' });
|
|
33
40
|
const [loading, setLoading] = useState(false);
|
|
41
|
+
|
|
42
|
+
// Biometric states
|
|
43
|
+
const [isBiometricSupported, setIsBiometricSupported] = useState(false);
|
|
44
|
+
const [biometricEnabled, setBiometricEnabled] = useState(false);
|
|
45
|
+
const [biometricType, setBiometricType] = useState<string>('');
|
|
34
46
|
|
|
35
47
|
// Animations
|
|
36
48
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
37
49
|
const slideAnim = useRef(new Animated.Value(50)).current;
|
|
38
50
|
const scaleAnim = useRef(new Animated.Value(0.9)).current;
|
|
51
|
+
const biometricPulse = useRef(new Animated.Value(1)).current;
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
// Animation startup
|
|
41
55
|
Animated.parallel([
|
|
42
56
|
Animated.timing(fadeAnim, {
|
|
43
57
|
toValue: 1,
|
|
@@ -56,8 +70,157 @@ export default function LoginScreen() {
|
|
|
56
70
|
useNativeDriver: true,
|
|
57
71
|
}),
|
|
58
72
|
]).start();
|
|
73
|
+
|
|
74
|
+
// Check biometric support
|
|
75
|
+
checkBiometricSupport();
|
|
76
|
+
checkBiometricEnabled();
|
|
59
77
|
}, []);
|
|
60
78
|
|
|
79
|
+
// Pulse animation untuk biometric button
|
|
80
|
+
const startBiometricPulse = () => {
|
|
81
|
+
Animated.loop(
|
|
82
|
+
Animated.sequence([
|
|
83
|
+
Animated.timing(biometricPulse, {
|
|
84
|
+
toValue: 1.1,
|
|
85
|
+
duration: 1000,
|
|
86
|
+
useNativeDriver: true,
|
|
87
|
+
}),
|
|
88
|
+
Animated.timing(biometricPulse, {
|
|
89
|
+
toValue: 1,
|
|
90
|
+
duration: 1000,
|
|
91
|
+
useNativeDriver: true,
|
|
92
|
+
}),
|
|
93
|
+
])
|
|
94
|
+
).start();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const checkBiometricSupport = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const compatible = await LocalAuthentication.hasHardwareAsync();
|
|
100
|
+
setIsBiometricSupported(compatible);
|
|
101
|
+
|
|
102
|
+
if (compatible) {
|
|
103
|
+
const enrolled = await LocalAuthentication.isEnrolledAsync();
|
|
104
|
+
if (enrolled) {
|
|
105
|
+
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
|
|
106
|
+
if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
|
|
107
|
+
setBiometricType('Fingerprint');
|
|
108
|
+
} else if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
|
|
109
|
+
setBiometricType('Face ID');
|
|
110
|
+
} else {
|
|
111
|
+
setBiometricType('Biometric');
|
|
112
|
+
}
|
|
113
|
+
startBiometricPulse();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error checking biometric support:', error);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const checkBiometricEnabled = async () => {
|
|
122
|
+
try {
|
|
123
|
+
const enabled = await SecureStore.getItemAsync(BIOMETRIC_ENABLED_KEY);
|
|
124
|
+
setBiometricEnabled(enabled === 'true');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('Error checking biometric enabled:', error);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const saveBiometricCredentials = async (email: string, password: string) => {
|
|
131
|
+
try {
|
|
132
|
+
await SecureStore.setItemAsync(BIOMETRIC_ENABLED_KEY, 'true');
|
|
133
|
+
await SecureStore.setItemAsync(STORED_EMAIL_KEY, email);
|
|
134
|
+
await SecureStore.setItemAsync(STORED_PASSWORD_KEY, password);
|
|
135
|
+
setBiometricEnabled(true);
|
|
136
|
+
Toast.show({
|
|
137
|
+
type: 'success',
|
|
138
|
+
text1: 'Biometric Enabled',
|
|
139
|
+
text2: `${biometricType} login has been enabled`,
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error saving biometric credentials:', error);
|
|
143
|
+
Toast.show({
|
|
144
|
+
type: 'error',
|
|
145
|
+
text1: 'Error',
|
|
146
|
+
text2: 'Failed to enable biometric login',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleBiometricLogin = async () => {
|
|
152
|
+
try {
|
|
153
|
+
// Check if biometric is enrolled
|
|
154
|
+
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
|
|
155
|
+
if (!isEnrolled) {
|
|
156
|
+
Alert.alert(
|
|
157
|
+
'Biometric Not Set Up',
|
|
158
|
+
'Please set up biometric authentication in your device settings first.',
|
|
159
|
+
[{ text: 'OK' }]
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Authenticate with biometric
|
|
165
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
166
|
+
promptMessage: `Login with ${biometricType}`,
|
|
167
|
+
cancelLabel: 'Cancel',
|
|
168
|
+
disableDeviceFallback: false,
|
|
169
|
+
fallbackLabel: 'Use Password',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (result.success) {
|
|
173
|
+
// Retrieve stored credentials
|
|
174
|
+
const storedEmail = await SecureStore.getItemAsync(STORED_EMAIL_KEY);
|
|
175
|
+
const storedPassword = await SecureStore.getItemAsync(STORED_PASSWORD_KEY);
|
|
176
|
+
|
|
177
|
+
if (storedEmail && storedPassword) {
|
|
178
|
+
setLoading(true);
|
|
179
|
+
try {
|
|
180
|
+
await login({ email: storedEmail, password: storedPassword });
|
|
181
|
+
Toast.show({
|
|
182
|
+
type: 'success',
|
|
183
|
+
text1: 'Welcome Back!',
|
|
184
|
+
text2: 'Login successful',
|
|
185
|
+
});
|
|
186
|
+
} catch (error: any) {
|
|
187
|
+
Toast.show({
|
|
188
|
+
type: 'error',
|
|
189
|
+
text1: 'Login Failed',
|
|
190
|
+
text2: error.message || 'An error occurred',
|
|
191
|
+
});
|
|
192
|
+
} finally {
|
|
193
|
+
setLoading(false);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
Alert.alert(
|
|
197
|
+
'No Saved Credentials',
|
|
198
|
+
'Please login with email and password first to enable biometric login.',
|
|
199
|
+
[{ text: 'OK' }]
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
// Authentication failed or was cancelled
|
|
204
|
+
if (result.error === 'user_cancel' || result.error === 'system_cancel') {
|
|
205
|
+
// User cancelled, do nothing
|
|
206
|
+
} else {
|
|
207
|
+
Toast.show({
|
|
208
|
+
type: 'error',
|
|
209
|
+
text1: 'Authentication Failed',
|
|
210
|
+
text2: 'Please try again',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Biometric authentication error:', error);
|
|
216
|
+
Toast.show({
|
|
217
|
+
type: 'error',
|
|
218
|
+
text1: 'Error',
|
|
219
|
+
text2: 'Biometric authentication failed',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
61
224
|
const validateForm = () => {
|
|
62
225
|
const emailError = validation.email(email);
|
|
63
226
|
const passwordError = validation.password(password);
|
|
@@ -76,13 +239,67 @@ export default function LoginScreen() {
|
|
|
76
239
|
setLoading(true);
|
|
77
240
|
try {
|
|
78
241
|
await login({ email, password });
|
|
242
|
+
|
|
243
|
+
// Jika login berhasil dan biometric supported, tawarkan untuk enable biometric
|
|
244
|
+
if (isBiometricSupported && !biometricEnabled) {
|
|
245
|
+
Alert.alert(
|
|
246
|
+
'Enable Biometric Login?',
|
|
247
|
+
`Would you like to enable ${biometricType} login for faster access next time?`,
|
|
248
|
+
[
|
|
249
|
+
{
|
|
250
|
+
text: 'Not Now',
|
|
251
|
+
style: 'cancel',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
text: 'Enable',
|
|
255
|
+
onPress: () => saveBiometricCredentials(email, password),
|
|
256
|
+
},
|
|
257
|
+
]
|
|
258
|
+
);
|
|
259
|
+
}
|
|
79
260
|
} catch (error: any) {
|
|
80
|
-
|
|
261
|
+
Toast.show({
|
|
262
|
+
type: 'error',
|
|
263
|
+
text1: 'Login Failed',
|
|
264
|
+
text2: error.message || 'An error occurred',
|
|
265
|
+
});
|
|
81
266
|
} finally {
|
|
82
267
|
setLoading(false);
|
|
83
268
|
}
|
|
84
269
|
};
|
|
85
270
|
|
|
271
|
+
const disableBiometric = async () => {
|
|
272
|
+
Alert.alert(
|
|
273
|
+
'Disable Biometric Login?',
|
|
274
|
+
'You will need to login with email and password next time.',
|
|
275
|
+
[
|
|
276
|
+
{
|
|
277
|
+
text: 'Cancel',
|
|
278
|
+
style: 'cancel',
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
text: 'Disable',
|
|
282
|
+
style: 'destructive',
|
|
283
|
+
onPress: async () => {
|
|
284
|
+
try {
|
|
285
|
+
await SecureStore.deleteItemAsync(BIOMETRIC_ENABLED_KEY);
|
|
286
|
+
await SecureStore.deleteItemAsync(STORED_EMAIL_KEY);
|
|
287
|
+
await SecureStore.deleteItemAsync(STORED_PASSWORD_KEY);
|
|
288
|
+
setBiometricEnabled(false);
|
|
289
|
+
Toast.show({
|
|
290
|
+
type: 'success',
|
|
291
|
+
text1: 'Biometric Disabled',
|
|
292
|
+
text2: 'Biometric login has been disabled',
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Error disabling biometric:', error);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
]
|
|
300
|
+
);
|
|
301
|
+
};
|
|
302
|
+
|
|
86
303
|
return (
|
|
87
304
|
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
88
305
|
{/* Decorative Background Elements */}
|
|
@@ -133,6 +350,54 @@ export default function LoginScreen() {
|
|
|
133
350
|
</View>
|
|
134
351
|
</View>
|
|
135
352
|
|
|
353
|
+
{/* Biometric Login Button - Show if enabled */}
|
|
354
|
+
{isBiometricSupported && biometricEnabled && (
|
|
355
|
+
<Animated.View style={{ transform: [{ scale: biometricPulse }] }}>
|
|
356
|
+
<TouchableOpacity
|
|
357
|
+
style={[styles.biometricButton, {
|
|
358
|
+
backgroundColor: `${colors.primary}15`,
|
|
359
|
+
borderColor: colors.primary,
|
|
360
|
+
}]}
|
|
361
|
+
onPress={handleBiometricLogin}
|
|
362
|
+
activeOpacity={0.7}
|
|
363
|
+
>
|
|
364
|
+
<View style={[styles.biometricIconWrapper, { backgroundColor: colors.primary }]}>
|
|
365
|
+
<Fingerprint size={28} color="#FFFFFF" strokeWidth={2} />
|
|
366
|
+
</View>
|
|
367
|
+
<View style={styles.biometricTextContainer}>
|
|
368
|
+
<Text style={[styles.biometricTitle, { color: colors.text }]}>
|
|
369
|
+
Login with {biometricType}
|
|
370
|
+
</Text>
|
|
371
|
+
<Text style={[styles.biometricSubtitle, { color: colors.textSecondary }]}>
|
|
372
|
+
Quick and secure access
|
|
373
|
+
</Text>
|
|
374
|
+
</View>
|
|
375
|
+
<TouchableOpacity
|
|
376
|
+
style={styles.biometricSettingsButton}
|
|
377
|
+
onPress={disableBiometric}
|
|
378
|
+
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
379
|
+
>
|
|
380
|
+
<Text style={[styles.biometricSettingsText, { color: colors.textSecondary }]}>
|
|
381
|
+
⚙️
|
|
382
|
+
</Text>
|
|
383
|
+
</TouchableOpacity>
|
|
384
|
+
</TouchableOpacity>
|
|
385
|
+
</Animated.View>
|
|
386
|
+
)}
|
|
387
|
+
|
|
388
|
+
{/* Divider if biometric is shown */}
|
|
389
|
+
{isBiometricSupported && biometricEnabled && (
|
|
390
|
+
<View style={styles.dividerContainer}>
|
|
391
|
+
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
|
392
|
+
<View style={[styles.dividerTextWrapper, { backgroundColor: colors.background }]}>
|
|
393
|
+
<Text style={[styles.dividerText, { color: colors.textSecondary }]}>
|
|
394
|
+
{/* or sign in with email */}
|
|
395
|
+
</Text>
|
|
396
|
+
</View>
|
|
397
|
+
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
|
398
|
+
</View>
|
|
399
|
+
)}
|
|
400
|
+
|
|
136
401
|
{/* Form Container with Card Style */}
|
|
137
402
|
<View style={[styles.formCard, {
|
|
138
403
|
backgroundColor: colors.card || colors.background,
|
|
@@ -249,7 +514,7 @@ export default function LoginScreen() {
|
|
|
249
514
|
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
|
250
515
|
<View style={[styles.dividerTextWrapper, { backgroundColor: colors.background }]}>
|
|
251
516
|
<Text style={[styles.dividerText, { color: colors.textSecondary }]}>
|
|
252
|
-
or
|
|
517
|
+
{/* or */}
|
|
253
518
|
</Text>
|
|
254
519
|
</View>
|
|
255
520
|
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
|
@@ -372,6 +637,45 @@ const styles = StyleSheet.create({
|
|
|
372
637
|
textAlign: 'center',
|
|
373
638
|
opacity: 0.8,
|
|
374
639
|
},
|
|
640
|
+
biometricButton: {
|
|
641
|
+
flexDirection: 'row',
|
|
642
|
+
alignItems: 'center',
|
|
643
|
+
borderRadius: 20,
|
|
644
|
+
padding: 20,
|
|
645
|
+
marginBottom: 24,
|
|
646
|
+
borderWidth: 2,
|
|
647
|
+
elevation: 2,
|
|
648
|
+
shadowColor: '#000',
|
|
649
|
+
shadowOffset: { width: 0, height: 2 },
|
|
650
|
+
shadowOpacity: 0.1,
|
|
651
|
+
shadowRadius: 8,
|
|
652
|
+
},
|
|
653
|
+
biometricIconWrapper: {
|
|
654
|
+
width: 56,
|
|
655
|
+
height: 56,
|
|
656
|
+
borderRadius: 28,
|
|
657
|
+
alignItems: 'center',
|
|
658
|
+
justifyContent: 'center',
|
|
659
|
+
marginRight: 16,
|
|
660
|
+
},
|
|
661
|
+
biometricTextContainer: {
|
|
662
|
+
flex: 1,
|
|
663
|
+
},
|
|
664
|
+
biometricTitle: {
|
|
665
|
+
fontSize: 16,
|
|
666
|
+
fontWeight: '700',
|
|
667
|
+
marginBottom: 4,
|
|
668
|
+
},
|
|
669
|
+
biometricSubtitle: {
|
|
670
|
+
fontSize: 13,
|
|
671
|
+
opacity: 0.7,
|
|
672
|
+
},
|
|
673
|
+
biometricSettingsButton: {
|
|
674
|
+
padding: 8,
|
|
675
|
+
},
|
|
676
|
+
biometricSettingsText: {
|
|
677
|
+
fontSize: 20,
|
|
678
|
+
},
|
|
375
679
|
formCard: {
|
|
376
680
|
borderRadius: 24,
|
|
377
681
|
padding: 24,
|
|
@@ -452,7 +756,7 @@ const styles = StyleSheet.create({
|
|
|
452
756
|
height: 1,
|
|
453
757
|
},
|
|
454
758
|
dividerTextWrapper: {
|
|
455
|
-
paddingHorizontal: 16,
|
|
759
|
+
// paddingHorizontal: 16,
|
|
456
760
|
position: 'absolute',
|
|
457
761
|
left: '50%',
|
|
458
762
|
transform: [{ translateX: -20 }],
|
|
@@ -77,22 +77,20 @@ export default function MainLayout() {
|
|
|
77
77
|
}}
|
|
78
78
|
/>
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/>
|
|
95
|
-
)}
|
|
80
|
+
<Tabs.Screen
|
|
81
|
+
name="settings"
|
|
82
|
+
options={{
|
|
83
|
+
title: 'Settings',
|
|
84
|
+
tabBarIcon: ({ color, focused }) => (
|
|
85
|
+
<View style={[
|
|
86
|
+
styles.iconContainer,
|
|
87
|
+
focused && { backgroundColor: `${colors.primary}15` }
|
|
88
|
+
]}>
|
|
89
|
+
<Settings size={24} color={color} strokeWidth={focused ? 2.5 : 2} />
|
|
90
|
+
</View>
|
|
91
|
+
),
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
96
94
|
</Tabs>
|
|
97
95
|
);
|
|
98
96
|
}
|
|
@@ -29,16 +29,14 @@ export default function ProfileScreen() {
|
|
|
29
29
|
const [saving, setSaving] = useState(false);
|
|
30
30
|
const [currentUser, setCurrentUser] = useState<any>(null);
|
|
31
31
|
|
|
32
|
-
// Profile data state
|
|
33
|
-
const [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
joinDate: '',
|
|
41
|
-
});
|
|
32
|
+
// Profile data state - FIX: Separate state untuk setiap field
|
|
33
|
+
const [name, setName] = useState('');
|
|
34
|
+
const [email, setEmail] = useState('');
|
|
35
|
+
const [phone, setPhone] = useState('');
|
|
36
|
+
const [location, setLocation] = useState('');
|
|
37
|
+
const [about, setAbout] = useState('');
|
|
38
|
+
const [avatar, setAvatar] = useState('');
|
|
39
|
+
const [joinDate, setJoinDate] = useState('');
|
|
42
40
|
|
|
43
41
|
// Load current user data
|
|
44
42
|
useEffect(() => {
|
|
@@ -46,18 +44,19 @@ export default function ProfileScreen() {
|
|
|
46
44
|
const userProfile = users.find(u => u.id === user.id || u.email === user.email);
|
|
47
45
|
if (userProfile) {
|
|
48
46
|
setCurrentUser(userProfile);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
setName(userProfile.name || '');
|
|
48
|
+
setEmail(userProfile.email || '');
|
|
49
|
+
setPhone(userProfile.phone || '');
|
|
50
|
+
setLocation(userProfile.location || '');
|
|
51
|
+
setAbout(userProfile.about || '');
|
|
52
|
+
setAvatar(userProfile.avatar || '');
|
|
53
|
+
setJoinDate(
|
|
54
|
+
new Date(userProfile.createdAt).toLocaleDateString('en-US', {
|
|
57
55
|
year: 'numeric',
|
|
58
|
-
month: 'long'
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
month: 'long',
|
|
57
|
+
day: 'numeric'
|
|
58
|
+
})
|
|
59
|
+
);
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
}, [user, users]);
|
|
@@ -69,11 +68,11 @@ export default function ProfileScreen() {
|
|
|
69
68
|
try {
|
|
70
69
|
const updateData: UpdateUserInput = {
|
|
71
70
|
id: currentUser.id,
|
|
72
|
-
name
|
|
73
|
-
email
|
|
74
|
-
phone
|
|
75
|
-
location
|
|
76
|
-
about
|
|
71
|
+
name,
|
|
72
|
+
email,
|
|
73
|
+
phone,
|
|
74
|
+
location,
|
|
75
|
+
about,
|
|
77
76
|
role: currentUser.role,
|
|
78
77
|
status: currentUser.status,
|
|
79
78
|
avatar: currentUser.avatar,
|
|
@@ -83,6 +82,7 @@ export default function ProfileScreen() {
|
|
|
83
82
|
|
|
84
83
|
if (result.success) {
|
|
85
84
|
setIsEditing(false);
|
|
85
|
+
Alert.alert('Success', 'Profile updated successfully');
|
|
86
86
|
}
|
|
87
87
|
} catch (error) {
|
|
88
88
|
Alert.alert('Error', 'Failed to update profile');
|
|
@@ -95,23 +95,16 @@ export default function ProfileScreen() {
|
|
|
95
95
|
setIsEditing(false);
|
|
96
96
|
// Reset to original values
|
|
97
97
|
if (currentUser) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
about: currentUser.about || '',
|
|
104
|
-
avatar: currentUser.avatar || '',
|
|
105
|
-
joinDate: new Date(currentUser.createdAt).toLocaleDateString('en-US', {
|
|
106
|
-
year: 'numeric',
|
|
107
|
-
month: 'long'
|
|
108
|
-
}),
|
|
109
|
-
});
|
|
98
|
+
setName(currentUser.name || '');
|
|
99
|
+
setEmail(currentUser.email || '');
|
|
100
|
+
setPhone(currentUser.phone || '');
|
|
101
|
+
setLocation(currentUser.location || '');
|
|
102
|
+
setAbout(currentUser.about || '');
|
|
110
103
|
}
|
|
111
104
|
};
|
|
112
105
|
|
|
113
|
-
const InfoCard = ({ icon: Icon, label, value, editable = false,
|
|
114
|
-
<View style={[styles.infoCard, { backgroundColor:
|
|
106
|
+
const InfoCard = ({ icon: Icon, label, value, editable = false, onChangeText }: any) => (
|
|
107
|
+
<View style={[styles.infoCard, { backgroundColor: colors.card || colors.background }]}>
|
|
115
108
|
<View style={[styles.iconWrapper, { backgroundColor: `${colors.primary}15` }]}>
|
|
116
109
|
<Icon size={20} color={colors.primary} />
|
|
117
110
|
</View>
|
|
@@ -119,11 +112,12 @@ export default function ProfileScreen() {
|
|
|
119
112
|
<Text style={[styles.infoLabel, { color: colors.textSecondary }]}>{label}</Text>
|
|
120
113
|
{isEditing && editable ? (
|
|
121
114
|
<TextInput
|
|
122
|
-
style={[styles.infoInput, { color: colors.text, borderColor: colors.
|
|
115
|
+
style={[styles.infoInput, { color: colors.text, borderColor: `${colors.primary}30` }]}
|
|
123
116
|
value={value}
|
|
124
|
-
onChangeText={
|
|
117
|
+
onChangeText={onChangeText}
|
|
125
118
|
placeholder={label}
|
|
126
119
|
placeholderTextColor={colors.textSecondary}
|
|
120
|
+
editable={!saving}
|
|
127
121
|
/>
|
|
128
122
|
) : (
|
|
129
123
|
<Text style={[styles.infoValue, { color: colors.text }]}>{value || '-'}</Text>
|
|
@@ -141,7 +135,7 @@ export default function ProfileScreen() {
|
|
|
141
135
|
|
|
142
136
|
if (loading && !currentUser) {
|
|
143
137
|
return (
|
|
144
|
-
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
|
138
|
+
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
|
|
145
139
|
<AppHeader
|
|
146
140
|
variant="back"
|
|
147
141
|
title="Profile"
|
|
@@ -161,7 +155,7 @@ export default function ProfileScreen() {
|
|
|
161
155
|
|
|
162
156
|
if (!currentUser) {
|
|
163
157
|
return (
|
|
164
|
-
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
|
158
|
+
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
|
|
165
159
|
<AppHeader
|
|
166
160
|
variant="back"
|
|
167
161
|
title="Profile"
|
|
@@ -179,7 +173,7 @@ export default function ProfileScreen() {
|
|
|
179
173
|
}
|
|
180
174
|
|
|
181
175
|
return (
|
|
182
|
-
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
|
176
|
+
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} edges={['top']}>
|
|
183
177
|
<AppHeader
|
|
184
178
|
variant="back"
|
|
185
179
|
title="Profile"
|
|
@@ -190,38 +184,50 @@ export default function ProfileScreen() {
|
|
|
190
184
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
191
185
|
{/* Header with Avatar */}
|
|
192
186
|
<View style={styles.header}>
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
187
|
+
<View style={[styles.avatarContainer, { backgroundColor: `${colors.primary}20` }]}>
|
|
188
|
+
{avatar ? (
|
|
189
|
+
<Image
|
|
190
|
+
source={{ uri: avatar }}
|
|
191
|
+
style={styles.avatarImage}
|
|
192
|
+
defaultSource={require('@/assets/images/icon.png')}
|
|
193
|
+
/>
|
|
194
|
+
) : (
|
|
195
|
+
<Text style={[styles.avatarText, { color: colors.primary }]}>
|
|
196
|
+
{name.charAt(0).toUpperCase()}
|
|
197
|
+
</Text>
|
|
198
|
+
)}
|
|
204
199
|
</View>
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
|
|
201
|
+
<Text style={[styles.userName, { color: colors.text }]}>{name}</Text>
|
|
202
|
+
|
|
203
|
+
<View style={styles.badgeContainer}>
|
|
204
|
+
<View style={[styles.roleBadge, { backgroundColor: `${colors.primary}15` }]}>
|
|
205
|
+
<Text style={[styles.roleText, { color: colors.primary }]}>
|
|
206
|
+
{currentUser.role.toUpperCase()}
|
|
207
|
+
</Text>
|
|
208
|
+
</View>
|
|
209
|
+
|
|
209
210
|
<View style={[
|
|
210
|
-
styles.
|
|
211
|
-
{ backgroundColor: currentUser.status === 'active' ? '#
|
|
212
|
-
]} />
|
|
213
|
-
<Text style={[
|
|
214
|
-
styles.statusText,
|
|
215
|
-
{ color: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
|
|
211
|
+
styles.statusBadge,
|
|
212
|
+
{ backgroundColor: currentUser.status === 'active' ? '#10b98115' : '#ef444415' }
|
|
216
213
|
]}>
|
|
217
|
-
{
|
|
218
|
-
|
|
214
|
+
<View style={[
|
|
215
|
+
styles.statusDot,
|
|
216
|
+
{ backgroundColor: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
|
|
217
|
+
]} />
|
|
218
|
+
<Text style={[
|
|
219
|
+
styles.statusText,
|
|
220
|
+
{ color: currentUser.status === 'active' ? '#10b981' : '#ef4444' }
|
|
221
|
+
]}>
|
|
222
|
+
{currentUser.status === 'active' ? 'Active' : 'Inactive'}
|
|
223
|
+
</Text>
|
|
224
|
+
</View>
|
|
219
225
|
</View>
|
|
220
226
|
</View>
|
|
221
227
|
|
|
222
228
|
{/* Stats */}
|
|
223
229
|
<View style={styles.statsContainer}>
|
|
224
|
-
<StatCard title="Articles
|
|
230
|
+
<StatCard title="Articles" value="127" color="#10b981" />
|
|
225
231
|
<StatCard title="Favorites" value="34" color="#f59e0b" />
|
|
226
232
|
<StatCard title="Comments" value="89" color="#3b82f6" />
|
|
227
233
|
</View>
|
|
@@ -231,7 +237,7 @@ export default function ProfileScreen() {
|
|
|
231
237
|
<TouchableOpacity
|
|
232
238
|
style={[styles.editButton, { backgroundColor: colors.primary }]}
|
|
233
239
|
onPress={() => setIsEditing(true)}
|
|
234
|
-
activeOpacity={0.
|
|
240
|
+
activeOpacity={0.7}
|
|
235
241
|
>
|
|
236
242
|
<Edit2 size={18} color="#FFFFFF" />
|
|
237
243
|
<Text style={styles.editButtonText}>Edit Profile</Text>
|
|
@@ -239,21 +245,31 @@ export default function ProfileScreen() {
|
|
|
239
245
|
) : (
|
|
240
246
|
<View style={styles.editActions}>
|
|
241
247
|
<TouchableOpacity
|
|
242
|
-
style={[styles.actionButton, styles.cancelButton, {
|
|
248
|
+
style={[styles.actionButton, styles.cancelButton, {
|
|
249
|
+
borderColor: colors.border,
|
|
250
|
+
backgroundColor: colors.card || colors.background
|
|
251
|
+
}]}
|
|
243
252
|
onPress={handleCancel}
|
|
244
|
-
activeOpacity={0.
|
|
253
|
+
activeOpacity={0.7}
|
|
245
254
|
disabled={saving}
|
|
246
255
|
>
|
|
247
256
|
<X size={18} color={colors.text} />
|
|
248
257
|
<Text style={[styles.actionButtonText, { color: colors.text }]}>Cancel</Text>
|
|
249
258
|
</TouchableOpacity>
|
|
250
259
|
<TouchableOpacity
|
|
251
|
-
style={[styles.actionButton, styles.saveButton, {
|
|
260
|
+
style={[styles.actionButton, styles.saveButton, {
|
|
261
|
+
backgroundColor: colors.primary,
|
|
262
|
+
opacity: saving ? 0.7 : 1
|
|
263
|
+
}]}
|
|
252
264
|
onPress={handleSave}
|
|
253
|
-
activeOpacity={0.
|
|
265
|
+
activeOpacity={0.7}
|
|
254
266
|
disabled={saving}
|
|
255
267
|
>
|
|
256
|
-
|
|
268
|
+
{saving ? (
|
|
269
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
270
|
+
) : (
|
|
271
|
+
<Save size={18} color="#FFFFFF" />
|
|
272
|
+
)}
|
|
257
273
|
<Text style={[styles.actionButtonText, { color: '#FFFFFF' }]}>
|
|
258
274
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
259
275
|
</Text>
|
|
@@ -263,61 +279,66 @@ export default function ProfileScreen() {
|
|
|
263
279
|
|
|
264
280
|
{/* Profile Information */}
|
|
265
281
|
<View style={styles.section}>
|
|
266
|
-
<Text style={[styles.sectionTitle, { color: colors.text }]}>
|
|
282
|
+
<Text style={[styles.sectionTitle, { color: colors.text }]}>Personal Information</Text>
|
|
267
283
|
|
|
268
284
|
<InfoCard
|
|
269
285
|
icon={User}
|
|
270
|
-
label="Name"
|
|
271
|
-
value={
|
|
286
|
+
label="Full Name"
|
|
287
|
+
value={name}
|
|
272
288
|
editable
|
|
273
|
-
|
|
289
|
+
onChangeText={setName}
|
|
274
290
|
/>
|
|
275
291
|
<InfoCard
|
|
276
292
|
icon={Mail}
|
|
277
|
-
label="Email"
|
|
278
|
-
value={
|
|
293
|
+
label="Email Address"
|
|
294
|
+
value={email}
|
|
279
295
|
editable
|
|
280
|
-
|
|
296
|
+
onChangeText={setEmail}
|
|
281
297
|
/>
|
|
282
298
|
<InfoCard
|
|
283
299
|
icon={Phone}
|
|
284
|
-
label="Phone"
|
|
285
|
-
value={
|
|
300
|
+
label="Phone Number"
|
|
301
|
+
value={phone}
|
|
286
302
|
editable
|
|
287
|
-
|
|
303
|
+
onChangeText={setPhone}
|
|
288
304
|
/>
|
|
289
305
|
<InfoCard
|
|
290
306
|
icon={MapPin}
|
|
291
307
|
label="Location"
|
|
292
|
-
value={
|
|
308
|
+
value={location}
|
|
293
309
|
editable
|
|
294
|
-
|
|
310
|
+
onChangeText={setLocation}
|
|
295
311
|
/>
|
|
296
312
|
<InfoCard
|
|
297
313
|
icon={Calendar}
|
|
298
314
|
label="Member Since"
|
|
299
|
-
value={
|
|
315
|
+
value={joinDate}
|
|
300
316
|
/>
|
|
301
317
|
</View>
|
|
302
318
|
|
|
303
319
|
{/* Bio Section */}
|
|
304
320
|
<View style={styles.section}>
|
|
305
|
-
<Text style={[styles.sectionTitle, { color: colors.text }]}>About</Text>
|
|
306
|
-
<View style={[styles.bioCard, { backgroundColor:
|
|
321
|
+
<Text style={[styles.sectionTitle, { color: colors.text }]}>About Me</Text>
|
|
322
|
+
<View style={[styles.bioCard, { backgroundColor: colors.card || colors.background }]}>
|
|
307
323
|
{isEditing ? (
|
|
308
324
|
<TextInput
|
|
309
|
-
style={[styles.bioInput, {
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
style={[styles.bioInput, {
|
|
326
|
+
color: colors.text,
|
|
327
|
+
borderColor: `${colors.primary}30`,
|
|
328
|
+
backgroundColor: colors.background
|
|
329
|
+
}]}
|
|
330
|
+
value={about}
|
|
331
|
+
onChangeText={setAbout}
|
|
312
332
|
placeholder="Tell us about yourself..."
|
|
313
333
|
placeholderTextColor={colors.textSecondary}
|
|
314
334
|
multiline
|
|
315
335
|
numberOfLines={4}
|
|
316
336
|
textAlignVertical="top"
|
|
337
|
+
editable={!saving}
|
|
317
338
|
/>
|
|
318
339
|
) : (
|
|
319
340
|
<Text style={[styles.bioText, { color: colors.text }]}>
|
|
320
|
-
{
|
|
341
|
+
{about || 'No bio available. Add your bio by editing your profile.'}
|
|
321
342
|
</Text>
|
|
322
343
|
)}
|
|
323
344
|
</View>
|
|
@@ -350,50 +371,53 @@ const styles = StyleSheet.create({
|
|
|
350
371
|
},
|
|
351
372
|
header: {
|
|
352
373
|
alignItems: 'center',
|
|
353
|
-
paddingTop:
|
|
354
|
-
paddingBottom:
|
|
374
|
+
paddingTop: 40,
|
|
375
|
+
paddingBottom: 32,
|
|
355
376
|
paddingHorizontal: 24,
|
|
356
377
|
},
|
|
357
378
|
avatarContainer: {
|
|
358
|
-
width:
|
|
359
|
-
height:
|
|
360
|
-
borderRadius:
|
|
379
|
+
width: 120,
|
|
380
|
+
height: 120,
|
|
381
|
+
borderRadius: 60,
|
|
361
382
|
justifyContent: 'center',
|
|
362
383
|
alignItems: 'center',
|
|
363
|
-
marginBottom:
|
|
364
|
-
elevation:
|
|
384
|
+
marginBottom: 20,
|
|
385
|
+
elevation: 6,
|
|
365
386
|
shadowColor: '#000',
|
|
366
|
-
shadowOffset: { width: 0, height:
|
|
367
|
-
shadowOpacity: 0.
|
|
368
|
-
shadowRadius:
|
|
387
|
+
shadowOffset: { width: 0, height: 4 },
|
|
388
|
+
shadowOpacity: 0.15,
|
|
389
|
+
shadowRadius: 12,
|
|
390
|
+
overflow: 'hidden',
|
|
391
|
+
},
|
|
392
|
+
avatarImage: {
|
|
393
|
+
width: '100%',
|
|
394
|
+
height: '100%',
|
|
395
|
+
borderRadius: 60,
|
|
369
396
|
},
|
|
370
397
|
avatarText: {
|
|
371
|
-
fontSize:
|
|
398
|
+
fontSize: 48,
|
|
372
399
|
fontWeight: '800',
|
|
373
|
-
color: '#FFFFFF',
|
|
374
|
-
},
|
|
375
|
-
userAvatar: {
|
|
376
|
-
// width: 100,
|
|
377
|
-
// height: 50,
|
|
378
|
-
borderRadius: 25,
|
|
379
|
-
marginRight: 12,
|
|
380
400
|
},
|
|
381
401
|
userName: {
|
|
382
402
|
fontSize: 28,
|
|
383
403
|
fontWeight: '800',
|
|
384
|
-
marginBottom:
|
|
404
|
+
marginBottom: 12,
|
|
405
|
+
textAlign: 'center',
|
|
406
|
+
},
|
|
407
|
+
badgeContainer: {
|
|
408
|
+
flexDirection: 'row',
|
|
409
|
+
alignItems: 'center',
|
|
410
|
+
gap: 8,
|
|
385
411
|
},
|
|
386
412
|
roleBadge: {
|
|
387
413
|
paddingHorizontal: 16,
|
|
388
414
|
paddingVertical: 6,
|
|
389
415
|
borderRadius: 20,
|
|
390
|
-
marginBottom: 8,
|
|
391
416
|
},
|
|
392
417
|
roleText: {
|
|
393
|
-
fontSize:
|
|
418
|
+
fontSize: 12,
|
|
394
419
|
fontWeight: '700',
|
|
395
|
-
|
|
396
|
-
letterSpacing: 0.5,
|
|
420
|
+
letterSpacing: 0.8,
|
|
397
421
|
},
|
|
398
422
|
statusBadge: {
|
|
399
423
|
flexDirection: 'row',
|
|
@@ -420,43 +444,50 @@ const styles = StyleSheet.create({
|
|
|
420
444
|
},
|
|
421
445
|
statCard: {
|
|
422
446
|
flex: 1,
|
|
423
|
-
padding:
|
|
447
|
+
padding: 20,
|
|
424
448
|
borderRadius: 16,
|
|
425
449
|
alignItems: 'center',
|
|
426
|
-
elevation:
|
|
450
|
+
elevation: 2,
|
|
427
451
|
shadowColor: '#000',
|
|
428
|
-
shadowOffset: { width: 0, height:
|
|
429
|
-
shadowOpacity: 0.
|
|
430
|
-
shadowRadius:
|
|
452
|
+
shadowOffset: { width: 0, height: 2 },
|
|
453
|
+
shadowOpacity: 0.08,
|
|
454
|
+
shadowRadius: 8,
|
|
431
455
|
},
|
|
432
456
|
statValue: {
|
|
433
|
-
fontSize:
|
|
457
|
+
fontSize: 28,
|
|
434
458
|
fontWeight: '800',
|
|
435
459
|
marginBottom: 4,
|
|
436
460
|
},
|
|
437
461
|
statTitle: {
|
|
438
462
|
fontSize: 11,
|
|
439
463
|
fontWeight: '600',
|
|
464
|
+
textTransform: 'uppercase',
|
|
465
|
+
letterSpacing: 0.5,
|
|
440
466
|
},
|
|
441
467
|
editButton: {
|
|
442
468
|
flexDirection: 'row',
|
|
443
469
|
alignItems: 'center',
|
|
444
470
|
justifyContent: 'center',
|
|
445
471
|
marginHorizontal: 24,
|
|
446
|
-
marginBottom:
|
|
447
|
-
paddingVertical:
|
|
472
|
+
marginBottom: 32,
|
|
473
|
+
paddingVertical: 16,
|
|
448
474
|
borderRadius: 16,
|
|
449
475
|
gap: 8,
|
|
476
|
+
elevation: 3,
|
|
477
|
+
shadowColor: '#000',
|
|
478
|
+
shadowOffset: { width: 0, height: 2 },
|
|
479
|
+
shadowOpacity: 0.1,
|
|
480
|
+
shadowRadius: 8,
|
|
450
481
|
},
|
|
451
482
|
editButtonText: {
|
|
452
483
|
color: '#FFFFFF',
|
|
453
|
-
fontSize:
|
|
484
|
+
fontSize: 16,
|
|
454
485
|
fontWeight: '700',
|
|
455
486
|
},
|
|
456
487
|
editActions: {
|
|
457
488
|
flexDirection: 'row',
|
|
458
489
|
paddingHorizontal: 24,
|
|
459
|
-
marginBottom:
|
|
490
|
+
marginBottom: 32,
|
|
460
491
|
gap: 12,
|
|
461
492
|
},
|
|
462
493
|
actionButton: {
|
|
@@ -464,9 +495,14 @@ const styles = StyleSheet.create({
|
|
|
464
495
|
flexDirection: 'row',
|
|
465
496
|
alignItems: 'center',
|
|
466
497
|
justifyContent: 'center',
|
|
467
|
-
paddingVertical:
|
|
498
|
+
paddingVertical: 16,
|
|
468
499
|
borderRadius: 16,
|
|
469
500
|
gap: 8,
|
|
501
|
+
elevation: 2,
|
|
502
|
+
shadowColor: '#000',
|
|
503
|
+
shadowOffset: { width: 0, height: 1 },
|
|
504
|
+
shadowOpacity: 0.08,
|
|
505
|
+
shadowRadius: 4,
|
|
470
506
|
},
|
|
471
507
|
cancelButton: {
|
|
472
508
|
borderWidth: 2,
|
|
@@ -480,12 +516,13 @@ const styles = StyleSheet.create({
|
|
|
480
516
|
},
|
|
481
517
|
section: {
|
|
482
518
|
paddingHorizontal: 24,
|
|
483
|
-
marginBottom:
|
|
519
|
+
marginBottom: 32,
|
|
484
520
|
},
|
|
485
521
|
sectionTitle: {
|
|
486
|
-
fontSize:
|
|
522
|
+
fontSize: 20,
|
|
487
523
|
fontWeight: '800',
|
|
488
524
|
marginBottom: 16,
|
|
525
|
+
letterSpacing: 0.3,
|
|
489
526
|
},
|
|
490
527
|
infoCard: {
|
|
491
528
|
flexDirection: 'row',
|
|
@@ -493,14 +530,19 @@ const styles = StyleSheet.create({
|
|
|
493
530
|
padding: 16,
|
|
494
531
|
borderRadius: 16,
|
|
495
532
|
marginBottom: 12,
|
|
533
|
+
elevation: 1,
|
|
534
|
+
shadowColor: '#000',
|
|
535
|
+
shadowOffset: { width: 0, height: 1 },
|
|
536
|
+
shadowOpacity: 0.05,
|
|
537
|
+
shadowRadius: 4,
|
|
496
538
|
},
|
|
497
539
|
iconWrapper: {
|
|
498
|
-
width:
|
|
499
|
-
height:
|
|
500
|
-
borderRadius:
|
|
540
|
+
width: 48,
|
|
541
|
+
height: 48,
|
|
542
|
+
borderRadius: 14,
|
|
501
543
|
justifyContent: 'center',
|
|
502
544
|
alignItems: 'center',
|
|
503
|
-
marginRight:
|
|
545
|
+
marginRight: 14,
|
|
504
546
|
},
|
|
505
547
|
infoContent: {
|
|
506
548
|
flex: 1,
|
|
@@ -508,32 +550,40 @@ const styles = StyleSheet.create({
|
|
|
508
550
|
infoLabel: {
|
|
509
551
|
fontSize: 12,
|
|
510
552
|
fontWeight: '600',
|
|
511
|
-
marginBottom:
|
|
553
|
+
marginBottom: 6,
|
|
554
|
+
textTransform: 'uppercase',
|
|
555
|
+
letterSpacing: 0.5,
|
|
512
556
|
},
|
|
513
557
|
infoValue: {
|
|
514
|
-
fontSize:
|
|
558
|
+
fontSize: 16,
|
|
515
559
|
fontWeight: '600',
|
|
516
560
|
},
|
|
517
561
|
infoInput: {
|
|
518
|
-
fontSize:
|
|
562
|
+
fontSize: 16,
|
|
519
563
|
fontWeight: '600',
|
|
520
|
-
borderBottomWidth:
|
|
521
|
-
paddingVertical:
|
|
564
|
+
borderBottomWidth: 2,
|
|
565
|
+
paddingVertical: 6,
|
|
566
|
+
paddingHorizontal: 4,
|
|
522
567
|
},
|
|
523
568
|
bioCard: {
|
|
524
|
-
padding:
|
|
569
|
+
padding: 20,
|
|
525
570
|
borderRadius: 16,
|
|
571
|
+
elevation: 1,
|
|
572
|
+
shadowColor: '#000',
|
|
573
|
+
shadowOffset: { width: 0, height: 1 },
|
|
574
|
+
shadowOpacity: 0.05,
|
|
575
|
+
shadowRadius: 4,
|
|
526
576
|
},
|
|
527
577
|
bioText: {
|
|
528
578
|
fontSize: 15,
|
|
529
|
-
lineHeight:
|
|
579
|
+
lineHeight: 24,
|
|
530
580
|
},
|
|
531
581
|
bioInput: {
|
|
532
582
|
fontSize: 15,
|
|
533
|
-
lineHeight:
|
|
534
|
-
minHeight:
|
|
535
|
-
borderWidth:
|
|
583
|
+
lineHeight: 24,
|
|
584
|
+
minHeight: 120,
|
|
585
|
+
borderWidth: 2,
|
|
536
586
|
borderRadius: 12,
|
|
537
|
-
padding:
|
|
587
|
+
padding: 16,
|
|
538
588
|
},
|
|
539
589
|
});
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"react-native-worklets": "0.5.1",
|
|
32
32
|
"react-native-svg": "15.12.1",
|
|
33
33
|
"react-native-toast-message": "^2.3.3",
|
|
34
|
-
"expo-linear-gradient": "~15.0.7"
|
|
34
|
+
"expo-linear-gradient": "~15.0.7",
|
|
35
|
+
"expo-local-authentication": "~17.0.7"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@babel/core": "^7.26.0",
|