mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 20:01:59 +00:00
Merge pull request #21 from Nez27/feat/optimized-page
Fix UI and logic, refactor and optimized code
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://example.com/sitemap.xml
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,33 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
// Components
|
||||
import Header from './Header';
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
const StyledAppLayout = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const Main = styled.main`
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
const AppLayout = () => {
|
||||
return (
|
||||
<StyledAppLayout>
|
||||
<Header />
|
||||
<Sidebar />
|
||||
<Main>
|
||||
<Outlet />
|
||||
</Main>
|
||||
</StyledAppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
// Components
|
||||
import Header from '../Header';
|
||||
import Sidebar from '../Sidebar';
|
||||
|
||||
// Styled
|
||||
import { Main, StyledAppLayout } from './styled';
|
||||
|
||||
const AppLayout = () => {
|
||||
return (
|
||||
<StyledAppLayout>
|
||||
<Header />
|
||||
<Sidebar />
|
||||
<Main>
|
||||
<Outlet />
|
||||
</Main>
|
||||
</StyledAppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledAppLayout = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const Main = styled.main`
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
export { StyledAppLayout, Main };
|
||||
@@ -1,3 +1,4 @@
|
||||
// Styled
|
||||
import { StyledActionBtn, StyledForm } from './styled';
|
||||
|
||||
interface IFormProps {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Styled
|
||||
import { Error, Label, StyledFormRow } from './styled';
|
||||
|
||||
interface IFormRow {
|
||||
|
||||
@@ -16,6 +16,8 @@ const Error = styled.p`
|
||||
color: var(--error-text);
|
||||
font-size: var(--fs-sm-2x);
|
||||
margin-top: 5px;
|
||||
padding-inline: 5px;
|
||||
width: 220px;
|
||||
`;
|
||||
|
||||
export { StyledFormRow, Label, Error };
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Components
|
||||
import HeaderMenu from '../HeaderMenu';
|
||||
|
||||
// Styled
|
||||
import { StyledHeader } from './styled';
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<StyledHeader>
|
||||
<p>Hi, Admin!</p>
|
||||
<HeaderMenu />
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
+1
-13
@@ -1,8 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Components
|
||||
import HeaderMenu from './HeaderMenu';
|
||||
|
||||
const StyledHeader = styled.header`
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 20px 40px;
|
||||
@@ -13,13 +10,4 @@ const StyledHeader = styled.header`
|
||||
gap: 30px;
|
||||
`;
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<StyledHeader>
|
||||
<p>Hi, Admin!</p>
|
||||
<HeaderMenu />
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export { StyledHeader };
|
||||
@@ -1,26 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Components
|
||||
import { IoPersonCircleOutline } from 'react-icons/io5';
|
||||
import { HiOutlineLogout } from 'react-icons/hi';
|
||||
import ButtonIcon from '../commons/styles/ButtonIcon';
|
||||
|
||||
const StyledHeaderMenu = styled.ul`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
const HeaderMenu = () => {
|
||||
return (
|
||||
<StyledHeaderMenu>
|
||||
<ButtonIcon>
|
||||
<IoPersonCircleOutline />
|
||||
</ButtonIcon>
|
||||
<ButtonIcon>
|
||||
<HiOutlineLogout />
|
||||
</ButtonIcon>
|
||||
</StyledHeaderMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderMenu;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Components
|
||||
import { IoPersonCircleOutline } from 'react-icons/io5';
|
||||
import { HiOutlineLogout } from 'react-icons/hi';
|
||||
import ButtonIcon from '../../commons/styles/ButtonIcon';
|
||||
|
||||
// Styled
|
||||
import { StyledHeaderMenu } from './styled';
|
||||
|
||||
const HeaderMenu = () => {
|
||||
return (
|
||||
<StyledHeaderMenu>
|
||||
<li>
|
||||
<ButtonIcon aria-label="Profile">
|
||||
<IoPersonCircleOutline />
|
||||
</ButtonIcon>
|
||||
</li>
|
||||
<li>
|
||||
<ButtonIcon aria-label="Logout">
|
||||
<HiOutlineLogout />
|
||||
</ButtonIcon>
|
||||
</li>
|
||||
</StyledHeaderMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderMenu;
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledHeaderMenu = styled.ul`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
export { StyledHeaderMenu };
|
||||
@@ -37,7 +37,7 @@ const Toggle = ({ id }: { id: string }): React.JSX.Element => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledToggle onClick={handleClick}>
|
||||
<StyledToggle onClick={handleClick} aria-label={`Menu item ${id}`}>
|
||||
<HiEllipsisVertical />
|
||||
</StyledToggle>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Styled
|
||||
import { StyledMessage } from './styled';
|
||||
|
||||
const Message = ({ children }: { children: string }) => {
|
||||
return <StyledMessage>{children}</StyledMessage>;
|
||||
};
|
||||
|
||||
export default Message;
|
||||
+1
-5
@@ -6,8 +6,4 @@ const StyledMessage = styled.p`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const Message = ({ children }: { children: string }) => {
|
||||
return <StyledMessage>{children}</StyledMessage>;
|
||||
};
|
||||
|
||||
export default Message;
|
||||
export { StyledMessage };
|
||||
@@ -0,0 +1,28 @@
|
||||
// Components
|
||||
import { MdSpaceDashboard } from 'react-icons/md';
|
||||
import { HiUsers } from 'react-icons/hi2';
|
||||
import { BsHouseDoorFill } from 'react-icons/bs';
|
||||
|
||||
// Styled
|
||||
import { StyledNav, StyledNavLink } from './styled';
|
||||
|
||||
const Nav = () => {
|
||||
return (
|
||||
<StyledNav>
|
||||
<StyledNavLink to={'/dashboard'}>
|
||||
<MdSpaceDashboard />
|
||||
<span>Dashboard</span>
|
||||
</StyledNavLink>
|
||||
<StyledNavLink to={'/user'}>
|
||||
<HiUsers />
|
||||
<span>User</span>
|
||||
</StyledNavLink>
|
||||
<StyledNavLink to={'/room'}>
|
||||
<BsHouseDoorFill />
|
||||
<span>Room</span>
|
||||
</StyledNavLink>
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
+1
-25
@@ -1,11 +1,6 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Components
|
||||
import { MdSpaceDashboard } from 'react-icons/md';
|
||||
import { HiUsers } from 'react-icons/hi2';
|
||||
import { BsHouseDoorFill } from 'react-icons/bs';
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
margin-top: 70px;
|
||||
display: flex;
|
||||
@@ -38,23 +33,4 @@ const StyledNavLink = styled(NavLink)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Nav = () => {
|
||||
return (
|
||||
<StyledNav>
|
||||
<StyledNavLink to={'/dashboard'}>
|
||||
<MdSpaceDashboard />
|
||||
<span>Dashboard</span>
|
||||
</StyledNavLink>
|
||||
<StyledNavLink to={'/user'}>
|
||||
<HiUsers />
|
||||
<span>User</span>
|
||||
</StyledNavLink>
|
||||
<StyledNavLink to={'/room'}>
|
||||
<BsHouseDoorFill />
|
||||
<span>Room</span>
|
||||
</StyledNavLink>
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
export { StyledNav, StyledNavLink };
|
||||
+5
-35
@@ -1,38 +1,8 @@
|
||||
import { memo } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const StyledOrder = styled.div`
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
interface IOrderBtn {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const OrderButton = styled.button<IOrderBtn>`
|
||||
border: none;
|
||||
|
||||
${(props) =>
|
||||
props.active &&
|
||||
css`
|
||||
background-color: var(--primary-color);
|
||||
color: var(--light-text);
|
||||
`}
|
||||
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
font-size: var(--fs-sm-x);
|
||||
padding: 5px 10px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--hover-dark-background-color);
|
||||
}
|
||||
`;
|
||||
// Styled
|
||||
import { OrderButton, StyledOrder } from './styled';
|
||||
|
||||
interface IOrderProps {
|
||||
options: {
|
||||
@@ -41,7 +11,7 @@ interface IOrderProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
const OrderBy = ({ options }: IOrderProps) => {
|
||||
const OrderBy = memo(({ options }: IOrderProps) => {
|
||||
const field = 'orderBy';
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -67,6 +37,6 @@ const OrderBy = ({ options }: IOrderProps) => {
|
||||
))}
|
||||
</StyledOrder>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default OrderBy;
|
||||
@@ -0,0 +1,38 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
const StyledOrder = styled.div`
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
interface IOrderBtn {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const OrderButton = styled.button<IOrderBtn>`
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
${(props) =>
|
||||
props.active &&
|
||||
css`
|
||||
background-color: var(--primary-color);
|
||||
color: var(--light-text);
|
||||
cursor: no-drop;
|
||||
`}
|
||||
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
font-size: var(--fs-sm-x);
|
||||
padding: 5px 10px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--hover-dark-background-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export { StyledOrder, OrderButton };
|
||||
+5
-12
@@ -1,19 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledSearch = styled.input`
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: var(--fs-sm-x);
|
||||
padding: 5px 10px;
|
||||
`;
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { StyledSearch } from './styled';
|
||||
|
||||
interface ISearch {
|
||||
setPlaceHolder: string;
|
||||
setValueSearch: React.Dispatch<React.SetStateAction<string>>;
|
||||
setValueSearch: (phone: string) => void;
|
||||
}
|
||||
|
||||
const Search = ({ setValueSearch, setPlaceHolder }: ISearch) => {
|
||||
const Search = memo(({ setValueSearch, setPlaceHolder }: ISearch) => {
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,6 +23,6 @@ const Search = ({ setValueSearch, setPlaceHolder }: ISearch) => {
|
||||
placeholder={setPlaceHolder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default Search;
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledSearch = styled.input`
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: var(--fs-sm-x);
|
||||
padding: 5px 10px;
|
||||
`;
|
||||
|
||||
export { StyledSearch };
|
||||
@@ -1,32 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface ISelect {
|
||||
options: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
value: string;
|
||||
onChange: React.ChangeEventHandler<HTMLSelectElement>;
|
||||
}
|
||||
|
||||
const StyledSelect = styled.select`
|
||||
font-size: var(--fs-sm-x);
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const Select = ({ options, value, onChange }: ISelect) => {
|
||||
return (
|
||||
<StyledSelect value={value} onChange={onChange}>
|
||||
{options.map((option) => (
|
||||
<option value={option.value} key={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ISelectOptions } from '../../globals/interfaces';
|
||||
import { StyledSelect } from './styled';
|
||||
|
||||
interface ISelect {
|
||||
options: ISelectOptions[];
|
||||
value: string;
|
||||
onChange: React.ChangeEventHandler<HTMLSelectElement>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const Select = ({ options, value, onChange, name }: ISelect) => {
|
||||
if (!options) return;
|
||||
|
||||
return (
|
||||
<StyledSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
aria-label="Sort"
|
||||
name={name}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option value={option.value} key={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledSelect = styled.select`
|
||||
font-size: var(--fs-sm-x);
|
||||
padding: 5px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export { StyledSelect };
|
||||
@@ -0,0 +1,16 @@
|
||||
// Components
|
||||
import Nav from '../Nav';
|
||||
|
||||
// Styled
|
||||
import { Heading, StyledSidebar } from './styled';
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<StyledSidebar>
|
||||
<Heading>Hotel Management</Heading>
|
||||
<Nav />
|
||||
</StyledSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
+1
-13
@@ -1,8 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Components
|
||||
import Nav from './Nav';
|
||||
|
||||
const StyledSidebar = styled.aside`
|
||||
padding: 50px 20px;
|
||||
|
||||
@@ -17,13 +14,4 @@ const Heading = styled.h1`
|
||||
color: var(--primary-color);
|
||||
`;
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<StyledSidebar>
|
||||
<Heading>Hotel Management</Heading>
|
||||
<Nav />
|
||||
</StyledSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
export { StyledSidebar, Heading };
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
// Components
|
||||
import Select from './Select';
|
||||
import Select from '../Select';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ISortByProps {
|
||||
options: {
|
||||
@@ -10,7 +11,7 @@ interface ISortByProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
const SortBy = ({ options }: ISortByProps) => {
|
||||
const SortBy = memo(({ options }: ISortByProps) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const sortBy = searchParams.get('sortBy') || '';
|
||||
|
||||
@@ -27,6 +28,6 @@ const SortBy = ({ options }: ISortByProps) => {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default SortBy;
|
||||
@@ -11,6 +11,8 @@ const EDIT_SUCCESS = 'Edit success';
|
||||
const CONFIRM_DELETE = 'Are you sure to delete it?';
|
||||
const DELETE_SUCCESS = 'Delete success';
|
||||
const REQUIRED_FIELD_ERROR = 'This is required field';
|
||||
const DISCOUNT_FIELD_ERROR =
|
||||
'Discount should be greater than 0 and less than 100';
|
||||
|
||||
export {
|
||||
invalidFormatMsg,
|
||||
@@ -20,4 +22,5 @@ export {
|
||||
CONFIRM_DELETE,
|
||||
DELETE_SUCCESS,
|
||||
REQUIRED_FIELD_ERROR,
|
||||
DISCOUNT_FIELD_ERROR,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,6 @@ export const DASHBOARD: string = '/dashboard';
|
||||
export const USER: string = '/user';
|
||||
export const ROOM: string = '/room';
|
||||
export const OTHER_PATH: string = '*';
|
||||
export const BASE_URL: string = 'https://hotel-management-api.loiphan.com/';
|
||||
export const BASE_URL: string = 'http://localhost:3000/';
|
||||
export const USER_PATH = 'users';
|
||||
export const ROOM_PATH = 'rooms';
|
||||
|
||||
@@ -20,6 +20,11 @@ interface ITableBody<T> {
|
||||
render?: (value: T) => JSX.Element;
|
||||
}
|
||||
|
||||
interface ISelectOptions {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface IDialogProps<T> {
|
||||
title?: string;
|
||||
children?: JSX.Element[] | JSX.Element;
|
||||
@@ -31,4 +36,11 @@ interface IDialogProps<T> {
|
||||
isAdd?: boolean;
|
||||
}
|
||||
|
||||
export type { IMenusContext, IButton, ITable, ITableBody, IDialogProps };
|
||||
export type {
|
||||
IMenusContext,
|
||||
IButton,
|
||||
ITable,
|
||||
ITableBody,
|
||||
IDialogProps,
|
||||
ISelectOptions,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
type TUser = {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
identifiedCode: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
roomId: string;
|
||||
roomId: number;
|
||||
};
|
||||
|
||||
type TRoom = {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
amount: string;
|
||||
price: string;
|
||||
discount: string;
|
||||
amount: number;
|
||||
price: number;
|
||||
discount: number;
|
||||
description: string;
|
||||
status: boolean;
|
||||
};
|
||||
|
||||
@@ -56,7 +56,8 @@ const getPropValues = (stateSchema: TStateSchema, prop?: TPropValues) => {
|
||||
|
||||
type TValidator = {
|
||||
validatorFunc: (value: string) => boolean;
|
||||
prop: string;
|
||||
prop?: string;
|
||||
customErrorMsg?: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
@@ -65,12 +66,20 @@ type TValidator = {
|
||||
* @param param0 Pass TValidator object
|
||||
* @returns An object contains condition validator
|
||||
*/
|
||||
const addValidator = ({ validatorFunc, prop, required = true }: TValidator) => {
|
||||
const addValidator = ({
|
||||
validatorFunc,
|
||||
prop = '',
|
||||
customErrorMsg = '',
|
||||
required = true,
|
||||
}: TValidator) => {
|
||||
return {
|
||||
required,
|
||||
validator: {
|
||||
func: validatorFunc,
|
||||
error: invalidFormatMsg(prop),
|
||||
// prettier-ignore
|
||||
error: customErrorMsg
|
||||
? customErrorMsg
|
||||
: invalidFormatMsg(prop),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -86,12 +95,15 @@ const getValueFromObj = <T>(obj: T | null = null): TKeyString => {
|
||||
if (obj) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
const tempValue = obj[key as keyof typeof obj];
|
||||
const value: string | boolean | number =
|
||||
typeof tempValue === 'boolean' ||
|
||||
typeof tempValue === 'string' ||
|
||||
typeof tempValue === 'number'
|
||||
? tempValue
|
||||
: '';
|
||||
let value: string | boolean = '';
|
||||
|
||||
if (typeof tempValue === 'string' || typeof tempValue === 'boolean') {
|
||||
value = tempValue;
|
||||
}
|
||||
|
||||
if (typeof tempValue === 'number') {
|
||||
value = tempValue.toString();
|
||||
}
|
||||
|
||||
result = { ...result, [`${key}Value`]: value };
|
||||
}
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
/**
|
||||
* 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
|
||||
* @param value Value need to be checked
|
||||
@@ -16,6 +7,16 @@ const isValidNumber = (value: string): boolean => {
|
||||
return /^[0-9]*$/.test(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check value is valid discount or not
|
||||
* @param value Value need to be checked
|
||||
* @returns A boolean indicating whether or not the argument has valid
|
||||
*/
|
||||
const isValidDiscount = (value: string): boolean => {
|
||||
console.log(Boolean(+value >= 0 && +value <= 100));
|
||||
return +value >= 0 && +value <= 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check value is valid phone number or not
|
||||
* @param value Value need to be checked
|
||||
@@ -41,9 +42,9 @@ const isValidString = (value: string): boolean => {
|
||||
const skipCheck = () => true;
|
||||
|
||||
export {
|
||||
isValidName,
|
||||
isValidNumber,
|
||||
isValidPhoneNumber,
|
||||
isValidString,
|
||||
skipCheck,
|
||||
isValidDiscount,
|
||||
};
|
||||
|
||||
@@ -19,15 +19,15 @@ import { searchQuery } from '../helpers/utils';
|
||||
*/
|
||||
const useFetch = (
|
||||
path: string,
|
||||
columnSearch: string,
|
||||
keyWord: string,
|
||||
tempSortBy: string,
|
||||
tempOrderBy: string,
|
||||
columnSearch: string = '',
|
||||
keyWord: string = '',
|
||||
tempSortBy?: string,
|
||||
tempOrderBy?: string,
|
||||
reload?: boolean,
|
||||
) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [errorFetchMsg, setErrorFetchMsg] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear data
|
||||
@@ -61,16 +61,16 @@ const useFetch = (
|
||||
|
||||
setIsPending(false);
|
||||
setData(json);
|
||||
setErrorMsg(null);
|
||||
setErrorFetchMsg(null);
|
||||
} catch (error) {
|
||||
setErrorMsg(`Could not fetch data.\n ${error}`);
|
||||
setErrorFetchMsg(`Could not fetch data.\n ${error}`);
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [path, reload, columnSearch, keyWord, tempOrderBy, tempSortBy]);
|
||||
return { data, isPending, errorMsg };
|
||||
return { data, isPending, errorFetchMsg };
|
||||
};
|
||||
|
||||
export { useFetch };
|
||||
|
||||
@@ -110,7 +110,11 @@ const useForm = (
|
||||
|
||||
// Event handler for handling changes in input.
|
||||
const handleOnChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
(
|
||||
event: ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
>,
|
||||
) => {
|
||||
setIsDirty(true);
|
||||
|
||||
let error = '';
|
||||
|
||||
@@ -24,6 +24,7 @@ import useForm from '../../hooks/useForm.ts';
|
||||
|
||||
// Utils
|
||||
import {
|
||||
isValidDiscount,
|
||||
isValidNumber,
|
||||
isValidString,
|
||||
skipCheck,
|
||||
@@ -35,6 +36,7 @@ import { sendRequest } from '../../helpers/sendRequest.ts';
|
||||
import { STATUS_CODE } from '../../constants/statusCode.ts';
|
||||
import {
|
||||
ADD_SUCCESS,
|
||||
DISCOUNT_FIELD_ERROR,
|
||||
EDIT_SUCCESS,
|
||||
errorMsg,
|
||||
} from '../../constants/messages.ts';
|
||||
@@ -46,6 +48,7 @@ const FormBtn = styled(Button)`
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
background-color: var(--disabled-btn-color);
|
||||
cursor: no-drop;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -75,7 +78,7 @@ const RoomForm = ({
|
||||
const initialValue: string = isAdd
|
||||
? ''
|
||||
: room!
|
||||
&& room.id;
|
||||
&& room.id.toString();
|
||||
|
||||
const {
|
||||
idValue,
|
||||
@@ -134,8 +137,8 @@ const RoomForm = ({
|
||||
prop: 'price',
|
||||
}),
|
||||
discount: addValidator({
|
||||
validatorFunc: isValidNumber,
|
||||
prop: 'discount'
|
||||
validatorFunc: isValidDiscount,
|
||||
customErrorMsg: DISCOUNT_FIELD_ERROR
|
||||
}),
|
||||
status: addValidator({
|
||||
validatorFunc: skipCheck,
|
||||
@@ -150,12 +153,23 @@ const RoomForm = ({
|
||||
|
||||
// Submit form
|
||||
const onSubmitForm = async (state: TKeyValue) => {
|
||||
// Convert to room type
|
||||
const data: TRoom = {
|
||||
id: +state.id!,
|
||||
name: '' + state.name,
|
||||
amount: +state.amount!,
|
||||
discount: +state.discount!,
|
||||
price: +state.price!,
|
||||
status: !!state.status,
|
||||
description: '' + state.description,
|
||||
};
|
||||
|
||||
try {
|
||||
if (isAdd) {
|
||||
// Add request
|
||||
const response = await sendRequest(
|
||||
ROOM_PATH,
|
||||
JSON.stringify(state),
|
||||
JSON.stringify(data),
|
||||
'POST',
|
||||
);
|
||||
|
||||
@@ -170,7 +184,7 @@ const RoomForm = ({
|
||||
// Edit request
|
||||
const response = await sendRequest(
|
||||
ROOM_PATH + `/${room!.id}`,
|
||||
JSON.stringify(state),
|
||||
JSON.stringify(data),
|
||||
'PUT',
|
||||
);
|
||||
|
||||
@@ -197,8 +211,6 @@ const RoomForm = ({
|
||||
onResetForm();
|
||||
};
|
||||
|
||||
console.log(initialValue);
|
||||
|
||||
// prettier-ignore
|
||||
const {
|
||||
values,
|
||||
@@ -296,8 +308,8 @@ const RoomForm = ({
|
||||
label="Discount"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.roomId && dirty.roomId
|
||||
? (errors.roomId as string)
|
||||
errors.discount && dirty.discount
|
||||
? (errors.discount as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
@@ -341,7 +353,7 @@ const RoomForm = ({
|
||||
// prettier-ignore
|
||||
isAdd
|
||||
? 'Add'
|
||||
: 'Edit'
|
||||
: 'Save'
|
||||
}
|
||||
</FormBtn>
|
||||
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
|
||||
|
||||
@@ -71,8 +71,8 @@ const RoomRow = ({
|
||||
|
||||
// prettier-ignore
|
||||
const statusText = status
|
||||
? 'Valid'
|
||||
: 'Invalid';
|
||||
? 'Invalid'
|
||||
: 'Valid';
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
@@ -84,9 +84,9 @@ const RoomRow = ({
|
||||
<div>{statusText}</div>
|
||||
|
||||
<Menus.Menu>
|
||||
<Menus.Toggle id={id} />
|
||||
<Menus.Toggle id={id.toString()} />
|
||||
|
||||
<Menus.List id={id}>
|
||||
<Menus.List id={id.toString()}>
|
||||
<Menus.Button
|
||||
icon={<HiSquare2Stack />}
|
||||
onClick={() => handleOnEdit(room)}
|
||||
@@ -125,7 +125,7 @@ const RoomTable = ({
|
||||
? searchParams.get('orderBy')!
|
||||
: '';
|
||||
|
||||
const { data, isPending, errorMsg } = useFetch(
|
||||
const { data, isPending, errorFetchMsg } = useFetch(
|
||||
'rooms',
|
||||
'name',
|
||||
nameSearch,
|
||||
@@ -141,10 +141,10 @@ const RoomTable = ({
|
||||
setRooms([]);
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
console.error(errorMsg);
|
||||
if (errorFetchMsg) {
|
||||
console.error(errorFetchMsg);
|
||||
}
|
||||
}, [data, errorMsg]);
|
||||
}, [data, errorFetchMsg]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
isValidString,
|
||||
isValidNumber,
|
||||
isValidPhoneNumber,
|
||||
isValidName,
|
||||
} from '../../helpers/validators';
|
||||
import { addValidator, getValueFromObj } from '../../helpers/utils.ts';
|
||||
import { sendRequest } from '../../helpers/sendRequest.ts';
|
||||
@@ -40,6 +39,11 @@ import {
|
||||
errorMsg,
|
||||
} from '../../constants/messages.ts';
|
||||
import { USER_PATH } from '../../constants/path.ts';
|
||||
import Select from '../../components/Select/index.tsx';
|
||||
import { useFetch } from '../../hooks/useFetch.ts';
|
||||
|
||||
// Interfaces
|
||||
import { ISelectOptions } from '../../globals/interfaces.ts';
|
||||
|
||||
const FormBtn = styled(Button)`
|
||||
width: 100%;
|
||||
@@ -47,6 +51,7 @@ const FormBtn = styled(Button)`
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
background-color: var(--disabled-btn-color);
|
||||
cursor: no-drop;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -66,6 +71,27 @@ const UserForm = ({
|
||||
isAdd,
|
||||
}: IUserFormProp) => {
|
||||
const [reset, setReset] = useState(true);
|
||||
const [options, setOptions] = useState<ISelectOptions[]>();
|
||||
const { data, errorFetchMsg } = useFetch('rooms');
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const tempData = data as TKeyValue[];
|
||||
const tempOptions: ISelectOptions[] = [];
|
||||
tempData.forEach((item) => {
|
||||
tempOptions.push({
|
||||
label: item.name! as string,
|
||||
value: item.id! as string,
|
||||
});
|
||||
});
|
||||
|
||||
setOptions(tempOptions);
|
||||
}
|
||||
|
||||
if (errorFetchMsg) {
|
||||
toast.error(errorFetchMsg);
|
||||
}
|
||||
}, [data, errorFetchMsg]);
|
||||
|
||||
if (isAdd) {
|
||||
// If this is add form, reset value
|
||||
@@ -76,7 +102,7 @@ const UserForm = ({
|
||||
const initialValue: string = isAdd
|
||||
? ''
|
||||
: user!
|
||||
&& user.id;
|
||||
&& user.id.toLocaleString();
|
||||
|
||||
const {
|
||||
idValue,
|
||||
@@ -116,7 +142,7 @@ const UserForm = ({
|
||||
// prettier-ignore
|
||||
const stateValidatorSchema: TValidator = {
|
||||
name: addValidator({
|
||||
validatorFunc: isValidName,
|
||||
validatorFunc: isValidString,
|
||||
prop: 'full name'
|
||||
}),
|
||||
identifiedCode: addValidator({
|
||||
@@ -139,12 +165,22 @@ const UserForm = ({
|
||||
|
||||
// Submit form
|
||||
const onSubmitForm = async (state: TKeyValue) => {
|
||||
// Convert to user type
|
||||
const data: TUser = {
|
||||
id: +state.id!,
|
||||
name: '' + state.name,
|
||||
identifiedCode: '' + state.identifiedCode,
|
||||
phone: '' + state.phone,
|
||||
roomId: +state.roomId!,
|
||||
address: '' + state.address,
|
||||
};
|
||||
|
||||
try {
|
||||
if (isAdd) {
|
||||
// Add request
|
||||
const response = await sendRequest(
|
||||
USER_PATH,
|
||||
JSON.stringify(state),
|
||||
JSON.stringify(data),
|
||||
'POST',
|
||||
);
|
||||
|
||||
@@ -159,7 +195,7 @@ const UserForm = ({
|
||||
// Edit request
|
||||
const response = await sendRequest(
|
||||
USER_PATH + `/${user!.id}`,
|
||||
JSON.stringify(state),
|
||||
JSON.stringify(data),
|
||||
'PUT',
|
||||
);
|
||||
|
||||
@@ -285,11 +321,17 @@ const UserForm = ({
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
{/* <Input
|
||||
type="text"
|
||||
name="roomId"
|
||||
value={roomId as string}
|
||||
onChange={handleOnChange}
|
||||
/> */}
|
||||
<Select
|
||||
name="roomId"
|
||||
value={roomId as string}
|
||||
onChange={handleOnChange}
|
||||
options={options!}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
@@ -316,7 +358,7 @@ const UserForm = ({
|
||||
// prettier-ignore
|
||||
isAdd
|
||||
? 'Add'
|
||||
: 'Edit'
|
||||
: 'Save'
|
||||
}
|
||||
</FormBtn>
|
||||
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
|
||||
|
||||
@@ -22,13 +22,13 @@ import { useFetch } from '../../hooks/useFetch';
|
||||
import { USER_PATH } from '../../constants/path';
|
||||
import { STATUS_CODE } from '../../constants/statusCode';
|
||||
import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages';
|
||||
import { USER_PAGE } from '../../constants/variables';
|
||||
|
||||
// Styled
|
||||
import Spinner from '../../commons/styles/Spinner';
|
||||
|
||||
// Utils
|
||||
import { sendRequest } from '../../helpers/sendRequest';
|
||||
import { USER_PAGE } from '../../constants/variables';
|
||||
|
||||
interface IUserRow {
|
||||
user: TUser;
|
||||
@@ -78,9 +78,9 @@ const UserRow = ({
|
||||
<div>{roomId}</div>
|
||||
|
||||
<Menus.Menu>
|
||||
<Menus.Toggle id={id} />
|
||||
<Menus.Toggle id={id.toString()} />
|
||||
|
||||
<Menus.List id={id}>
|
||||
<Menus.List id={id.toString()}>
|
||||
<Menus.Button
|
||||
icon={<HiSquare2Stack />}
|
||||
onClick={() => handleOnEdit(user)}
|
||||
@@ -119,7 +119,7 @@ const UserTable = ({
|
||||
? searchParams.get('orderBy')!
|
||||
: '';
|
||||
|
||||
const { data, isPending, errorMsg } = useFetch(
|
||||
const { data, isPending, errorFetchMsg } = useFetch(
|
||||
'users',
|
||||
'phone',
|
||||
phoneSearch,
|
||||
@@ -135,10 +135,10 @@ const UserTable = ({
|
||||
setUsers([]);
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
console.error(errorMsg);
|
||||
if (errorFetchMsg) {
|
||||
console.error(errorFetchMsg);
|
||||
}
|
||||
}, [data, errorMsg]);
|
||||
}, [data, errorFetchMsg]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -163,7 +163,7 @@ const UserTable = ({
|
||||
<div>Name</div>
|
||||
<div>Identified Code</div>
|
||||
<div>Phone</div>
|
||||
<div>Room</div>
|
||||
<div>Room Id</div>
|
||||
</Table.Header>
|
||||
<Table.Body<TUser>
|
||||
data={users}
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../../assets/fonts/Poppins-Regular.woff2') format('woff2'),
|
||||
url('../../assets/fonts/Poppins-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../../assets/fonts/Poppins-Medium.woff2') format('woff2'),
|
||||
url('../../assets/fonts/Poppins-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../../assets/fonts/Poppins-SemiBold.woff2') format('woff2'),
|
||||
url('../../assets/fonts/Poppins-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../../assets/fonts/Poppins-Bold.woff2') format('woff2'),
|
||||
url('../../assets/fonts/Poppins-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-family: 'Cabin';
|
||||
src: url('../../assets/fonts/Cabin.woff2') format('woff2'),
|
||||
url('../../assets/fonts/Cabin.ttf') format('truetype');
|
||||
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Poppins', Times, serif;
|
||||
font-family: 'Cabin', Times, serif;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user