mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 13:38:51 +00:00
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+37
@@ -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>) => {
|
||||
|
||||
+14
-5
@@ -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,
|
||||
@@ -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 };
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,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;
|
||||
+52
-56
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+53
-70
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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" }]
|
||||
|
||||
@@ -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()],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user