From 4ba73ea7a7a8e52e7918f3d57fcd86f14e0b55ef Mon Sep 17 00:00:00 2001 From: Edward Chen Date: Mon, 13 May 2024 02:26:50 +1000 Subject: [PATCH 1/2] feat: added exchange connection page --- .vscode/settings.json | 2 +- .../crypto/exchange/_components/Form.tsx | 67 +++++ .../crypto/exchange/_components/StepOne.tsx | 50 ++++ .../crypto/exchange/_components/StepThree.tsx | 61 ++++ .../crypto/exchange/_components/StepTwo.tsx | 75 +++++ .../crypto/exchange/exchangePlatforms.ts | 26 ++ src/app/(protected)/crypto/exchange/page.tsx | 4 +- src/shared/components/form/FormField.tsx | 2 +- src/shared/components/form/RadioButton.tsx | 262 ++++++++++++++++++ .../components/form/WizardFormElements.tsx | 95 +++++++ src/shared/utils/directions.ts | 111 ++++++++ 11 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 src/app/(protected)/crypto/exchange/_components/Form.tsx create mode 100644 src/app/(protected)/crypto/exchange/_components/StepOne.tsx create mode 100644 src/app/(protected)/crypto/exchange/_components/StepThree.tsx create mode 100644 src/app/(protected)/crypto/exchange/_components/StepTwo.tsx create mode 100644 src/app/(protected)/crypto/exchange/exchangePlatforms.ts create mode 100644 src/shared/components/form/RadioButton.tsx create mode 100644 src/shared/components/form/WizardFormElements.tsx create mode 100644 src/shared/utils/directions.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 8eb76a8..7589729 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,5 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "cSpell.words": ["endregion", "linebreak", "Topbar"] + "cSpell.words": ["endregion", "linebreak", "Topbar", "vanta"] } diff --git a/src/app/(protected)/crypto/exchange/_components/Form.tsx b/src/app/(protected)/crypto/exchange/_components/Form.tsx new file mode 100644 index 0000000..c42534e --- /dev/null +++ b/src/app/(protected)/crypto/exchange/_components/Form.tsx @@ -0,0 +1,67 @@ +'use client'; + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +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 = ({ onSubmit }) => { + const [page, setPage] = useState(1); + const [data, setData] = useState({}); + + const nextPage = (newData: any) => { + setData(newData); + setPage(page + 1); + }; + + const previousPage = () => { + setPage(page - 1); + }; + + const submitHandler = (newData: any) => { + setData(newData); + onSubmit(newData); + }; + + return ( + + + + + + + + + + + {page === 1 && } + {page === 2 && ( + + )} + {page === 3 && } + + + + + + ); +}; + +WizardForm.propTypes = { + onSubmit: PropTypes.func.isRequired, +}; + +export default WizardForm; diff --git a/src/app/(protected)/crypto/exchange/_components/StepOne.tsx b/src/app/(protected)/crypto/exchange/_components/StepOne.tsx new file mode 100644 index 0000000..dc4f237 --- /dev/null +++ b/src/app/(protected)/crypto/exchange/_components/StepOne.tsx @@ -0,0 +1,50 @@ +'use client'; + +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { FormGroup, FormGroupField } from '@/shared/components/form/FormElements'; +import { Button } from '@/shared/components/Button'; +import { + WizardButtonToolbar, + WizardFormContainer, + WizardTitle, +} from '@/shared/components/form/WizardFormElements'; +import FormField from '@/shared/components/form/FormField'; +import renderRadioButtonField from '@/shared/components/form/RadioButton'; +import { exchangePlatformsGroup } from '../exchangePlatforms'; + +interface StepOneProps { + onSubmit: (data: any) => void; + defaultValues: Record; +} + +const StepOne: React.FC = ({ onSubmit, defaultValues }) => { + const { handleSubmit } = useForm({ defaultValues }); + return ( + + Select your exchange + +
+ {exchangePlatformsGroup.map((item) => ( + + + + ))} +
+
+ + + +
+ ); +}; + +export default StepOne; diff --git a/src/app/(protected)/crypto/exchange/_components/StepThree.tsx b/src/app/(protected)/crypto/exchange/_components/StepThree.tsx new file mode 100644 index 0000000..d65d637 --- /dev/null +++ b/src/app/(protected)/crypto/exchange/_components/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/app/(protected)/crypto/exchange/_components/StepTwo.tsx b/src/app/(protected)/crypto/exchange/_components/StepTwo.tsx new file mode 100644 index 0000000..eb4eba9 --- /dev/null +++ b/src/app/(protected)/crypto/exchange/_components/StepTwo.tsx @@ -0,0 +1,75 @@ +'use client'; + +import React from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import PasswordField from '@/shared/components/form/Password'; +import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/form/FormElements'; +import { Button } from '@/shared/components/Button'; +import { + WizardButtonToolbar, + WizardFormContainer, + WizardTitle, +} from '@/shared/components/form/WizardFormElements'; +import FormField from '@/shared/components/form/FormField'; + +interface StepTwoProps { + onSubmit: (data: any) => void; + defaultValues: Record; + previousPage: () => void; +} + +const StepTwo: React.FC = ({ onSubmit, previousPage, defaultValues }) => { + const { + handleSubmit, + control, + formState: { errors }, + } = useForm({ defaultValues }); + return ( + + Fill your API keys + + Display name + + + + API key + + + + API secret + + } + /> + + + + + + + + ); +}; + +export default StepTwo; diff --git a/src/app/(protected)/crypto/exchange/exchangePlatforms.ts b/src/app/(protected)/crypto/exchange/exchangePlatforms.ts new file mode 100644 index 0000000..88556fa --- /dev/null +++ b/src/app/(protected)/crypto/exchange/exchangePlatforms.ts @@ -0,0 +1,26 @@ +export const exchangePlatformsGroup = [ + { + name: 'Radio button 1', + label: 'Binance', + radioValue: '1', + initialValue: '1', + }, + { + name: 'Radio button 2', + label: 'TradeSanta', + radioValue: '2', + }, + { + name: 'radio_disabled_button', + label: 'CoinRule', + radioValue: '1', + disabled: true, + }, + { + name: 'radio_disabled_button', + label: 'JoinQuant', + radioValue: '2', + initialValue: '2', + disabled: true, + }, +]; diff --git a/src/app/(protected)/crypto/exchange/page.tsx b/src/app/(protected)/crypto/exchange/page.tsx index 8c6fb97..03f89e9 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 Form from './_components/Form'; const CryptoExchanges = () => { useTitle('Exchanges - BeeQuant'); @@ -10,9 +11,10 @@ const CryptoExchanges = () => { -

