From 47a4c1be41cf30a72af0ae8b1b8ecd666f94988a Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Wed, 1 Nov 2023 14:25:45 +0700 Subject: [PATCH 1/3] Update hooks and add content for room page --- hotel-management/src/hooks/useFetch.ts | 25 +- hotel-management/src/hooks/useForm.ts | 24 +- hotel-management/src/hooks/useOutsideClick.ts | 6 + hotel-management/src/pages/Room.tsx | 15 - hotel-management/src/pages/Room/Dialog.tsx | 55 +++ hotel-management/src/pages/Room/Form.tsx | 334 ++++++++++++++++++ hotel-management/src/pages/Room/Table.tsx | 198 +++++++++++ hotel-management/src/pages/Room/index.tsx | 58 +++ hotel-management/src/pages/Room/styled.ts | 24 ++ hotel-management/src/pages/User/Dialog.tsx | 8 +- hotel-management/src/pages/User/Form.tsx | 63 +++- hotel-management/src/pages/User/Table.tsx | 6 +- hotel-management/src/pages/User/index.tsx | 2 +- 13 files changed, 770 insertions(+), 48 deletions(-) delete mode 100644 hotel-management/src/pages/Room.tsx create mode 100644 hotel-management/src/pages/Room/Dialog.tsx create mode 100644 hotel-management/src/pages/Room/Form.tsx create mode 100644 hotel-management/src/pages/Room/Table.tsx create mode 100644 hotel-management/src/pages/Room/index.tsx create mode 100644 hotel-management/src/pages/Room/styled.ts diff --git a/hotel-management/src/hooks/useFetch.ts b/hotel-management/src/hooks/useFetch.ts index eda4eea..59647c5 100644 --- a/hotel-management/src/hooks/useFetch.ts +++ b/hotel-management/src/hooks/useFetch.ts @@ -2,12 +2,25 @@ import { useEffect, useState } from 'react'; // Constants import { BASE_URL } from '../constants/path'; -import { searchQuery } from '../helpers/utils'; import { DEFAULT_ORDER_BY, DEFAULT_SORT_BY } from '../constants/config'; -export const useFetch = ( +// Utils +import { searchQuery } from '../helpers/utils'; + +/** + * + * @param path The path of url + * @param columnSearch Column need to search + * @param keyWord The key word to search + * @param tempSortBy Sort by + * @param tempOrderBy Order by + * @param reload Fetch again + * @returns data: A data after fetch, isPending: A boolean indicating whether or not the progress of fetch data is done, errorMsg: A error message from the server. + */ +const useFetch = ( path: string, - phoneNum: string, + columnSearch: string, + keyWord: string, tempSortBy: string, tempOrderBy: string, reload?: boolean, @@ -35,7 +48,7 @@ export const useFetch = ( : DEFAULT_ORDER_BY; // Query search - const query = searchQuery(phoneNum, sortBy, orderBy); + const query = searchQuery(columnSearch, keyWord, sortBy, orderBy); try { const response = await fetch(BASE_URL + path + '?' + query); @@ -56,6 +69,8 @@ export const useFetch = ( }; fetchData(); - }, [path, reload, phoneNum, tempOrderBy, tempSortBy]); + }, [path, reload, columnSearch, keyWord, tempOrderBy, tempSortBy]); return { data, isPending, errorMsg }; }; + +export { useFetch }; diff --git a/hotel-management/src/hooks/useForm.ts b/hotel-management/src/hooks/useForm.ts index bc28fc7..c92c6e9 100644 --- a/hotel-management/src/hooks/useForm.ts +++ b/hotel-management/src/hooks/useForm.ts @@ -1,16 +1,11 @@ import { useState, useEffect, useCallback, ChangeEvent } from 'react'; // Utils -import { - ERROR, - VALUE, - getPropValues, - isObject, - isRequired, -} from '../helpers/utils'; +import { getPropValues, isObject, isRequired } from '../helpers/utils'; // Types import { TKeyValue, TValidator } from '../globals/types'; +import { ERROR, VALUE } from '../constants/variables'; /** * Custom hooks to validate your Form... @@ -36,11 +31,11 @@ const useForm = ( useEffect(() => { setInitialErrorState(initialValue); setDisable(true); + setDirty(getPropValues(stateSchema)); // If initial value true, setValues again from stateSchema // and enabled button if (initialValue) { - setValues({}); setValues(getPropValues(stateSchema, VALUE)); setDisable(false); } @@ -116,10 +111,19 @@ const useForm = ( (event: ChangeEvent) => { setIsDirty(true); + let error = ''; const name = (event.target! as HTMLInputElement).name; - const value = (event.target! as HTMLInputElement).value; + let value: string | boolean = ''; - const error = validateFormFields(name, value); + if ((event.target! as HTMLInputElement).type === 'checkbox') { + value = (event.target! as HTMLInputElement).checked; + } else { + value = (event.target! as HTMLInputElement).value; + } + + if (typeof value === 'string') { + error = validateFormFields(name, value)!; + } setValues((prevState) => ({ ...prevState, [name]: value })); setErrors((prevState) => ({ ...prevState, [name]: error })); diff --git a/hotel-management/src/hooks/useOutsideClick.ts b/hotel-management/src/hooks/useOutsideClick.ts index c7278d5..bc6e040 100644 --- a/hotel-management/src/hooks/useOutsideClick.ts +++ b/hotel-management/src/hooks/useOutsideClick.ts @@ -1,5 +1,11 @@ import { useEffect, useRef } from 'react'; +/** + * Custom hook to call handler when click outside element + * @param handler Handler function + * @param listeningCapturing A boolean value indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. If not specified, defaults to false. + * @returns Return a mutable ref object + */ export const useOutsideClick = ( handler: () => void, listeningCapturing = true, diff --git a/hotel-management/src/pages/Room.tsx b/hotel-management/src/pages/Room.tsx deleted file mode 100644 index a7185f5..0000000 --- a/hotel-management/src/pages/Room.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components'; - -const StyledRoom = styled.main` - padding: 20px; -`; - -const Room = () => { - return ( - -

