Skip to content

Commit fd61cb2

Browse files
ft(#6):update-trainee-profile (#59)
1 parent 0994ace commit fd61cb2

File tree

10 files changed

+1435
-4
lines changed

10 files changed

+1435
-4
lines changed

Diff for: app/dashboard/EditProfile/EditProfile.tsx

+394
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
import {
2+
View,
3+
Text,
4+
TextInput,
5+
ScrollView,
6+
Image,
7+
TouchableOpacity,
8+
useColorScheme,
9+
ActivityIndicator,
10+
} from 'react-native';
11+
import { Picker } from '@react-native-picker/picker';
12+
import { useToast } from 'react-native-toast-notifications';
13+
import { UPDATE_PROFILE } from '@/graphql/mutations/UpdateProfile.mutation';
14+
import { GET_PROFILE } from '@/graphql/queries/GetProfile';
15+
import { SvgXml } from 'react-native-svg';
16+
import { editPic } from '@/assets/Icons/dashboard/Icons';
17+
import { useState, useEffect, useCallback } from 'react';
18+
import { router } from 'expo-router';
19+
import { useFormik } from 'formik';
20+
import { COUNTRIES } from '@/constants/countries';
21+
import { useMutation, useQuery } from '@apollo/client';
22+
import Resume from '@/app/dashboard/Resume/Resume';
23+
import { EditProfileSchema } from '@/validations/editProfile.schema';
24+
import AsyncStorage from '@react-native-async-storage/async-storage';
25+
import { Ionicons } from '@expo/vector-icons';
26+
27+
type FormValues = {
28+
firstName: string;
29+
lastName: string;
30+
phoneNumber: string;
31+
githubUsername: string;
32+
address: string;
33+
city: string;
34+
country: string;
35+
biography: string;
36+
};
37+
38+
const EditProfile = () => {
39+
const [activeTab, setActiveTab] = useState<'EDITING PROFILE' | 'UPLOAD RESUME'>(
40+
'EDITING PROFILE'
41+
);
42+
const colorScheme = useColorScheme();
43+
const textColor = colorScheme === 'dark' ? 'text-gray-100' : 'text-gray-800';
44+
const bgColor = colorScheme === 'dark' ? 'bg-secondary-dark-900' : 'bg-secondary-light-300';
45+
const toast = useToast();
46+
const [Loading, setLoading] = useState(false);
47+
const [tab, setTab] = useState(0);
48+
const [profile, setProfile] = useState<any>({});
49+
const [userToken, setUserToken] = useState<string | null>(null);
50+
51+
const handleTabPress = (tab: 'EDITING PROFILE' | 'UPLOAD RESUME') => {
52+
setActiveTab(tab);
53+
setTab(tab === 'EDITING PROFILE' ? 0 : 1);
54+
};
55+
56+
useEffect(() => {
57+
const fetchToken = async () => {
58+
try {
59+
const token = await AsyncStorage.getItem('authToken');
60+
if (token) {
61+
setUserToken(token);
62+
} else {
63+
toast.show('Token Not found.', { type: 'danger', placement: 'top', duration: 3000 });
64+
}
65+
} catch (error) {
66+
toast.show('Failed to retrieve token.', {
67+
type: 'danger',
68+
placement: 'top',
69+
duration: 3000,
70+
});
71+
}
72+
};
73+
fetchToken();
74+
}, []);
75+
76+
const { data, loading, error } = useQuery(GET_PROFILE, {
77+
context: {
78+
headers: {
79+
Authorization: `Bearer ${userToken}`,
80+
},
81+
},
82+
skip: !userToken,
83+
});
84+
useEffect(() => {
85+
if (error) {
86+
toast.show('Error fetching profile.', { type: 'danger', placement: 'top', duration: 3000 });
87+
}
88+
}, [error]);
89+
90+
useEffect(() => {
91+
if (data) {
92+
setProfile(data.getProfile);
93+
}
94+
}, [data]);
95+
96+
const [UpdateProfile] = useMutation(UPDATE_PROFILE, {
97+
context: {
98+
headers: {
99+
Authorization: `Bearer ${userToken}`,
100+
},
101+
},
102+
update(cache, { data: { updateProfile } }) {
103+
cache.modify({
104+
fields: {
105+
getProfile(existingProfile = {}) {
106+
return { ...existingProfile, ...updateProfile };
107+
},
108+
},
109+
});
110+
},
111+
});
112+
113+
const formik = useFormik<FormValues>({
114+
initialValues: {
115+
firstName: profile.firstName,
116+
lastName: profile.lastName,
117+
phoneNumber: profile.phoneNumber,
118+
githubUsername: profile.githubUsername,
119+
address: profile.address,
120+
city: profile.city,
121+
country: profile.country,
122+
biography: profile.biography,
123+
},
124+
enableReinitialize: true,
125+
126+
onSubmit: async (values: FormValues) => {
127+
setLoading(true);
128+
try {
129+
const { data } = await UpdateProfile({
130+
variables: {
131+
...values,
132+
},
133+
});
134+
135+
if (data) {
136+
toast.show('Profile has been updated', {
137+
type: 'success',
138+
placement: 'top',
139+
duration: 4000,
140+
});
141+
router.push('/dashboard/trainee/Profile');
142+
}
143+
} catch (error) {
144+
toast.show('Failed to update profile', {
145+
type: 'danger',
146+
placement: 'top',
147+
duration: 4000,
148+
});
149+
} finally {
150+
setLoading(false);
151+
}
152+
},
153+
validationSchema: EditProfileSchema,
154+
});
155+
156+
if (loading) {
157+
return (
158+
<View className="flex justify-center items-center">
159+
<ActivityIndicator size="large" color="#fff" />
160+
</View>
161+
);
162+
}
163+
164+
if (error) {
165+
return (
166+
<View className="flex justify-center items-center">
167+
<Text className="text-red-500">Error fetching profile data</Text>
168+
</View>
169+
);
170+
}
171+
172+
return (
173+
<ScrollView className={`flex p-4 pb-8 bg-gray-900 ${bgColor}`}>
174+
<View className="relative pb-10">
175+
<Image
176+
source={
177+
profile.cover ? { uri: profile.cover } : require('@/assets/images/background.png')
178+
}
179+
className="w-full h-48"
180+
resizeMode="cover"
181+
/>
182+
<View className="absolute top-32 left-5 flex-row items-center">
183+
<View className="w-24 h-24 rounded-full bg-yellow-400">
184+
<Image
185+
source={
186+
profile.avatar
187+
? { uri: profile.avatar }
188+
: {
189+
uri: `https://ui-avatars.com/api/?name=${encodeURIComponent(profile.name)}&background=random&color=fff`,
190+
}
191+
}
192+
className="w-full h-full rounded-full"
193+
/>
194+
</View>
195+
<TouchableOpacity className="-ml-2">
196+
<SvgXml xml={editPic} />
197+
</TouchableOpacity>
198+
</View>
199+
</View>
200+
201+
<View className="flex-row justify-between mt-4">
202+
<TouchableOpacity
203+
className={`${bgColor} px-4 py-2 border-b-4 ${
204+
activeTab === 'EDITING PROFILE'
205+
? 'border-indigo-400 shadow-sm rounded-lg'
206+
: 'border-transparent shadow-sm rounded-lg'
207+
}`}
208+
onPress={() => handleTabPress('EDITING PROFILE')}
209+
>
210+
<Text className={`${textColor}`}>EDITING PROFILE</Text>
211+
</TouchableOpacity>
212+
<TouchableOpacity
213+
className={`${bgColor} px-4 py-2 border-b-4 ${
214+
activeTab === 'UPLOAD RESUME'
215+
? 'border-indigo-400 shadow-sm rounded-lg'
216+
: 'border-transparent shadow-sm rounded-lg'
217+
}`}
218+
onPress={() => handleTabPress('UPLOAD RESUME')}
219+
>
220+
<Text className={`${textColor}`}>UPLOAD RESUME</Text>
221+
</TouchableOpacity>
222+
</View>
223+
224+
{tab === 0 && (
225+
<View className={`bg-[#272728] px-4 pt-10 pb-6 rounded-lg ${bgColor} shadow-lg`}>
226+
<TouchableOpacity
227+
onPress={() => router.push('/dashboard/trainee/Profile')}
228+
className="flex-row items-center mb-4 bg-action-500 w-40 p-2 rounded-lg"
229+
>
230+
<Ionicons name="arrow-back" size={20} color={'white'} />
231+
<Text className="text-white text-lg ">Back to Profile</Text>
232+
</TouchableOpacity>
233+
234+
<View className="pb-4">
235+
<Text className={`${textColor} pb-2`}>First Name</Text>
236+
<View
237+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
238+
>
239+
<TextInput
240+
testID="first-name"
241+
className={`p-3 rounded-lg ${textColor}`}
242+
placeholder="First Name"
243+
placeholderTextColor="#888"
244+
onChangeText={formik.handleChange('firstName')}
245+
value={formik.values.firstName}
246+
/>
247+
</View>
248+
249+
<Text className="text-error-500 my-1">{formik.errors.firstName}</Text>
250+
251+
<Text className={`${textColor} pb-2`}>Last Name</Text>
252+
<View
253+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
254+
>
255+
<TextInput
256+
testID="last-name"
257+
className={`p-3 rounded-lg ${textColor}`}
258+
placeholder="Last Name"
259+
placeholderTextColor="#888"
260+
onChangeText={formik.handleChange('lastName')}
261+
value={formik.values.lastName}
262+
/>
263+
</View>
264+
<Text className="text-error-500 my-1">{formik.errors.lastName}</Text>
265+
266+
<Text className={`${textColor} pb-2`}>Telephone</Text>
267+
<View
268+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
269+
>
270+
<TextInput
271+
testID="telephone"
272+
className={`p-3 rounded-lg ${textColor}`}
273+
placeholder="Telephone"
274+
placeholderTextColor="#888"
275+
onChangeText={formik.handleChange('phoneNumber')}
276+
value={formik.values.phoneNumber}
277+
keyboardType="numeric"
278+
maxLength={13}
279+
/>
280+
</View>
281+
282+
<Text className={`${textColor} pb-2 pt-4`}>GitHub Username</Text>
283+
<View
284+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
285+
>
286+
<TextInput
287+
testID="githubUsername"
288+
className={`p-3 rounded-lg ${textColor}`}
289+
placeholder="GitHub Username"
290+
placeholderTextColor="#888"
291+
onChangeText={formik.handleChange('githubUsername')}
292+
value={formik.values.githubUsername}
293+
/>
294+
</View>
295+
296+
<View className="flex-row gap-4 pb-4 pt-4 w-full">
297+
<View className="flex w-[47%]">
298+
<Text className={`${textColor} pb-2`}>Address</Text>
299+
<View
300+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
301+
>
302+
<TextInput
303+
testID="address"
304+
className={`p-3 rounded-lg ${textColor} flex mr-2 `}
305+
placeholder="Address"
306+
placeholderTextColor="#888"
307+
onChangeText={formik.handleChange('address')}
308+
value={formik.values.address}
309+
/>
310+
</View>
311+
</View>
312+
313+
<View className="flex w-[49%]">
314+
<Text className={`${textColor} pb-2`}>City</Text>
315+
<View
316+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
317+
>
318+
<TextInput
319+
testID="city"
320+
className={`p-3 rounded-lg ${textColor} flex mr-2`}
321+
placeholder="City"
322+
placeholderTextColor="#888"
323+
onChangeText={formik.handleChange('city')}
324+
value={formik.values.city}
325+
/>
326+
</View>
327+
</View>
328+
</View>
329+
330+
<Text className={`${textColor} pb-2`}>Country</Text>
331+
332+
<View className={`${bgColor} rounded-lg shadow border-2 border-[#D2D2D2] mb-4`}>
333+
<Picker
334+
selectedValue={formik.values.country}
335+
onValueChange={(itemValue: string) => formik.setFieldValue('country', itemValue)}
336+
style={{ color: colorScheme === 'dark' ? '#F3F4F6' : '#1F2937' }}
337+
>
338+
<Picker.Item
339+
label="Select a country"
340+
value=""
341+
style={{ color: colorScheme === 'dark' ? '#F3F4F6' : '#1F2937' }}
342+
/>
343+
{COUNTRIES.map((country) => (
344+
<Picker.Item
345+
key={country.value}
346+
label={country.title}
347+
value={country.value}
348+
style={{ color: colorScheme === 'dark' ? '#1F2937' : '#1F2937' }}
349+
/>
350+
))}
351+
</Picker>
352+
</View>
353+
354+
<Text className={`${textColor} pb-2`}>Biography</Text>
355+
<View
356+
className={`flex-row items-center ${bgColor} rounded-lg shadow border-2 border-[#D2D2D2]`}
357+
>
358+
<TextInput
359+
testID="biography"
360+
className={`p-3 rounded-lg ${textColor} rounded-lg h-32`}
361+
placeholder="Biography"
362+
placeholderTextColor="#888"
363+
onChangeText={formik.handleChange('biography')}
364+
value={formik.values.biography}
365+
multiline
366+
style={{ textAlignVertical: 'top' }}
367+
/>
368+
</View>
369+
</View>
370+
371+
<TouchableOpacity
372+
testID="submit-button"
373+
className="bg-action-500 p-4 rounded-lg"
374+
onPress={() => formik.handleSubmit()}
375+
>
376+
{Loading ? (
377+
<ActivityIndicator color="#fff" />
378+
) : (
379+
<Text className="text-white text-center text-lg">Update Profile</Text>
380+
)}
381+
</TouchableOpacity>
382+
</View>
383+
)}
384+
385+
{tab === 1 && (
386+
<View>
387+
<Resume />
388+
</View>
389+
)}
390+
</ScrollView>
391+
);
392+
};
393+
394+
export default EditProfile;

0 commit comments

Comments
 (0)