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] && }