Room page

-
- ); -}; - -export default Room; diff --git a/hotel-management/src/pages/Room/Dialog.tsx b/hotel-management/src/pages/Room/Dialog.tsx new file mode 100644 index 0000000..5fd8f99 --- /dev/null +++ b/hotel-management/src/pages/Room/Dialog.tsx @@ -0,0 +1,55 @@ +import { forwardRef, useEffect } from 'react'; + +// Components +import Dialog from '../../components/Dialog'; +import RoomForm from './Form'; + +// Interfaces +import { IDialogProps } from '../../globals/interfaces'; + +// Types +import { TRoom } from '../../globals/types'; + +const RoomDialog = forwardRef((props, ref) => { + const dialogRef = ref as React.MutableRefObject< + HTMLDialogElement | undefined + >; + // prettier-ignore + const { + onClose, + setReload, + reload, + data, + isAdd + } = props; + + useEffect(() => { + if (dialogRef.current) { + dialogRef.current.addEventListener('click', (e: MouseEvent) => { + const dialogDimensions = dialogRef.current!.getBoundingClientRect(); + if ( + e.clientX < dialogDimensions.left || + e.clientX > dialogDimensions.right || + e.clientY < dialogDimensions.top || + e.clientY > dialogDimensions.bottom + ) { + dialogRef.current!.close(); + } + }); + } + }, [dialogRef]); + + return ( + + + + ); +}) as React.FC>; + +export default RoomDialog; diff --git a/hotel-management/src/pages/Room/Form.tsx b/hotel-management/src/pages/Room/Form.tsx new file mode 100644 index 0000000..4a744c6 --- /dev/null +++ b/hotel-management/src/pages/Room/Form.tsx @@ -0,0 +1,334 @@ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import toast from 'react-hot-toast'; + +// Styled +import Input from '../../commons/styles/Input.ts'; +import TextArea from '../../commons/styles/TextArea.ts'; + +// Components +import Form from '../../components/Form/index.tsx'; +import FormRow from '../../components/FormRow/index.tsx'; +import Button from '../../commons/styles/Button.ts'; + +// Types +import { + TKeyValue, + TRoom, + TStateSchema, + TValidator, +} from '../../globals/types.ts'; + +// Hooks +import useForm from '../../hooks/useForm.ts'; + +// Utils +import { + isValidNumber, + isValidString, + skipCheck, +} from '../../helpers/validators.ts'; +import { addValidator, getValueFromObj } from '../../helpers/utils.ts'; +import { sendRequest } from '../../helpers/sendRequest.ts'; + +// Constants +import { STATUS_CODE } from '../../constants/statusCode.ts'; +import { + ADD_SUCCESS, + EDIT_SUCCESS, + errorMsg, +} from '../../constants/messages.ts'; +import { ROOM_PATH } from '../../constants/path.ts'; + +const FormBtn = styled(Button)` + width: 100%; + + &:disabled, + &[disabled] { + background-color: var(--disabled-btn-color); + } +`; + +interface IRoomFormProp { + onClose: () => void; + reload: boolean; + setReload: React.Dispatch>; + room?: TRoom | null; + isAdd: boolean; +} + +const RoomForm = ({ + onClose, + reload, + setReload, + room, + isAdd, +}: IRoomFormProp) => { + const [reset, setReset] = useState(true); + + if (isAdd) { + // If this is add form, reset value + room = null; + } + + // prettier-ignore + const initialValue: string = isAdd + ? '' + : room! + && room.id; + + const { + idValue, + nameValue, + amountValue, + priceValue, + discountValue, + statusValue, + descriptionValue, + } = getValueFromObj(room); + + // Define your state schema + const stateSchema: TStateSchema = { + id: { value: idValue || '' }, + name: { value: nameValue || '', error: '' }, + amount: { value: amountValue || '', error: '' }, + price: { value: priceValue || '', error: '' }, + discount: { value: discountValue || '', error: '' }, + status: { value: statusValue || '', error: '' }, + description: { value: descriptionValue || '', error: '' }, + }; + + // prettier-ignore + const stateValidatorSchema: TValidator = { + name: addValidator({ + validatorFunc: isValidString, + prop: 'name' + }), + amount: addValidator({ + validatorFunc: isValidNumber, + prop: 'amount', + }), + price: addValidator({ + validatorFunc: isValidNumber, + prop: 'price', + }), + discount: addValidator({ + validatorFunc: isValidNumber, + prop: 'discount' + }), + status: addValidator({ + validatorFunc: skipCheck, + prop: 'status', + required: false, + }), + description: addValidator({ + validatorFunc: isValidString, + prop: 'description' + }), + }; + + // Submit form + const onSubmitForm = async (state: TKeyValue) => { + try { + if (isAdd) { + // Add request + const response = await sendRequest( + ROOM_PATH, + JSON.stringify(state), + 'POST', + ); + + if (response.statusCode === STATUS_CODE.CREATE) { + toast.success(ADD_SUCCESS); + + onResetForm(); + } else { + throw new Error(errorMsg(response.statusCode, response.msg)); + } + } else { + // Edit request + const response = await sendRequest( + ROOM_PATH + `/${room!.id}`, + JSON.stringify(state), + 'PUT', + ); + + if (response.statusCode == STATUS_CODE.OK) { + toast.success(EDIT_SUCCESS); + } else { + throw new Error(errorMsg(response.statusCode, response.msg)); + } + } + // Reload table data + setReload(!reload); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + + onClose(); + }; + + // Close and reset form + const closeAndReset = () => { + onClose(); + onResetForm(); + }; + + console.log(initialValue); + + // prettier-ignore + const { + values, + errors, + dirty, + handleOnChange, + handleOnSubmit, + disable } = + useForm( + stateSchema, + stateValidatorSchema, + onSubmitForm, + initialValue + ); + + // prettier-ignore + const { + id, + name, + amount, + price, + discount, + status, + description + } = values; + + // Clear form value when this is a add form + useEffect(() => { + if (isAdd) { + Object.keys(values).forEach((key) => (values[key] = '')); + } + }, [isAdd]); // eslint-disable-line + + // Reset form + const onResetForm = () => { + setReset(!reset); + Object.keys(values).forEach((key) => (values[key] = '')); + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + +