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