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'; import { StyledActionBtn, StyledForm } from './styled';
interface IFormProps { interface IFormProps {
@@ -1,3 +1,4 @@
// Styled
import { Error, Label, StyledFormRow } from './styled'; import { Error, Label, StyledFormRow } from './styled';
interface IFormRow { interface IFormRow {
@@ -16,6 +16,8 @@ const Error = styled.p`
color: var(--error-text); color: var(--error-text);
font-size: var(--fs-sm-2x); font-size: var(--fs-sm-2x);
margin-top: 5px; margin-top: 5px;
padding-inline: 5px;
width: 220px;
`; `;
export { StyledFormRow, Label, Error }; 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'; import styled from 'styled-components';
// Components
import HeaderMenu from './HeaderMenu';
const StyledHeader = styled.header` const StyledHeader = styled.header`
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 20px 40px; padding: 20px 40px;
@@ -13,13 +10,4 @@ const StyledHeader = styled.header`
gap: 30px; gap: 30px;
`; `;
const Header = () => { export { StyledHeader };
return (
<StyledHeader>
<p>Hi, Admin!</p>
<HeaderMenu />
</StyledHeader>
);
};
export default Header;
@@ -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 ( return (
<StyledToggle onClick={handleClick}> <StyledToggle onClick={handleClick} aria-label={`Menu item ${id}`}>
<HiEllipsisVertical /> <HiEllipsisVertical />
</StyledToggle> </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; padding: 20px;
`; `;
const Message = ({ children }: { children: string }) => { export { StyledMessage };
return <StyledMessage>{children}</StyledMessage>;
};
export default Message;
@@ -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 { NavLink } from 'react-router-dom';
import styled from 'styled-components'; 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` const StyledNav = styled.nav`
margin-top: 70px; margin-top: 70px;
display: flex; display: flex;
@@ -38,23 +33,4 @@ const StyledNavLink = styled(NavLink)`
} }
`; `;
const Nav = () => { export { StyledNav, StyledNavLink };
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,38 +1,8 @@
import { memo } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import styled, { css } from 'styled-components';
const StyledOrder = styled.div` // Styled
border: 1px solid var(--border-color); import { OrderButton, StyledOrder } from './styled';
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);
}
`;
interface IOrderProps { interface IOrderProps {
options: { options: {
@@ -41,7 +11,7 @@ interface IOrderProps {
}[]; }[];
} }
const OrderBy = ({ options }: IOrderProps) => { const OrderBy = memo(({ options }: IOrderProps) => {
const field = 'orderBy'; const field = 'orderBy';
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -67,6 +37,6 @@ const OrderBy = ({ options }: IOrderProps) => {
))} ))}
</StyledOrder> </StyledOrder>
); );
}; });
export default OrderBy; 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 { memo, useEffect, useState } from 'react';
import styled from 'styled-components'; import { StyledSearch } from './styled';
const StyledSearch = styled.input`
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
font-size: var(--fs-sm-x);
padding: 5px 10px;
`;
interface ISearch { interface ISearch {
setPlaceHolder: string; 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(''); const [query, setQuery] = useState('');
useEffect(() => { useEffect(() => {
@@ -30,6 +23,6 @@ const Search = ({ setValueSearch, setPlaceHolder }: ISearch) => {
placeholder={setPlaceHolder} placeholder={setPlaceHolder}
/> />
); );
}; });
export default Search; 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'; import styled from 'styled-components';
// Components
import Nav from './Nav';
const StyledSidebar = styled.aside` const StyledSidebar = styled.aside`
padding: 50px 20px; padding: 50px 20px;
@@ -17,13 +14,4 @@ const Heading = styled.h1`
color: var(--primary-color); color: var(--primary-color);
`; `;
const Sidebar = () => { export { StyledSidebar, Heading };
return (
<StyledSidebar>
<Heading>Hotel Management</Heading>
<Nav />
</StyledSidebar>
);
};
export default Sidebar;
@@ -1,7 +1,8 @@
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
// Components // Components
import Select from './Select'; import Select from '../Select';
import { memo } from 'react';
interface ISortByProps { interface ISortByProps {
options: { options: {
@@ -10,7 +11,7 @@ interface ISortByProps {
}[]; }[];
} }
const SortBy = ({ options }: ISortByProps) => { const SortBy = memo(({ options }: ISortByProps) => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const sortBy = searchParams.get('sortBy') || ''; const sortBy = searchParams.get('sortBy') || '';
@@ -27,6 +28,6 @@ const SortBy = ({ options }: ISortByProps) => {
onChange={handleChange} onChange={handleChange}
/> />
); );
}; });
export default SortBy; export default SortBy;
@@ -11,6 +11,8 @@ const EDIT_SUCCESS = 'Edit success';
const CONFIRM_DELETE = 'Are you sure to delete it?'; const CONFIRM_DELETE = 'Are you sure to delete it?';
const DELETE_SUCCESS = 'Delete success'; const DELETE_SUCCESS = 'Delete success';
const REQUIRED_FIELD_ERROR = 'This is required field'; const REQUIRED_FIELD_ERROR = 'This is required field';
const DISCOUNT_FIELD_ERROR =
'Discount should be greater than 0 and less than 100';
export { export {
invalidFormatMsg, invalidFormatMsg,
@@ -20,4 +22,5 @@ export {
CONFIRM_DELETE, CONFIRM_DELETE,
DELETE_SUCCESS, DELETE_SUCCESS,
REQUIRED_FIELD_ERROR, 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 USER: string = '/user';
export const ROOM: string = '/room'; export const ROOM: string = '/room';
export const OTHER_PATH: string = '*'; 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 USER_PATH = 'users';
export const ROOM_PATH = 'rooms'; export const ROOM_PATH = 'rooms';
+13 -1
View File
@@ -20,6 +20,11 @@ interface ITableBody<T> {
render?: (value: T) => JSX.Element; render?: (value: T) => JSX.Element;
} }
interface ISelectOptions {
value: string;
label: string;
}
interface IDialogProps<T> { interface IDialogProps<T> {
title?: string; title?: string;
children?: JSX.Element[] | JSX.Element; children?: JSX.Element[] | JSX.Element;
@@ -31,4 +36,11 @@ interface IDialogProps<T> {
isAdd?: boolean; 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 = { type TUser = {
id: string; id: number;
name: string; name: string;
identifiedCode: string; identifiedCode: string;
address: string; address: string;
phone: string; phone: string;
roomId: string; roomId: number;
}; };
type TRoom = { type TRoom = {
id: string; id: number;
name: string; name: string;
amount: string; amount: number;
price: string; price: number;
discount: string; discount: number;
description: string; description: string;
status: boolean; status: boolean;
}; };
+21 -9
View File
@@ -56,7 +56,8 @@ const getPropValues = (stateSchema: TStateSchema, prop?: TPropValues) => {
type TValidator = { type TValidator = {
validatorFunc: (value: string) => boolean; validatorFunc: (value: string) => boolean;
prop: string; prop?: string;
customErrorMsg?: string;
required?: boolean; required?: boolean;
}; };
@@ -65,12 +66,20 @@ type TValidator = {
* @param param0 Pass TValidator object * @param param0 Pass TValidator object
* @returns An object contains condition validator * @returns An object contains condition validator
*/ */
const addValidator = ({ validatorFunc, prop, required = true }: TValidator) => { const addValidator = ({
validatorFunc,
prop = '',
customErrorMsg = '',
required = true,
}: TValidator) => {
return { return {
required, required,
validator: { validator: {
func: validatorFunc, 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) { if (obj) {
for (const key of Object.keys(obj)) { for (const key of Object.keys(obj)) {
const tempValue = obj[key as keyof typeof obj]; const tempValue = obj[key as keyof typeof obj];
const value: string | boolean | number = let value: string | boolean = '';
typeof tempValue === 'boolean' ||
typeof tempValue === 'string' || if (typeof tempValue === 'string' || typeof tempValue === 'boolean') {
typeof tempValue === 'number' value = tempValue;
? tempValue }
: '';
if (typeof tempValue === 'number') {
value = tempValue.toString();
}
result = { ...result, [`${key}Value`]: value }; 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 * Check value is valid number or not
* @param value Value need to be checked * @param value Value need to be checked
@@ -16,6 +7,16 @@ const isValidNumber = (value: string): boolean => {
return /^[0-9]*$/.test(value); 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 * Check value is valid phone number or not
* @param value Value need to be checked * @param value Value need to be checked
@@ -41,9 +42,9 @@ const isValidString = (value: string): boolean => {
const skipCheck = () => true; const skipCheck = () => true;
export { export {
isValidName,
isValidNumber, isValidNumber,
isValidPhoneNumber, isValidPhoneNumber,
isValidString, isValidString,
skipCheck, skipCheck,
isValidDiscount,
}; };
+8 -8
View File
@@ -19,15 +19,15 @@ import { searchQuery } from '../helpers/utils';
*/ */
const useFetch = ( const useFetch = (
path: string, path: string,
columnSearch: string, columnSearch: string = '',
keyWord: string, keyWord: string = '',
tempSortBy: string, tempSortBy?: string,
tempOrderBy: string, tempOrderBy?: string,
reload?: boolean, reload?: boolean,
) => { ) => {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorFetchMsg, setErrorFetchMsg] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
// Clear data // Clear data
@@ -61,16 +61,16 @@ const useFetch = (
setIsPending(false); setIsPending(false);
setData(json); setData(json);
setErrorMsg(null); setErrorFetchMsg(null);
} catch (error) { } catch (error) {
setErrorMsg(`Could not fetch data.\n ${error}`); setErrorFetchMsg(`Could not fetch data.\n ${error}`);
setIsPending(false); setIsPending(false);
} }
}; };
fetchData(); fetchData();
}, [path, reload, columnSearch, keyWord, tempOrderBy, tempSortBy]); }, [path, reload, columnSearch, keyWord, tempOrderBy, tempSortBy]);
return { data, isPending, errorMsg }; return { data, isPending, errorFetchMsg };
}; };
export { useFetch }; export { useFetch };
+5 -1
View File
@@ -110,7 +110,11 @@ const useForm = (
// Event handler for handling changes in input. // Event handler for handling changes in input.
const handleOnChange = useCallback( const handleOnChange = useCallback(
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { (
event: ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => {
setIsDirty(true); setIsDirty(true);
let error = ''; let error = '';
+22 -10
View File
@@ -24,6 +24,7 @@ import useForm from '../../hooks/useForm.ts';
// Utils // Utils
import { import {
isValidDiscount,
isValidNumber, isValidNumber,
isValidString, isValidString,
skipCheck, skipCheck,
@@ -35,6 +36,7 @@ import { sendRequest } from '../../helpers/sendRequest.ts';
import { STATUS_CODE } from '../../constants/statusCode.ts'; import { STATUS_CODE } from '../../constants/statusCode.ts';
import { import {
ADD_SUCCESS, ADD_SUCCESS,
DISCOUNT_FIELD_ERROR,
EDIT_SUCCESS, EDIT_SUCCESS,
errorMsg, errorMsg,
} from '../../constants/messages.ts'; } from '../../constants/messages.ts';
@@ -46,6 +48,7 @@ const FormBtn = styled(Button)`
&:disabled, &:disabled,
&[disabled] { &[disabled] {
background-color: var(--disabled-btn-color); background-color: var(--disabled-btn-color);
cursor: no-drop;
} }
`; `;
@@ -75,7 +78,7 @@ const RoomForm = ({
const initialValue: string = isAdd const initialValue: string = isAdd
? '' ? ''
: room! : room!
&& room.id; && room.id.toString();
const { const {
idValue, idValue,
@@ -134,8 +137,8 @@ const RoomForm = ({
prop: 'price', prop: 'price',
}), }),
discount: addValidator({ discount: addValidator({
validatorFunc: isValidNumber, validatorFunc: isValidDiscount,
prop: 'discount' customErrorMsg: DISCOUNT_FIELD_ERROR
}), }),
status: addValidator({ status: addValidator({
validatorFunc: skipCheck, validatorFunc: skipCheck,
@@ -150,12 +153,23 @@ const RoomForm = ({
// Submit form // Submit form
const onSubmitForm = async (state: TKeyValue) => { 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 { try {
if (isAdd) { if (isAdd) {
// Add request // Add request
const response = await sendRequest( const response = await sendRequest(
ROOM_PATH, ROOM_PATH,
JSON.stringify(state), JSON.stringify(data),
'POST', 'POST',
); );
@@ -170,7 +184,7 @@ const RoomForm = ({
// Edit request // Edit request
const response = await sendRequest( const response = await sendRequest(
ROOM_PATH + `/${room!.id}`, ROOM_PATH + `/${room!.id}`,
JSON.stringify(state), JSON.stringify(data),
'PUT', 'PUT',
); );
@@ -197,8 +211,6 @@ const RoomForm = ({
onResetForm(); onResetForm();
}; };
console.log(initialValue);
// prettier-ignore // prettier-ignore
const { const {
values, values,
@@ -296,8 +308,8 @@ const RoomForm = ({
label="Discount" label="Discount"
error={ error={
// prettier-ignore // prettier-ignore
errors.roomId && dirty.roomId errors.discount && dirty.discount
? (errors.roomId as string) ? (errors.discount as string)
: '' : ''
} }
> >
@@ -341,7 +353,7 @@ const RoomForm = ({
// prettier-ignore // prettier-ignore
isAdd isAdd
? 'Add' ? 'Add'
: 'Edit' : 'Save'
} }
</FormBtn> </FormBtn>
<FormBtn type="button" styled="secondary" onClick={closeAndReset}> <FormBtn type="button" styled="secondary" onClick={closeAndReset}>
+8 -8
View File
@@ -71,8 +71,8 @@ const RoomRow = ({
// prettier-ignore // prettier-ignore
const statusText = status const statusText = status
? 'Valid' ? 'Invalid'
: 'Invalid'; : 'Valid';
return ( return (
<Table.Row> <Table.Row>
@@ -84,9 +84,9 @@ const RoomRow = ({
<div>{statusText}</div> <div>{statusText}</div>
<Menus.Menu> <Menus.Menu>
<Menus.Toggle id={id} /> <Menus.Toggle id={id.toString()} />
<Menus.List id={id}> <Menus.List id={id.toString()}>
<Menus.Button <Menus.Button
icon={<HiSquare2Stack />} icon={<HiSquare2Stack />}
onClick={() => handleOnEdit(room)} onClick={() => handleOnEdit(room)}
@@ -125,7 +125,7 @@ const RoomTable = ({
? searchParams.get('orderBy')! ? searchParams.get('orderBy')!
: ''; : '';
const { data, isPending, errorMsg } = useFetch( const { data, isPending, errorFetchMsg } = useFetch(
'rooms', 'rooms',
'name', 'name',
nameSearch, nameSearch,
@@ -141,10 +141,10 @@ const RoomTable = ({
setRooms([]); setRooms([]);
} }
if (errorMsg) { if (errorFetchMsg) {
console.error(errorMsg); console.error(errorFetchMsg);
} }
}, [data, errorMsg]); }, [data, errorFetchMsg]);
return ( return (
<> <>
+49 -7
View File
@@ -27,7 +27,6 @@ import {
isValidString, isValidString,
isValidNumber, isValidNumber,
isValidPhoneNumber, isValidPhoneNumber,
isValidName,
} from '../../helpers/validators'; } from '../../helpers/validators';
import { addValidator, getValueFromObj } from '../../helpers/utils.ts'; import { addValidator, getValueFromObj } from '../../helpers/utils.ts';
import { sendRequest } from '../../helpers/sendRequest.ts'; import { sendRequest } from '../../helpers/sendRequest.ts';
@@ -40,6 +39,11 @@ import {
errorMsg, errorMsg,
} from '../../constants/messages.ts'; } from '../../constants/messages.ts';
import { USER_PATH } from '../../constants/path.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)` const FormBtn = styled(Button)`
width: 100%; width: 100%;
@@ -47,6 +51,7 @@ const FormBtn = styled(Button)`
&:disabled, &:disabled,
&[disabled] { &[disabled] {
background-color: var(--disabled-btn-color); background-color: var(--disabled-btn-color);
cursor: no-drop;
} }
`; `;
@@ -66,6 +71,27 @@ const UserForm = ({
isAdd, isAdd,
}: IUserFormProp) => { }: IUserFormProp) => {
const [reset, setReset] = useState(true); 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 (isAdd) {
// If this is add form, reset value // If this is add form, reset value
@@ -76,7 +102,7 @@ const UserForm = ({
const initialValue: string = isAdd const initialValue: string = isAdd
? '' ? ''
: user! : user!
&& user.id; && user.id.toLocaleString();
const { const {
idValue, idValue,
@@ -116,7 +142,7 @@ const UserForm = ({
// prettier-ignore // prettier-ignore
const stateValidatorSchema: TValidator = { const stateValidatorSchema: TValidator = {
name: addValidator({ name: addValidator({
validatorFunc: isValidName, validatorFunc: isValidString,
prop: 'full name' prop: 'full name'
}), }),
identifiedCode: addValidator({ identifiedCode: addValidator({
@@ -139,12 +165,22 @@ const UserForm = ({
// Submit form // Submit form
const onSubmitForm = async (state: TKeyValue) => { 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 { try {
if (isAdd) { if (isAdd) {
// Add request // Add request
const response = await sendRequest( const response = await sendRequest(
USER_PATH, USER_PATH,
JSON.stringify(state), JSON.stringify(data),
'POST', 'POST',
); );
@@ -159,7 +195,7 @@ const UserForm = ({
// Edit request // Edit request
const response = await sendRequest( const response = await sendRequest(
USER_PATH + `/${user!.id}`, USER_PATH + `/${user!.id}`,
JSON.stringify(state), JSON.stringify(data),
'PUT', 'PUT',
); );
@@ -285,11 +321,17 @@ const UserForm = ({
: '' : ''
} }
> >
<Input {/* <Input
type="text" type="text"
name="roomId" name="roomId"
value={roomId as string} value={roomId as string}
onChange={handleOnChange} onChange={handleOnChange}
/> */}
<Select
name="roomId"
value={roomId as string}
onChange={handleOnChange}
options={options!}
/> />
</FormRow> </FormRow>
@@ -316,7 +358,7 @@ const UserForm = ({
// prettier-ignore // prettier-ignore
isAdd isAdd
? 'Add' ? 'Add'
: 'Edit' : 'Save'
} }
</FormBtn> </FormBtn>
<FormBtn type="button" styled="secondary" onClick={closeAndReset}> <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 { USER_PATH } from '../../constants/path';
import { STATUS_CODE } from '../../constants/statusCode'; import { STATUS_CODE } from '../../constants/statusCode';
import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages'; import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages';
import { USER_PAGE } from '../../constants/variables';
// Styled // Styled
import Spinner from '../../commons/styles/Spinner'; import Spinner from '../../commons/styles/Spinner';
// Utils // Utils
import { sendRequest } from '../../helpers/sendRequest'; import { sendRequest } from '../../helpers/sendRequest';
import { USER_PAGE } from '../../constants/variables';
interface IUserRow { interface IUserRow {
user: TUser; user: TUser;
@@ -78,9 +78,9 @@ const UserRow = ({
<div>{roomId}</div> <div>{roomId}</div>
<Menus.Menu> <Menus.Menu>
<Menus.Toggle id={id} /> <Menus.Toggle id={id.toString()} />
<Menus.List id={id}> <Menus.List id={id.toString()}>
<Menus.Button <Menus.Button
icon={<HiSquare2Stack />} icon={<HiSquare2Stack />}
onClick={() => handleOnEdit(user)} onClick={() => handleOnEdit(user)}
@@ -119,7 +119,7 @@ const UserTable = ({
? searchParams.get('orderBy')! ? searchParams.get('orderBy')!
: ''; : '';
const { data, isPending, errorMsg } = useFetch( const { data, isPending, errorFetchMsg } = useFetch(
'users', 'users',
'phone', 'phone',
phoneSearch, phoneSearch,
@@ -135,10 +135,10 @@ const UserTable = ({
setUsers([]); setUsers([]);
} }
if (errorMsg) { if (errorFetchMsg) {
console.error(errorMsg); console.error(errorFetchMsg);
} }
}, [data, errorMsg]); }, [data, errorFetchMsg]);
return ( return (
<> <>
@@ -163,7 +163,7 @@ const UserTable = ({
<div>Name</div> <div>Name</div>
<div>Identified Code</div> <div>Identified Code</div>
<div>Phone</div> <div>Phone</div>
<div>Room</div> <div>Room Id</div>
</Table.Header> </Table.Header>
<Table.Body<TUser> <Table.Body<TUser>
data={users} data={users}
@@ -1,39 +1,11 @@
@font-face { @font-face {
font-family: 'Poppins'; font-family: 'Cabin';
src: url('../../assets/fonts/Poppins-Regular.woff2') format('woff2'), src: url('../../assets/fonts/Cabin.woff2') format('woff2'),
url('../../assets/fonts/Poppins-Regular.ttf') format('truetype'); url('../../assets/fonts/Cabin.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-display: swap; font-display: swap;
} }
* { * {
font-family: 'Poppins', Times, serif; font-family: 'Cabin', Times, serif;
} }