Crypto Exchanges

+

Connect New Exchange

+
{}} /> ); }; diff --git a/src/shared/components/form/FormField.tsx b/src/shared/components/form/FormField.tsx index b6a705f..3db0525 100644 --- a/src/shared/components/form/FormField.tsx +++ b/src/shared/components/form/FormField.tsx @@ -13,7 +13,7 @@ type FormFieldProps = { const FormField: React.FC = ({ input = null, - meta: { touched = false, error = '' }, + meta: { touched = false, error = '' } = {}, component: Component = 'input', isAboveError = false, wrapperClassName = '', diff --git a/src/shared/components/form/RadioButton.tsx b/src/shared/components/form/RadioButton.tsx new file mode 100644 index 0000000..32138d1 --- /dev/null +++ b/src/shared/components/form/RadioButton.tsx @@ -0,0 +1,262 @@ +import React from 'react'; +import styled from 'styled-components'; +import CheckIcon from 'mdi-react/CheckIcon'; +import CloseIcon from 'mdi-react/CloseIcon'; +import { renderComponentField } from '@/shared/components/form/FormField'; +import { colorAccent, colorAccentHover, colorIcon, colorText, colorWhite } from '@/styles/palette'; +import { left, paddingLeft, marginLeft, marginRight, paddingRight } from '@/utils/directions'; + +interface RadioButtonProps { + onChange: (value: string) => void; + name: string; + value: string; + label?: string; + disabled?: boolean; + radioValue?: string; + styleType?: 'colored' | 'button' | 'colored-click' | undefined; +} + +interface RadioButtonWrapProps { + disabled?: boolean; + styleType?: string; +} + +const RadioButton: React.FC = ({ + onChange, + radioValue = '', + styleType = 'colored', + disabled = false, + label = '', + name = '', + value = '', +}) => { + const handleChange = () => { + if (typeof onChange === 'function') { + onChange(radioValue || ''); + } + }; + + return ( + + + + {styleType === 'button' ? ( + + + + + ) : ( + '' + )} + {label} + + ); +}; + +export default renderComponentField(RadioButton); + +// region STYLES + +const RadioButtonCustom = styled.span` + width: 18px; + height: 18px; + border-radius: 50%; + position: relative; + transition: all 0.3s; + border: 1px solid ${colorIcon}; + display: inline-block; + vertical-align: middle; +`; + +const RadioButtonInput = styled.input` + display: none; + + &:checked + ${RadioButtonCustom} { + border-color: ${colorAccent}; + + &::before { + content: ''; + display: block; + position: absolute; + width: 6px; + height: 6px; + top: calc(50% - 3px); + background: ${colorAccent}; + border-radius: 50%; + ${left}: calc(50% - 3px); + } + } +`; + +const RadioButtonLabel = styled.span` + line-height: 18px; + transition: all 0.3s; + margin-top: 5px; + display: inline-block; + vertical-align: middle; + ${paddingLeft}: 9px; + color: ${colorText}; +`; + +const RadioButtonSvgWrap = styled.span``; + +const RadioButtonWrap = styled.label` + display: inline-block; + cursor: pointer; + flex-direction: inherit; + padding: 0; + ${paddingRight}: 20px; + + ${(props) => + props.disabled && + ` + pointer-events: none; + cursor: default; + opacity: 0.4; + `} + + &:hover { + ${RadioButtonCustom} { + border-color: ${colorAccent}; + } + + ${RadioButtonLabel} { + color: ${colorAccent}; + } + } + + ${(props) => + props.styleType === 'colored' && + ` + flex-direction: inherit; + padding: 0; + + ${RadioButtonCustom} { + border: 2px solid ${colorAccent}; + } + `} + + ${(props) => + props.styleType === 'colored-click' && + ` + flex-direction: inherit; + padding: 0; + + ${RadioButtonInput}:checked + ${RadioButtonCustom} { + background: ${colorAccent}; + + &::before { + background: ${colorWhite}; + } + } + + ${ + props.disabled && + ` + + ${RadioButtonInput}:checked + ${RadioButtonCustom} { + background: transparent; + + &::before { + background: ${colorAccent}; + } + } + ` + } + `} + + ${(props) => + props.styleType === 'button' && + ` + background: ${colorAccent}; + min-width: 150px; + color: ${colorWhite}; + height: 24px; + border-radius: 4px; + transition: all 0.3s; + display: flex; + flex-direction: inherit; + padding: 0 10px; + width: 100%; + justify-content: center; + align-items: center; + + ${RadioButtonCustom} { + display: none; + } + + ${RadioButtonSvgWrap} { + height: 16px; + line-height: 1; + ${marginRight(props)}: 4px; + + svg { + fill: ${colorWhite}; + width: 14px; + height: 14px; + } + + svg:first-of-type { + display: none; + } + } + + ${RadioButtonInput}:checked ~ ${RadioButtonSvgWrap} { + + svg:first-of-type { + display: block; + } + + svg:last-of-type { + display: none; + } + } + + ${RadioButtonLabel} { + margin-top: auto; + margin-bottom: auto; + padding: 0; + color: ${colorWhite}; + ${marginLeft(props)}: 0; + } + + &:hover { + background: ${colorAccentHover}; + + ${RadioButtonLabel} { + color: ${colorWhite}; + } + } + `}; + + @media screen and (max-width: 370px) { + display: flex; + align-items: center; + } + + @media screen and (max-width: 575px) { + display: inline-block; + } + + @media screen and (max-width: 515px) { + display: flex; + align-items: center; + } + + @media screen and (max-width: 500px) { + display: inline-block; + } + + @media screen and (max-width: 356px) { + display: flex; + align-items: center; + } +`; + +// endregion diff --git a/src/shared/components/form/WizardFormElements.tsx b/src/shared/components/form/WizardFormElements.tsx new file mode 100644 index 0000000..2e4d790 --- /dev/null +++ b/src/shared/components/form/WizardFormElements.tsx @@ -0,0 +1,95 @@ +import styled from 'styled-components'; +import { FormButtonToolbar, FormContainer } from '@/shared/components/form/FormElements'; +import { CardBody } from '@/shared/components/Card'; +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)` + max-width: 610px; + width: 100%; + margin-top: 50px; + margin-bottom: 100px; + padding: 0 25px; +`; + +export const WizardButtonToolbar = styled(FormButtonToolbar)` + margin-left: auto; + margin-right: auto; +`; + +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; +`; diff --git a/src/shared/utils/directions.ts b/src/shared/utils/directions.ts new file mode 100644 index 0000000..ebf716a --- /dev/null +++ b/src/shared/utils/directions.ts @@ -0,0 +1,111 @@ +import theme from 'styled-theming'; + +export const direction = theme('direction', { + ltr: 'ltr', + rtl: 'rtl', +}); + +export const right = theme('direction', { + ltr: 'right', + rtl: 'left', +}); + +export const left = theme('direction', { + ltr: 'left', + rtl: 'right', +}); + +export const marginRight = theme('direction', { + ltr: 'margin-right', + rtl: 'margin-left', +}); + +export const marginLeft = theme('direction', { + ltr: 'margin-left', + rtl: 'margin-right', +}); + +export const paddingLeft = theme('direction', { + ltr: 'padding-left', + rtl: 'padding-right', +}); + +export const paddingRight = theme('direction', { + ltr: 'padding-right', + rtl: 'padding-left', +}); + +export const borderTopLeftRadius = theme('direction', { + ltr: 'border-top-left-radius', + rtl: 'border-top-right-radius', +}); + +export const borderBottomLeftRadius = theme('direction', { + ltr: 'border-bottom-left-radius', + rtl: 'border-bottom-right-radius', +}); + +export const borderTopRightRadius = theme('direction', { + ltr: 'border-top-right-radius', + rtl: "border-top-left-radius'", +}); + +export const borderBottomRightRadius = theme('direction', { + ltr: 'border-bottom-right-radius', + rtl: 'border-bottom-left-radius', +}); + +export const borderLeft = theme('direction', { + ltr: 'border-left', + rtl: 'border-right', +}); + +export const borderRight = theme('direction', { + ltr: 'border-right', + rtl: 'border-left', +}); + +export const translate = theme('direction', { + ltr: 'translate(-50%, -50%);', + rtl: 'translate(50%, -50%);', +}); + +export const mirrorY = theme('direction', { + ltr: 'scale(1, 1)', + rtl: 'scale(1, -1)', +}); + +export const borderRightColor = theme('direction', { + ltr: 'border-right-color', + rtl: 'border-left-color', +}); + +export const flexFlow = theme('direction', { + ltr: 'row nowrap', + rtl: 'row-reverse nowrap', +}); + +export const row = theme('direction', { + ltr: 'row', + rtl: 'row-reverse', +}); + +export const sidebarClose = theme('direction', { + ltr: 'translateX(0)', + rtl: 'translateX(0)', +}); + +export const sidebarNoDesktop = theme('direction', { + ltr: 'translateX(calc(0%))', + rtl: 'translateX(calc(100%))', +}); + +export const borderRadius = theme('border', { + on: '15px', + off: '5px', +}); + +export const shadow = theme('shadow', { + on: '0 10px 30px 1px rgba(0, 0, 0, 0.06)', + off: 'none', +}); From cdaaf1e729bca510ac4e38b7d96446b85a7cc060 Mon Sep 17 00:00:00 2001 From: Edward Chen Date: Mon, 13 May 2024 02:35:36 +1000 Subject: [PATCH 2/2] fix: fixed build error --- src/app/(protected)/crypto/exchange/_components/Form.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/(protected)/crypto/exchange/_components/Form.tsx b/src/app/(protected)/crypto/exchange/_components/Form.tsx index c42534e..3aedd71 100644 --- a/src/app/(protected)/crypto/exchange/_components/Form.tsx +++ b/src/app/(protected)/crypto/exchange/_components/Form.tsx @@ -49,7 +49,11 @@ const WizardForm: React.FC = ({ onSubmit }) => { {page === 1 && } {page === 2 && ( - + )} {page === 3 && }