Merge pull request #21 from Nez27/feat/optimized-page

Fix UI and logic, refactor and optimized code
This commit is contained in:
Loi Phan
2023-11-02 11:53:07 +07:00
committed by GitHub
51 changed files with 415 additions and 299 deletions
+4
View File
@@ -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.
@@ -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,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;
@@ -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,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 };
@@ -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 };
@@ -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,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 };
@@ -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,
};
+1 -1
View File
@@ -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';
+13 -1
View File
@@ -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,
};
+6 -6
View File
@@ -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;
};
+21 -9
View File
@@ -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 };
}
+11 -10
View File
@@ -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,
};
+8 -8
View File
@@ -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 };
+5 -1
View File
@@ -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 = '';
+22 -10
View File
@@ -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}>
+8 -8
View File
@@ -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 (
<>
+49 -7
View File
@@ -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}>
+8 -8
View File
@@ -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;
}