Merge pull request #20 from Nez27/feat/update-hooks-and-add-content-room-page

Update hooks and add content for room page
This commit is contained in:
Loi Phan
2023-11-01 15:02:13 +07:00
committed by GitHub
14 changed files with 798 additions and 49 deletions
+3 -1
View File
@@ -87,7 +87,9 @@ const getValueFromObj = <T>(obj: T | null = null): TKeyString => {
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 === 'boolean' ||
typeof tempValue === 'string' ||
typeof tempValue === 'number'
? tempValue
: '';
+20 -5
View File
@@ -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 };
+16 -10
View File
@@ -1,17 +1,14 @@
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';
// Constants
import { ERROR, VALUE } from '../constants/variables';
/**
* Custom hooks to validate your Form...
*
@@ -36,11 +33,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 +113,19 @@ const useForm = (
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
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 }));
@@ -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,
-15
View File
@@ -1,15 +0,0 @@
import styled from 'styled-components';
const StyledRoom = styled.main`
padding: 20px;
`;
const Room = () => {
return (
<StyledRoom>
<p>Room page</p>
</StyledRoom>
);
};
export default Room;
@@ -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 (
<Dialog title={'Add room'} onClose={onClose} ref={dialogRef}>
<RoomForm
onClose={onClose!}
reload={reload!}
setReload={setReload!}
room={data}
isAdd={isAdd!}
/>
</Dialog>
);
}) as React.FC<IDialogProps<TRoom>>;
export default RoomDialog;
+355
View File
@@ -0,0 +1,355 @@
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<React.SetStateAction<boolean>>;
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<TRoom>(room);
// Define your state schema
// prettier-ignore
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 (
<Form onSubmit={handleOnSubmit}>
<Input type="hidden" name="id" value={id as string} />
<FormRow
label="Name"
error={
// prettier-ignore
errors.name && dirty.name
? (errors.name as string)
: ''
}
>
<Input
type="text"
name="name"
value={name as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow
label="Amount"
// prettier-ignore
error={
errors.amount && dirty.amount
? (errors.amount as string)
: ''
}
>
<Input
type="text"
name="amount"
value={amount as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow
label="Price"
error={
// prettier-ignore
errors.price && dirty.price
? (errors.price as string)
: ''
}
>
<Input
type="text"
name="price"
value={price as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow
label="Discount"
error={
// prettier-ignore
errors.roomId && dirty.roomId
? (errors.roomId as string)
: ''
}
>
<Input
type="text"
name="discount"
value={discount as string}
onChange={handleOnChange}
/>
</FormRow>
<FormRow label="Status">
<Input
type="checkbox"
name="status"
checked={status as boolean}
onChange={handleOnChange}
/>
</FormRow>
<FormRow
label="Description"
error={
// prettier-ignore
errors.description && dirty.description
? (errors.description as string)
: ''
}
>
<TextArea
name="description"
rows={3}
value={description as string}
onChange={handleOnChange}
/>
</FormRow>
<Form.Action>
<FormBtn type="submit" name="submit" disabled={disable}>
{
// prettier-ignore
isAdd
? 'Add'
: 'Edit'
}
</FormBtn>
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
Close
</FormBtn>
</Form.Action>
</Form>
);
};
export default RoomForm;
+198
View File
@@ -0,0 +1,198 @@
import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
// Components
import { HiSquare2Stack } from 'react-icons/hi2';
import { HiTrash } from 'react-icons/hi';
import { StyledOperationTable } from './styled';
import Menus from '../../components/Menus/Menus';
import Table from '../../components/Table';
import Direction from '../../commons/styles/Direction';
import Message from '../../components/Message';
import Search from '../../components/Search';
import SortBy from '../../components/SortBy';
import OrderBy from '../../components/OrderBy';
// Types
import { TRoom } from '../../globals/types';
// Constants
import { useFetch } from '../../hooks/useFetch';
import { ROOM_PATH } from '../../constants/path';
import { STATUS_CODE } from '../../constants/statusCode';
import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages';
import { ROOM_PAGE } from '../../constants/variables';
// Styled
import Spinner from '../../commons/styles/Spinner';
// Utils
import { sendRequest } from '../../helpers/sendRequest';
interface IRoomRow {
room: TRoom;
openFormDialog: () => void;
setRoom: React.Dispatch<React.SetStateAction<TRoom | null>>;
reload: boolean;
setReload: React.Dispatch<React.SetStateAction<boolean>>;
}
const RoomRow = ({
room,
openFormDialog,
setRoom,
reload,
setReload,
}: IRoomRow) => {
const handleOnEdit = (room: TRoom) => {
setRoom(room);
openFormDialog();
};
const handleOnDelete = async (room: TRoom) => {
if (confirm(CONFIRM_DELETE)) {
const response = await sendRequest(
ROOM_PATH + `/${room.id}`,
null,
'DELETE',
);
if (response.statusCode === STATUS_CODE.OK) {
toast.success(DELETE_SUCCESS);
// Reload table
setReload(!reload);
}
}
};
const { id, name, amount, price, discount, status } = room;
// prettier-ignore
const statusText = status
? 'Valid'
: 'Invalid';
return (
<Table.Row>
<div>{id}</div>
<div>{name}</div>
<div>{amount}</div>
<div>{price}</div>
<div>{discount}</div>
<div>{statusText}</div>
<Menus.Menu>
<Menus.Toggle id={id} />
<Menus.List id={id}>
<Menus.Button
icon={<HiSquare2Stack />}
onClick={() => handleOnEdit(room)}
>
Edit
</Menus.Button>
<Menus.Button icon={<HiTrash />} onClick={() => handleOnDelete(room)}>
Delete
</Menus.Button>
</Menus.List>
</Menus.Menu>
</Table.Row>
);
};
interface IRoomTable {
reload: boolean;
setReload: React.Dispatch<React.SetStateAction<boolean>>;
openFormDialog: () => void;
setRoom?: React.Dispatch<React.SetStateAction<TRoom | null>>;
}
const RoomTable = ({
reload,
setReload,
openFormDialog,
setRoom,
}: IRoomTable) => {
const [nameSearch, setNameSearch] = useState('');
const [searchParams] = useSearchParams();
const [rooms, setRooms] = useState<TRoom[]>([]);
const sortByValue = searchParams.get('sortBy')
? searchParams.get('sortBy')!
: '';
const orderByValue = searchParams.get('orderBy')
? searchParams.get('orderBy')!
: '';
const { data, isPending, errorMsg } = useFetch(
'rooms',
'name',
nameSearch,
sortByValue,
orderByValue,
reload,
);
useEffect(() => {
if (data) {
setRooms(data);
} else {
setRooms([]);
}
if (errorMsg) {
console.error(errorMsg);
}
}, [data, errorMsg]);
return (
<>
<Direction>
<StyledOperationTable>
<OrderBy options={ROOM_PAGE.ORDERBY_OPTIONS} />
<SortBy options={ROOM_PAGE.SORTBY_OPTIONS} />
<Search
setValueSearch={setNameSearch}
setPlaceHolder="Search by name..."
/>
</StyledOperationTable>
{isPending && <Spinner />}
{rooms.length ? (
<Menus>
<Table columns="10% 20% 20% 20% 10% 10% 5%">
<Table.Header>
<div>Id</div>
<div>Name</div>
<div>Amount</div>
<div>Price</div>
<div>Discount</div>
<div>Status</div>
</Table.Header>
<Table.Body<TRoom>
data={rooms}
render={(room: TRoom) => (
<RoomRow
room={room}
key={room.id}
reload={reload}
setReload={setReload}
openFormDialog={openFormDialog}
setRoom={setRoom!}
/>
)}
/>
</Table>
</Menus>
) : (
!isPending && <Message>No data to show here!</Message>
)}
</Direction>
</>
);
};
export default RoomTable;
+58
View File
@@ -0,0 +1,58 @@
import { useRef, useState } from 'react';
// Components
import Direction from '../../commons/styles/Direction';
import Button from '../../commons/styles/Button';
import RoomTable from './Table';
// Styled
import { StyledRoom, Title } from './styled';
import RoomDialog from './Dialog';
// Types
import { TRoom } from '../../globals/types';
const Room = () => {
const dialogRef = useRef<HTMLDialogElement>();
const [reload, setReload] = useState(true);
const [room, setRoom] = useState<TRoom | null>(null);
const [isAdd, setIsAdd] = useState(false);
const openFormDialog = (isAddForm: boolean = false) => {
setIsAdd(isAddForm);
dialogRef.current?.showModal();
};
const closeFormDialog = () => {
dialogRef.current?.close();
};
return (
<>
<StyledRoom>
<Direction type="horizontal">
<Title>List Room</Title>
<Button onClick={() => openFormDialog(true)}>Add room</Button>
</Direction>
<RoomTable
reload={reload}
setReload={setReload}
openFormDialog={() => openFormDialog()}
setRoom={setRoom}
/>
</StyledRoom>
<RoomDialog
onClose={closeFormDialog}
ref={dialogRef}
setReload={setReload}
reload={reload}
data={room}
isAdd={isAdd}
/>
</>
);
};
export default Room;
+24
View File
@@ -0,0 +1,24 @@
import styled from 'styled-components';
const StyledRoom = styled.main`
padding: 20px;
padding-bottom: 100px;
display: flex;
flex-direction: column;
gap: 30px;
`;
const Title = styled.h2`
font-size: var(--fs-md);
color: var(--dark-text);
text-transform: capitalize;
`;
const StyledOperationTable = styled.div`
display: flex;
gap: 30px;
justify-content: flex-end;
`;
export { StyledRoom, Title, StyledOperationTable };
+7 -3
View File
@@ -7,16 +7,20 @@ import UserForm from './Form';
// Interfaces
import { IDialogProps } from '../../globals/interfaces';
// Types
import { TUser } from '../../globals/types';
const UserDialog = forwardRef((props, ref) => {
const dialogRef = ref as React.MutableRefObject<
HTMLDialogElement | undefined
>;
// prettier-ignore
const {
onClose,
setReload,
reload,
user,
data,
isAdd
} = props;
@@ -42,11 +46,11 @@ const UserDialog = forwardRef((props, ref) => {
onClose={onClose!}
reload={reload!}
setReload={setReload!}
user={user}
user={data}
isAdd={isAdd!}
/>
</Dialog>
);
}) as React.FC<IDialogProps>;
}) as React.FC<IDialogProps<TUser>>;
export default UserDialog;
+50 -13
View File
@@ -24,12 +24,12 @@ import useForm from '../../hooks/useForm';
// Utils
import {
isValidAddress,
isValidString,
isValidNumber,
isValidPhoneNumber,
isValidString,
isValidName,
} from '../../helpers/validators';
import { addValidator, getValueUser } from '../../helpers/utils.ts';
import { addValidator, getValueFromObj } from '../../helpers/utils.ts';
import { sendRequest } from '../../helpers/sendRequest.ts';
// Constants
@@ -66,26 +66,57 @@ const UserForm = ({
isAdd,
}: IUserFormProp) => {
const [reset, setReset] = useState(true);
if (isAdd) {
// If this is add form, reset value
user = null;
}
// prettier-ignore
const initialValue: string = isAdd
? ''
: user!
&& user.id;
const {
idValue,
nameValue,
identifiedCodeValue,
phoneValue,
roomIdValue,
addressValue,
} = getValueFromObj<TUser>(user);
// Define your state schema
// prettier-ignore
const stateSchema: TStateSchema = {
id: { value: getValueUser(user, 'id') },
name: { value: getValueUser(user, 'name'), error: '' },
identifiedCode: { value: getValueUser(user, 'identifiedCode'), error: '' },
phone: { value: getValueUser(user, 'phone'), error: '' },
roomId: { value: getValueUser(user, 'roomId'), error: '' },
address: { value: getValueUser(user, 'address'), error: '' },
id: { value: idValue || '' },
name: {
value: nameValue || '',
error: '' ,
},
identifiedCode: {
value: identifiedCodeValue || '',
error: '',
},
phone: {
value: phoneValue || '',
error: '',
},
roomId: {
value: roomIdValue || '',
error: '' ,
},
address: {
value: addressValue || '',
error: ''
},
};
// prettier-ignore
const stateValidatorSchema: TValidator = {
name: addValidator({
validatorFunc: isValidString,
validatorFunc: isValidName,
prop: 'full name'
}),
identifiedCode: addValidator({
@@ -101,7 +132,7 @@ const UserForm = ({
prop: 'room number'
}),
address: addValidator({
validatorFunc: isValidAddress,
validatorFunc: isValidString,
prop: 'address'
}),
};
@@ -122,6 +153,8 @@ const UserForm = ({
} else {
throw new Error(errorMsg(response.statusCode, response.msg));
}
onResetForm();
} else {
// Edit request
const response = await sendRequest(
@@ -138,7 +171,6 @@ const UserForm = ({
}
// Reload table data
setReload(!reload);
onResetForm();
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
@@ -280,7 +312,12 @@ const UserForm = ({
<Form.Action>
<FormBtn type="submit" name="submit" disabled={disable}>
Add
{
// prettier-ignore
isAdd
? 'Add'
: 'Edit'
}
</FormBtn>
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
Close
+5 -1
View File
@@ -121,6 +121,7 @@ const UserTable = ({
const { data, isPending, errorMsg } = useFetch(
'users',
'phone',
phoneSearch,
sortByValue,
orderByValue,
@@ -146,7 +147,10 @@ const UserTable = ({
<OrderBy options={USER_PAGE.ORDERBY_OPTIONS} />
<SortBy options={USER_PAGE.SORTBY_OPTIONS} />
<Search setPhoneSearch={setPhoneSearch} />
<Search
setValueSearch={setPhoneSearch}
setPlaceHolder="Search by phone..."
/>
</StyledOperationTable>
{isPending && <Spinner />}
+1 -1
View File
@@ -48,7 +48,7 @@ const User = () => {
ref={dialogRef}
setReload={setReload}
reload={reload}
user={user}
data={user}
isAdd={isAdd}
/>
</>