From ad0b2826e253a82cdf3d4979cf285f007b197dd2 Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 13 Dec 2023 12:49:10 +0700 Subject: [PATCH] Update table component, remove unnecessary code --- hotel-management/src/App.tsx | 95 +++--------- .../src/commons/styles/Spinner.ts | 1 - .../src/components/AppLayout/index.tsx | 59 +++++++- .../src/components/ButtonIcon/index.tsx | 6 + .../src/components/ButtonIcon/styled.ts | 68 ++++++++- .../src/components/Table/index.tsx | 98 +++++++++---- .../src/components/Table/styled.ts | 4 +- hotel-management/src/pages/Room/RoomRow.tsx | 113 --------------- hotel-management/src/pages/Room/RoomTable.tsx | 83 ----------- .../src/pages/Room/__tests__/RoomRow.test.tsx | 29 ---- .../pages/Room/__tests__/RoomTable.test.tsx | 22 --- hotel-management/src/pages/Room/index.tsx | 137 +++++++++++++++--- hotel-management/src/pages/Room/styled.ts | 10 +- .../pages/User/__tests__/UserTable.test.tsx | 4 +- hotel-management/src/services/roomServices.ts | 14 +- .../src/styles/abstracts/variables.css | 7 + 16 files changed, 350 insertions(+), 400 deletions(-) delete mode 100644 hotel-management/src/pages/Room/RoomRow.tsx delete mode 100644 hotel-management/src/pages/Room/RoomTable.tsx delete mode 100644 hotel-management/src/pages/Room/__tests__/RoomRow.test.tsx delete mode 100644 hotel-management/src/pages/Room/__tests__/RoomTable.test.tsx diff --git a/hotel-management/src/App.tsx b/hotel-management/src/App.tsx index 3789d45..b837bdc 100644 --- a/hotel-management/src/App.tsx +++ b/hotel-management/src/App.tsx @@ -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 ( - - - - }> - - - - } - > - } - /> - } /> - } /> - } /> - } /> - - } /> - } /> + + + }> + + + + } + > + } /> + } /> + } /> + } /> + } /> - - - + } /> + } /> + + + diff --git a/hotel-management/src/commons/styles/Spinner.ts b/hotel-management/src/commons/styles/Spinner.ts index 38f3f49..ebe349b 100644 --- a/hotel-management/src/commons/styles/Spinner.ts +++ b/hotel-management/src/commons/styles/Spinner.ts @@ -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; `; diff --git a/hotel-management/src/components/AppLayout/index.tsx b/hotel-management/src/components/AppLayout/index.tsx index 57b5c2d..e3fc6cc 100644 --- a/hotel-management/src/components/AppLayout/index.tsx +++ b/hotel-management/src/components/AppLayout/index.tsx @@ -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 ( - -
- -
- -
- + + +
+ +
+ +
+ + ); }; diff --git a/hotel-management/src/components/ButtonIcon/index.tsx b/hotel-management/src/components/ButtonIcon/index.tsx index 99a49e9..0249f6f 100644 --- a/hotel-management/src/components/ButtonIcon/index.tsx +++ b/hotel-management/src/components/ButtonIcon/index.tsx @@ -7,7 +7,9 @@ interface IButtonIcon extends ButtonHTMLAttributes { 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 ( {icon} {text && {text}} diff --git a/hotel-management/src/components/ButtonIcon/styled.ts b/hotel-management/src/components/ButtonIcon/styled.ts index 042cae9..f308b2b 100644 --- a/hotel-management/src/components/ButtonIcon/styled.ts +++ b/hotel-management/src/components/ButtonIcon/styled.ts @@ -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; +} + +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` background: none; @@ -12,18 +58,24 @@ const StyledButtonIcon = styled.button` 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; - - &:hover { - background-color: var(--hover-background-color); + border-radius: var(--radius-md); + + &:disabled { + cursor: not-allowed; } + ${(props) => variations[props.variations!]} + & svg { ${(props) => props.iconSize && @@ -45,6 +97,8 @@ const StyledButtonIcon = styled.button` StyledButtonIcon.defaultProps = { iconSize: '16px', iconColor: '#000', + fontSize: 'var(--fs-sm-2x)', + variations: 'default', }; export { StyledButtonIcon }; diff --git a/hotel-management/src/components/Table/index.tsx b/hotel-management/src/components/Table/index.tsx index a5108d2..fd5fd11 100644 --- a/hotel-management/src/components/Table/index.tsx +++ b/hotel-management/src/components/Table/index.tsx @@ -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 = { columns: ColumnProps[]; rows: T[]; + stateSelected: { + itemSelected: T | undefined; + setItemSelected: Dispatch> + }; count?: number | null; + onRowClick?: (rowData: T) => void; + enabledOrder?: boolean; + enabledSort?: boolean; + enabledSearch?: boolean; }; -const Table = ({ rows, columns, count }: Props) => { +const Table = ({ + rows, + columns, + count, + onRowClick, + stateSelected, + enabledSort = false, + enabledSearch = false, + enabledOrder = false +}: Props) => { const width = parseWidthString(columns.map((item) => item.width)); + const { itemSelected, setItemSelected } = stateSelected; const headers = columns.map((column, index) => { return
{column.title}
; }); const renderRows = rows.map((row, rowIndex) => { + const handleRowClick = () => { + if (onRowClick) { + onRowClick(row); + setItemSelected(row); + } + }; + return ( - } - > + 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 ( -
- {value[0]}   -   {value[1]} -
- ); + return (
+ {value[0]}   +   {value[1]} +
); } return
{value}
; })} -
- ); + ); }); - return ( - + return ( + + {enabledOrder && } + {enabledSort && } + {enabledSearch && } + + + {rows && rows.length ? ( {headers} {renderRows} - {Boolean(count) && ( - - - - )} - - ); + {Boolean(count) && ( + + )} + ) : (No data to show here!)} + ); }; export default Table; diff --git a/hotel-management/src/components/Table/styled.ts b/hotel-management/src/components/Table/styled.ts index dbf0406..7cc00d9 100644 --- a/hotel-management/src/components/Table/styled.ts +++ b/hotel-management/src/components/Table/styled.ts @@ -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) { diff --git a/hotel-management/src/pages/Room/RoomRow.tsx b/hotel-management/src/pages/Room/RoomRow.tsx deleted file mode 100644 index 96d0f5e..0000000 --- a/hotel-management/src/pages/Room/RoomRow.tsx +++ /dev/null @@ -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) => ( - } - label={'Edit'} - /> - ), - [] - ); - const renderDeleteBtn = useCallback( - (onOpenModal: () => void) => ( - } - onClick={onOpenModal} - disabled={status} - label={'Delete'} - /> - ), - [status] - ); - - const renderRow = useCallback( - (onCloseModal: () => void) => ( - - ), - [room] - ); - - const renderConfirmMessage = useCallback( - (onCloseModal: () => void) => ( - setIsDeleteRoom(id)} - onCloseModal={onCloseModal} - /> - ), - [id, name, setIsDeleteRoom] - ); - - return ( - -
{id}
-
{name}
-
{formattedPrice}
-
{status ? 'Unavailable' : 'Available'}
- -
- - - - - - - - - - - - - - -
-
- ); -}; - -export default RoomRow; diff --git a/hotel-management/src/pages/Room/RoomTable.tsx b/hotel-management/src/pages/Room/RoomTable.tsx deleted file mode 100644 index efc1927..0000000 --- a/hotel-management/src/pages/Room/RoomTable.tsx +++ /dev/null @@ -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{ - 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 ( - <> - - - - - - - - - {isLoading && } - - {tempRooms && tempRooms.length ? ( - columns={columns} rows={tempRooms} count={count}/> - ) : ( - !isLoading && No data to show here! - )} - - - ); -}; - -export default RoomTable; diff --git a/hotel-management/src/pages/Room/__tests__/RoomRow.test.tsx b/hotel-management/src/pages/Room/__tests__/RoomRow.test.tsx deleted file mode 100644 index fcb9ece..0000000 --- a/hotel-management/src/pages/Room/__tests__/RoomRow.test.tsx +++ /dev/null @@ -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( - - - - ); - - test('Should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/hotel-management/src/pages/Room/__tests__/RoomTable.test.tsx b/hotel-management/src/pages/Room/__tests__/RoomTable.test.tsx deleted file mode 100644 index 559a5a0..0000000 --- a/hotel-management/src/pages/Room/__tests__/RoomTable.test.tsx +++ /dev/null @@ -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( - - - - - - ); - - test('Should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/hotel-management/src/pages/Room/index.tsx b/hotel-management/src/pages/Room/index.tsx index 22eff7b..51736c9 100644 --- a/hotel-management/src/pages/Room/index.tsx +++ b/hotel-management/src/pages/Room/index.tsx @@ -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 { + status: string; +} const Room = () => { + const [itemSelected, setItemSelected] = useState(); + + 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 ( List Room - ( - - )} - /> - ( - - )} - /> + + ( + } + text={'Add Room'} + iconSize={'18px'} + fontSize={'var(--fs-sm)'} + variations={'primary'} + iconColor={'white'} + onClick={onCloseModal} + /> + )} + /> + ( + + )} + /> + + } + text={'Edit'} + iconSize={'18px'} + fontSize={'var(--fs-sm)'} + variations={'success'} + iconColor={'white'} + disabled={Boolean(!itemSelected)} + /> + } + text={'Delete'} + iconSize={'18px'} + fontSize={'var(--fs-sm)'} + variations={'danger'} + iconColor={'white'} + disabled={Boolean(!itemSelected)} + /> + - + {isLoading && } + + { + tempRooms && + Boolean(tempRooms.length) && + + columns={columns} + rows={tempRooms} + count={count} + enabledSort={true} + enabledOrder={true} + enabledSearch={true} + stateSelected={{ + itemSelected, + setItemSelected, + }} + onRowClick={(data) => { + setItemSelected(data); + }} />} ); }; diff --git a/hotel-management/src/pages/Room/styled.ts b/hotel-management/src/pages/Room/styled.ts index d6ded8e..239f9e9 100644 --- a/hotel-management/src/pages/Room/styled.ts +++ b/hotel-management/src/pages/Room/styled.ts @@ -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 }; diff --git a/hotel-management/src/pages/User/__tests__/UserTable.test.tsx b/hotel-management/src/pages/User/__tests__/UserTable.test.tsx index 4de08a9..397fa01 100644 --- a/hotel-management/src/pages/User/__tests__/UserTable.test.tsx +++ b/hotel-management/src/pages/User/__tests__/UserTable.test.tsx @@ -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( - + ); diff --git a/hotel-management/src/services/roomServices.ts b/hotel-management/src/services/roomServices.ts index a880394..f59d4a1 100644 --- a/hotel-management/src/services/roomServices.ts +++ b/hotel-management/src/services/roomServices.ts @@ -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 }; diff --git a/hotel-management/src/styles/abstracts/variables.css b/hotel-management/src/styles/abstracts/variables.css index 8ced783..303edd7 100644 --- a/hotel-management/src/styles/abstracts/variables.css +++ b/hotel-management/src/styles/abstracts/variables.css @@ -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;