mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 13:38:51 +00:00
Merge branch 'react-practice-02' into feat/unit-test-for-function
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+20
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+102
-24
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 'removeUser':
|
||||
case 'updateStatusUser': {
|
||||
const tempArr = state.usersAvailable;
|
||||
const indexItemUpdate = tempArr.findIndex(
|
||||
(item) => item.id === action.payload[0].id
|
||||
);
|
||||
tempArr[indexItemUpdate].isBooked = action.payload[0].isBooked;
|
||||
|
||||
return {
|
||||
...state,
|
||||
usersAvailable: state.usersAvailable.filter(
|
||||
(item) => item.id !== action.payload[0].id
|
||||
),
|
||||
usersAvailable: tempArr,
|
||||
};
|
||||
}
|
||||
|
||||
case 'removeUser':
|
||||
return {
|
||||
...state,
|
||||
usersAvailable: state.usersAvailable.filter((user) => user.id !== action.payload[0].id)
|
||||
}
|
||||
|
||||
case 'addRoom': {
|
||||
const tempArr = state.roomsAvailable;
|
||||
@@ -125,14 +137,25 @@ const reducer = (state: IUserRoomState, action: IAction) => {
|
||||
};
|
||||
}
|
||||
|
||||
case 'removeRoom':
|
||||
case 'updateStatusRoom': {
|
||||
const tempArr = state.roomsAvailable;
|
||||
const indexItemUpdate = tempArr.findIndex(
|
||||
(item) => item.id === action.payload[0].id
|
||||
);
|
||||
tempArr[indexItemUpdate].status = action.payload[0].status;
|
||||
|
||||
return {
|
||||
...state,
|
||||
roomsAvailable: state.roomsAvailable.filter(
|
||||
(item) => item.id !== action.payload[0].id
|
||||
),
|
||||
roomsAvailable: tempArr,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
case 'removeRoom':
|
||||
return {
|
||||
...state,
|
||||
roomsAvailable: state.roomsAvailable.filter((room) => room.id !== action.payload[0].id)
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error('Action unknown');
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
|
||||
+12
-8
@@ -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
|
||||
|
||||
@@ -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} <TbArrowNarrowRight />
|
||||
{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 };
|
||||
@@ -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 };
|
||||
@@ -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,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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,17 +19,35 @@ 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>
|
||||
<div>{name}</div>
|
||||
<div>{phone}</div>
|
||||
<div>{
|
||||
isBooked
|
||||
? 'Yes'
|
||||
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
Reference in New Issue
Block a user