|
1 | 1 | <script lang="ts">
|
2 | 2 | import ErrorMessage from '$comp/ErrorMessage.svelte';
|
3 | 3 | import Loading from '$comp/Loading.svelte';
|
4 |
| - import { H3, Muted } from '$comp/typography'; |
| 4 | + import { A, H3, Muted, Small } from '$comp/typography'; |
5 | 5 | import * as Avatar from '$comp/ui/avatar';
|
6 | 6 | import * as Form from '$comp/ui/form';
|
7 | 7 | import { Input } from '$comp/ui/input';
|
8 | 8 | import { Separator } from '$comp/ui/separator';
|
9 | 9 | import { applyServerSideErrors } from '$features/shared/validation';
|
10 |
| - import { getMeQuery, mutateUser } from '$features/users/api.svelte'; |
| 10 | + import { getMeQuery, mutateEmailAddress, mutateUser } from '$features/users/api.svelte'; |
11 | 11 | import { getGravatarFromCurrentUser } from '$features/users/gravatar.svelte';
|
12 |
| - import { UpdateUser } from '$features/users/models'; |
13 |
| - import { ProblemDetails } from "@exceptionless/fetchclient"; |
| 12 | + import { UpdateUser, User } from '$features/users/models'; |
| 13 | + import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; |
14 | 14 | import { toast } from 'svelte-sonner';
|
15 | 15 | import { defaults, superForm } from 'sveltekit-superforms';
|
16 |
| - import SuperDebug from 'sveltekit-superforms'; |
17 | 16 | import { classvalidatorClient } from 'sveltekit-superforms/adapters';
|
18 | 17 | import { debounce } from 'throttle-debounce';
|
19 | 18 |
|
20 |
| - let toastId = $state<number|string>(); |
| 19 | + let toastId = $state<number | string>(); |
21 | 20 | const userResponse = getMeQuery();
|
| 21 | + const isEmailAddressVerified = $derived(userResponse.data?.is_email_address_verified ?? false); |
22 | 22 | const gravatar = getGravatarFromCurrentUser(userResponse);
|
23 | 23 | const updateUser = mutateUser({
|
24 | 24 | get id() {
|
25 | 25 | return userResponse.data?.id;
|
26 | 26 | }
|
27 | 27 | });
|
| 28 | + const updateEmailAddress = mutateEmailAddress({ |
| 29 | + get id() { |
| 30 | + return userResponse.data?.id; |
| 31 | + } |
| 32 | + }); |
| 33 | +
|
| 34 | + const updateEmailAddressForm = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(User)), { |
| 35 | + id: 'update-email-address', |
| 36 | + async onUpdate({ form, result }) { |
| 37 | + if (!form.valid) { |
| 38 | + return; |
| 39 | + } |
28 | 40 |
|
29 |
| - const form = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(UpdateUser)), { |
30 |
| - onError() { |
31 |
| - toastId = toast.error('An error occurred while saving your full name.'); |
| 41 | + toast.dismiss(toastId); |
| 42 | + try { |
| 43 | + await updateEmailAddress.mutateAsync(form.data); |
| 44 | + toastId = toast.success('Account updated successfully.'); |
| 45 | +
|
| 46 | + // HACK: This is to prevent sveltekit from stealing focus |
| 47 | + result.type = 'failure'; |
| 48 | + } catch (error: unknown) { |
| 49 | + if (error instanceof ProblemDetails) { |
| 50 | + applyServerSideErrors(form, error); |
| 51 | + result.status = error.status ?? 500; |
| 52 | + toastId = toast.error(form.message ?? 'Error saving email address. Please try again.'); |
| 53 | + } |
| 54 | + } |
32 | 55 | },
|
| 56 | + SPA: true, |
| 57 | + validators: classvalidatorClient(User) |
| 58 | + }); |
| 59 | +
|
| 60 | + const updateUserForm = superForm(defaults(userResponse.data ?? {}, classvalidatorClient(UpdateUser)), { |
| 61 | + id: 'update-user', |
33 | 62 | async onUpdate({ form, result }) {
|
34 | 63 | if (!form.valid) {
|
35 | 64 | return;
|
|
39 | 68 | try {
|
40 | 69 | form.data = await updateUser.mutateAsync(form.data);
|
41 | 70 | toastId = toast.success('Account updated successfully.');
|
| 71 | +
|
| 72 | + // HACK: This is to prevent sveltekit from stealing focus |
| 73 | + result.type = 'failure'; |
42 | 74 | } catch (error: unknown) {
|
43 | 75 | if (error instanceof ProblemDetails) {
|
44 |
| - result.status = error.status ?? 500; |
45 | 76 | applyServerSideErrors(form, error);
|
| 77 | + result.status = error.status ?? 500; |
| 78 | + toastId = toast.error(form.message ?? 'Error saving full name. Please try again.'); |
46 | 79 | }
|
47 |
| -
|
48 |
| - throw error; |
49 | 80 | }
|
50 | 81 | },
|
51 | 82 | SPA: true,
|
52 | 83 | validators: classvalidatorClient(UpdateUser)
|
53 | 84 | });
|
54 | 85 |
|
55 | 86 | $effect(() => {
|
56 |
| - if (userResponse.isSuccess && !$submitting) { |
57 |
| - form.reset({ data: userResponse.data }); |
| 87 | + if (!userResponse.isSuccess) { |
| 88 | + return; |
| 89 | + } |
| 90 | +
|
| 91 | + if (!$updateEmailAddressFormSubmitting && !$updateEmailAddressFormTainted) { |
| 92 | + updateEmailAddressForm.reset({ data: userResponse.data, keepMessage: true }); |
| 93 | + } |
| 94 | +
|
| 95 | + if (!$updateUserFormSubmitting && !$updateUserFormTainted) { |
| 96 | + updateUserForm.reset({ data: userResponse.data, keepMessage: true }); |
58 | 97 | }
|
59 | 98 | });
|
60 | 99 |
|
61 |
| - const { enhance, form: formData, message, submit, submitting, tainted } = form; |
62 |
| - const debouncedSubmit = debounce(1000, submit); |
| 100 | + const { |
| 101 | + enhance: updateEmailAddressFormEnhance, |
| 102 | + form: updateEmailAddressFormData, |
| 103 | + message: updateEmailAddressFormMessage, |
| 104 | + submit: updateEmailAddressFormSubmit, |
| 105 | + submitting: updateEmailAddressFormSubmitting, |
| 106 | + tainted: updateEmailAddressFormTainted |
| 107 | + } = updateEmailAddressForm; |
| 108 | + const debouncedUpdateEmailAddressFormSubmit = debounce(1000, updateEmailAddressFormSubmit); |
| 109 | +
|
| 110 | + const { |
| 111 | + enhance: updateUserFormEnhance, |
| 112 | + form: updateUserFormData, |
| 113 | + message: updateUserFormMessage, |
| 114 | + submit: updateUserFormSubmit, |
| 115 | + submitting: updateUserFormSubmitting, |
| 116 | + tainted: updateUserFormTainted |
| 117 | + } = updateUserForm; |
| 118 | + const debouncedUpdatedUserFormSubmit = debounce(1000, updateUserFormSubmit); |
| 119 | +
|
| 120 | + async function resendVerificationEmail() { |
| 121 | + toast.dismiss(toastId); |
| 122 | + const client = useFetchClient(); |
| 123 | + try { |
| 124 | + await client.get(`users/${userResponse.data?.id}/resend-verification-email`); |
| 125 | + toastId = toast.success('Please check your inbox for the verification email.'); |
| 126 | + } catch { |
| 127 | + toastId = toast.error('Error sending verification email. Please try again.'); |
| 128 | + } |
| 129 | + } |
63 | 130 | </script>
|
64 | 131 |
|
65 | 132 | <div class="space-y-6">
|
|
79 | 146 | </Avatar.Root>
|
80 | 147 | <Muted>Your avatar is generated by requesting a Gravatar image with the email address below.</Muted>
|
81 | 148 |
|
82 |
| - <SuperDebug data={$tainted} /> |
83 |
| - <SuperDebug data={$formData} /> |
84 |
| - <form use:enhance> |
85 |
| - <ErrorMessage message={$message}></ErrorMessage> |
86 |
| - <Form.Field {form} name="full_name"> |
| 149 | + <form use:updateUserFormEnhance> |
| 150 | + <Form.Field form={updateUserForm} name="full_name"> |
87 | 151 | <Form.Control let:attrs>
|
88 | 152 | <Form.Label>Full Name</Form.Label>
|
89 |
| - <Input {...attrs} bind:value={$formData.full_name} placeholder="Full Name" autocomplete="name" required oninput={debouncedSubmit} /> |
| 153 | + <Input |
| 154 | + {...attrs} |
| 155 | + bind:value={$updateUserFormData.full_name} |
| 156 | + placeholder="Full Name" |
| 157 | + autocomplete="name" |
| 158 | + required |
| 159 | + oninput={debouncedUpdatedUserFormSubmit} |
| 160 | + /> |
90 | 161 | </Form.Control>
|
91 | 162 | <Form.Description />
|
92 | 163 | <Form.FieldErrors />
|
| 164 | + <ErrorMessage message={$updateUserFormMessage}></ErrorMessage> |
93 | 165 | </Form.Field>
|
94 |
| - <!-- <Form.Field {form} name="email_address"> |
| 166 | + </form> |
| 167 | + <form use:updateEmailAddressFormEnhance> |
| 168 | + <Form.Field form={updateEmailAddressForm} name="email_address"> |
95 | 169 | <Form.Control let:attrs>
|
96 | 170 | <Form.Label>Email</Form.Label>
|
97 |
| - <Input {...attrs} bind:value={$formData.email_address} placeholder="Enter email address" autocomplete="email" required /> |
| 171 | + <Input |
| 172 | + {...attrs} |
| 173 | + bind:value={$updateEmailAddressFormData.email_address} |
| 174 | + placeholder="Enter email address" |
| 175 | + autocomplete="email" |
| 176 | + required |
| 177 | + oninput={debouncedUpdateEmailAddressFormSubmit} |
| 178 | + /> |
98 | 179 | </Form.Control>
|
99 | 180 | <Form.Description />
|
100 | 181 | <Form.FieldErrors />
|
101 |
| - </Form.Field> --> |
102 |
| - <!-- <Form.Button> |
103 |
| - {#if $submitting} |
104 |
| - <Loading class="mr-2" variant="secondary"></Loading> Saving... |
105 |
| - {:else} |
106 |
| - Save |
107 |
| - {/if} |
108 |
| - </Form.Button> --> |
| 182 | + <ErrorMessage message={$updateEmailAddressFormMessage}></ErrorMessage> |
| 183 | + </Form.Field> |
109 | 184 | </form>
|
| 185 | + {#if !isEmailAddressVerified} |
| 186 | + <Small> |
| 187 | + Email not verified. <A class="cursor-pointer" onclick={resendVerificationEmail}>Resend</A> verification email. |
| 188 | + </Small> |
| 189 | + {/if} |
110 | 190 | </div>
|
0 commit comments