mirror of
https://github.com/Nezumi-2711/react-training.git
synced 2026-09-22 20:01:59 +00:00
Merge pull request #22 from Nez27/feat/apply-form-hook
Apply react-hook-form
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"@emotion/is-prop-valid": "^1.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-router-dom": "^6.17.0",
|
||||
|
||||
Generated
+12
@@ -14,6 +14,9 @@ dependencies:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.47.0
|
||||
version: 7.47.0(react@18.2.0)
|
||||
react-hot-toast:
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -1815,6 +1818,15 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-hook-form@7.47.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-hot-toast@2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import {
|
||||
FieldValues,
|
||||
RegisterOptions,
|
||||
UseFormRegister,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import { StyledSelect } from './styled';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export interface ISelectOptions {
|
||||
value: string;
|
||||
@@ -6,27 +13,82 @@ export interface ISelectOptions {
|
||||
}
|
||||
interface ISelect {
|
||||
options: ISelectOptions[];
|
||||
value: string;
|
||||
onChange: React.ChangeEventHandler<HTMLSelectElement>;
|
||||
name?: string;
|
||||
optionsConfigForm?: RegisterOptions<FieldValues, string> | undefined;
|
||||
value?: string;
|
||||
onChange?: React.ChangeEventHandler<HTMLSelectElement> | undefined;
|
||||
id?: string;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
const Select = ({ options, value, onChange, name }: ISelect) => {
|
||||
if (!options) return;
|
||||
interface IRender extends ISelect {
|
||||
register: UseFormRegister<FieldValues>;
|
||||
children: ReactNode[];
|
||||
}
|
||||
|
||||
const RenderSelect = ({
|
||||
options,
|
||||
id,
|
||||
value,
|
||||
optionsConfigForm,
|
||||
onChange,
|
||||
register,
|
||||
children,
|
||||
ariaLabel,
|
||||
}: IRender) => {
|
||||
if (options && !register) {
|
||||
return (
|
||||
<StyledSelect
|
||||
aria-label={ariaLabel}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
>
|
||||
{children}
|
||||
</StyledSelect>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledSelect
|
||||
aria-label={ariaLabel}
|
||||
id={id}
|
||||
value={value}
|
||||
{...register(id!, optionsConfigForm)}
|
||||
onChange={onChange}
|
||||
>
|
||||
{children}
|
||||
</StyledSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const Select = ({
|
||||
options,
|
||||
id,
|
||||
optionsConfigForm,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel
|
||||
}: ISelect) => {
|
||||
const { register } = useFormContext() ?? {};
|
||||
|
||||
if (!options) return;
|
||||
|
||||
return (
|
||||
<RenderSelect
|
||||
register={register}
|
||||
options={options}
|
||||
optionsConfigForm={optionsConfigForm}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
aria-label="Sort"
|
||||
name={name}
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option value={option.value} key={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
</RenderSelect>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ const SortBy = memo(({ options }: ISortByProps) => {
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
value={sortBy}
|
||||
onChange={handleChange}
|
||||
ariaLabel='Sort'
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const INVALID_FIELD = 'This field is invalid format!';
|
||||
const INVALID_PHONE = 'Phone number is invalid!';
|
||||
const REQUIRED_FIELD_ERROR = 'This field is required!';
|
||||
const INVALID_DISCOUNT = 'Discount should be greater than 0 and less than 100!';
|
||||
|
||||
export { REQUIRED_FIELD_ERROR, INVALID_DISCOUNT, INVALID_FIELD, INVALID_PHONE };
|
||||
@@ -1,7 +1,3 @@
|
||||
const invalidFormatMsg = (field: string) => {
|
||||
return `Invalid ${field} format`;
|
||||
};
|
||||
|
||||
const errorMsg = (errorCode: number, msg: string) => {
|
||||
return `Error code: ${errorCode}. Message: ${msg}`;
|
||||
};
|
||||
@@ -10,17 +6,11 @@ const ADD_SUCCESS = 'Add success';
|
||||
const EDIT_SUCCESS = 'Edit success';
|
||||
const CONFIRM_DELETE = 'Are you sure to delete it?';
|
||||
const DELETE_SUCCESS = 'Delete success';
|
||||
const REQUIRED_FIELD_ERROR = 'This is required field';
|
||||
const DISCOUNT_FIELD_ERROR =
|
||||
'Discount should be greater than 0 and less than 100';
|
||||
|
||||
export {
|
||||
invalidFormatMsg,
|
||||
ADD_SUCCESS,
|
||||
errorMsg,
|
||||
EDIT_SUCCESS,
|
||||
CONFIRM_DELETE,
|
||||
DELETE_SUCCESS,
|
||||
REQUIRED_FIELD_ERROR,
|
||||
DISCOUNT_FIELD_ERROR,
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ type TRoom = {
|
||||
name: string;
|
||||
price: number;
|
||||
discount: number;
|
||||
description: string;
|
||||
status: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Constants
|
||||
import { REQUIRED_FIELD_ERROR, invalidFormatMsg } from '../constants/messages';
|
||||
import { REQUIRED_FIELD_ERROR } from '../constants/formValidateMessage';
|
||||
|
||||
// Types
|
||||
import {
|
||||
@@ -18,15 +18,6 @@ const isBool = (value: unknown) => {
|
||||
return typeof value === 'boolean';
|
||||
};
|
||||
|
||||
/**
|
||||
* The function check value has type object or not
|
||||
* @param value The value need to checked
|
||||
* @returns A boolean indicating whether or not the argument has type object.
|
||||
*/
|
||||
const isObject = (value: unknown) => {
|
||||
return typeof value === 'object' && value !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set required error for value
|
||||
* @param value The value set required or not
|
||||
@@ -54,36 +45,6 @@ const getPropValues = (stateSchema: TStateSchema, prop?: TPropValues) => {
|
||||
}, {} as TKeyValue);
|
||||
};
|
||||
|
||||
type TValidator = {
|
||||
validatorFunc: (value: string) => boolean;
|
||||
prop?: string;
|
||||
customErrorMsg?: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create validator object
|
||||
* @param param0 Pass TValidator object
|
||||
* @returns An object contains condition validator
|
||||
*/
|
||||
const addValidator = ({
|
||||
validatorFunc,
|
||||
prop = '',
|
||||
customErrorMsg = '',
|
||||
required = true,
|
||||
}: TValidator) => {
|
||||
return {
|
||||
required,
|
||||
validator: {
|
||||
func: validatorFunc,
|
||||
// prettier-ignore
|
||||
error: customErrorMsg
|
||||
? customErrorMsg
|
||||
: invalidFormatMsg(prop),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the object contains values of object pass
|
||||
* @param obj Object need to get value
|
||||
@@ -124,19 +85,16 @@ const searchQuery = (
|
||||
columnSearch: string,
|
||||
keySearch: string,
|
||||
sort: string,
|
||||
order: string,
|
||||
order: string
|
||||
) => {
|
||||
// prettier-ignore
|
||||
const phoneParams = keySearch
|
||||
? `${columnSearch}_like=` + keySearch
|
||||
const phoneParams = keySearch
|
||||
? `${columnSearch}_like=` + keySearch
|
||||
: '';
|
||||
|
||||
// prettier-ignore
|
||||
const sortParams = sort
|
||||
? '_sort=' + sort
|
||||
const sortParams = sort
|
||||
? '_sort=' + sort
|
||||
: '';
|
||||
|
||||
// prettier-ignore
|
||||
const orderParams = order
|
||||
? '_order=' + order
|
||||
: '';
|
||||
@@ -159,11 +117,9 @@ const searchQuery = (
|
||||
};
|
||||
|
||||
export {
|
||||
isObject,
|
||||
isRequired,
|
||||
getPropValues,
|
||||
getValueFromObj,
|
||||
addValidator,
|
||||
searchQuery,
|
||||
REQUIRED_FIELD_ERROR,
|
||||
REQUIRED_FIELD_ERROR
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @param value Value need to be checked
|
||||
* @returns A boolean indicating whether or not the argument has valid
|
||||
*/
|
||||
const isValidNumber = (value: string): boolean => {
|
||||
return /^[0-9]*$/.test(value);
|
||||
const isValidNumber = (value: unknown): boolean => {
|
||||
return /^[0-9]*$/.test(value as string);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,10 +54,11 @@ const useFetch = (
|
||||
const response = await fetch(BASE_URL + path + '?' + query);
|
||||
const json = await response.json();
|
||||
|
||||
if (!response.ok)
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Error code: ${response.status} \n Messages: ${response.statusText}`,
|
||||
`Error code: ${response.status} \n Messages: ${response.text}`,
|
||||
);
|
||||
}
|
||||
|
||||
setIsPending(false);
|
||||
setData(json);
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import { useState, useEffect, useCallback, ChangeEvent } from 'react';
|
||||
|
||||
// Utils
|
||||
import { getPropValues, isObject, isRequired } from '../helpers/utils';
|
||||
|
||||
// Types
|
||||
import { TKeyValue, TValidator } from '../globals/types';
|
||||
|
||||
// Constants
|
||||
import { ERROR, INITIAL_STATE_SCHEMA, VALUE } from '../constants/variables';
|
||||
|
||||
/**
|
||||
* Custom hooks to validate your Form...
|
||||
*
|
||||
* @param stateSchema model you stateSchema.
|
||||
* @param stateValidatorSchema model your validation.
|
||||
* @param submitFormCallback function to be execute during form submission.
|
||||
* @returns
|
||||
*/
|
||||
const useForm = (
|
||||
stateSchema = {},
|
||||
stateValidatorSchema = {} as TValidator,
|
||||
submitFormCallback: (values: TKeyValue) => void,
|
||||
initialValue: string = '',
|
||||
) => {
|
||||
const [values, setValues] = useState<TKeyValue>(INITIAL_STATE_SCHEMA);
|
||||
const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR));
|
||||
const [valid, isValid] = useState(getPropValues(stateSchema));
|
||||
const [disable, setDisable] = useState(true);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
||||
// Get a local copy of stateSchema
|
||||
useEffect(() => {
|
||||
setInitialErrorState(initialValue);
|
||||
setDisable(true);
|
||||
isValid(getPropValues(stateSchema));
|
||||
setValues(getPropValues(stateSchema, VALUE));
|
||||
|
||||
// If initial value true, setValues again from stateSchema
|
||||
// and enabled button
|
||||
if (initialValue) {
|
||||
setValues(getPropValues(stateSchema, VALUE));
|
||||
setDisable(false);
|
||||
}
|
||||
}, [initialValue]); // eslint-disable-line
|
||||
|
||||
// Validate fields in forms
|
||||
const validateFormFields = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const validator = stateValidatorSchema;
|
||||
// Making sure that stateValidatorSchema name is same in
|
||||
// stateSchema
|
||||
if (!validator[name]) return;
|
||||
|
||||
const field = validator[name];
|
||||
|
||||
let error = '';
|
||||
|
||||
// Skip check id field
|
||||
if (name !== 'id' && name !== 'roomId') {
|
||||
error = isRequired(value, field!.required);
|
||||
|
||||
if (isObject(field['validator']) && error === '') {
|
||||
const fieldValidator = field['validator'];
|
||||
|
||||
// Test the function callback if the value is meet the criteria
|
||||
const testFunc = fieldValidator!['func'];
|
||||
if (!testFunc!(value)) {
|
||||
error = fieldValidator!['error']!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
},
|
||||
[stateValidatorSchema],
|
||||
);
|
||||
|
||||
// Set Initial Error State
|
||||
// When hooks was first rendered...
|
||||
const setInitialErrorState = useCallback(
|
||||
(initialValue: string) => {
|
||||
Object.keys(errors).map((name) =>
|
||||
setErrors((prevState) => ({
|
||||
...prevState,
|
||||
[name]: !initialValue // Skip error when initialValue have values
|
||||
? validateFormFields(name, values[name] as string)
|
||||
: '',
|
||||
})),
|
||||
);
|
||||
},
|
||||
[errors, values, validateFormFields],
|
||||
);
|
||||
|
||||
// Used to disable submit button if there's a value in errors
|
||||
// or the required field in state has no value.
|
||||
// Wrapped in useCallback to cached the function to avoid intensive memory leaked
|
||||
// in every re-render in component
|
||||
const validateErrorState = useCallback(
|
||||
() => Object.values(errors).some((error) => error),
|
||||
[errors],
|
||||
);
|
||||
|
||||
// For every changed in our state this will be fired
|
||||
// To be able to disable the button
|
||||
useEffect(() => {
|
||||
if (isDirty) {
|
||||
setDisable(validateErrorState());
|
||||
}
|
||||
}, [errors, isDirty, validateErrorState]);
|
||||
|
||||
// Event handler for handling changes in input.
|
||||
const handleOnChange = useCallback(
|
||||
(
|
||||
event: ChangeEvent<
|
||||
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
>,
|
||||
) => {
|
||||
setIsDirty(true);
|
||||
|
||||
let error = '';
|
||||
const name = (event.target! as HTMLInputElement).name;
|
||||
let value: string | boolean = '';
|
||||
|
||||
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 }));
|
||||
isValid((prevState) => ({ ...prevState, [name]: true }));
|
||||
},
|
||||
[validateFormFields],
|
||||
);
|
||||
|
||||
const handleOnSubmit = useCallback(
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Making sure that there's no error in the state
|
||||
// before calling the submit callback function
|
||||
// and disabled button
|
||||
|
||||
if (!validateErrorState()) {
|
||||
submitFormCallback(values);
|
||||
setDisable(true);
|
||||
}
|
||||
},
|
||||
[validateErrorState, submitFormCallback, values],
|
||||
);
|
||||
|
||||
return {
|
||||
handleOnChange,
|
||||
handleOnSubmit,
|
||||
values,
|
||||
errors,
|
||||
disable,
|
||||
setValues,
|
||||
setErrors,
|
||||
valid,
|
||||
};
|
||||
};
|
||||
|
||||
export default useForm;
|
||||
@@ -1,46 +1,20 @@
|
||||
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/LabelControl/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 {
|
||||
isValidDiscount,
|
||||
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,
|
||||
DISCOUNT_FIELD_ERROR,
|
||||
EDIT_SUCCESS,
|
||||
errorMsg,
|
||||
} from '../../constants/messages.ts';
|
||||
import { TRoom } from '../../globals/types.ts';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ROOM_PATH } from '../../constants/path.ts';
|
||||
import { STATUS_CODE } from '../../constants/statusCode.ts';
|
||||
import toast from 'react-hot-toast';
|
||||
import { ADD_SUCCESS, EDIT_SUCCESS, errorMsg } from '../../constants/messages.ts';
|
||||
import { sendRequest } from '../../helpers/sendRequest.ts';
|
||||
import Input from '../../commons/styles/Input.ts';
|
||||
import FormRow from '../../components/LabelControl/index.tsx';
|
||||
import { INVALID_DISCOUNT, INVALID_FIELD, REQUIRED_FIELD_ERROR } from '../../constants/formValidateMessage.ts';
|
||||
import { isValidNumber, isValidString } from '../../helpers/validators.ts';
|
||||
import Form from '../../components/Form/index.tsx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const FormBtn = styled(Button)`
|
||||
width: 100%;
|
||||
@@ -67,106 +41,34 @@ const RoomForm = ({
|
||||
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.toString();
|
||||
|
||||
const formMethods = useForm<TRoom>();
|
||||
const {
|
||||
idValue,
|
||||
nameValue,
|
||||
priceValue,
|
||||
discountValue,
|
||||
statusValue,
|
||||
descriptionValue,
|
||||
} = getValueFromObj<TRoom>(room);
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isDirty, isValid },
|
||||
trigger,
|
||||
} = formMethods;
|
||||
|
||||
// Define your state schema
|
||||
// prettier-ignore
|
||||
const stateSchema: TStateSchema = {
|
||||
id: {
|
||||
value: idValue || ''
|
||||
},
|
||||
name: {
|
||||
value: nameValue || '',
|
||||
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'
|
||||
}),
|
||||
price: addValidator({
|
||||
validatorFunc: isValidNumber,
|
||||
prop: 'price',
|
||||
}),
|
||||
discount: addValidator({
|
||||
validatorFunc: isValidDiscount,
|
||||
customErrorMsg: DISCOUNT_FIELD_ERROR
|
||||
}),
|
||||
status: addValidator({
|
||||
validatorFunc: skipCheck,
|
||||
prop: 'status',
|
||||
required: false,
|
||||
}),
|
||||
description: addValidator({
|
||||
validatorFunc: isValidString,
|
||||
prop: 'description'
|
||||
}),
|
||||
};
|
||||
useEffect(() => {
|
||||
if(room) {
|
||||
reset(room);
|
||||
}
|
||||
}, [room, reset]);
|
||||
|
||||
// Submit form
|
||||
const onSubmitForm = async (state: TKeyValue) => {
|
||||
// Convert to room type
|
||||
const data: TRoom = {
|
||||
id: +state.id!,
|
||||
name: '' + state.name,
|
||||
discount: +state.discount!,
|
||||
price: +state.price!,
|
||||
status: !!state.status,
|
||||
description: '' + state.description,
|
||||
};
|
||||
|
||||
const onSubmit = async (room: TRoom) => {
|
||||
try {
|
||||
if (isAdd) {
|
||||
// Add request
|
||||
const response = await sendRequest(
|
||||
ROOM_PATH,
|
||||
JSON.stringify(data),
|
||||
'POST',
|
||||
JSON.stringify(room),
|
||||
'POST'
|
||||
);
|
||||
|
||||
if (response.statusCode === STATUS_CODE.CREATE) {
|
||||
toast.success(ADD_SUCCESS);
|
||||
|
||||
onResetForm();
|
||||
} else {
|
||||
throw new Error(errorMsg(response.statusCode, response.msg));
|
||||
}
|
||||
@@ -174,8 +76,8 @@ const RoomForm = ({
|
||||
// Edit request
|
||||
const response = await sendRequest(
|
||||
ROOM_PATH + `/${room!.id}`,
|
||||
JSON.stringify(data),
|
||||
'PUT',
|
||||
JSON.stringify(room),
|
||||
'PUT'
|
||||
);
|
||||
|
||||
if (response.statusCode == STATUS_CODE.OK) {
|
||||
@@ -192,146 +94,73 @@ const RoomForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Close and reset form
|
||||
const closeAndReset = () => {
|
||||
onClose();
|
||||
onResetForm();
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
const {
|
||||
values,
|
||||
errors,
|
||||
valid,
|
||||
handleOnChange,
|
||||
handleOnSubmit,
|
||||
disable } =
|
||||
useForm(
|
||||
stateSchema,
|
||||
stateValidatorSchema,
|
||||
onSubmitForm,
|
||||
initialValue
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
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 && valid.name
|
||||
? (errors.name as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="name"
|
||||
value={name as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
label="Price"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.price && valid.price
|
||||
? (errors.price as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="price"
|
||||
value={price as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<Form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input type="hidden" id="id" {...register('id')} />
|
||||
<FormRow label="Name" error={errors?.name?.message}>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
{...register('name', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkValidName: (value) =>
|
||||
isValidString(value) || INVALID_FIELD,
|
||||
},
|
||||
onChange: () => trigger('name'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label="Discount"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.discount && valid.discount
|
||||
? (errors.discount as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="discount"
|
||||
value={discount as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
label="Price"
|
||||
error={errors?.price?.message}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
id="price"
|
||||
{...register('price', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkIdentifiedCode: (v) => isValidNumber(v) || INVALID_FIELD,
|
||||
},
|
||||
onChange: () => trigger('price'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow label="Status">
|
||||
<Input
|
||||
type="checkbox"
|
||||
name="status"
|
||||
checked={status as boolean}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow label="discount" error={errors?.discount?.message}>
|
||||
<Input
|
||||
type="text"
|
||||
id="phone"
|
||||
{...register('discount', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkPhoneNum: (v) => isValidNumber(v) || INVALID_DISCOUNT,
|
||||
},
|
||||
onChange: () => trigger('discount'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label="Description"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.description && valid.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'
|
||||
: 'Save'
|
||||
}
|
||||
</FormBtn>
|
||||
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
|
||||
Close
|
||||
</FormBtn>
|
||||
</Form.Action>
|
||||
</Form>
|
||||
<Form.Action>
|
||||
<FormBtn type="submit" name="submit" disabled={!isDirty || !isValid}>
|
||||
{
|
||||
// prettier-ignore
|
||||
isAdd
|
||||
? 'Add'
|
||||
: 'Save'
|
||||
}
|
||||
</FormBtn>
|
||||
<FormBtn type="button" styled="secondary" onClick={onClose}>
|
||||
Close
|
||||
</FormBtn>
|
||||
</Form.Action>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const RoomRow = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { id, name, price, discount, status } = room;
|
||||
const { id, name, price, status } = room;
|
||||
|
||||
// prettier-ignore
|
||||
const statusText = status
|
||||
@@ -79,7 +79,6 @@ const RoomRow = ({
|
||||
<div>{id}</div>
|
||||
<div>{name}</div>
|
||||
<div>{price}</div>
|
||||
<div>{discount}</div>
|
||||
<div>{statusText}</div>
|
||||
|
||||
<Menus.Menu>
|
||||
@@ -162,12 +161,11 @@ const RoomTable = ({
|
||||
|
||||
{rooms.length ? (
|
||||
<Menus>
|
||||
<Table columns="10% 30% 20% 10% 20% 5%">
|
||||
<Table columns="10% 40% 20% 20% 5%">
|
||||
<Table.Header>
|
||||
<div>Id</div>
|
||||
<div>Name</div>
|
||||
<div>Price</div>
|
||||
<div>Discount</div>
|
||||
<div>Status</div>
|
||||
</Table.Header>
|
||||
<Table.Body<TRoom>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import toast from 'react-hot-toast';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
// Styled
|
||||
import Input from '../../commons/styles/Input';
|
||||
@@ -9,27 +9,17 @@ import TextArea from '../../commons/styles/TextArea';
|
||||
// Components
|
||||
import Form from '../../components/Form';
|
||||
import FormRow from '../../components/LabelControl/index.tsx';
|
||||
import Button from '../../commons/styles/Button.ts';
|
||||
|
||||
// Types
|
||||
import {
|
||||
TKeyValue,
|
||||
TStateSchema,
|
||||
TUser,
|
||||
TValidator,
|
||||
} from '../../globals/types';
|
||||
|
||||
// Hooks
|
||||
import useForm from '../../hooks/useForm';
|
||||
import { TKeyValue, TUser } from '../../globals/types';
|
||||
|
||||
// Utils
|
||||
import { sendRequest } from '../../helpers/sendRequest.ts';
|
||||
import {
|
||||
isValidString,
|
||||
isValidNumber,
|
||||
isValidPhoneNumber,
|
||||
} from '../../helpers/validators';
|
||||
import { addValidator, getValueFromObj } from '../../helpers/utils.ts';
|
||||
import { sendRequest } from '../../helpers/sendRequest.ts';
|
||||
isValidString,
|
||||
} from '../../helpers/validators.ts';
|
||||
|
||||
// Constants
|
||||
import { STATUS_CODE } from '../../constants/statusCode.ts';
|
||||
@@ -40,17 +30,13 @@ import {
|
||||
} from '../../constants/messages.ts';
|
||||
import { USER_PATH } from '../../constants/path.ts';
|
||||
import Select, { ISelectOptions } from '../../components/Select';
|
||||
|
||||
// Hooks
|
||||
import { useFetch } from '../../hooks/useFetch.ts';
|
||||
|
||||
const FormBtn = styled(Button)`
|
||||
width: 100%;
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
background-color: var(--disabled-btn-color);
|
||||
cursor: no-drop;
|
||||
}
|
||||
`;
|
||||
// Styled
|
||||
import { FormBtn } from './styled.ts';
|
||||
import { INVALID_FIELD, INVALID_PHONE, REQUIRED_FIELD_ERROR } from '../../constants/formValidateMessage.ts';
|
||||
|
||||
interface IUserFormProp {
|
||||
onClose: () => void;
|
||||
@@ -67,10 +53,27 @@ const UserForm = ({
|
||||
user,
|
||||
isAdd,
|
||||
}: IUserFormProp) => {
|
||||
const [reset, setReset] = useState(true);
|
||||
const formMethods = useForm<TUser>({
|
||||
defaultValues: {
|
||||
roomId: 1,
|
||||
},
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isDirty, isValid },
|
||||
trigger,
|
||||
} = formMethods;
|
||||
const [options, setOptions] = useState<ISelectOptions[]>();
|
||||
const { data, errorFetchMsg } = useFetch('rooms');
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset(user);
|
||||
}
|
||||
}, [reset, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const tempData = data as TKeyValue[];
|
||||
@@ -90,95 +93,15 @@ const UserForm = ({
|
||||
}
|
||||
}, [data, errorFetchMsg]);
|
||||
|
||||
if (isAdd) {
|
||||
// If this is add form, reset value
|
||||
user = null;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const initialValue: string = isAdd
|
||||
? ''
|
||||
: user!
|
||||
&& user.id.toLocaleString();
|
||||
|
||||
const {
|
||||
idValue,
|
||||
nameValue,
|
||||
identifiedCodeValue,
|
||||
phoneValue,
|
||||
roomIdValue,
|
||||
addressValue,
|
||||
} = getValueFromObj<TUser>(user);
|
||||
|
||||
// Define your state schema
|
||||
// prettier-ignore
|
||||
const stateSchema: TStateSchema = {
|
||||
id: { value: idValue || '' },
|
||||
name: {
|
||||
value: nameValue || '',
|
||||
error: '' ,
|
||||
},
|
||||
identifiedCode: {
|
||||
value: identifiedCodeValue || '',
|
||||
error: '',
|
||||
},
|
||||
phone: {
|
||||
value: phoneValue || '',
|
||||
error: '',
|
||||
},
|
||||
roomId: {
|
||||
value: roomIdValue || '' + (options && options[0].value),
|
||||
error: '' ,
|
||||
},
|
||||
address: {
|
||||
value: addressValue || '',
|
||||
error: ''
|
||||
},
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
const stateValidatorSchema: TValidator = {
|
||||
name: addValidator({
|
||||
validatorFunc: isValidString,
|
||||
prop: 'full name'
|
||||
}),
|
||||
identifiedCode: addValidator({
|
||||
validatorFunc: isValidNumber,
|
||||
prop: 'identified code',
|
||||
}),
|
||||
phone: addValidator({
|
||||
validatorFunc: isValidPhoneNumber,
|
||||
prop: 'phone number',
|
||||
}),
|
||||
roomId: addValidator({
|
||||
validatorFunc: isValidNumber,
|
||||
prop: 'room number'
|
||||
}),
|
||||
address: addValidator({
|
||||
validatorFunc: isValidString,
|
||||
prop: 'address'
|
||||
}),
|
||||
};
|
||||
|
||||
// Submit form
|
||||
const onSubmitForm = async (state: TKeyValue) => {
|
||||
// Convert to user type
|
||||
const user: TUser = {
|
||||
id: +state.id!,
|
||||
name: '' + state.name,
|
||||
identifiedCode: '' + state.identifiedCode,
|
||||
phone: '' + state.phone,
|
||||
roomId: +state.roomId!,
|
||||
address: '' + state.address,
|
||||
};
|
||||
|
||||
const onSubmit = async (user: TUser) => {
|
||||
try {
|
||||
if (isAdd) {
|
||||
// Add request
|
||||
const response = await sendRequest(
|
||||
USER_PATH,
|
||||
JSON.stringify(user),
|
||||
'POST',
|
||||
'POST'
|
||||
);
|
||||
|
||||
if (response.statusCode === STATUS_CODE.CREATE) {
|
||||
@@ -188,14 +111,13 @@ const UserForm = ({
|
||||
}
|
||||
|
||||
// TODO Update status when user create
|
||||
|
||||
onResetForm();
|
||||
|
||||
} else {
|
||||
// Edit request
|
||||
const response = await sendRequest(
|
||||
USER_PATH + `/${user!.id}`,
|
||||
JSON.stringify(data),
|
||||
'PUT',
|
||||
JSON.stringify(user),
|
||||
'PUT'
|
||||
);
|
||||
|
||||
if (response.statusCode == STATUS_CODE.OK) {
|
||||
@@ -212,153 +134,96 @@ const UserForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Close and reset form
|
||||
const closeAndReset = () => {
|
||||
onClose();
|
||||
onResetForm();
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
const {
|
||||
values,
|
||||
errors,
|
||||
valid,
|
||||
handleOnChange,
|
||||
handleOnSubmit,
|
||||
disable } =
|
||||
useForm(
|
||||
stateSchema,
|
||||
stateValidatorSchema,
|
||||
onSubmitForm,
|
||||
initialValue
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
identifiedCode,
|
||||
phone,
|
||||
roomId,
|
||||
address
|
||||
} = values;
|
||||
|
||||
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="Full Name"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.name && valid.name
|
||||
? (errors.name as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="name"
|
||||
value={name as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormProvider {...formMethods}>
|
||||
<Form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input type="hidden" id="id" {...register('id')} />
|
||||
<FormRow label="Full Name" error={errors?.name?.message}>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
{...register('name', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkValidName: (value) =>
|
||||
isValidString(value) || INVALID_FIELD,
|
||||
},
|
||||
onChange: () => trigger('name'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label="Identified Code"
|
||||
error={
|
||||
errors.identifiedCode && valid.identifiedCode
|
||||
? (errors.identifiedCode as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="identifiedCode"
|
||||
value={identifiedCode as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
label="Identified Code"
|
||||
error={errors?.identifiedCode?.message}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
id="identifiedCode"
|
||||
{...register('identifiedCode', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkIdentifiedCode: (v) =>
|
||||
isValidNumber(v) || INVALID_FIELD,
|
||||
},
|
||||
onChange: () => trigger('identifiedCode'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label="Phone"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.phone && valid.phone
|
||||
? (errors.phone as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
name="phone"
|
||||
value={phone as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow label="Phone" error={errors?.phone?.message}>
|
||||
<Input
|
||||
type="text"
|
||||
id="phone"
|
||||
{...register('phone', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkPhoneNum: (v) =>
|
||||
isValidPhoneNumber(v) || INVALID_PHONE,
|
||||
},
|
||||
onChange: () => trigger('phone'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label="Room"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.roomId && valid.roomId
|
||||
? (errors.roomId as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Select
|
||||
name="roomId"
|
||||
value={roomId as string}
|
||||
onChange={handleOnChange}
|
||||
options={options!}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow label="Room">
|
||||
<Select id="roomId" options={options!} ariaLabel='RoomId'/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
label="Address"
|
||||
error={
|
||||
// prettier-ignore
|
||||
errors.address && valid.address
|
||||
? (errors.address as string)
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
name="address"
|
||||
rows={3}
|
||||
value={address as string}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow label="Address" error={errors.address?.message}>
|
||||
<TextArea
|
||||
id="address"
|
||||
rows={3}
|
||||
{...register('address', {
|
||||
required: REQUIRED_FIELD_ERROR,
|
||||
validate: {
|
||||
checkValidString: (v) =>
|
||||
isValidString(v) || INVALID_FIELD,
|
||||
},
|
||||
onChange: () => trigger('address'),
|
||||
})}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<Form.Action>
|
||||
<FormBtn type="submit" name="submit" disabled={disable}>
|
||||
{
|
||||
// prettier-ignore
|
||||
isAdd
|
||||
<Form.Action>
|
||||
<FormBtn type="submit" name="submit" disabled={!isDirty || !isValid}>
|
||||
{
|
||||
// prettier-ignore
|
||||
isAdd
|
||||
? 'Add'
|
||||
: 'Save'
|
||||
}
|
||||
</FormBtn>
|
||||
<FormBtn type="button" styled="secondary" onClick={closeAndReset}>
|
||||
Close
|
||||
</FormBtn>
|
||||
</Form.Action>
|
||||
</Form>
|
||||
}
|
||||
</FormBtn>
|
||||
<FormBtn type="button" styled="secondary" onClick={onClose}>
|
||||
Close
|
||||
</FormBtn>
|
||||
</Form.Action>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Components
|
||||
import Button from '../../commons/styles/Button';
|
||||
|
||||
const StyledUser = styled.main`
|
||||
padding: 20px;
|
||||
padding-bottom: 100px;
|
||||
@@ -21,4 +24,14 @@ const StyledOperationTable = styled.div`
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
export { StyledUser, Title, StyledOperationTable };
|
||||
const FormBtn = styled(Button)`
|
||||
width: 100%;
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
background-color: var(--disabled-btn-color);
|
||||
cursor: no-drop;
|
||||
}
|
||||
`;
|
||||
|
||||
export { StyledUser, Title, StyledOperationTable, FormBtn };
|
||||
|
||||
Reference in New Issue
Block a user