diff --git a/package.json b/package.json
index 1288012..ec3c6a3 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"version": "0.0.1",
"type": "module",
"scripts": {
- "dev": "PORT=5173 next dev ",
+ "dev": "set PORT=5173 && next dev ",
"build": "next build",
"lint": "next lint",
"mock": "node ./mock/index.js",
diff --git a/src/app/(auth)/login/_components/LogInForm.tsx b/src/app/(auth)/login/_components/LogInForm.tsx
index 6b936f4..2a7fc7c 100644
--- a/src/app/(auth)/login/_components/LogInForm.tsx
+++ b/src/app/(auth)/login/_components/LogInForm.tsx
@@ -5,6 +5,7 @@ import { useForm, Controller } from 'react-hook-form';
import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon';
import { Alert } from 'react-bootstrap';
import Link from 'next/link';
+import { useRouter } from 'next/navigation';
import PasswordField from '@/shared/components/form/Password';
import {
FormGroup,
@@ -35,6 +36,10 @@ const LogInForm = ({ onSubmit, error = '' }: LogInFormProps) => {
formState: { errors },
} = useForm();
const rememberMe = watch('rememberMe');
+ const router = useRouter();
+ const handleRegisterButtonClick = () => {
+ router.push('/register');
+ };
useEffect(() => {
if (rememberMe !== undefined && typeof window !== 'undefined') {
@@ -122,8 +127,8 @@ const LogInForm = ({ onSubmit, error = '' }: LogInFormProps) => {
Sign In
-
- Create Account
+
+ Create Account
);
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index 791585c..10b8cba 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -21,6 +21,7 @@ import { USER_LOGIN } from '@/graphql/auth';
import { AUTH_TOKEN, EMAIL } from '@/shared/constants/storage';
import { useSearchParams } from '@/hooks/useSearchParams';
import { useRouter } from 'next/navigation';
+import Loading from '@/shared/components/Loading';
import LogInForm from './_components/LogInForm';
const Login = () => {
@@ -28,8 +29,10 @@ const Login = () => {
const [error, setError] = useState('');
const originUrl = useSearchParams().get('orgUrl');
const [login] = useMutation(USER_LOGIN);
+ const [isLoading, setIsLoading] = useState(false);
const onSubmit = async (data: { email: string; password: string; remember_me: boolean }) => {
+ setIsLoading(true);
const result = await login({
variables: data,
});
@@ -56,42 +59,47 @@ const Login = () => {
} else {
// for login failed
setError(`Login failed: ${result.data.login.message}`);
+ setIsLoading(false);
}
};
return (
-
-
-
-
- Welcome to
-
-
- BeeQuant
- AI
-
-
- Trading smart, trading with BeeQuant AI
-
-
-
- Or Easily Using
-
-
- {/* @ts-ignore - Ignoring because of complex union types incorrectly inferred */}
-
-
-
-
-
-
-
-
-
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+ Welcome to
+
+
+ BeeQuant
+ AI
+
+
+ Trading smart, trading with BeeQuant AI
+
+
+
+ Or Easily Using
+
+
+ {/* @ts-ignore - Ignoring because of complex union types incorrectly inferred */}
+
+
+
+
+
+
+
+
+
+ )}
);
};
diff --git a/src/app/(auth)/register/_components/RegisterSuccess.test.tsx b/src/app/(auth)/register/_components/RegisterSuccess.test.tsx
index e2e8577..909d48c 100644
--- a/src/app/(auth)/register/_components/RegisterSuccess.test.tsx
+++ b/src/app/(auth)/register/_components/RegisterSuccess.test.tsx
@@ -1,12 +1,17 @@
-import { screen, render } from '@testing-library/react';
+import { screen, render, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { useUserContext } from '@/hooks/userHooks';
+import { useRouter } from 'next/navigation';
import RegisterSuccess from './RegisterSuccess';
jest.mock('@/containers/Layout/topbar/BasicTopbarComponents', () => ({
TopbarDownIcon: () => Mocked TopbarDownIcon
,
}));
+jest.mock('next/navigation', () => ({
+ useRouter: jest.fn(),
+}));
+
const mockLocalStorage = (() => {
let store: Record = {};
return {
@@ -26,6 +31,15 @@ Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
});
+interface MockAppRouterInstance {
+ push: jest.Mock;
+ back?: jest.Mock;
+ forward?: jest.Mock;
+ refresh?: jest.Mock;
+ replace?: jest.Mock;
+ prefetch?: jest.Mock;
+}
+
jest.mock('@/hooks/userHooks', () => ({
useUserContext: jest.fn(),
}));
@@ -52,7 +66,7 @@ describe('RegisterSuccess component', () => {
);
- const successMessage = screen.getByText(/Your registration is successful/i);
+ const successMessage = screen.getByText(/We have sent you a verification email/i);
expect(successMessage).toBeInTheDocument();
});
@@ -66,14 +80,25 @@ describe('RegisterSuccess component', () => {
expect(image).toBeInTheDocument();
});
- it('should render button with correct link', () => {
- const { getByText } = render(
-
-
-
+ it('triggers router push on button click', async () => {
+ const pushMock = jest.fn();
+
+ const mockRouterInstance: MockAppRouterInstance = {
+ push: pushMock,
+ back: jest.fn(),
+ forward: jest.fn(),
+ refresh: jest.fn(),
+ replace: jest.fn(),
+ prefetch: jest.fn(),
+ };
+
+ (useRouter as jest.MockedFunction).mockReturnValue(
+ mockRouterInstance as unknown as ReturnType
);
- const button = getByText('Back to Login');
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute('href', '/login');
+ const { getByText } = render();
+ fireEvent.click(getByText('Finished sign-up. Ready to trade'));
+ await waitFor(() => {
+ expect(pushMock).toHaveBeenCalledWith('/login');
+ });
});
});
diff --git a/src/app/(auth)/register/_components/RegisterSuccess.tsx b/src/app/(auth)/register/_components/RegisterSuccess.tsx
index c87937e..c835345 100644
--- a/src/app/(auth)/register/_components/RegisterSuccess.tsx
+++ b/src/app/(auth)/register/_components/RegisterSuccess.tsx
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import styled from 'styled-components';
-import Link from 'next/link';
+import { useRouter } from 'next/navigation';
import {
AccountButton,
AccountCard,
@@ -12,30 +12,40 @@ import {
AccountWrap,
} from '@/shared/components/account/AccountElements';
-const RegisterSuccess = () => (
-
-
-
-
-
-
-
-
- Congratulations !
-
-
-
- Your registration is successful
-
-
- {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */}
-
- Back to Login
-
-
-
-
-);
+const RegisterSuccess = () => {
+ const router = useRouter();
+ const handleFinishedButtonClick = () => {
+ router.push('/login');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Please check your mailbox
+
+
+
+ We have sent you a verification email
+
+
+ {/*
+ @ts-ignore
+ - Ignoring because of complex union types that are not correctly inferred
+ */}
+
+ Finished sign-up. Ready to trade
+
+
+
+
+ );
+};
export default RegisterSuccess;
diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx
index 3c05641..4ea2a1a 100644
--- a/src/app/(auth)/register/page.tsx
+++ b/src/app/(auth)/register/page.tsx
@@ -15,11 +15,13 @@ import { useMutation } from '@apollo/client';
import { USER_REGISTER } from '@/graphql/auth';
import { useTitle } from '@/hooks/useTitle';
import Link from 'next/link';
+import Loading from '@/shared/components/Loading';
import RegisterForm from './_components/RegisterForm';
import RegisterSuccess from './_components/RegisterSuccess';
const Register = () => {
const [register] = useMutation(USER_REGISTER);
+ const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [isRegistered, setIsRegistered] = useState(false);
@@ -31,6 +33,7 @@ const Register = () => {
displayName: string;
ref: string;
}) => {
+ setIsLoading(true);
const result = await register({
variables: {
input: data,
@@ -39,15 +42,21 @@ const Register = () => {
if (result.data.register.code === 200) {
setIsRegistered(true);
+ setIsLoading(false);
}
// for register failed
setError(`Register failed: ${result.data.register.message}`);
+ setIsLoading(false);
};
if (isRegistered) {
return ;
}
+ if (isLoading) {
+ return ;
+ }
+
return (
diff --git a/src/app/verify-email/_components/VerifyFail.test.tsx b/src/app/verify-email/_components/VerifyFail.test.tsx
new file mode 100644
index 0000000..6fbeb7d
--- /dev/null
+++ b/src/app/verify-email/_components/VerifyFail.test.tsx
@@ -0,0 +1,71 @@
+import { screen, render, fireEvent, waitFor } from '@testing-library/react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { useRouter } from 'next/navigation';
+import VerifyFail from './VerifyFail';
+
+jest.mock('next/navigation', () => ({
+ useRouter: jest.fn(),
+}));
+
+interface MockAppRouterInstance {
+ push: jest.Mock;
+ back?: jest.Mock;
+ forward?: jest.Mock;
+ refresh?: jest.Mock;
+ replace?: jest.Mock;
+ prefetch?: jest.Mock;
+}
+
+describe('VerifyFail component', () => {
+ it('should render successfully and show verification failed message', () => {
+ render(
+
+
+
+ );
+
+ const errorMessage = screen.getByText(/Link expired/i);
+ expect(errorMessage).toBeInTheDocument();
+
+ const titleMessage = screen.getByText(/Verification failed/i);
+ expect(titleMessage).toBeInTheDocument();
+ });
+
+ it('should render image', () => {
+ render(
+
+
+
+ );
+ const image = screen.getByAltText('404');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('triggers router push on button click', async () => {
+ const pushMock = jest.fn();
+
+ const mockRouterInstance: MockAppRouterInstance = {
+ push: pushMock,
+ back: jest.fn(),
+ forward: jest.fn(),
+ refresh: jest.fn(),
+ replace: jest.fn(),
+ prefetch: jest.fn(),
+ };
+
+ (useRouter as jest.MockedFunction).mockReturnValue(
+ mockRouterInstance as unknown as ReturnType
+ );
+
+ render(
+
+
+
+ );
+
+ fireEvent.click(screen.getByText('Back to Login'));
+ await waitFor(() => {
+ expect(pushMock).toHaveBeenCalledWith('/login');
+ });
+ });
+});
diff --git a/src/app/verify-email/_components/VerifyFail.tsx b/src/app/verify-email/_components/VerifyFail.tsx
new file mode 100644
index 0000000..3012850
--- /dev/null
+++ b/src/app/verify-email/_components/VerifyFail.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+import styled from 'styled-components';
+import { useRouter } from 'next/navigation';
+import {
+ AccountButton,
+ AccountCard,
+ AccountContent,
+ AccountHead,
+ AccountLogo,
+ AccountLogoError,
+ AccountTitle,
+ AccountWrap,
+} from '@/shared/components/account/AccountElements';
+
+const VerifyFail = ({ error }: { error: string }) => {
+ const router = useRouter();
+ const handleBackToLoginButtonClick = () => {
+ router.push('/login');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Opps !
+
+ Verification failed
+
+
+
+ {error}
+
+
+ {/*
+ @ts-ignore
+ - Ignoring because of complex union types that are not correctly inferred
+ */}
+
+ Back to Login
+
+
+
+
+ );
+};
+
+export default VerifyFail;
+
+// region STYLES
+
+const AccountImage = styled.img`
+ max-width: 500px;
+ width: 100%;
+ margin-bottom: 40px;
+`;
+
+// endregion
diff --git a/src/app/verify-email/_components/VerifySuccess.test.tsx b/src/app/verify-email/_components/VerifySuccess.test.tsx
new file mode 100644
index 0000000..f4eaab7
--- /dev/null
+++ b/src/app/verify-email/_components/VerifySuccess.test.tsx
@@ -0,0 +1,65 @@
+import { screen, render, fireEvent, waitFor } from '@testing-library/react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { useRouter } from 'next/navigation';
+import VerifySuccess from './VerifySuccess';
+
+jest.mock('next/navigation', () => ({
+ useRouter: jest.fn(),
+}));
+
+describe('VerifySuccess component', () => {
+ it('should render successfully and show verification success message', () => {
+ render(
+
+
+
+ );
+
+ const titleMessage1 = screen.getByText(/Congratulations !/i);
+ expect(titleMessage1).toBeInTheDocument();
+
+ const titleMessage2 = screen.getByText(/Email Verified/i);
+ expect(titleMessage2).toBeInTheDocument();
+
+ const successMessage = screen.getByText(/Your registration is successful/i);
+ expect(successMessage).toBeInTheDocument();
+ });
+
+ it('should render image', () => {
+ render(
+
+
+
+ );
+ const image = screen.getByAltText('success');
+ expect(image).toBeInTheDocument();
+ });
+
+ it('triggers router push on button click', async () => {
+ const pushMock = jest.fn();
+
+ const mockRouterInstance = {
+ push: pushMock,
+ back: jest.fn(),
+ forward: jest.fn(),
+ refresh: jest.fn(),
+ replace: jest.fn(),
+ prefetch: jest.fn(),
+ };
+
+ (useRouter as jest.MockedFunction).mockReturnValue(
+ mockRouterInstance as unknown as ReturnType
+ );
+
+ render(
+
+
+
+ );
+
+ fireEvent.click(screen.getByText('Back to Login'));
+ await waitFor(() => {
+ expect(pushMock).toHaveBeenCalledWith('/login');
+ });
+ });
+});
diff --git a/src/app/verify-email/_components/VerifySuccess.tsx b/src/app/verify-email/_components/VerifySuccess.tsx
new file mode 100644
index 0000000..1656ba4
--- /dev/null
+++ b/src/app/verify-email/_components/VerifySuccess.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+import styled from 'styled-components';
+import { useRouter } from 'next/navigation';
+import {
+ AccountButton,
+ AccountCard,
+ AccountContent,
+ AccountHead,
+ AccountLogo,
+ AccountLogoAccent,
+ AccountTitle,
+ AccountWrap,
+} from '@/shared/components/account/AccountElements';
+
+const VerifySuccess = () => {
+ const router = useRouter();
+ const handleBackToLoginButtonClick = () => {
+ router.push('/login');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ Congratulations !
+
+ Email Verified
+
+
+
+ Your registration is successful
+
+
+ {/*
+ @ts-ignore
+ - Ignoring because of complex union types that are not correctly inferred
+ */}
+
+ Back to Login
+
+
+
+
+ );
+};
+
+export default VerifySuccess;
+
+// region STYLES
+
+const AccountImage = styled.img`
+ max-width: 500px;
+ width: 100%;
+ margin-bottom: 40px;
+`;
+
+// endregion
diff --git a/src/app/verify-email/page.test.tsx b/src/app/verify-email/page.test.tsx
new file mode 100644
index 0000000..b15f9a6
--- /dev/null
+++ b/src/app/verify-email/page.test.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { MockedProvider } from '@apollo/client/testing';
+import { VERIFY_EMAIL } from '@/graphql/verifyEmail';
+import Page from './page';
+
+jest.mock('next/navigation', () => ({
+ ...jest.requireActual('next/navigation'),
+ useSearchParams: jest.fn(() => ({
+ get: jest.fn((key) => {
+ if (key === 'email') return 'test@example.com';
+ if (key === 'token') return 'testtoken123';
+ return null;
+ }),
+ })),
+ useRouter: jest.fn(),
+}));
+
+const mockMutationResult = (code: number) => ({
+ data: {
+ verifyEmail: {
+ code,
+ message: code === 200 ? 'Your registration is successful' : 'Verification failed',
+ },
+ },
+});
+
+describe('Email Verification Page', () => {
+ it('should render success message on successful email verification', async () => {
+ const mocks = [
+ {
+ request: {
+ query: VERIFY_EMAIL,
+ variables: {
+ email: 'test@example.com',
+ token: 'testtoken123',
+ },
+ },
+ result: mockMutationResult(200),
+ },
+ ];
+
+ const { getByText } = render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(getByText('Your registration is successful')).toBeInTheDocument();
+ });
+ });
+
+ it('should render failure message on failed email verification', async () => {
+ const failMocks = [
+ {
+ request: {
+ query: VERIFY_EMAIL,
+ variables: {
+ email: 'test@example.com',
+ token: 'testtoken123',
+ },
+ },
+ result: mockMutationResult(10008),
+ },
+ ];
+ const { getByText } = render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(getByText('Verification failed')).toBeInTheDocument();
+ });
+ });
+
+ it('should render error message if an error occurs during verification', async () => {
+ const errorMocks = [
+ {
+ request: {
+ query: VERIFY_EMAIL,
+ variables: {
+ email: 'test@example.com',
+ token: 'testtoken123',
+ },
+ },
+ error: new Error('An error occurred'),
+ },
+ ];
+
+ const { getByText } = render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(getByText('An error occurred')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/app/verify-email/page.tsx b/src/app/verify-email/page.tsx
new file mode 100644
index 0000000..c3dd6f3
--- /dev/null
+++ b/src/app/verify-email/page.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import { useState, useEffect, Suspense } from 'react';
+import { useSearchParams } from 'next/navigation';
+import { VERIFY_EMAIL } from '@/graphql/verifyEmail';
+import { useMutation } from '@apollo/client';
+import Loading from '@/shared/components/Loading';
+import VerifySuccess from './_components/VerifySuccess';
+import VerifyFail from './_components/VerifyFail';
+
+const Search = () => {
+ const searchParams = useSearchParams();
+ const email = searchParams.get('email');
+ const token = searchParams.get('token');
+ const [verifyEmail] = useMutation(VERIFY_EMAIL);
+ const [mutationError, setMutationError] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [isVerified, setIsVerified] = useState(false);
+
+ useEffect(() => {
+ const fetchAndVerifyEmail = async () => {
+ try {
+ const result = await verifyEmail({
+ variables: {
+ email,
+ token,
+ },
+ });
+
+ if (result.data.verifyEmail.code === 200) {
+ setIsVerified(true);
+ setIsLoading(false);
+ } else {
+ setMutationError(`${result.data.verifyEmail.message}`);
+ setIsLoading(false);
+ }
+ } catch (e) {
+ setMutationError('An error occurred');
+ setIsLoading(false);
+ }
+ };
+
+ fetchAndVerifyEmail();
+ }, []);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (isVerified) {
+ return ;
+ }
+
+ return ;
+};
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
diff --git a/src/graphql/verifyEmail.ts b/src/graphql/verifyEmail.ts
new file mode 100644
index 0000000..6ad8099
--- /dev/null
+++ b/src/graphql/verifyEmail.ts
@@ -0,0 +1,10 @@
+import { gql } from '@apollo/client';
+
+export const VERIFY_EMAIL = gql`
+ mutation VerifyEmail($email: String!, $token: String!) {
+ verifyEmail(email: $email, token: $token) {
+ code
+ message
+ }
+ }
+`;
diff --git a/src/hooks/userHooks.ts b/src/hooks/userHooks.ts
index 89a4580..1501da7 100644
--- a/src/hooks/userHooks.ts
+++ b/src/hooks/userHooks.ts
@@ -31,7 +31,12 @@ export const useLoadUser = () => {
displayName,
refetchHandler: refetch,
});
- if (pathName.match('/login') && typeof window !== 'undefined') {
+ if (
+ pathName.match('/login') &&
+ pathName.match('/register') &&
+ !pathName.match('/verify-email') &&
+ typeof window !== 'undefined'
+ ) {
router.push('/dashboard');
}
}
@@ -41,7 +46,12 @@ export const useLoadUser = () => {
refetchHandler: refetch,
});
console.error('failed retrieving user info, backing to login');
- if (!pathName.match('/login') && typeof window !== 'undefined') {
+ if (
+ !pathName.match('/login') &&
+ !pathName.match('/register') &&
+ !pathName.match('/verify-email') &&
+ typeof window !== 'undefined'
+ ) {
router.push(`/login?orgUrl=${pathName}`);
}
},
diff --git a/src/shared/components/account/AccountElements.tsx b/src/shared/components/account/AccountElements.tsx
index dc32112..a0db4f4 100644
--- a/src/shared/components/account/AccountElements.tsx
+++ b/src/shared/components/account/AccountElements.tsx
@@ -8,6 +8,7 @@ import {
colorBlueHover,
colorDarkRed,
colorDustyWhite,
+ colorLightRed,
colorLightText,
colorVeryLightRed,
colorWhite,
@@ -73,6 +74,10 @@ export const AccountLogoAccent = styled.span`
color: ${colorBlue};
`;
+export const AccountLogoError = styled.span`
+ color: ${colorLightRed};
+`;
+
export const AccountOr = styled.div`
text-align: center;
margin-top: 35px;