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.
+
+
+
+
+ >
+ );
+}
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 (