mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 20:01:59 +00:00
Fix comments
This commit is contained in:
@@ -49,6 +49,7 @@
|
|||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"lint-staged": "^15.0.1",
|
"lint-staged": "^15.0.1",
|
||||||
"typescript": "^5.0.2",
|
"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:
|
vite:
|
||||||
specifier: ^4.4.5
|
specifier: ^4.4.5
|
||||||
version: 4.4.11
|
version: 4.4.11
|
||||||
|
vite-tsconfig-paths:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1(typescript@5.2.2)(vite@4.4.11)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1406,6 +1409,10 @@ packages:
|
|||||||
slash: 3.0.0
|
slash: 3.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/globrex@0.1.2:
|
||||||
|
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/goober@2.1.13(csstype@3.1.2):
|
/goober@2.1.13(csstype@3.1.2):
|
||||||
resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==}
|
resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2088,6 +2095,19 @@ packages:
|
|||||||
typescript: 5.2.2
|
typescript: 5.2.2
|
||||||
dev: true
|
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:
|
/tslib@2.6.2:
|
||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -2132,6 +2152,23 @@ packages:
|
|||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
dev: true
|
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:
|
/vite@4.4.11:
|
||||||
resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==}
|
resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
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';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Header from '../Header';
|
import Header from '@component/Header';
|
||||||
import Sidebar from '../Sidebar';
|
import Sidebar from '@component/Sidebar';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { Main, StyledAppLayout } from './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,7 +1,7 @@
|
|||||||
// Components
|
// Components
|
||||||
import { IoPersonCircleOutline } from 'react-icons/io5';
|
import { IoPersonCircleOutline } from 'react-icons/io5';
|
||||||
import { HiOutlineLogout } from 'react-icons/hi';
|
import { HiOutlineLogout } from 'react-icons/hi';
|
||||||
import ButtonIcon from '../../commons/styles/ButtonIcon';
|
import ButtonIcon from '@component/ButtonIcon/ButtonIcon';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { StyledHeaderMenu } from './styled';
|
import { StyledHeaderMenu } from './styled';
|
||||||
@@ -10,14 +10,18 @@ const HeaderMenu = () => {
|
|||||||
return (
|
return (
|
||||||
<StyledHeaderMenu>
|
<StyledHeaderMenu>
|
||||||
<li>
|
<li>
|
||||||
<ButtonIcon aria-label="Profile">
|
<ButtonIcon
|
||||||
<IoPersonCircleOutline />
|
aria-label="Profile"
|
||||||
</ButtonIcon>
|
icon={<IoPersonCircleOutline />}
|
||||||
|
iconStyle={{ size: '23px' }}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ButtonIcon aria-label="Logout">
|
<ButtonIcon
|
||||||
<HiOutlineLogout />
|
aria-label="Logout"
|
||||||
</ButtonIcon>
|
icon={<HiOutlineLogout />}
|
||||||
|
iconStyle={{size: '23px' }}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</StyledHeaderMenu>
|
</StyledHeaderMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import { MouseEvent, ReactNode, useContext, useState } from 'react';
|
|||||||
import { HiEllipsisVertical } from 'react-icons/hi2';
|
import { HiEllipsisVertical } from 'react-icons/hi2';
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useOutsideClick } from '../../hooks/useOutsideClick';
|
import { useOutsideClick } from '@hook/useOutsideClick';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { StyledMenu, StyledButton, StyledList, StyledToggle } from './styled';
|
import { StyledMenu, StyledList, StyledToggle } from './styled';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import MenusContext from '../../contexts/MenuContext';
|
import MenusContext from '@context/MenuContext';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../../types/common';
|
import { Nullable } from '@type/common';
|
||||||
|
import ButtonIcon from '@component/ButtonIcon/ButtonIcon';
|
||||||
|
import { COLOR } from '@constant/styles';
|
||||||
|
|
||||||
interface IButton {
|
interface IButton {
|
||||||
children?: string;
|
children?: string;
|
||||||
@@ -74,9 +76,14 @@ const Button = ({ children, icon, onClick }: IButton): ReactNode => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<StyledButton onClick={handleClick}>
|
<ButtonIcon
|
||||||
{icon} <span>{children}</span>
|
icon={icon}
|
||||||
</StyledButton>
|
onClick={handleClick}
|
||||||
|
iconStyle={{ color: COLOR.PRIMARY, size: '19px' }}
|
||||||
|
style={{ fontSize: '16px' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ButtonIcon>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useOutsideClick } from '../../hooks/useOutsideClick';
|
import { useOutsideClick } from '@hook/useOutsideClick';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import { ModalContext } from '../../contexts/ModalContext';
|
import { ModalContext } from '@context/ModalContext';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled';
|
import { Overlay, StyledModal, StyledModalContent, TitleModal } from './styled';
|
||||||
@@ -34,14 +34,14 @@ const Modal = ({ children }: IModal) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IOpen {
|
interface IOpen {
|
||||||
children: ReactElement;
|
renderChildren: (onCloseModal: () => void) => ReactNode;
|
||||||
modalName: string;
|
modalName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Open = ({ children, modalName }: IOpen) => {
|
const Open = ({ renderChildren, modalName }: IOpen) => {
|
||||||
const { open } = useContext(ModalContext);
|
const { open } = useContext(ModalContext);
|
||||||
|
|
||||||
return cloneElement(children, { onClick: () => open!(modalName) });
|
return renderChildren(() => open!(modalName));
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IWindow {
|
interface IWindow {
|
||||||
@@ -54,9 +54,6 @@ const Window = ({ children, name, title }: IWindow) => {
|
|||||||
const { openName, close } = useContext(ModalContext);
|
const { openName, close } = useContext(ModalContext);
|
||||||
const ref = useOutsideClick<HTMLDivElement>(close!);
|
const ref = useOutsideClick<HTMLDivElement>(close!);
|
||||||
|
|
||||||
console.log('name: ', name);
|
|
||||||
console.log('openName: ', openName);
|
|
||||||
|
|
||||||
if (name !== openName) return null;
|
if (name !== openName) return null;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BsHouseDoorFill } from 'react-icons/bs';
|
|||||||
import { StyledNav, StyledNavLink } from './styled';
|
import { StyledNav, StyledNavLink } from './styled';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { DASHBOARD, ROOM, USER } from '../../constants/path';
|
import { DASHBOARD, ROOM, USER } from '@constant/path';
|
||||||
|
|
||||||
const Nav = () => {
|
const Nav = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
@@ -11,7 +10,7 @@ interface IOrderProps {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrderBy = memo(({ options }: IOrderProps) => {
|
const OrderBy = ({ options }: IOrderProps) => {
|
||||||
const field = 'orderBy';
|
const field = 'orderBy';
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const currentOrder = searchParams.get(field) || options[0].value;
|
const currentOrder = searchParams.get(field) || options[0].value;
|
||||||
@@ -35,6 +34,6 @@ const OrderBy = memo(({ options }: IOrderProps) => {
|
|||||||
))}
|
))}
|
||||||
</StyledOrder>
|
</StyledOrder>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default OrderBy;
|
export default OrderBy;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { StyledSearch } from './styled';
|
import { StyledSearch } from './styled';
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
import { useDebounce } from '../../hooks/useDebounce';
|
import { useDebounce } from '@hook/useDebounce';
|
||||||
|
|
||||||
interface ISearch {
|
interface ISearch {
|
||||||
setPlaceHolder: string;
|
setPlaceHolder: string;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
FieldValues,
|
FieldValues,
|
||||||
RegisterOptions,
|
RegisterOptions,
|
||||||
UseFormRegister,
|
|
||||||
useFormContext,
|
useFormContext,
|
||||||
} from 'react-hook-form';
|
} from 'react-hook-form';
|
||||||
import { ChangeEventHandler, ReactNode } from 'react';
|
import { ChangeEventHandler } from 'react';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { StyledSelect } from './styled';
|
import { StyledSelect } from './styled';
|
||||||
@@ -22,46 +21,6 @@ interface ISelect {
|
|||||||
ariaLabel: string;
|
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 = ({
|
const Select = ({
|
||||||
options,
|
options,
|
||||||
id,
|
id,
|
||||||
@@ -75,21 +34,20 @@ const Select = ({
|
|||||||
if (!options) return;
|
if (!options) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RenderSelect
|
<StyledSelect
|
||||||
register={register}
|
aria-label={ariaLabel}
|
||||||
options={options}
|
|
||||||
optionsConfigForm={optionsConfigForm}
|
|
||||||
id={id}
|
id={id}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
{...(register
|
||||||
ariaLabel={ariaLabel}
|
? { ...register(id!, optionsConfigForm) }
|
||||||
|
: { onChange: onChange })}
|
||||||
>
|
>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<option value={option.value} key={option.value}>
|
<option value={option.value} key={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</RenderSelect>
|
</StyledSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Components
|
// Components
|
||||||
import Nav from '../Nav';
|
import Nav from '@component/Nav';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { Heading, StyledSidebar } from './styled';
|
import { Heading, StyledSidebar } from './styled';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { ChangeEvent, memo } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Select from '../Select';
|
import Select from '@component/Select';
|
||||||
|
|
||||||
interface ISortByProps {
|
interface ISortByProps {
|
||||||
options: {
|
options: {
|
||||||
@@ -11,7 +11,7 @@ interface ISortByProps {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortBy = memo(({ options }: ISortByProps) => {
|
const SortBy = ({ options }: ISortByProps) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const sortBy = searchParams.get('sortBy') || '';
|
const sortBy = searchParams.get('sortBy') || '';
|
||||||
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||||
@@ -27,6 +27,6 @@ const SortBy = memo(({ options }: ISortByProps) => {
|
|||||||
ariaLabel="Sort"
|
ariaLabel="Sort"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default SortBy;
|
export default SortBy;
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import { ReactNode, useContext } from 'react';
|
|||||||
import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled';
|
import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import TableContext from '../../contexts/TableContext';
|
import TableContext from '@context/TableContext';
|
||||||
|
|
||||||
export interface ITable {
|
export interface ITable {
|
||||||
columns?: string;
|
columns?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IHeader {
|
||||||
|
headerColumn: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ITableBody<T> {
|
interface ITableBody<T> {
|
||||||
data?: T[];
|
data?: T[];
|
||||||
render?: CallbackMapFunc<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);
|
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>) => {
|
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 = {
|
const INIT_VALUE_USER_FORM = {
|
||||||
name: '',
|
name: '',
|
||||||
id: 0,
|
id: 0,
|
||||||
@@ -68,11 +65,23 @@ const INIT_VALUE_ROOM_FORM = {
|
|||||||
discount: 0,
|
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 {
|
export {
|
||||||
USER_PAGE,
|
USER_PAGE,
|
||||||
ROOM_PAGE,
|
ROOM_PAGE,
|
||||||
VALUE,
|
FORM,
|
||||||
ERROR,
|
REGEX,
|
||||||
ORDERBY_OPTIONS,
|
ORDERBY_OPTIONS,
|
||||||
INIT_VALUE_USER_FORM,
|
INIT_VALUE_USER_FORM,
|
||||||
INIT_VALUE_ROOM_FORM,
|
INIT_VALUE_ROOM_FORM,
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const COLOR = {
|
||||||
|
PRIMARY: '#0010ba',
|
||||||
|
BLACK: '#000',
|
||||||
|
DEFAULT: 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIZE = {
|
||||||
|
DEFAULT: '14px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export { COLOR, SIZE };
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// Constants
|
// Constants
|
||||||
import { BASE_URL } from '../constants/path';
|
import { BASE_URL } from '@constant/path';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { IResponse } from '../types/responses';
|
import { IResponse } from '@type/responses';
|
||||||
|
|
||||||
type TMethodRequest = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
type TMethodRequest = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Check value is valid name or not
|
* Compare the regex with value
|
||||||
* @param value Values need to be checked
|
* @param regex The regex to compare
|
||||||
* @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
|
|
||||||
* @param value Value need to be checked
|
* @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 => {
|
const isValidRegex = (regex: RegExp, value: string): boolean => {
|
||||||
return /^[0-9]*$/.test(value);
|
return regex.test(value);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check value is valid discount or not
|
* Check value is valid discount or not
|
||||||
@@ -25,14 +17,6 @@ const isValidDiscount = (value: number): boolean => {
|
|||||||
return value >= 0 && value <= 100;
|
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
|
* Check value is valid string or not
|
||||||
@@ -43,12 +27,6 @@ const isValidString = (value: string): boolean => {
|
|||||||
return value.trim().length >= 5;
|
return value.trim().length >= 5;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A method skip check value
|
|
||||||
* @returns Return true
|
|
||||||
*/
|
|
||||||
const skipCheck = () => true;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check object is empty or not
|
* Check object is empty or not
|
||||||
* @param obj Object need to be check
|
* @param obj Object need to be check
|
||||||
@@ -59,11 +37,8 @@ const isEmptyObj = (obj: object) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isValidName,
|
isValidRegex,
|
||||||
isValidNumber,
|
|
||||||
isValidPhoneNumber,
|
|
||||||
isValidString,
|
isValidString,
|
||||||
skipCheck,
|
|
||||||
isValidDiscount,
|
isValidDiscount,
|
||||||
isEmptyObj,
|
isEmptyObj,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { BASE_URL } from '../constants/path';
|
import { BASE_URL } from '@constant/path';
|
||||||
import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '../constants/config';
|
import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '@constant/config';
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import { searchQuery } from '../helpers/helper';
|
import { searchQuery } from '@helper/helper';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../types/common';
|
import { Nullable } from '@type/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The function use to fetch data on server API.
|
* The function use to fetch data on server API.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ForwardedRef, useEffect, useRef } from 'react';
|
import { ForwardedRef, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../types/common';
|
import { Nullable } from '@type/common';
|
||||||
|
|
||||||
const useForwardRef = <T>(
|
const useForwardRef = <T>(
|
||||||
ref: ForwardedRef<T>,
|
ref: ForwardedRef<T>,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MutableRefObject, useEffect, useRef } from 'react';
|
import { MutableRefObject, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../types/common';
|
import { Nullable } from '@type/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook to call handler when click outside element
|
* Custom hook to call handler when click outside element
|
||||||
|
|||||||
@@ -2,25 +2,26 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { StyledOperationTable } from './styled';
|
import Menus from '@component/Menus';
|
||||||
import Menus from '../../components/Menus';
|
import Table from '@component/Table';
|
||||||
import Table from '../../components/Table';
|
import Message from '@component/Message';
|
||||||
import Direction from '../../commons/styles/Direction';
|
import Search from '@component/Search';
|
||||||
import Message from '../../components/Message';
|
import SortBy from '@component/SortBy';
|
||||||
import Search from '../../components/Search';
|
import OrderBy from '@component/OrderBy';
|
||||||
import SortBy from '../../components/SortBy';
|
import RoomRow from './RoomRow';
|
||||||
import OrderBy from '../../components/OrderBy';
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { IRoom } from '../../types/rooms';
|
import { IRoom } from '@type/rooms';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { useFetch } from '../../hooks/useFetch';
|
import { useFetch } from '@hook/useFetch';
|
||||||
import { ORDERBY_OPTIONS, ROOM_PAGE } from '../../constants/variables';
|
import { ORDERBY_OPTIONS, ROOM_PAGE } from '@constant/commons';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import Spinner from '../../commons/styles/Spinner';
|
import { StyledOperationTable } from './styled';
|
||||||
import RoomRow from './RoomRow';
|
import Direction from '@commonStyle/Direction';
|
||||||
|
import Spinner from '@commonStyle/Spinner';
|
||||||
|
|
||||||
|
|
||||||
interface IRoomTable {
|
interface IRoomTable {
|
||||||
reload: boolean;
|
reload: boolean;
|
||||||
@@ -28,6 +29,7 @@ interface IRoomTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RoomTable = ({ reload, setReload }: IRoomTable) => {
|
const RoomTable = ({ reload, setReload }: IRoomTable) => {
|
||||||
|
const columnName = ['Id', 'Name', 'Price', 'Status'];
|
||||||
const [nameSearch, setNameSearch] = useState('');
|
const [nameSearch, setNameSearch] = useState('');
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [rooms, setRooms] = useState<IRoom[]>([]);
|
const [rooms, setRooms] = useState<IRoom[]>([]);
|
||||||
@@ -77,12 +79,7 @@ const RoomTable = ({ reload, setReload }: IRoomTable) => {
|
|||||||
{rooms.length ? (
|
{rooms.length ? (
|
||||||
<Menus>
|
<Menus>
|
||||||
<Table columns="10% 40% 20% 20% 5%">
|
<Table columns="10% 40% 20% 20% 5%">
|
||||||
<Table.Header>
|
<Table.Header headerColumn={columnName}/>
|
||||||
<div>Id</div>
|
|
||||||
<div>Name</div>
|
|
||||||
<div>Price</div>
|
|
||||||
<div>Status</div>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body<IRoom>
|
<Table.Body<IRoom>
|
||||||
data={rooms}
|
data={rooms}
|
||||||
render={(room: IRoom) => (
|
render={(room: IRoom) => (
|
||||||
|
|||||||
@@ -6,36 +6,35 @@ import styled from 'styled-components';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import Button from '../../commons/styles/Button.ts';
|
import Button from '@commonStyle/Button.ts';
|
||||||
import Input from '../../commons/styles/Input.ts';
|
import Input from '@commonStyle/Input.ts';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../../types/common.ts';
|
import { Nullable } from '@type/common.ts';
|
||||||
import { IRoom } from '../../types/rooms.ts';
|
import { IRoom } from '@type/rooms.ts';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import {
|
import { ADD_SUCCESS, EDIT_SUCCESS } from '@constant/messages.ts';
|
||||||
ADD_SUCCESS,
|
|
||||||
EDIT_SUCCESS,
|
|
||||||
} from '../../constants/messages.ts';
|
|
||||||
import {
|
import {
|
||||||
INVALID_DISCOUNT,
|
INVALID_DISCOUNT,
|
||||||
INVALID_FIELD,
|
INVALID_FIELD,
|
||||||
REQUIRED_FIELD_ERROR,
|
REQUIRED_FIELD_ERROR,
|
||||||
} from '../../constants/formValidateMessage.ts';
|
} from '@constant/formValidateMessage.ts';
|
||||||
|
import { INIT_VALUE_ROOM_FORM, REGEX } from '../../constants/commons.ts';
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import {
|
import {
|
||||||
isValidDiscount,
|
isValidDiscount,
|
||||||
isValidNumber,
|
isValidRegex,
|
||||||
isValidString,
|
isValidString,
|
||||||
} from '../../helpers/validators.ts';
|
} from '@helper/validators.ts';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import FormRow from '../../components/LabelControl/index.tsx';
|
import FormRow from '@component/LabelControl/index.tsx';
|
||||||
import Form from '../../components/Form/index.tsx';
|
import Form from '@component/Form/index.tsx';
|
||||||
import { addRoom, updateRoom } from '../../services/roomServices.ts';
|
|
||||||
import { INIT_VALUE_ROOM_FORM } from '../../constants/variables.ts';
|
// Services
|
||||||
|
import { addRoom, updateRoom } from '@service/roomServices.ts';
|
||||||
|
|
||||||
const FormBtn = styled(Button)`
|
const FormBtn = styled(Button)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -82,30 +81,25 @@ const RoomForm = ({
|
|||||||
// Calculate final price
|
// Calculate final price
|
||||||
room.finalPrice = room.price - (room.price * room.discount) / 100;
|
room.finalPrice = room.price - (room.price * room.discount) / 100;
|
||||||
|
|
||||||
try {
|
if (!roomEdit) {
|
||||||
if (!roomEdit) {
|
// Add request
|
||||||
// Add request
|
const response = await addRoom(room);
|
||||||
const response = await addRoom(room);
|
|
||||||
|
|
||||||
if(response) {
|
|
||||||
toast.success(ADD_SUCCESS);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Edit request
|
|
||||||
const response = await updateRoom(room);
|
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
toast.success(EDIT_SUCCESS);
|
toast.success(ADD_SUCCESS);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Reload table data
|
} else {
|
||||||
setReload!(!reload);
|
// Edit request
|
||||||
} catch (error: unknown) {
|
const response = await updateRoom(room);
|
||||||
if (error instanceof Error) {
|
|
||||||
toast.error(error.message);
|
if (response) {
|
||||||
|
toast.success(EDIT_SUCCESS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload table data
|
||||||
|
setReload!(!reload);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
onCloseModal!();
|
onCloseModal!();
|
||||||
};
|
};
|
||||||
@@ -136,7 +130,7 @@ const RoomForm = ({
|
|||||||
required: REQUIRED_FIELD_ERROR,
|
required: REQUIRED_FIELD_ERROR,
|
||||||
validate: {
|
validate: {
|
||||||
checkIdentifiedCode: (v) =>
|
checkIdentifiedCode: (v) =>
|
||||||
isValidNumber(v.toString()) || INVALID_FIELD,
|
isValidRegex(REGEX.NUMBER, v.toString()) || INVALID_FIELD,
|
||||||
},
|
},
|
||||||
onChange: () => trigger('price'),
|
onChange: () => trigger('price'),
|
||||||
})}
|
})}
|
||||||
@@ -159,7 +153,11 @@ const RoomForm = ({
|
|||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
||||||
<Form.Action>
|
<Form.Action>
|
||||||
<FormBtn type="submit" name="submit" disabled={!isDirty || !isValid || isSubmitting}>
|
<FormBtn
|
||||||
|
type="submit"
|
||||||
|
name="submit"
|
||||||
|
disabled={!isDirty || !isValid || isSubmitting}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
!roomEdit
|
!roomEdit
|
||||||
? 'Add'
|
? 'Add'
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ import { Dispatch, SetStateAction } from 'react';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { ROOM_PATH } from '../../constants/path';
|
import { ROOM_PATH } from '@constant/path';
|
||||||
import { STATUS_CODE } from '../../constants/responseStatus';
|
import { STATUS_CODE } from '@constant/responseStatus';
|
||||||
import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages';
|
import { CONFIRM_DELETE, DELETE_SUCCESS } from '@constant/messages';
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import { sendRequest } from '../../helpers/sendRequest';
|
import { sendRequest } from '@helper/sendRequest';
|
||||||
import { formatCurrency } from '../../helpers/helper';
|
import { formatCurrency } from '@helper/helper';
|
||||||
import Modal from '../../components/Modal';
|
|
||||||
import RoomForm from './RoomForm';
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { IRoom } from '../../types/rooms';
|
import { IRoom } from '@type/rooms';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Table from '../../components/Table';
|
import RoomForm from './RoomForm';
|
||||||
import Menus from '../../components/Menus';
|
import Modal from '@component/Modal';
|
||||||
|
import Table from '@component/Table';
|
||||||
|
import Menus from '@component/Menus';
|
||||||
import { RiEditBoxFill } from 'react-icons/ri';
|
import { RiEditBoxFill } from 'react-icons/ri';
|
||||||
import { HiTrash } from 'react-icons/hi';
|
import { HiTrash } from 'react-icons/hi';
|
||||||
|
|
||||||
@@ -43,7 +43,9 @@ const RoomRow = ({ room, reload, setReload }: IRoomRow) => {
|
|||||||
|
|
||||||
const { id, name, finalPrice, status } = room;
|
const { id, name, finalPrice, status } = room;
|
||||||
|
|
||||||
const statusText = status ? 'Unavailable' : 'Available';
|
const statusText = status
|
||||||
|
? 'Unavailable'
|
||||||
|
: 'Available';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
@@ -58,9 +60,14 @@ const RoomRow = ({ room, reload, setReload }: IRoomRow) => {
|
|||||||
<Menus.Toggle id={id.toString()} />
|
<Menus.Toggle id={id.toString()} />
|
||||||
|
|
||||||
<Menus.List id={id.toString()}>
|
<Menus.List id={id.toString()}>
|
||||||
<Modal.Open modalName="edit">
|
<Modal.Open
|
||||||
<Menus.Button icon={<RiEditBoxFill />}>Edit</Menus.Button>
|
modalName="edit"
|
||||||
</Modal.Open>
|
renderChildren={(onCloseModal) => (
|
||||||
|
<Menus.Button onClick={onCloseModal} icon={<RiEditBoxFill />}>
|
||||||
|
Edit
|
||||||
|
</Menus.Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Menus.Button
|
<Menus.Button
|
||||||
icon={<HiTrash />}
|
icon={<HiTrash />}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import RoomTable from './RomTable';
|
import RoomTable from './RomTable';
|
||||||
|
import RoomForm from './RoomForm';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { StyledRoom, Title } from './styled';
|
import { StyledRoom, Title } from './styled';
|
||||||
import Direction from '../../commons/styles/Direction';
|
import Direction from '@commonStyle/Direction.ts';
|
||||||
import Button from '../../commons/styles/Button';
|
import Button from '@commonStyle/Button';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import Modal from '../../components/Modal';
|
import Modal from '@component/Modal';
|
||||||
import RoomForm from './RoomForm';
|
|
||||||
|
|
||||||
const Room = () => {
|
const Room = () => {
|
||||||
const [reload, setReload] = useState(true);
|
const [reload, setReload] = useState(true);
|
||||||
@@ -22,19 +22,19 @@ const Room = () => {
|
|||||||
<Title>List Room</Title>
|
<Title>List Room</Title>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Modal.Open modalName="room-form">
|
<Modal.Open
|
||||||
<Button>Add room</Button>
|
modalName="room-form"
|
||||||
</Modal.Open>
|
renderChildren={(onCloseModal) => (
|
||||||
|
<Button onClick={onCloseModal}>Add room</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Modal.Window name="room-form" title="Add form">
|
<Modal.Window name="room-form" title="Add form">
|
||||||
<RoomForm setReload={setReload} reload={reload} />
|
<RoomForm setReload={setReload} reload={reload} />
|
||||||
</Modal.Window>
|
</Modal.Window>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Direction>
|
</Direction>
|
||||||
|
|
||||||
<RoomTable
|
<RoomTable reload={reload} setReload={setReload} />
|
||||||
reload={reload}
|
|
||||||
setReload={setReload}
|
|
||||||
/>
|
|
||||||
</StyledRoom>
|
</StyledRoom>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,41 +11,38 @@ import toast from 'react-hot-toast';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import Input from '../../commons/styles/Input.ts';
|
import Input from '@commonStyle/Input.ts';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Form from '../../components/Form/index.tsx';
|
import Form from '@component/Form/index.tsx';
|
||||||
import FormRow from '../../components/LabelControl/index.tsx';
|
import FormRow from '@component/LabelControl/index.tsx';
|
||||||
import Select, { ISelectOptions } from '../../components/Select/index.tsx';
|
import Select, { ISelectOptions } from '@component/Select/index.tsx';
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import {
|
import {
|
||||||
isEmptyObj,
|
isEmptyObj, isValidRegex,
|
||||||
isValidName,
|
} from '@helper/validators.ts';
|
||||||
isValidNumber,
|
|
||||||
isValidPhoneNumber,
|
|
||||||
} from '../../helpers/validators.ts';
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { ADD_SUCCESS, EDIT_SUCCESS } from '../../constants/messages.ts';
|
import { ADD_SUCCESS, EDIT_SUCCESS } from '@constant/messages.ts';
|
||||||
import {
|
import {
|
||||||
INVALID_FIELD,
|
INVALID_FIELD,
|
||||||
INVALID_PHONE,
|
INVALID_PHONE,
|
||||||
REQUIRED_FIELD_ERROR,
|
REQUIRED_FIELD_ERROR,
|
||||||
} from '../../constants/formValidateMessage.ts';
|
} from '@constant/formValidateMessage.ts';
|
||||||
import { INIT_VALUE_USER_FORM } from '../../constants/variables.ts';
|
import { INIT_VALUE_USER_FORM, REGEX } from '@constant/commons.ts';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import { FormBtn } from './styled.ts';
|
import { FormBtn } from './styled.ts';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { getAllRoom, updateRoomStatus } from '../../services/roomServices.ts';
|
import { getAllRoom, updateRoomStatus } from '@service/roomServices.ts';
|
||||||
|
import { createUser, updateUser } from '@service/userServices.ts';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../../types/common.ts';
|
import { Nullable } from '@type/common.ts';
|
||||||
import { IUser } from '../../types/users.ts';
|
import { IUser } from '@type/users.ts';
|
||||||
import { IRoom } from '../../types/rooms.ts';
|
import { IRoom } from '@type/rooms.ts';
|
||||||
import { createUser, updateUser } from '../../services/userServices.ts';
|
|
||||||
|
|
||||||
interface IUserFormProp {
|
interface IUserFormProp {
|
||||||
onCloseModal?: () => void;
|
onCloseModal?: () => void;
|
||||||
@@ -109,35 +106,29 @@ const UserForm = ({ onCloseModal, reload, setReload, user }: IUserFormProp) => {
|
|||||||
// Submit form
|
// Submit form
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (newUser: IUser) => {
|
async (newUser: IUser) => {
|
||||||
try {
|
if (!user) {
|
||||||
if (!user) {
|
// Add request
|
||||||
// Add request
|
const response = await createUser(newUser);
|
||||||
const response = await createUser(newUser);
|
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
toast.success(ADD_SUCCESS);
|
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);
|
|
||||||
}
|
}
|
||||||
// Reload table data
|
|
||||||
setReload(!reload);
|
// Update room status
|
||||||
} catch (error: unknown) {
|
updateRoomStatus(newUser.roomId, true);
|
||||||
if (error instanceof Error) {
|
} else {
|
||||||
toast.error(error.message);
|
// 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();
|
reset();
|
||||||
onCloseModal!();
|
onCloseModal!();
|
||||||
@@ -169,7 +160,7 @@ const UserForm = ({ onCloseModal, reload, setReload, user }: IUserFormProp) => {
|
|||||||
{...register('name', {
|
{...register('name', {
|
||||||
required: REQUIRED_FIELD_ERROR,
|
required: REQUIRED_FIELD_ERROR,
|
||||||
validate: {
|
validate: {
|
||||||
checkValidName: (value) => isValidName(value) || INVALID_FIELD,
|
checkValidName: (value) => isValidRegex(REGEX.NAME, value) || INVALID_FIELD,
|
||||||
},
|
},
|
||||||
onChange: () => trigger('name'),
|
onChange: () => trigger('name'),
|
||||||
})}
|
})}
|
||||||
@@ -187,7 +178,7 @@ const UserForm = ({ onCloseModal, reload, setReload, user }: IUserFormProp) => {
|
|||||||
required: REQUIRED_FIELD_ERROR,
|
required: REQUIRED_FIELD_ERROR,
|
||||||
validate: {
|
validate: {
|
||||||
checkIdentifiedCode: (v) =>
|
checkIdentifiedCode: (v) =>
|
||||||
isValidNumber(v.toString()) || INVALID_FIELD,
|
isValidRegex(REGEX.NUMBER, v.toString()) || INVALID_FIELD,
|
||||||
},
|
},
|
||||||
onChange: () => trigger('identifiedCode'),
|
onChange: () => trigger('identifiedCode'),
|
||||||
})}
|
})}
|
||||||
@@ -201,7 +192,7 @@ const UserForm = ({ onCloseModal, reload, setReload, user }: IUserFormProp) => {
|
|||||||
{...register('phone', {
|
{...register('phone', {
|
||||||
required: REQUIRED_FIELD_ERROR,
|
required: REQUIRED_FIELD_ERROR,
|
||||||
validate: {
|
validate: {
|
||||||
checkPhoneNum: (v) => isValidPhoneNumber(v) || INVALID_PHONE,
|
checkPhoneNum: (v) => isValidRegex(REGEX.PHONE, v) || INVALID_PHONE,
|
||||||
},
|
},
|
||||||
onChange: () => trigger('phone'),
|
onChange: () => trigger('phone'),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -2,31 +2,27 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Menus from '../../components/Menus';
|
import Menus from '@component/Menus';
|
||||||
import Table from '../../components/Table';
|
import Table from '@component/Table';
|
||||||
import Direction from '../../commons/styles/Direction';
|
import Message from '@component/Message';
|
||||||
import Message from '../../components/Message';
|
import Search from '@component/Search';
|
||||||
import Search from '../../components/Search';
|
import SortBy from '@component/SortBy';
|
||||||
import SortBy from '../../components/SortBy';
|
import OrderBy from '@component/OrderBy';
|
||||||
import OrderBy from '../../components/OrderBy';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { IUser } from '../../types/users';
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
import { useFetch } from '../../hooks/useFetch';
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
import { ORDERBY_OPTIONS, USER_PAGE } from '../../constants/variables';
|
|
||||||
|
|
||||||
// Styled
|
|
||||||
import { StyledOperationTable } from './styled';
|
|
||||||
import Spinner from '../../commons/styles/Spinner';
|
|
||||||
import UserRow from './UserRow';
|
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 {
|
interface IUserTable {
|
||||||
reload: boolean;
|
reload: boolean;
|
||||||
@@ -34,6 +30,7 @@ interface IUserTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserTable = ({ reload, setReload }: IUserTable) => {
|
const UserTable = ({ reload, setReload }: IUserTable) => {
|
||||||
|
const columnName = ['Id', 'Name', 'Identified Code', 'Phone', 'Room Id'];
|
||||||
const [users, setUsers] = useState<IUser[]>([]);
|
const [users, setUsers] = useState<IUser[]>([]);
|
||||||
const [phoneSearch, setPhoneSearch] = useState('');
|
const [phoneSearch, setPhoneSearch] = useState('');
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -83,13 +80,7 @@ const UserTable = ({ reload, setReload }: IUserTable) => {
|
|||||||
{users.length ? (
|
{users.length ? (
|
||||||
<Menus>
|
<Menus>
|
||||||
<Table columns="10% 30% 20% 20% 10% 5%">
|
<Table columns="10% 30% 20% 20% 10% 5%">
|
||||||
<Table.Header>
|
<Table.Header headerColumn={columnName} />
|
||||||
<div>Id</div>
|
|
||||||
<div>Name</div>
|
|
||||||
<div>Identified Code</div>
|
|
||||||
<div>Phone</div>
|
|
||||||
<div>Room Id</div>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body<IUser>
|
<Table.Body<IUser>
|
||||||
data={users}
|
data={users}
|
||||||
render={(user: IUser) => (
|
render={(user: IUser) => (
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { Dispatch, SetStateAction } from 'react';
|
|||||||
// Components
|
// Components
|
||||||
import { RiEditBoxFill } from 'react-icons/ri';
|
import { RiEditBoxFill } from 'react-icons/ri';
|
||||||
import { IoExit } from 'react-icons/io5';
|
import { IoExit } from 'react-icons/io5';
|
||||||
import Modal from '../../components/Modal';
|
import Modal from '@component/Modal';
|
||||||
|
import Table from '@component/Table';
|
||||||
|
import Menus from '@component/Menus';
|
||||||
import UserForm from './FormUser';
|
import UserForm from './FormUser';
|
||||||
import Table from '../../components/Table';
|
|
||||||
import Menus from '../../components/Menus';
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { STATUS_CODE } from '../../constants/responseStatus';
|
import { STATUS_CODE } from '@constant/responseStatus';
|
||||||
import { CONFIRM_MESSAGE, DENIED_ACTION } from '../../constants/messages';
|
import { CONFIRM_MESSAGE, DENIED_ACTION } from '@constant/messages';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
import { updateRoomStatus } from '../../services/roomServices';
|
import { updateRoomStatus } from '@service/roomServices';
|
||||||
import { checkOutUser } from '../../services/userServices';
|
import { checkOutUser } from '@service/userServices';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { IUser } from '../../types/users';
|
import { IUser } from '@type/users';
|
||||||
|
|
||||||
interface IUserRow {
|
interface IUserRow {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
@@ -56,17 +56,27 @@ const UserRow = ({ user, reload, setReload }: IUserRow) => {
|
|||||||
<div>{name}</div>
|
<div>{name}</div>
|
||||||
<div>{identifiedCode}</div>
|
<div>{identifiedCode}</div>
|
||||||
<div>{phone}</div>
|
<div>{phone}</div>
|
||||||
<div>{roomId ? roomId : 'None'}</div>
|
<div>
|
||||||
|
{
|
||||||
|
roomId
|
||||||
|
? roomId
|
||||||
|
: 'None'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Menus.Menu>
|
<Menus.Menu>
|
||||||
<Menus.Toggle id={id.toString()} />
|
<Menus.Toggle id={id.toString()} />
|
||||||
|
|
||||||
<Menus.List id={id.toString()}>
|
<Menus.List id={id.toString()}>
|
||||||
<Modal.Open modalName="edit">
|
<Modal.Open
|
||||||
<Menus.Button icon={<RiEditBoxFill />}>Edit</Menus.Button>
|
modalName="edit"
|
||||||
</Modal.Open>
|
renderChildren={(onCloseModal) => (
|
||||||
|
<Menus.Button onClick={onCloseModal} icon={<RiEditBoxFill />}>
|
||||||
|
Edit
|
||||||
|
</Menus.Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Menus.Button
|
<Menus.Button
|
||||||
icon={<IoExit />}
|
icon={<IoExit />}
|
||||||
onClick={() => handleCheckOut(user)}
|
onClick={() => handleCheckOut(user)}
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { useState } from 'react';
|
|||||||
import UserTable from './TableUser';
|
import UserTable from './TableUser';
|
||||||
|
|
||||||
// Styled
|
// Styled
|
||||||
import Button from '../../commons/styles/Button';
|
import Button from '@commonStyle/Button';
|
||||||
import Direction from '../../commons/styles/Direction';
|
import Direction from '@commonStyle/Direction.ts';
|
||||||
import { StyledUser, Title } from './styled';
|
import { StyledUser, Title } from './styled';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import Modal from '../../components/Modal';
|
import Modal from '@component/Modal';
|
||||||
import UserForm from './FormUser';
|
import UserForm from './FormUser';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
import { FORM } from '@constant/commons';
|
||||||
|
|
||||||
const User = () => {
|
const User = () => {
|
||||||
|
const ADD_ROOM = 'Add room';
|
||||||
const [reload, setReload] = useState(true);
|
const [reload, setReload] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -22,10 +26,13 @@ const User = () => {
|
|||||||
<Title>List User</Title>
|
<Title>List User</Title>
|
||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Modal.Open modalName="user-form">
|
<Modal.Open
|
||||||
<Button>Add user</Button>
|
modalName={FORM.USER}
|
||||||
</Modal.Open>
|
renderChildren={(onCloseModal) => (
|
||||||
<Modal.Window name="user-form" title="Add form">
|
<Button onClick={onCloseModal}>Add user</Button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Modal.Window name={FORM.USER} title={ADD_ROOM}>
|
||||||
<UserForm setReload={setReload} reload={reload} />
|
<UserForm setReload={setReload} reload={reload} />
|
||||||
</Modal.Window>
|
</Modal.Window>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../types/common';
|
import { Nullable } from '@type/common';
|
||||||
import { IResponse } from '../types/responses';
|
import { IResponse } from '@type/responses';
|
||||||
import { IRoom } from '../types/rooms';
|
import { IRoom } from '@type/rooms';
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import { sendRequest } from '../helpers/sendRequest';
|
import { sendRequest } from '@helper/sendRequest';
|
||||||
import { errorMsg } from '../helpers/helper';
|
import { errorMsg } from '@helper/helper';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { STATUS_CODE, RESPONSE_MESSAGE } from '../constants/responseStatus';
|
import { STATUS_CODE, RESPONSE_MESSAGE } from '@constant/responseStatus';
|
||||||
import { ROOM_PATH } from '../constants/path';
|
import { ROOM_PATH } from '@constant/path';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all rooms from server
|
* Get all rooms from server
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import { USER_PATH } from '../constants/path';
|
import { USER_PATH } from '@constant/path';
|
||||||
import { STATUS_CODE } from '../constants/responseStatus';
|
import { STATUS_CODE } from '@constant/responseStatus';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Nullable } from '../types/common';
|
import { Nullable } from '@type/common';
|
||||||
import { IResponse } from '../types/responses';
|
import { IResponse } from '@type/responses';
|
||||||
import { IUser } from '../types/users';
|
import { IUser } from '@type/users';
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
import { sendRequest } from '../helpers/sendRequest';
|
import { sendRequest } from '@helper/sendRequest';
|
||||||
import { errorMsg } from '../helpers/helper';
|
import { errorMsg } from '@helper/helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create user to the server
|
* Create user to the server
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"types": ["node"],
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
@@ -18,7 +19,20 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": 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"],
|
"include": ["src"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tsconfigPaths()],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user