From de55d465172c623a652ff849869765331078371b Mon Sep 17 00:00:00 2001 From: CliffDawen Date: Thu, 27 Jun 2024 10:53:59 +1000 Subject: [PATCH] feat: Connect exchange feature (exchange-key) added exchange platform page: - implemented feat: Connect exchange feature (exchange-key) added exchange platform page: - implemented adding exchange platform pages - Implemented unites for added pages - Updated graphql Resolve CP-18 --- .../crypto/exchange/exchangePlatforms.ts | 8 ++ src/app/(protected)/crypto/exchange/page.tsx | 4 +- src/graphql/codegen/gql.ts | 8 ++ src/graphql/codegen/graphql.ts | 117 +++++++++++++--- src/graphql/user.ts | 9 ++ .../protect/crypto/exchange/StepOne.test.tsx | 43 ++++++ .../protect/crypto/exchange/StepOne.tsx | 79 +++++++++++ .../crypto/exchange/StepThree.test.tsx | 33 +++++ .../protect/crypto/exchange/StepThree.tsx | 61 ++++++++ .../protect/crypto/exchange/StepTwo.test.tsx | 115 ++++++++++++++++ .../protect/crypto/exchange/StepTwo.tsx | 130 ++++++++++++++++++ .../crypto/exchange/WizardForm.test.tsx | 72 ++++++++++ .../protect/crypto/exchange/WizardForm.tsx | 68 +++++++++ .../components/form/WizardFormElements.tsx | 127 +++++++++++++++++ 14 files changed, 853 insertions(+), 21 deletions(-) create mode 100644 src/app/(protected)/crypto/exchange/exchangePlatforms.ts create mode 100644 src/module/protect/crypto/exchange/StepOne.test.tsx create mode 100644 src/module/protect/crypto/exchange/StepOne.tsx create mode 100644 src/module/protect/crypto/exchange/StepThree.test.tsx create mode 100644 src/module/protect/crypto/exchange/StepThree.tsx create mode 100644 src/module/protect/crypto/exchange/StepTwo.test.tsx create mode 100644 src/module/protect/crypto/exchange/StepTwo.tsx create mode 100644 src/module/protect/crypto/exchange/WizardForm.test.tsx create mode 100644 src/module/protect/crypto/exchange/WizardForm.tsx create mode 100644 src/shared/components/form/WizardFormElements.tsx diff --git a/src/app/(protected)/crypto/exchange/exchangePlatforms.ts b/src/app/(protected)/crypto/exchange/exchangePlatforms.ts new file mode 100644 index 0000000..760f6fe --- /dev/null +++ b/src/app/(protected)/crypto/exchange/exchangePlatforms.ts @@ -0,0 +1,8 @@ +export const exchangePlatformsGroup = [ + { + name: 'Radio button 1', + label: 'Binance', + radioValue: '1', + initialValue: '1', + }, +]; diff --git a/src/app/(protected)/crypto/exchange/page.tsx b/src/app/(protected)/crypto/exchange/page.tsx index 8c6fb97..8b67e6c 100644 --- a/src/app/(protected)/crypto/exchange/page.tsx +++ b/src/app/(protected)/crypto/exchange/page.tsx @@ -2,6 +2,7 @@ import { Col, Container, Row } from 'react-bootstrap'; import { useTitle } from '@/hooks/useTitle'; +import WizardForm from 'module/protect/crypto/exchange/WizardForm'; const CryptoExchanges = () => { useTitle('Exchanges - BeeQuant'); @@ -10,9 +11,10 @@ const CryptoExchanges = () => { -

Crypto Exchanges

+

Connect New Exchange

+ {}} />
); }; diff --git a/src/graphql/codegen/gql.ts b/src/graphql/codegen/gql.ts index 38cac59..37b2fd8 100644 --- a/src/graphql/codegen/gql.ts +++ b/src/graphql/codegen/gql.ts @@ -23,6 +23,8 @@ const documents = { types.GetUserByIdDocument, '\n mutation updateUser($id: String!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input)\n }\n': types.UpdateUserDocument, + '\n mutation CreateUserExchange($input: CreateUserExchangeInput!) {\n createUserExchange(input: $input) {\n code\n message\n }\n }\n': + types.CreateUserExchangeDocument, }; /** @@ -69,6 +71,12 @@ export function gql( export function gql( source: '\n mutation updateUser($id: String!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input)\n }\n' ): (typeof documents)['\n mutation updateUser($id: String!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input)\n }\n']; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql( + source: '\n mutation CreateUserExchange($input: CreateUserExchangeInput!) {\n createUserExchange(input: $input) {\n code\n message\n }\n }\n' +): (typeof documents)['\n mutation CreateUserExchange($input: CreateUserExchangeInput!) {\n createUserExchange(input: $input) {\n code\n message\n }\n }\n']; export function gql(source: string) { return (documents as any)[source] ?? {}; diff --git a/src/graphql/codegen/graphql.ts b/src/graphql/codegen/graphql.ts index d53c2b1..5d7e7ed 100644 --- a/src/graphql/codegen/graphql.ts +++ b/src/graphql/codegen/graphql.ts @@ -20,19 +20,28 @@ export type Scalars = { Float: { input: number; output: number }; /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */ DateTime: { input: any; output: any }; + /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ + JSON: { input: any; output: any }; }; -export type CreateExchangeKeyInput = { - /** Access key */ +/** Input type for creating a user-exchange connection for an exchange account */ +export type CreateUserExchangeInput = { + /** Access key for connecting to the exchange */ accessKey: Scalars['String']['input']; - /** Exchange name */ + /** Name of the exchange */ exchangeName: Scalars['String']['input']; - /** Remarks */ - remarks?: InputMaybe; - /** Secret key */ + /** Name of this new user-exchange connection */ + name: Scalars['String']['input']; + /** Secret key for connecting to the exchange */ secretKey: Scalars['String']['input']; }; +export type CreateUserExchangeResponse = { + __typename?: 'CreateUserExchangeResponse'; + code: Scalars['Int']['output']; + message?: Maybe; +}; + export type CreateUserInput = { /** User display name */ displayName: Scalars['String']['input']; @@ -56,25 +65,29 @@ export type ExchangeKey = { createdBy?: Maybe; deletedAt?: Maybe; deletedBy?: Maybe; - /** Exchange name */ - exchangeName: Scalars['String']['output']; id: Scalars['String']['output']; - /** Remarks */ - remarks: Scalars['String']['output']; /** Secret key */ secretKey: Scalars['String']['output']; updatedAt?: Maybe; updatedBy?: Maybe; }; +export type ExchangeType = { + __typename?: 'ExchangeType'; + /** Exchange ID */ + id: Scalars['String']['output']; + /** Exchange name */ + name: Scalars['String']['output']; +}; + export type Mutation = { __typename?: 'Mutation'; /** Change password */ changePassword: Result; - /** Create exchange key */ - createExchangeKey: Scalars['Boolean']['output']; /** Create new user */ createUser: Scalars['Boolean']['output']; + /** Create a user exchange connection */ + createUserExchange: CreateUserExchangeResponse; /** Hard delete an user */ deleteUser: Scalars['Boolean']['output']; /** Hard delete an user key */ @@ -93,14 +106,14 @@ export type MutationChangePasswordArgs = { input: UpdatePasswordInput; }; -export type MutationCreateExchangeKeyArgs = { - input: CreateExchangeKeyInput; -}; - export type MutationCreateUserArgs = { input: CreateUserInput; }; +export type MutationCreateUserExchangeArgs = { + input: CreateUserExchangeInput; +}; + export type MutationDeleteUserArgs = { id: Scalars['String']['input']; }; @@ -127,10 +140,14 @@ export type Query = { __typename?: 'Query'; /** Find exchange key by id */ getExchangeKeyById: ExchangeKey; + /** Get all exchanges */ + getExchanges: Array; /** Find user by email */ getUserByEmail: UserType; /** Find user by id */ getUserById: UserType; + /** Get balances for user exchange connections */ + getUserExchangeBalances: Array; /** Find user by context */ getUserInfo: UserType; /** Get all users */ @@ -159,10 +176,6 @@ export type Result = { export type UpdateExchangeKeyInput = { /** Access key */ accessKey: Scalars['String']['input']; - /** Exchange name */ - exchangeName: Scalars['String']['input']; - /** Remarks */ - remarks?: InputMaybe; /** Secret key */ secretKey: Scalars['String']['input']; }; @@ -187,6 +200,14 @@ export type UpdateUserInput = { ref?: InputMaybe; }; +export type UserExchangePublic = { + __typename?: 'UserExchangePublic'; + /** Balance data */ + balance?: Maybe; + /** The name of the exchange account connection provided by user */ + name: Scalars['String']['output']; +}; + export type UserType = { __typename?: 'UserType'; /** User display name */ @@ -256,6 +277,19 @@ export type UpdateUserMutationVariables = Exact<{ export type UpdateUserMutation = { __typename?: 'Mutation'; updateUser: boolean }; +export type CreateUserExchangeMutationVariables = Exact<{ + input: CreateUserExchangeInput; +}>; + +export type CreateUserExchangeMutation = { + __typename?: 'Mutation'; + createUserExchange: { + __typename?: 'CreateUserExchangeResponse'; + code: number; + message?: string | null; + }; +}; + export const LoginDocument = { kind: 'Document', definitions: [ @@ -478,3 +512,46 @@ export const UpdateUserDocument = { }, ], } as unknown as DocumentNode; +export const CreateUserExchangeDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'CreateUserExchange' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'CreateUserExchangeInput' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'createUserExchange' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'input' }, + value: { kind: 'Variable', name: { kind: 'Name', value: 'input' } }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'code' } }, + { kind: 'Field', name: { kind: 'Name', value: 'message' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; diff --git a/src/graphql/user.ts b/src/graphql/user.ts index 748b58a..10b2590 100644 --- a/src/graphql/user.ts +++ b/src/graphql/user.ts @@ -26,3 +26,12 @@ export const UPDATE_USER = gql(` updateUser(id: $id, input: $input) } `); + +export const CREATE_USER_EXCHANGE = gql(` + mutation CreateUserExchange($input: CreateUserExchangeInput!) { + createUserExchange(input: $input) { + code + message + } + } +`); diff --git a/src/module/protect/crypto/exchange/StepOne.test.tsx b/src/module/protect/crypto/exchange/StepOne.test.tsx new file mode 100644 index 0000000..680b9a5 --- /dev/null +++ b/src/module/protect/crypto/exchange/StepOne.test.tsx @@ -0,0 +1,43 @@ +// StepOne.test.js + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import StepOne from './StepOne'; +import { exchangePlatformsGroup } from '../../../../app/(protected)/crypto/exchange/exchangePlatforms'; +// Mock props +const defaultValues = {}; +const onSubmit = jest.fn(); + +describe('StepOne Component', () => { + beforeEach(() => { + render(); + onSubmit.mockClear(); + }); + + test('renders StepOne correctly', () => { + expect(screen.getByText('Select your exchange')).toBeInTheDocument(); + exchangePlatformsGroup.forEach((item) => { + expect(screen.getByLabelText(item.label)).toBeInTheDocument(); + }); + expect(screen.getByRole('button', { name: /Next/i })).toBeInTheDocument(); + }); + + test('submits the form with selected exchange', async () => { + fireEvent.click(screen.getByLabelText('Binance')); + + const radioButton = screen.getByLabelText('Binance') as HTMLInputElement; + console.log('Radio button checked state:', radioButton.checked); + expect(radioButton).toBeChecked(); + + await fireEvent.click(await screen.findByRole('button', { name: /Next/i })); + + expect(onSubmit).toHaveBeenCalledWith({ exchange: '1' }); + }); + + test('does not submit the form without selecting exchange', () => { + fireEvent.click(screen.getByRole('button', { name: /Next/i })); + + expect(onSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/module/protect/crypto/exchange/StepOne.tsx b/src/module/protect/crypto/exchange/StepOne.tsx new file mode 100644 index 0000000..c2176ae --- /dev/null +++ b/src/module/protect/crypto/exchange/StepOne.tsx @@ -0,0 +1,79 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { FormGroup, FormGroupField } from '@/shared/components/form/FormElements'; +import styled from 'styled-components'; +import { + WizardButtonToolbar, + WizardFormContainer, + WizardTitle, + StyledButton, + WizardLabel, +} from '@/shared/components/form/WizardFormElements'; +import { exchangePlatformsGroup } from '../../../../app/(protected)/crypto/exchange/exchangePlatforms'; + +interface StepOneProps { + onSubmit: (data: any) => void; + defaultValues: Record; +} + +const StepOne: React.FC = ({ onSubmit, defaultValues }) => { + const [formData, setFormData] = useState(defaultValues); + + useEffect(() => { + setFormData(defaultValues); + }, [defaultValues]); + + const handlePlatformSelect = (platform: string) => { + setFormData((prevData) => ({ + ...prevData, + exchange: platform, + })); + }; + + const handleNext = (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.exchange) { + return; + } + onSubmit(formData); + }; + + return ( + <> + + Select your exchange + +
+ {exchangePlatformsGroup.map((item) => ( + + + handlePlatformSelect(item.radioValue)} + /> + {item.label} + + + ))} +
+
+ + + Next + + +
+ + ); +}; + +export default StepOne; + +const Input = styled.input` + margin-right: 10px; + width: 20px; + height: 20px; +`; diff --git a/src/module/protect/crypto/exchange/StepThree.test.tsx b/src/module/protect/crypto/exchange/StepThree.test.tsx new file mode 100644 index 0000000..6ef9dcd --- /dev/null +++ b/src/module/protect/crypto/exchange/StepThree.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import StepThree from './StepThree'; +import { MockedProvider } from '@apollo/client/testing'; + +describe('StepThree Component', () => { + test('renders StepThree component correctly', () => { + render( + + + + ); + + // Check if the image is rendered + const image = screen.getByAltText(/success/i); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', '/img/success.png'); + + // Check if the title is rendered + const title = screen.getByText(/Your exchange key is added successfully/i); + expect(title).toBeInTheDocument(); + + // Check if the "Cool!" accent is rendered + const coolAccent = screen.getByText(/Cool!/i); + expect(coolAccent).toBeInTheDocument(); + + // Check if the button is rendered + const button = screen.getByRole('link', { name: /Back to Exchange Management/i }); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('href', '/crypto/exchange/details'); + }); +}); diff --git a/src/module/protect/crypto/exchange/StepThree.tsx b/src/module/protect/crypto/exchange/StepThree.tsx new file mode 100644 index 0000000..d65d637 --- /dev/null +++ b/src/module/protect/crypto/exchange/StepThree.tsx @@ -0,0 +1,61 @@ +'use client'; + +import Link from 'next/link'; +import styled from 'styled-components'; +import { Button } from '@/shared/components/Button'; +import { borderLeft, paddingLeft } from '@/styles/directions'; +import { colorBlue } from '@/styles/palette'; + +const StepThree = () => ( + + + + + Cool! +

Your exchange key is added successfully

+
+ +
+
+); + +export default StepThree; + +const StepThreeContainer = styled.div` + text-align: center; + height: 100%; + overflow: auto; + display: flex; + + button { + margin: 0; + } +`; + +const StepThreeContent = styled.div` + margin: auto; + display: flex; + flex-direction: column; + align-items: center; +`; + +const StepThreeImage = styled.img` + max-width: 500px; + width: 100%; +`; + +const StepThreeTitleHead = styled.div` + margin-bottom: 30px; + ${paddingLeft}: 20px; + ${borderLeft}: 4px solid ${colorBlue}; +`; + +const StepThreeTitleAccent = styled.span` + color: ${colorBlue}; + font-size: 24px; + align-self: flex-start; +`; + +// endregion diff --git a/src/module/protect/crypto/exchange/StepTwo.test.tsx b/src/module/protect/crypto/exchange/StepTwo.test.tsx new file mode 100644 index 0000000..1fca284 --- /dev/null +++ b/src/module/protect/crypto/exchange/StepTwo.test.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import StepTwo from './StepTwo'; +import { MockedProvider } from '@apollo/client/testing'; +import { CREATE_USER_EXCHANGE } from '@/graphql/user'; + +const defaultValues = { + exchange: '1', +}; + +const mocks = [ + { + request: { + query: CREATE_USER_EXCHANGE, + variables: { + input: { + name: 'Test Display Name', + exchangeName: 'binance', + accessKey: 'test-api-key', + secretKey: 'test-api-secret', + }, + }, + }, + result: { + data: { + createUserExchange: { + id: '1', + name: 'Test Display Name', + exchangeName: 'binance', + accessKey: 'test-api-key', + secretKey: 'test-api-secret', + }, + }, + }, + }, +]; + +describe('StepTwo Component', () => { + test('renders form controls', () => { + render( + + + + ); + + expect(screen.getByPlaceholderText(/Display name/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/API key/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/API secret/i)).toBeInTheDocument(); + expect(screen.getByText(/Back/i)).toBeInTheDocument(); + expect(screen.getByText(/Submit/i)).toBeInTheDocument(); + }); + + test('updates state on user input', () => { + render( + + + + ); + + fireEvent.change(screen.getByPlaceholderText(/Display name/i), { + target: { value: 'Test Display Name' }, + }); + fireEvent.change(screen.getByPlaceholderText(/API key/i), { + target: { value: 'test-api-key' }, + }); + fireEvent.change(screen.getByPlaceholderText(/API secret/i), { + target: { value: 'test-api-secret' }, + }); + + expect(screen.getByPlaceholderText(/Display name/i)).toHaveValue('Test Display Name'); + expect(screen.getByPlaceholderText(/API key/i)).toHaveValue('test-api-key'); + expect(screen.getByPlaceholderText(/API secret/i)).toHaveValue('test-api-secret'); + }); + + test('shows error message when fields are empty', async () => { + render( + + + + ); + + fireEvent.click(screen.getByText(/Submit/i)); + + await waitFor(() => { + expect(screen.getByText(/Display name cannot be null/i)).toBeInTheDocument(); + }); + }); + + test('submits the form correctly', async () => { + const onSubmit = jest.fn(); + + render( + + + + ); + + fireEvent.change(screen.getByPlaceholderText(/Display name/i), { + target: { value: 'Test Display Name' }, + }); + fireEvent.change(screen.getByPlaceholderText(/API key/i), { + target: { value: 'test-api-key' }, + }); + fireEvent.change(screen.getByPlaceholderText(/API secret/i), { + target: { value: 'test-api-secret' }, + }); + + fireEvent.click(screen.getByText(/Submit/i)); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/module/protect/crypto/exchange/StepTwo.tsx b/src/module/protect/crypto/exchange/StepTwo.tsx new file mode 100644 index 0000000..8103636 --- /dev/null +++ b/src/module/protect/crypto/exchange/StepTwo.tsx @@ -0,0 +1,130 @@ +'use client'; + +import React from 'react'; +import { FormGroup, FormGroupLabel } from '@/shared/components/form/FormElements'; +import { + WizardButtonToolbar, + WizardFormContainer, + WizardTitle, + StyledButton, +} from '@/shared/components/form/WizardFormElements'; +import { CREATE_USER_EXCHANGE } from '@/graphql/user'; +import { useMutation } from '@apollo/client'; +import { useState } from 'react'; +import styled from 'styled-components'; +import { exchangePlatformsGroup } from '../../../../app/(protected)/crypto/exchange/exchangePlatforms'; + +interface StepTwoProps { + onSubmit: (data: any) => void; + defaultValues: Record; + previousPage: () => void; +} + +const StepTwo: React.FC = ({ onSubmit, previousPage, defaultValues }) => { + const [message, setMessage] = useState(''); + + const [createUserExchange] = useMutation(CREATE_USER_EXCHANGE, { + onCompleted: (data) => { + onSubmit(data); + }, + onError: (error) => { + setMessage(`Error: ${error.message}`); + }, + }); + const [displayName, setDisplayName] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [apiSecret, setApiSecret] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const selectedExchange = defaultValues.exchange; + const exchangePlatforms = exchangePlatformsGroup.find( + (platform) => platform.radioValue === selectedExchange + ); + + if (displayName.trim() === '') { + setMessage('Display name cannot be null.'); + return; + } + + if (apiKey.trim() === '') { + setMessage('Api key cannot be null.'); + return; + } + + if (apiSecret.trim() === '') { + setMessage('Api secret cannot be null.'); + return; + } + + try { + await createUserExchange({ + variables: { + input: { + name: displayName, + exchangeName: exchangePlatforms?.label.toLowerCase() || 'unknown', + accessKey: apiKey, + secretKey: apiSecret, + }, + }, + }); + } catch (error) { + setMessage('invalid api key and secret'); + } + }; + + return ( + + Fill your API keys + + + Display name + setDisplayName(e.target.value)} + placeholder="Display name" + /> + + + API key + setApiKey(e.target.value)} + placeholder="API key" + /> + + + API secret + setApiSecret(e.target.value)} + placeholder="API secret" + /> + + + + Back + + Submit + + {message && {message}} + + ); +}; + +export default StepTwo; + +const ErrorMessage = styled.div` + background-color: #f8d7da; + color: #721c24; + padding: 10px; + margin-left: auto; + margin-right: auto; + border: 1px; + solid #f5c6cb; + border-radius: 5px; + text-align: center; +`; diff --git a/src/module/protect/crypto/exchange/WizardForm.test.tsx b/src/module/protect/crypto/exchange/WizardForm.test.tsx new file mode 100644 index 0000000..4430b72 --- /dev/null +++ b/src/module/protect/crypto/exchange/WizardForm.test.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +interface StepProps { + onSubmit: (data: any) => void; + previousPage?: () => void; + defaultValues: any; +} + +const MockStepOne: React.FC = ({ onSubmit, defaultValues }) => ( +
+ StepOne +
Default: {JSON.stringify(defaultValues)}
+ +
+); + +const MockStepTwo: React.FC = ({ onSubmit, previousPage, defaultValues }) => ( +
+ StepTwo +
Default: {JSON.stringify(defaultValues)}
+ + +
+); + +const MockStepThree: React.FC> = ({ defaultValues }) => ( +
+ StepThree +
Default: {JSON.stringify(defaultValues)}
+
+); + +jest.mock('./StepOne', () => MockStepOne); +jest.mock('./StepTwo', () => MockStepTwo); +jest.mock('./StepThree', () => MockStepThree); + +interface WizardFormProps { + onSubmit: (data: any) => void; +} +const WizardForm: React.FC = require('./WizardForm').default; + +describe('WizardForm', () => { + test('renders StepOne initially with empty defaultValues', () => { + render( {}} />); + expect(screen.getByTestId('step-one')).toBeInTheDocument(); + expect(screen.getByText('Default: {}')).toBeInTheDocument(); + }); + + test('navigates to StepTwo and passes correct defaultValues', () => { + render( {}} />); + fireEvent.click(screen.getByText('Next')); + expect(screen.getByTestId('step-two')).toBeInTheDocument(); + expect(screen.getByText('Default: {"step":"one"}')).toBeInTheDocument(); + }); + + test('navigates back to StepOne and resets defaultValues', () => { + render( {}} />); + fireEvent.click(screen.getByText('Next')); // To StepTwo + fireEvent.click(screen.getByText('Previous')); // Back to StepOne + expect(screen.getByTestId('step-one')).toBeInTheDocument(); + expect(screen.getByText('Default: {}')).toBeInTheDocument(); + }); + + test('navigates to StepThree and passes accumulated defaultValues', () => { + render( {}} />); + fireEvent.click(screen.getByText('Next')); // To StepTwo + fireEvent.click(screen.getByText('Next')); // To StepThree + expect(screen.getByTestId('step-three')).toBeInTheDocument(); + }); +}); diff --git a/src/module/protect/crypto/exchange/WizardForm.tsx b/src/module/protect/crypto/exchange/WizardForm.tsx new file mode 100644 index 0000000..5b8af87 --- /dev/null +++ b/src/module/protect/crypto/exchange/WizardForm.tsx @@ -0,0 +1,68 @@ +'use client'; + +import React, { useState } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { Card } from '@/shared/components/Card'; +import { + WizardFormWrap, + WizardStepMini, + WizardSteps, + WizardWrap, +} from '@/shared/components/form/WizardFormElements'; +import StepOne from './StepOne'; +import StepTwo from './StepTwo'; +import StepThree from './StepThree'; + +interface WizardFormProps { + onSubmit: (data: any) => void; +} + +const WizardForm: React.FC = () => { + const [page, setPage] = useState(1); + const [data, setData] = useState({}); + const [key, setKey] = useState(0); + + const nextPage = (newData: any) => { + setData((preData) => ({ ...preData, ...newData })); + setPage((prevPage) => prevPage + 1); + setKey((prevKey) => prevKey + 1); + }; + + const previousPage = () => { + setPage((prevPage) => prevPage - 1); + setKey((prevKey) => prevKey + 1); + setData({}); + }; + + return ( + + + + + + + + + + + {page === 1 && ( + + )} + {page === 2 && ( + + )} + {page === 3 && } + + + + + + ); +}; + +export default WizardForm; diff --git a/src/shared/components/form/WizardFormElements.tsx b/src/shared/components/form/WizardFormElements.tsx new file mode 100644 index 0000000..240b86d --- /dev/null +++ b/src/shared/components/form/WizardFormElements.tsx @@ -0,0 +1,127 @@ +import styled from 'styled-components'; +import { CardBody } from '@/shared/components/Card'; +import { FormButtonToolbar, FormContainer } from '@/shared/components/form/FormElements'; +import { + colorAdditional, + colorBlue, + colorBorder, + colorHover, + colorWhite, + colorBackground, + colorText, +} from '@/styles/palette'; + +interface WizardStepProps { + $active: boolean; +} + +export const WizardWrap = styled(CardBody)` + background-color: ${colorBackground}; +`; + +export const WizardFormContainer = styled(FormContainer)` + display: flex; + flex-direction: column; + justify-content: flex-start; + max-width: 610px; + margin-top: 50px; + margin-bottom: 100px; + padding: 0 250px; + box-sizing: border-box; + padding: 0 0; + width: 100%; +`; + +export const WizardButtonToolbar = styled(FormButtonToolbar)` + width: 100%; + justify-content: flex-start; + box-sizing: border-box; + padding: 20px 0; + left: 0; + gap: 438px; +`; + +export const WizardSteps = styled.div` + display: flex; +`; + +export const WizardStep = styled.div` + width: 100%; + text-align: center; + height: 55px; + text-transform: uppercase; + display: flex; + transition: background 0.3s; + border-radius: 5px; + border: 1px solid ${(props) => (props.$active ? colorBlue : colorBorder)}; + background: ${(props) => (props.$active ? colorBlue : colorHover)}; + + p { + font-weight: 700; + margin: auto; + font-size: 14px; + transition: all 0.3s; + color: ${(props) => (props.$active ? colorWhite : colorText)}; + } +`; + +export const WizardStepMini = styled.div` + width: 100%; + text-align: center; + height: 10px; + text-transform: uppercase; + display: flex; + transition: background 0.3s; + border-radius: 5px; + border: 1px solid ${(props) => (props.$active ? colorBlue : colorBorder)}; + background: ${(props) => (props.$active ? colorBlue : colorHover)}; + + p { + font-weight: 700; + margin: auto; + font-size: 14px; + transition: all 0.3s; + color: ${(props) => (props.$active ? colorWhite : colorText)}; + } +`; + +export const WizardFormWrap = styled.div` + display: flex; + justify-content: center; +`; + +export const WizardTitle = styled.h3` + margin-bottom: 40px; + margin-left: auto; + margin-right: auto; + font-weight: 500; +`; + +export const WizardDescription = styled.p` + color: ${colorAdditional}; + margin: 0; + max-width: 410px; +`; + +export const StyledButton = styled.button` + background-color: #66b3ff; + border: none; + color: white; + padding: 10px 20px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + border-radius: 8px; +`; + +export const WizardLabel = styled.label` + display: flex; + align-items: center; + font-size: 16px; + margin-bottom: 10px; + font-weight: 500; + color: ${colorText}; +`;