Skip to content

Commit 1859a95

Browse files
committed
fix: discard changes modal and birthdate update
1 parent 64ee40d commit 1859a95

2 files changed

Lines changed: 104 additions & 68 deletions

File tree

src/__tests__/profile/edit_profile.test.tsx

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -349,35 +349,4 @@ describe('EditProfileScreen (adjusted)', () => {
349349
expect(mockGoBack).toHaveBeenCalled();
350350
});
351351
});
352-
353-
it('does not call handleFormSubmit when header button is pressed and form is invalid', async () => {
354-
(updateUserProfile as jest.Mock).mockResolvedValueOnce({});
355-
356-
const { getByTestId } = render(
357-
<NavigationContainer>
358-
<QueryClientProvider client={mockQueryClient}>
359-
<EditProfileScreen />
360-
</QueryClientProvider>
361-
</NavigationContainer>
362-
);
363-
364-
// Set invalid website
365-
await act(async () => {
366-
fireEvent.changeText(getByTestId('edit-website-input'), 'invalid-url');
367-
});
368-
369-
// Extract the latest headerRight button
370-
const headerOptions = mockSetOptions.mock.calls[mockSetOptions.mock.calls.length - 1][0];
371-
const headerRightButton = headerOptions.headerRight();
372-
373-
// The button should be disabled due to invalid form
374-
expect(headerRightButton.props.disabled).toBe(true);
375-
376-
// Try pressing it (won't call handleFormSubmit because disabled)
377-
await act(async () => {
378-
headerRightButton.props.onPress();
379-
});
380-
381-
expect(mockGoBack).not.toHaveBeenCalled();
382-
});
383352
});

src/screens/profile/EditProfileScreen.tsx

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,29 +46,27 @@ const EditProfileScreen = () => {
4646
const { user, updateUser } = useUserStore((state) => state);
4747
const [isLoading, setLoading] = useState(false);
4848
const [error, setError] = useState<string | null>(null);
49-
const [isError, setIsError] = useState<boolean>(false);
5049
const [websiteError, setWebsiteError] = useState<string | null>(null);
5150
const [birthDateError, setBirthDateError] = useState<string | null>(null);
5251

53-
const [birthDate, setBirthDate] = useState<string | null>(user.birthDate || null);
52+
const [birthDate, setBirthDate] = useState<string | null>(user?.birthDate || null);
5453
const [birthDateInput, setBirthDateInput] = useState<string>(
55-
formatBirthDate(user.birthDate) || ''
54+
formatBirthDate(user?.birthDate ?? null) || ''
5655
);
5756
const [showDatePicker, setShowDatePicker] = useState<boolean>(false);
5857

59-
const [avatarUrl, setAvatarUrl] = useState<string | null>(user.avatarUrl || null);
60-
const [bannerUrl, setBannerUrl] = useState<string | null>(user.bannerUrl || null);
58+
const [avatarUrl, setAvatarUrl] = useState<string | null>(user?.avatarUrl || null);
59+
const [bannerUrl, setBannerUrl] = useState<string | null>(user?.bannerUrl || null);
6160
const [isEditingBanner, setIsEditingBanner] = useState<boolean>(false);
6261
const [menuVisible, setMenuVisible] = useState<boolean>(false);
6362

6463
const [formDataChanged, setFormDataChanged] = useState<boolean>(false);
6564

6665
const [formData, setFormData] = useState({
67-
displayName: user.displayName || '',
68-
bio: user.bio || '',
69-
location: user.location || '',
70-
websiteUrl: user.websiteUrl || '',
71-
birthDate: user.birthDate || null,
66+
displayName: user?.displayName || '',
67+
bio: user?.bio || '',
68+
location: user?.location || '',
69+
websiteUrl: user?.websiteUrl || '',
7270
});
7371

7472
const openDatePicker = () => {
@@ -94,13 +92,15 @@ const EditProfileScreen = () => {
9492
return;
9593
}
9694
// format as YYYY-MM-DD
97-
const newBirthDate = selectedDate.toISOString().split('T')[0];
95+
const newBirthDate = selectedDate.toISOString().split('T')[0].trim();
9896
setBirthDateInput(newBirthDateInput);
9997
// update in date picker
10098
setBirthDate(newBirthDate);
10199
// update form data
102-
setFormData((prev) => ({ ...prev, birthDate }));
100+
// console.log('selected date:', newBirthDate);
101+
setFormData((prev) => ({ ...prev, birthDate: newBirthDate || '' }));
103102
setFormDataChanged(true);
103+
setBirthDateError(null);
104104
}
105105
};
106106

