diff --git a/src/app/(protected)/account/management/page.tsx b/src/app/(protected)/account/management/page.tsx new file mode 100644 index 0000000..7ff8cf5 --- /dev/null +++ b/src/app/(protected)/account/management/page.tsx @@ -0,0 +1,7 @@ +import UpdatePasswordLink from 'module/protected/account/management/UpdatePasswordLink'; + +const AccountManagement = () => { + return ; +}; + +export default AccountManagement; diff --git a/src/app/(protected)/account/settings/page.tsx b/src/app/(protected)/account/settings/page.tsx index 9c1033e..a3f7196 100644 --- a/src/app/(protected)/account/settings/page.tsx +++ b/src/app/(protected)/account/settings/page.tsx @@ -1,4 +1,4 @@ -import SettingForm from 'module/account/settings/SettingForm'; +import SettingForm from 'module/protected/account/settings/SettingForm'; export default function SettingPage() { return ; diff --git a/src/app/(protected)/account/update/password/page.tsx b/src/app/(protected)/account/update/password/page.tsx new file mode 100644 index 0000000..d4b70f3 --- /dev/null +++ b/src/app/(protected)/account/update/password/page.tsx @@ -0,0 +1,7 @@ +import UpdatePasswordForm from 'module/protected/account/update/password/UpdatePasswordForm'; + +const UpdatePasswordPage = () => { + return ; +}; + +export default UpdatePasswordPage; diff --git a/src/containers/Layout/sidebar/SidebarContent.tsx b/src/containers/Layout/sidebar/SidebarContent.tsx index 410e1fb..2c7d299 100644 --- a/src/containers/Layout/sidebar/SidebarContent.tsx +++ b/src/containers/Layout/sidebar/SidebarContent.tsx @@ -104,6 +104,11 @@ const SidebarContent = ({ onClick, $collapse }: SidebarContentProps) => { route={getRouteByKey(ROUTE_KEY.ACCOUNT_PROFILE).path} onClick={onClick} /> + { onClick={toggleCollapse} /> + ; + +export type ChangePasswordMutation = { + __typename?: 'Mutation'; + changePassword: { + __typename?: 'Result'; + code: number; + message?: string | null; + data?: string | null; + }; +}; + export type GetUserInfoQueryVariables = Exact<{ [key: string]: never }>; export type GetUserInfoQuery = { @@ -357,6 +371,50 @@ export const RegisterDocument = { }, ], } as unknown as DocumentNode; +export const ChangePasswordDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'ChangePassword' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'input' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'UpdatePasswordInput' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'changePassword' }, + 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' } }, + { kind: 'Field', name: { kind: 'Name', value: 'data' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; export const GetUserInfoDocument = { kind: 'Document', definitions: [ diff --git a/src/module/protected/account/management/CardLink.test.tsx b/src/module/protected/account/management/CardLink.test.tsx new file mode 100644 index 0000000..2a27de0 --- /dev/null +++ b/src/module/protected/account/management/CardLink.test.tsx @@ -0,0 +1,49 @@ +import { ReactNode } from 'react'; +import { render, screen } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import CardLink from './CardLink'; + +jest.mock('@/shared/components/Card', () => ({ + Card: ({ children }: { children?: ReactNode }) =>
{children}
, + CardBody: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), + CardTitle: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), + CardSubhead: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), + CardTitleWrap: ({ children }: { children?: ReactNode }) => ( +
{children}
+ ), +})); + +describe('CardLink', () => { + const defaultProps = { + cardTitle: 'Test Title', + cardSubhead: 'Test Subhead', + route: '/test-route', + }; + + it('should render without crashing', () => { + render( + + + + ); + expect(screen.getByTestId('card')).toBeInTheDocument(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Subhead')).toBeInTheDocument(); + }); + + it('should navigate to the correct route on click', () => { + render( + + + + ); + const linkElement = screen.getByRole('link'); + expect(linkElement).toHaveAttribute('href', defaultProps.route); + }); +}); diff --git a/src/module/protected/account/management/CardLink.tsx b/src/module/protected/account/management/CardLink.tsx new file mode 100644 index 0000000..393f12c --- /dev/null +++ b/src/module/protected/account/management/CardLink.tsx @@ -0,0 +1,26 @@ +import { Col } from 'react-bootstrap'; +import { Card, CardBody, CardTitle, CardTitleWrap, CardSubhead } from '@/shared/components/Card'; +import Link from 'next/link'; + +type CardLinkProps = { + cardTitle: string; + cardSubhead: string; + route: string; +}; + +const CardLink = ({ cardTitle, cardSubhead, route }: CardLinkProps) => ( + + + + + + {cardTitle} + {cardSubhead} + + + + + +); + +export default CardLink; diff --git a/src/module/protected/account/management/UpdatePasswordLink.test.tsx b/src/module/protected/account/management/UpdatePasswordLink.test.tsx new file mode 100644 index 0000000..6031943 --- /dev/null +++ b/src/module/protected/account/management/UpdatePasswordLink.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import UpdatePasswordLink from './UpdatePasswordLink'; + +function renderAccountManagement() { + render( + + + + ); +} + +describe('AccountManagement Component', () => { + it('should render without crashing', () => { + renderAccountManagement(); + expect(screen.getByText('Account Management')).toBeInTheDocument(); + }); + + it('should display the page title correctly', () => { + renderAccountManagement(); + expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Account Management'); + }); + + it('should render the CardLink component with correct props', () => { + renderAccountManagement(); + expect(screen.getByText('Reset Password')).toBeInTheDocument(); + expect(screen.getByText('Update your password')).toBeInTheDocument(); + }); +}); diff --git a/src/module/protected/account/management/UpdatePasswordLink.tsx b/src/module/protected/account/management/UpdatePasswordLink.tsx new file mode 100644 index 0000000..78ec183 --- /dev/null +++ b/src/module/protected/account/management/UpdatePasswordLink.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { Col, Container, Row } from 'react-bootstrap'; +import { useTitle } from '@/hooks/useTitle'; +import CardLink from './CardLink'; + +export default function UpdatePasswordLink() { + useTitle('Account Management - BeeQuant'); + + return ( + + + +

Account Management

+ +
+ + + +
+ ); +} diff --git a/src/module/account/settings/SettingForm.test.tsx b/src/module/protected/account/settings/SettingForm.test.tsx similarity index 100% rename from src/module/account/settings/SettingForm.test.tsx rename to src/module/protected/account/settings/SettingForm.test.tsx diff --git a/src/module/account/settings/SettingForm.tsx b/src/module/protected/account/settings/SettingForm.tsx similarity index 100% rename from src/module/account/settings/SettingForm.tsx rename to src/module/protected/account/settings/SettingForm.tsx diff --git a/src/module/account/settings/SettingFormLayout.tsx b/src/module/protected/account/settings/SettingFormLayout.tsx similarity index 97% rename from src/module/account/settings/SettingFormLayout.tsx rename to src/module/protected/account/settings/SettingFormLayout.tsx index f6ff6fb..95aeb92 100644 --- a/src/module/account/settings/SettingFormLayout.tsx +++ b/src/module/protected/account/settings/SettingFormLayout.tsx @@ -6,7 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@/shared/components/Button'; import * as z from 'zod'; import { DisplayErrorMsgs, EmailErrorMsgs, RefErrorMsgs } from '@/shared/utils/helpers'; -import FormInput from './_component/FormInput/FormInput'; +import FormInput from '@/shared/components/form/FormInput'; const formSchema = z.object({ displayName: z diff --git a/src/module/protected/account/update/password/ChangePasswordSuccess.tsx b/src/module/protected/account/update/password/ChangePasswordSuccess.tsx new file mode 100644 index 0000000..febbc92 --- /dev/null +++ b/src/module/protected/account/update/password/ChangePasswordSuccess.tsx @@ -0,0 +1,29 @@ +import { Button, Container, Row, Col } from 'react-bootstrap'; +import { + StyledModal, + StyledModalHeader, + StyledModalTitle, + StyledModalFooter, +} from '@/shared/components/Modal'; +import Link from 'next/link'; + +const ChangePasswordSuccess = ({ success }: { success: string }) => ( + + + + + + You have successfully reset your password! + + + + + + + + + + +); + +export default ChangePasswordSuccess; diff --git a/src/module/protected/account/update/password/FormLayout.tsx b/src/module/protected/account/update/password/FormLayout.tsx new file mode 100644 index 0000000..2b8cc07 --- /dev/null +++ b/src/module/protected/account/update/password/FormLayout.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation } from '@apollo/client'; +import { USER_CHANGE_PASSWORD } from '@/graphql/auth'; +import { FormButtonToolbar, FormContainer } from '@/shared/components/form/FormElements'; +import ChangePasswordSuccess from './ChangePasswordSuccess'; +import { Alert } from 'react-bootstrap'; +import { Button } from '@/shared/components/Button'; +import { useForm, Controller } from 'react-hook-form'; +import FormPasswordInput from '@/shared/components/form/FormPasswordInput'; +import * as z from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { PasswordCompareErrorMsgs, PasswordErrorMsgs } from '@/shared/utils/helpers'; + +type FormData = { oldPassword: string; newPassword: string; repeatNewPassword: string }; + +export default function FormLayout() { + const [changePassword] = useMutation(USER_CHANGE_PASSWORD); + const [success, setSuccess] = useState(''); + const [error, setError] = useState(''); + + const passwordSchema = z + .string() + .min(1, { message: PasswordErrorMsgs.Required }) + .min(8, { message: PasswordErrorMsgs.MinLength }) + .max(32, { message: PasswordErrorMsgs.MaxLength }) + .regex( + /^(?=.*[A-Za-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*_+=\\~<>:;”’(),./[\]_`|{}-])[A-Za-z0-9#?!@$%^&*_+=\\~<>:;”’(),./[\]_`|{}-]+$/, + { message: PasswordErrorMsgs.Invalid } + ); + + const updatePasswordSchema = z + .object({ + oldPassword: passwordSchema, + newPassword: passwordSchema, + repeatNewPassword: passwordSchema, + }) + .refine(({ newPassword, repeatNewPassword }) => newPassword === repeatNewPassword, { + message: PasswordCompareErrorMsgs.NotMatch, + path: ['repeatNewPassword'], + }); + + const { + handleSubmit, + control, + formState: { errors }, + } = useForm({ + defaultValues: { oldPassword: '', newPassword: '', repeatNewPassword: '' }, + resolver: zodResolver(updatePasswordSchema), + }); + + const onSubmit = async (data: FormData) => { + const { oldPassword, newPassword } = data; + const result = await changePassword({ + variables: { + input: { oldPassword, newPassword }, + }, + }); + + if (result.data?.changePassword.code === 200) { + setSuccess('Password changed successfully!'); + setError(''); + } else { + setError(`Change password failed: ${result.data?.changePassword.message}`); + setSuccess(''); + } + }; + return ( + + + + {error} + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + {/* eslint-disable-next-line max-len */} + {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + + + ); +} diff --git a/src/module/protected/account/update/password/UpdatePasswordForm.test.tsx b/src/module/protected/account/update/password/UpdatePasswordForm.test.tsx new file mode 100644 index 0000000..6fb7e56 --- /dev/null +++ b/src/module/protected/account/update/password/UpdatePasswordForm.test.tsx @@ -0,0 +1,194 @@ +import { render, screen } from '@testing-library/react'; +import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { USER_CHANGE_PASSWORD } from '@/graphql/auth'; +import UpdatePasswordPage from './UpdatePasswordForm'; +import userEvent from '@testing-library/user-event'; +import { PasswordCompareErrorMsgs, PasswordErrorMsgs } from '@/shared/utils/helpers'; + +function renderPage(mockData: MockedResponse[]) { + render( + + + + ); +} + +const changePasswordMockSuccess = [ + { + request: { + query: USER_CHANGE_PASSWORD, + variables: { + input: { + oldPassword: 'oldPass123!', + newPassword: 'newPass123!', + }, + }, + }, + result: { + data: { + changePassword: { + code: 200, + message: 'You have successfully reset your password!', + data: null, + }, + }, + }, + }, +]; + +const changePasswordMockFailure = [ + { + request: { + query: USER_CHANGE_PASSWORD, + variables: { + input: { + oldPassword: 'diffOldPass123!', + newPassword: 'newPass123!', + }, + }, + }, + result: { + data: { + changePassword: { + code: 400, + message: 'Incorrect password', + data: null, + }, + }, + }, + }, +]; + +describe('ChangePassword', () => { + afterEach(() => { + jest.resetModules(); + }); + + it('should render correctly', () => { + renderPage([]); + expect(screen.getByLabelText('Old Password')).toBeInTheDocument(); + expect(screen.getByLabelText('New Password')).toBeInTheDocument(); + expect(screen.getByLabelText('Repeat New Password')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /submit/i })).toBeInTheDocument(); + }); + + it('should handle successful password change', async () => { + renderPage(changePasswordMockSuccess); + + const oldPasswordInput = screen.getByLabelText('Old Password'); + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(oldPasswordInput, 'oldPass123!'); + await userEvent.type(newPasswordInput, 'newPass123!'); + await userEvent.type(repeatNewPasswordInput, 'newPass123!'); + + await userEvent.click(submitButton); + + expect(await screen.findByText(/You have successfully reset your password!/i)).toBeVisible(); + expect(await screen.findByText(/Ready to trade again/i)).toBeVisible(); + }); + + it('should navigate to the login page on click of "Ready to trade again" button', async () => { + renderPage(changePasswordMockSuccess); + + const oldPasswordInput = screen.getByLabelText('Old Password'); + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(oldPasswordInput, 'oldPass123!'); + await userEvent.type(newPasswordInput, 'newPass123!'); + await userEvent.type(repeatNewPasswordInput, 'newPass123!'); + await userEvent.click(submitButton); + + expect(await screen.findByRole('link')).toHaveAttribute('href', '/login'); + }); + + it('should handle password change failure, caused by incorrect current password', async () => { + renderPage(changePasswordMockFailure); + + const oldPasswordInput = screen.getByLabelText('Old Password'); + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(oldPasswordInput, 'diffOldPass123!'); + await userEvent.type(newPasswordInput, 'newPass123!'); + await userEvent.type(repeatNewPasswordInput, 'newPass123!'); + await userEvent.click(submitButton); + + expect(await screen.findByText(/incorrect password/i)).toBeVisible(); + }); + + it('should handle password change failure, caused by password field validation - missing field', async () => { + renderPage([]); + + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(newPasswordInput, 'newPass123!'); + await userEvent.type(repeatNewPasswordInput, 'newPass123!'); + await userEvent.click(submitButton); + + expect(await screen.findByText(PasswordErrorMsgs.Required)).toBeVisible(); + }); + + it('should handle password change failure, caused by password field validation - passwords not match', async () => { + renderPage([]); + + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(newPasswordInput, 'newPass123!'); + await userEvent.type(repeatNewPasswordInput, 'diffNewPass123!'); + await userEvent.click(submitButton); + + expect(await screen.findByText(PasswordCompareErrorMsgs.NotMatch)).toBeVisible(); + }); + + it('should handle password change failure, caused by password field validation - password fail minLength validation', async () => { + renderPage([]); + + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(newPasswordInput, 'newPass'); + await userEvent.type(repeatNewPasswordInput, 'newPass'); + await userEvent.click(submitButton); + + expect(await screen.findAllByText(PasswordErrorMsgs.MinLength)).toHaveLength(2); + }); + + it('should handle password change failure, caused by password field validation - password fail maxLength validation', async () => { + renderPage([]); + + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(newPasswordInput, 'seihuangabcfromchinashanghai1234567890'); + await userEvent.type(repeatNewPasswordInput, 'seihuangabcfromchinashanghai1234567890'); + await userEvent.click(submitButton); + + expect(await screen.findAllByText(PasswordErrorMsgs.MaxLength)).toHaveLength(2); + }); + + it('should handle password change failure, caused by password field validation - password fail pattern validation', async () => { + renderPage([]); + + const newPasswordInput = screen.getByLabelText('New Password'); + const repeatNewPasswordInput = screen.getByLabelText('Repeat New Password'); + const submitButton = screen.getByRole('button', { name: /submit/i }); + + await userEvent.type(newPasswordInput, 'as123456'); + await userEvent.type(repeatNewPasswordInput, 'as123456'); + await userEvent.click(submitButton); + + expect(await screen.findAllByText(PasswordErrorMsgs.Invalid)).toHaveLength(2); + }); +}); diff --git a/src/module/protected/account/update/password/UpdatePasswordForm.tsx b/src/module/protected/account/update/password/UpdatePasswordForm.tsx new file mode 100644 index 0000000..b108662 --- /dev/null +++ b/src/module/protected/account/update/password/UpdatePasswordForm.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { Container, Row, Col } from 'react-bootstrap'; +import { Card, CardBody, CardTitleWrap, CardTitle, CardSubhead } from '@/shared/components/Card'; +import FormLayout from './FormLayout'; + +export default function UpdatePasswordForm() { + return ( + + + + + + + Reset Password + Update your password + + + + + + + + ); +} diff --git a/src/routes/routeConfig.ts b/src/routes/routeConfig.ts index 11968d1..73ac93a 100644 --- a/src/routes/routeConfig.ts +++ b/src/routes/routeConfig.ts @@ -13,6 +13,8 @@ import ExchangeDetails from 'app/(protected)/crypto/exchange/details/page'; import PriceDetails from 'app/(protected)/crypto/price/details/page'; import BotDetail from 'app/(protected)/bot/details/page'; import BotCreate from 'app/(protected)/bot/create/page'; +import AccountManagement from 'app/(protected)/account/management/page'; +import ChangePassword from 'app/(protected)/account/update/password/page'; interface IRoute { path: string; @@ -26,6 +28,8 @@ export const ROUTE_KEY = { DASHBOARD: 'dashboard', EXCHANGE_MANAGEMENT: 'exchange_management', ACCOUNT_PROFILE: 'account_profile', + ACCOUNT_MANAGEMENT: 'account_management', + CHANGE_PASSWORD: 'change_password', PAGE_404: 'page_404', LOGIN: 'login', REGISTER: 'register', @@ -86,6 +90,18 @@ export const ROUTE_CONFIG: Record = { title: 'Profile - BeeQuant', component: Profile, }, + [ROUTE_KEY.ACCOUNT_MANAGEMENT]: { + path: '/account/management', + name: 'Account Management', + title: 'Account Management - BeeQuant', + component: AccountManagement, + }, + [ROUTE_KEY.CHANGE_PASSWORD]: { + path: '/account/update/password', + name: 'Reset Password', + title: 'Reset Password - BeeQuant', + component: ChangePassword, + }, [ROUTE_KEY.BOT_DASHBOARD]: { path: '/bot/dashboard', name: 'Bots Dashboard', diff --git a/src/shared/components/Card.tsx b/src/shared/components/Card.tsx index ab55add..a3bed4b 100644 --- a/src/shared/components/Card.tsx +++ b/src/shared/components/Card.tsx @@ -9,6 +9,10 @@ interface CardTitleProps extends React.HTMLAttributes { theme?: string; } +interface CardTitleWrapperProps extends React.HTMLAttributes { + $marginBottom?: string; +} + export const Card = styled(BootstrapCard)` width: 100%; padding-bottom: 30px; @@ -25,12 +29,11 @@ export const CardBody = styled(Card.Body)` padding: 20px; `; -export const CardTitleWrap = styled.div` - margin-bottom: 30px; +export const CardTitleWrap = styled.div` + margin-bottom: ${(props) => props.$marginBottom || '30px'}; text-transform: uppercase; position: relative; text-align: ${left}; - &:not(:first-child) { margin-top: 40px; } diff --git a/src/shared/components/Modal.tsx b/src/shared/components/Modal.tsx new file mode 100644 index 0000000..e79a9d8 --- /dev/null +++ b/src/shared/components/Modal.tsx @@ -0,0 +1,28 @@ +import styled from 'styled-components'; +import { Modal } from 'react-bootstrap'; +import { borderRadius, shadow } from '@/styles/styles'; +import { colorBackground, colorWhite, colorGray } from '@/styles/palette'; + +export const StyledModal = styled(Modal)` + border-radius: ${borderRadius}; + box-shadow: ${shadow}; +`; + +export const StyledModalHeader = styled(Modal.Header)` + background-color: ${colorBackground}; + padding: 50px 20px 30px; + border-bottom: 0 none; +`; + +export const StyledModalTitle = styled(Modal.Title)<{ theme: string }>` + color: ${(props) => (props.theme === 'dark' ? colorWhite : colorGray)}; + font-size: 20px; + font-weight: 700; + margin: auto; +`; + +export const StyledModalFooter = styled(Modal.Footer)` + background-color: ${colorBackground}; + padding: 20px; + border-top: 0 none; +`; diff --git a/src/module/account/settings/_component/FormInput/_component/FormIcon.tsx b/src/shared/components/form/FormIcon.tsx similarity index 100% rename from src/module/account/settings/_component/FormInput/_component/FormIcon.tsx rename to src/shared/components/form/FormIcon.tsx diff --git a/src/module/account/settings/_component/FormInput/FormInput.tsx b/src/shared/components/form/FormInput.tsx similarity index 95% rename from src/module/account/settings/_component/FormInput/FormInput.tsx rename to src/shared/components/form/FormInput.tsx index c5f5037..8c3f83c 100644 --- a/src/module/account/settings/_component/FormInput/FormInput.tsx +++ b/src/shared/components/form/FormInput.tsx @@ -4,7 +4,7 @@ import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/f import Error from '@/shared/components/form/Error'; import styled from 'styled-components'; import { FieldErrors } from 'react-hook-form'; -import FormIcon from './_component/FormIcon'; +import FormIcon from './FormIcon'; interface FormInputProps { placeholder: string; diff --git a/src/module/account/settings/_component/FormInput/FormPasswordInput.tsx b/src/shared/components/form/FormPasswordInput.tsx similarity index 85% rename from src/module/account/settings/_component/FormInput/FormPasswordInput.tsx rename to src/shared/components/form/FormPasswordInput.tsx index acca610..60c86f5 100644 --- a/src/module/account/settings/_component/FormInput/FormPasswordInput.tsx +++ b/src/shared/components/form/FormPasswordInput.tsx @@ -4,12 +4,12 @@ import { FormGroupField, FormGroupLabel, } from '@/shared/components/form/FormElements'; -import { useState } from 'react'; import { FieldErrors } from 'react-hook-form'; import styled from 'styled-components'; import EyeIcon from 'mdi-react/EyeIcon'; import Error from '@/shared/components/form/Error'; -import FormIcon from './_component/FormIcon'; +import { useState } from 'react'; +import FormIcon from './FormIcon'; interface FormInputProps { placeholder: string; @@ -32,7 +32,12 @@ export default function FormPasswordInput({ - + {errors[name] && }