Refactor code

This commit is contained in:
2023-11-02 23:24:03 +07:00
parent cc147f071c
commit 6560c304c3
28 changed files with 163 additions and 157 deletions
+3 -3
View File
@@ -6,7 +6,7 @@
- Introduce: Build a hotel management web application using HTML5, CSS3, Typescript, json-server, React and compile by Vite.
- Plan: [Link](https://docs.google.com/document/d/1t2kx17iGCceEe3EbaKO6-3g70ejOJ1PAu9u3KXSKWi4/edit?usp=sharing)
- Deploy: _Update later_
- Deploy: [Link](https://hotel-management.loiphan.com/)
## Requirements
@@ -31,7 +31,7 @@
## Technical
- [HTML5](https://www.tutorialspoint.com/html5/html5_overview.htm): CSS (Cascading Style Sheets) consist of a group of formatting rules that you use to control the layout and appearance of the content on a web page.
- [HTML5](https://www.tutorialspoint.com/html5/html5_overview.htm): HTML5 is the latest standard of Hypertext Markup Language, the code that describes the structure and presentation of web pages.
- [CSS3](https://www.htmlgoodies.com/html5/an-overview-of-css3/): Cascading Style Sheets (CSS) is a language used to illustrate a documents look, style, and format in any markup language. In simple words, it is used to style and organize the layout of Web pages. CSS3 is the latest version of an earlier CSS version, CSS2.
- [Typescript](https://www.typescriptlang.org/): TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
- [React](https://react.dev/): React is a framework that employs Webpack to automatically compile React, JSX, and ES6 code while handling CSS file prefixes. React is a JavaScript-based UI development library. Although React is a library rather than a language, it is widely used in web development.
@@ -42,7 +42,7 @@
- Timeline:
- Estimate: 11 days
- Actual: -- days.
- Actual: 13 days.
- Calendar:
- Start: 2023/10/16
- End: 2023/10/30
@@ -10,8 +10,8 @@ import { Main, StyledAppLayout } from './styled';
const AppLayout = () => {
return (
<StyledAppLayout>
<Header />
<Sidebar />
<Header accountName='Admin'/>
<Sidebar heading='Hotel Management'/>
<Main>
<Outlet />
</Main>
@@ -1,11 +1,19 @@
import { forwardRef } from 'react';
// Components
import { IDialogProps } from '../../globals/interfaces';
// Styled
import { StyledBody, StyledDialog, StyledTitle } from './styled';
export interface IDialogProps<T> {
title?: string;
children?: JSX.Element[] | JSX.Element;
onClose?: () => void;
reload?: boolean;
setReload?: React.Dispatch<React.SetStateAction<boolean>>;
ref?: React.MutableRefObject<HTMLDialogElement | undefined>;
data?: T | null;
isAdd?: boolean;
}
const Dialog = forwardRef((props, ref) => {
const { title, children, onClose } = props as IDialogProps<unknown>;
return (
@@ -4,10 +4,14 @@ import HeaderMenu from '../HeaderMenu';
// Styled
import { StyledHeader } from './styled';
const Header = () => {
interface IHeader {
accountName: string
}
const Header = ({accountName}: IHeader) => {
return (
<StyledHeader>
<p>Hi, Admin!</p>
<p>Hi, {accountName}!</p>
<HeaderMenu />
</StyledHeader>
);
@@ -5,12 +5,15 @@ import { HiEllipsisVertical } from 'react-icons/hi2';
import { useOutsideClick } from '../../hooks/useOutsideClick';
import { StyledMenu, StyledButton, StyledList, StyledToggle } from './styled';
// Interfaces
import { IButton } from '../../globals/interfaces';
// Contexts
import MenusContext from '../../contexts/MenuContext';
interface IButton {
children?: string;
icon?: JSX.Element;
onClick?: () => void;
}
const Menus = ({ children }: { children: JSX.Element }) => {
const [openId, setOpenId] = useState('');
@@ -30,7 +33,6 @@ const Toggle = ({ id }: { id: string }): React.JSX.Element => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
// prettier-ignore
openId === '' || openId !== id
? open!(id)
: close!();
@@ -6,18 +6,21 @@ import { BsHouseDoorFill } from 'react-icons/bs';
// Styled
import { StyledNav, StyledNavLink } from './styled';
// Constants
import { DASHBOARD, ROOM, USER } from '../../constants/path';
const Nav = () => {
return (
<StyledNav>
<StyledNavLink to={'/dashboard'}>
<StyledNavLink to={DASHBOARD}>
<MdSpaceDashboard />
<span>Dashboard</span>
</StyledNavLink>
<StyledNavLink to={'/user'}>
<StyledNavLink to={USER}>
<HiUsers />
<span>User</span>
</StyledNavLink>
<StyledNavLink to={'/room'}>
<StyledNavLink to={ROOM}>
<BsHouseDoorFill />
<span>Room</span>
</StyledNavLink>
@@ -1,6 +1,9 @@
import { ISelectOptions } from '../../globals/interfaces';
import { StyledSelect } from './styled';
export interface ISelectOptions {
value: string;
label: string;
}
interface ISelect {
options: ISelectOptions[];
value: string;
@@ -4,10 +4,14 @@ import Nav from '../Nav';
// Styled
import { Heading, StyledSidebar } from './styled';
const Sidebar = () => {
interface ISidebar {
heading: string;
}
const Sidebar = ({heading}: ISidebar) => {
return (
<StyledSidebar>
<Heading>Hotel Management</Heading>
<Heading>{heading}</Heading>
<Nav />
</StyledSidebar>
);
@@ -1,8 +1,8 @@
import { useSearchParams } from 'react-router-dom';
import { memo } from 'react';
// Components
import Select from '../Select';
import { memo } from 'react';
interface ISortByProps {
options: {
@@ -3,24 +3,31 @@ import { ReactNode, useContext } from 'react';
// Components
import { StyledBody, StyledHeader, StyledRow, StyledTable } from './styled';
// Interfaces
import { ITable, ITableBody } from '../../globals/interfaces';
// Contexts
import TableContext from '../../contexts/TableContext';
interface ITable {
columns?: string;
children: React.ReactNode;
}
interface ITableBody<T> {
data?: T[];
render?: (value: T) => JSX.Element;
}
type CallbackMapFunc<T> = (value: T, index: number, array: T[]) => ReactNode;
const Table = ({ columns, children }: ITable) => {
return (
<TableContext.Provider value={columns!}>
<TableContext.Provider value={{ columns }}>
<StyledTable>{children}</StyledTable>
</TableContext.Provider>
);
};
const Header = ({ children }: ITable) => {
const columns = useContext(TableContext);
const { columns } = useContext(TableContext);
return <StyledHeader columns={columns}>{children}</StyledHeader>;
};
@@ -33,7 +40,7 @@ const Body = <T,>({ data, render }: ITableBody<T>) => {
};
const Row = ({ children }: ITable) => {
const columns = useContext(TableContext);
const { columns } = useContext(TableContext);
return <StyledRow columns={columns}>{children}</StyledRow>;
};
+36 -16
View File
@@ -1,26 +1,46 @@
import { Toaster } from 'react-hot-toast';
import { DefaultToastOptions, ToastOptions, ToastPosition, Toaster } from 'react-hot-toast';
import { CSSProperties } from 'styled-components';
const Toast = () => {
interface IToast {
position: ToastPosition;
gutter: number;
containerStyle: CSSProperties;
toastOptions: DefaultToastOptions;
success: ToastOptions;
error: ToastOptions;
style: CSSProperties | undefined;
}
const Toast = ({position, gutter, containerStyle, success, error, style}: IToast) => {
return (
<Toaster
position="top-center"
gutter={12}
containerStyle={{ margin: '8px', zIndex: 1 }}
position={position}
gutter={gutter}
containerStyle={containerStyle}
toastOptions={{
success: {
duration: 3000,
},
error: {
duration: 5000,
},
style: {
fontSize: '16px',
maxWidth: '500px',
padding: '16px 24px',
},
success,
error,
style
}}
/>
);
};
Toast.defaultProps = {
position: "top-center",
gutter: 12,
containerStyle: { margin: '8px', zIndex: 1 },
success: {
duration: 3000,
},
error:{
duration: 5000,
},
style: {
fontSize: '16px',
maxWidth: '500px',
padding: '16px 24px',
}
}
export default Toast;
+5 -5
View File
@@ -1,7 +1,7 @@
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 DASHBOARD = '/dashboard';
export const USER = '/user';
export const ROOM = '/room';
export const OTHER_PATH = '*';
export const BASE_URL = 'https://hotel-management-api.loiphan.com/';
export const USER_PATH = 'users';
export const ROOM_PATH = 'rooms';
+10 -1
View File
@@ -74,7 +74,16 @@ const ROOM_PAGE = {
],
}
const INITIAL_STATE_SCHEMA = {
id: '',
name: '',
identifiedCode: '',
phone: '',
roomId: 'undefined',
address: '',
};
const VALUE = 'value';
const ERROR = 'error';
export { USER_PAGE, ROOM_PAGE, VALUE, ERROR };
export { USER_PAGE, ROOM_PAGE, VALUE, ERROR, INITIAL_STATE_SCHEMA };
+5 -2
View File
@@ -1,7 +1,10 @@
import { createContext } from 'react';
// Interfaces
import { IMenusContext } from '../globals/interfaces';
interface IMenusContext {
openId?: string;
close?: () => void;
open?: React.Dispatch<React.SetStateAction<string>>;
}
const MenusContext = createContext<IMenusContext>({});
@@ -1,5 +1,9 @@
import { createContext } from 'react';
const TableContext = createContext('');
interface ITableContext {
columns?: string;
}
const TableContext = createContext<ITableContext>({});
export default TableContext;
@@ -1,46 +0,0 @@
interface IMenusContext {
openId?: string;
close?: () => void;
open?: React.Dispatch<React.SetStateAction<string>>;
}
interface IButton {
children?: string;
icon?: JSX.Element;
onClick?: () => void;
}
interface ITable {
columns?: string;
children: React.ReactNode;
}
interface ITableBody<T> {
data?: T[];
render?: (value: T) => JSX.Element;
}
interface ISelectOptions {
value: string;
label: string;
}
interface IDialogProps<T> {
title?: string;
children?: JSX.Element[] | JSX.Element;
onClose?: () => void;
reload?: boolean;
setReload?: React.Dispatch<React.SetStateAction<boolean>>;
ref?: React.MutableRefObject<HTMLDialogElement | undefined>;
data?: T | null;
isAdd?: boolean;
}
export type {
IMenusContext,
IButton,
ITable,
ITableBody,
IDialogProps,
ISelectOptions,
};
+1 -1
View File
@@ -124,7 +124,7 @@ const searchQuery = (
columnSearch: string,
keySearch: string,
sort: string,
order: string,
order: string
) => {
// prettier-ignore
const phoneParams = keySearch
@@ -13,7 +13,6 @@ const isValidNumber = (value: string): boolean => {
* @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;
};
+3 -3
View File
@@ -8,7 +8,7 @@ import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '../constants/config';
import { searchQuery } from '../helpers/utils';
/**
*
* The function use to fetch data on server API.
* @param path The path of url
* @param columnSearch Column need to search
* @param keyWord The key word to search
@@ -23,7 +23,7 @@ const useFetch = (
keyWord: string = '',
tempSortBy?: string,
tempOrderBy?: string,
reload?: boolean,
reload?: boolean
) => {
const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(false);
@@ -56,7 +56,7 @@ const useFetch = (
if (!response.ok)
throw new Error(
`Error code: ${response.status} \n Messages: ${response.statusText}`,
`Error code: ${response.status} \n Messages: ${response.statusText}`
);
setIsPending(false);
+16 -15
View File
@@ -7,7 +7,7 @@ import { getPropValues, isObject, isRequired } from '../helpers/utils';
import { TKeyValue, TValidator } from '../globals/types';
// Constants
import { ERROR, VALUE } from '../constants/variables';
import { ERROR, INITIAL_STATE_SCHEMA, VALUE } from '../constants/variables';
/**
* Custom hooks to validate your Form...
@@ -21,11 +21,11 @@ const useForm = (
stateSchema = {},
stateValidatorSchema = {} as TValidator,
submitFormCallback: (values: TKeyValue) => void,
initialValue: string = '',
initialValue: string = ''
) => {
const [values, setValues] = useState(getPropValues(stateSchema, VALUE));
const [values, setValues] = useState<TKeyValue>(INITIAL_STATE_SCHEMA);
const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR));
const [dirty, setDirty] = useState(getPropValues(stateSchema));
const [valid, isValid] = useState(getPropValues(stateSchema));
const [disable, setDisable] = useState(true);
const [isDirty, setIsDirty] = useState(false);
@@ -33,7 +33,8 @@ const useForm = (
useEffect(() => {
setInitialErrorState(initialValue);
setDisable(true);
setDirty(getPropValues(stateSchema));
isValid(getPropValues(stateSchema));
setValues(getPropValues(stateSchema, VALUE));
// If initial value true, setValues again from stateSchema
// and enabled button
@@ -56,7 +57,7 @@ const useForm = (
let error = '';
// Skip check id field
if (name !== 'id') {
if (name !== 'id' && name !== 'roomId') {
error = isRequired(value, field!.required);
if (isObject(field['validator']) && error === '') {
@@ -72,7 +73,7 @@ const useForm = (
return error;
},
[stateValidatorSchema],
[stateValidatorSchema]
);
// Set Initial Error State
@@ -85,10 +86,10 @@ const useForm = (
[name]: !initialValue // Skip error when initialValue have values
? validateFormFields(name, values[name] as string)
: '',
})),
}))
);
},
[errors, values, validateFormFields],
[errors, values, validateFormFields]
);
// Used to disable submit button if there's a value in errors
@@ -97,7 +98,7 @@ const useForm = (
// in every re-render in component
const validateErrorState = useCallback(
() => Object.values(errors).some((error) => error),
[errors],
[errors]
);
// For every changed in our state this will be fired
@@ -113,7 +114,7 @@ const useForm = (
(
event: ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
>
) => {
setIsDirty(true);
@@ -133,9 +134,9 @@ const useForm = (
setValues((prevState) => ({ ...prevState, [name]: value }));
setErrors((prevState) => ({ ...prevState, [name]: error }));
setDirty((prevState) => ({ ...prevState, [name]: true }));
isValid((prevState) => ({ ...prevState, [name]: true }));
},
[validateFormFields],
[validateFormFields]
);
const handleOnSubmit = useCallback(
@@ -151,7 +152,7 @@ const useForm = (
setDisable(true);
}
},
[validateErrorState, submitFormCallback, values],
[validateErrorState, submitFormCallback, values]
);
return {
@@ -162,7 +163,7 @@ const useForm = (
disable,
setValues,
setErrors,
dirty,
valid,
};
};
+1 -4
View File
@@ -1,12 +1,9 @@
import { forwardRef, useEffect } from 'react';
// Components
import Dialog from '../../components/Dialog';
import Dialog, { IDialogProps } from '../../components/Dialog';
import RoomForm from './Form';
// Interfaces
import { IDialogProps } from '../../globals/interfaces';
// Types
import { TRoom } from '../../globals/types';
+9 -9
View File
@@ -8,7 +8,7 @@ import TextArea from '../../commons/styles/TextArea.ts';
// Components
import Form from '../../components/Form/index.tsx';
import FormRow from '../../components/FormRow/index.tsx';
import FormRow from '../../components/LabelControl/index.tsx';
import Button from '../../commons/styles/Button.ts';
// Types
@@ -170,7 +170,7 @@ const RoomForm = ({
const response = await sendRequest(
ROOM_PATH,
JSON.stringify(data),
'POST',
'POST'
);
if (response.statusCode === STATUS_CODE.CREATE) {
@@ -185,7 +185,7 @@ const RoomForm = ({
const response = await sendRequest(
ROOM_PATH + `/${room!.id}`,
JSON.stringify(data),
'PUT',
'PUT'
);
if (response.statusCode == STATUS_CODE.OK) {
@@ -215,7 +215,7 @@ const RoomForm = ({
const {
values,
errors,
dirty,
valid,
handleOnChange,
handleOnSubmit,
disable } =
@@ -257,7 +257,7 @@ const RoomForm = ({
label="Name"
error={
// prettier-ignore
errors.name && dirty.name
errors.name && valid.name
? (errors.name as string)
: ''
}
@@ -274,7 +274,7 @@ const RoomForm = ({
label="Amount"
// prettier-ignore
error={
errors.amount && dirty.amount
errors.amount && valid.amount
? (errors.amount as string)
: ''
}
@@ -291,7 +291,7 @@ const RoomForm = ({
label="Price"
error={
// prettier-ignore
errors.price && dirty.price
errors.price && valid.price
? (errors.price as string)
: ''
}
@@ -308,7 +308,7 @@ const RoomForm = ({
label="Discount"
error={
// prettier-ignore
errors.discount && dirty.discount
errors.discount && valid.discount
? (errors.discount as string)
: ''
}
@@ -334,7 +334,7 @@ const RoomForm = ({
label="Description"
error={
// prettier-ignore
errors.description && dirty.description
errors.description && valid.description
? (errors.description as string)
: ''
}
+1 -1
View File
@@ -6,7 +6,7 @@ import toast from 'react-hot-toast';
import { HiSquare2Stack } from 'react-icons/hi2';
import { HiTrash } from 'react-icons/hi';
import { StyledOperationTable } from './styled';
import Menus from '../../components/Menus/Menus';
import Menus from '../../components/Menus';
import Table from '../../components/Table';
import Direction from '../../commons/styles/Direction';
import Message from '../../components/Message';
+1 -4
View File
@@ -1,12 +1,9 @@
import { forwardRef, useEffect } from 'react';
// Components
import Dialog from '../../components/Dialog';
import Dialog, { IDialogProps } from '../../components/Dialog';
import UserForm from './Form';
// Interfaces
import { IDialogProps } from '../../globals/interfaces';
// Types
import { TUser } from '../../globals/types';
+11 -20
View File
@@ -8,7 +8,7 @@ import TextArea from '../../commons/styles/TextArea';
// Components
import Form from '../../components/Form';
import FormRow from '../../components/FormRow';
import FormRow from '../../components/LabelControl/index.tsx';
import Button from '../../commons/styles/Button.ts';
// Types
@@ -39,12 +39,9 @@ import {
errorMsg,
} from '../../constants/messages.ts';
import { USER_PATH } from '../../constants/path.ts';
import Select from '../../components/Select/index.tsx';
import Select, { ISelectOptions } from '../../components/Select/index.tsx';
import { useFetch } from '../../hooks/useFetch.ts';
// Interfaces
import { ISelectOptions } from '../../globals/interfaces.ts';
const FormBtn = styled(Button)`
width: 100%;
@@ -130,7 +127,7 @@ const UserForm = ({
error: '',
},
roomId: {
value: roomIdValue || '',
value: roomIdValue || '' + (options && options[0].value),
error: '' ,
},
address: {
@@ -181,7 +178,7 @@ const UserForm = ({
const response = await sendRequest(
USER_PATH,
JSON.stringify(data),
'POST',
'POST'
);
if (response.statusCode === STATUS_CODE.CREATE) {
@@ -196,7 +193,7 @@ const UserForm = ({
const response = await sendRequest(
USER_PATH + `/${user!.id}`,
JSON.stringify(data),
'PUT',
'PUT'
);
if (response.statusCode == STATUS_CODE.OK) {
@@ -226,7 +223,7 @@ const UserForm = ({
const {
values,
errors,
dirty,
valid,
handleOnChange,
handleOnSubmit,
disable } =
@@ -266,7 +263,7 @@ const UserForm = ({
label="Full Name"
error={
// prettier-ignore
errors.name && dirty.name
errors.name && valid.name
? (errors.name as string)
: ''
}
@@ -282,7 +279,7 @@ const UserForm = ({
<FormRow
label="Identified Code"
error={
errors.identifiedCode && dirty.identifiedCode
errors.identifiedCode && valid.identifiedCode
? (errors.identifiedCode as string)
: ''
}
@@ -299,7 +296,7 @@ const UserForm = ({
label="Phone"
error={
// prettier-ignore
errors.phone && dirty.phone
errors.phone && valid.phone
? (errors.phone as string)
: ''
}
@@ -316,17 +313,11 @@ const UserForm = ({
label="Room"
error={
// prettier-ignore
errors.roomId && dirty.roomId
errors.roomId && valid.roomId
? (errors.roomId as string)
: ''
}
>
{/* <Input
type="text"
name="roomId"
value={roomId as string}
onChange={handleOnChange}
/> */}
<Select
name="roomId"
value={roomId as string}
@@ -339,7 +330,7 @@ const UserForm = ({
label="Address"
error={
// prettier-ignore
errors.address && dirty.address
errors.address && valid.address
? (errors.address as string)
: ''
}
+1 -1
View File
@@ -6,7 +6,7 @@ import toast from 'react-hot-toast';
import { HiSquare2Stack } from 'react-icons/hi2';
import { HiTrash } from 'react-icons/hi';
import { StyledOperationTable } from './styled';
import Menus from '../../components/Menus/Menus';
import Menus from '../../components/Menus';
import Table from '../../components/Table';
import Direction from '../../commons/styles/Direction';
import Message from '../../components/Message';