diff --git a/package.json b/package.json index 6e0d6dec8..0ec606dec 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "image-size": "^1.1.1", "jose": "^5.2.2", "js-cookie": "^3.0.5", - "lucide-react": "^0.334.0", + "lucide-react": "^0.358.0", "nanoid": "^5.0.5", "nanostores": "^0.9.5", "node-html-parser": "^6.1.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08ac06715..742114163 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,8 +60,8 @@ dependencies: specifier: ^3.0.5 version: 3.0.5 lucide-react: - specifier: ^0.334.0 - version: 0.334.0(react@18.2.0) + specifier: ^0.358.0 + version: 0.358.0(react@18.2.0) nanoid: specifier: ^5.0.5 version: 5.0.5 @@ -4236,8 +4236,8 @@ packages: engines: {node: '>=12'} dev: false - /lucide-react@0.334.0(react@18.2.0): - resolution: {integrity: sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==} + /lucide-react@0.358.0(react@18.2.0): + resolution: {integrity: sha512-rBSptRjZTMBm24zsFhR6pK/NgbT18JegZGKcH4+1H3+UigMSRpeoWLtR/fAwMYwYnlJOZB+y8WpeHne9D6X6Kg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 dependencies: diff --git a/src/components/AuthenticationFlow/TriggerVerifyEmail.tsx b/src/components/AuthenticationFlow/TriggerVerifyEmail.tsx new file mode 100644 index 000000000..9baaac6af --- /dev/null +++ b/src/components/AuthenticationFlow/TriggerVerifyEmail.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react'; +import { httpPatch } from '../../lib/http'; +import { setAuthToken } from '../../lib/jwt'; +import { Spinner } from '../ReactIcons/Spinner'; +import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2'; +import { getUrlParams } from '../../lib/browser'; +import { CheckIcon } from '../ReactIcons/CheckIcon'; + +export function TriggerVerifyEmail() { + const { code } = getUrlParams() as { code: string }; + + // const [isLoading, setIsLoading] = useState(true); + const [status, setStatus] = useState<'loading' | 'error' | 'success'>( + 'loading', + ); + const [error, setError] = useState(''); + + const triggerVerify = (code: string) => { + setStatus('loading'); + + httpPatch<{ token: string }>( + `${import.meta.env.PUBLIC_API_URL}/v1-verify-new-email/${code}`, + {}, + ) + .then(({ response, error }) => { + if (!response?.token) { + setError(error?.message || 'Something went wrong. Please try again.'); + setStatus('error'); + + return; + } + + setAuthToken(response.token); + setStatus('success'); + }) + .catch((err) => { + setStatus('error'); + setError('Something went wrong. Please try again.'); + }); + }; + + useEffect(() => { + if (!code) { + setStatus('error'); + setError('Something went wrong. Please try again later.'); + return; + } + + triggerVerify(code); + }, [code]); + + const isLoading = status === 'loading'; + if (status === 'success') { + return ( +
+ +

+ Email Update Successful +

+

+ Your email has been changed successfully. Happy learning! +

+
+ ); + } + + return ( +
+
+ {isLoading && } + {error && } +

+ Verifying your new Email +

+
+ {isLoading &&

Please wait while we verify your new Email..

} + {error &&

{error}

} +
+
+
+ ); +} diff --git a/src/components/ProfileSettings/ProfileSettingsPage.tsx b/src/components/ProfileSettings/ProfileSettingsPage.tsx new file mode 100644 index 000000000..d4203bd00 --- /dev/null +++ b/src/components/ProfileSettings/ProfileSettingsPage.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { UpdateEmailForm } from '../UpdateEmail/UpdateEmailForm'; +import UpdatePasswordForm from '../UpdatePassword/UpdatePasswordForm'; +import { pageProgressMessage } from '../../stores/page'; +import { httpGet } from '../../lib/http'; +import { useToast } from '../../hooks/use-toast'; + +export function ProfileSettingsPage() { + const toast = useToast(); + + const [authProvider, setAuthProvider] = useState(''); + const [currentEmail, setCurrentEmail] = useState(''); + const [newEmail, setNewEmail] = useState(''); + + const loadProfile = async () => { + const { error, response } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-me`, + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + + return; + } + + const { authProvider, email, newEmail } = response; + setAuthProvider(authProvider); + setCurrentEmail(email); + setNewEmail(newEmail || ''); + }; + + useEffect(() => { + loadProfile().finally(() => { + pageProgressMessage.set(''); + }); + }, []); + + return ( + <> + +
+ { + setNewEmail(newEmail); + loadProfile().finally(() => {}); + }} + onVerificationCancel={() => { + loadProfile().finally(() => {}); + }} + /> + + ); +} diff --git a/src/components/UpdateEmail/UpdateEmailForm.tsx b/src/components/UpdateEmail/UpdateEmailForm.tsx new file mode 100644 index 000000000..4fa2d41fc --- /dev/null +++ b/src/components/UpdateEmail/UpdateEmailForm.tsx @@ -0,0 +1,245 @@ +import { type FormEvent, useState } from 'react'; +import { httpPatch } from '../../lib/http'; +import { pageProgressMessage } from '../../stores/page'; +import { useToast } from '../../hooks/use-toast'; +import { cn } from '../../lib/classname'; +import { ArrowUpRight, X } from 'lucide-react'; + +type UpdateEmailFormProps = { + authProvider: string; + currentEmail: string; + newEmail?: string; + onSendVerificationCode?: (newEmail: string) => void; + onVerificationCancel?: () => void; +}; + +export function UpdateEmailForm(props: UpdateEmailFormProps) { + const { + authProvider, + currentEmail, + newEmail: defaultNewEmail = '', + onSendVerificationCode, + onVerificationCancel, + } = props; + const toast = useToast(); + + const [isLoading, setIsLoading] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(defaultNewEmail !== ''); + const [newEmail, setNewEmail] = useState(defaultNewEmail); + const [isResendDone, setIsResendDone] = useState(false); + + const handleSentVerificationCode = async (e: FormEvent) => { + e.preventDefault(); + if (!newEmail || !newEmail.includes('@') || isSubmitted) { + return; + } + + setIsLoading(true); + pageProgressMessage.set('Sending verification code'); + const { response, error } = await httpPatch( + `${import.meta.env.PUBLIC_API_URL}/v1-update-user-email`, + { email: newEmail }, + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + setIsLoading(false); + pageProgressMessage.set(''); + + return; + } + + pageProgressMessage.set(''); + setIsLoading(false); + setIsSubmitted(true); + onSendVerificationCode?.(newEmail); + }; + + const handleResendVerificationCode = async () => { + if (isResendDone) { + toast.error('You have already resent the verification code'); + return; + } + + setIsLoading(true); + pageProgressMessage.set('Resending verification code'); + const { response, error } = await httpPatch( + `${import.meta.env.PUBLIC_API_URL}/v1-resend-email-verification-code`, + { email: newEmail }, + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + setIsLoading(false); + pageProgressMessage.set(''); + + return; + } + + toast.success('Verification code has been resent'); + pageProgressMessage.set(''); + setIsResendDone(true); + setIsLoading(false); + }; + + const handleCancelEmailVerification = async () => { + setIsLoading(true); + pageProgressMessage.set('Cancelling email verification'); + const { response, error } = await httpPatch( + `${import.meta.env.PUBLIC_API_URL}/v1-cancel-email-verification`, + {}, + ); + + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); + setIsLoading(false); + pageProgressMessage.set(''); + + return; + } + + pageProgressMessage.set(''); + onVerificationCancel?.(); + setIsSubmitted(false); + setNewEmail(''); + setIsLoading(false); + }; + + if (authProvider && authProvider !== 'email') { + return ( +
+

Update Email

+

+ You have used {authProvider} when signing up. Please set your password + first. +

+ +
+ + +
+

+ Please set your password first to update your email. +

+
+ ); + } + + return ( + <> +
+

Update Email

+

+ Use the form below to update your email. +

+
+ +
+
+ + +
+
+
+ + + {isSubmitted && ( +
+ +
+ )} +
+ setNewEmail(e.target.value)} + disabled={isSubmitted} + /> + {!isSubmitted && ( + + )} + {isSubmitted && ( + <> + +
+ + A verification link has been sent to your{' '} + new email address. Please follow the instructions + in email to verify and update your email. + +
+ + )} +
+
+ + ); +} diff --git a/src/components/UpdatePassword/UpdatePasswordForm.tsx b/src/components/UpdatePassword/UpdatePasswordForm.tsx index ab65fd649..0509237b4 100644 --- a/src/components/UpdatePassword/UpdatePasswordForm.tsx +++ b/src/components/UpdatePassword/UpdatePasswordForm.tsx @@ -1,26 +1,28 @@ -import { type FormEvent, useEffect, useState } from 'react'; -import { httpGet, httpPost } from '../../lib/http'; -import { pageProgressMessage } from '../../stores/page'; +import { type FormEvent, useState } from 'react'; +import { httpPost } from '../../lib/http'; +import { useToast } from '../../hooks/use-toast'; + +type UpdatePasswordFormProps = { + authProvider: string; +}; + +export default function UpdatePasswordForm(props: UpdatePasswordFormProps) { + const { authProvider } = props; + + const toast = useToast(); -export default function UpdatePasswordForm() { - const [authProvider, setAuthProvider] = useState(''); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [newPasswordConfirmation, setNewPasswordConfirmation] = useState(''); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); - setError(''); - setSuccess(''); if (newPassword !== newPasswordConfirmation) { - setError('Passwords do not match'); + toast.error('Passwords do not match'); setIsLoading(false); return; @@ -32,50 +34,26 @@ export default function UpdatePasswordForm() { oldPassword: authProvider === 'email' ? currentPassword : 'social-auth', password: newPassword, confirmPassword: newPasswordConfirmation, - } + }, ); - if (error) { - setError(error.message || 'Something went wrong'); + if (error || !response) { + toast.error(error?.message || 'Something went wrong'); setIsLoading(false); return; } - setError(''); setCurrentPassword(''); setNewPassword(''); setNewPasswordConfirmation(''); - setSuccess('Password updated successfully'); - setIsLoading(false); - }; - - const loadProfile = async () => { - setIsLoading(true); - - const { error, response } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-me` - ); - - if (error || !response) { - setIsLoading(false); - setError(error?.message || 'Something went wrong'); - - return; - } - - const { authProvider } = response; - setAuthProvider(authProvider); - + toast.success('Password updated successfully'); setIsLoading(false); + setTimeout(() => { + window.location.reload(); + }, 1000); }; - useEffect(() => { - loadProfile().finally(() => { - pageProgressMessage.set(''); - }); - }, []); - return (
@@ -98,7 +76,7 @@ export default function UpdatePasswordForm() { type="password" name="current-password" id="current-password" - autoComplete={"current-password"} + autoComplete={'current-password'} className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-100" required minLength={6} @@ -122,7 +100,7 @@ export default function UpdatePasswordForm() { type="password" name="new-password" id="new-password" - autoComplete={"new-password"} + autoComplete={'new-password'} className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" required minLength={6} @@ -145,7 +123,7 @@ export default function UpdatePasswordForm() { name="new-password-confirmation" id="new-password-confirmation" className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1" - autoComplete={"new-password"} + autoComplete={'new-password'} required minLength={6} placeholder="Confirm New Password" @@ -156,19 +134,11 @@ export default function UpdatePasswordForm() { />
- {error && ( -

{error}

- )} - - {success && ( -

- {success} -

- )} -