Merge pull request #50 from Nez27/feat/fix-comments

Fix comment
This commit is contained in:
Loi Phan
2023-12-14 19:54:59 +07:00
committed by GitHub
128 changed files with 1426 additions and 1428 deletions
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom'
+5 -12
View File
@@ -1,25 +1,18 @@
import type { Config } from 'jest'; import type { Config } from 'jest';
const config: Config = { const config: Config = {
preset: 'ts-jest',
verbose: true, verbose: true,
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
transform: { transform: {
'^.+\\.tsx?$': 'ts-jest', '^.+\\.tsx?$': 'ts-jest',
'.+\\.(svg|css|styl|less|sass|scss|png|jpg|ttf|woff|gif)$':
'jest-transform-stub',
}, },
moduleNameMapper: { moduleNameMapper: {
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.ts', '@src/(.*)': ['<rootDir>/src/$1'],
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
'@service/(.*)': ['<rootDir>/src/services/$1'],
'@constant/(.*)': ['<rootDir>/src/constants/$1'],
'@hook/(.*)': ['<rootDir>/src/hooks/$1'],
'@helper/(.*)': ['<rootDir>/src/helpers/$1'],
'@context/(.*)': ['<rootDir>/src/contexts/$1'],
'@component/(.*)': ['<rootDir>/src/components/$1'],
'@commonStyle/(.*)': ['<rootDir>/src/commons/styles/$1'],
'@type/(.*)': ['<rootDir>/src/types/$1'],
'@page/(.*)': ['<rootDir>/src/pages/$1'],
}, },
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
setupFiles: ['<rootDir>/.jest/setEnvVar.ts'], setupFiles: ['<rootDir>/.jest/setEnvVar.ts'],
testEnvironmentOptions: { testEnvironmentOptions: {
customExportConditions: [''], customExportConditions: [''],
-1
View File
@@ -1 +0,0 @@
import "@testing-library/jest-dom";
+2
View File
@@ -28,6 +28,7 @@
"styled-components": "^6.1.0" "styled-components": "^6.1.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.23.3",
"@testing-library/jest-dom": "^6.1.4", "@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.0", "@testing-library/react": "^14.1.0",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
@@ -45,6 +46,7 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-transform-stub": "^2.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.2", "typescript": "^5.0.2",
+121
View File
@@ -40,6 +40,9 @@ dependencies:
version: 6.1.0(react-dom@18.2.0)(react@18.2.0) version: 6.1.0(react-dom@18.2.0)(react@18.2.0)
devDependencies: devDependencies:
'@babel/preset-typescript':
specifier: ^7.23.3
version: 7.23.3(@babel/core@7.23.2)
'@testing-library/jest-dom': '@testing-library/jest-dom':
specifier: ^6.1.4 specifier: ^6.1.4
version: 6.1.4(@types/jest@29.5.8)(jest@29.7.0) version: 6.1.4(@types/jest@29.5.8)(jest@29.7.0)
@@ -91,6 +94,9 @@ devDependencies:
jest-environment-jsdom: jest-environment-jsdom:
specifier: ^29.7.0 specifier: ^29.7.0
version: 29.7.0 version: 29.7.0
jest-transform-stub:
specifier: ^2.0.0
version: 2.0.0
ts-jest: ts-jest:
specifier: ^29.1.1 specifier: ^29.1.1
version: 29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@5.2.2) version: 29.1.1(@babel/core@7.23.2)(jest@29.7.0)(typescript@5.2.2)
@@ -175,6 +181,13 @@ packages:
jsesc: 2.5.2 jsesc: 2.5.2
dev: true dev: true
/@babel/helper-annotate-as-pure@7.22.5:
resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.0
dev: true
/@babel/helper-compilation-targets@7.22.15: /@babel/helper-compilation-targets@7.22.15:
resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -186,6 +199,24 @@ packages:
semver: 6.3.1 semver: 6.3.1
dev: true dev: true
/@babel/helper-create-class-features-plugin@7.23.5(@babel/core@7.23.2):
resolution: {integrity: sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.23.2
'@babel/helper-annotate-as-pure': 7.22.5
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-function-name': 7.23.0
'@babel/helper-member-expression-to-functions': 7.23.0
'@babel/helper-optimise-call-expression': 7.22.5
'@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.2)
'@babel/helper-skip-transparent-expression-wrappers': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
semver: 6.3.1
dev: true
/@babel/helper-environment-visitor@7.22.20: /@babel/helper-environment-visitor@7.22.20:
resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -206,6 +237,13 @@ packages:
'@babel/types': 7.23.0 '@babel/types': 7.23.0
dev: true dev: true
/@babel/helper-member-expression-to-functions@7.23.0:
resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.0
dev: true
/@babel/helper-module-imports@7.22.15: /@babel/helper-module-imports@7.22.15:
resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -227,11 +265,44 @@ packages:
'@babel/helper-validator-identifier': 7.22.20 '@babel/helper-validator-identifier': 7.22.20
dev: true dev: true
/@babel/helper-module-transforms@7.23.3(@babel/core@7.23.2):
resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.23.2
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-module-imports': 7.22.15
'@babel/helper-simple-access': 7.22.5
'@babel/helper-split-export-declaration': 7.22.6
'@babel/helper-validator-identifier': 7.22.20
dev: true
/@babel/helper-optimise-call-expression@7.22.5:
resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.0
dev: true
/@babel/helper-plugin-utils@7.22.5: /@babel/helper-plugin-utils@7.22.5:
resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dev: true dev: true
/@babel/helper-replace-supers@7.22.20(@babel/core@7.23.2):
resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/core': 7.23.2
'@babel/helper-environment-visitor': 7.22.20
'@babel/helper-member-expression-to-functions': 7.23.0
'@babel/helper-optimise-call-expression': 7.22.5
dev: true
/@babel/helper-simple-access@7.22.5: /@babel/helper-simple-access@7.22.5:
resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -239,6 +310,13 @@ packages:
'@babel/types': 7.23.0 '@babel/types': 7.23.0
dev: true dev: true
/@babel/helper-skip-transparent-expression-wrappers@7.22.5:
resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.23.0
dev: true
/@babel/helper-split-export-declaration@7.22.6: /@babel/helper-split-export-declaration@7.22.6:
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -418,6 +496,18 @@ packages:
'@babel/helper-plugin-utils': 7.22.5 '@babel/helper-plugin-utils': 7.22.5
dev: true dev: true
/@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.2):
resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.23.2
'@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.2)
'@babel/helper-plugin-utils': 7.22.5
'@babel/helper-simple-access': 7.22.5
dev: true
/@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.23.2): /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.23.2):
resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -438,6 +528,33 @@ packages:
'@babel/helper-plugin-utils': 7.22.5 '@babel/helper-plugin-utils': 7.22.5
dev: true dev: true
/@babel/plugin-transform-typescript@7.23.5(@babel/core@7.23.2):
resolution: {integrity: sha512-2fMkXEJkrmwgu2Bsv1Saxgj30IXZdJ+84lQcKKI7sm719oXs0BBw2ZENKdJdR1PjWndgLCEBNXJOri0fk7RYQA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.23.2
'@babel/helper-annotate-as-pure': 7.22.5
'@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.23.2)
'@babel/helper-plugin-utils': 7.22.5
'@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.2)
dev: true
/@babel/preset-typescript@7.23.3(@babel/core@7.23.2):
resolution: {integrity: sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.23.2
'@babel/helper-plugin-utils': 7.22.5
'@babel/helper-validator-option': 7.22.15
'@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.2)
'@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.2)
'@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.23.2)
dev: true
/@babel/runtime@7.23.2: /@babel/runtime@7.23.2:
resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -3383,6 +3500,10 @@ packages:
- supports-color - supports-color
dev: true dev: true
/jest-transform-stub@2.0.0:
resolution: {integrity: sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==}
dev: true
/jest-util@29.7.0: /jest-util@29.7.0:
resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+4 -4
View File
@@ -1,14 +1,14 @@
import { render, act, RenderResult } from '@testing-library/react'; import { render, act, RenderResult } from '@testing-library/react';
// Services // Services
import * as userServices from '@service/userServices'; import * as userServices from '@src/services/userServices';
import * as roomServices from '@service/roomServices'; import * as roomServices from '@src/services/roomServices';
// Components // Components
import App from './App'; import App from './App';
jest.mock('@service/userServices'); jest.mock('@src/services/userServices');
jest.mock('@service/roomServices'); jest.mock('@src/services/roomServices');
describe('App', () => { describe('App', () => {
let wrapper: RenderResult | null = null; let wrapper: RenderResult | null = null;
+24 -79
View File
@@ -2,106 +2,51 @@ 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 Toast from './components/Toast';
import RouteProtected from '@component/RouteProtected'; import RouteProtected from '@src/components/RouteProtected';
import RootLayout from '@component/RootLayout'; import RootLayout from '@src/components/RootLayout';
// Pages // Pages
import User from './pages/User'; import User from './pages/User';
import Booking from './pages/Booking'; 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 Login from '@src/pages/Login';
import Account from '@page/Account'; import Account from '@src/pages/Account';
// Constants // Constants
import * as PATH from './constants/path'; import * as PATH from './constants/path';
// Hooks
import {
UserRoomAvailableContext,
initialState,
reducer,
} from '@context/UserRoomAvailableContext';
// Services
import { getAllUsers } from '@service/userServices';
import { getAllRooms } from '@service/roomServices';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
function App() { function App() {
const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer(
reducer,
initialState
);
// Init list user and room available
useEffect(() => {
const load = async () => {
const tempUser = await getAllUsers({
sortBy: 'id',
orderBy: 'asc',
phoneSearch: '',
page: 0,
});
if (tempUser) {
dispatch({ type: 'initUser', payload: tempUser.data });
}
const tempRoom = await getAllRooms({
sortBy: 'id',
orderBy: 'asc',
roomSearch: '',
page: 0,
});
if (tempRoom) {
dispatch({ type: 'initRoom', payload: tempRoom.data });
}
};
load();
}, []);
const store = useMemo(() => {
return { roomsAvailable, usersAvailable, dispatch };
}, [roomsAvailable, usersAvailable]);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<StyleSheetManager shouldForwardProp={shouldForwardProp}> <StyleSheetManager shouldForwardProp={shouldForwardProp}>
<UserRoomAvailableContext.Provider value={store}> <BrowserRouter>
<BrowserRouter> <Routes>
<Routes> <Route element={<RootLayout />}>
<Route element={<RootLayout />}> <Route
<Route element={
element={ <RouteProtected>
<RouteProtected> <AppLayout />
<AppLayout /> </RouteProtected>
</RouteProtected> }
} >
> <Route index element={<Navigate replace to={PATH.BOOKING} />} />
<Route <Route path={PATH.BOOKING} element={<Booking />} />
index <Route path={PATH.USER} element={<User />} />
element={<Navigate replace to={PATH.BOOKING} />} <Route path={PATH.ROOM} element={<Room />} />
/> <Route path={PATH.ACCOUNT} element={<Account />} />
<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>
</Routes> <Route path={PATH.LOGIN} element={<Login />} />
</BrowserRouter> <Route path={PATH.OTHER_PATH} element={<NotFound />} />
</UserRoomAvailableContext.Provider> </Route>
</Routes>
</BrowserRouter>
</StyleSheetManager> </StyleSheetManager>
<Toast /> <Toast />
@@ -1,7 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
// Types // Types
import { TDirection } from '@type/common'; import { TDirection } from '@src/types/common';
interface IDirection { interface IDirection {
type?: TDirection; type?: TDirection;
@@ -16,7 +16,6 @@ const Spinner = styled.div`
top/10px 10px no-repeat, top/10px 10px no-repeat,
conic-gradient(#0000 30%, var(--primary-color)); conic-gradient(#0000 30%, var(--primary-color));
-webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 10px), #000 0); -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 10px), #000 0);
mask: none;
animation: ${rotate} 1.5s infinite linear; animation: ${rotate} 1.5s infinite linear;
`; `;
@@ -21,8 +21,9 @@ exports[`AppLayout Should render correctly 1`] = `
<li> <li>
<button <button
aria-label="Profile" aria-label="Profile"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
fill="currentColor" fill="currentColor"
@@ -41,14 +42,14 @@ exports[`AppLayout Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="Logout" aria-label="Logout"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -67,7 +68,6 @@ exports[`AppLayout Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
</ul> </ul>
@@ -172,8 +172,9 @@ exports[`AppLayout Should render correctly 1`] = `
<li> <li>
<button <button
aria-label="Profile" aria-label="Profile"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
fill="currentColor" fill="currentColor"
@@ -192,14 +193,14 @@ exports[`AppLayout Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="Logout" aria-label="Logout"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -218,7 +219,6 @@ exports[`AppLayout Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
</ul> </ul>
@@ -1,26 +1,71 @@
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
// Components // Components
import Header from '@component/Header'; import Header from '../Header';
import Sidebar from '@component/Sidebar'; import Sidebar from '../Sidebar';
// Styled // Styled
import { Main, StyledAppLayout } from './styled'; import { Main, StyledAppLayout } from './styled';
// Hooks // Hooks
import { useAccount } from '@hook/authentication/useAccount'; import { useAccount } from '@src/hooks/authentication/useAccount';
// Contexts
import { initialState, reducer, UserRoomAvailableContext } from '@src/contexts/UserRoomAvailableContext.ts';
import { useEffect, useReducer } from 'react';
// Services
import { getAllUsers } from '@src/services/userServices.ts';
import { getAllRooms } from '@src/services/roomServices.ts';
const AppLayout = () => { const AppLayout = () => {
const { account } = useAccount(); const { account } = useAccount();
const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer(
reducer,
initialState
);
// Init list user and room available
// Init list user and room available
useEffect(() => {
const load = async () => {
const tempUser = await getAllUsers({
sortBy: 'id',
orderBy: 'asc',
phoneSearch: '',
page: 0,
});
if (tempUser) {
dispatch({ type: 'initUser', payload: tempUser.data });
}
const tempRoom = await getAllRooms({
sortBy: 'id',
orderBy: 'asc',
roomSearch: '',
page: 0,
});
if (tempRoom) {
dispatch({ type: 'initRoom', payload: tempRoom.data });
}
};
load();
}, []);
return ( return (
<StyledAppLayout> <UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable, dispatch}}>
<Header accountName={account?.user_metadata.fullName} /> <StyledAppLayout>
<Sidebar heading="Hotel Management" /> <Header accountName={account?.user_metadata.fullName} />
<Main> <Sidebar heading="Hotel Management" />
<Outlet /> <Main>
</Main> <Outlet />
</StyledAppLayout> </Main>
</StyledAppLayout>
</UserRoomAvailableContext.Provider>
); );
}; };
@@ -9,7 +9,6 @@ describe('ButtonIcon', () => {
<ButtonIcon <ButtonIcon
aria-label="Logout" aria-label="Logout"
icon={<HiOutlineLogout />} icon={<HiOutlineLogout />}
iconStyle={{ size: '23px' }}
/> />
); );
@@ -6,8 +6,10 @@ exports[`ButtonIcon Should render correctly 1`] = `
"baseElement": <body> "baseElement": <body>
<div> <div>
<button <button
class="sc-bdnyFh bxHnoO" aria-label="Logout"
iconstyle="[object Object]" class="sc-bdnyFh gPNQLR"
iconcolor="#000"
iconsize="16px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -26,14 +28,15 @@ exports[`ButtonIcon Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</div> </div>
</body>, </body>,
"container": <div> "container": <div>
<button <button
class="sc-bdnyFh bxHnoO" aria-label="Logout"
iconstyle="[object Object]" class="sc-bdnyFh gPNQLR"
iconcolor="#000"
iconsize="16px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -52,7 +55,6 @@ exports[`ButtonIcon Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</div>, </div>,
"debug": [Function], "debug": [Function],
@@ -1,50 +1,35 @@
import { ReactNode } from 'react'; import { ButtonHTMLAttributes, ReactNode } from 'react';
// Styled // Styled
import { StyledButtonIcon } from './styled'; import { StyledButtonIcon } from './styled';
interface IButtonIcon { interface IButtonIcon extends ButtonHTMLAttributes<HTMLButtonElement> {
icon: ReactNode; icon: ReactNode;
children?: ReactNode; iconSize?: string;
style?: { iconColor?: string;
fontSize?: string; fontSize?: string;
backgroundColor?: string; text?: string;
color?: string; variations?: string;
};
iconStyle?: {
color?: string;
size?: string;
};
onClick?: () => void;
disabled?: boolean;
label?: string;
} }
const ButtonIcon = ({ const ButtonIcon = ({
icon, icon,
children, iconSize,
style, iconColor,
iconStyle, text,
onClick, fontSize,
disabled, variations,
label, ...props
}: IButtonIcon) => { }: IButtonIcon) => {
const isHaveChildren = Boolean(children);
return ( return (
<StyledButtonIcon <StyledButtonIcon
aria-label={label} iconColor={iconColor}
isHaveChildren={isHaveChildren} iconSize={iconSize}
style={{ fontSize={fontSize}
fontSize: style?.fontSize, variations={variations}
backgroundColor: style?.backgroundColor, {...props}
color: style?.color,
}}
iconStyle={iconStyle}
onClick={onClick}
disabled={disabled}
> >
{icon} <span>{children}</span> {icon} {text && <span>{text}</span>}
</StyledButtonIcon> </StyledButtonIcon>
); );
}; };
@@ -1,89 +1,104 @@
import styled, { css } from 'styled-components'; import styled, { css, RuleSet } from 'styled-components';
interface IStyledButtonIcon { interface IStyledButtonIcon {
isHaveChildren: boolean; iconSize?: string;
style?: { iconColor?: string;
fontSize?: string; fontSize?: string;
backgroundColor?: string; variations?: string;
color?: string;
};
iconStyle?: {
color?: string;
size?: string;
};
} }
export interface IVariations {
[key: string]: RuleSet<object>;
}
const variations: IVariations = {
success: css`
background-color: var(--success-btn-color);
color: var(--light-text);
&:hover {
background-color: var(--success-btn-hover-color);
}
&:disabled {
background-color: var(--success-btn-hover-color);
}
`,
primary: css`
background-color: var(--primary-color);
color: var(--light-text);
&:hover {
background-color: var(--primary-hover-color);
}
`,
danger: css`
background-color: var(--danger-btn-color);
color: var(--light-text);
&:hover {
background-color: var(--danger-btn-hover-color);
}
&:disabled {
background-color: var(--danger-btn-hover-color);
}
`,
default: css`
&:hover {
background-color: var(--hover-background-color);
}
`,
};
const StyledButtonIcon = styled.button<IStyledButtonIcon>` const StyledButtonIcon = styled.button<IStyledButtonIcon>`
background: none; background: none;
padding: 10px; display: flex;
align-items: center;
gap: 8px;
${(props) =>
props.fontSize &&
css`
font-size: ${props.fontSize};
`}
padding: 10px 20px;
transition: all 0.2s; transition: all 0.2s;
border: none; border: none;
border-radius: var(--radius-md);
${(props) =>
props.style?.fontSize && &:disabled {
css` cursor: not-allowed;
font-size: ${props.style.fontSize};
`}
${(props) =>
props.style?.fontSize &&
css`
font-size: ${props.style.fontSize};
`}
${(props) =>
props.color &&
css`
font-size: ${props.color};
`}
${(props) =>
props.isHaveChildren
? css`
display: flex;
align-items: center;
gap: 10px;
text-align: left;
width: 100%;
`
: css`
border-radius: var(--radius-md);
`}
&:hover {
background-color: var(--hover-background-color);
} }
${(props) => variations[props.variations!]}
& svg { & svg {
${(props) => ${(props) =>
props.iconStyle?.size && props.iconSize &&
css` css`
width: ${props.iconStyle.size}; width: ${props.iconSize};
height: ${props.iconStyle.size}; height: ${props.iconSize};
`} `}
${(props) => ${(props) =>
props.iconStyle?.color && props.iconColor &&
css` css`
color: ${props.iconStyle.color}; color: ${props.iconColor};
`} `}
transition: all 0.3s; transition: all 0.3s;
} }
`; `;
StyledButtonIcon.defaultProps = { StyledButtonIcon.defaultProps = {
style: { iconSize: '16px',
fontSize: '14px', iconColor: '#000',
backgroundColor: 'var(--primary-color)', fontSize: 'var(--fs-sm-2x)',
color: '#000', variations: 'default',
},
iconStyle: {
color: '#000',
size: '14px',
},
}; };
export { StyledButtonIcon }; export { StyledButtonIcon };
@@ -4,7 +4,7 @@ import { memo, useCallback } from 'react';
import { StyledConfirmDelete } from './styled'; import { StyledConfirmDelete } from './styled';
// Component // Component
import Button from '@commonStyle/Button'; import Button from '@src/commons/styles/Button';
interface IConfirmMessage { interface IConfirmMessage {
message: string; message: string;
@@ -4,7 +4,7 @@ import { render } from '@testing-library/react';
import Form from '.'; import Form from '.';
// Styled // Styled
import Input from '@commonStyle/Input'; import Input from '@src/commons/styles/Input';
describe('Form ', () => { describe('Form ', () => {
const isDisable = true; const isDisable = true;
@@ -11,7 +11,7 @@ import {
} from './styled'; } from './styled';
// Types // Types
import { TDirection } from '@type/common'; import { TDirection } from '@src/types/common';
interface IFormProps { interface IFormProps {
children: ReactNode; children: ReactNode;
@@ -1,14 +1,14 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
// Types // Types
import { TDirection } from '@type/common'; import { TDirection } from '@src/types/common';
interface IStyledFormRow { interface IStyledFormRow {
direction?: TDirection; direction?: TDirection;
} }
// Styled // Styled
import Button from '@commonStyle/Button'; import Button from '@src/commons/styles/Button';
const StyledForm = styled.form` const StyledForm = styled.form`
display: flex; display: flex;
@@ -19,8 +19,9 @@ exports[`Header Should render correctly 1`] = `
<li> <li>
<button <button
aria-label="Profile" aria-label="Profile"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
fill="currentColor" fill="currentColor"
@@ -39,14 +40,14 @@ exports[`Header Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="Logout" aria-label="Logout"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -65,7 +66,6 @@ exports[`Header Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
</ul> </ul>
@@ -87,8 +87,9 @@ exports[`Header Should render correctly 1`] = `
<li> <li>
<button <button
aria-label="Profile" aria-label="Profile"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
fill="currentColor" fill="currentColor"
@@ -107,14 +108,14 @@ exports[`Header Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="Logout" aria-label="Logout"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -133,7 +134,6 @@ exports[`Header Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
</ul> </ul>
@@ -11,8 +11,9 @@ exports[`HeaderMenu Should render correctly 1`] = `
<li> <li>
<button <button
aria-label="Profile" aria-label="Profile"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
fill="currentColor" fill="currentColor"
@@ -31,14 +32,14 @@ exports[`HeaderMenu Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="Logout" aria-label="Logout"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -57,7 +58,6 @@ exports[`HeaderMenu Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
</ul> </ul>
@@ -70,8 +70,9 @@ exports[`HeaderMenu Should render correctly 1`] = `
<li> <li>
<button <button
aria-label="Profile" aria-label="Profile"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
fill="currentColor" fill="currentColor"
@@ -90,14 +91,14 @@ exports[`HeaderMenu Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
<li> <li>
<button <button
aria-label="Logout" aria-label="Logout"
class="sc-bdnyFh bxHnoO" class="sc-bdnyFh olJWF"
iconstyle="[object Object]" iconcolor="#000"
iconsize="23px"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -116,7 +117,6 @@ exports[`HeaderMenu Should render correctly 1`] = `
/> />
</svg> </svg>
<span />
</button> </button>
</li> </li>
</ul> </ul>
@@ -3,13 +3,13 @@ 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';
import ButtonIcon from '@component/ButtonIcon'; import ButtonIcon from '../ButtonIcon';
// Styled // Styled
import { StyledHeaderMenu } from './styled'; import { StyledHeaderMenu } from './styled';
// Hooks // Hooks
import { useLogout } from '@hook/authentication/useLogout'; import { useLogout } from '@src/hooks/authentication/useLogout';
const HeaderMenu = () => { const HeaderMenu = () => {
const { logout, isPending } = useLogout(); const { logout, isPending } = useLogout();
@@ -19,19 +19,19 @@ const HeaderMenu = () => {
<StyledHeaderMenu> <StyledHeaderMenu>
<li> <li>
<ButtonIcon <ButtonIcon
label="Profile" aria-label="Profile"
icon={<IoPersonCircleOutline />} icon={<IoPersonCircleOutline />}
iconStyle={{ size: '23px' }} iconSize='23px'
onClick={() => navigate('/account')} onClick={() => navigate('/account')}
/> />
</li> </li>
<li> <li>
<ButtonIcon <ButtonIcon
onClick={logout} onClick={() => logout()}
disabled={isPending} disabled={isPending}
label='Logout' aria-label='Logout'
icon={<HiOutlineLogout />} icon={<HiOutlineLogout />}
iconStyle={{ size: '23px' }} iconSize='23px'
/> />
</li> </li>
</StyledHeaderMenu> </StyledHeaderMenu>
@@ -17,12 +17,16 @@ describe('Menus', () => {
<Menus.Toggle id={id} /> <Menus.Toggle id={id} />
<Menus.List id={id.toString()}> <Menus.List id={id.toString()}>
<Menus.Button onClick={handleOnClick} icon={<RiEditBoxFill />}> <Menus.Button
Edit onClick={handleOnClick}
</Menus.Button> icon={<RiEditBoxFill />}
<Menus.Button onClick={handleOnClick} icon={<RiDeleteBin2Line />}> label="Edit"
Delete />
</Menus.Button> <Menus.Button
onClick={handleOnClick}
icon={<RiDeleteBin2Line />}
label="Delete"
/>
</Menus.List> </Menus.List>
</Menus.Menu> </Menus.Menu>
</Menus> </Menus>
+13 -24
View File
@@ -2,26 +2,22 @@ import { MouseEvent, ReactNode, useContext, useState } from 'react';
// Components // Components
import { HiEllipsisVertical } from 'react-icons/hi2'; import { HiEllipsisVertical } from 'react-icons/hi2';
import ButtonIcon from '@component/ButtonIcon'; import ButtonIcon from '../ButtonIcon';
// Hooks // Hooks
import { useOutsideClick } from '@hook/useOutsideClick'; import { useOutsideClick } from '@src/hooks/useOutsideClick';
// Styled // Styled
import { import { StyledMenu, StyledList, StyledToggle } from './styled';
StyledMenu,
StyledList,
StyledToggle
} from './styled';
// Contexts // Contexts
import MenusContext from '@context/MenuContext'; import MenusContext from '@src/contexts/MenuContext';
// Types // Types
import { Nullable } from '@type/common'; import { Nullable } from '@src/types/common';
interface IButton { interface IButton {
children?: string; label?: string;
icon?: ReactNode; icon?: ReactNode;
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
@@ -46,17 +42,11 @@ const Menus = ({ children }: { children: ReactNode }) => {
}; };
const Toggle = ({ id }: { id: string }): ReactNode => { const Toggle = ({ id }: { id: string }): ReactNode => {
const { const { openId, close, open } = useContext(MenusContext);
openId,
close,
open
} = useContext(MenusContext);
const handleClick = (e: MouseEvent<HTMLButtonElement>) => { const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
openId !== id openId !== id ? open!(id) : close!();
? open!(id)
: close!();
}; };
return ( return (
@@ -81,7 +71,7 @@ const List = ({
return <StyledList ref={ref}>{children}</StyledList>; return <StyledList ref={ref}>{children}</StyledList>;
}; };
const Button = ({ children, icon, onClick, disabled }: IButton): ReactNode => { const Button = ({ label, icon, onClick, disabled }: IButton): ReactNode => {
const { close } = useContext(MenusContext); const { close } = useContext(MenusContext);
const handleClick = () => { const handleClick = () => {
onClick?.(); onClick?.();
@@ -91,14 +81,13 @@ const Button = ({ children, icon, onClick, disabled }: IButton): ReactNode => {
return ( return (
<li> <li>
<ButtonIcon <ButtonIcon
text={label}
aria-label={label}
icon={icon} icon={icon}
iconColor='var(--primary-color)'
onClick={handleClick} onClick={handleClick}
iconStyle={{ color: 'var(--primary-color)', size: '19px' }}
style={{ fontSize: '16px' }}
disabled={disabled} disabled={disabled}
> />
{children}
</ButtonIcon>
</li> </li>
); );
}; };
@@ -2,7 +2,7 @@ import { render } from '@testing-library/react';
// Components // Components
import Modal from '.'; import Modal from '.';
import Button from '@commonStyle/Button'; import Button from '@src/commons/styles/Button';
describe('Modal', () => { describe('Modal', () => {
const wrapper = render( const wrapper = render(
@@ -13,9 +13,11 @@ describe('Modal', () => {
<Button onClick={onCloseModal}>Add room</Button> <Button onClick={onCloseModal}>Add room</Button>
)} )}
/> />
<Modal.Window name="room-form" title="Add form"> <Modal.Window
<p>This is a windows</p> name="room-form"
</Modal.Window> title="Add form"
renderChildren={(fn) => <p onClick={fn}>This is a windows</p>}
/>
</Modal> </Modal>
); );
@@ -1,17 +1,15 @@
import { import {
ReactElement,
ReactNode, ReactNode,
cloneElement,
useContext, useContext,
useState, useState,
} from 'react'; } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
// Hooks // Hooks
import { useOutsideClick } from '@hook/useOutsideClick'; import { useOutsideClick } from '@src/hooks/useOutsideClick';
// Contexts // Contexts
import { ModalContext } from '@context/ModalContext'; import { ModalContext } from '@src/contexts/ModalContext';
// Styled // Styled
import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled'; import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled';
@@ -25,7 +23,7 @@ const Modal = ({ children }: IModal) => {
const close = () => setOpenName(''); const close = () => setOpenName('');
const open = setOpenName; const open = setOpenName;
return ( return (
<ModalContext.Provider value={{ openName, close, open }}> <ModalContext.Provider value={{ openName, close, open }}>
{children} {children}
@@ -34,7 +32,7 @@ const Modal = ({ children }: IModal) => {
}; };
interface IOpen { interface IOpen {
renderChildren: (onCloseModal: () => void) => ReactNode; renderChildren: (onOpenModal: () => void) => ReactNode;
modalName: string; modalName: string;
} }
@@ -45,12 +43,12 @@ const Open = ({ renderChildren, modalName }: IOpen) => {
}; };
interface IWindow { interface IWindow {
children: ReactElement; renderChildren: (callBack: () => void) => ReactNode;
name: string; name: string;
title: string; title: string;
} }
const Window = ({ children, name, title }: IWindow) => { const Window = ({ renderChildren, name, title }: IWindow) => {
const { openName, close } = useContext(ModalContext); const { openName, close } = useContext(ModalContext);
const ref = useOutsideClick<HTMLDivElement>(close!); const ref = useOutsideClick<HTMLDivElement>(close!);
@@ -61,7 +59,7 @@ const Window = ({ children, name, title }: IWindow) => {
<StyledModal ref={ref}> <StyledModal ref={ref}>
<StyledModalContent> <StyledModalContent>
<TitleModal>{title}</TitleModal> <TitleModal>{title}</TitleModal>
{cloneElement(children, { onCloseModal: close })} {renderChildren(close!)}
</StyledModalContent> </StyledModalContent>
</StyledModal> </StyledModal>
</Overlay>, </Overlay>,
@@ -1,4 +1,3 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
@@ -7,7 +7,7 @@ import { BsHouseDoorFill } from 'react-icons/bs';
import { StyledNav, StyledNavLink } from './styled'; import { StyledNav, StyledNavLink } from './styled';
// Constants // Constants
import { BOOKING, ROOM, USER } from '@constant/path'; import { BOOKING, ROOM, USER } from '@src/constants/path';
const Nav = () => { const Nav = () => {
return ( return (
@@ -12,10 +12,10 @@ exports[`Pagination Should render correctly 1`] = `
Showing Showing
1 1
to to
5 10
of of
<span> <span>
10 20
</span> </span>
results results
</p> </p>
@@ -84,10 +84,10 @@ exports[`Pagination Should render correctly 1`] = `
Showing Showing
1 1
to to
5 10
of of
<span> <span>
10 20
</span> </span>
results results
</p> </p>
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
import Pagination from '.'; import Pagination from '.';
describe('Pagination', () => { describe('Pagination', () => {
const count = 10; const count = 20;
let wrapper: RenderResult | null = null; let wrapper: RenderResult | null = null;
beforeEach(() => { beforeEach(() => {
@@ -1,7 +1,7 @@
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// Constants // Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config'; import { DEFAULT_PAGE_SIZE } from '@src/constants/config';
// Styled // Styled
import { Buttons, PaginationBtn, StyledPagination } from './styled'; import { Buttons, PaginationBtn, StyledPagination } from './styled';
@@ -1,11 +1,11 @@
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
// Styled // Styled
import Spinner from '@commonStyle/Spinner'; import Spinner from '@src/commons/styles/Spinner';
import { FullPageLayout } from './styled'; import { FullPageLayout } from './styled';
// Hooks // Hooks
import { useAccount } from '@hook/authentication/useAccount'; import { useAccount } from '@src/hooks/authentication/useAccount';
const RootLayout = () => { const RootLayout = () => {
const { isPending } = useAccount(); const { isPending } = useAccount();
@@ -1,6 +1,6 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import RouteProtected from '.'; import RouteProtected from '.';
import AppLayout from '@component/AppLayout'; import AppLayout from '../AppLayout';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
@@ -1,4 +1,4 @@
import { useAccount } from '@hook/authentication/useAccount'; import { useAccount } from '@src/hooks/authentication/useAccount';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
@@ -9,7 +9,7 @@ jest.mock('react-router-dom', () => ({
useSearchParams: jest.fn(), useSearchParams: jest.fn(),
})); }));
jest.mock('@hook/useDebounce', () => ({ jest.mock('@src/hooks/useDebounce', () => ({
useDebounce: jest.fn((value) => value), useDebounce: jest.fn((value) => value),
})); }));
@@ -5,10 +5,10 @@ import { useSearchParams } from 'react-router-dom';
import { StyledSearch } from './styled'; import { StyledSearch } from './styled';
// Hooks // Hooks
import { useDebounce } from '@hook/useDebounce'; import { useDebounce } from '@src/hooks/useDebounce';
// Types // Types
import { Nullable } from '@type/common'; import { Nullable } from '@src/types/common';
interface ISearch { interface ISearch {
setPlaceHolder: string; setPlaceHolder: string;
@@ -17,7 +17,12 @@ interface ISearch {
const Search = ({ setPlaceHolder }: ISearch) => { const Search = ({ setPlaceHolder }: ISearch) => {
const field = 'search'; const field = 'search';
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [query, setQuery] = useState<Nullable<string>>(null); const [query, setQuery] = useState<Nullable<string>>(
searchParams.get(field)
? searchParams.get(field)
: '',
);
const debounceValue = useDebounce<Nullable<string>>(query, 700); const debounceValue = useDebounce<Nullable<string>>(query, 700);
useEffect(() => { useEffect(() => {
@@ -32,6 +37,7 @@ const Search = ({ setPlaceHolder }: ISearch) => {
<StyledSearch <StyledSearch
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder={setPlaceHolder} placeholder={setPlaceHolder}
value={query!}
/> />
); );
}; };
@@ -1,5 +1,5 @@
// Components // Components
import Nav from '@component/Nav'; import Nav from '../Nav';
// Styled // Styled
import { Heading, StyledSidebar } from './styled'; import { Heading, StyledSidebar } from './styled';
@@ -2,7 +2,7 @@ import { useSearchParams } from 'react-router-dom';
import { ChangeEvent, memo } from 'react'; import { ChangeEvent, memo } from 'react';
// Components // Components
import Select from '@component/Select'; import Select from '../Select';
interface ISortByProps { interface ISortByProps {
options: { options: {
@@ -1,60 +0,0 @@
import { render } from '@testing-library/react';
// Components
import Table from '.';
// Types
import { IUser } from '@type/user';
interface ITableRow {
user: IUser;
}
describe('Table', () => {
const columnName = ['Column 1', 'Column 2'];
const tempUser: IUser[] = [
{
id: 1,
name: 'Nezumi',
phone: '123',
isBooked: true,
isDelete: true,
},
{
id: 2,
name: 'Loi Phan',
phone: '453',
isBooked: false,
isDelete: true,
},
];
const TableRow = ({ user }: ITableRow) => {
const {
id,
name,
phone,
isBooked
} = user;
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{phone}</div>
<div>{isBooked}</div>
</Table.Row>
);
};
const renderRow = (user: IUser) => <TableRow user={user} key={user.id} />;
const wrapper = render(
<Table columns="10% 35% 30% 15% 10%">
<Table.Header headerColumn={columnName} />
<Table.Body<IUser> data={tempUser} render={renderRow} />
</Table>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
+132 -51
View File
@@ -1,70 +1,151 @@
import { ReactNode, useContext } from 'react'; // Types
import {ColumnProps} from '@src/types/common';
// Styled // Styled
import { import {
StyledBody, StyledBody,
StyledFooter,
StyledHeader, StyledHeader,
StyledRow, StyledRow,
StyledTable, StyledTable,
StyledFooter,
} from './styled'; } from './styled';
// Contexts // Components
import TableContext from '@context/TableContext'; import Pagination from '../Pagination';
import {TbArrowNarrowRight} from 'react-icons/tb';
import {StyledTableOption} from '@src/pages/Room/styled.ts';
import OrderBy from '@src/components/OrderBy';
import {ORDERBY_OPTIONS} from '@src/constants/commons.ts';
import SortBy from '@src/components/SortBy';
import Search from '@src/components/Search';
import Direction from '@src/commons/styles/Direction.ts';
import {Dispatch, SetStateAction} from 'react';
export interface ITable { const parseWidthString = (columnsWidth: number[]): string => {
columns?: string; let result: string = '';
children: ReactNode;
}
export interface IHeader { const totalWidth = columnsWidth.reduce((
headerColumn: string[]; partialSum,
} a
) => partialSum + a, 0);
columnsWidth.forEach((width) => {
result += Math.round((
width / totalWidth
) * 100)
.toString() + '% ';
});
interface ITableBody<T> { return result;
data?: T[];
render?: CallbackMapFunc<T>;
}
type CallbackMapFunc<T> = (value: T, index: number, array: T[]) => ReactNode;
const Table = ({ columns, children }: ITable) => {
return (
<TableContext.Provider value={{ columns }}>
<StyledTable>{children}</StyledTable>
</TableContext.Provider>
);
}; };
const Header = ({ headerColumn }: IHeader) => { type Props<T> = {
const { columns } = useContext(TableContext); columns: ColumnProps[];
rows: T[];
stateSelected: {
itemSelected: T | undefined;
setItemSelected: Dispatch<SetStateAction<T | undefined>>;
};
count?: number | null;
onRowClick?: (rowData: T) => void;
enabledOrder?: boolean;
sortBy?: {
value: string;
label: string;
}[];
searchPlaceHolder?: string;
};
const Table = <T, >({
rows,
columns,
count,
onRowClick,
stateSelected,
sortBy,
searchPlaceHolder,
enabledOrder = false,
}: Props<T>) => {
const width = parseWidthString(columns.map((item) => item.width));
const {itemSelected, setItemSelected} = stateSelected;
const headers = columns.map((
column,
index
) => {
return <div key={`headCell-${index}`}>{column.title}</div>;
});
const renderRows = rows.map((
row,
rowIndex
) => {
const handleRowClick = () => {
if (onRowClick) {
onRowClick(row);
setItemSelected(row);
}
};
return (
<StyledRow
key={`row-${rowIndex}`}
width={width}
onDoubleClick={handleRowClick}
className={
JSON.stringify(itemSelected) === JSON.stringify(row)
? 'selected'
: ''
}
>
{columns.map((
column,
columnIndex
) => {
const value = row[column.key as keyof typeof row] as string;
if (column.isDateValue) {
return (
<div
style={{display: 'flex', alignItems: 'center'}}
key={`cell-${columnIndex}`}
>
{value[0]} &nbsp; <TbArrowNarrowRight/>
&nbsp; {value[1]}
</div>
);
}
return <div key={`cell-${columnIndex}`}>{value}</div>;
})}
</StyledRow>
);
});
return ( return (
<StyledHeader columns={columns}> <Direction>
{headerColumn.map((item) => ( <StyledTableOption>
<div key={item}>{item}</div> {enabledOrder && <OrderBy options={ORDERBY_OPTIONS}/>}
))} {Boolean(sortBy?.length) && <SortBy options={sortBy!}/>}
</StyledHeader> {Boolean(searchPlaceHolder) && <Search setPlaceHolder={searchPlaceHolder!}/>}
</StyledTableOption>
{rows && rows.length &&
(
<StyledTable>
<StyledHeader width={width}>{headers}</StyledHeader>
<StyledBody>{renderRows}</StyledBody>
{Boolean(count) && (
<StyledFooter>
<Pagination count={count!}/>
</StyledFooter>
)}
</StyledTable>
)
}
</Direction>
); );
}; }
const Body = <T,>({ data, render }: ITableBody<T>) => {
return (
data!.length && (
<StyledBody>{data?.map(render as CallbackMapFunc<T>)}</StyledBody>
)
);
};
const Row = ({ children }: ITable) => {
const { columns } = useContext(TableContext);
return <StyledRow columns={columns}>{children}</StyledRow>;
};
Table.Header = Header;
Table.Body = Body;
Table.Row = Row;
Table.Footer = StyledFooter;
export default Table; export default Table;
@@ -1,7 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
// Interfaces interface ICommonRow {
import { ITable } from '.'; width: string;
}
const StyledTable = styled.div` const StyledTable = styled.div`
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
@@ -10,9 +11,9 @@ const StyledTable = styled.div`
font-size: 20px; font-size: 20px;
`; `;
const CommonRow = styled.div<ITable>` const CommonRow = styled.div<ICommonRow>`
display: grid; display: grid;
grid-template-columns: ${(props) => props.columns}; grid-template-columns: ${(props) => props.width};
column-gap: 10px; column-gap: 10px;
align-items: center; align-items: center;
justify-items: center; justify-items: center;
@@ -24,6 +25,16 @@ const StyledBody = styled.div`
const StyledRow = styled(CommonRow)` const StyledRow = styled(CommonRow)`
padding: 20px; padding: 20px;
cursor: pointer;
&:hover {
background-color: var(--item-hover);
}
&.selected {
background-color: var(--item-selected);
}
&:not(:last-child) { &:not(:last-child) {
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
@@ -0,0 +1,35 @@
import { Dispatch, createContext } from 'react';
export type TItemSelectAction = 'setData' | 'removeData';
interface IAction<T> {
type: TItemSelectAction;
payload: T;
}
interface IItemSelect {
data?: any;
dispatch?: Dispatch<IAction<unknown>>;
}
const ItemSelectContext = createContext<IItemSelect>({});
const itemSelectReducer =
<T>() =>
(state: T, action: IAction<T>) => {
switch (action.type) {
// Init data for the first load
case 'setData': {
const tempData = { ...state };
return {
data: tempData,
};
}
default:
throw new Error('Action unknown');
}
};
export { itemSelectReducer, ItemSelectContext };
@@ -1,7 +1,7 @@
import { Dispatch, createContext } from 'react'; import { Dispatch, createContext } from 'react';
// Types // Types
import { IDataState } from '@type/common'; import { IDataState } from '@src/types/common';
interface IUserRoomState { interface IUserRoomState {
usersAvailable: IDataState[]; usersAvailable: IDataState[];
@@ -1,8 +1,8 @@
// Contexts // Contexts
import { reducer } from '@context/UserRoomAvailableContext'; import { reducer } from '../UserRoomAvailableContext';
// Types // Types
import { IDataState } from '@type/common'; import { IDataState } from '@src/types/common';
describe('userRoomAvailableContext', () => { describe('userRoomAvailableContext', () => {
let initialState: { let initialState: {
@@ -1,5 +1,5 @@
// Helpers // Helpers
import { formatCurrency, getDayDiff } from '@helper/helper'; import { formatCurrency, getDayDiff } from '../helper';
describe('formatCurrency', () => { describe('formatCurrency', () => {
test('Should format correctly', () => { test('Should format correctly', () => {
@@ -1,5 +1,5 @@
// Constants // Constants
import { REGEX } from '@constant/commons'; import { REGEX } from '@src/constants/commons';
// Helpers // Helpers
import { import {
@@ -7,7 +7,7 @@ import {
isValidDiscount, isValidDiscount,
isValidRegex, isValidRegex,
isValidString, isValidString,
} from '@helper/validators'; } from '../validators';
describe('isValidPhone', () => { describe('isValidPhone', () => {
const isValidPhone = (phone: string) => const isValidPhone = (phone: string) =>
+5
View File
@@ -29,7 +29,12 @@ const getDayDiff = (startDate: Date, endDate: Date): number => {
return 0; return 0;
} }
const findItemInListById = <T,>(id: number, listItem: T[]): T | undefined => {
return listItem.find((item) => item["id" as keyof typeof item] === id);
}
export { export {
formatCurrency, formatCurrency,
getDayDiff, getDayDiff,
findItemInListById,
}; };
@@ -2,7 +2,7 @@ import { act, fireEvent, render } from '@testing-library/react';
import { useState } from 'react'; import { useState } from 'react';
// Hooks // Hooks
import { useDebounce } from '@hook/useDebounce'; import { useDebounce } from '../useDebounce';
jest.useFakeTimers(); jest.useFakeTimers();
@@ -1,7 +1,7 @@
import { fireEvent, render, renderHook, screen } from '@testing-library/react'; import { fireEvent, render, renderHook, screen } from '@testing-library/react';
// Hooks // Hooks
import { useOutsideClick } from '@hook/useOutsideClick'; import { useOutsideClick } from '../useOutsideClick';
describe('useOutsideClick', () => { describe('useOutsideClick', () => {
test('Call handler when click outside element', () => { test('Call handler when click outside element', () => {
@@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react';
import { User } from '@supabase/supabase-js'; import { User } from '@supabase/supabase-js';
// Services // Services
import * as authenticationService from '@service/authenticationService'; import * as authenticationService from '@src/services/authenticationService';
// Hooks // Hooks
import { useAccount } from '../useAccount'; import { useAccount } from '../useAccount';
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
// Services // Services
import { getCurrentAccount } from '@service/authenticationService'; import { getCurrentAccount } from '@src/services/authenticationService';
/** /**
* Get data of account currently login * Get data of account currently login
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { login as loginFn } from '@service/authenticationService'; import { login as loginFn } from '@src/services/authenticationService';
/** /**
* Login web * Login web
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
// Services // Services
import { logout as logoutFn } from '@service/authenticationService'; import { logout as logoutFn } from '@src/services/authenticationService';
/** /**
* Logout web * Logout web
@@ -2,10 +2,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
// Services // Services
import { updateAccount as updateAccountFn } from '@service/authenticationService'; import { updateAccount as updateAccountFn } from '@src/services/authenticationService';
// Constants // Constants
import { UPDATE_ACCOUNT_SUCCESSFUL } from '@constant/messages'; import { UPDATE_ACCOUNT_SUCCESSFUL } from '@src/constants/messages';
/** /**
* Update account currently login. * Update account currently login.
@@ -11,7 +11,7 @@ import { URLSearchParams } from 'url';
import { useBookings } from '../useBookings'; import { useBookings } from '../useBookings';
// Services // Services
import { getAllBookings } from '@service/bookingServices'; import { getAllBookings } from '@src/services/bookingServices';
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
useSearchParams: jest.fn(), useSearchParams: jest.fn(),
@@ -22,7 +22,7 @@ jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(), useQuery: jest.fn(),
})); }));
jest.mock('@service/bookingServices', () => ({ jest.mock('@src/services/bookingServices', () => ({
getAllBookings: jest.fn(), getAllBookings: jest.fn(),
})); }));
@@ -67,7 +67,7 @@ describe('useBookings hook', () => {
}); });
test('Should prefetch the second page when in the first page', async () => { test('Should prefetch the second page when in the first page', async () => {
const sampleDate = { data: [{ id: 1, name: 'Booking 1' }], count: 7 }; const sampleDate = { data: [{ id: 1, name: 'Booking 1' }], count: 11 };
(queryClient.prefetchQuery as jest.Mock) = jest.fn(); (queryClient.prefetchQuery as jest.Mock) = jest.fn();
(useSearchParams as jest.Mock).mockReturnValue([ (useSearchParams as jest.Mock).mockReturnValue([
@@ -105,7 +105,7 @@ describe('useBookings hook', () => {
}); });
test('Should prefetch the previous page and next page when in the second page', async () => { test('Should prefetch the previous page and next page when in the second page', async () => {
const sampleDate = { data: [{ id: 1, name: 'Booking 1' }], count: 12 }; const sampleDate = { data: [{ id: 1, name: 'Booking 1' }], count: 22 };
(useSearchParams as jest.Mock).mockReturnValue([ (useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams('page=2'), new URLSearchParams('page=2'),
@@ -8,13 +8,13 @@ import useCheckOut from '../useCheckout';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
// Services // Services
import { checkOutBooking } from '@service/bookingServices'; import { checkOutBooking } from '@src/services/bookingServices';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '@src/hooks/useUserRoomAvailable';
// Constants // Constants
import { CHECKOUT_SUCCESS } from '@constant/messages'; import { CHECKOUT_SUCCESS } from '@src/constants/messages';
// Mock services and constants // Mock services and constants
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -22,10 +22,10 @@ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/bookingServices', () => ({ jest.mock('@src/services/bookingServices', () => ({
checkOutBooking: jest.fn(), checkOutBooking: jest.fn(),
})); }));
jest.mock('@hook/useUserRoomAvailable', () => ({ jest.mock('@src/hooks/useUserRoomAvailable', () => ({
useUserRoomAvailable: jest.fn(), useUserRoomAvailable: jest.fn(),
})); }));
@@ -11,20 +11,20 @@ import toast from 'react-hot-toast';
import { useCreateBooking } from '../useCreateBooking'; import { useCreateBooking } from '../useCreateBooking';
// Types // Types
import { IBooking } from '@type/booking'; import { IBooking } from '@src/types/booking';
// Constants // Constants
import { ADD_SUCCESS } from '@constant/messages'; import { ADD_SUCCESS } from '@src/constants/messages';
// Services // Services
import { createBooking } from '@service/bookingServices'; import { createBooking } from '@src/services/bookingServices';
// Mock the necessary dependencies // Mock the necessary dependencies
jest.mock('@tanstack/react-query', () => ({ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/bookingServices', () => ({ jest.mock('@src/services/bookingServices', () => ({
createBooking: jest.fn(), createBooking: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -8,13 +8,13 @@ import {
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Types // Types
import { IBooking } from '@type/booking'; import { IBooking } from '@src/types/booking';
// Constants // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@src/constants/messages';
// Services // Services
import { updateBooking } from '@service/bookingServices'; import { updateBooking } from '@src/services/bookingServices';
// Hooks // Hooks
import { useUpdateBooking } from '../useUpdateBooking'; import { useUpdateBooking } from '../useUpdateBooking';
@@ -24,7 +24,7 @@ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/bookingServices', () => ({ jest.mock('@src/services/bookingServices', () => ({
updateBooking: jest.fn(), updateBooking: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -3,10 +3,10 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// Services // Services
import { getAllBookings } from '@service/bookingServices'; import { getAllBookings } from '@src/services/bookingServices';
// Constants // Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config'; import { DEFAULT_PAGE_SIZE } from '@src/constants/config';
/** /**
* Fetch booking from database * Fetch booking from database
@@ -2,13 +2,13 @@ import toast from 'react-hot-toast';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
// Constants // Constants
import { CHECKOUT_SUCCESS } from '@constant/messages'; import { CHECKOUT_SUCCESS } from '@src/constants/messages';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '../useUserRoomAvailable';
// Services // Services
import { checkOutBooking as checkOutBookingFn } from '@service/bookingServices'; import { checkOutBooking as checkOutBookingFn } from '@src/services/bookingServices';
const useCheckOut = () => { const useCheckOut = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -2,10 +2,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { createBooking as createBookingFn } from '@service/bookingServices'; import { createBooking as createBookingFn} from '@src/services/bookingServices';
// Constants // Constants
import { ADD_SUCCESS } from '@constant/messages'; import { ADD_SUCCESS } from '@src/constants/messages';
/** /**
* Create booking on database * Create booking on database
@@ -2,10 +2,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { updateBooking as updateBookingFn } from '@service/bookingServices'; import { updateBooking as updateBookingFn } from '@src/services/bookingServices';
// Constants // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@src/constants/messages';
/** /**
* Update booking from database * Update booking from database
@@ -8,22 +8,22 @@ import {
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Types // Types
import { IRoom } from '@type/room'; import { IRoom } from '@src/types/room';
// Hooks // Hooks
import { useCreateRoom } from '../useCreateRoom'; import { useCreateRoom } from '../useCreateRoom';
// Constants // Constants
import { ADD_SUCCESS } from '@constant/messages'; import { ADD_SUCCESS } from '@src/constants/messages';
// Services // Services
import { createRoom } from '@service/roomServices'; import { createRoom } from '@src/services/roomServices';
jest.mock('@tanstack/react-query', () => ({ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/roomServices', () => ({ jest.mock('@src/services/roomServices', () => ({
createRoom: jest.fn(), createRoom: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -8,7 +8,7 @@ import { renderHook } from '@testing-library/react';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
// Services // Services
import { getAllRooms } from '@service/roomServices'; import { getAllRooms } from '@src/services/roomServices';
// Hooks // Hooks
import { useRooms } from '../useRooms'; import { useRooms } from '../useRooms';
@@ -22,7 +22,7 @@ jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(), useQuery: jest.fn(),
})); }));
jest.mock('@service/roomServices', () => ({ jest.mock('@src/services/roomServices', () => ({
getAllRooms: jest.fn(), getAllRooms: jest.fn(),
})); }));
@@ -69,7 +69,7 @@ describe('useRooms hook', () => {
}); });
test('Should prefetch the second page when in the first page', async () => { test('Should prefetch the second page when in the first page', async () => {
const sampleDate = { data: [{ id: 1, name: 'Room 1' }], count: 7 }; const sampleDate = { data: [{ id: 1, name: 'Room 1' }], count: 11 };
(useSearchParams as jest.Mock).mockReturnValue([ (useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams('page=1'), new URLSearchParams('page=1'),
@@ -106,7 +106,7 @@ describe('useRooms hook', () => {
}); });
test('Should prefetch the previous page and next page when in the second page', async () => { test('Should prefetch the previous page and next page when in the second page', async () => {
const sampleDate = { data: [{ id: 1, name: 'Room 1' }], count: 12 }; const sampleDate = { data: [{ id: 1, name: 'Room 1' }], count: 22 };
(useSearchParams as jest.Mock).mockReturnValue([ (useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams('page=2'), new URLSearchParams('page=2'),
@@ -8,13 +8,13 @@ import {
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Types // Types
import { IRoom } from '@type/room'; import { IRoom } from '@src/types/room';
// Constants // Constants
import { DELETE_SUCCESS } from '@constant/messages'; import { DELETE_SUCCESS } from '@src/constants/messages';
// Services // Services
import { setIsDeleteRoom } from '@service/roomServices'; import { setIsDeleteRoom } from '@src/services/roomServices';
// Hooks // Hooks
import { useSetIsDeleteRoom } from '../useSetIsDeleteRoom'; import { useSetIsDeleteRoom } from '../useSetIsDeleteRoom';
@@ -24,7 +24,7 @@ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/roomServices', () => ({ jest.mock('@src/services/roomServices', () => ({
setIsDeleteRoom: jest.fn(), setIsDeleteRoom: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -8,13 +8,13 @@ import {
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Types // Types
import { IRoom } from '@type/room'; import { IRoom } from '@src/types/room';
// Constants // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@src/constants/messages';
// Services // Services
import { updateRoom } from '@service/roomServices'; import { updateRoom } from '@src/services/roomServices';
// Hooks // Hooks
import { useUpdateRoom } from '../useUpdateRoom'; import { useUpdateRoom } from '../useUpdateRoom';
@@ -24,7 +24,7 @@ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/roomServices', () => ({ jest.mock('@src/services/roomServices', () => ({
updateRoom: jest.fn(), updateRoom: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -2,13 +2,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { createRoom as createRoomFn } from '@service/roomServices'; import { createRoom as createRoomFn} from '@src/services/roomServices';
// Constants // Constants
import { ADD_SUCCESS } from '@constant/messages'; import { ADD_SUCCESS } from '@src/constants/messages';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '../useUserRoomAvailable';
/** /**
* Create room on database * Create room on database
+2 -2
View File
@@ -3,10 +3,10 @@ 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 '@src/services/roomServices';
// Constants // Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config'; import { DEFAULT_PAGE_SIZE } from '@src/constants/config';
/** /**
* Fetch room from database * Fetch room from database
@@ -2,13 +2,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { setIsDeleteRoom as setIsDeleteRoomFn } from '@service/roomServices'; import { setIsDeleteRoom as setIsDeleteRoomFn } from '@src/services/roomServices';
// Constants // Constants
import * as messages from '@constant/messages'; import { DELETE_SUCCESS } from '@src/constants/messages';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '../useUserRoomAvailable';
/** /**
* Delete the room from database * Delete the room from database
@@ -25,7 +25,7 @@ const useSetIsDeleteRoom = () => {
} = useMutation({ } = useMutation({
mutationFn: setIsDeleteRoomFn, mutationFn: setIsDeleteRoomFn,
onSuccess: (room) => { onSuccess: (room) => {
toast.success(messages.DELETE_SUCCESS); toast.success(DELETE_SUCCESS);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['rooms'], queryKey: ['rooms'],
}); });
@@ -2,13 +2,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { updateRoom as updateRoomFn } from '@service/roomServices'; import { updateRoom as updateRoomFn } from '@src/services/roomServices';
// Constants // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@src/constants/messages';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '../useUserRoomAvailable';
/** /**
* Update room from database * Update room from database
@@ -0,0 +1,22 @@
import { useContext } from 'react';
// Contexts
import { ItemSelectContext } from '@src/contexts/ItemSelected';
/**
* The hooks check the place call is in UserRoomAvailableProvider or not
* @returns The context of UserRoomAvailableContext
*/
const useItemSelect = () => {
const context = useContext(ItemSelectContext);
if (context === undefined) {
throw new Error(
'ItemSelectContext was used outside of ItemSelectContextProvider'
);
}
return context;
};
export { useItemSelect };
@@ -1,7 +1,7 @@
import { MutableRefObject, useEffect, useRef } from 'react'; import { MutableRefObject, useEffect, useRef } from 'react';
// Types // Types
import { Nullable } from '@type/common'; import { Nullable } from '@src/types/common';
/** /**
* Custom hook to call handler when click outside element * Custom hook to call handler when click outside element
@@ -1,7 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
// Contexts // Contexts
import { UserRoomAvailableContext } from '@context/UserRoomAvailableContext'; import { UserRoomAvailableContext } from '@src/contexts/UserRoomAvailableContext';
/** /**
* The hooks check the place call is in UserRoomAvailableProvider or not * The hooks check the place call is in UserRoomAvailableProvider or not
@@ -11,20 +11,20 @@ import { ReactNode } from 'react';
import { useCreateUser } from '../useCreateUser'; import { useCreateUser } from '../useCreateUser';
// Types // Types
import { IUser } from '@type/user'; import { IUser } from '@src/types/user';
// Constants // Constants
import { ADD_SUCCESS } from '@constant/messages'; import { ADD_SUCCESS } from '@src/constants/messages';
// Services // Services
import { createUser } from '@service/userServices'; import { createUser } from '@src/services/userServices';
// Mock the necessary dependencies // Mock the necessary dependencies
jest.mock('@tanstack/react-query', () => ({ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/userServices', () => ({ jest.mock('@src/services/userServices', () => ({
createUser: jest.fn(), createUser: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -8,13 +8,13 @@ import {
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Types // Types
import { IUser } from '@type/user'; import { IUser } from '@src/types/user';
// Constants // Constants
import { DELETE_SUCCESS } from '@constant/messages'; import { DELETE_SUCCESS } from '@src/constants/messages';
// Services // Services
import { setIsDeleteUser } from '@service/userServices'; import { setIsDeleteUser } from '@src/services/userServices';
// Hooks // Hooks
import { useIsDeleteUser } from '../useSetIsDeleteUser'; import { useIsDeleteUser } from '../useSetIsDeleteUser';
@@ -24,7 +24,7 @@ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/userServices', () => ({ jest.mock('@src/services/userServices', () => ({
setIsDeleteUser: jest.fn(), setIsDeleteUser: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -8,13 +8,13 @@ import {
import { ReactNode } from 'react'; import { ReactNode } from 'react';
// Types // Types
import { IUser } from '@type/user'; import { IUser } from '@src/types/user';
// Constants // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@src/constants/messages';
// Services // Services
import { updateUser } from '@service/userServices'; import { updateUser } from '@src/services/userServices';
// Hooks // Hooks
import { useUpdateUser } from '../useUpdateUser'; import { useUpdateUser } from '../useUpdateUser';
@@ -24,7 +24,7 @@ jest.mock('@tanstack/react-query', () => ({
...jest.requireActual('@tanstack/react-query'), ...jest.requireActual('@tanstack/react-query'),
useQueryClient: jest.fn(), useQueryClient: jest.fn(),
})); }));
jest.mock('@service/userServices', () => ({ jest.mock('@src/services/userServices', () => ({
updateUser: jest.fn(), updateUser: jest.fn(),
})); }));
jest.mock('react-hot-toast'); jest.mock('react-hot-toast');
@@ -8,7 +8,7 @@ import { renderHook } from '@testing-library/react';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
// Services // Services
import { getAllUsers } from '@service/userServices'; import { getAllUsers } from '@src/services/userServices';
// Hooks // Hooks
import { useUsers } from '../useUsers'; import { useUsers } from '../useUsers';
@@ -22,7 +22,7 @@ jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(), useQuery: jest.fn(),
})); }));
jest.mock('@service/userServices', () => ({ jest.mock('@src/services/userServices', () => ({
getAllUsers: jest.fn(), getAllUsers: jest.fn(),
})); }));
@@ -69,7 +69,7 @@ describe('useUsers hook', () => {
}); });
test('Should prefetch the second page when in the first page', async () => { test('Should prefetch the second page when in the first page', async () => {
const sampleDate = { data: [{ id: 1, name: 'User 1' }], count: 7 }; const sampleDate = { data: [{ id: 1, name: 'User 1' }], count: 11 };
(useSearchParams as jest.Mock).mockReturnValue([ (useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams('page=1'), new URLSearchParams('page=1'),
@@ -106,7 +106,7 @@ describe('useUsers hook', () => {
}); });
test('Should prefetch the previous page and next page when in the second page', async () => { test('Should prefetch the previous page and next page when in the second page', async () => {
const sampleDate = { data: [{ id: 1, name: 'User 1' }], count: 12 }; const sampleDate = { data: [{ id: 1, name: 'User 1' }], count: 22 };
(useSearchParams as jest.Mock).mockReturnValue([ (useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams('page=2'), new URLSearchParams('page=2'),
@@ -2,13 +2,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { createUser as createUserFn } from '@service/userServices'; import { createUser as createUserFn } from '@src/services/userServices';
// Constants // Constants
import { ADD_SUCCESS } from '@constant/messages'; import { ADD_SUCCESS } from '@src/constants/messages';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '../useUserRoomAvailable';
/** /**
* Create user in database * Create user in database
@@ -2,11 +2,13 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Services // Services
import { setIsDeleteUser as setIsDeleteUserFn } from '@service/userServices'; import { setIsDeleteUser as setIsDeleteUserFn } from '@src/services/userServices';
// Constants // Constants
import * as messages from '@constant/messages'; import { DELETE_SUCCESS } from '@src/constants/messages';
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable';
// Hooks
import { useUserRoomAvailable } from '../useUserRoomAvailable';
/** /**
* Delete the user from database * Delete the user from database
@@ -23,7 +25,7 @@ const useIsDeleteUser = () => {
} = useMutation({ } = useMutation({
mutationFn: setIsDeleteUserFn, mutationFn: setIsDeleteUserFn,
onSuccess: (user) => { onSuccess: (user) => {
toast.success(messages.DELETE_SUCCESS); toast.success(DELETE_SUCCESS);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['users'], queryKey: ['users'],
}); });
@@ -2,13 +2,13 @@ import toast from 'react-hot-toast';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
// Services // Services
import { updateUser as updateUserFn } from '@service/userServices'; import { updateUser as updateUserFn } from '@src/services/userServices';
// Constants // Constants
import { UPDATE_SUCCESS } from '@constant/messages'; import { UPDATE_SUCCESS } from '@src/constants/messages';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '../useUserRoomAvailable';
/** /**
* Update user from database * Update user from database
+2 -2
View File
@@ -3,10 +3,10 @@ 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 '@src/services/userServices';
// Constants // Constants
import { DEFAULT_PAGE_SIZE } from '@constant/config'; import { DEFAULT_PAGE_SIZE } from '@src/constants/config';
/** /**
* Fetch data of users from database * Fetch data of users from database
@@ -1,17 +1,17 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
// Styled // Styled
import Input from '@commonStyle/Input'; import Input from '@src/commons/styles/Input';
// Components // Components
import Form from '@component/Form'; import Form from '@src/components/Form';
// Constants // Constants
import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage'; import { REQUIRED_FIELD_ERROR } from '@src/constants/formValidateMessage';
// Hooks // Hooks
import { useUpdateAccount } from '@hook/authentication/useUpdateAccount'; import { useUpdateAccount } from '@src/hooks/authentication/useUpdateAccount';
import { useAccount } from '@hook/authentication/useAccount'; import { useAccount } from '@src/hooks/authentication/useAccount';
interface IAccountForm { interface IAccountForm {
fullName: string; fullName: string;
@@ -3,24 +3,24 @@ import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
// Styled // Styled
import Input from '@commonStyle/Input'; import Input from '@src/commons/styles/Input';
// Components // Components
import Form from '@component/Form'; import Form from '@src/components/Form';
// Constants // Constants
import { REGEX } from '@constant/commons'; import { REGEX } from '@src/constants/commons';
import { import {
PASSWORD_CONDITION, PASSWORD_CONDITION,
PASSWORD_NOT_MATCH, PASSWORD_NOT_MATCH,
PASSWORD_NOT_MATCH_REQUIREMENT, PASSWORD_NOT_MATCH_REQUIREMENT,
} from '@constant/formValidateMessage'; } from '@src/constants/formValidateMessage';
// Helpers // Helpers
import { isValidRegex } from '@helper/validators'; import { isValidRegex } from '@src/helpers/validators';
// Hooks // Hooks
import { useUpdateAccount } from '@hook/authentication/useUpdateAccount'; import { useUpdateAccount } from '@src/hooks/authentication/useUpdateAccount';
interface IAccountPasswordForm { interface IAccountPasswordForm {
password: string; password: string;
@@ -2,14 +2,14 @@ import { act, fireEvent, render, RenderResult, waitFor } from '@testing-library/
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
// Components // Components
import AccountInforForm from '@page/Account/AccountInforForm'; import AccountInforForm from '@src/pages/Account/AccountInforForm';
// Hooks // Hooks
import { useAccount } from '@hook/authentication/useAccount'; import { useAccount } from '@src/hooks/authentication/useAccount';
import { useUpdateAccount } from '@hook/authentication/useUpdateAccount'; import { useUpdateAccount } from '@src/hooks/authentication/useUpdateAccount';
jest.mock('@hook/authentication/useUpdateAccount'); jest.mock('@src/hooks/authentication/useUpdateAccount');
jest.mock('@hook/authentication/useAccount'); jest.mock('@src/hooks/authentication/useAccount');
const mockAccount = { const mockAccount = {
email: 'admin@nezumi.site', email: 'admin@nezumi.site',
@@ -10,9 +10,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AccountPasswordForm from '../AccountPasswordForm'; import AccountPasswordForm from '../AccountPasswordForm';
// Hooks // Hooks
import { useUpdateAccount } from '@hook/authentication/useUpdateAccount'; import { useUpdateAccount } from '@src/hooks/authentication/useUpdateAccount';
jest.mock('@hook/authentication/useUpdateAccount'); jest.mock('@src/hooks/authentication/useUpdateAccount');
describe('AccountPasswordForm', () => { describe('AccountPasswordForm', () => {
let wrapper: RenderResult | null = null; let wrapper: RenderResult | null = null;
@@ -2,29 +2,29 @@ import { useCallback, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
// Styled // Styled
import Input from '@commonStyle/Input.ts'; import Input from '@src/commons/styles/Input';
// Constants // Constants
import { REQUIRED_FIELD_ERROR } from '@constant/formValidateMessage.ts'; import { REQUIRED_FIELD_ERROR } from '@src/constants/formValidateMessage';
// Components // Components
import Form from '@component/Form/index.tsx'; import Form from '@src/components/Form';
import Select, { ISelectOptions } from '@component/Select/index.tsx'; import Select, { ISelectOptions } from '@src/components/Select/index.tsx';
// Hooks // Hooks
import { useCreateBooking } from '@hook/bookings/useCreateBooking.ts'; import { useCreateBooking } from '@src/hooks/bookings/useCreateBooking';
import { useUpdateBooking } from '@hook/bookings/useUpdateBooking.ts'; import { useUpdateBooking } from '@src/hooks/bookings/useUpdateBooking';
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable.ts'; import { useUserRoomAvailable } from '@src/hooks/useUserRoomAvailable';
// Helpers // Helpers
import { formatCurrency, getDayDiff } from '@helper/helper.ts'; import { formatCurrency, getDayDiff } from '@src/helpers/helper';
// Services // Services
import { getRoomById, updateRoomStatus } from '@service/roomServices.ts'; import { getRoomById, updateRoomStatus } from '@src/services/roomServices';
import { updateUserBookedStatus } from '@service/userServices.ts'; import { updateUserBookedStatus } from '@src/services/userServices';
// Types // Types
import { IBooking, TBookingResponse } from '@type/booking.ts'; import { IBooking, TBookingResponse } from '@src/types/booking';
interface IBookingFormProp { interface IBookingFormProp {
onCloseModal?: () => void; onCloseModal?: () => void;
@@ -37,6 +37,7 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => {
usersAvailable, usersAvailable,
dispatch dispatch
} = useUserRoomAvailable(); } = useUserRoomAvailable();
const { isCreating, createBooking } = useCreateBooking(); const { isCreating, createBooking } = useCreateBooking();
const { isUpdating, updateBooking } = useUpdateBooking(); const { isUpdating, updateBooking } = useUpdateBooking();
const isLoading = isCreating || isUpdating; const isLoading = isCreating || isUpdating;
@@ -83,7 +84,7 @@ const BookingForm = ({ onCloseModal, booking }: IBookingFormProp) => {
type: 'updateStatusRoom', type: 'updateStatusRoom',
payload: [{ id: newBooking.roomId, status: true }], payload: [{ id: newBooking.roomId, status: true }],
}); });
// Update room, user available in server // Update room, user available in server
await updateRoomStatus(newBooking.roomId, true); await updateRoomStatus(newBooking.roomId, true);
await updateUserBookedStatus(newBooking.userId, true); await updateUserBookedStatus(newBooking.userId, true);
@@ -1,129 +0,0 @@
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 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>{
status
? 'Check in'
: 'Check out'
}</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;
@@ -1,73 +0,0 @@
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;
@@ -8,25 +8,25 @@ import {
} from '@testing-library/react'; } from '@testing-library/react';
// Types // Types
import { TBookingResponse } from '@type/booking'; import { TBookingResponse } from '@src/types/booking';
// Components // Components
import BookingForm from '../BookingForm'; import BookingForm from '../BookingForm';
// Hooks // Hooks
import { useUserRoomAvailable } from '@hook/useUserRoomAvailable'; import { useUserRoomAvailable } from '@src/hooks/useUserRoomAvailable';
const mockUpdateBooking = jest.fn(); const mockUpdateBooking = jest.fn();
jest.mock('@hook/bookings/useUpdateBooking', () => ({ jest.mock('@src/hooks/bookings/useUpdateBooking', () => ({
...jest.requireActual('@hook/bookings/useUpdateBooking'), ...jest.requireActual('@src/hooks/bookings/useUpdateBooking'),
useUpdateBooking: jest.fn(() => ({ useUpdateBooking: jest.fn(() => ({
isUpdating: false, isUpdating: false,
updateBooking: mockUpdateBooking, updateBooking: mockUpdateBooking,
})), })),
})); }));
jest.mock('@hook/useUserRoomAvailable.ts'); jest.mock('@src/hooks/useUserRoomAvailable.ts');
const mockUseUserRoomAvailable = useUserRoomAvailable as jest.MockedFunction< const mockUseUserRoomAvailable = useUserRoomAvailable as jest.MockedFunction<
typeof useUserRoomAvailable typeof useUserRoomAvailable
@@ -1,37 +0,0 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
// Types
import { TBookingResponse } from '@type/booking';
// Components
import BookingRow from '../BookingRow';
describe('BookingRow', () => {
const tempBooking: TBookingResponse = {
id: 1,
startDate: '2024-11-21',
endDate: '2024-11-22',
amount: 1200,
status: true,
rooms: {
id: 1,
name: 'Room 1',
},
users: {
id: 2,
name: 'Room 2',
},
};
const queryClient = new QueryClient();
const wrapper = render(
<QueryClientProvider client={queryClient}>
<BookingRow booking={tempBooking} />
</QueryClientProvider>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
@@ -1,22 +0,0 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { render } from '@testing-library/react';
// Components
import BookingTable from '@page/Booking/BookingTable';
describe('UserTable', () => {
const queryClient = new QueryClient();
const wrapper = render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<BookingTable />
</BrowserRouter>
</QueryClientProvider>
);
test('Should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});
});
@@ -79,7 +79,7 @@ exports[`BookingForm Should render correctly 1`] = `
<input <input
class="sc-bdnyFh gXSWSo" class="sc-bdnyFh gXSWSo"
id="startDate" id="startDate"
min="2023-12-04" min="2023-12-06"
name="startDate" name="startDate"
type="date" type="date"
/> />
@@ -223,7 +223,7 @@ exports[`BookingForm Should render correctly 1`] = `
<input <input
class="sc-bdnyFh gXSWSo" class="sc-bdnyFh gXSWSo"
id="startDate" id="startDate"
min="2023-12-04" min="2023-12-06"
name="startDate" name="startDate"
type="date" type="date"
/> />
+192 -19
View File
@@ -1,36 +1,209 @@
// Styled
import Direction from '@commonStyle/Direction';
import { StyledBooking, Title } from './styled';
import Button from '@commonStyle/Button';
// Components // Components
import Modal from '@component/Modal';
import BookingTable from './BookingTable';
import BookingForm from './BookingForm'; import BookingForm from './BookingForm';
import Modal from '@src/components/Modal';
import ButtonIcon from '@src/components/ButtonIcon';
import Direction from '@src/commons/styles/Direction';
import {FiEdit} from 'react-icons/fi';
import {MdOutlineAddCircleOutline} from 'react-icons/md';
import Table from '@src/components/Table';
// Constants // Styled
import { FORM } from '@constant/commons'; import {ActionTable, StyledBooking, Title} from './styled';
import Spinner from '@src/commons/styles/Spinner.ts';
// Types
import {ColumnProps} from '@src/types/common.ts';
import {TBookingResponse} from '@src/types/booking.ts';
// Hooks
import {useBookings} from '@src/hooks/bookings/useBookings.ts';
import {useEffect, useState} from 'react';
import {FORM} from '@src/constants/commons.ts';
import {findItemInListById, formatCurrency} from '@src/helpers/helper.ts';
import ConfirmMessage from '@src/components/ConfirmMessage';
import useCheckOut from '@src/hooks/bookings/useCheckout.ts';
import toast from 'react-hot-toast';
import {ImExit} from "react-icons/im";
import Message from "@src/components/Message";
interface IBookingTable extends Omit<TBookingResponse, 'status' | 'amount'> {
amount: string;
status: string;
}
const Booking = () => { const Booking = () => {
const {checkOutBooking} = useCheckOut();
const [itemSelected, setItemSelected] = useState<IBookingTable>();
let bookingSelected: TBookingResponse | undefined;
const columns: ColumnProps[] = [
{
key: 'user',
title: 'User',
width: 10
},
{
key: 'date',
title: 'Date',
width: 25,
isDateValue: true
},
{
key: 'room',
title: 'Room',
width: 20
},
{
key: 'amount',
title: 'Amount',
width: 15
},
{
key: 'status',
title: 'Status',
width: 20
}
];
const {isLoading, bookings, count} = useBookings();
// Reset value when bookings changed.
useEffect(() => {
setItemSelected(undefined);
}, [bookings]);
const tempBookings = bookings?.map((booking) => (
{
...booking,
date: [booking.startDate, booking.endDate],
amount: formatCurrency(booking.amount),
room: booking.rooms!.name,
user: booking.users!.name,
status: booking.status
? 'Check in'
: 'Check out',
}
));
if (itemSelected && bookings) {
bookingSelected = findItemInListById<TBookingResponse>(itemSelected.id, bookings);
}
const handleClickCheckOutBtn = () => {
if (itemSelected && itemSelected.status) {
checkOutBooking({
idBooking: itemSelected.id,
roomId: itemSelected!.rooms!.id,
userId: itemSelected!.users!.id
});
} else {
toast.error('User already checkout!');
}
};
return ( return (
<StyledBooking> <StyledBooking>
<Direction type="horizontal"> <Direction type="horizontal">
<Title>List Booking</Title> <Title>List Booking</Title>
<Modal> <Modal>
<Modal.Open <ActionTable>
modalName={FORM.BOOKING} <Modal.Open
renderChildren={(onCloseModal) => ( modalName={FORM.ROOM}
<Button onClick={onCloseModal}>Add booking</Button> renderChildren={(onCloseModal) => (
)} <ButtonIcon
/> icon={<MdOutlineAddCircleOutline/>}
<Modal.Window name={FORM.BOOKING} title="Add form"> text={'Add Booking'}
<BookingForm /> iconSize={'18px'}
</Modal.Window> fontSize={'var(--fs-sm)'}
variations={'primary'}
iconColor={'white'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.EDIT}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<FiEdit/>}
text={'Edit'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'success'}
iconColor={'white'}
disabled={!itemSelected || itemSelected?.status === 'Check out'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Open
modalName={FORM.CHECKOUT}
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<ImExit />}
text={'Check out'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'danger'}
iconColor={'white'}
disabled={!itemSelected || itemSelected?.status === 'Check out'}
onClick={onCloseModal}
/>
)}
/>
<Modal.Window
name={FORM.ROOM}
title="Add form"
renderChildren={(onCloseModal) => (
<BookingForm onCloseModal={onCloseModal}/>
)}
/>
<Modal.Window
name={FORM.EDIT}
title="Edit Booking"
renderChildren={(onCloseModal) =>
<BookingForm booking={bookingSelected} onCloseModal={onCloseModal}/>
}/>
<Modal.Window
name={FORM.CHECKOUT}
title="Check Out"
renderChildren={(onCloseModal) =>
<ConfirmMessage
message={`Are you sure to check out this user? "${bookingSelected!.users!.name}"?`}
onConfirm={handleClickCheckOutBtn}
onCloseModal={onCloseModal}
/>
}/>
</ActionTable>
</Modal> </Modal>
</Direction> </Direction>
<BookingTable /> {isLoading && <Spinner/>}
{
tempBookings &&
Boolean(tempBookings.length) &&
<Table<IBookingTable>
columns={columns}
rows={tempBookings}
count={count}
searchPlaceHolder={'Search by name...'}
stateSelected={{
itemSelected,
setItemSelected
}}
onRowClick={(data) => {
setItemSelected(data);
}}/>
}
{tempBookings && Boolean(!tempBookings.length) && <Message>No data to show here!</Message>}
</StyledBooking> </StyledBooking>
); );
}; };
+8 -2
View File
@@ -14,14 +14,20 @@ const Title = styled.h2`
text-transform: capitalize; text-transform: capitalize;
`; `;
const StyledOperationTable = styled.div` const StyledTableOption = styled.div`
display: flex; display: flex;
gap: 30px; gap: 30px;
justify-content: flex-end; justify-content: flex-end;
`; `;
const ActionTable = styled.div`
display: flex;
gap: 30px;
`
export { export {
StyledBooking, StyledBooking,
Title, Title,
StyledOperationTable StyledTableOption,
ActionTable
}; };
@@ -1,29 +1,29 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
// Components // Components
import Form from '@component/Form'; import Form from '@src/components/Form';
// Styled // Styled
import { FieldInput, StyledLoginForm } from './styled'; import { FieldInput, StyledLoginForm } from './styled';
import Button from '@commonStyle/Button'; import Button from '@src/commons/styles/Button';
// Constants // Constants
import { import {
INVALID_EMAIL, INVALID_EMAIL,
REQUIRED_FIELD_ERROR, REQUIRED_FIELD_ERROR,
} from '@constant/formValidateMessage'; } from '@src/constants/formValidateMessage';
// Types // Types
import { ILogin } from '@type/common'; import { ILogin } from '@src/types/common';
// Hooks // Hooks
import { useLogin } from '@hook/authentication/useLogin'; import { useLogin } from '@src/hooks/authentication/useLogin';
// Helpers // Helpers
import { isValidRegex } from '@helper/validators'; import { isValidRegex } from '@src/helpers/validators';
// Constants // Constants
import { REGEX } from '@constant/commons'; import { REGEX } from '@src/constants/commons';
const LoginForm = () => { const LoginForm = () => {
const { const {
@@ -1,10 +1,10 @@
import { useLogin } from "@hook/authentication/useLogin"; import { useLogin } from "@src/hooks/authentication/useLogin";
import LoginForm from "@page/Login/LoginForm"; import LoginForm from "@src/pages/Login/LoginForm";
import { fireEvent, render, waitFor } from "@testing-library/react"; import { fireEvent, render, waitFor } from "@testing-library/react";
// Mock the useLogin hook // Mock the useLogin hook
jest.mock('@hook/authentication/useLogin', () => ({ jest.mock('@src/hooks/authentication/useLogin', () => ({
useLogin: jest.fn(), useLogin: jest.fn(),
})); }));
+1 -1
View File
@@ -7,7 +7,7 @@ import { LoginLayout, StyledLogo, TitleStyled } from './styled';
import LoginForm from './LoginForm'; import LoginForm from './LoginForm';
// Hooks // Hooks
import { useAccount } from '@hook/authentication/useAccount'; import { useAccount } from '@src/hooks/authentication/useAccount';
const Login = () => { const Login = () => {
const { account } = useAccount(); const { account } = useAccount();
+1 -1
View File
@@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
// Styled // Styled
import CommonInput from '@commonStyle/CommonInput'; import CommonInput from '@src/commons/styles/CommonInput';
const LoginLayout = styled.main` const LoginLayout = styled.main`
height: 100vh; height: 100vh;
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import NotFound from ".."; import NotFound from "..";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -9,18 +9,18 @@ jest.mock('react-router-dom', () => ({
describe('NotFound', () => { describe('NotFound', () => {
test('Should render correctly', () => { test('Should render correctly', () => {
render(<NotFound />); const wrapper = render(<NotFound />);
expect(screen.getByText('The page not found!')).toBeInTheDocument(); expect(wrapper.getByText('The page not found!')).toBeInTheDocument();
expect(screen.getByText('Go back!')).toBeInTheDocument(); expect(wrapper.getByText('Go back!')).toBeInTheDocument();
}); });
test('Should call navigate correctly when click button', () => { test('Should call navigate correctly when click button', () => {
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
(useNavigate as jest.Mock).mockReturnValue(mockNavigate); (useNavigate as jest.Mock).mockReturnValue(mockNavigate);
render(<NotFound />); const wrapper = render(<NotFound />);
fireEvent.click(screen.getByText('Go back!')); fireEvent.click(wrapper.getByText('Go back!'));
expect(mockNavigate).toHaveBeenCalledTimes(1); expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(-1); expect(mockNavigate).toHaveBeenCalledWith(-1);
+7 -7
View File
@@ -1,27 +1,27 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
// Styled // Styled
import Input from '@commonStyle/Input.ts'; import Input from '@src/commons/styles/Input.ts';
// Types // Types
import { IRoom } from '@type/room.ts'; import { IRoom } from '@src/types/room.ts';
// Constants // Constants
import { import {
INVALID_FIELD, INVALID_FIELD,
REQUIRED_FIELD_ERROR, REQUIRED_FIELD_ERROR,
} from '@constant/formValidateMessage.ts'; } from '@src/constants/formValidateMessage.ts';
import { REGEX } from '../../constants/commons.ts'; import { REGEX } from '../../constants/commons.ts';
// Helpers // Helpers
import { isValidRegex, isValidString } from '@helper/validators.ts'; import { isValidRegex, isValidString } from '@src/helpers/validators.ts';
// Components // Components
import Form from '@component/Form/index.tsx'; import Form from '@src/components/Form/index.tsx';
// Hooks // Hooks
import { useCreateRoom } from '@hook/rooms/useCreateRoom.ts'; import { useCreateRoom } from '@src/hooks/rooms/useCreateRoom.ts';
import { useUpdateRoom } from '@hook/rooms/useUpdateRoom.ts'; import { useUpdateRoom } from '@src/hooks/rooms/useUpdateRoom.ts';
interface IRoomFormProp { interface IRoomFormProp {
onCloseModal?: () => void; onCloseModal?: () => void;

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