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

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

After

Width:  |  Height:  |  Size: 15 KiB

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

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