mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 20:01:59 +00:00
Update table component, remove unnecessary code
This commit is contained in:
@@ -2,7 +2,6 @@ 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';
|
||||||
@@ -21,87 +20,33 @@ import Account from '@src/pages/Account';
|
|||||||
// Constants
|
// Constants
|
||||||
import * as PATH from './constants/path';
|
import * as PATH from './constants/path';
|
||||||
|
|
||||||
// Hooks
|
|
||||||
import {
|
|
||||||
UserRoomAvailableContext,
|
|
||||||
initialState,
|
|
||||||
reducer,
|
|
||||||
} from '@src/contexts/UserRoomAvailableContext';
|
|
||||||
|
|
||||||
// Services
|
|
||||||
import { getAllUsers } from '@src/services/userServices';
|
|
||||||
import { getAllRooms } from '@src/services/roomServices';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer(
|
|
||||||
reducer,
|
|
||||||
initialState
|
|
||||||
);
|
|
||||||
|
|
||||||
// Init list user and room available
|
|
||||||
useEffect(() => {
|
|
||||||
const load = async () => {
|
|
||||||
const tempUser = await getAllUsers({
|
|
||||||
sortBy: 'id',
|
|
||||||
orderBy: 'asc',
|
|
||||||
phoneSearch: '',
|
|
||||||
page: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tempUser) {
|
|
||||||
dispatch({ type: 'initUser', payload: tempUser.data });
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempRoom = await getAllRooms({
|
|
||||||
sortBy: 'id',
|
|
||||||
orderBy: 'asc',
|
|
||||||
roomSearch: '',
|
|
||||||
page: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tempRoom) {
|
|
||||||
dispatch({ type: 'initRoom', payload: tempRoom.data });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const store = useMemo(() => {
|
|
||||||
return { roomsAvailable, usersAvailable, dispatch };
|
|
||||||
}, [roomsAvailable, usersAvailable]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
|
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
|
||||||
<UserRoomAvailableContext.Provider value={store}>
|
<BrowserRouter>
|
||||||
<BrowserRouter>
|
<Routes>
|
||||||
<Routes>
|
<Route element={<RootLayout />}>
|
||||||
<Route element={<RootLayout />}>
|
<Route
|
||||||
<Route
|
element={
|
||||||
element={
|
<RouteProtected>
|
||||||
<RouteProtected>
|
<AppLayout />
|
||||||
<AppLayout />
|
</RouteProtected>
|
||||||
</RouteProtected>
|
}
|
||||||
}
|
>
|
||||||
>
|
<Route index element={<Navigate replace to={PATH.BOOKING} />} />
|
||||||
<Route
|
<Route path={PATH.BOOKING} element={<Booking />} />
|
||||||
index
|
<Route path={PATH.USER} element={<User />} />
|
||||||
element={<Navigate replace to={PATH.BOOKING} />}
|
<Route path={PATH.ROOM} element={<Room />} />
|
||||||
/>
|
<Route path={PATH.ACCOUNT} element={<Account />} />
|
||||||
<Route path={PATH.BOOKING} element={<Booking />} />
|
|
||||||
<Route path={PATH.USER} element={<User />} />
|
|
||||||
<Route path={PATH.ROOM} element={<Room />} />
|
|
||||||
<Route path={PATH.ACCOUNT} element={<Account />} />
|
|
||||||
</Route>
|
|
||||||
<Route path={PATH.LOGIN} element={<Login />} />
|
|
||||||
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
<Route path={PATH.LOGIN} element={<Login />} />
|
||||||
</BrowserRouter>
|
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
|
||||||
</UserRoomAvailableContext.Provider>
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
</StyleSheetManager>
|
</StyleSheetManager>
|
||||||
|
|
||||||
<Toast />
|
<Toast />
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const Spinner = styled.div`
|
|||||||
top/10px 10px no-repeat,
|
top/10px 10px no-repeat,
|
||||||
conic-gradient(#0000 30%, var(--primary-color));
|
conic-gradient(#0000 30%, var(--primary-color));
|
||||||
-webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 10px), #000 0);
|
-webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 10px), #000 0);
|
||||||
mask: none;
|
|
||||||
animation: ${rotate} 1.5s infinite linear;
|
animation: ${rotate} 1.5s infinite linear;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,62 @@ import { Main, StyledAppLayout } from './styled';
|
|||||||
// Hooks
|
// Hooks
|
||||||
import { useAccount } from '@src/hooks/authentication/useAccount';
|
import { useAccount } from '@src/hooks/authentication/useAccount';
|
||||||
|
|
||||||
|
// Contexts
|
||||||
|
import { initialState, reducer, UserRoomAvailableContext } from '@src/contexts/UserRoomAvailableContext.ts';
|
||||||
|
import { useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { getAllUsers } from '@src/services/userServices.ts';
|
||||||
|
import { getAllRooms } from '@src/services/roomServices.ts';
|
||||||
|
|
||||||
const AppLayout = () => {
|
const AppLayout = () => {
|
||||||
const { account } = useAccount();
|
const { account } = useAccount();
|
||||||
|
|
||||||
|
const [{ usersAvailable, roomsAvailable }, dispatch] = useReducer(
|
||||||
|
reducer,
|
||||||
|
initialState
|
||||||
|
);
|
||||||
|
|
||||||
|
// Init list user and room available
|
||||||
|
// Init list user and room available
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
const tempUser = await getAllUsers({
|
||||||
|
sortBy: 'id',
|
||||||
|
orderBy: 'asc',
|
||||||
|
phoneSearch: '',
|
||||||
|
page: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tempUser) {
|
||||||
|
dispatch({ type: 'initUser', payload: tempUser.data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRoom = await getAllRooms({
|
||||||
|
sortBy: 'id',
|
||||||
|
orderBy: 'asc',
|
||||||
|
roomSearch: '',
|
||||||
|
page: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tempRoom) {
|
||||||
|
dispatch({ type: 'initRoom', payload: tempRoom.data });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAppLayout>
|
<UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable}}>
|
||||||
<Header accountName={account?.user_metadata.fullName} />
|
<StyledAppLayout>
|
||||||
<Sidebar heading="Hotel Management" />
|
<Header accountName={account?.user_metadata.fullName} />
|
||||||
<Main>
|
<Sidebar heading="Hotel Management" />
|
||||||
<Outlet />
|
<Main>
|
||||||
</Main>
|
<Outlet />
|
||||||
</StyledAppLayout>
|
</Main>
|
||||||
|
</StyledAppLayout>
|
||||||
|
</UserRoomAvailableContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ interface IButtonIcon extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
iconSize?: string;
|
iconSize?: string;
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
|
fontSize?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
variations?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonIcon = ({
|
const ButtonIcon = ({
|
||||||
@@ -15,12 +17,16 @@ const ButtonIcon = ({
|
|||||||
iconSize,
|
iconSize,
|
||||||
iconColor,
|
iconColor,
|
||||||
text,
|
text,
|
||||||
|
fontSize,
|
||||||
|
variations,
|
||||||
...props
|
...props
|
||||||
}: IButtonIcon) => {
|
}: IButtonIcon) => {
|
||||||
return (
|
return (
|
||||||
<StyledButtonIcon
|
<StyledButtonIcon
|
||||||
iconColor={iconColor}
|
iconColor={iconColor}
|
||||||
iconSize={iconSize}
|
iconSize={iconSize}
|
||||||
|
fontSize={fontSize}
|
||||||
|
variations={variations}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{icon} {text && <span>{text}</span>}
|
{icon} {text && <span>{text}</span>}
|
||||||
|
|||||||
@@ -1,10 +1,56 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css, RuleSet } from 'styled-components';
|
||||||
|
|
||||||
interface IStyledButtonIcon {
|
interface IStyledButtonIcon {
|
||||||
iconSize?: string;
|
iconSize?: string;
|
||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
variations?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IVariations {
|
||||||
|
[key: string]: RuleSet<object>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variations: IVariations = {
|
||||||
|
success: css`
|
||||||
|
background-color: var(--success-btn-color);
|
||||||
|
color: var(--light-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--success-btn-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: var(--success-btn-hover-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
primary: css`
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--light-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary-hover-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
danger: css`
|
||||||
|
background-color: var(--danger-btn-color);
|
||||||
|
color: var(--light-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--danger-btn-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: var(--danger-btn-hover-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
default: css`
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--hover-background-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
const StyledButtonIcon = styled.button<IStyledButtonIcon>`
|
const StyledButtonIcon = styled.button<IStyledButtonIcon>`
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
@@ -12,18 +58,24 @@ const StyledButtonIcon = styled.button<IStyledButtonIcon>`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
font-size: var(--fs-sm-2x);
|
${(props) =>
|
||||||
|
props.fontSize &&
|
||||||
|
css`
|
||||||
|
font-size: ${props.fontSize};
|
||||||
|
`}
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px 20px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
&:hover {
|
|
||||||
background-color: var(--hover-background-color);
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${(props) => variations[props.variations!]}
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.iconSize &&
|
props.iconSize &&
|
||||||
@@ -45,6 +97,8 @@ const StyledButtonIcon = styled.button<IStyledButtonIcon>`
|
|||||||
StyledButtonIcon.defaultProps = {
|
StyledButtonIcon.defaultProps = {
|
||||||
iconSize: '16px',
|
iconSize: '16px',
|
||||||
iconColor: '#000',
|
iconColor: '#000',
|
||||||
|
fontSize: 'var(--fs-sm-2x)',
|
||||||
|
variations: 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
export { StyledButtonIcon };
|
export { StyledButtonIcon };
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
// Types
|
||||||
import { ColumnProps } from '@src/types/common';
|
import { ColumnProps } from '@src/types/common';
|
||||||
|
|
||||||
|
// Styled
|
||||||
import {
|
import {
|
||||||
StyledBody,
|
StyledBody, StyledFooter, StyledHeader, StyledRow, StyledTable
|
||||||
StyledFooter,
|
|
||||||
StyledHeader,
|
|
||||||
StyledRow,
|
|
||||||
StyledTable,
|
|
||||||
} from './styled';
|
} from './styled';
|
||||||
|
|
||||||
|
// Components
|
||||||
import Pagination from '../Pagination';
|
import Pagination from '../Pagination';
|
||||||
import { TbArrowNarrowRight } from 'react-icons/tb';
|
import { TbArrowNarrowRight } from 'react-icons/tb';
|
||||||
|
import { StyledTableOption } from '@src/pages/Room/styled.ts';
|
||||||
|
import OrderBy from '@src/components/OrderBy';
|
||||||
|
import { ORDERBY_OPTIONS, ROOM_PAGE } from '@src/constants/commons.ts';
|
||||||
|
import SortBy from '@src/components/SortBy';
|
||||||
|
import Search from '@src/components/Search';
|
||||||
|
import Message from '@src/components/Message';
|
||||||
|
import Direction from '@src/commons/styles/Direction.ts';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
const parseWidthString = (columnsWidth: number[]): string => {
|
const parseWidthString = (columnsWidth: number[]): string => {
|
||||||
let result: string = '';
|
let result: string = '';
|
||||||
@@ -23,61 +32,86 @@ const parseWidthString = (columnsWidth: number[]): string => {
|
|||||||
type Props<T> = {
|
type Props<T> = {
|
||||||
columns: ColumnProps[];
|
columns: ColumnProps[];
|
||||||
rows: T[];
|
rows: T[];
|
||||||
|
stateSelected: {
|
||||||
|
itemSelected: T | undefined;
|
||||||
|
setItemSelected: Dispatch<SetStateAction<T | undefined>>
|
||||||
|
};
|
||||||
count?: number | null;
|
count?: number | null;
|
||||||
|
onRowClick?: (rowData: T) => void;
|
||||||
|
enabledOrder?: boolean;
|
||||||
|
enabledSort?: boolean;
|
||||||
|
enabledSearch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Table = <T,>({ rows, columns, count }: Props<T>) => {
|
const Table = <T, >({
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
count,
|
||||||
|
onRowClick,
|
||||||
|
stateSelected,
|
||||||
|
enabledSort = false,
|
||||||
|
enabledSearch = false,
|
||||||
|
enabledOrder = false
|
||||||
|
}: Props<T>) => {
|
||||||
const width = parseWidthString(columns.map((item) => item.width));
|
const width = parseWidthString(columns.map((item) => item.width));
|
||||||
|
const { itemSelected, setItemSelected } = stateSelected;
|
||||||
|
|
||||||
const headers = columns.map((column, index) => {
|
const headers = columns.map((column, index) => {
|
||||||
return <div key={`headCell-${index}`}>{column.title}</div>;
|
return <div key={`headCell-${index}`}>{column.title}</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderRows = rows.map((row, rowIndex) => {
|
const renderRows = rows.map((row, rowIndex) => {
|
||||||
|
const handleRowClick = () => {
|
||||||
|
if (onRowClick) {
|
||||||
|
onRowClick(row);
|
||||||
|
setItemSelected(row);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow
|
<StyledRow
|
||||||
key={`row-${rowIndex}`}
|
key={`row-${rowIndex}`}
|
||||||
width={width}
|
width={width}
|
||||||
onClick={
|
onDoubleClick={handleRowClick}
|
||||||
row[
|
className={JSON.stringify(itemSelected) === JSON.stringify(row)
|
||||||
'onClick' as keyof typeof row
|
? 'selected'
|
||||||
] as React.MouseEventHandler<HTMLDivElement>
|
: ''
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{columns.map((column, columnIndex) => {
|
{columns.map((column, columnIndex) => {
|
||||||
const value = row[column.key as keyof typeof row] as string;
|
const value = row[column.key as keyof typeof row] as string;
|
||||||
|
|
||||||
if (column.isDateValue) {
|
if (column.isDateValue) {
|
||||||
return (
|
return (<div
|
||||||
<div
|
style={{ display: 'flex', alignItems: 'center' }}
|
||||||
style={{ display: 'flex', alignItems: 'center' }}
|
key={`cell-${columnIndex}`}
|
||||||
key={`cell-${columnIndex}`}
|
>
|
||||||
>
|
{value[0]} <TbArrowNarrowRight />
|
||||||
{value[0]} <TbArrowNarrowRight />
|
{value[1]}
|
||||||
{value[1]}
|
</div>);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div key={`cell-${columnIndex}`}>{value}</div>;
|
return <div key={`cell-${columnIndex}`}>{value}</div>;
|
||||||
})}
|
})}
|
||||||
</StyledRow>
|
</StyledRow>);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (<Direction>
|
||||||
<StyledTable>
|
<StyledTableOption>
|
||||||
|
{enabledOrder && <OrderBy options={ORDERBY_OPTIONS} />}
|
||||||
|
{enabledSort && <SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />}
|
||||||
|
{enabledSearch && <Search setPlaceHolder="Search by name..." />}
|
||||||
|
</StyledTableOption>
|
||||||
|
|
||||||
|
{rows && rows.length ? (<StyledTable>
|
||||||
<StyledHeader width={width}>{headers}</StyledHeader>
|
<StyledHeader width={width}>{headers}</StyledHeader>
|
||||||
|
|
||||||
<StyledBody>{renderRows}</StyledBody>
|
<StyledBody>{renderRows}</StyledBody>
|
||||||
|
|
||||||
{Boolean(count) && (
|
{Boolean(count) && (<StyledFooter>
|
||||||
<StyledFooter>
|
<Pagination count={count!} />
|
||||||
<Pagination count={count!} />
|
</StyledFooter>)}
|
||||||
</StyledFooter>
|
</StyledTable>) : (<Message>No data to show here!</Message>)}
|
||||||
)}
|
</Direction>);
|
||||||
</StyledTable>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Table;
|
export default Table;
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const StyledBody = styled.div`
|
|||||||
const StyledRow = styled(CommonRow)`
|
const StyledRow = styled(CommonRow)`
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
&.active {
|
&.selected {
|
||||||
background-color: red;
|
background-color: var(--item-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
import { formatCurrency } from '@src/helpers/helper';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { IRoom } from '@src/types/room';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import RoomForm from './RoomForm';
|
|
||||||
import Modal from '@src/components/Modal';
|
|
||||||
import Table from '@src/components/Table';
|
|
||||||
import Menus from '@src/components/Menus';
|
|
||||||
import { RiEditBoxFill } from 'react-icons/ri';
|
|
||||||
import { HiTrash } from 'react-icons/hi';
|
|
||||||
import ConfirmMessage from '@src/components/ConfirmMessage';
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
import { useSetIsDeleteRoom } from '@src/hooks/rooms/useSetIsDeleteRoom';
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
import { FORM } from '@src/constants/commons';
|
|
||||||
|
|
||||||
interface IRoomRow {
|
|
||||||
room: IRoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RoomRow = ({ room }: IRoomRow) => {
|
|
||||||
const { id, name, price, status } = room;
|
|
||||||
const { setIsDeleteRoom } = useSetIsDeleteRoom();
|
|
||||||
const formattedPrice = useMemo(() => formatCurrency(price), [price]);
|
|
||||||
const renderEditBtn = useCallback(
|
|
||||||
(onOpenModal: () => void) => (
|
|
||||||
<Menus.Button
|
|
||||||
onClick={onOpenModal}
|
|
||||||
icon={<RiEditBoxFill />}
|
|
||||||
label={'Edit'}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const renderDeleteBtn = useCallback(
|
|
||||||
(onOpenModal: () => void) => (
|
|
||||||
<Menus.Button
|
|
||||||
icon={<HiTrash />}
|
|
||||||
onClick={onOpenModal}
|
|
||||||
disabled={status}
|
|
||||||
label={'Delete'}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[status]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderRow = useCallback(
|
|
||||||
(onCloseModal: () => void) => (
|
|
||||||
<RoomForm room={room} onCloseModal={onCloseModal} />
|
|
||||||
),
|
|
||||||
[room]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderConfirmMessage = useCallback(
|
|
||||||
(onCloseModal: () => void) => (
|
|
||||||
<ConfirmMessage
|
|
||||||
message={`Are you sure to delete ${name}?`}
|
|
||||||
onConfirm={() => setIsDeleteRoom(id)}
|
|
||||||
onCloseModal={onCloseModal}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[id, name, setIsDeleteRoom]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Row>
|
|
||||||
<div>{id}</div>
|
|
||||||
<div>{name}</div>
|
|
||||||
<div>{formattedPrice}</div>
|
|
||||||
<div>{status ? 'Unavailable' : 'Available'}</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.DELETE}
|
|
||||||
renderChildren={renderDeleteBtn}
|
|
||||||
/>
|
|
||||||
</Menus.List>
|
|
||||||
|
|
||||||
<Modal.Window
|
|
||||||
name={FORM.EDIT}
|
|
||||||
title="Edit Room"
|
|
||||||
renderChildren={renderRow}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal.Window
|
|
||||||
name={FORM.DELETE}
|
|
||||||
title="Delete Room"
|
|
||||||
renderChildren={renderConfirmMessage}
|
|
||||||
/>
|
|
||||||
</Menus.Menu>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
</Table.Row>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomRow;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// Components
|
|
||||||
import Table from '@src/components/Table';
|
|
||||||
import Message from '@src/components/Message';
|
|
||||||
import Search from '@src/components/Search';
|
|
||||||
import SortBy from '@src/components/SortBy';
|
|
||||||
import OrderBy from '@src/components/OrderBy';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { IRoom } from '@src/types/room';
|
|
||||||
import { ColumnProps } from '@src/types/common';
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
import { ORDERBY_OPTIONS, ROOM_PAGE } from '@src/constants/commons';
|
|
||||||
|
|
||||||
// Styled
|
|
||||||
import { StyledOperationTable } from './styled';
|
|
||||||
import Direction from '@src/commons/styles/Direction';
|
|
||||||
import Spinner from '@src/commons/styles/Spinner';
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
import { useRooms } from '@src/hooks/rooms/useRooms';
|
|
||||||
|
|
||||||
interface IRoomTable extends Omit<IRoom, 'status'>{
|
|
||||||
status: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RoomTable = () => {
|
|
||||||
const columns: ColumnProps[] = [
|
|
||||||
{
|
|
||||||
key: 'id',
|
|
||||||
title: 'Id',
|
|
||||||
width: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
width: 40,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'price',
|
|
||||||
title: 'Price',
|
|
||||||
width: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
title: 'Status',
|
|
||||||
width: 20,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const { isLoading, rooms, count } = useRooms();
|
|
||||||
|
|
||||||
const tempRooms = rooms?.map((room) => ({
|
|
||||||
...room,
|
|
||||||
status: room.status
|
|
||||||
? 'Unavailable'
|
|
||||||
: 'Available',
|
|
||||||
onClick: () => console.log(room),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Direction>
|
|
||||||
<StyledOperationTable>
|
|
||||||
<OrderBy options={ORDERBY_OPTIONS} />
|
|
||||||
|
|
||||||
<SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />
|
|
||||||
<Search setPlaceHolder="Search by name..." />
|
|
||||||
</StyledOperationTable>
|
|
||||||
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
|
|
||||||
{tempRooms && tempRooms.length ? (
|
|
||||||
<Table<IRoomTable> columns={columns} rows={tempRooms} count={count}/>
|
|
||||||
) : (
|
|
||||||
!isLoading && <Message>No data to show here!</Message>
|
|
||||||
)}
|
|
||||||
</Direction>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RoomTable;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { IRoom } from '@src/types/room';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import RoomRow from '../RoomRow';
|
|
||||||
|
|
||||||
describe('RoomRow', () => {
|
|
||||||
const tempRoom: IRoom = {
|
|
||||||
id: 1,
|
|
||||||
name: 'Temp Room',
|
|
||||||
price: 2399,
|
|
||||||
status: false,
|
|
||||||
isDelete: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
const wrapper = render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<RoomRow room={tempRoom} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
test('Should render correctly', () => {
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import RoomTable from '@src/pages/Room/RoomTable';
|
|
||||||
|
|
||||||
describe('UserTable', () => {
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
const wrapper = render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<BrowserRouter>
|
|
||||||
<RoomTable />
|
|
||||||
</BrowserRouter>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
test('Should render correctly', () => {
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +1,138 @@
|
|||||||
// Components
|
// Components
|
||||||
import RoomTable from './RoomTable';
|
|
||||||
import RoomForm from './RoomForm';
|
import RoomForm from './RoomForm';
|
||||||
import Modal from '@src/components/Modal';
|
import Modal from '@src/components/Modal';
|
||||||
|
import ButtonIcon from '@src/components/ButtonIcon';
|
||||||
|
import Direction from '@src/commons/styles/Direction';
|
||||||
|
import { FiEdit } from 'react-icons/fi';
|
||||||
|
import { FaRegTrashAlt } from 'react-icons/fa';
|
||||||
|
import { MdOutlineAddCircleOutline } from 'react-icons/md';
|
||||||
|
import Table from '@src/components/Table';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { StyledRoom, Title } from './styled';
|
import { ActionTable, StyledRoom, Title } from './styled';
|
||||||
import Direction from '@src/commons/styles/Direction';
|
import Spinner from '@src/commons/styles/Spinner.ts';
|
||||||
import Button from '@src/commons/styles/Button';
|
|
||||||
|
// Types
|
||||||
|
import { ColumnProps } from '@src/types/common.ts';
|
||||||
|
import { IRoom } from '@src/types/room.ts';
|
||||||
|
|
||||||
|
// Hooks
|
||||||
|
import { useRooms } from '@src/hooks/rooms/useRooms.ts';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface IRoomTable extends Omit<IRoom, 'status'> {
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
const Room = () => {
|
const Room = () => {
|
||||||
|
const [itemSelected, setItemSelected] = useState<IRoomTable>();
|
||||||
|
|
||||||
|
const columns: ColumnProps[] = [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
title: 'Id',
|
||||||
|
width: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
width: 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'price',
|
||||||
|
title: 'Price',
|
||||||
|
width: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
title: 'Status',
|
||||||
|
width: 20
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const { isLoading, rooms, count } = useRooms();
|
||||||
|
|
||||||
|
// Reset value when rooms changed.
|
||||||
|
useEffect(() => {
|
||||||
|
setItemSelected(undefined);
|
||||||
|
}, [rooms]);
|
||||||
|
|
||||||
|
const tempRooms = rooms?.map((room) => ({
|
||||||
|
...room,
|
||||||
|
status: room.status
|
||||||
|
? 'Unavailable'
|
||||||
|
: 'Available'
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRoom>
|
<StyledRoom>
|
||||||
<Direction type="horizontal">
|
<Direction type="horizontal">
|
||||||
<Title>List Room</Title>
|
<Title>List Room</Title>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Modal.Open
|
<ActionTable>
|
||||||
modalName="room-form"
|
<Modal.Open
|
||||||
renderChildren={(onCloseModal) => (
|
modalName="room-form"
|
||||||
<Button onClick={onCloseModal}>Add room</Button>
|
renderChildren={(onCloseModal) => (
|
||||||
)}
|
<ButtonIcon
|
||||||
/>
|
icon={<MdOutlineAddCircleOutline />}
|
||||||
<Modal.Window
|
text={'Add Room'}
|
||||||
name="room-form"
|
iconSize={'18px'}
|
||||||
title="Add form"
|
fontSize={'var(--fs-sm)'}
|
||||||
renderChildren={(onCloseModal) => (
|
variations={'primary'}
|
||||||
<RoomForm onCloseModal={onCloseModal} />
|
iconColor={'white'}
|
||||||
)}
|
onClick={onCloseModal}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Modal.Window
|
||||||
|
name="room-form"
|
||||||
|
title="Add form"
|
||||||
|
renderChildren={(onCloseModal) => (
|
||||||
|
<RoomForm onCloseModal={onCloseModal} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ButtonIcon
|
||||||
|
icon={<FiEdit />}
|
||||||
|
text={'Edit'}
|
||||||
|
iconSize={'18px'}
|
||||||
|
fontSize={'var(--fs-sm)'}
|
||||||
|
variations={'success'}
|
||||||
|
iconColor={'white'}
|
||||||
|
disabled={Boolean(!itemSelected)}
|
||||||
|
/>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={<FaRegTrashAlt />}
|
||||||
|
text={'Delete'}
|
||||||
|
iconSize={'18px'}
|
||||||
|
fontSize={'var(--fs-sm)'}
|
||||||
|
variations={'danger'}
|
||||||
|
iconColor={'white'}
|
||||||
|
disabled={Boolean(!itemSelected)}
|
||||||
|
/>
|
||||||
|
</ActionTable>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Direction>
|
</Direction>
|
||||||
|
|
||||||
<RoomTable />
|
{isLoading && <Spinner />}
|
||||||
|
|
||||||
|
{
|
||||||
|
tempRooms &&
|
||||||
|
Boolean(tempRooms.length) &&
|
||||||
|
<Table<IRoomTable>
|
||||||
|
columns={columns}
|
||||||
|
rows={tempRooms}
|
||||||
|
count={count}
|
||||||
|
enabledSort={true}
|
||||||
|
enabledOrder={true}
|
||||||
|
enabledSearch={true}
|
||||||
|
stateSelected={{
|
||||||
|
itemSelected,
|
||||||
|
setItemSelected,
|
||||||
|
}}
|
||||||
|
onRowClick={(data) => {
|
||||||
|
setItemSelected(data);
|
||||||
|
}} />}
|
||||||
</StyledRoom>
|
</StyledRoom>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,14 +14,20 @@ const Title = styled.h2`
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledOperationTable = styled.div`
|
const StyledTableOption = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ActionTable = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
export {
|
export {
|
||||||
StyledRoom,
|
StyledRoom,
|
||||||
Title,
|
Title,
|
||||||
StyledOperationTable
|
StyledTableOption,
|
||||||
|
ActionTable
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import UserTable from '@src/pages/User/UserTable.tsx';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import RoomTable from '@src/pages/Room/RoomTable';
|
|
||||||
|
|
||||||
describe('UserTable', () => {
|
describe('UserTable', () => {
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -11,7 +11,7 @@ describe('UserTable', () => {
|
|||||||
const wrapper = render(
|
const wrapper = render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<RoomTable />
|
<UserTable />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ERROR_DELETE_ROOM,
|
ERROR_DELETE_ROOM,
|
||||||
ERROR_FETCHING_ROOM,
|
ERROR_FETCHING_ROOM,
|
||||||
ERROR_UPDATE_ROOM,
|
ERROR_UPDATE_ROOM,
|
||||||
ROOMS_TABLE,
|
ROOMS_TABLE
|
||||||
} from '@src/constants/messages';
|
} from '@src/constants/messages';
|
||||||
|
|
||||||
interface IGetAllRooms {
|
interface IGetAllRooms {
|
||||||
@@ -26,11 +26,11 @@ interface IGetAllRooms {
|
|||||||
* @returns Return all rooms in database
|
* @returns Return all rooms in database
|
||||||
*/
|
*/
|
||||||
const getAllRooms = async ({
|
const getAllRooms = async ({
|
||||||
sortBy,
|
sortBy,
|
||||||
orderBy,
|
orderBy,
|
||||||
roomSearch,
|
roomSearch,
|
||||||
page,
|
page
|
||||||
}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => {
|
}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => {
|
||||||
const from = (page - 1) * DEFAULT_PAGE_SIZE;
|
const from = (page - 1) * DEFAULT_PAGE_SIZE;
|
||||||
const to = from + DEFAULT_PAGE_SIZE - 1;
|
const to = from + DEFAULT_PAGE_SIZE - 1;
|
||||||
|
|
||||||
@@ -157,5 +157,5 @@ export {
|
|||||||
createRoom,
|
createRoom,
|
||||||
setIsDeleteRoom,
|
setIsDeleteRoom,
|
||||||
getRoomById,
|
getRoomById,
|
||||||
updateRoomStatus,
|
updateRoomStatus
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #0010ba;
|
--primary-color: #0010ba;
|
||||||
|
--primary-hover-color: #0010ba8c;
|
||||||
|
|
||||||
--header-table-color: #ebebeb;
|
--header-table-color: #ebebeb;
|
||||||
|
--item-selected: #f5f5f5;
|
||||||
--border-color: #8c8c8c;
|
--border-color: #8c8c8c;
|
||||||
--footer-table-color: #ebebeb;
|
--footer-table-color: #ebebeb;
|
||||||
|
|
||||||
--secondary-btn-color: #d1d1d1;
|
--secondary-btn-color: #d1d1d1;
|
||||||
--disabled-btn-color: #7f82a6;
|
--disabled-btn-color: #7f82a6;
|
||||||
|
|
||||||
--danger-btn-color: #ff4f4f;
|
--danger-btn-color: #ff4f4f;
|
||||||
|
--danger-btn-hover-color: #ff4f4f8c;
|
||||||
|
|
||||||
|
--success-btn-color: #228b22;
|
||||||
|
--success-btn-hover-color: #228b2299;
|
||||||
|
|
||||||
--hover-background-color: #f3f4f6;
|
--hover-background-color: #f3f4f6;
|
||||||
--hover-dark-background-color: #d0d0d0;
|
--hover-dark-background-color: #d0d0d0;
|
||||||
|
|||||||
Reference in New Issue
Block a user