Merge pull request #24 from Nez27/feat/fix-comments

Fix comments
This commit is contained in:
Loi Phan
2023-11-08 20:19:18 +07:00
committed by GitHub
44 changed files with 979 additions and 947 deletions
+2 -1
View File
@@ -49,6 +49,7 @@
"husky": "^8.0.0",
"lint-staged": "^15.0.1",
"typescript": "^5.0.2",
"vite": "^4.4.5"
"vite": "^4.4.5",
"vite-tsconfig-paths": "^4.2.1"
}
}
+37
View File
@@ -70,6 +70,9 @@ devDependencies:
vite:
specifier: ^4.4.5
version: 4.4.11
vite-tsconfig-paths:
specifier: ^4.2.1
version: 4.2.1(typescript@5.2.2)(vite@4.4.11)
packages:
@@ -1406,6 +1409,10 @@ packages:
slash: 3.0.0
dev: true
/globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
dev: true
/goober@2.1.13(csstype@3.1.2):
resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==}
peerDependencies:
@@ -2088,6 +2095,19 @@ packages:
typescript: 5.2.2
dev: true
/tsconfck@2.1.2(typescript@5.2.2):
resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==}
engines: {node: ^14.13.1 || ^16 || >=18}
hasBin: true
peerDependencies:
typescript: ^4.3.5 || ^5.0.0
peerDependenciesMeta:
typescript:
optional: true
dependencies:
typescript: 5.2.2
dev: true
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
@@ -2132,6 +2152,23 @@ packages:
punycode: 2.3.0
dev: true
/vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.4.11):
resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==}
peerDependencies:
vite: '*'
peerDependenciesMeta:
vite:
optional: true
dependencies:
debug: 4.3.4
globrex: 0.1.2
tsconfck: 2.1.2(typescript@5.2.2)
vite: 4.4.11
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/vite@4.4.11:
resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -1,22 +0,0 @@
import styled from 'styled-components';
const ButtonIcon = styled.button`
background: none;
padding: 8px;
transition: all 0.2s;
border: none;
border-radius: var(--radius-md);
&:hover {
background-color: var(--hover-background-color);
}
& svg {
width: 25px;
height: 25px;
}
`;
export default ButtonIcon;
@@ -1,8 +1,8 @@
import { Outlet } from 'react-router-dom';
// Components
import Header from '../Header';
import Sidebar from '../Sidebar';
import Header from '@component/Header';
import Sidebar from '@component/Sidebar';
// Styled
import { Main, StyledAppLayout } from './styled';
@@ -0,0 +1,131 @@
import { COLOR, SIZE } from '@constant/styles';
import { ReactNode } from 'react';
import styled, { css } from 'styled-components';
interface IStyledButtonIcon {
isHaveChildren: boolean;
style?: {
fontSize?: string;
backgroundColor?: string;
color?: string;
};
iconStyle?: {
color?: string;
size?: string;
};
}
const StyledButtonIcon = styled.button<IStyledButtonIcon>`
background: none;
padding: 10px;
transition: all 0.2s;
border: none;
${(props) =>
props.style?.fontSize &&
css`
font-size: ${props.style.fontSize};
`}
${(props) =>
props.style?.fontSize &&
css`
font-size: ${props.style.fontSize};
`}
${(props) =>
props.color &&
css`
font-size: ${props.color};
`}
${(props) =>
props.isHaveChildren
? css`
display: flex;
align-items: center;
gap: 10px;
text-align: left;
width: 100%;
`
: css`
border-radius: var(--radius-md);
`}
&:hover {
background-color: var(--hover-background-color);
}
& svg {
${(props) =>
props.iconStyle?.size &&
css`
width: ${props.iconStyle.size};
height: ${props.iconStyle.size};
`}
${(props) =>
props.iconStyle?.color &&
css`
color: ${props.iconStyle.color};
`}
transition: all 0.3s;
}
`;
StyledButtonIcon.defaultProps = {
style: {
fontSize: SIZE.DEFAULT,
backgroundColor: COLOR.DEFAULT,
color: COLOR.BLACK,
},
iconStyle: {
color: COLOR.BLACK,
size: SIZE.DEFAULT,
},
};
interface IButtonIcon {
icon: ReactNode;
children?: ReactNode;
style?: {
fontSize?: string;
backgroundColor?: string;
color?: string;
};
iconStyle?: {
color?: string;
size?: string;
};
onClick?: () => void;
}
const ButtonIcon = ({
icon,
children,
style,
iconStyle,
onClick,
}: IButtonIcon) => {
const isHaveChildren = Boolean(children);
return (
<StyledButtonIcon
isHaveChildren={isHaveChildren}
style={{
fontSize: style?.fontSize,
backgroundColor: style?.backgroundColor,
color: style?.color,
}}
iconStyle={iconStyle}
onClick={onClick}
>
{icon} <span>{children}</span>
</StyledButtonIcon>
);
};
export default ButtonIcon;
@@ -1,29 +0,0 @@
import { MutableRefObject, ReactNode, forwardRef } from 'react';
// Styled
import { StyledBody, StyledDialog, StyledTitle } from './styled';
// Type
import { Nullable } from '../../types/common';
export interface IDialogProps {
title?: string;
children?: ReactNode;
onClose?: () => void;
ref?: MutableRefObject<Nullable<HTMLDialogElement>>;
}
const Dialog = forwardRef<HTMLDialogElement, IDialogProps>(
(props: IDialogProps, ref) => {
const { title, children, onClose } = props;
return (
<StyledDialog ref={ref} onClose={onClose}>
<StyledTitle>{title}</StyledTitle>
<StyledBody>{children}</StyledBody>
</StyledDialog>
);
}
);
export default Dialog;
@@ -1,23 +0,0 @@
import styled from 'styled-components';
const StyledDialog = styled.dialog`
border-radius: var(--radius-sm);
border-color: var(--border-color);
`;
const StyledTitle = styled.p`
font-size: var(--fs-md);
text-transform: capitalize;
font-weight: 600;
padding: 20px 40px;
border-bottom: 1px solid var(--border-color);
text-align: center;
`;
const StyledBody = styled.div`
padding: 10px 20px;
`;
export { StyledDialog, StyledTitle, StyledBody };
@@ -1,7 +1,7 @@
// Components
import { IoPersonCircleOutline } from 'react-icons/io5';
import { HiOutlineLogout } from 'react-icons/hi';
import ButtonIcon from '../../commons/styles/ButtonIcon';
import ButtonIcon from '@component/ButtonIcon/ButtonIcon';
// Styled
import { StyledHeaderMenu } from './styled';
@@ -10,14 +10,18 @@ const HeaderMenu = () => {
return (
<StyledHeaderMenu>
<li>
<ButtonIcon aria-label="Profile">
<IoPersonCircleOutline />
</ButtonIcon>
<ButtonIcon
aria-label="Profile"
icon={<IoPersonCircleOutline />}
iconStyle={{ size: '23px' }}
/>
</li>
<li>
<ButtonIcon aria-label="Logout">
<HiOutlineLogout />
</ButtonIcon>
<ButtonIcon
aria-label="Logout"
icon={<HiOutlineLogout />}
iconStyle={{size: '23px' }}
/>
</li>
</StyledHeaderMenu>
);
@@ -4,16 +4,18 @@ import { MouseEvent, ReactNode, useContext, useState } from 'react';
import { HiEllipsisVertical } from 'react-icons/hi2';
// Hooks
import { useOutsideClick } from '../../hooks/useOutsideClick';
import { useOutsideClick } from '@hook/useOutsideClick';
// Styled
import { StyledMenu, StyledButton, StyledList, StyledToggle } from './styled';
import { StyledMenu, StyledList, StyledToggle } from './styled';
// Contexts
import MenusContext from '../../contexts/MenuContext';
import MenusContext from '@context/MenuContext';
// Types
import { Nullable } from '../../types/common';
import { Nullable } from '@type/common';
import ButtonIcon from '@component/ButtonIcon/ButtonIcon';
import { COLOR } from '@constant/styles';
interface IButton {
children?: string;
@@ -58,7 +60,7 @@ const List = ({
children: ReactNode;
}): Nullable<ReactNode> => {
const { openId, close } = useContext(MenusContext);
const ref = useOutsideClick(close!, false);
const ref = useOutsideClick<HTMLUListElement>(close!, false);
if (openId !== id) return null;
@@ -74,9 +76,14 @@ const Button = ({ children, icon, onClick }: IButton): ReactNode => {
return (
<li>
<StyledButton onClick={handleClick}>
{icon} <span>{children}</span>
</StyledButton>
<ButtonIcon
icon={icon}
onClick={handleClick}
iconStyle={{ color: COLOR.PRIMARY, size: '19px' }}
style={{ fontSize: '16px' }}
>
{children}
</ButtonIcon>
</li>
);
};
@@ -0,0 +1,75 @@
import {
ReactElement,
ReactNode,
cloneElement,
useContext,
useState,
} from 'react';
import { createPortal } from 'react-dom';
// Hooks
import { useOutsideClick } from '@hook/useOutsideClick';
// Contexts
import { ModalContext } from '@context/ModalContext';
// Styled
import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled';
interface IModal {
children: ReactNode;
}
const Modal = ({ children }: IModal) => {
const [openName, setOpenName] = useState('');
const close = () => setOpenName('');
const open = setOpenName;
return (
<ModalContext.Provider value={{ openName, close, open }}>
{children}
</ModalContext.Provider>
);
};
interface IOpen {
renderChildren: (onCloseModal: () => void) => ReactNode;
modalName: string;
}
const Open = ({ renderChildren, modalName }: IOpen) => {
const { open } = useContext(ModalContext);
return renderChildren(() => open!(modalName));
};
interface IWindow {
children: ReactElement;
name: string;
title: string;
}
const Window = ({ children, name, title }: IWindow) => {
const { openName, close } = useContext(ModalContext);
const ref = useOutsideClick<HTMLDivElement>(close!);
if (name !== openName) return null;
return createPortal(
<Overlay>
<StyledModal ref={ref}>
<StyledModalContent>
<TitleModal>{title}</TitleModal>
{cloneElement(children, { onCloseModal: close })}
</StyledModalContent>
</StyledModal>
</Overlay>,
document.body
);
};
Modal.Open = Open;
Modal.Window = Window;
export default Modal;
@@ -0,0 +1,49 @@
import styled from 'styled-components';
const StyledModal = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--color-grey-0);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
padding: 3.2rem 4rem;
transition: all 0.5s;
`;
const StyledModalContent = styled.div`
background-color: var(--form-color);
border-radius: var(--radius-md);
padding: 30px 30px;
`;
const Overlay = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: var(--overlay-color);
z-index: 1000;
transition: all 0.5s;
`;
const TitleModal = styled.p`
font-size: var(--fs-md);
font-weight: 600;
text-align: center;
margin-bottom: 30px;
`
export { StyledModal, Overlay, StyledModalContent, TitleModal };
@@ -7,7 +7,7 @@ import { BsHouseDoorFill } from 'react-icons/bs';
import { StyledNav, StyledNavLink } from './styled';
// Constants
import { DASHBOARD, ROOM, USER } from '../../constants/path';
import { DASHBOARD, ROOM, USER } from '@constant/path';
const Nav = () => {
return (
@@ -1,4 +1,3 @@
import { memo } from 'react';
import { useSearchParams } from 'react-router-dom';
// Styled
@@ -11,7 +10,7 @@ interface IOrderProps {
}[];
}
const OrderBy = memo(({ options }: IOrderProps) => {
const OrderBy = ({ options }: IOrderProps) => {
const field = 'orderBy';
const [searchParams, setSearchParams] = useSearchParams();
const currentOrder = searchParams.get(field) || options[0].value;
@@ -35,6 +34,6 @@ const OrderBy = memo(({ options }: IOrderProps) => {
))}
</StyledOrder>
);
});
};
export default OrderBy;
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { StyledSearch } from './styled';
// Hooks
import { useDebounce } from '../../hooks/useDebounce';
import { useDebounce } from '@hook/useDebounce';
interface ISearch {
setPlaceHolder: string;
@@ -1,10 +1,9 @@
import {
FieldValues,
RegisterOptions,
UseFormRegister,
useFormContext,
} from 'react-hook-form';
import { ChangeEventHandler, ReactNode } from 'react';
import { ChangeEventHandler } from 'react';
// Styled
import { StyledSelect } from './styled';
@@ -22,46 +21,6 @@ interface ISelect {
ariaLabel: string;
}
interface IRender extends ISelect {
register: UseFormRegister<FieldValues>;
children: ReactNode[];
}
const RenderSelect = ({
options,
id,
value,
optionsConfigForm,
onChange,
register,
children,
ariaLabel,
}: IRender) => {
if (options && !register) {
return (
<StyledSelect
aria-label={ariaLabel}
id={id}
value={value}
onChange={onChange}
>
{children}
</StyledSelect>
);
}
return (
<StyledSelect
aria-label={ariaLabel}
id={id}
value={value}
{...register(id!, optionsConfigForm)}
>
{children}
</StyledSelect>
);
};
const Select = ({
options,
id,
@@ -75,21 +34,20 @@ const Select = ({
if (!options) return;
return (
<RenderSelect
register={register}
options={options}
optionsConfigForm={optionsConfigForm}
<StyledSelect
aria-label={ariaLabel}
id={id}
value={value}
onChange={onChange}
ariaLabel={ariaLabel}
{...(register
? { ...register(id!, optionsConfigForm) }
: { onChange: onChange })}
>
{options.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
))}
</RenderSelect>
</StyledSelect>
);
};
@@ -1,5 +1,5 @@
// Components
import Nav from '../Nav';
import Nav from '@component/Nav';
// Styled
import { Heading, StyledSidebar } from './styled';
@@ -1,8 +1,8 @@
import { useSearchParams } from 'react-router-dom';
import { ChangeEvent, memo } from 'react';
import { ChangeEvent } from 'react';
// Components
import Select from '../Select';
import Select from '@component/Select';
interface ISortByProps {
options: {
@@ -11,7 +11,7 @@ interface ISortByProps {
}[];
}
const SortBy = memo(({ options }: ISortByProps) => {
const SortBy = ({ options }: ISortByProps) => {
const [searchParams, setSearchParams] = useSearchParams();
const sortBy = searchParams.get('sortBy') || '';
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
@@ -27,6 +27,6 @@ const SortBy = memo(({ options }: ISortByProps) => {
ariaLabel="Sort"
/>
);
});
};
export default SortBy;
@@ -4,13 +4,17 @@ import { ReactNode, useContext } from 'react';
import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled';
// Contexts
import TableContext from '../../contexts/TableContext';
import TableContext from '@context/TableContext';
export interface ITable {
columns?: string;
children: ReactNode;
}
export interface IHeader {
headerColumn: string[];
}
interface ITableBody<T> {
data?: T[];
render?: CallbackMapFunc<T>;
@@ -26,10 +30,12 @@ const Table = ({ columns, children }: ITable) => {
);
};
const Header = ({ children }: ITable) => {
const Header = ({ headerColumn }: IHeader) => {
const { columns } = useContext(TableContext);
return <StyledHeader columns={columns}>{children}</StyledHeader>;
return <StyledHeader columns={columns}>
{headerColumn.map((item) => <div key={item}>{item}</div>)}
</StyledHeader>;
};
const Body = <T,>({ data, render }: ITableBody<T>) => {
@@ -51,9 +51,6 @@ const ROOM_PAGE = {
],
};
const VALUE = 'value';
const ERROR = 'error';
const INIT_VALUE_USER_FORM = {
name: '',
id: 0,
@@ -68,11 +65,23 @@ const INIT_VALUE_ROOM_FORM = {
discount: 0,
};
const FORM = {
EDIT: 'edit',
USER: 'user-form',
ROOM: 'room-form',
};
const REGEX = {
PHONE: /(84|0[3|5|7|8|9])+([0-9]{8})\b/g,
NAME: /^[a-zA-Z]{2,}(?: [a-zA-Z]+){0,2}$/gm,
NUMBER: /^[0-9]*$/,
};
export {
USER_PAGE,
ROOM_PAGE,
VALUE,
ERROR,
FORM,
REGEX,
ORDERBY_OPTIONS,
INIT_VALUE_USER_FORM,
INIT_VALUE_ROOM_FORM,
+11
View File
@@ -0,0 +1,11 @@
const COLOR = {
PRIMARY: '#0010ba',
BLACK: '#000',
DEFAULT: 'transparent',
};
const SIZE = {
DEFAULT: '14px',
};
export { COLOR, SIZE };
@@ -0,0 +1,11 @@
import { createContext } from 'react';
interface IModalContext {
openName?: string;
close?: () => void;
open?: React.Dispatch<React.SetStateAction<string>>;
}
const ModalContext = createContext<IModalContext>({});
export { ModalContext };
+2 -2
View File
@@ -1,8 +1,8 @@
// Constants
import { BASE_URL } from '../constants/path';
import { BASE_URL } from '@constant/path';
// Types
import { IResponse } from '../types/responses';
import { IResponse } from '@type/responses';
type TMethodRequest = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
+7 -32
View File
@@ -1,20 +1,12 @@
/**
* Check value is valid name or not
* @param value Values need to be checked
* @returns A boolean indicating whether or not the argument has valid
*/
const isValidName = (value: string): boolean => {
return /^[a-zA-Z]{2,}(?: [a-zA-Z]+){0,2}$/gm.test(value);
};
/**
* Check value is valid number or not
* Compare the regex with value
* @param regex The regex to compare
* @param value Value need to be checked
* @returns A boolean indicating whether or not the argument has valid
* @returns A boolean indicating whether of not the argument has valid
*/
const isValidNumber = (value: string): boolean => {
return /^[0-9]*$/.test(value);
};
const isValidRegex = (regex: RegExp, value: string): boolean => {
return regex.test(value);
}
/**
* Check value is valid discount or not
@@ -25,14 +17,6 @@ const isValidDiscount = (value: number): boolean => {
return value >= 0 && value <= 100;
};
/**
* Check value is valid phone number or not
* @param value Value need to be checked
* @returns A boolean indicating whether or not the argument has valid
*/
const isValidPhoneNumber = (value: string): boolean => {
return /(84|0[3|5|7|8|9])+([0-9]{8})\b/g.test(value);
};
/**
* Check value is valid string or not
@@ -43,12 +27,6 @@ const isValidString = (value: string): boolean => {
return value.trim().length >= 5;
};
/**
* A method skip check value
* @returns Return true
*/
const skipCheck = () => true;
/**
* Check object is empty or not
* @param obj Object need to be check
@@ -59,11 +37,8 @@ const isEmptyObj = (obj: object) => {
};
export {
isValidName,
isValidNumber,
isValidPhoneNumber,
isValidRegex,
isValidString,
skipCheck,
isValidDiscount,
isEmptyObj,
};
+4 -4
View File
@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
// Constants
import { BASE_URL } from '../constants/path';
import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '../constants/config';
import { BASE_URL } from '@constant/path';
import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '@constant/config';
// Helpers
import { searchQuery } from '../helpers/helper';
import { searchQuery } from '@helper/helper';
// Types
import { Nullable } from '../types/common';
import { Nullable } from '@type/common';
/**
* The function use to fetch data on server API.
+1 -1
View File
@@ -1,7 +1,7 @@
import { ForwardedRef, useEffect, useRef } from 'react';
// Types
import { Nullable } from '../types/common';
import { Nullable } from '@type/common';
const useForwardRef = <T>(
ref: ForwardedRef<T>,
@@ -1,7 +1,7 @@
import { MutableRefObject, useEffect, useRef } from 'react';
// Types
import { Nullable } from '../types/common';
import { Nullable } from '@type/common';
/**
* Custom hook to call handler when click outside element
@@ -9,11 +9,11 @@ import { Nullable } from '../types/common';
* @param listeningCapturing A boolean value indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. If not specified, defaults to false.
* @returns Return a mutable ref object
*/
export const useOutsideClick = (
export const useOutsideClick = <T extends Node>(
handler: () => void,
listeningCapturing = true
): MutableRefObject<Nullable<HTMLUListElement>> => {
const ref = useRef<HTMLUListElement>(null);
): MutableRefObject<Nullable<T>> => {
const ref = useRef<T>(null);
useEffect(() => {
const handleClick = (e: Event) => {
@@ -1,70 +0,0 @@
import {
Dispatch,
MutableRefObject,
SetStateAction,
forwardRef,
useEffect,
} from 'react';
// Components
import Dialog from '../../components/Dialog';
import RoomForm from './Form';
// Types
import { Nullable } from '../../types/common';
import { IRoom } from '../../types/rooms';
// Hooks
import { useForwardRef } from '../../hooks/useForwardRef';
interface IRoomDialog {
onClose?: () => void;
reload?: boolean;
setReload?: Dispatch<SetStateAction<boolean>>;
ref?: MutableRefObject<Nullable<HTMLDialogElement>>;
room?: Nullable<IRoom>;
isAdd?: boolean;
}
const RoomDialog = forwardRef<HTMLDialogElement, IRoomDialog>(
(props: IRoomDialog, ref) => {
const dialogRef = useForwardRef(ref);
const {
onClose,
setReload,
reload,
room,
isAdd
} = props;
useEffect(() => {
if (dialogRef.current) {
dialogRef.current.addEventListener('click', (e: MouseEvent) => {
const dialogDimensions = dialogRef.current!.getBoundingClientRect();
if (
e.clientX < dialogDimensions.left ||
e.clientX > dialogDimensions.right ||
e.clientY < dialogDimensions.top ||
e.clientY > dialogDimensions.bottom
) {
dialogRef.current!.close();
}
});
}
}, [dialogRef]);
return (
<Dialog title={'Add room'} onClose={onClose} ref={dialogRef}>
<RoomForm
onClose={onClose!}
reload={reload!}
setReload={setReload!}
room={room}
isAdd={isAdd!}
/>
</Dialog>
);
}
);
export default RoomDialog;
@@ -0,0 +1,104 @@
import { useSearchParams } from 'react-router-dom';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
// Components
import Menus from '@component/Menus';
import Table from '@component/Table';
import Message from '@component/Message';
import Search from '@component/Search';
import SortBy from '@component/SortBy';
import OrderBy from '@component/OrderBy';
import RoomRow from './RoomRow';
// Types
import { IRoom } from '@type/rooms';
// Constants
import { useFetch } from '@hook/useFetch';
import { ORDERBY_OPTIONS, ROOM_PAGE } from '@constant/commons';
// Styled
import { StyledOperationTable } from './styled';
import Direction from '@commonStyle/Direction';
import Spinner from '@commonStyle/Spinner';
interface IRoomTable {
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
}
const RoomTable = ({ reload, setReload }: IRoomTable) => {
const columnName = ['Id', 'Name', 'Price', 'Status'];
const [nameSearch, setNameSearch] = useState('');
const [searchParams] = useSearchParams();
const [rooms, setRooms] = useState<IRoom[]>([]);
const sortByValue = searchParams.get('sortBy')
? searchParams.get('sortBy')!
: '';
const orderByValue = searchParams.get('orderBy')
? searchParams.get('orderBy')!
: '';
const { data, isPending, errorFetchMsg } = useFetch(
'rooms',
'name',
nameSearch,
sortByValue,
orderByValue,
reload
);
useEffect(() => {
if (data) {
setRooms(data);
} else {
setRooms([]);
}
if (errorFetchMsg) {
console.error(errorFetchMsg);
}
}, [data, errorFetchMsg]);
return (
<>
<Direction>
<StyledOperationTable>
<OrderBy options={ORDERBY_OPTIONS} />
<SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />
<Search
setValueSearch={setNameSearch}
setPlaceHolder="Search by name..."
/>
</StyledOperationTable>
{isPending && <Spinner />}
{rooms.length ? (
<Menus>
<Table columns="10% 40% 20% 20% 5%">
<Table.Header headerColumn={columnName}/>
<Table.Body<IRoom>
data={rooms}
render={(room: IRoom) => (
<RoomRow
room={room}
key={room.id}
reload={reload}
setReload={setReload}
/>
)}
/>
</Table>
</Menus>
) : (
!isPending && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default RoomTable;
@@ -6,36 +6,35 @@ import styled from 'styled-components';
import { useForm } from 'react-hook-form';
// Styled
import Button from '../../commons/styles/Button.ts';
import Input from '../../commons/styles/Input.ts';
import Button from '@commonStyle/Button.ts';
import Input from '@commonStyle/Input.ts';
// Types
import { Nullable } from '../../types/common';
import { IRoom } from '../../types/rooms';
import { Nullable } from '@type/common.ts';
import { IRoom } from '@type/rooms.ts';
// Constants
import {
ADD_SUCCESS,
EDIT_SUCCESS,
} from '../../constants/messages.ts';
import { ADD_SUCCESS, EDIT_SUCCESS } from '@constant/messages.ts';
import {
INVALID_DISCOUNT,
INVALID_FIELD,
REQUIRED_FIELD_ERROR,
} from '../../constants/formValidateMessage.ts';
} from '@constant/formValidateMessage.ts';
import { INIT_VALUE_ROOM_FORM, REGEX } from '../../constants/commons.ts';
// Helpers
import {
isValidDiscount,
isValidNumber,
isValidRegex,
isValidString,
} from '../../helpers/validators.ts';
} from '@helper/validators.ts';
// Components
import FormRow from '../../components/LabelControl/index.tsx';
import Form from '../../components/Form/index.tsx';
import { addRoom, updateRoom } from '../../services/roomServices.ts';
import { INIT_VALUE_ROOM_FORM } from '../../constants/variables.ts';
import FormRow from '@component/LabelControl/index.tsx';
import Form from '@component/Form/index.tsx';
// Services
import { addRoom, updateRoom } from '@service/roomServices.ts';
const FormBtn = styled(Button)`
width: 100%;
@@ -48,68 +47,61 @@ const FormBtn = styled(Button)`
`;
interface IRoomFormProp {
onClose: () => void;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
room?: Nullable<IRoom>;
isAdd: boolean;
onCloseModal?: () => void;
reload?: boolean;
setReload?: Dispatch<SetStateAction<boolean>>;
roomEdit?: Nullable<IRoom>;
}
const RoomForm = ({
onClose,
onCloseModal,
reload,
setReload,
room,
isAdd,
roomEdit,
}: IRoomFormProp) => {
const formMethods = useForm<IRoom>();
const {
register,
handleSubmit,
reset,
formState: { errors, isDirty, isValid },
formState: { errors, isDirty, isValid, isSubmitting },
trigger,
} = formMethods;
useEffect(() => {
if (room && !isAdd) {
reset(room);
if (roomEdit) {
reset(roomEdit);
} else {
reset(INIT_VALUE_ROOM_FORM);
}
}, [room, reset, isAdd]);
}, [roomEdit, reset]);
// Submit form
const onSubmit = async (room: IRoom) => {
// Calculate final price
room.finalPrice = room.price - (room.price * room.discount) / 100;
try {
if (isAdd) {
// Add request
const response = await addRoom(room);
if(response) {
toast.success(ADD_SUCCESS);
}
} else {
// Edit request
const response = await updateRoom(room);
if (!roomEdit) {
// Add request
const response = await addRoom(room);
if (response) {
toast.success(EDIT_SUCCESS);
}
if (response) {
toast.success(ADD_SUCCESS);
}
// Reload table data
setReload(!reload);
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
} else {
// Edit request
const response = await updateRoom(room);
if (response) {
toast.success(EDIT_SUCCESS);
}
}
// Reload table data
setReload!(!reload);
reset();
onClose();
onCloseModal!();
};
return (
@@ -122,7 +114,7 @@ const RoomForm = ({
{...register('name', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkValidName: (value) => isValidString(value) || INVALID_FIELD,
checkValidRoomName: (value) => isValidString(value) || INVALID_FIELD,
},
onChange: () => trigger('name'),
})}
@@ -137,8 +129,8 @@ const RoomForm = ({
valueAsNumber: true,
required: REQUIRED_FIELD_ERROR,
validate: {
checkIdentifiedCode: (v) =>
isValidNumber(v.toString()) || INVALID_FIELD,
checkPrice: (v) =>
isValidRegex(new RegExp(REGEX.NUMBER), v.toString()) || INVALID_FIELD,
},
onChange: () => trigger('price'),
})}
@@ -153,7 +145,7 @@ const RoomForm = ({
valueAsNumber: true,
required: REQUIRED_FIELD_ERROR,
validate: {
checkPhoneNum: (v) => isValidDiscount(v) || INVALID_DISCOUNT,
checkDiscount: (v) => isValidDiscount(v) || INVALID_DISCOUNT,
},
onChange: () => trigger('discount'),
})}
@@ -161,14 +153,18 @@ const RoomForm = ({
</FormRow>
<Form.Action>
<FormBtn type="submit" name="submit" disabled={!isDirty || !isValid}>
<FormBtn
type="submit"
name="submit"
disabled={!isDirty || !isValid || isSubmitting}
>
{
isAdd
? 'Add'
: 'Save'
!roomEdit
? 'Add'
: 'Save'
}
</FormBtn>
<FormBtn type="button" styled="secondary" onClick={onClose}>
<FormBtn type="button" styled="secondary" onClick={onCloseModal}>
Close
</FormBtn>
</Form.Action>
@@ -0,0 +1,90 @@
import { Dispatch, SetStateAction } from 'react';
import toast from 'react-hot-toast';
// Constants
import { ROOM_PATH } from '@constant/path';
import { STATUS_CODE } from '@constant/responseStatus';
import { CONFIRM_DELETE, DELETE_SUCCESS } from '@constant/messages';
// Helpers
import { sendRequest } from '@helper/sendRequest';
import { formatCurrency } from '@helper/helper';
// Types
import { IRoom } from '@type/rooms';
// Components
import RoomForm from './RoomForm';
import Modal from '@component/Modal';
import Table from '@component/Table';
import Menus from '@component/Menus';
import { RiEditBoxFill } from 'react-icons/ri';
import { HiTrash } from 'react-icons/hi';
interface IRoomRow {
room: IRoom;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
}
const RoomRow = ({ room, reload, setReload }: IRoomRow) => {
const handleDelete = async (room: IRoom) => {
if (confirm(CONFIRM_DELETE)) {
const response = await sendRequest(ROOM_PATH + `/${room.id}`, 'DELETE');
if (response.statusCode === STATUS_CODE.OK) {
toast.success(DELETE_SUCCESS);
// Reload table
setReload(!reload);
}
}
};
const { id, name, finalPrice, status } = room;
const statusText = status
? 'Unavailable'
: 'Available';
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{formatCurrency(finalPrice)}</div>
<div>{statusText}</div>
<div>
<Modal>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Modal.Open
modalName="edit"
renderChildren={(onCloseModal) => (
<Menus.Button onClick={onCloseModal} icon={<RiEditBoxFill />}>
Edit
</Menus.Button>
)}
/>
<Menus.Button
icon={<HiTrash />}
onClick={() => handleDelete(room)}
>
Delete
</Menus.Button>
</Menus.List>
<Modal.Window name="edit" title="Edit Room">
<RoomForm roomEdit={room} setReload={setReload} reload={reload} />
</Modal.Window>
</Menus.Menu>
</Modal>
</div>
</Table.Row>
);
};
export default RoomRow;
-191
View File
@@ -1,191 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import toast from 'react-hot-toast';
// Components
import { RiEditBoxFill } from 'react-icons/ri';
import { HiTrash } from 'react-icons/hi';
import { StyledOperationTable } from './styled';
import Menus from '../../components/Menus';
import Table from '../../components/Table';
import Direction from '../../commons/styles/Direction';
import Message from '../../components/Message';
import Search from '../../components/Search';
import SortBy from '../../components/SortBy';
import OrderBy from '../../components/OrderBy';
// Types
import { Nullable } from '../../types/common';
import { IRoom } from '../../types/rooms';
// Constants
import { useFetch } from '../../hooks/useFetch';
import { ROOM_PATH } from '../../constants/path';
import { STATUS_CODE } from '../../constants/responseStatus';
import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages';
import { ORDERBY_OPTIONS, ROOM_PAGE } from '../../constants/variables';
// Styled
import Spinner from '../../commons/styles/Spinner';
// Helpers
import { sendRequest } from '../../helpers/sendRequest';
import { formatCurrency } from '../../helpers/helper';
interface IRoomRow {
room: IRoom;
openFormDialog: () => void;
setRoom: Dispatch<SetStateAction<Nullable<IRoom>>>;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
}
const RoomRow = ({
room,
openFormDialog,
setRoom,
reload,
setReload,
}: IRoomRow) => {
const handleEdit = (room: IRoom) => {
setRoom(room);
openFormDialog();
};
const handleDelete = async (room: IRoom) => {
if (confirm(CONFIRM_DELETE)) {
const response = await sendRequest(ROOM_PATH + `/${room.id}`, 'DELETE');
if (response.statusCode === STATUS_CODE.OK) {
toast.success(DELETE_SUCCESS);
// Reload table
setReload(!reload);
}
}
};
const { id, name, finalPrice, status } = room;
const statusText = status
? 'Unavailable'
: 'Available';
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{formatCurrency(finalPrice)}</div>
<div>{statusText}</div>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Menus.Button
icon={<RiEditBoxFill />}
onClick={() => handleEdit(room)}
>
Edit
</Menus.Button>
<Menus.Button icon={<HiTrash />} onClick={() => handleDelete(room)}>
Delete
</Menus.Button>
</Menus.List>
</Menus.Menu>
</Table.Row>
);
};
interface IRoomTable {
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
openFormDialog: () => void;
setRoom?: Dispatch<SetStateAction<Nullable<IRoom>>>;
}
const RoomTable = ({
reload,
setReload,
openFormDialog,
setRoom,
}: IRoomTable) => {
const [nameSearch, setNameSearch] = useState('');
const [searchParams] = useSearchParams();
const [rooms, setRooms] = useState<IRoom[]>([]);
const sortByValue = searchParams.get('sortBy')
? searchParams.get('sortBy')!
: '';
const orderByValue = searchParams.get('orderBy')
? searchParams.get('orderBy')!
: '';
const { data, isPending, errorFetchMsg } = useFetch(
'rooms',
'name',
nameSearch,
sortByValue,
orderByValue,
reload
);
useEffect(() => {
if (data) {
setRooms(data);
} else {
setRooms([]);
}
if (errorFetchMsg) {
console.error(errorFetchMsg);
}
}, [data, errorFetchMsg]);
return (
<>
<Direction>
<StyledOperationTable>
<OrderBy options={ORDERBY_OPTIONS} />
<SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />
<Search
setValueSearch={setNameSearch}
setPlaceHolder="Search by name..."
/>
</StyledOperationTable>
{isPending && <Spinner />}
{rooms.length ? (
<Menus>
<Table columns="10% 40% 20% 20% 5%">
<Table.Header>
<div>Id</div>
<div>Name</div>
<div>Price</div>
<div>Status</div>
</Table.Header>
<Table.Body<IRoom>
data={rooms}
render={(room: IRoom) => (
<RoomRow
room={room}
key={room.id}
reload={reload}
setReload={setReload}
openFormDialog={openFormDialog}
setRoom={setRoom!}
/>
)}
/>
</Table>
</Menus>
) : (
!isPending && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default RoomTable;
+19 -35
View File
@@ -1,57 +1,41 @@
import { useRef, useState } from 'react';
import { useState } from 'react';
// Components
import RoomTable from './Table';
import RoomTable from './RomTable';
import RoomForm from './RoomForm';
// Styled
import { StyledRoom, Title } from './styled';
import Direction from '../../commons/styles/Direction';
import Button from '../../commons/styles/Button';
import RoomDialog from './Dialog';
import Direction from '@commonStyle/Direction.ts';
import Button from '@commonStyle/Button';
// Types
import { Nullable } from '../../types/common';
import { IRoom } from '../../types/rooms';
import Modal from '@component/Modal';
const Room = () => {
const dialogRef = useRef<HTMLDialogElement>(null);
const [reload, setReload] = useState(true);
const [room, setRoom] = useState<Nullable<IRoom>>(null);
const [isAdd, setIsAdd] = useState(false);
const openFormDialog = (isAddForm: boolean = false) => {
setIsAdd(isAddForm);
dialogRef.current?.showModal();
};
const closeFormDialog = () => {
dialogRef.current?.close();
};
return (
<>
<StyledRoom>
<Direction type="horizontal">
<Title>List Room</Title>
<Button onClick={() => openFormDialog(true)}>Add room</Button>
<Modal>
<Modal.Open
modalName="room-form"
renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add room</Button>
)}
/>
<Modal.Window name="room-form" title="Add form">
<RoomForm setReload={setReload} reload={reload} />
</Modal.Window>
</Modal>
</Direction>
<RoomTable
reload={reload}
setReload={setReload}
openFormDialog={() => openFormDialog()}
setRoom={setRoom}
/>
<RoomTable reload={reload} setReload={setReload} />
</StyledRoom>
<RoomDialog
onClose={closeFormDialog}
ref={dialogRef}
setReload={setReload}
reload={reload}
room={room}
isAdd={isAdd}
/>
</>
);
};
@@ -1,69 +0,0 @@
import {
Dispatch,
MutableRefObject,
SetStateAction,
forwardRef,
useEffect,
} from 'react';
// Components
import Dialog from '../../components/Dialog';
import UserForm from './Form';
// Types
import { Nullable } from '../../types/common';
import { IUser } from '../../types/users';
// Hooks
import { useForwardRef } from '../../hooks/useForwardRef';
interface IUserDialog {
onClose: () => void;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
ref: MutableRefObject<Nullable<HTMLDialogElement>>;
user: Nullable<IUser>;
isAdd: boolean;
}
const UserDialog = forwardRef<HTMLDialogElement, IUserDialog>((props, ref) => {
const dialogRef = useForwardRef(ref);
const {
onClose,
setReload,
reload,
user,
isAdd,
} = props;
useEffect(() => {
if (dialogRef.current) {
dialogRef.current.addEventListener('click', (e: MouseEvent) => {
const dialogDimensions = dialogRef.current!.getBoundingClientRect();
if (
e.clientX < dialogDimensions.left ||
e.clientX > dialogDimensions.right ||
e.clientY < dialogDimensions.top ||
e.clientY > dialogDimensions.bottom
) {
dialogRef.current!.close();
}
});
}
}, [dialogRef]);
return (
<Dialog title={'Add user'} onClose={onClose} ref={dialogRef}>
<UserForm
onClose={onClose!}
reload={reload!}
setReload={setReload!}
user={user}
isAdd={isAdd!}
/>
</Dialog>
);
});
export default UserDialog;
-203
View File
@@ -1,203 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import toast from 'react-hot-toast';
// Components
import { RiEditBoxFill } from 'react-icons/ri';
import { IoExit } from 'react-icons/io5';
import Menus from '../../components/Menus';
import Table from '../../components/Table';
import Direction from '../../commons/styles/Direction';
import Message from '../../components/Message';
import Search from '../../components/Search';
import SortBy from '../../components/SortBy';
import OrderBy from '../../components/OrderBy';
// Types
import { Nullable } from '../../types/common';
import { IUser } from '../../types/users';
// Hooks
import { useFetch } from '../../hooks/useFetch';
// Constants
import { STATUS_CODE } from '../../constants/responseStatus';
import { ORDERBY_OPTIONS, USER_PAGE } from '../../constants/variables';
import { CONFIRM_MESSAGE, DENIED_ACTION } from '../../constants/messages';
// Styled
import { StyledOperationTable } from './styled';
import Spinner from '../../commons/styles/Spinner';
// Services
import { updateRoomStatus } from '../../services/roomServices';
import { checkOutUser } from '../../services/userServices';
interface IUserRow {
user: IUser;
openFormDialog: () => void;
setUser: Dispatch<SetStateAction<Nullable<IUser>>>;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
}
const UserRow = ({
user,
openFormDialog,
setUser,
reload,
setReload,
}: IUserRow) => {
const handleEdit = (user: IUser) => {
setUser(user);
openFormDialog();
};
const handleCheckOut = async (user: IUser) => {
if(user.roomId !== 0) {
if (confirm(CONFIRM_MESSAGE)) {
const resUpdateStatus = await updateRoomStatus(user.roomId, false);
const resCheckoutUser = await checkOutUser(user);
if (
resCheckoutUser?.statusCode === STATUS_CODE.OK &&
resUpdateStatus?.statusCode === STATUS_CODE.OK
) {
toast.success('Check out complete!');
// Reload table
setReload(!reload);
}
}
} else {
toast.error(DENIED_ACTION);
}
};
const { id, name, identifiedCode, phone, roomId } = user;
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{identifiedCode}</div>
<div>{phone}</div>
<div>{
roomId
? roomId
: 'None'
}</div>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Menus.Button
icon={<RiEditBoxFill />}
onClick={() => handleEdit(user)}
>
Edit
</Menus.Button>
<Menus.Button icon={<IoExit />} onClick={() => handleCheckOut(user)}>
Check out
</Menus.Button>
</Menus.List>
</Menus.Menu>
</Table.Row>
);
};
interface IUserTable {
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
openFormDialog: () => void;
setUser?: Dispatch<SetStateAction<Nullable<IUser>>>;
}
const UserTable = ({
reload,
setReload,
openFormDialog,
setUser,
}: IUserTable) => {
const [users, setUsers] = useState<IUser[]>([]);
const [phoneSearch, setPhoneSearch] = useState('');
const [searchParams] = useSearchParams();
const sortByValue = searchParams.get('sortBy')
? searchParams.get('sortBy')!
: '';
const orderByValue = searchParams.get('orderBy')
? searchParams.get('orderBy')!
: '';
const { data, isPending, errorFetchMsg } = useFetch(
'users',
'phone',
phoneSearch,
sortByValue,
orderByValue,
reload
);
useEffect(() => {
if (data) {
setUsers(data);
} else {
setUsers([]);
}
if (errorFetchMsg) {
console.error(errorFetchMsg);
}
}, [data, errorFetchMsg, setUsers]);
return (
<>
<Direction>
<StyledOperationTable>
<OrderBy options={ORDERBY_OPTIONS} />
<SortBy options={USER_PAGE.SORTBY_OPTIONS} />
<Search
setValueSearch={setPhoneSearch}
setPlaceHolder="Search by phone..."
/>
</StyledOperationTable>
{isPending && <Spinner />}
{users.length ? (
<Menus>
<Table columns="10% 30% 20% 20% 10% 5%">
<Table.Header>
<div>Id</div>
<div>Name</div>
<div>Identified Code</div>
<div>Phone</div>
<div>Room Id</div>
</Table.Header>
<Table.Body<IUser>
data={users}
render={(user: IUser) => (
<UserRow
user={user}
key={user.id}
reload={reload}
setReload={setReload}
openFormDialog={openFormDialog}
setUser={setUser!}
/>
)}
/>
</Table>
</Menus>
) : (
!isPending && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default UserTable;
@@ -11,63 +11,51 @@ import toast from 'react-hot-toast';
import { FormProvider, useForm } from 'react-hook-form';
// Styled
import Input from '../../commons/styles/Input';
import Input from '@commonStyle/Input.ts';
// Components
import Form from '../../components/Form';
import FormRow from '../../components/LabelControl/index.tsx';
import Select, { ISelectOptions } from '../../components/Select';
import Form from '@component/Form/index.tsx';
import FormRow from '@component/LabelControl/index.tsx';
import Select, { ISelectOptions } from '@component/Select/index.tsx';
// Helpers
import {
isEmptyObj,
isValidName,
isValidNumber,
isValidPhoneNumber,
} from '../../helpers/validators.ts';
import { isEmptyObj, isValidRegex } from '@helper/validators.ts';
// Constants
import { ADD_SUCCESS, EDIT_SUCCESS } from '../../constants/messages.ts';
import { ADD_SUCCESS, EDIT_SUCCESS } from '@constant/messages.ts';
import {
INVALID_FIELD,
INVALID_PHONE,
REQUIRED_FIELD_ERROR,
} from '../../constants/formValidateMessage.ts';
import { INIT_VALUE_USER_FORM } from '../../constants/variables.ts';
} from '@constant/formValidateMessage.ts';
import { INIT_VALUE_USER_FORM, REGEX } from '@constant/commons.ts';
// Styled
import { FormBtn } from './styled.ts';
// Services
import { getAllRoom, updateRoomStatus } from '../../services/roomServices.ts';
import { getAllRoom, updateRoomStatus } from '@service/roomServices.ts';
import { createUser, updateUser } from '@service/userServices.ts';
// Types
import { Nullable } from '../../types/common.ts';
import { IUser } from '../../types/users.ts';
import { IRoom } from '../../types/rooms.ts';
import { createUser, updateUser } from '../../services/userServices.ts';
import { Nullable } from '@type/common.ts';
import { IUser } from '@type/users.ts';
import { IRoom } from '@type/rooms.ts';
interface IUserFormProp {
onClose: () => void;
onCloseModal?: () => void;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
user: Nullable<IUser>;
isAdd: boolean;
user?: Nullable<IUser>;
}
const UserForm = ({
onClose,
reload,
setReload,
user,
isAdd,
}: IUserFormProp) => {
const UserForm = ({ onCloseModal, reload, setReload, user }: IUserFormProp) => {
const formMethods = useForm<IUser>();
const {
register,
handleSubmit,
reset,
formState: { errors, isDirty, isValid },
formState: { errors, isDirty, isValid, isSubmitting },
trigger,
} = formMethods;
const [rooms, setRooms] = useState<IRoom[]>([]);
@@ -77,9 +65,7 @@ const UserForm = ({
useEffect(() => {
const load = async () => {
const options: ISelectOptions[] = [];
const tempUser = isAdd
? {}
: { ...user };
const tempUser = !user ? { roomId: 0 } : { ...user };
// Load and set default options room
if (rooms.length > 0) {
@@ -111,45 +97,39 @@ const UserForm = ({
};
load();
}, [reset, user, isAdd, rooms]);
}, [reset, user, rooms]);
// Submit form
const onSubmit = useCallback(
async (newUser: IUser) => {
try {
if (isAdd) {
// Add request
const response = await createUser(newUser);
if (!user) {
// Add request
const response = await createUser(newUser);
if (response) {
toast.success(ADD_SUCCESS);
}
// Update room status
updateRoomStatus(newUser.roomId, true);
} else {
// Edit request
const response = await updateUser(newUser);
if (response) {
toast.success(EDIT_SUCCESS);
}
// Update room status
updateRoomStatus(user!.roomId, true, newUser.roomId);
if (response) {
toast.success(ADD_SUCCESS);
}
// Reload table data
setReload(!reload);
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
// Update room status
updateRoomStatus(newUser.roomId, true);
} else {
// Edit request
const response = await updateUser(newUser);
if (response) {
toast.success(EDIT_SUCCESS);
}
// Update room status
updateRoomStatus(user!.roomId, true, newUser.roomId);
}
// Reload table data
setReload(!reload);
reset();
onClose();
onCloseModal!();
},
[isAdd, onClose, reload, reset, setReload, user]
[onCloseModal, reload, reset, setReload, user]
);
// Load all rooms
@@ -176,7 +156,8 @@ const UserForm = ({
{...register('name', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkValidName: (value) => isValidName(value) || INVALID_FIELD,
checkValidName: (value) =>
isValidRegex(new RegExp(REGEX.NAME), value) || INVALID_FIELD,
},
onChange: () => trigger('name'),
})}
@@ -194,7 +175,8 @@ const UserForm = ({
required: REQUIRED_FIELD_ERROR,
validate: {
checkIdentifiedCode: (v) =>
isValidNumber(v.toString()) || INVALID_FIELD,
isValidRegex(new RegExp(REGEX.NUMBER), v.toString()) ||
INVALID_FIELD,
},
onChange: () => trigger('identifiedCode'),
})}
@@ -208,7 +190,8 @@ const UserForm = ({
{...register('phone', {
required: REQUIRED_FIELD_ERROR,
validate: {
checkPhoneNum: (v) => isValidPhoneNumber(v) || INVALID_PHONE,
checkPhoneNum: (v) =>
isValidRegex(new RegExp(REGEX.PHONE), v) || INVALID_PHONE,
},
onChange: () => trigger('phone'),
})}
@@ -233,14 +216,14 @@ const UserForm = ({
</FormRow>
<Form.Action>
<FormBtn type="submit" name="submit" disabled={!isDirty || !isValid}>
{
isAdd
? 'Add'
: 'Save'
}
<FormBtn
type="submit"
name="submit"
disabled={!isDirty || !isValid || isSubmitting}
>
{!user ? 'Add' : 'Save'}
</FormBtn>
<FormBtn type="button" styled="secondary" onClick={onClose}>
<FormBtn type="button" styled="secondary" onClick={onCloseModal}>
Close
</FormBtn>
</Form.Action>
@@ -0,0 +1,98 @@
import toast from 'react-hot-toast';
import { Dispatch, SetStateAction } from 'react';
// Components
import { RiEditBoxFill } from 'react-icons/ri';
import { IoExit } from 'react-icons/io5';
import Modal from '@component/Modal';
import Table from '@component/Table';
import Menus from '@component/Menus';
import UserForm from './UserForm';
// Constants
import { STATUS_CODE } from '@constant/responseStatus';
import { CONFIRM_MESSAGE, DENIED_ACTION } from '@constant/messages';
// Services
import { updateRoomStatus } from '@service/roomServices';
import { checkOutUser } from '@service/userServices';
// Types
import { IUser } from '@type/users';
interface IUserRow {
user: IUser;
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
}
const UserRow = ({ user, reload, setReload }: IUserRow) => {
const handleCheckOut = async (user: IUser) => {
if (user.roomId !== 0) {
if (confirm(CONFIRM_MESSAGE)) {
const resUpdateStatus = await updateRoomStatus(user.roomId, false);
const resCheckoutUser = await checkOutUser(user);
if (
resCheckoutUser?.statusCode === STATUS_CODE.OK &&
resUpdateStatus?.statusCode === STATUS_CODE.OK
) {
toast.success('Check out complete!');
// Reload table
setReload(!reload);
}
}
} else {
toast.error(DENIED_ACTION);
}
};
const { id, name, identifiedCode, phone, roomId } = user;
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{identifiedCode}</div>
<div>{phone}</div>
<div>
{
roomId
? roomId
: 'None'
}
</div>
<div>
<Modal>
<Menus.Menu>
<Menus.Toggle id={id.toString()} />
<Menus.List id={id.toString()}>
<Modal.Open
modalName="edit"
renderChildren={(onCloseModal) => (
<Menus.Button onClick={onCloseModal} icon={<RiEditBoxFill />}>
Edit
</Menus.Button>
)}
/>
<Menus.Button
icon={<IoExit />}
onClick={() => handleCheckOut(user)}
>
Check out
</Menus.Button>
</Menus.List>
<Modal.Window name="edit" title="Edit user">
<UserForm user={user} setReload={setReload} reload={reload} />
</Modal.Window>
</Menus.Menu>
</Modal>
</div>
</Table.Row>
);
};
export default UserRow;
@@ -0,0 +1,105 @@
import { useSearchParams } from 'react-router-dom';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
// Components
import Menus from '@component/Menus';
import Table from '@component/Table';
import Message from '@component/Message';
import Search from '@component/Search';
import SortBy from '@component/SortBy';
import OrderBy from '@component/OrderBy';
import UserRow from './UserRow';
// Types
import { IUser } from '@type/users';
// Hooks
import { useFetch } from '@hook/useFetch';
// Constants
import { ORDERBY_OPTIONS, USER_PAGE } from '@constant/commons';
// Styled
import Direction from '@commonStyle/Direction';
import { StyledOperationTable } from './styled';
import Spinner from '@commonStyle/Spinner';
interface IUserTable {
reload: boolean;
setReload: Dispatch<SetStateAction<boolean>>;
}
const UserTable = ({ reload, setReload }: IUserTable) => {
const columnName = ['Id', 'Name', 'Identified Code', 'Phone', 'Room Id'];
const [users, setUsers] = useState<IUser[]>([]);
const [phoneSearch, setPhoneSearch] = useState('');
const [searchParams] = useSearchParams();
const sortByValue = searchParams.get('sortBy')
? searchParams.get('sortBy')!
: '';
const orderByValue = searchParams.get('orderBy')
? searchParams.get('orderBy')!
: '';
const { data, isPending, errorFetchMsg } = useFetch(
'users',
'phone',
phoneSearch,
sortByValue,
orderByValue,
reload
);
useEffect(() => {
if (data) {
setUsers(data);
} else {
setUsers([]);
}
if (errorFetchMsg) {
console.error(errorFetchMsg);
}
}, [data, errorFetchMsg, setUsers]);
return (
<>
<Direction>
<StyledOperationTable>
<OrderBy options={ORDERBY_OPTIONS} />
<SortBy options={USER_PAGE.SORTBY_OPTIONS} />
<Search
setValueSearch={setPhoneSearch}
setPlaceHolder="Search by phone..."
/>
</StyledOperationTable>
{isPending && <Spinner />}
{users.length ? (
<Menus>
<Table columns="10% 30% 20% 20% 10% 5%">
<Table.Header headerColumn={columnName} />
<Table.Body<IUser>
data={users}
render={(user: IUser) => (
<UserRow
user={user}
key={user.id}
reload={reload}
setReload={setReload}
/>
)}
/>
</Table>
</Menus>
) : (
!isPending && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default UserTable;
+23 -35
View File
@@ -1,57 +1,45 @@
import { useRef, useState } from 'react';
import { useState } from 'react';
// Components
import UserTable from './Table';
import UserTable from './UserTable';
// Styled
import Button from '../../commons/styles/Button';
import Direction from '../../commons/styles/Direction';
import Button from '@commonStyle/Button';
import Direction from '@commonStyle/Direction.ts';
import { StyledUser, Title } from './styled';
import UserDialog from './Dialog';
// Types
import { Nullable } from '../../types/common';
import { IUser } from '../../types/users';
import Modal from '@component/Modal';
import UserForm from './UserForm';
// Constants
import { FORM } from '@constant/commons';
const User = () => {
const dialogRef = useRef<HTMLDialogElement>(null);
const TITLE = 'Add user';
const [reload, setReload] = useState(true);
const [user, setUser] = useState<Nullable<IUser>>(null);
const [isAdd, setIsAdd] = useState(false);
const openFormDialog = (isAddForm: boolean = false) => {
setIsAdd(isAddForm);
dialogRef.current?.showModal();
};
const closeFormDialog = () => {
dialogRef.current?.close();
};
return (
<>
<StyledUser>
<Direction type="horizontal">
<Title>List User</Title>
<Button onClick={() => openFormDialog(true)}>Add user</Button>
<Modal>
<Modal.Open
modalName={FORM.USER}
renderChildren={(onCloseModal) => (
<Button onClick={onCloseModal}>Add user</Button>
)}
/>
<Modal.Window name={FORM.USER} title={TITLE}>
<UserForm setReload={setReload} reload={reload} />
</Modal.Window>
</Modal>
</Direction>
<UserTable
reload={reload}
setReload={setReload}
openFormDialog={() => openFormDialog()}
setUser={setUser}
/>
<UserTable reload={reload} setReload={setReload} />
</StyledUser>
<UserDialog
onClose={closeFormDialog}
ref={dialogRef}
setReload={setReload}
reload={reload}
user={user}
isAdd={isAdd}
/>
</>
);
};
@@ -1,17 +1,17 @@
import toast from 'react-hot-toast';
// Types
import { Nullable } from '../types/common';
import { IResponse } from '../types/responses';
import { IRoom } from '../types/rooms';
import { Nullable } from '@type/common';
import { IResponse } from '@type/responses';
import { IRoom } from '@type/rooms';
// Helpers
import { sendRequest } from '../helpers/sendRequest';
import { errorMsg } from '../helpers/helper';
import { sendRequest } from '@helper/sendRequest';
import { errorMsg } from '@helper/helper';
// Constants
import { STATUS_CODE, RESPONSE_MESSAGE } from '../constants/responseStatus';
import { ROOM_PATH } from '../constants/path';
import { STATUS_CODE, RESPONSE_MESSAGE } from '@constant/responseStatus';
import { ROOM_PATH } from '@constant/path';
/**
* Get all rooms from server
@@ -1,17 +1,17 @@
import toast from 'react-hot-toast';
// Constants
import { USER_PATH } from '../constants/path';
import { STATUS_CODE } from '../constants/responseStatus';
import { USER_PATH } from '@constant/path';
import { STATUS_CODE } from '@constant/responseStatus';
// Types
import { Nullable } from '../types/common';
import { IResponse } from '../types/responses';
import { IUser } from '../types/users';
import { Nullable } from '@type/common';
import { IResponse } from '@type/responses';
import { IUser } from '@type/users';
// Helpers
import { sendRequest } from '../helpers/sendRequest';
import { errorMsg } from '../helpers/helper';
import { sendRequest } from '@helper/sendRequest';
import { errorMsg } from '@helper/helper';
/**
* Create user to the server
@@ -17,6 +17,10 @@
--radius-sm: 5px;
--radius-md: 10px;
--overlay-color: #0000004d;
--form-color: #fff;
--shadow-sm: 0px 10px 20px rgba(0, 0, 0, 0.2);
--fs-md: 30px;
+14 -1
View File
@@ -18,7 +18,20 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
/* Alias */
"paths": {
"@service/*": ["./src/services/*"],
"@constant/*": ["./src/constants/*"],
"@hook/*": ["./src/hooks/*"],
"@helper/*": ["./src/helpers/*"],
"@context/*": ["./src/contexts/*"],
"@component/*": ["./src/components/*"],
"@commonStyle/*": ["./src/commons/styles/*"],
"@type/*": ["./src/types/*"],
"@page/*": ["./src/pages/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
+2 -1
View File
@@ -1,7 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [react(), tsconfigPaths()],
})