@@ -189,39 +189,34 @@ const EditProfileScreen = () => {
189189
else setWebsiteError(null);
190190
};
191191

192-
useEffect(() => {
193-
if (websiteError || birthDateError || error) setIsError(true);
194-
else setIsError(false);
195-
}, [websiteError, birthDateError, error]);
196-
197192
const handleFormSubmit = useCallback(async () => {
198-
if (isError) return;
193+
/// clear previous errors
194+
setError(null);
199195

200196
try {
201-
// clear any previous errors
202-
setError(null);
203-
setIsError(false);
204197
setLoading(true);
205198

206-
// to be implemented with mock later: updates to profile media
207-
208-
const request: UpdateProfileRequest = {
209-
displayName: formData.displayName || '', //fallback
210-
bio: formData.bio === '' ? null : formData.bio,
211-
location: formData.location === '' ? null : formData.location,
212-
websiteUrl: formData.websiteUrl === '' ? null : formData.websiteUrl,
213-
birthDate: formData.birthDate === '' ? null : formData.birthDate,
214-
};
215-
216199
if (formDataChanged) {
200+
// to be implemented with mock later: updates to profile media
201+
202+
const request: UpdateProfileRequest = {
203+
displayName: formData.displayName.trim() || '', //fallback
204+
bio: formData.bio.trim() === '' ? null : formData.bio,
205+
location: formData.location.trim() === '' ? null : formData.location,
206+
websiteUrl: formData.websiteUrl.trim() === '' ? null : formData.websiteUrl,
207+
birthDate: birthDate === '' ? null : birthDate,
208+
};
209+
210+
// console.log(request);
211+
// console.log(user);
217212
// call update profile
218213
await updateUserProfile(request);
219214

220215
// update user store
221216
updateUser({ ...request, avatarUrl, bannerUrl });
222217

223218
// update react query cache for profile screen
224-
queryClient.setQueryData(['profile', user.username], (user: UserProfile) => ({
219+
queryClient.setQueryData(['profile', user?.username], (user: UserProfile) => ({
225220
...user,
226221
...request,
227222
birthDate: birthDate,
@@ -236,8 +231,14 @@ const EditProfileScreen = () => {
236231
} catch {
237232
const errorMessage = 'Failed to update profile';
238233
setError(errorMessage);
234+
setFormDataChanged(false);
239235
} finally {
240236
setLoading(false);
237+
setError(null);
238+
setFormDataChanged(false);
239+
setLoading(false);
240+
setWebsiteError(null);
241+
setBirthDateError(null);
241242
setFormDataChanged(false);
242243
}
243244
}, [
@@ -250,9 +251,15 @@ const EditProfileScreen = () => {
250251
bannerUrl,
251252
formDataChanged,
252253
queryClient,
253-
isError,
254254
]);
255255

256+
const isSaveDisabled =
257+
isLoading ||
258+
formData.displayName.trim().length === 0 ||
259+
!!websiteError ||
260+
!!birthDateError ||
261+
!formDataChanged;
262+
256263
useEffect(() => {
257264
navigation.setOptions({
258265
headerRight: () => (
@@ -263,12 +270,23 @@ const EditProfileScreen = () => {
263270
onPress={() => {
264271
handleFormSubmit();
265272
}}
266-
disabled={isLoading || formData.displayName?.length === 0 || isError}
273+
disabled={isSaveDisabled}
267274
testID="save-profile-button"
268275
/>
269276
),
270277
});
271-
}, [navigation, handleFormSubmit, formData, isLoading, isError]);
278+
}, [navigation, handleFormSubmit, formData, isSaveDisabled]);
279+
280+
useEffect(() => {
281+
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
282+
if (!formDataChanged) return;
283+
284+
e.preventDefault();
285+
// Show warning modal
286+
setMenuVisible(true);
287+
});
288+
return unsubscribe;
289+
}, [navigation, formDataChanged]);
272290

273291
if (isLoading) {
274292
return (
@@ -325,7 +343,7 @@ const EditProfileScreen = () => {
325343
value={formData.displayName}
326344
label="Name"
327345
onChangeText={(value) => {
328-
setFormData((prev) => ({ ...prev, ['displayName']: value.trim() }));
346+
setFormData((prev) => ({ ...prev, ['displayName']: value }));
329347
setFormDataChanged(true);
330348
}}
331349
autoCapitalize="none"
@@ -335,7 +353,7 @@ const EditProfileScreen = () => {
335353
value={formData.bio}
336354
label="Bio"
337355
onChangeText={(value) => {
338-
setFormData((prev) => ({ ...prev, ['bio']: value.trim() }));
356+
setFormData((prev) => ({ ...prev, ['bio']: value }));
339357
setFormDataChanged(true);
340358
}}
341359
autoCapitalize="none"
@@ -415,6 +433,45 @@ const EditProfileScreen = () => {
415433
</Pressable>
416434
</Modal>
417435

436+
{/* show warning modal when navigating away with unsaved changes */}
437+
<Modal
438+
transparent
439+
visible={menuVisible}
440+
animationType="fade"
441+
onRequestClose={() => setMenuVisible(false)}
442+
>
443+
<Pressable onPress={() => setMenuVisible(false)} style={styles.overlay}>
444+
<View style={[styles.dropdown, styles.dropDownWarning]}>
445+
<Text
446+
className="text-bold text-lg my-2"
447+
style={[styles.dropdownText, styles.dropdownWarningTitleText]}
448+
>
449+
Edit Profile?
450+
</Text>
451+
<Text style={styles.dropdownText}>Discard changes?</Text>
452+
<View className="flex-row justify-end gap-4 mt-4">
453+
<Pressable
454+
style={styles.dropdownItem}
455+
onPress={() => {
456+
setMenuVisible(false);
457+
}}
458+
>
459+
<Text style={[styles.dropdownText, styles.dropdownWarningText]}>Cancel</Text>
460+
</Pressable>
461+
462+
<Pressable
463+
style={styles.dropdownItem}
464+
onPress={() => {
465+
navigation.goBack();
466+
}}
467+
>
468+
<Text style={[styles.dropdownText, styles.dropdownWarningText]}>Discard</Text>
469+
</Pressable>
470+
</View>
471+
</View>
472+
</Pressable>
473+
</Modal>
474+
418475
{Platform.OS === 'ios' && showDatePicker && (
419476
<Modal transparent animationType="slide" visible={showDatePicker}>
420477
<View className="flex-1 justify-end">
@@ -512,5 +569,15 @@ function getStyles(theme: 'dark' | 'light') {
512569
errorContainer: {
513570
color: colors[theme].destructive,
514571
},
572+
dropdownWarningText: {
573+
fontWeight: 'bold',
574+
},
575+
dropdownWarningTitleText: {
576+
fontWeight: 'bold',
577+
fontSize: 18,
578+
},
579+
dropDownWarning: {
580+
paddingHorizontal: 20,
581+
},
515582
});
516583
}

0 commit comments

Comments
 (0)