Merge branch 'react-practice-02' into feat/unit-test-for-function

This commit is contained in:
2023-11-30 23:30:09 +07:00
115 changed files with 3072 additions and 2076 deletions
+1
View File
@@ -18,6 +18,7 @@
"@emotion/is-prop-valid": "^1.2.1", "@emotion/is-prop-valid": "^1.2.1",
"@supabase/supabase-js": "^2.38.4", "@supabase/supabase-js": "^2.38.4",
"@tanstack/react-query": "^5.8.4", "@tanstack/react-query": "^5.8.4",
"@tanstack/react-query-devtools": "^5.8.4",
"@types/react-test-renderer": "^18.0.6", "@types/react-test-renderer": "^18.0.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
+20
View File
@@ -14,6 +14,9 @@ dependencies:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^5.8.4 specifier: ^5.8.4
version: 5.8.4(react-dom@18.2.0)(react@18.2.0) version: 5.8.4(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query-devtools':
specifier: ^5.8.4
version: 5.8.4(@tanstack/react-query@5.8.4)(react-dom@18.2.0)(react@18.2.0)
'@types/react-test-renderer': '@types/react-test-renderer':
specifier: ^18.0.6 specifier: ^18.0.6
version: 18.0.6 version: 18.0.6
@@ -1132,6 +1135,23 @@ packages:
resolution: {integrity: sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==} resolution: {integrity: sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==}
dev: false dev: false
/@tanstack/query-devtools@5.8.4:
resolution: {integrity: sha512-F1dRbITNt9tMUoM9WCH8WQ2c54116hv52m/PKK8ZiN/pO2wGVzTZtKuLanF8pFpwmNchjIixcMw/a57HY5ivcw==}
dev: false
/@tanstack/react-query-devtools@5.8.4(@tanstack/react-query@5.8.4)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-mffs51FJqXU/5rwhbwv393DccL6et7uK2pRLwOcmMrWbPyW8vpxr9oidaghHX4cdVeP/7u5owW9yMpBhBAJfcQ==}
peerDependencies:
'@tanstack/react-query': ^5.8.4
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
'@tanstack/query-devtools': 5.8.4
'@tanstack/react-query': 5.8.4(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@tanstack/react-query@5.8.4(react-dom@18.2.0)(react@18.2.0): /@tanstack/react-query@5.8.4(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CD+AkXzg8J72JrE6ocmuBEJfGzEzu/bzkD6sFXFDDB5yji9N20JofXZlN6n0+CaPJuIi+e4YLCbGsyPFKkfNQA==} resolution: {integrity: sha512-CD+AkXzg8J72JrE6ocmuBEJfGzEzu/bzkD6sFXFDDB5yji9N20JofXZlN6n0+CaPJuIi+e4YLCbGsyPFKkfNQA==}
peerDependencies: peerDependencies:
+40 -22
View File
@@ -20,30 +20,48 @@ describe('App', () => {
}); });
test('Should init room, user data', async () => { test('Should init room, user data', async () => {
const mockGetUserNotBooked = jest.spyOn(userServices, 'getUserNotBooked'); const mockGetUserNotBooked = jest.spyOn(userServices, 'getAllUsers');
const mockGetRoomAvailable = jest.spyOn(roomServices, 'getRoomsAvailable'); const mockGetRoomAvailable = jest.spyOn(roomServices, 'getAllRooms');
mockGetUserNotBooked.mockResolvedValue([ mockGetUserNotBooked.mockResolvedValue({
{ data: [
id: 1, {
name: 'Nezumi', id: 1,
}, name: 'Nezumi',
{ phone: '0324432321',
id: 2, isBooked: true,
name: 'Loi Phan', isDelete: true,
}, },
]); {
id: 1,
name: 'Loi Phan',
phone: '0324432123',
isBooked: true,
isDelete: true,
},
],
count: 2,
});
mockGetRoomAvailable.mockResolvedValue([ mockGetRoomAvailable.mockResolvedValue({
{ data: [
id: 1, {
name: 'Room 1', id: 1,
}, name: 'Room 1',
{ price: 230,
id: 2, status: true,
name: 'Room 2', isDelete: true,
}, },
]); {
id: 2,
name: 'Room 2',
price: 242,
status: true,
isDelete: true
},
],
count: 2
});
expect(mockGetUserNotBooked).toHaveBeenCalled(); expect(mockGetUserNotBooked).toHaveBeenCalled();
expect(mockGetRoomAvailable).toHaveBeenCalled(); expect(mockGetRoomAvailable).toHaveBeenCalled();
+46 -24
View File
@@ -2,35 +2,37 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import isPropValid from '@emotion/is-prop-valid'; import isPropValid from '@emotion/is-prop-valid';
import { StyleSheetManager } from 'styled-components'; import { StyleSheetManager } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect, useMemo, useReducer } from 'react';
// Components // Components
import AppLayout from './components/AppLayout'; import AppLayout from './components/AppLayout';
import Toast from './components/Toast';
import RouteProtected from '@component/RouteProtected';
import RootLayout from '@component/RootLayout';
// Pages
import User from './pages/User'; import User from './pages/User';
import Dashboard from './pages/Dashboard'; import Booking from './pages/Booking';
import Room from './pages/Room'; import Room from './pages/Room';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import Login from '@page/Login';
import Account from '@page/Account';
// Constants // Constants
import * as PATH from './constants/path'; import * as PATH from './constants/path';
import Toast from './components/Toast';
// Hooks
import { import {
UserRoomAvailableContext, UserRoomAvailableContext,
initialState, initialState,
reducer, reducer,
} from '@context/UserRoomAvailableContext'; } from '@context/UserRoomAvailableContext';
import { useEffect, useMemo, useReducer } from 'react';
// Services // Services
import { getUserNotBooked } from '@service/userServices'; import { getAllUsers } from '@service/userServices';
import { getRoomsAvailable } from '@service/roomServices'; import { getAllRooms } from '@service/roomServices';
const queryClient = new QueryClient({ const queryClient = new QueryClient();
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
function App() { function App() {
const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer( const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer(
@@ -41,16 +43,26 @@ function App() {
// Init list user and room available // Init list user and room available
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
const tempUser = await getUserNotBooked(); const tempUser = await getAllUsers({
sortBy: 'id',
orderBy: 'asc',
phoneSearch: '',
page: 0,
});
if (tempUser) { if (tempUser) {
dispatch({ type: 'initUser', payload: tempUser }); dispatch({ type: 'initUser', payload: tempUser.data });
} }
const tempRoom = await getRoomsAvailable(); const tempRoom = await getAllRooms({
sortBy: 'id',
orderBy: 'asc',
roomSearch: '',
page: 0,
});
if (tempRoom) { if (tempRoom) {
dispatch({ type: 'initRoom', payload: tempRoom }); dispatch({ type: 'initRoom', payload: tempRoom.data });
} }
}; };
@@ -67,16 +79,26 @@ function App() {
<UserRoomAvailableContext.Provider value={store}> <UserRoomAvailableContext.Provider value={store}>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route element={<AppLayout />}> <Route element={<RootLayout />}>
<Route <Route
index element={
element={<Navigate replace to={PATH.DASHBOARD} />} <RouteProtected>
/> <AppLayout />
<Route path={PATH.DASHBOARD} element={<Dashboard />} /> </RouteProtected>
<Route path={PATH.USER} element={<User />} /> }
<Route path={PATH.ROOM} element={<Room />} /> >
<Route
index
element={<Navigate replace to={PATH.BOOKING} />}
/>
<Route path={PATH.BOOKING} element={<Booking />} />
<Route path={PATH.USER} element={<User />} />
<Route path={PATH.ROOM} element={<Room />} />
<Route path={PATH.ACCOUNT} element={<Account />} />
</Route>
<Route path={PATH.LOGIN} element={<Login />} />
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
</Route> </Route>
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</UserRoomAvailableContext.Provider> </UserRoomAvailableContext.Provider>
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -16,10 +16,10 @@ const variations: IVariations = {
background-color: var(--danger-btn-color); background-color: var(--danger-btn-color);
color: var(--light-text); color: var(--light-text);
`, `,
} as const; };
interface IButtonStyle { interface IButtonStyle {
variations?: keyof typeof variations; variations?: keyof IVariations;
} }
const Button = styled.button<IButtonStyle>` const Button = styled.button<IButtonStyle>`
@@ -36,7 +36,8 @@ const Button = styled.button<IButtonStyle>`
&:disabled, &:disabled,
&[disabled] { &[disabled] {
cursor: no-drop; cursor: not-allowed;
background-color: var(--disabled-btn-color);
} }
`; `;
@@ -1,6 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
type TDirection = 'vertical' | 'horizontal'; // Types
import { TDirection } from '@type/common';
interface IDirection { interface IDirection {
type?: TDirection; type?: TDirection;
+2 -1
View File
@@ -3,7 +3,7 @@ import styled, { css } from 'styled-components';
// Styled // Styled
import CommonInput from './CommonInput'; import CommonInput from './CommonInput';
type TInput = 'text' | 'checkbox' | 'hidden'; type TInput = 'text' | 'checkbox' | 'hidden' | 'date' | 'password';
interface IInputTyped { interface IInputTyped {
type?: TInput; type?: TInput;
@@ -11,6 +11,7 @@ interface IInputTyped {
const Input = styled.input<IInputTyped>` const Input = styled.input<IInputTyped>`
${CommonInput} ${CommonInput}
width: 160px;
${(props) => ${(props) =>
props.type === 'checkbox' && props.type === 'checkbox' &&
@@ -3,12 +3,17 @@ import { BrowserRouter } from 'react-router-dom';
// Components // Components
import AppLayout from '.'; import AppLayout from '.';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
describe('AppLayout', () => { describe('AppLayout', () => {
const queryClient = new QueryClient();
const wrapper = renderer.create( const wrapper = renderer.create(
<BrowserRouter> <QueryClientProvider client={queryClient}>
<AppLayout /> <BrowserRouter>
</BrowserRouter> <AppLayout />
</BrowserRouter>
</QueryClientProvider>
); );
test('Should render correctly', () => { test('Should render correctly', () => {
@@ -9,7 +9,6 @@ exports[`AppLayout Should render correctly 1`] = `
> >
<p> <p>
Hi, Hi,
Admin
! !
</p> </p>
<ul <ul
@@ -24,6 +23,7 @@ exports[`AppLayout Should render correctly 1`] = `
} }
} }
isHaveChildren={false} isHaveChildren={false}
onClick={[Function]}
style={ style={
{ {
"backgroundColor": undefined, "backgroundColor": undefined,
@@ -60,12 +60,14 @@ exports[`AppLayout Should render correctly 1`] = `
<li> <li>
<button <button
className="sc-bdnyFh bxHnoO" className="sc-bdnyFh bxHnoO"
disabled={false}
iconStyle={ iconStyle={
{ {
"size": "23px", "size": "23px",
} }
} }
isHaveChildren={false} isHaveChildren={false}
onClick={[Function]}
style={ style={
{ {
"backgroundColor": undefined, "backgroundColor": undefined,
@@ -105,7 +107,8 @@ exports[`AppLayout Should render correctly 1`] = `
className="sc-jSFipO ejTmsl" className="sc-jSFipO ejTmsl"
> >
<h1 <h1
className="sc-gKAaef hNxcoF" className="sc-gKAaef kdwrvt"
onClick={[Function]}
> >
Hotel Management Hotel Management
</h1> </h1>
@@ -114,7 +117,7 @@ exports[`AppLayout Should render correctly 1`] = `
> >
<a <a
className="sc-eCAqax hQZHUY" className="sc-eCAqax hQZHUY"
href="/dashboard" href="/booking"
onClick={[Function]} onClick={[Function]}
> >
<svg <svg
@@ -132,15 +135,11 @@ exports[`AppLayout Should render correctly 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M0 0h24v24H0z" d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM5 7h14v2H5V7z"
fill="none"
/>
<path
d="M11 21H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h6v18zm2 0h6c1.1 0 2-.9 2-2v-7h-8v9zm8-11V5c0-1.1-.9-2-2-2h-6v7h8z"
/> />
</svg> </svg>
<span> <span>
Dashboard Booking
</span> </span>
</a> </a>
<a <a
@@ -7,10 +7,15 @@ import Sidebar from '@component/Sidebar';
// Styled // Styled
import { Main, StyledAppLayout } from './styled'; import { Main, StyledAppLayout } from './styled';
// Hooks
import { useAccount } from '@hook/authentication/useAccount';
const AppLayout = () => { const AppLayout = () => {
const { account } = useAccount();
return ( return (
<StyledAppLayout> <StyledAppLayout>
<Header accountName="Admin" /> <Header accountName={account?.user_metadata.fullName} />
<Sidebar heading="Hotel Management" /> <Sidebar heading="Hotel Management" />
<Main> <Main>
<Outlet /> <Outlet />
@@ -1,4 +1,6 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Styled
import { StyledButtonIcon } from './styled'; import { StyledButtonIcon } from './styled';
interface IButtonIcon { interface IButtonIcon {
@@ -14,6 +16,7 @@ interface IButtonIcon {
size?: string; size?: string;
}; };
onClick?: () => void; onClick?: () => void;
disabled?: boolean;
} }
const ButtonIcon = ({ const ButtonIcon = ({
@@ -22,6 +25,7 @@ const ButtonIcon = ({
style, style,
iconStyle, iconStyle,
onClick, onClick,
disabled,
}: IButtonIcon) => { }: IButtonIcon) => {
const isHaveChildren = Boolean(children); const isHaveChildren = Boolean(children);
@@ -35,6 +39,7 @@ const ButtonIcon = ({
}} }}
iconStyle={iconStyle} iconStyle={iconStyle}
onClick={onClick} onClick={onClick}
disabled={disabled}
> >
{icon} <span>{children}</span> {icon} <span>{children}</span>
</StyledButtonIcon> </StyledButtonIcon>
@@ -1,22 +1,33 @@
import renderer from 'react-test-renderer';
// Components // Components
import ConfirmMessage from '.'; import ConfirmMessage from '.';
import { RenderResult, fireEvent, render } from '@testing-library/react';
describe('ConfirmMessage', () => { describe('ConfirmMessage testing snapshot', () => {
const isDisable = true;
const message = 'Hello World!'; const message = 'Hello World!';
const handleOnConfirm = jest.fn(); const handleOnConfirm = jest.fn();
const handleOnCloseModal = jest.fn();
let wrapper: RenderResult | null = null;
const wrapper = renderer.create( beforeEach(() => {
<ConfirmMessage wrapper = render(
disabled={isDisable} <ConfirmMessage
message={message} message={message}
onConfirm={handleOnConfirm} onConfirm={handleOnConfirm}
/> onCloseModal={handleOnCloseModal}
); />
);
});
test('Should render correctly', () => { test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('handleOnConfirm should be call', () => {
const confirmBtn = wrapper!.getByText('Yes');
fireEvent.click(confirmBtn);
expect(handleOnConfirm).toHaveBeenCalled();
expect(handleOnCloseModal).toHaveBeenCalled();
});
}); });
@@ -1,28 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmMessage Should render correctly 1`] = ` exports[`ConfirmMessage testing snapshot Should render correctly 1`] = `
<div {
className="sc-bdnyFh dukbvo" "asFragment": [Function],
> "baseElement": <body>
<p> <div>
Hello World! <div
</p> class="sc-bdnyFh jIONew"
<div> >
<button <p>
className="sc-gtsqUy eZzwwl" Hello World!
disabled={true} </p>
onClick={[MockFunction]} <div>
variations="danger" <button
class="sc-gtsqUy hSoKBM"
variations="danger"
>
Yes
</button>
<button
class="sc-gtsqUy hesuRJ"
variations="secondary"
>
No
</button>
</div>
</div>
</div>
</body>,
"container": <div>
<div
class="sc-bdnyFh jIONew"
> >
Delete <p>
</button> Hello World!
<button </p>
className="sc-gtsqUy JDRgw" <div>
disabled={true} <button
variations="secondary" class="sc-gtsqUy hSoKBM"
> variations="danger"
Cancel >
</button> Yes
</div> </button>
</div> <button
class="sc-gtsqUy hesuRJ"
variations="secondary"
>
No
</button>
</div>
</div>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`; `;
@@ -1,4 +1,4 @@
import { memo } from 'react'; import { memo, useCallback } from 'react';
// Styled // Styled
import { StyledConfirmDelete } from './styled'; import { StyledConfirmDelete } from './styled';
@@ -8,27 +8,27 @@ import Button from '@commonStyle/Button';
interface IConfirmMessage { interface IConfirmMessage {
message: string; message: string;
disabled: boolean;
onConfirm: () => void; onConfirm: () => void;
onCloseModal?: () => void; onCloseModal?: () => void;
} }
const ConfirmMessage = memo( const ConfirmMessage = memo(
({ message, disabled, onConfirm, onCloseModal }: IConfirmMessage) => { ({ message, onConfirm, onCloseModal }: IConfirmMessage) => {
const handleConfirmBtn = useCallback(() => {
onConfirm();
onCloseModal!();
}, [onCloseModal, onConfirm]);
return ( return (
<StyledConfirmDelete> <StyledConfirmDelete>
<p>{message}</p> <p>{message}</p>
<div> <div>
<Button variations="danger" disabled={disabled} onClick={onConfirm}> <Button variations="danger" onClick={handleConfirmBtn}>
Delete Yes
</Button> </Button>
<Button <Button variations="secondary" onClick={onCloseModal}>
variations="secondary" No
disabled={disabled}
onClick={onCloseModal}
>
Cancel
</Button> </Button>
</div> </div>
</StyledConfirmDelete> </StyledConfirmDelete>
@@ -13,7 +13,7 @@ const StyledConfirmDelete = styled.div`
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
padding-inline: 50px; padding-inline: 100px;
} }
`; `;
@@ -4,9 +4,6 @@ import renderer from 'react-test-renderer';
import Form from '.'; import Form from '.';
import Input from '@commonStyle/Input'; import Input from '@commonStyle/Input';
// Styled
import { FormBtn } from '@page/User/styled';
describe('Form ', () => { describe('Form ', () => {
const isDisable = true; const isDisable = true;
const errorMessage = 'This is error message'; const errorMessage = 'This is error message';
@@ -24,12 +21,12 @@ describe('Form ', () => {
</Form.Row> </Form.Row>
<Form.Action> <Form.Action>
<FormBtn type="submit" name="submit" disabled={isDisable}> <Form.Button type="submit" name="submit" disabled={isDisable}>
Confirm Confirm
</FormBtn> </Form.Button>
<FormBtn type="button" variations="secondary" onClick={handleOnClick}> <Form.Button type="button" variations="secondary" onClick={handleOnClick}>
Close Close
</FormBtn> </Form.Button>
</Form.Action> </Form.Action>
</Form> </Form>
); );
@@ -2,56 +2,58 @@
exports[`Form Should render correctly 1`] = ` exports[`Form Should render correctly 1`] = `
<form <form
className="sc-bdnyFh bzxSLR" className="sc-gtsqUy jMGskL"
onSubmit={[MockFunction]} onSubmit={[MockFunction]}
> >
<div <div
className="sc-dlniIP gZHESL" className="sc-hKFymg ivIEWo"
direction="horizontal"
> >
<label <label
className="sc-hKFymg ddFGOI" className="sc-eCAqax dnXnhF"
> >
Name Name
</label> </label>
<div> <div>
<input <input
className="sc-jSFipO enjwJE" className="sc-iCoHzw csWeiH"
id="name" id="name"
type="text" type="text"
/> />
<p <p
className="sc-eCAqax bvOWPP" className="sc-jSFipO iOcxfq"
> >
This is error message This is error message
</p> </p>
</div> </div>
</div> </div>
<div <div
className="sc-dlniIP gZHESL" className="sc-hKFymg ivIEWo"
direction="horizontal"
> >
<label <label
className="sc-hKFymg ddFGOI" className="sc-eCAqax dnXnhF"
> >
Price Price
</label> </label>
<div> <div>
<input <input
className="sc-jSFipO enjwJE" className="sc-iCoHzw csWeiH"
id="price" id="price"
type="text" type="text"
/> />
<p <p
className="sc-eCAqax bvOWPP" className="sc-jSFipO iOcxfq"
> >
This is error message This is error message
</p> </p>
</div> </div>
</div> </div>
<div <div
className="sc-gtsqUy idhRSV" className="sc-dlniIP hBqqvG"
> >
<button <button
className="sc-gKAaef sc-jrsKJM gifCOZ KLoQf" className="sc-bdnyFh sc-gKAaef bEtbaY fRlVwz"
disabled={true} disabled={true}
name="submit" name="submit"
type="submit" type="submit"
@@ -60,7 +62,7 @@ exports[`Form Should render correctly 1`] = `
Confirm Confirm
</button> </button>
<button <button
className="sc-gKAaef sc-jrsKJM gbgZUe KLoQf" className="sc-bdnyFh sc-gKAaef buXbPz fRlVwz"
onClick={[MockFunction]} onClick={[MockFunction]}
type="button" type="button"
variations="secondary" variations="secondary"
@@ -7,8 +7,12 @@ import {
Error, Error,
Label, Label,
StyledFormRow, StyledFormRow,
FormBtn,
} from './styled'; } from './styled';
// Types
import { TDirection } from '@type/common';
interface IFormProps { interface IFormProps {
children: ReactNode; children: ReactNode;
onSubmit: (event: FormEvent<HTMLFormElement>) => void; onSubmit: (event: FormEvent<HTMLFormElement>) => void;
@@ -19,6 +23,7 @@ interface IFormRow {
label: string; label: string;
error?: string; error?: string;
children: ReactNode; children: ReactNode;
direction?: TDirection;
} }
const Form = ({ children, onSubmit, id }: IFormProps) => { const Form = ({ children, onSubmit, id }: IFormProps) => {
@@ -29,10 +34,9 @@ const Form = ({ children, onSubmit, id }: IFormProps) => {
); );
}; };
const FormRow = ({ label, error, children, direction }: IFormRow) => {
const FormRow = ({ label, error, children }: IFormRow) => {
return ( return (
<StyledFormRow> <StyledFormRow direction={direction}>
<Label>{label}</Label> <Label>{label}</Label>
<div> <div>
{children} {children}
@@ -44,5 +48,6 @@ const FormRow = ({ label, error, children }: IFormRow) => {
Form.Row = FormRow; Form.Row = FormRow;
Form.Action = StyledActionBtn; Form.Action = StyledActionBtn;
Form.Button = FormBtn;
export default Form; export default Form;
+37 -7
View File
@@ -1,4 +1,14 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
// Types
import { TDirection } from '@type/common';
interface IStyledFormRow {
direction?: TDirection;
}
// Styled
import Button from '@commonStyle/Button';
const StyledForm = styled.form` const StyledForm = styled.form`
display: flex; display: flex;
@@ -13,11 +23,20 @@ const StyledActionBtn = styled.div`
gap: 100px; gap: 100px;
`; `;
const StyledFormRow = styled.div` const StyledFormRow = styled.div<IStyledFormRow>`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
gap: 100px; ${(props) =>
props.direction === 'horizontal'
? css`
align-items: center;
gap: 100px;
`
: css`
flex-direction: column;
gap: 15px;
`}
`; `;
const Label = styled.label` const Label = styled.label`
@@ -31,8 +50,19 @@ const Error = styled.p`
margin-top: 5px; margin-top: 5px;
padding-inline: 5px; padding-inline: 5px;
width: 220px;
`; `;
export { StyledForm, StyledActionBtn, StyledFormRow, Label, Error }; const FormBtn = styled(Button)`
width: 100%;
&:disabled,
&[disabled] {
background-color: var(--disabled-btn-color);
}
`;
StyledFormRow.defaultProps = {
direction: 'horizontal',
};
export { StyledForm, StyledActionBtn, StyledFormRow, Label, Error, FormBtn };
@@ -2,9 +2,19 @@ import renderer from 'react-test-renderer';
// Components // Components
import Header from '.'; import Header from '.';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
describe('Header', () => { describe('Header', () => {
const wrapper = renderer.create(<Header accountName="Nezumi" />); const queryClient = new QueryClient();
const wrapper = renderer.create(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Header accountName="Nezumi" />
</BrowserRouter>
</QueryClientProvider>
);
test('Should render correctly', () => { test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
@@ -21,6 +21,7 @@ exports[`Header Should render correctly 1`] = `
} }
} }
isHaveChildren={false} isHaveChildren={false}
onClick={[Function]}
style={ style={
{ {
"backgroundColor": undefined, "backgroundColor": undefined,
@@ -57,12 +58,14 @@ exports[`Header Should render correctly 1`] = `
<li> <li>
<button <button
className="sc-bdnyFh bxHnoO" className="sc-bdnyFh bxHnoO"
disabled={false}
iconStyle={ iconStyle={
{ {
"size": "23px", "size": "23px",
} }
} }
isHaveChildren={false} isHaveChildren={false}
onClick={[Function]}
style={ style={
{ {
"backgroundColor": undefined, "backgroundColor": undefined,
@@ -2,9 +2,19 @@ import renderer from 'react-test-renderer';
// Components // Components
import HeaderMenu from '.'; import HeaderMenu from '.';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
describe('HeaderMenu', () => { describe('HeaderMenu', () => {
const wrapper = renderer.create(<HeaderMenu />); const queryClient = new QueryClient();
const wrapper = renderer.create(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<HeaderMenu />
</BrowserRouter>
</QueryClientProvider>
);
test('Should render correctly', () => { test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
@@ -13,6 +13,7 @@ exports[`HeaderMenu Should render correctly 1`] = `
} }
} }
isHaveChildren={false} isHaveChildren={false}
onClick={[Function]}
style={ style={
{ {
"backgroundColor": undefined, "backgroundColor": undefined,
@@ -49,12 +50,14 @@ exports[`HeaderMenu Should render correctly 1`] = `
<li> <li>
<button <button
className="sc-bdnyFh bxHnoO" className="sc-bdnyFh bxHnoO"
disabled={false}
iconStyle={ iconStyle={
{ {
"size": "23px", "size": "23px",
} }
} }
isHaveChildren={false} isHaveChildren={false}
onClick={[Function]}
style={ style={
{ {
"backgroundColor": undefined, "backgroundColor": undefined,
@@ -1,3 +1,5 @@
import { useNavigate } from 'react-router-dom';
// Components // Components
import { IoPersonCircleOutline } from 'react-icons/io5'; import { IoPersonCircleOutline } from 'react-icons/io5';
import { HiOutlineLogout } from 'react-icons/hi'; import { HiOutlineLogout } from 'react-icons/hi';
@@ -6,7 +8,13 @@ import ButtonIcon from '@component/ButtonIcon';
// Styled // Styled
import { StyledHeaderMenu } from './styled'; import { StyledHeaderMenu } from './styled';
// Hooks
import { useLogout } from '@hook/authentication/useLogout';
const HeaderMenu = () => { const HeaderMenu = () => {
const { logout, isPending } = useLogout();
const navigate = useNavigate();
return ( return (
<StyledHeaderMenu> <StyledHeaderMenu>
<li> <li>
@@ -14,13 +22,16 @@ const HeaderMenu = () => {
aria-label="Profile" aria-label="Profile"
icon={<IoPersonCircleOutline />} icon={<IoPersonCircleOutline />}
iconStyle={{ size: '23px' }} iconStyle={{ size: '23px' }}
onClick={() => navigate('/account')}
/> />
</li> </li>
<li> <li>
<ButtonIcon <ButtonIcon
onClick={logout}
disabled={isPending}
aria-label="Logout" aria-label="Logout"
icon={<HiOutlineLogout />} icon={<HiOutlineLogout />}
iconStyle={{size: '23px' }} iconStyle={{ size: '23px' }}
/> />
</li> </li>
</StyledHeaderMenu> </StyledHeaderMenu>
@@ -23,6 +23,7 @@ interface IButton {
children?: string; children?: string;
icon?: ReactNode; icon?: ReactNode;
onClick?: () => void; onClick?: () => void;
disabled?: boolean;
} }
const Menus = ({ children }: { children: ReactNode }) => { const Menus = ({ children }: { children: ReactNode }) => {
@@ -69,7 +70,7 @@ const List = ({
return <StyledList ref={ref}>{children}</StyledList>; return <StyledList ref={ref}>{children}</StyledList>;
}; };
const Button = ({ children, icon, onClick }: IButton): ReactNode => { const Button = ({ children, icon, onClick, disabled }: IButton): ReactNode => {
const { close } = useContext(MenusContext); const { close } = useContext(MenusContext);
const handleClick = () => { const handleClick = () => {
onClick?.(); onClick?.();
@@ -83,6 +84,7 @@ const Button = ({ children, icon, onClick }: IButton): ReactNode => {
onClick={handleClick} onClick={handleClick}
iconStyle={{ color: COLOR.PRIMARY, size: '19px' }} iconStyle={{ color: COLOR.PRIMARY, size: '19px' }}
style={{ fontSize: '16px' }} style={{ fontSize: '16px' }}
disabled={disabled}
> >
{children} {children}
</ButtonIcon> </ButtonIcon>
@@ -2,7 +2,7 @@
exports[`Modal Should render correctly 1`] = ` exports[`Modal Should render correctly 1`] = `
<button <button
className="sc-eCAqax jeAcqZ" className="sc-eCAqax iNUTro"
onClick={[Function]} onClick={[Function]}
variations="primary" variations="primary"
> >
@@ -6,13 +6,6 @@ const StyledModal = styled.div`
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background-color: var(--color-grey-0);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
padding: 3.2rem 4rem;
transition: all 0.5s; transition: all 0.5s;
`; `;
@@ -6,7 +6,7 @@ exports[`Nav Should render correctly 1`] = `
> >
<a <a
className="sc-gtsqUy gbSQWi" className="sc-gtsqUy gbSQWi"
href="/dashboard" href="/booking"
onClick={[Function]} onClick={[Function]}
> >
<svg <svg
@@ -24,15 +24,11 @@ exports[`Nav Should render correctly 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M0 0h24v24H0z" d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM5 7h14v2H5V7z"
fill="none"
/>
<path
d="M11 21H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h6v18zm2 0h6c1.1 0 2-.9 2-2v-7h-8v9zm8-11V5c0-1.1-.9-2-2-2h-6v7h8z"
/> />
</svg> </svg>
<span> <span>
Dashboard Booking
</span> </span>
</a> </a>
<a <a
@@ -1,5 +1,5 @@
// Components // Components
import { MdSpaceDashboard } from 'react-icons/md'; import { BiSolidCalendarAlt } from 'react-icons/bi';
import { HiUsers } from 'react-icons/hi2'; import { HiUsers } from 'react-icons/hi2';
import { BsHouseDoorFill } from 'react-icons/bs'; import { BsHouseDoorFill } from 'react-icons/bs';
@@ -7,14 +7,14 @@ import { BsHouseDoorFill } from 'react-icons/bs';
import { StyledNav, StyledNavLink } from './styled'; import { StyledNav, StyledNavLink } from './styled';
// Constants // Constants
import { DASHBOARD, ROOM, USER } from '@constant/path'; import { BOOKING, ROOM, USER } from '@constant/path';
const Nav = () => { const Nav = () => {
return ( return (
<StyledNav> <StyledNav>
<StyledNavLink to={DASHBOARD}> <StyledNavLink to={BOOKING}>
<MdSpaceDashboard /> <BiSolidCalendarAlt />
<span>Dashboard</span> <span>Booking</span>
</StyledNavLink> </StyledNavLink>
<StyledNavLink to={USER}> <StyledNavLink to={USER}>
<HiUsers /> <HiUsers />
@@ -9,7 +9,7 @@ exports[`Order Should render correctly 1`] = `
class="sc-bdnyFh aZFGz" class="sc-bdnyFh aZFGz"
> >
<button <button
class="sc-gtsqUy fohQqJ" class="sc-gtsqUy bOTmZQ"
disabled="" disabled=""
> >
Option 1 Option 1
@@ -27,7 +27,7 @@ exports[`Order Should render correctly 1`] = `
class="sc-bdnyFh aZFGz" class="sc-bdnyFh aZFGz"
> >
<button <button
class="sc-gtsqUy fohQqJ" class="sc-gtsqUy bOTmZQ"
disabled="" disabled=""
> >
Option 1 Option 1
@@ -26,7 +26,6 @@ const OrderBy = ({ options }: IOrderProps) => {
<OrderButton <OrderButton
key={option.value} key={option.value}
onClick={() => handleClick(option.value)} onClick={() => handleClick(option.value)}
active={option.value === currentOrder}
disabled={option.value === currentOrder} disabled={option.value === currentOrder}
> >
{option.label} {option.label}
@@ -9,7 +9,7 @@ const StyledOrder = styled.div`
`; `;
interface IOrderBtn { interface IOrderBtn {
active: boolean; disabled: boolean;
} }
const OrderButton = styled.button<IOrderBtn>` const OrderButton = styled.button<IOrderBtn>`
@@ -25,11 +25,11 @@ const OrderButton = styled.button<IOrderBtn>`
padding: 5px 10px; padding: 5px 10px;
${(props) => ${(props) =>
props.active && props.disabled &&
css` css`
background-color: var(--primary-color); background-color: var(--primary-color);
color: var(--light-text); color: var(--light-text);
cursor: no-drop; cursor: not-allowed;
`} `}
&:hover:not(:disabled) { &:hover:not(:disabled) {
@@ -0,0 +1,80 @@
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
// Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config';
// Styled
import { Buttons, PaginationBtn, StyledPagination } from './styled';
// Components
import { MdKeyboardArrowRight, MdKeyboardArrowLeft } from 'react-icons/md';
interface IPagination {
count: number;
}
const Pagination = ({ count }: IPagination) => {
const [searchParams, setSearchParams] = useSearchParams();
const currentPage = !searchParams.get('page')
? 1
: Number(searchParams.get('page'));
const totalPage = Math.ceil(count / DEFAULT_PAGE_SIZE);
const nextPage = useCallback(() => {
const next = currentPage === totalPage
? currentPage
: currentPage + 1;
searchParams.set('page', next.toString());
setSearchParams(searchParams);
}, [currentPage, searchParams, totalPage, setSearchParams]);
const previousPage = useCallback(() => {
const previous = currentPage === 1
? currentPage
: currentPage - 1;
searchParams.set('page', previous.toString());
setSearchParams(searchParams);
}, [currentPage, searchParams, setSearchParams]);
const fromIndex = useMemo(
() => (currentPage - 1) * DEFAULT_PAGE_SIZE + 1,
[currentPage]
);
const toIndex = useMemo(
() => (currentPage === totalPage
? count
: currentPage * DEFAULT_PAGE_SIZE),
[currentPage, count, totalPage]
);
return (
totalPage > 1 && (
<StyledPagination>
<p>
Showing {fromIndex} to {toIndex} of <span>{count}</span> results
</p>
<Buttons>
<PaginationBtn onClick={previousPage} disabled={currentPage === 1}>
<MdKeyboardArrowLeft /> <span>Previous</span>
</PaginationBtn>
<PaginationBtn
onClick={nextPage}
disabled={currentPage === totalPage}
>
<span>Next</span>
<MdKeyboardArrowRight />
</PaginationBtn>
</Buttons>
</StyledPagination>
)
);
};
export default Pagination;
@@ -0,0 +1,50 @@
import styled from 'styled-components';
interface IPaginationBtn {
disabled: boolean;
}
const StyledPagination = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--fs-sm-x);
`;
const Buttons = styled.div`
display: flex;
gap: 30px;
`;
const PaginationBtn = styled.button<IPaginationBtn>`
background-color: var(--secondary-btn-color);
border: none;
border-radius: var(--radius-sm);
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 5px 10px;
transition: all 0.3s;
& svg {
height: 20px;
width: 20px;
}
&:hover:not(:disabled) {
background-color: var(--primary-color);
color: var(--light-text);
}
&:disabled {
cursor: not-allowed;
}
`;
export { StyledPagination, PaginationBtn, Buttons };
@@ -0,0 +1,24 @@
import { Outlet } from 'react-router-dom';
// Styled
import Spinner from '@commonStyle/Spinner';
import { FullPageLayout } from './styled';
// Hooks
import { useAccount } from '@hook/authentication/useAccount';
const RootLayout = () => {
const { isPending } = useAccount();
return isPending ? (
<FullPageLayout>
<Spinner />
</FullPageLayout>
) : (
<>
<Outlet />
</>
);
};
export default RootLayout;
@@ -0,0 +1,10 @@
import styled from 'styled-components';
const FullPageLayout = styled.div`
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
`;
export { FullPageLayout };
@@ -0,0 +1,15 @@
import { useAccount } from '@hook/authentication/useAccount';
import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';
interface IRouteProtected {
children: ReactNode;
}
const RouteProtected = ({ children }: IRouteProtected) => {
// Load user login
const { account } = useAccount();
return !account ? <Navigate to="/login" /> : children;
};
export default RouteProtected;
@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Search Should render correctly 1`] = `
{
"asFragment": [Function],
"baseElement": <body>
<div>
<input
class="sc-bdnyFh eAgOPb"
placeholder="Search placeholder..."
/>
</div>
</body>,
"container": <div>
<input
class="sc-bdnyFh eAgOPb"
placeholder="Search placeholder..."
/>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
@@ -4,7 +4,8 @@ const StyledSearch = styled.input`
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
font-size: var(--fs-sm-x); font-size: var(--fs-sm-x);
padding: 5px 10px; padding: 10px 10px;
`; `;
export { StyledSearch }; export { StyledSearch };
@@ -3,7 +3,8 @@
exports[`Select Should render correctly 1`] = ` exports[`Select Should render correctly 1`] = `
<select <select
aria-label="Sort" aria-label="Sort"
className="sc-bdnyFh dqXTRP" className="sc-bdnyFh hPOEsG"
disabled={false}
onChange={[MockFunction]} onChange={[MockFunction]}
value="Temp value" value="Temp value"
> >
@@ -19,6 +19,7 @@ interface ISelect {
onChange?: ChangeEventHandler<HTMLSelectElement> | undefined; onChange?: ChangeEventHandler<HTMLSelectElement> | undefined;
id?: string; id?: string;
ariaLabel: string; ariaLabel: string;
disable?: boolean;
} }
const Select = ({ const Select = ({
@@ -28,6 +29,7 @@ const Select = ({
value, value,
onChange, onChange,
ariaLabel, ariaLabel,
disable = false,
}: ISelect) => { }: ISelect) => {
const { register } = useFormContext() ?? {}; const { register } = useFormContext() ?? {};
@@ -38,6 +40,7 @@ const Select = ({
aria-label={ariaLabel} aria-label={ariaLabel}
id={id} id={id}
value={value} value={value}
disabled={disable}
{...(register {...(register
? { ...register(id!, optionsConfigForm) } ? { ...register(id!, optionsConfigForm) }
: { onChange: onChange })} : { onChange: onChange })}
@@ -6,6 +6,8 @@ const StyledSelect = styled.select`
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-weight: 500; font-weight: 500;
width: 200px;
`; `;
export { StyledSelect }; export { StyledSelect };
@@ -5,7 +5,8 @@ exports[`Sidebar Should render correctly 1`] = `
className="sc-dlniIP dfcuiS" className="sc-dlniIP dfcuiS"
> >
<h1 <h1
className="sc-hKFymg iQEJzQ" className="sc-hKFymg OpLao"
onClick={[Function]}
> >
Hotel Management Hotel Management
</h1> </h1>
@@ -14,7 +15,7 @@ exports[`Sidebar Should render correctly 1`] = `
> >
<a <a
className="sc-gtsqUy gbSQWi" className="sc-gtsqUy gbSQWi"
href="/dashboard" href="/booking"
onClick={[Function]} onClick={[Function]}
> >
<svg <svg
@@ -32,15 +33,11 @@ exports[`Sidebar Should render correctly 1`] = `
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M0 0h24v24H0z" d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM5 7h14v2H5V7z"
fill="none"
/>
<path
d="M11 21H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2h6v18zm2 0h6c1.1 0 2-.9 2-2v-7h-8v9zm8-11V5c0-1.1-.9-2-2-2h-6v7h8z"
/> />
</svg> </svg>
<span> <span>
Dashboard Booking
</span> </span>
</a> </a>
<a <a
@@ -3,15 +3,18 @@ import Nav from '@component/Nav';
// Styled // Styled
import { Heading, StyledSidebar } from './styled'; import { Heading, StyledSidebar } from './styled';
import { useNavigate } from 'react-router-dom';
interface ISidebar { interface ISidebar {
heading: string; heading: string;
} }
const Sidebar = ({ heading }: ISidebar) => { const Sidebar = ({ heading }: ISidebar) => {
const navigate = useNavigate();
return ( return (
<StyledSidebar> <StyledSidebar>
<Heading>{heading}</Heading> <Heading onClick={() => navigate('/')}>{heading}</Heading>
<Nav /> <Nav />
</StyledSidebar> </StyledSidebar>
); );
@@ -13,6 +13,8 @@ const Heading = styled.h1`
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
cursor: pointer;
color: var(--primary-color); color: var(--primary-color);
`; `;
@@ -7,7 +7,7 @@ exports[`SortBy Should render correctly 1`] = `
<div> <div>
<select <select
aria-label="Sort" aria-label="Sort"
class="sc-bdnyFh dqXTRP" class="sc-bdnyFh hPOEsG"
> >
<option <option
data-testid="select-option" data-testid="select-option"
@@ -27,7 +27,7 @@ exports[`SortBy Should render correctly 1`] = `
"container": <div> "container": <div>
<select <select
aria-label="Sort" aria-label="Sort"
class="sc-bdnyFh dqXTRP" class="sc-bdnyFh hPOEsG"
> >
<option <option
data-testid="select-option" data-testid="select-option"
@@ -4,7 +4,7 @@ import renderer from 'react-test-renderer';
import Table from '.'; import Table from '.';
// Types // Types
import { IUser } from '@type/users'; import { IUser } from '@type/user';
interface ITableRow { interface ITableRow {
user: IUser; user: IUser;
@@ -18,12 +18,14 @@ describe('Table', () => {
name: 'Nezumi', name: 'Nezumi',
phone: '123', phone: '123',
isBooked: true, isBooked: true,
isDelete: true,
}, },
{ {
id: 2, id: 2,
name: 'Loi Phan', name: 'Loi Phan',
phone: '453', phone: '453',
isBooked: false, isBooked: false,
isDelete: true,
}, },
]; ];
const TableRow = ({ user }: ITableRow) => { const TableRow = ({ user }: ITableRow) => {
@@ -1,7 +1,13 @@
import { ReactNode, useContext } from 'react'; import { ReactNode, useContext } from 'react';
// Components // Styled
import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled'; import {
StyledBody,
StyledHeader,
StyledRow,
StyledTable,
StyledFooter,
} from './styled';
// Contexts // Contexts
import TableContext from '@context/TableContext'; import TableContext from '@context/TableContext';
@@ -33,9 +39,13 @@ const Table = ({ columns, children }: ITable) => {
const Header = ({ headerColumn }: IHeader) => { const Header = ({ headerColumn }: IHeader) => {
const { columns } = useContext(TableContext); const { columns } = useContext(TableContext);
return <StyledHeader columns={columns}> return (
{headerColumn.map((item) => <div key={item}>{item}</div>)} <StyledHeader columns={columns}>
</StyledHeader>; {headerColumn.map((item) => (
<div key={item}>{item}</div>
))}
</StyledHeader>
);
}; };
const Body = <T,>({ data, render }: ITableBody<T>) => { const Body = <T,>({ data, render }: ITableBody<T>) => {
@@ -55,5 +65,6 @@ const Row = ({ children }: ITable) => {
Table.Header = Header; Table.Header = Header;
Table.Body = Body; Table.Body = Body;
Table.Row = Row; Table.Row = Row;
Table.Footer = StyledFooter;
export default Table; export default Table;
@@ -43,4 +43,19 @@ const StyledHeader = styled(CommonRow)`
font-weight: 600; font-weight: 600;
`; `;
export { StyledBody, StyledHeader, StyledRow, StyledTable }; const StyledFooter = styled.div`
display: flex;
justify-content: center;
padding: 15px;
background-color: var(--footer-table-color);
border-bottom-left-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
border-top: 1px solid var(--border-color);
&:not(:has(*)) {
display: none;
}
`;
export { StyledBody, StyledHeader, StyledRow, StyledTable, StyledFooter };
+4 -7
View File
@@ -48,18 +48,15 @@ const FORM = {
DELETE: 'delete', DELETE: 'delete',
USER: 'user-form', USER: 'user-form',
ROOM: 'room-form', ROOM: 'room-form',
BOOKING: 'booking-form',
CHECKOUT: 'checkout',
}; };
const REGEX = { const REGEX = {
PHONE: /(0[3|5|7|8|9])+([0-9]{8})\b/g, PHONE: /(0[3|5|7|8|9])+([0-9]{8})\b/g,
NAME: /^[a-zA-Z]{2,}(?: [a-zA-Z]+){0,2}$/gm, NAME: /^[a-zA-Z]{2,}(?: [a-zA-Z]+){0,2}$/gm,
NUMBER: /^[0-9]*$/, NUMBER: /^[0-9]*$/,
PASSWORD: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[#$@!%&*?])[A-Za-z\d#$@!%&*?]{8,16}$/
}; };
export { export { USER_PAGE, ROOM_PAGE, FORM, REGEX, ORDERBY_OPTIONS };
USER_PAGE,
ROOM_PAGE,
FORM,
REGEX,
ORDERBY_OPTIONS,
};
+8 -1
View File
@@ -1,6 +1,13 @@
const DEFAULT_SORT_BY = 'id'; const DEFAULT_SORT_BY = 'id';
const DEFAULT_ORDER_BY = 'asc'; const DEFAULT_ORDER_BY = 'asc';
const DEFAULT_PAGE_SIZE = 5;
const supabaseUrl = 'https://pjqujsjzzdrlrgqbxepe.supabase.co'; const supabaseUrl = 'https://pjqujsjzzdrlrgqbxepe.supabase.co';
const supabaseKey = process.env.VITE_SUPABASE_KEY; const supabaseKey = process.env.VITE_SUPABASE_KEY;
export { DEFAULT_SORT_BY, DEFAULT_ORDER_BY, supabaseKey, supabaseUrl }; export {
DEFAULT_SORT_BY,
DEFAULT_ORDER_BY,
supabaseKey,
supabaseUrl,
DEFAULT_PAGE_SIZE,
};
@@ -2,5 +2,19 @@ const INVALID_FIELD = 'This field is invalid format!';
const INVALID_PHONE = 'Phone number is invalid!'; const INVALID_PHONE = 'Phone number is invalid!';
const REQUIRED_FIELD_ERROR = 'This field is required!'; const REQUIRED_FIELD_ERROR = 'This field is required!';
const INVALID_DISCOUNT = 'Discount should be greater than 0 and less than 100!'; const INVALID_DISCOUNT = 'Discount should be greater than 0 and less than 100!';
const PASSWORD_CONDITION =
'Your password must have 1 uppercase, 1 lowercase, 1 special character, min 1 number and the length between from 8 to 16!';
const INVALID_PASSWORD = 'Invalid password';
const PASSWORD_NOT_MATCH = 'Password not match!';
const PASSWORD_NOT_MATCH_REQUIREMENT = 'Password not match the requirement!'
export { REQUIRED_FIELD_ERROR, INVALID_DISCOUNT, INVALID_FIELD, INVALID_PHONE }; export {
REQUIRED_FIELD_ERROR,
INVALID_DISCOUNT,
INVALID_FIELD,
INVALID_PHONE,
PASSWORD_CONDITION,
INVALID_PASSWORD,
PASSWORD_NOT_MATCH,
PASSWORD_NOT_MATCH_REQUIREMENT
};
+46 -5
View File
@@ -1,9 +1,33 @@
const ADD_SUCCESS = 'Add success'; const ADD_SUCCESS = 'Add success!';
const UPDATE_SUCCESS = 'Update success'; const UPDATE_SUCCESS = 'Update success!';
const CONFIRM_MESSAGE = 'Do you want to checkout this user?'; const CONFIRM_MESSAGE = 'Do you want to checkout this user?';
const CONFIRM_DELETE = 'Are you sure to delete it?' const CONFIRM_DELETE = 'Are you sure to delete it?';
const DELETE_SUCCESS = 'Delete success'; const DELETE_SUCCESS = 'Delete success!';
const CHECKOUT_SUCCESS = 'Check out success';
// Booking connection messages
const BOOKINGS_TABLE = 'bookings';
const ERROR_FETCHING_BOOKING = "Can't fetch booking data!";
const ERROR_UPDATE_BOOKING = "Can't update booking!";
const ERROR_CREATE_BOOKING = "Can't create booking!";
const ERROR_DELETE_BOOKING = "Can't delete booking!";
const CHECKOUT_SUCCESS = 'Check out success!';
const ERROR_CHECKOUT = "Can't checkout!";
// Room connection messages
const ROOMS_TABLE = 'rooms';
const ERROR_FETCHING_ROOM = "Can't fetch room data!";
const ERROR_UPDATE_ROOM = "Can't update room!";
const ERROR_CREATE_ROOM = "Can't create room!";
const ERROR_DELETE_ROOM = "Can't delete room!";
// User connection messages
const USERS_TABLE = 'users';
const ERROR_FETCHING_USER = "Users can't be loaded!";
const ERROR_CREATE_USER = "Can't create user!";
const ERROR_UPDATE_USER = "Can't update user!";
const ERROR_DELETE_USER = "Can't delete user!";
const UPDATE_ACCOUNT_SUCCESSFUL = 'Update account successfully!';
export { export {
ADD_SUCCESS, ADD_SUCCESS,
@@ -12,4 +36,21 @@ export {
CONFIRM_DELETE, CONFIRM_DELETE,
DELETE_SUCCESS, DELETE_SUCCESS,
CHECKOUT_SUCCESS, CHECKOUT_SUCCESS,
ERROR_CHECKOUT,
BOOKINGS_TABLE,
ERROR_FETCHING_BOOKING,
ERROR_UPDATE_BOOKING,
ERROR_CREATE_BOOKING,
ERROR_DELETE_BOOKING,
ROOMS_TABLE,
ERROR_FETCHING_ROOM,
ERROR_UPDATE_ROOM,
ERROR_DELETE_ROOM,
ERROR_CREATE_ROOM,
USERS_TABLE,
ERROR_FETCHING_USER,
ERROR_CREATE_USER,
ERROR_UPDATE_USER,
ERROR_DELETE_USER,
UPDATE_ACCOUNT_SUCCESSFUL,
}; };
+5 -1
View File
@@ -1,4 +1,8 @@
export const DASHBOARD = '/dashboard'; export const BOOKING = '/booking';
export const USER = '/user'; export const USER = '/user';
export const ROOM = '/room'; export const ROOM = '/room';
export const ACCOUNT = '/account';
export const OTHER_PATH = '*'; export const OTHER_PATH = '*';
export const USER_PATH = 'users';
export const ROOM_PATH = 'rooms';
export const LOGIN = 'login';
@@ -12,11 +12,13 @@ type TAction =
| 'initRoom' | 'initRoom'
| 'initUser' | 'initUser'
| 'addRoom' | 'addRoom'
| 'removeRoom' | 'updateStatusRoom'
| 'addUser' | 'addUser'
| 'removeUser' | 'updateStatusUser'
| 'updateUserName' | 'updateUserName'
| 'updateRoomName'; | 'updateRoomName'
| 'removeUser'
| 'removeRoom';
interface IAction { interface IAction {
type: TAction; type: TAction;
@@ -38,7 +40,6 @@ const initialState: IUserRoomState = {
const reducer = (state: IUserRoomState, action: IAction) => { const reducer = (state: IUserRoomState, action: IAction) => {
switch (action.type) { switch (action.type) {
case 'initRoom': case 'initRoom':
return { return {
...state, ...state,
@@ -84,13 +85,24 @@ const reducer = (state: IUserRoomState, action: IAction) => {
}; };
} }
case 'removeUser': case 'updateStatusUser': {
const tempArr = state.usersAvailable;
const indexItemUpdate = tempArr.findIndex(
(item) => item.id === action.payload[0].id
);
tempArr[indexItemUpdate].isBooked = action.payload[0].isBooked;
return { return {
...state, ...state,
usersAvailable: state.usersAvailable.filter( usersAvailable: tempArr,
(item) => item.id !== action.payload[0].id
),
}; };
}
case 'removeUser':
return {
...state,
usersAvailable: state.usersAvailable.filter((user) => user.id !== action.payload[0].id)
}
case 'addRoom': { case 'addRoom': {
const tempArr = state.roomsAvailable; const tempArr = state.roomsAvailable;
@@ -125,14 +137,25 @@ const reducer = (state: IUserRoomState, action: IAction) => {
}; };
} }
case 'removeRoom': case 'updateStatusRoom': {
const tempArr = state.roomsAvailable;
const indexItemUpdate = tempArr.findIndex(
(item) => item.id === action.payload[0].id
);
tempArr[indexItemUpdate].status = action.payload[0].status;
return { return {
...state, ...state,
roomsAvailable: state.roomsAvailable.filter( roomsAvailable: tempArr,
(item) => item.id !== action.payload[0].id
),
}; };
}
case 'removeRoom':
return {
...state,
roomsAvailable: state.roomsAvailable.filter((room) => room.id !== action.payload[0].id)
}
default: default:
throw new Error('Action unknown'); throw new Error('Action unknown');
} }
+25 -1
View File
@@ -10,4 +10,28 @@ const formatCurrency = (value: number): string => {
}).format(value); }).format(value);
}; };
export { formatCurrency };
/**
* Calculate the day durations
* @param startDate The start of date
* @param endDate The end of date
* @returns The number of days between 2 date input
*/
const getDayDiff = (startDate: Date, endDate: Date): number => {
const msInDay = 24 * 60 * 60 * 1000;
return Math.round(
Math.abs(Number(endDate) - Number(startDate)) / msInDay
);
}
/**
* Convert from currency string to the number
* @param currency The currency string
* @returns The number
*/
const convertCurrencyToNumber = (currency: string) => {
return Number(currency.replace(/[^0-9.-]+/g,""))
}
export { formatCurrency, getDayDiff, convertCurrencyToNumber };
+5 -1
View File
@@ -26,4 +26,8 @@ const isValidString = (value: string): boolean => {
return value.trim().length >= 5; return value.trim().length >= 5;
}; };
export { isValidRegex, isValidString, isValidDiscount }; const compareTwoDates = (startDate: Date, endDate: Date): boolean => {
return startDate > endDate;
};
export { isValidRegex, isValidString, isValidDiscount, compareTwoDates };
@@ -7,15 +7,15 @@ import { ReactNode } from 'react';
import { useRooms } from '@hook/rooms/useRooms'; import { useRooms } from '@hook/rooms/useRooms';
import { useCreateRoom } from '@hook/rooms/useCreateRoom'; import { useCreateRoom } from '@hook/rooms/useCreateRoom';
import { useUpdateRoom } from '@hook/rooms/useUpdateRoom'; import { useUpdateRoom } from '@hook/rooms/useUpdateRoom';
import { useDeleteRoom } from '@hook/rooms/useDeleteRoom'; import { useSetIsDeleteRoom } from '@hook/rooms/useSetIsDeleteRoom';
// Types // Types
import { IRoom } from '@type/rooms'; import { IRoom } from '@type/room';
const mockUseRooms = jest.fn(useRooms); const mockUseRooms = jest.fn(useRooms);
const mockUseCreateRoom = jest.fn(useCreateRoom); const mockUseCreateRoom = jest.fn(useCreateRoom);
const mockUseUpdateRoom = jest.fn(useUpdateRoom); const mockUseUpdateRoom = jest.fn(useUpdateRoom);
const mockUseDeleteRoom = jest.fn(useDeleteRoom); const mockUseDeleteRoom = jest.fn(useSetIsDeleteRoom);
interface IWrapper { interface IWrapper {
children: ReactNode; children: ReactNode;
} }
@@ -25,6 +25,7 @@ const sampleData: IRoom = {
name: 'Room name test', name: 'Room name test',
price: 9999, price: 9999,
status: true, status: true,
isDelete: true,
}; };
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -50,15 +51,18 @@ describe('Room hooks', () => {
name: 'Nezumi', name: 'Nezumi',
price: 2500, price: 2500,
status: true, status: true,
isDelete: true,
}, },
{ {
id: 2, id: 2,
name: 'Loi Phan', name: 'Loi Phan',
price: 8500, price: 8500,
status: false, status: false,
isDelete: true,
}, },
], ],
isLoading: false, isLoading: false,
count: 2
})); }));
const { result } = renderHook(() => mockUseRooms(), { wrapper }); const { result } = renderHook(() => mockUseRooms(), { wrapper });
@@ -108,8 +112,8 @@ describe('Room hooks', () => {
test('Should delete room correctly', async () => { test('Should delete room correctly', async () => {
mockUseDeleteRoom.mockImplementation(() => ({ mockUseDeleteRoom.mockImplementation(() => ({
deleteRoom: () => { setIsDeleteRoom: () => {
sampleData.id; sampleData
}, },
isDeleting: false, isDeleting: false,
isSuccess: true, isSuccess: true,
@@ -118,7 +122,7 @@ describe('Room hooks', () => {
act(() => { act(() => {
const testMethod = async () => { const testMethod = async () => {
result.current.deleteRoom(sampleData.id); result.current.setIsDeleteRoom(sampleData.id);
await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); await waitFor(() => expect(result.current.isSuccess).toBeTruthy());
}; };
@@ -10,7 +10,7 @@ import { useUpdateUser } from '@hook/users/useUpdateUser';
import { useUsers } from '@hook/users/useUsers'; import { useUsers } from '@hook/users/useUsers';
// Types // Types
import { IUser } from '@type/users'; import { IUser } from '@type/user';
const mockUseUsers = jest.fn(useUsers); const mockUseUsers = jest.fn(useUsers);
const mockUseCreateUser = jest.fn(useCreateUser); const mockUseCreateUser = jest.fn(useCreateUser);
@@ -25,6 +25,7 @@ const sampleData: IUser = {
name: 'User name test', name: 'User name test',
phone: '123456789', phone: '123456789',
isBooked: true, isBooked: true,
isDelete: true,
}; };
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -50,15 +51,18 @@ describe('Users hook', () => {
name: 'Nezumi', name: 'Nezumi',
phone: '0123456678', phone: '0123456678',
isBooked: true, isBooked: true,
isDelete: true,
}, },
{ {
id: 2, id: 2,
name: 'Loi Phan', name: 'Loi Phan',
phone: '0123456678', phone: '0123456678',
isBooked: false, isBooked: false,
isDelete: true,
}, },
], ],
isLoading: false, isLoading: false,
count: 2,
})); }));
const { result } = renderHook(() => mockUseUsers(), { wrapper }); const { result } = renderHook(() => mockUseUsers(), { wrapper });
@@ -0,0 +1,20 @@
import { useQuery } from '@tanstack/react-query';
// Services
import { getCurrentAccount } from '@service/authenticationService';
const useAccount = () => {
let isAuthenticated: boolean = false;
const { data: account, isPending } = useQuery({
queryKey: ['account'],
queryFn: getCurrentAccount,
});
if (account) {
isAuthenticated = account.role === 'authenticated';
}
return { isPending, account, isAuthenticated };
};
export { useAccount };
@@ -0,0 +1,34 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import toast from 'react-hot-toast';
// Services
import { login as loginFn } from '@service/authenticationService';
// Types
import { ILogin } from '@type/common';
const useLogin = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get('redirectTo') || null;
const { mutate: login, isPending } = useMutation({
mutationFn: ({ email, password }: ILogin) => loginFn({ email, password }),
onSuccess: (account) => {
queryClient.setQueryData(['account'], account.user);
redirectTo
? navigate(redirectTo)
: navigate('/');
},
onError: (err) => {
console.error(err.message);
toast.error('Email or password not correct!');
},
});
return { login, isPending };
};
export { useLogin };
@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
// Services
import { logout as logoutFn } from '@service/authenticationService';
const useLogout = () => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { mutate: logout, isPending } = useMutation({
mutationFn: logoutFn,
onSuccess: () => {
queryClient.removeQueries();
navigate('/login', { replace: true });
},
});
return { logout, isPending };
};
export { useLogout };
@@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-hot-toast';
// Services
import { updateAccount as updateAccountFn } from '@service/authenticationService';
// Constants
import { UPDATE_ACCOUNT_SUCCESSFUL } from '@constant/messages';
const useUpdateAccount = () => {
const queryClient = useQueryClient();
const { mutate: updateAccount, isPending: isUpdating } = useMutation({
mutationFn: updateAccountFn,
onSuccess: (account) => {
toast.success(UPDATE_ACCOUNT_SUCCESSFUL);
queryClient.setQueryData(['account'], account.user);
},
onError: (err) => toast.error(err.message),
});
return { updateAccount, isUpdating };
};
export { useUpdateAccount };
@@ -0,0 +1,73 @@
import toast from 'react-hot-toast';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
// Services
import { getAllBookings } from '@service/bookingServices';
// Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config';
/**
* Fetch booking from database
* @returns The status of loading bookings from database and data of bookings
*/
const useBookings = () => {
const queryClient = useQueryClient();
const [searchParams] = useSearchParams();
const searchValue = searchParams.get('search') || '';
const page = searchParams.get('page')
? Number(searchParams.get('page'))
: 1;
const {
isLoading,
data: { data: bookings, count } = {},
error,
} = useQuery({
queryKey: ['bookings', searchValue, page],
queryFn: () =>
getAllBookings({
userNameSearch: searchValue,
page,
}),
});
if (error) {
toast.error(error.message);
}
// Pre-fetching
const totalPage = Math.ceil(count! / DEFAULT_PAGE_SIZE);
if (page < totalPage) {
const nextPage = page + 1;
queryClient.prefetchQuery({
queryKey: ['bookings', searchValue, nextPage],
queryFn: () =>
getAllBookings({
userNameSearch: searchValue,
page: nextPage,
}),
});
}
if (page > 1) {
const previousPage = page - 1;
queryClient.prefetchQuery({
queryKey: ['bookings', searchValue, previousPage],
queryFn: () =>
getAllBookings({
userNameSearch: searchValue,
page: previousPage,
}),
});
}
return { isLoading, bookings, count };
};
export { useBookings };
@@ -0,0 +1,43 @@
import toast from 'react-hot-toast';
import { useMutation, useQueryClient } from '@tanstack/react-query';
// Constants
import { CHECKOUT_SUCCESS } from '@constant/messages';
// Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable';
// Services
import { checkOutBooking as checkOutBookingFn } from '@service/bookingServices';
const useCheckOut = () => {
const queryClient = useQueryClient();
const { dispatch } = useUserRoomAvailable();
const {
mutate: checkOutBooking,
isPending: isChecking,
isSuccess,
} = useMutation({
mutationFn: checkOutBookingFn,
onSuccess: (data) => {
toast.success(CHECKOUT_SUCCESS);
queryClient.invalidateQueries({ queryKey: ['bookings'] });
// Update status room, users
dispatch!({
type: 'updateStatusRoom',
payload: [{ id: data.roomId, status: false }],
});
dispatch!({
type: 'updateStatusUser',
payload: [{ id: data.userId, isBooked: false }],
});
},
onError: (err) => toast.error(err.message),
});
return { checkOutBooking, isChecking, isSuccess };
};
export default useCheckOut;
@@ -0,0 +1,33 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
// Services
import { createBooking as createBookingFn } from '@service/bookingServices';
// Constants
import { ADD_SUCCESS } from '@constant/messages';
/**
* Create booking on database
* @returns The boolean of isCreating and create booking function
*/
const useCreateBooking = () => {
const queryClient = useQueryClient();
const {
mutate: createBooking,
isPending: isCreating,
isSuccess,
} = useMutation({
mutationFn: createBookingFn,
onSuccess: () => {
toast.success(ADD_SUCCESS);
queryClient.invalidateQueries({ queryKey: ['bookings'] });
},
onError: (err) => toast.error(err.message),
});
return { isCreating, createBooking, isSuccess };
};
export { useCreateBooking };
@@ -0,0 +1,35 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
// Services
import { deleteBooking as deleteBookingFn } from '@service/bookingServices';
// Constants
import { DELETE_SUCCESS } from '@constant/messages';
/**
* Delete the booking from database
* @returns The boolean of status deleting and deleteBooking function
*/
const useDeleteBooking = () => {
const queryClient = useQueryClient();
const {
isPending: isDeleting,
mutate: deleteBooking,
isSuccess,
} = useMutation({
mutationFn: deleteBookingFn,
onSuccess: () => {
toast.success(DELETE_SUCCESS);
queryClient.invalidateQueries({
queryKey: ['bookings'],
});
},
onError: (err) => toast.error(err.message),
});
return { isDeleting, deleteBooking, isSuccess };
};
export { useDeleteBooking };
@@ -0,0 +1,33 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
// Services
import { updateBooking as updateBookingFn } from '@service/bookingServices';
// Constants
import { UPDATE_SUCCESS } from '@constant/messages';
/**
* Update booking from database
* @returns The status of updating and updateBooking function
*/
const useUpdateBooking = () => {
const queryClient = useQueryClient();
const {
mutate: updateBooking,
isPending: isUpdating,
isSuccess
} = useMutation({
mutationFn: updateBookingFn,
onSuccess: () => {
toast.success(UPDATE_SUCCESS);
queryClient.invalidateQueries({ queryKey: ['bookings'] });
},
onError: (err) => toast.error(err.message),
});
return { isUpdating, updateBooking, isSuccess };
};
export { useUpdateBooking };
+51 -6
View File
@@ -1,34 +1,79 @@
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// Services // Services
import { getAllRooms } from '@service/roomServices'; import { getAllRooms } from '@service/roomServices';
// Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config';
/** /**
* Fetch room from database * Fetch room from database
* @returns The status of loading rooms from database and data of rooms * @returns The status of loading rooms from database and data of rooms
*/ */
const useRooms = () => { const useRooms = () => {
const queryClient = useQueryClient();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const sortByValue = searchParams.get('sortBy') || 'id'; const sortByValue = searchParams.get('sortBy') || 'id';
const orderByValue = searchParams.get('orderBy') || 'asc'; const orderByValue = searchParams.get('orderBy') || 'asc';
const phoneSearch = searchParams.get('search') || ''; const searchValue = searchParams.get('search') || '';
const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1;
const { const {
isLoading, isLoading,
data: rooms, data: { data: rooms, count } = {},
error, error,
} = useQuery({ } = useQuery({
queryKey: ['rooms', sortByValue, orderByValue, phoneSearch], queryKey: ['rooms', sortByValue, orderByValue, searchValue, page],
queryFn: () => getAllRooms(sortByValue, orderByValue, phoneSearch), queryFn: () =>
getAllRooms({
sortBy: sortByValue,
orderBy: orderByValue,
roomSearch: searchValue,
page,
}),
}); });
if (error) { if (error) {
toast.error(error.message); toast.error(error.message);
} }
return { isLoading, rooms }; // Pre-fetching
const totalPage = Math.ceil(count! / DEFAULT_PAGE_SIZE);
if (page < totalPage) {
const nextPage = page + 1;
queryClient.prefetchQuery({
queryKey: ['rooms', sortByValue, orderByValue, searchValue, nextPage],
queryFn: () =>
getAllRooms({
sortBy: sortByValue,
orderBy: orderByValue,
roomSearch: searchValue,
page: nextPage,
}),
});
}
if (page > 1) {
const previousPage = page - 1;
queryClient.prefetchQuery({
queryKey: ['rooms', sortByValue, orderByValue, searchValue, previousPage],
queryFn: () =>
getAllRooms({
sortBy: sortByValue,
orderBy: orderByValue,
roomSearch: searchValue,
page: previousPage,
}),
});
}
return { isLoading, rooms, count };
}; };
export { useRooms }; export { useRooms };
@@ -2,34 +2,38 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { deleteRoom as deleteRoomFn } from '@service/roomServices'; import { setIsDeleteRoom as setIsDeleteRoomFn } from '@service/roomServices';
// Constants // Constants
import * as messages from '@constant/messages'; import * as messages from '@constant/messages';
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable';
/** /**
* Delete the room from database * Delete the room from database
* @returns The boolean of status deleting and deleteRoom function * @returns The boolean of status deleting and deleteRoom function
*/ */
const useDeleteRoom = () => { const useSetIsDeleteRoom = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { dispatch } = useUserRoomAvailable();
const { const {
isPending: isDeleting, isPending: isDeleting,
mutate: deleteRoom, mutate: setIsDeleteRoom,
isSuccess isSuccess,
} = useMutation({ } = useMutation({
mutationFn: deleteRoomFn, mutationFn: setIsDeleteRoomFn,
onSuccess: () => { onSuccess: (room) => {
toast.success(messages.DELETE_SUCCESS); toast.success(messages.DELETE_SUCCESS);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['rooms'], queryKey: ['rooms'],
}); });
dispatch?.({ type: 'removeRoom', payload: [{ id: room.id }] });
}, },
onError: (err) => toast.error(err.message), onError: (err) => toast.error(err.message),
}); });
return { isDeleting, deleteRoom, isSuccess }; return { isDeleting, setIsDeleteRoom, isSuccess };
}; };
export { useDeleteRoom }; export { useSetIsDeleteRoom };
@@ -0,0 +1,40 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
// Services
import { setIsDeleteUser as setIsDeleteUserFn } from '@service/userServices';
// Constants
import * as messages from '@constant/messages';
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable';
/**
* Delete the user from database
* @returns The boolean of status deleting and deleteUser function
*/
const useIsDeleteUser = () => {
const queryClient = useQueryClient();
const { dispatch } = useUserRoomAvailable();
const {
isPending: isDeleting,
mutate: setIsDeleteUser,
isSuccess,
} = useMutation({
mutationFn: setIsDeleteUserFn,
onSuccess: (user) => {
toast.success(messages.DELETE_SUCCESS);
queryClient.invalidateQueries({
queryKey: ['users'],
});
// Remove user from global state
dispatch?.({ type: 'removeUser', payload: [{ id: user.id }] });
},
onError: (err) => toast.error(err.message),
});
return { isDeleting, setIsDeleteUser, isSuccess };
};
export { useIsDeleteUser };
@@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
// Services // Services
import { updateUser as updateUserFn } from '@service/userServices'; import { updateUser as updateUserFn } from '@service/userServices';
// Messages // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@constant/messages';
// Hooks // Hooks
+50 -6
View File
@@ -1,34 +1,78 @@
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// Services // Services
import { getAllUsers } from '@service/userServices'; import { getAllUsers } from '@service/userServices';
// Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config';
/** /**
* Fetch data of users from database * Fetch data of users from database
* @returns The status of loading users and data of users * @returns The status of loading users and data of users
*/ */
const useUsers = () => { const useUsers = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryClient = useQueryClient();
const sortByValue = searchParams.get('sortBy') || 'id'; const sortByValue = searchParams.get('sortBy') || 'id';
const orderByValue = searchParams.get('orderBy') || 'asc'; const orderByValue = searchParams.get('orderBy') || 'asc';
const phoneSearch = searchParams.get('search') || ''; const searchValue = searchParams.get('search') || '';
const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1;
const { const {
isLoading, isLoading,
data: users, data: { data: users, count } = {},
error, error,
} = useQuery({ } = useQuery({
queryKey: ['users', sortByValue, orderByValue, phoneSearch], queryKey: ['users', sortByValue, orderByValue, searchValue, page],
queryFn: () => getAllUsers(sortByValue, orderByValue, phoneSearch), queryFn: () =>
getAllUsers({
sortBy: sortByValue,
orderBy: orderByValue,
phoneSearch: searchValue,
page,
}),
}); });
if (error) { if (error) {
toast.error(error.message); toast.error(error.message);
} }
return { isLoading, users }; // Pre-fetching
const totalPage = Math.ceil(count! / DEFAULT_PAGE_SIZE);
if (page < totalPage) {
const nextPage = page + 1;
queryClient.prefetchQuery({
queryKey: ['users', sortByValue, orderByValue, searchValue, nextPage],
queryFn: () =>
getAllUsers({
sortBy: sortByValue,
orderBy: orderByValue,
phoneSearch: searchValue,
page: nextPage,
}),
});
}
if (page > 1) {
const previousPage = page - 1;
queryClient.prefetchQuery({
queryKey: ['users', sortByValue, orderByValue, searchValue, previousPage],
queryFn: () =>
getAllUsers({
sortBy: sortByValue,
orderBy: orderByValue,
phoneSearch: searchValue,
page: previousPage,
}),
});
}
return { isLoading, users, count };
}; };
export { useUsers }; export { useUsers };
@@ -0,0 +1,59 @@
import { useForm } from 'react-hook-form';
// Styled
import Input from '@commonStyle/Input';
// Components
import Form from '@component/Form';
// Constants
import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage';
// Hooks
import { useUpdateAccount } from '@hook/authentication/useUpdateAccount';
import { useAccount } from '@hook/authentication/useAccount';
interface IAccountForm {
fullName: string;
}
const AccountInforForm = () => {
const { account } = useAccount();
const { updateAccount, isUpdating } = useUpdateAccount();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<IAccountForm>();
const onSubmit = (data: IAccountForm) => {
updateAccount(data);
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row label="Email:">
<Input id="email" readOnly value={account?.email} />
</Form.Row>
<Form.Row label="Full Name:" error={errors?.fullName?.message as string}>
<Input
id="fullName"
{...register('fullName', {
required: REQUIRED_FIELD_ERROR,
value: account?.user_metadata.fullName,
})}
/>
</Form.Row>
<Form.Action>
<Form.Button type="submit" name="submit" disabled={isUpdating}>
Update account
</Form.Button>
</Form.Action>
</Form>
);
};
export default AccountInforForm;
@@ -0,0 +1,93 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
// Styled
import Input from '@commonStyle/Input';
// Components
import Form from '@component/Form';
// Constants
import { REGEX } from '@constant/commons';
import {
PASSWORD_CONDITION,
PASSWORD_NOT_MATCH,
PASSWORD_NOT_MATCH_REQUIREMENT,
} from '@constant/formValidateMessage';
// Helpers
import { isValidRegex } from '@helper/validators';
// Hooks
import { useUpdateAccount } from '@hook/authentication/useUpdateAccount';
interface IAccountPasswordForm {
password: string;
confirmPassword: string;
}
const AccountPasswordForm = () => {
const { updateAccount, isUpdating } = useUpdateAccount();
const {
register,
handleSubmit,
getValues,
reset,
formState: { errors },
} = useForm<IAccountPasswordForm>();
useEffect(() => {
if (errors?.password?.message) {
toast.error(PASSWORD_CONDITION);
}
}, [errors?.password?.message]);
const onSubmit = (data: IAccountPasswordForm) => {
updateAccount(data, {
onSuccess: () => {
reset();
},
});
};
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row label="Password:" error={errors?.password?.message}>
<Input
id="password"
type="password"
{...register('password', {
validate: {
checkPassword: (v) =>
isValidRegex(REGEX.PASSWORD, v) ||
PASSWORD_NOT_MATCH_REQUIREMENT,
},
})}
/>
</Form.Row>
<Form.Row
label="Confirm Password:"
error={errors?.confirmPassword?.message}
>
<Input
id="confirmPassword"
type="password"
{...register('confirmPassword', {
validate: (value) =>
getValues().password === value || PASSWORD_NOT_MATCH,
})}
/>
</Form.Row>
<Form.Action>
<Form.Button type="submit" name="submit" disabled={isUpdating}>
Update password
</Form.Button>
</Form.Action>
</Form>
);
};
export default AccountPasswordForm;
@@ -0,0 +1,26 @@
// Styled
import { FormArea, StyledAccount, SubTitle, Title } from './styled';
// Components
import AccountInforForm from './AccountInforForm';
import AccountPasswordForm from './AccountPasswordForm';
const Account = () => {
return (
<StyledAccount>
<Title>Update account</Title>
<FormArea>
<SubTitle>Update information</SubTitle>
<AccountInforForm />
</FormArea>
<FormArea>
<SubTitle>Update password</SubTitle>
<AccountPasswordForm />
</FormArea>
</StyledAccount>
);
};
export default Account;
@@ -0,0 +1,36 @@
import styled from 'styled-components';
const Title = styled.h2`
font-size: var(--fs-md);
color: var(--dark-text);
text-transform: capitalize;
`;
const SubTitle = styled.h3`
font-size: var(--fs-sm);
text-transform: capitalize;
text-align: center;
margin-bottom: 20px;
`
const StyledAccount = styled.div`
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
`;
const FormArea = styled.div`
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
padding: 20px 40px;
width: 500px;
margin-top: 50px;
`
export { Title, StyledAccount, FormArea, SubTitle };
@@ -0,0 +1,307 @@
import { useCallback, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
// Styled
import Input from '@commonStyle/Input.ts';
// Constants
import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage.ts';
// Components
import Form from '@component/Form/index.tsx';
import Select, { ISelectOptions } from '@component/Select/index.tsx';
// Hooks
import { useCreateBooking } from '@hook/bookings/useCreateBooking.ts';
import { useUpdateBooking } from '@hook/bookings/useUpdateBooking.ts';
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable.ts';
// Helpers
import { formatCurrency, getDayDiff } from '@helper/helper.ts';
// Services
import { getRoomById, updateRoomStatus } from '@service/roomServices.ts';
import { updateUserBookedStatus } from '@service/userServices.ts';
// Types
import { IBooking, TBookingResponse } from '@type/booking.ts';
interface IBookingFormProp {
onCloseModal?: () => void;
booking?: TBookingResponse;
}
const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => {
const { roomsAvailable, usersAvailable, dispatch } = useUserRoomAvailable();
const { isCreating, createBooking } = useCreateBooking();
const { isUpdating, updateBooking } = useUpdateBooking();
const isLoading = isCreating || isUpdating;
const { id: editId, ...editValues } = { ...booking };
const [ amountValue, setAmountValue ] = useState(
editValues.amount
? formatCurrency(editValues.amount)
: '$0.00'
);
const formMethods = useForm<IBooking>({
defaultValues: editId
? {
startDate: editValues.startDate,
endDate: editValues.endDate,
roomId: editValues.rooms?.id,
userId: editValues.users?.id,
amount: editValues.amount!,
}
: {},
});
const {
register,
handleSubmit,
reset,
formState: { errors, isDirty, isValid },
trigger,
getValues,
setValue,
} = formMethods;
// Submit form
const onSubmit = async (newBooking: IBooking) => {
if (!editId) {
// Add request
createBooking(newBooking, {
onSuccess: async () => {
reset();
onCloseModal?.();
// Update room, user available in global state
dispatch!({
type: 'updateStatusUser',
payload: [{ id: newBooking.userId, isBooked: true }],
});
dispatch!({
type: 'updateStatusRoom',
payload: [{ id: newBooking.roomId, status: true }],
});
// Update room, user available in server
await updateRoomStatus(newBooking.roomId, true);
await updateUserBookedStatus(newBooking.userId, true);
},
});
} else {
// Edit request
newBooking.id = editId!;
updateBooking(newBooking, {
onSuccess: async () => {
reset();
onCloseModal?.();
// Update room status in global state
// Reset status old room
dispatch!({
type: 'updateStatusRoom',
payload: [{ id: booking!.rooms!.id, status: false }],
});
// Change status new room
dispatch!({
type: 'updateStatusRoom',
payload: [{ id: newBooking.roomId, status: true }],
});
// Update room status in server
// Reset status old room
await updateRoomStatus(booking!.rooms!.id, false);
// Change status new room
await updateRoomStatus(newBooking.roomId, true);
},
});
}
};
// Init user, room available to options choice
const userOptions = useMemo(() => {
const options: ISelectOptions[] = [];
usersAvailable?.forEach((item) => {
if (booking?.users?.id === item.id || !item.isBooked) {
options.push({
label: item.name!,
value: item.id.toString(),
});
}
});
return options;
}, [usersAvailable, booking?.users?.id]);
const roomOptions = useMemo(() => {
const options: ISelectOptions[] = [];
roomsAvailable?.forEach((item) => {
if (booking?.rooms?.id === item.id || !item.status) {
options.push({
label: item.name!,
value: item.id.toString(),
});
}
});
return options;
}, [roomsAvailable, booking?.rooms?.id]);
// Calculate the final price
const computePrice = useCallback(async () => {
const startDateValue = getValues('startDate');
const endDateValue = getValues('endDate');
const roomValue = getValues('roomId');
if (startDateValue && endDateValue && roomValue) {
setAmountValue('Loading...');
const daysDiff = getDayDiff(
new Date(startDateValue),
new Date(endDateValue)
);
// Fetch room data
const room = await getRoomById(roomValue.toString());
const amount = daysDiff * room.price;
setValue('amount', amount, { shouldValidate: true });
setAmountValue(formatCurrency(amount));
}
}, [getValues, setValue]);
const checkValidTwoDates = useCallback(() => {
const startDate = new Date(getValues().startDate);
const endDate = new Date(getValues().endDate);
if(startDate >= endDate) {
setValue('endDate', '');
}
}, [setValue, getValues]);
const startDateValidate = useMemo(
() => new Date().toISOString().split('T')[0],
[]
);
const endDateValidate = () => {
if (getValues('startDate')) {
const date = new Date(getValues('startDate'));
date.setDate(date.getDate() + 1);
return date.toISOString().split('T')[0];
}
return;
};
return (
<FormProvider {...formMethods}>
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row label="User" error={errors?.userId?.message}>
{userOptions.length ? (
<Select
ariaLabel="user-select"
options={userOptions}
id="userId"
disable={Boolean(booking?.users)}
optionsConfigForm={{
valueAsNumber: true,
onChange: () => trigger('userId'),
}}
/>
) : (
<p>No user available</p>
)}
</Form.Row>
<Form.Row label="Room" error={errors?.roomId?.message}>
{roomOptions.length ? (
<Select
ariaLabel="user-select"
options={roomOptions}
id="roomId"
optionsConfigForm={{
valueAsNumber: true,
onChange: () => trigger('roomId'),
}}
/>
) : (
<p>No room available</p>
)}
</Form.Row>
<Form.Row label="Start Date" error={errors?.startDate?.message}>
<Input
type="date"
id="startDate"
min={startDateValidate}
{...register('startDate', {
required: REQUIRED_FIELD_ERROR,
onChange: () => {
trigger('startDate');
checkValidTwoDates();
computePrice();
},
})}
/>
</Form.Row>
<Form.Row label="End Date" error={errors?.endDate?.message}>
<Input
type="date"
id="endDate"
min={endDateValidate()}
disabled={!getValues('startDate')}
{...register('endDate', {
required: REQUIRED_FIELD_ERROR,
onChange: () => {
trigger('endDate');
computePrice();
},
validate: (endDateValue) =>
endDateValue > getValues().startDate || 'Invalid date',
})}
/>
</Form.Row>
<Form.Row label="Amount">
<Input
type="hidden"
id="amount"
{...register('amount', {
required: REQUIRED_FIELD_ERROR,
onChange: () => trigger('amount'),
})}
readOnly
/>
<span>{amountValue}</span>
</Form.Row>
<Form.Action>
<Form.Button
type="submit"
name="submit"
disabled={!isDirty || !isValid || isLoading}
>
{!editId ? 'Add' : 'Save'}
</Form.Button>
<Form.Button
type="button"
variations="secondary"
onClick={onCloseModal}
>
Close
</Form.Button>
</Form.Action>
</Form>
</FormProvider>
);
};
export default BookingForm;
@@ -0,0 +1,118 @@
import toast from 'react-hot-toast';
import { useCallback, useMemo } from 'react';
// Helpers
import { formatCurrency } from '@helper/helper';
// Types
import { TBookingResponse } from '@type/booking';
// Components
import Modal from '@component/Modal';
import Table from '@component/Table';
import Menus from '@component/Menus';
import { RiEditBoxFill } from 'react-icons/ri';
import { TbArrowNarrowRight } from 'react-icons/tb';
import { ImExit } from 'react-icons/im';
import BookingForm from './BookingForm';
import ConfirmMessage from '@component/ConfirmMessage';
// Hooks
import useCheckOut from '@hook/bookings/useCheckout';
// Constants
import { FORM } from '@constant/commons';
interface IBookingRow {
booking: TBookingResponse;
}
const BookingRow = ({ booking }: IBookingRow) => {
const { checkOutBooking } = useCheckOut();
const { id, users, startDate, endDate, rooms, amount, status } = booking;
const statusText = status ? 'Check in' : 'Check out';
const formattedPrice = useMemo(() => formatCurrency(amount), [amount]);
const renderEditBtn = useCallback(
(onCloseModal: () => void) => (
<Menus.Button
onClick={onCloseModal}
icon={<RiEditBoxFill />}
disabled={!status}
>
Edit
</Menus.Button>
),
[status]
);
const renderCheckOutBtn = useCallback(
(onCloseModal: () => void) => (
<Menus.Button
onClick={onCloseModal}
icon={<ImExit />}
disabled={!status}
>
Checkout
</Menus.Button>
),
[status]
);
const handleClickCheckOutBtn = useCallback(() => {
if (booking.status) {
checkOutBooking({
idBooking: booking.id,
roomId: booking!.rooms!.id,
userId: booking!.users!.id,
});
} else {
toast.error('User already checkout!');
}
}, [booking, checkOutBooking]);
return (
<Table.Row>
<div>{users?.name}</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
{startDate} &nbsp; <TbArrowNarrowRight />
&nbsp; {endDate}
</div>
<div>{rooms?.name}</div>
<div>{formattedPrice}</div>
<div>{statusText}</div>
<div>
<Modal>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={renderEditBtn}
/>
<Modal.Open
modalName={FORM.CHECKOUT}
renderChildren={renderCheckOutBtn}
/>
</Menus.List>
<Modal.Window name={FORM.EDIT} title="Edit Booking">
<BookingForm booking={booking} key={booking.id} />
</Modal.Window>
<Modal.Window name={FORM.CHECKOUT} title="Checkout">
<ConfirmMessage
message={`Are you sure to checkout this user? '${booking.users?.name}'`}
onConfirm={handleClickCheckOutBtn}
/>
</Modal.Window>
</Menus.Menu>
</Modal>
</div>
</Table.Row>
);
};
export default BookingRow;
@@ -0,0 +1,73 @@
import { useCallback } from 'react';
// Components
import Menus from '@component/Menus';
import Table from '@component/Table';
import Message from '@component/Message';
import Search from '@component/Search';
import Pagination from '@component/Pagination';
import BookingRow from './BookingRow';
// Styled
import { StyledOperationTable } from './styled';
import Direction from '@commonStyle/Direction';
import Spinner from '@commonStyle/Spinner';
// Hooks
import { useBookings } from '@hook/bookings/useBookings';
// Types
import { TBookingResponse } from '@type/booking';
const BookingTable = () => {
const columnName = [
'User',
'Date',
'Room',
'Amount',
'Status'
];
const {
isLoading,
bookings,
count
} = useBookings();
const renderBookingRow = useCallback(
(booking: TBookingResponse) => (
<BookingRow booking={booking} key={booking.id} />
),
[]
);
return (
<>
<Direction>
<StyledOperationTable>
<Search setPlaceHolder="Search by name..." />
</StyledOperationTable>
{isLoading && <Spinner />}
{bookings && bookings.length ? (
<Menus>
<Table columns="15% 25% 20% 15% 10% 10% 5%">
<Table.Header headerColumn={columnName} />
<Table.Body<TBookingResponse>
data={bookings}
render={renderBookingRow}
/>
<Table.Footer>
<Pagination count={count!} />
</Table.Footer>
</Table>
</Menus>
) : (
!isLoading && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default BookingTable;
@@ -0,0 +1,38 @@
// Styled
import Direction from '@commonStyle/Direction';
import { StyledBooking, Title } from './styled';
import Button from '@commonStyle/Button';
// Components
import Modal from '@component/Modal';
import BookingTable from './BookingTable';
import BookingForm from './BookingForm';
// Constants
import { FORM } from '@constant/commons';
const Booking = () => {
return (
<StyledBooking>
<Direction type="horizontal">
<Title>List Booking</Title>
<Modal>
<Modal.Open
modalName={FORM.BOOKING}
renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add booking</Button>
)}
/>
<Modal.Window name={FORM.BOOKING} title="Add form">
<BookingForm />
</Modal.Window>
</Modal>
</Direction>
<BookingTable />
</StyledBooking>
);
};
export default Booking;
@@ -0,0 +1,24 @@
import styled from 'styled-components';
const StyledBooking = styled.main`
padding: 20px;
padding-bottom: 100px;
display: flex;
flex-direction: column;
gap: 30px;
`;
const Title = styled.h2`
font-size: var(--fs-md);
color: var(--dark-text);
text-transform: capitalize;
`;
const StyledOperationTable = styled.div`
display: flex;
gap: 30px;
justify-content: flex-end;
`;
export { StyledBooking, Title, StyledOperationTable };
-23
View File
@@ -1,23 +0,0 @@
import styled from 'styled-components';
// Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable';
const StyledDashboard = styled.main`
padding: 20px;
`;
const Dashboard = () => {
const { roomsAvailable, usersAvailable } = useUserRoomAvailable();
console.log('Rooms Available: ', roomsAvailable);
console.log('Users available: ', usersAvailable);
return (
<StyledDashboard>
<p>This page currently still develop. Please try again later!</p>
</StyledDashboard>
);
};
export default Dashboard;
@@ -0,0 +1,69 @@
import { useForm } from 'react-hook-form';
// Components
import Form from '@component/Form';
// Styled
import { FieldInput, StyledLoginForm } from './styled';
import Button from '@commonStyle/Button';
// Constants
import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage';
// Types
import { ILogin } from '@type/common';
// Hooks
import { useLogin } from '@hook/authentication/useLogin';
const LoginForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ILogin>();
const { login, isPending } = useLogin();
const onSubmit = (data: ILogin) => {
login(data);
};
return (
<StyledLoginForm>
<Form onSubmit={handleSubmit(onSubmit)}>
<Form.Row
label="Email:"
direction="vertical"
error={errors.email?.message}
>
<FieldInput
type="text"
{...register('email', {
required: REQUIRED_FIELD_ERROR,
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
</Form.Row>
<Form.Row
label="Password:"
direction="vertical"
error={errors.password?.message}
>
<FieldInput
type="password"
{...register('password', {
required: REQUIRED_FIELD_ERROR,
})}
/>
</Form.Row>
<Button type="submit" disabled={isPending}>Login</Button>
</Form>
</StyledLoginForm>
);
};
export default LoginForm;
@@ -0,0 +1,32 @@
import { Navigate } from 'react-router-dom';
// Styled
import { LoginLayout, StyledLogo, TitleStyled } from './styled';
// Assets
import logo from '@assets/images/logo.png';
// Components
import LoginForm from './LoginForm';
// Hooks
import { useAccount } from '@hook/authentication/useAccount';
const Login = () => {
const { account } = useAccount();
if (account) {
return <Navigate to={'/booking'} />;
}
return (
<LoginLayout>
<StyledLogo src={logo} />
<TitleStyled>Log In</TitleStyled>
<LoginForm />
</LoginLayout>
);
};
export default Login;
@@ -0,0 +1,43 @@
import styled from 'styled-components';
// Styled
import CommonInput from '@commonStyle/CommonInput';
const LoginLayout = styled.main`
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: var(--background-login);
`;
const StyledLogo = styled.img`
width: 250px;
height: 250px;
`;
const TitleStyled = styled.h1`
font-size: var(--fs-md);
`;
const StyledLoginForm = styled.div`
background-color: var(--form-color);
border-radius: var(--radius-sm);
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
padding: 30px 50px;
`;
const FieldInput = styled.input`
${CommonInput}
width: 300px;
`
export { LoginLayout, StyledLogo, TitleStyled, StyledLoginForm, FieldInput };
+5 -18
View File
@@ -1,11 +1,8 @@
import styled from 'styled-components';
// Styled // Styled
import Button from '@commonStyle/Button.ts';
import Input from '@commonStyle/Input.ts'; import Input from '@commonStyle/Input.ts';
// Types // Types
import { IRoom } from '@type/rooms.ts'; import { IRoom } from '@type/room.ts';
// Constants // Constants
import { import {
@@ -25,16 +22,6 @@ import { useForm } from 'react-hook-form';
import { useCreateRoom } from '@hook/rooms/useCreateRoom.ts'; import { useCreateRoom } from '@hook/rooms/useCreateRoom.ts';
import { useUpdateRoom } from '@hook/rooms/useUpdateRoom.ts'; import { useUpdateRoom } from '@hook/rooms/useUpdateRoom.ts';
const FormBtn = styled(Button)`
width: 100%;
&:disabled,
&[disabled] {
background-color: var(--disabled-btn-color);
cursor: no-drop;
}
`;
interface IRoomFormProp { interface IRoomFormProp {
onCloseModal?: () => void; onCloseModal?: () => void;
room?: IRoom; room?: IRoom;
@@ -115,7 +102,7 @@ const RoomForm = ({ onCloseModal, room }: IRoomFormProp) => {
</Form.Row> </Form.Row>
<Form.Action> <Form.Action>
<FormBtn <Form.Button
type="submit" type="submit"
name="submit" name="submit"
disabled={!isDirty || !isValid || isLoading} disabled={!isDirty || !isValid || isLoading}
@@ -125,10 +112,10 @@ const RoomForm = ({ onCloseModal, room }: IRoomFormProp) => {
? 'Add' ? 'Add'
: 'Save' : 'Save'
} }
</FormBtn> </Form.Button>
<FormBtn type="button" variations="secondary" onClick={onCloseModal}> <Form.Button type="button" variations="secondary" onClick={onCloseModal}>
Close Close
</FormBtn> </Form.Button>
</Form.Action> </Form.Action>
</Form> </Form>
); );
+4 -5
View File
@@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
import { formatCurrency } from '@helper/helper'; import { formatCurrency } from '@helper/helper';
// Types // Types
import { IRoom } from '@type/rooms'; import { IRoom } from '@type/room';
// Components // Components
import RoomForm from './RoomForm'; import RoomForm from './RoomForm';
@@ -16,7 +16,7 @@ import { HiTrash } from 'react-icons/hi';
import ConfirmMessage from '@component/ConfirmMessage'; import ConfirmMessage from '@component/ConfirmMessage';
// Hooks // Hooks
import { useDeleteRoom } from '@hook/rooms/useDeleteRoom'; import { useSetIsDeleteRoom } from '@hook/rooms/useSetIsDeleteRoom';
// Constants // Constants
import { FORM } from '@constant/commons'; import { FORM } from '@constant/commons';
@@ -27,7 +27,7 @@ interface IRoomRow {
const RoomRow = ({ room }: IRoomRow) => { const RoomRow = ({ room }: IRoomRow) => {
const { id, name, price, status } = room; const { id, name, price, status } = room;
const { isDeleting, deleteRoom } = useDeleteRoom(); const { setIsDeleteRoom } = useSetIsDeleteRoom();
const statusText = status const statusText = status
? 'Unavailable' ? 'Unavailable'
: 'Available'; : 'Available';
@@ -78,9 +78,8 @@ const RoomRow = ({ room }: IRoomRow) => {
<Modal.Window name={FORM.DELETE} title="Delete Room"> <Modal.Window name={FORM.DELETE} title="Delete Room">
<ConfirmMessage <ConfirmMessage
disabled={isDeleting}
message={`Are you sure to delete ${name}?`} message={`Are you sure to delete ${name}?`}
onConfirm={() => deleteRoom(id)} onConfirm={() => setIsDeleteRoom(id)}
/> />
</Modal.Window> </Modal.Window>
</Menus.Menu> </Menus.Menu>
@@ -10,7 +10,7 @@ import OrderBy from '@component/OrderBy';
import RoomRow from './RoomRow'; import RoomRow from './RoomRow';
// Types // Types
import { IRoom } from '@type/rooms'; import { IRoom } from '@type/room';
// Constants // Constants
import { ORDERBY_OPTIONS, ROOM_PAGE } from '@constant/commons'; import { ORDERBY_OPTIONS, ROOM_PAGE } from '@constant/commons';
@@ -22,10 +22,11 @@ import Spinner from '@commonStyle/Spinner';
// Hooks // Hooks
import { useRooms } from '@hook/rooms/useRooms'; import { useRooms } from '@hook/rooms/useRooms';
import Pagination from '@component/Pagination';
const RoomTable = () => { const RoomTable = () => {
const columnName = ['Id', 'Name', 'Price', 'Status']; const columnName = ['Id', 'Name', 'Price', 'Status'];
const { isLoading, rooms } = useRooms(); const { isLoading, rooms, count } = useRooms();
const renderRoomRow = useCallback( const renderRoomRow = useCallback(
(room: IRoom) => <RoomRow room={room} key={room.id} />, (room: IRoom) => <RoomRow room={room} key={room.id} />,
@@ -49,6 +50,9 @@ const RoomTable = () => {
<Table columns="10% 40% 20% 20% 5%"> <Table columns="10% 40% 20% 20% 5%">
<Table.Header headerColumn={columnName} /> <Table.Header headerColumn={columnName} />
<Table.Body<IRoom> data={rooms} render={renderRoomRow} /> <Table.Body<IRoom> data={rooms} render={renderRoomRow} />
<Table.Footer>
<Pagination count={count!}/>
</Table.Footer>
</Table> </Table>
</Menus> </Menus>
) : ( ) : (
+17 -19
View File
@@ -10,27 +10,25 @@ import Button from '@commonStyle/Button';
const Room = () => { const Room = () => {
return ( return (
<> <StyledRoom>
<StyledRoom> <Direction type="horizontal">
<Direction type="horizontal"> <Title>List Room</Title>
<Title>List Room</Title>
<Modal> <Modal>
<Modal.Open <Modal.Open
modalName="room-form" modalName="room-form"
renderChildren={(onCloseModal) => ( renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add room</Button> <Button onClick={onCloseModal}>Add room</Button>
)} )}
/> />
<Modal.Window name="room-form" title="Add form"> <Modal.Window name="room-form" title="Add form">
<RoomForm /> <RoomForm />
</Modal.Window> </Modal.Window>
</Modal> </Modal>
</Direction> </Direction>
<RoomTable /> <RoomTable />
</StyledRoom> </StyledRoom>
</>
); );
}; };
+5 -8
View File
@@ -19,11 +19,8 @@ import {
} from '@constant/formValidateMessage.ts'; } from '@constant/formValidateMessage.ts';
import { REGEX } from '@constant/commons.ts'; import { REGEX } from '@constant/commons.ts';
// Styled
import { FormBtn } from './styled.ts';
// Types // Types
import { IUser } from '@type/users.ts'; import { IUser } from '@type/user';
interface IUserFormProp { interface IUserFormProp {
onCloseModal?: () => void; onCloseModal?: () => void;
@@ -106,7 +103,7 @@ const UserForm = ({ onCloseModal, user }: IUserFormProp) => {
</Form.Row> </Form.Row>
<Form.Action> <Form.Action>
<FormBtn <Form.Button
type="submit" type="submit"
name="submit" name="submit"
disabled={!isDirty || !isValid || isLoading} disabled={!isDirty || !isValid || isLoading}
@@ -116,10 +113,10 @@ const UserForm = ({ onCloseModal, user }: IUserFormProp) => {
? 'Add' ? 'Add'
: 'Save' : 'Save'
} }
</FormBtn> </Form.Button>
<FormBtn type="button" variations="secondary" onClick={onCloseModal}> <Form.Button type="button" variations="secondary" onClick={onCloseModal}>
Close Close
</FormBtn> </Form.Button>
</Form.Action> </Form.Action>
</Form> </Form>
</FormProvider> </FormProvider>
+42 -12
View File
@@ -5,9 +5,13 @@ import Table from '@component/Table';
import Menus from '@component/Menus'; import Menus from '@component/Menus';
import UserForm from './UserForm'; import UserForm from './UserForm';
// Types // Types
import { IUser } from '@type/users'; import { IUser } from '@type/user';
import { FORM } from '@constant/commons';
import { HiTrash } from 'react-icons/hi';
import { useCallback } from 'react';
import ConfirmMessage from '@component/ConfirmMessage';
import { useIsDeleteUser } from '@hook/users/useSetIsDeleteUser';
interface IUserRow { interface IUserRow {
user: IUser; user: IUser;
@@ -15,17 +19,35 @@ interface IUserRow {
const UserRow = ({ user }: IUserRow) => { const UserRow = ({ user }: IUserRow) => {
const { id, name, phone, isBooked } = user; const { id, name, phone, isBooked } = user;
const { setIsDeleteUser } = useIsDeleteUser();
const renderEditBtn = useCallback(
(onCloseModal: () => void) => (
<Menus.Button onClick={onCloseModal} icon={<RiEditBoxFill />}>
Edit
</Menus.Button>
),
[]
);
const renderDeleteBtn = useCallback(
(onCloseModal: () => void) => (
<Menus.Button icon={<HiTrash />} onClick={onCloseModal}>
Delete
</Menus.Button>
),
[]
);
return ( return (
<Table.Row> <Table.Row>
<div>{id}</div> <div>{id}</div>
<div>{name}</div> <div>{name}</div>
<div>{phone}</div> <div>{phone}</div>
<div>{ <div>{
isBooked isBooked
? 'Yes' ? 'Yes'
: 'No' : 'No'
}</div> }</div>
<div> <div>
<Modal> <Modal>
<Menus.Menu> <Menus.Menu>
@@ -33,18 +55,26 @@ const UserRow = ({ user }: IUserRow) => {
<Menus.List id={id.toString()}> <Menus.List id={id.toString()}>
<Modal.Open <Modal.Open
modalName="edit" modalName={FORM.EDIT}
renderChildren={(onCloseModal) => ( renderChildren={renderEditBtn}
<Menus.Button onClick={onCloseModal} icon={<RiEditBoxFill />}> />
Edit
</Menus.Button> <Modal.Open
)} modalName={FORM.DELETE}
renderChildren={renderDeleteBtn}
/> />
</Menus.List> </Menus.List>
<Modal.Window name="edit" title="Edit user"> <Modal.Window name={FORM.EDIT} title="Edit user">
<UserForm user={user} /> <UserForm user={user} />
</Modal.Window> </Modal.Window>
<Modal.Window name={FORM.DELETE} title="Delete User">
<ConfirmMessage
message={`Are you sure to delete ${name}?`}
onConfirm={() => setIsDeleteUser(id)}
/>
</Modal.Window>
</Menus.Menu> </Menus.Menu>
</Modal> </Modal>
</div> </div>
@@ -10,7 +10,7 @@ import OrderBy from '@component/OrderBy';
import UserRow from './UserRow'; import UserRow from './UserRow';
// Types // Types
import { IUser } from '@type/users'; import { IUser } from '@type/user';
// Constants // Constants
import { ORDERBY_OPTIONS, USER_PAGE } from '@constant/commons'; import { ORDERBY_OPTIONS, USER_PAGE } from '@constant/commons';
@@ -22,10 +22,11 @@ import Spinner from '@commonStyle/Spinner';
// Hooks // Hooks
import { useUsers } from '@hook/users/useUsers'; import { useUsers } from '@hook/users/useUsers';
import Pagination from '@component/Pagination';
const UserTable = () => { const UserTable = () => {
const columnName = ['Id', 'Name', 'Phone', 'Is Booked']; const columnName = ['Id', 'Name', 'Phone', 'Is Booked'];
const { isLoading, users } = useUsers(); const { isLoading, users, count } = useUsers();
const renderUserRow = useCallback( const renderUserRow = useCallback(
(user: IUser) => ( (user: IUser) => (
@@ -54,6 +55,9 @@ const UserTable = () => {
<Table columns="10% 35% 30% 15% 10%"> <Table columns="10% 35% 30% 15% 10%">
<Table.Header headerColumn={columnName} /> <Table.Header headerColumn={columnName} />
<Table.Body<IUser> data={users} render={renderUserRow} /> <Table.Body<IUser> data={users} render={renderUserRow} />
<Table.Footer>
<Pagination count={count!}/>
</Table.Footer>
</Table> </Table>
</Menus> </Menus>
) : ( ) : (
@@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
// Types // Types
import { IUser } from '@type/users'; import { IUser } from '@type/user';
// Components // Components
import UserRow from '../UserRow'; import UserRow from '../UserRow';
@@ -13,6 +13,7 @@ describe('UserRow', () => {
name: 'Temp Room', name: 'Temp Room',
phone: '0324421232', phone: '0324421232',
isBooked: false, isBooked: false,
isDelete: true,
}; };
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const wrapper = renderer.create( const wrapper = renderer.create(
@@ -1,67 +1,184 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserForm Should render correctly 1`] = ` exports[`UserForm Should render correctly 1`] = `
<form {
className="sc-gtsqUy jMGskL" "asFragment": [Function],
onSubmit={[Function]} "baseElement": <body>
>
<div
className="sc-hKFymg frexHr"
>
<label
className="sc-eCAqax dnXnhF"
>
Full Name
</label>
<div> <div>
<input <form
className="sc-bdnyFh gejiRq" class="sc-dlniIP bCPNqw"
id="name" >
name="name" <div
onBlur={[Function]} class="sc-eCAqax dgDilB"
onChange={[Function]} direction="horizontal"
type="text" >
/> <label
class="sc-jSFipO cMYxin"
>
Full Name
</label>
<div>
<input
class="sc-bdnyFh gXSWSo"
id="name"
name="name"
type="text"
/>
</div>
</div>
<div
class="sc-eCAqax dgDilB"
direction="horizontal"
>
<label
class="sc-jSFipO cMYxin"
>
Phone
</label>
<div>
<input
class="sc-bdnyFh gXSWSo"
id="phone"
name="phone"
type="text"
/>
</div>
</div>
<div
class="sc-hKFymg dteuGK"
>
<button
class="sc-gtsqUy sc-iCoHzw iXmfC cwhbgM"
disabled=""
name="submit"
type="submit"
variations="primary"
>
Add
</button>
<button
class="sc-gtsqUy sc-iCoHzw hesuRJ cwhbgM"
type="button"
variations="secondary"
>
Close
</button>
</div>
</form>
</div> </div>
</div> </body>,
<div "container": <div>
className="sc-hKFymg frexHr" <form
> class="sc-dlniIP bCPNqw"
<label
className="sc-eCAqax dnXnhF"
> >
Phone <div
</label> class="sc-eCAqax dgDilB"
<div> direction="horizontal"
<input >
className="sc-bdnyFh gejiRq" <label
id="phone" class="sc-jSFipO cMYxin"
name="phone" >
onBlur={[Function]} Full Name
onChange={[Function]} </label>
type="text" <div>
/> <input
</div> class="sc-bdnyFh gXSWSo"
</div> id="name"
<div name="name"
className="sc-dlniIP hBqqvG" type="text"
> />
<button </div>
className="sc-gKAaef sc-jrsKJM gifCOZ KLoQf" </div>
disabled={true} <div
name="submit" class="sc-eCAqax dgDilB"
type="submit" direction="horizontal"
variations="primary" >
> <label
Add class="sc-jSFipO cMYxin"
</button> >
<button Phone
className="sc-gKAaef sc-jrsKJM gbgZUe KLoQf" </label>
type="button" <div>
variations="secondary" <input
> class="sc-bdnyFh gXSWSo"
Close id="phone"
</button> name="phone"
</div> type="text"
</form> />
</div>
</div>
<div
class="sc-hKFymg dteuGK"
>
<button
class="sc-gtsqUy sc-iCoHzw iXmfC cwhbgM"
disabled=""
name="submit"
type="submit"
variations="primary"
>
Add
</button>
<button
class="sc-gtsqUy sc-iCoHzw hesuRJ cwhbgM"
type="button"
variations="secondary"
>
Close
</button>
</div>
</form>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`; `;
@@ -18,11 +18,11 @@ exports[`UserRow Should render correctly 1`] = `
</div> </div>
<div> <div>
<div <div
className="sc-jrsKJM cdPhia" className="sc-kEqXeH jLboIv"
> >
<button <button
aria-label="Menu item 1" aria-label="Menu item 1"
className="sc-iqAbyq fyjyXl" className="sc-crzpnZ gdsCet"
onClick={[Function]} onClick={[Function]}
> >
<svg <svg
@@ -2,26 +2,24 @@
exports[`UserTable Should render correctly 1`] = ` exports[`UserTable Should render correctly 1`] = `
<div <div
className="sc-iJCSeZ crZyCc" className="sc-giAruI eEOXtt"
type="vertical" type="vertical"
> >
<div <div
className="sc-lmgPJu cuMSiO" className="sc-iJCSeZ boUFen"
> >
<div <div
className="sc-crzpnZ iahNiP" className="sc-dItHI exgtuU"
> >
<button <button
active={true} className="sc-bqGGcD entqcN"
className="sc-dItHI guVVWw"
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
> >
Ascending Ascending
</button> </button>
<button <button
active={false} className="sc-bqGGcD graLyf"
className="sc-dItHI fIRqtD"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
> >
@@ -30,7 +28,8 @@ exports[`UserTable Should render correctly 1`] = `
</div> </div>
<select <select
aria-label="Sort" aria-label="Sort"
className="sc-iqAbyq czAJZn" className="sc-crzpnZ iXFhDe"
disabled={false}
onChange={[Function]} onChange={[Function]}
value="" value=""
> >
@@ -54,13 +53,13 @@ exports[`UserTable Should render correctly 1`] = `
</option> </option>
</select> </select>
<input <input
className="sc-kEqXeH jMVPeG" className="sc-iqAbyq kOrhXJ"
onChange={[Function]} onChange={[Function]}
placeholder="Search by name..." placeholder="Search by name..."
/> />
</div> </div>
<div <div
className="sc-giAruI hMNXks" className="sc-ezyZrH dFKszz"
/> />
</div> </div>
`; `;
@@ -2,19 +2,19 @@
exports[`User Should render correctly 1`] = ` exports[`User Should render correctly 1`] = `
<main <main
className="sc-fKgKDd iDHRyD" className="sc-lmgPJu fmDjvX"
> >
<div <div
className="sc-lmgPJu bFEqIE" className="sc-cxNGUP cGfCbe"
type="horizontal" type="horizontal"
> >
<h2 <h2
className="sc-bCwene htGYaf" className="sc-iJCSeZ jfWGKb"
> >
List User List User
</h2> </h2>
<button <button
className="sc-kfYpNk QIeuT" className="sc-iemXqs gdUlEE"
onClick={[Function]} onClick={[Function]}
variations="primary" variations="primary"
> >
@@ -22,26 +22,24 @@ exports[`User Should render correctly 1`] = `
</button> </button>
</div> </div>
<div <div
className="sc-lmgPJu hehIVl" className="sc-cxNGUP hDpxxT"
type="vertical" type="vertical"
> >
<div <div
className="sc-iwaiBT eOzYWb" className="sc-giAruI kILZpe"
> >
<div <div
className="sc-crzpnZ iahNiP" className="sc-dItHI exgtuU"
> >
<button <button
active={true} className="sc-bqGGcD entqcN"
className="sc-dItHI guVVWw"
disabled={true} disabled={true}
onClick={[Function]} onClick={[Function]}
> >
Ascending Ascending
</button> </button>
<button <button
active={false} className="sc-bqGGcD graLyf"
className="sc-dItHI fIRqtD"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
> >
@@ -50,7 +48,8 @@ exports[`User Should render correctly 1`] = `
</div> </div>
<select <select
aria-label="Sort" aria-label="Sort"
className="sc-iqAbyq czAJZn" className="sc-crzpnZ iXFhDe"
disabled={false}
onChange={[Function]} onChange={[Function]}
value="" value=""
> >
@@ -74,13 +73,13 @@ exports[`User Should render correctly 1`] = `
</option> </option>
</select> </select>
<input <input
className="sc-kEqXeH jMVPeG" className="sc-iqAbyq kOrhXJ"
onChange={[Function]} onChange={[Function]}
placeholder="Search by phone..." placeholder="Search by phone..."
/> />
</div> </div>
<div <div
className="sc-iJCSeZ gDbWTd" className="sc-ezyZrH dFKszz"
/> />
</div> </div>
</main> </main>

Some files were not shown because too many files have changed in this diff Show More