Update table component, remove unnecessary code

This commit is contained in:
2023-12-13 12:49:10 +07:00
parent d543558a01
commit ad0b2826e2
16 changed files with 350 additions and 400 deletions
+20 -75
View File
@@ -2,7 +2,6 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import isPropValid from '@emotion/is-prop-valid';
import { StyleSheetManager } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect, useMemo, useReducer } from 'react';
// Components
import AppLayout from './components/AppLayout';
@@ -21,87 +20,33 @@ import Account from '@src/pages/Account';
// Constants
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();
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 (
<QueryClientProvider client={queryClient}>
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
<UserRoomAvailableContext.Provider value={store}>
<BrowserRouter>
<Routes>
<Route element={<RootLayout />}>
<Route
element={
<RouteProtected>
<AppLayout />
</RouteProtected>
}
>
<Route
index
element={<Navigate replace to={PATH.BOOKING} />}
/>
<Route path={PATH.BOOKING} element={<Booking />} />
<Route path={PATH.USER} element={<User />} />
<Route path={PATH.ROOM} element={<Room />} />
<Route path={PATH.ACCOUNT} element={<Account />} />
</Route>
<Route path={PATH.LOGIN} element={<Login />} />
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
<BrowserRouter>
<Routes>
<Route element={<RootLayout />}>
<Route
element={
<RouteProtected>
<AppLayout />
</RouteProtected>
}
>
<Route index element={<Navigate replace to={PATH.BOOKING} />} />
<Route path={PATH.BOOKING} element={<Booking />} />
<Route path={PATH.USER} element={<User />} />
<Route path={PATH.ROOM} element={<Room />} />
<Route path={PATH.ACCOUNT} element={<Account />} />
</Route>
</Routes>
</BrowserRouter>
</UserRoomAvailableContext.Provider>
<Route path={PATH.LOGIN} element={<Login />} />
<Route path={PATH.OTHER_PATH} element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
</StyleSheetManager>
<Toast />
@@ -16,7 +16,6 @@ const Spinner = styled.div`
top/10px 10px no-repeat,
conic-gradient(#0000 30%, var(--primary-color));
-webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 10px), #000 0);
mask: none;
animation: ${rotate} 1.5s infinite linear;
`;
@@ -10,17 +10,62 @@ import { Main, StyledAppLayout } from './styled';
// Hooks
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 { 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 (
<StyledAppLayout>
<Header accountName={account?.user_metadata.fullName} />
<Sidebar heading="Hotel Management" />
<Main>
<Outlet />
</Main>
</StyledAppLayout>
<UserRoomAvailableContext.Provider value={{usersAvailable, roomsAvailable}}>
<StyledAppLayout>
<Header accountName={account?.user_metadata.fullName} />
<Sidebar heading="Hotel Management" />
<Main>
<Outlet />
</Main>
</StyledAppLayout>
</UserRoomAvailableContext.Provider>
);
};
@@ -7,7 +7,9 @@ interface IButtonIcon extends ButtonHTMLAttributes<HTMLButtonElement> {
icon: ReactNode;
iconSize?: string;
iconColor?: string;
fontSize?: string;
text?: string;
variations?: string;
}
const ButtonIcon = ({
@@ -15,12 +17,16 @@ const ButtonIcon = ({
iconSize,
iconColor,
text,
fontSize,
variations,
...props
}: IButtonIcon) => {
return (
<StyledButtonIcon
iconColor={iconColor}
iconSize={iconSize}
fontSize={fontSize}
variations={variations}
{...props}
>
{icon} {text && <span>{text}</span>}
@@ -1,10 +1,56 @@
import styled, { css } from 'styled-components';
import styled, { css, RuleSet } from 'styled-components';
interface IStyledButtonIcon {
iconSize?: 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>`
background: none;
@@ -12,18 +58,24 @@ const StyledButtonIcon = styled.button<IStyledButtonIcon>`
align-items: center;
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;
width: 100%;
border: none;
border-radius: var(--radius-md);
&:hover {
background-color: var(--hover-background-color);
&:disabled {
cursor: not-allowed;
}
${(props) => variations[props.variations!]}
& svg {
${(props) =>
props.iconSize &&
@@ -45,6 +97,8 @@ const StyledButtonIcon = styled.button<IStyledButtonIcon>`
StyledButtonIcon.defaultProps = {
iconSize: '16px',
iconColor: '#000',
fontSize: 'var(--fs-sm-2x)',
variations: 'default',
};
export { StyledButtonIcon };
+66 -32
View File
@@ -1,13 +1,22 @@
// Types
import { ColumnProps } from '@src/types/common';
// Styled
import {
StyledBody,
StyledFooter,
StyledHeader,
StyledRow,
StyledTable,
StyledBody, StyledFooter, StyledHeader, StyledRow, StyledTable
} from './styled';
// Components
import Pagination from '../Pagination';
import { TbArrowNarrowRight } from 'react-icons/tb';
import { StyledTableOption } from '@src/pages/Room/styled.ts';
import OrderBy from '@src/components/OrderBy';
import { ORDERBY_OPTIONS, 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 => {
let result: string = '';
@@ -23,61 +32,86 @@ const parseWidthString = (columnsWidth: number[]): string => {
type Props<T> = {
columns: ColumnProps[];
rows: T[];
stateSelected: {
itemSelected: T | undefined;
setItemSelected: Dispatch<SetStateAction<T | undefined>>
};
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 { itemSelected, setItemSelected } = stateSelected;
const headers = columns.map((column, index) => {
return <div key={`headCell-${index}`}>{column.title}</div>;
});
const renderRows = rows.map((row, rowIndex) => {
const handleRowClick = () => {
if (onRowClick) {
onRowClick(row);
setItemSelected(row);
}
};
return (
<StyledRow
key={`row-${rowIndex}`}
width={width}
onClick={
row[
'onClick' as keyof typeof row
] as React.MouseEventHandler<HTMLDivElement>
}
>
onDoubleClick={handleRowClick}
className={JSON.stringify(itemSelected) === JSON.stringify(row)
? 'selected'
: ''
}>
{columns.map((column, columnIndex) => {
const value = row[column.key as keyof typeof row] as string;
if (column.isDateValue) {
return (
<div
style={{ display: 'flex', alignItems: 'center' }}
key={`cell-${columnIndex}`}
>
{value[0]} &nbsp; <TbArrowNarrowRight />
&nbsp; {value[1]}
</div>
);
return (<div
style={{ display: 'flex', alignItems: 'center' }}
key={`cell-${columnIndex}`}
>
{value[0]} &nbsp; <TbArrowNarrowRight />
&nbsp; {value[1]}
</div>);
}
return <div key={`cell-${columnIndex}`}>{value}</div>;
})}
</StyledRow>
);
</StyledRow>);
});
return (
<StyledTable>
return (<Direction>
<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>
<StyledBody>{renderRows}</StyledBody>
{Boolean(count) && (
<StyledFooter>
<Pagination count={count!} />
</StyledFooter>
)}
</StyledTable>
);
{Boolean(count) && (<StyledFooter>
<Pagination count={count!} />
</StyledFooter>)}
</StyledTable>) : (<Message>No data to show here!</Message>)}
</Direction>);
};
export default Table;
@@ -26,8 +26,8 @@ const StyledBody = styled.div`
const StyledRow = styled(CommonRow)`
padding: 20px;
&.active {
background-color: red;
&.selected {
background-color: var(--item-selected);
}
&:not(:last-child) {
-113
View File
@@ -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();
});
});
+119 -18
View File
@@ -1,37 +1,138 @@
// Components
import RoomTable from './RoomTable';
import RoomForm from './RoomForm';
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
import { StyledRoom, Title } from './styled';
import Direction from '@src/commons/styles/Direction';
import Button from '@src/commons/styles/Button';
import { ActionTable, StyledRoom, Title } from './styled';
import Spinner from '@src/commons/styles/Spinner.ts';
// 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 [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 (
<StyledRoom>
<Direction type="horizontal">
<Title>List Room</Title>
<Modal>
<Modal.Open
modalName="room-form"
renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add room</Button>
)}
/>
<Modal.Window
name="room-form"
title="Add form"
renderChildren={(onCloseModal) => (
<RoomForm onCloseModal={onCloseModal} />
)}
/>
<ActionTable>
<Modal.Open
modalName="room-form"
renderChildren={(onCloseModal) => (
<ButtonIcon
icon={<MdOutlineAddCircleOutline />}
text={'Add Room'}
iconSize={'18px'}
fontSize={'var(--fs-sm)'}
variations={'primary'}
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>
</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>
);
};
+8 -2
View File
@@ -14,14 +14,20 @@ const Title = styled.h2`
text-transform: capitalize;
`;
const StyledOperationTable = styled.div`
const StyledTableOption = styled.div`
display: flex;
gap: 30px;
justify-content: flex-end;
`;
const ActionTable = styled.div`
display: flex;
gap: 30px;
`
export {
StyledRoom,
Title,
StyledOperationTable
StyledTableOption,
ActionTable
};
@@ -1,9 +1,9 @@
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import UserTable from '@src/pages/User/UserTable.tsx';
// Components
import RoomTable from '@src/pages/Room/RoomTable';
describe('UserTable', () => {
const queryClient = new QueryClient();
@@ -11,7 +11,7 @@ describe('UserTable', () => {
const wrapper = render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<RoomTable />
<UserTable />
</BrowserRouter>
</QueryClientProvider>
);
@@ -11,7 +11,7 @@ import {
ERROR_DELETE_ROOM,
ERROR_FETCHING_ROOM,
ERROR_UPDATE_ROOM,
ROOMS_TABLE,
ROOMS_TABLE
} from '@src/constants/messages';
interface IGetAllRooms {
@@ -26,11 +26,11 @@ interface IGetAllRooms {
* @returns Return all rooms in database
*/
const getAllRooms = async ({
sortBy,
orderBy,
roomSearch,
page,
}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => {
sortBy,
orderBy,
roomSearch,
page
}: IGetAllRooms): Promise<{ data: IRoom[]; count: number | null }> => {
const from = (page - 1) * DEFAULT_PAGE_SIZE;
const to = from + DEFAULT_PAGE_SIZE - 1;
@@ -157,5 +157,5 @@ export {
createRoom,
setIsDeleteRoom,
getRoomById,
updateRoomStatus,
updateRoomStatus
};
@@ -1,13 +1,20 @@
:root {
--primary-color: #0010ba;
--primary-hover-color: #0010ba8c;
--header-table-color: #ebebeb;
--item-selected: #f5f5f5;
--border-color: #8c8c8c;
--footer-table-color: #ebebeb;
--secondary-btn-color: #d1d1d1;
--disabled-btn-color: #7f82a6;
--danger-btn-color: #ff4f4f;
--danger-btn-hover-color: #ff4f4f8c;
--success-btn-color: #228b22;
--success-btn-hover-color: #228b2299;
--hover-background-color: #f3f4f6;
--hover-dark-background-color: #d0d0d0;