Update hooks and add content for room page

This commit is contained in:
2023-11-01 14:25:45 +07:00
parent 4c92c2dcc0
commit 47a4c1be41
13 changed files with 770 additions and 48 deletions
-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;
+334
View File
@@ -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<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
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 };
+5 -3
View File
@@ -6,17 +6,19 @@ import UserForm from './Form';
// Interfaces
import { IDialogProps } from '../../globals/interfaces';
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 +44,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}
/>
</>