Merge pull request #16 from Nez27/feat/edit-delete-user

Implement add and edit user function
This commit is contained in:
Loi Phan
2023-10-30 10:23:21 +07:00
committed by GitHub
12 changed files with 243 additions and 141 deletions
+2
View File
@@ -12,6 +12,8 @@ dist
dist-ssr dist-ssr
*.local *.local
src/db.json
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
-76
View File
@@ -1,76 +0,0 @@
{
"users": [
{
"id": "123",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "231",
"name": "Phan Huu Loi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Ho Chi Minh"
},
{
"id": "312",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "523",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "534",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "756",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "23",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "756",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
},
{
"id": "756",
"name": "Nezumi",
"identifiedCode": "123",
"phone": "0937425123",
"roomId": 246,
"address": "Da Nang"
}
]
}
+13 -3
View File
@@ -2,10 +2,20 @@ const invalidFormatMsg = (field: string) => {
return `Invalid ${field} format`; return `Invalid ${field} format`;
}; };
const ADD_SUCCESS = 'Add success';
const errorMsg = (errorCode: number, msg: string) => { const errorMsg = (errorCode: number, msg: string) => {
return `Error code: ${errorCode}. Message: ${msg}`; return `Error code: ${errorCode}. Message: ${msg}`;
}; };
export { invalidFormatMsg, ADD_SUCCESS, errorMsg }; const ADD_SUCCESS = 'Add success';
const EDIT_SUCCESS = 'Edit success';
const CONFIRM_DELETE = 'Are you sure to delete it?';
const DELETE_SUCCESS = 'Delete success';
export {
invalidFormatMsg,
ADD_SUCCESS,
errorMsg,
EDIT_SUCCESS,
CONFIRM_DELETE,
DELETE_SUCCESS,
};
+1
View File
@@ -3,3 +3,4 @@ export const USER: string = '/user';
export const ROOM: string = '/room'; export const ROOM: string = '/room';
export const OTHER_PATH: string = '*'; export const OTHER_PATH: string = '*';
export const BASE_URL: string = 'https://hotel-management-api.loiphan.com/'; export const BASE_URL: string = 'https://hotel-management-api.loiphan.com/';
export const USER_PATH = 'users';
@@ -1,4 +1,5 @@
const STATUS_CODE = { const STATUS_CODE = {
OK: 200,
CREATE: 201, CREATE: 201,
NOT_FOUND: 404, NOT_FOUND: 404,
}; };
@@ -1,3 +1,6 @@
// Types
import { TUser } from './types';
interface IMenusContext { interface IMenusContext {
openId?: string; openId?: string;
close?: () => void; close?: () => void;
@@ -27,6 +30,8 @@ interface IDialogProps {
reload?: boolean; reload?: boolean;
setReload?: React.Dispatch<React.SetStateAction<boolean>>; setReload?: React.Dispatch<React.SetStateAction<boolean>>;
ref?: React.MutableRefObject<HTMLDialogElement | undefined>; ref?: React.MutableRefObject<HTMLDialogElement | undefined>;
user?: TUser | null;
isAdd?: boolean;
} }
export type { IMenusContext, IButton, ITable, ITableBody, IDialogProps }; export type { IMenusContext, IButton, ITable, ITableBody, IDialogProps };
+13 -1
View File
@@ -1,5 +1,8 @@
// Constants
import { invalidFormatMsg } from '../constants/messages'; import { invalidFormatMsg } from '../constants/messages';
import { TKeyValue, TPropValues, TStateSchema } from '../globals/types';
// Types
import { TKeyValue, TPropValues, TStateSchema, TUser } from '../globals/types';
const VALUE = 'value'; const VALUE = 'value';
const ERROR = 'error'; const ERROR = 'error';
@@ -44,10 +47,19 @@ const addValidator = ({ validatorFunc, prop, required = true }: TValidator) => {
}; };
}; };
const getValueUser = (user: TUser | null = null, prop: string): string => {
if (user) {
return user[prop as keyof TUser];
}
return '';
};
export { export {
isObject, isObject,
isRequired, isRequired,
getPropValues, getPropValues,
getValueUser,
addValidator, addValidator,
VALUE, VALUE,
ERROR, ERROR,
+25 -5
View File
@@ -24,6 +24,7 @@ const useForm = (
stateSchema = {}, stateSchema = {},
stateValidatorSchema = {} as TValidator, stateValidatorSchema = {} as TValidator,
submitFormCallback: (values: TKeyValue) => void, submitFormCallback: (values: TKeyValue) => void,
initialValue: string = '',
) => { ) => {
const [values, setValues] = useState(getPropValues(stateSchema, VALUE)); const [values, setValues] = useState(getPropValues(stateSchema, VALUE));
const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR)); const [errors, setErrors] = useState(getPropValues(stateSchema, ERROR));
@@ -33,8 +34,17 @@ const useForm = (
// Get a local copy of stateSchema // Get a local copy of stateSchema
useEffect(() => { useEffect(() => {
setInitialErrorState(); setInitialErrorState(initialValue);
}, []); // eslint-disable-line setDisable(true);
// If initial value true, setValues again from stateSchema
// and enabled button
if (initialValue) {
setValues({});
setValues(getPropValues(stateSchema, VALUE));
setDisable(false);
}
}, [initialValue]); // eslint-disable-line
// Validate fields in forms // Validate fields in forms
const validateFormFields = useCallback( const validateFormFields = useCallback(
@@ -47,6 +57,9 @@ const useForm = (
const field = validator[name]; const field = validator[name];
let error = ''; let error = '';
// Skip check id field
if (name !== 'id') {
error = isRequired(value, field!.required); error = isRequired(value, field!.required);
if (isObject(field['validator']) && error === '') { if (isObject(field['validator']) && error === '') {
@@ -58,6 +71,7 @@ const useForm = (
error = fieldValidator!['error']!; error = fieldValidator!['error']!;
} }
} }
}
return error; return error;
}, },
@@ -66,14 +80,19 @@ const useForm = (
// Set Initial Error State // Set Initial Error State
// When hooks was first rendered... // When hooks was first rendered...
const setInitialErrorState = useCallback(() => { const setInitialErrorState = useCallback(
(initialValue: string) => {
Object.keys(errors).map((name) => Object.keys(errors).map((name) =>
setErrors((prevState) => ({ setErrors((prevState) => ({
...prevState, ...prevState,
[name]: validateFormFields(name, values[name] as string), [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 // Used to disable submit button if there's a value in errors
// or the required field in state has no value. // or the required field in state has no value.
@@ -116,6 +135,7 @@ const useForm = (
// Making sure that there's no error in the state // Making sure that there's no error in the state
// before calling the submit callback function // before calling the submit callback function
// and disabled button // and disabled button
if (!validateErrorState()) { if (!validateErrorState()) {
submitFormCallback(values); submitFormCallback(values);
setDisable(true); setDisable(true);
+15 -2
View File
@@ -11,7 +11,14 @@ const UserDialog = forwardRef((props, ref) => {
const dialogRef = ref as React.MutableRefObject< const dialogRef = ref as React.MutableRefObject<
HTMLDialogElement | undefined HTMLDialogElement | undefined
>; >;
const { onClose, setReload, reload } = props; // prettier-ignore
const {
onClose,
setReload,
reload,
user,
isAdd
} = props;
useEffect(() => { useEffect(() => {
if (dialogRef.current) { if (dialogRef.current) {
@@ -31,7 +38,13 @@ const UserDialog = forwardRef((props, ref) => {
return ( return (
<Dialog title={'Add user'} onClose={onClose} ref={dialogRef}> <Dialog title={'Add user'} onClose={onClose} ref={dialogRef}>
<UserForm onClose={onClose!} reload={reload!} setReload={setReload!} /> <UserForm
onClose={onClose!}
reload={reload!}
setReload={setReload!}
user={user}
isAdd={isAdd!}
/>
</Dialog> </Dialog>
); );
}) as React.FC<IDialogProps>; }) as React.FC<IDialogProps>;
+68 -21
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -14,8 +14,8 @@ import Button from '../../commons/styles/Button.ts';
// Types // Types
import { import {
TKeyValue, TKeyValue,
TResponse,
TStateSchema, TStateSchema,
TUser,
TValidator, TValidator,
} from '../../globals/types'; } from '../../globals/types';
@@ -29,12 +29,17 @@ import {
isValidPhoneNumber, isValidPhoneNumber,
isValidString, isValidString,
} from '../../helpers/validators'; } from '../../helpers/validators';
import { addValidator } from '../../helpers/utils.ts'; import { addValidator, getValueUser } from '../../helpers/utils.ts';
import { sendRequest } from '../../helpers/sendRequest.ts'; import { sendRequest } from '../../helpers/sendRequest.ts';
// Constants // Constants
import { STATUS_CODE } from '../../constants/statusCode.ts'; import { STATUS_CODE } from '../../constants/statusCode.ts';
import { ADD_SUCCESS, errorMsg } from '../../constants/messages.ts'; import {
ADD_SUCCESS,
EDIT_SUCCESS,
errorMsg,
} from '../../constants/messages.ts';
import { USER_PATH } from '../../constants/path.ts';
const FormBtn = styled(Button)` const FormBtn = styled(Button)`
width: 100%; width: 100%;
@@ -49,18 +54,32 @@ interface IUserFormProp {
onClose: () => void; onClose: () => void;
reload: boolean; reload: boolean;
setReload: React.Dispatch<React.SetStateAction<boolean>>; setReload: React.Dispatch<React.SetStateAction<boolean>>;
user?: TUser | null;
isAdd: boolean;
} }
const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => { const UserForm = ({
onClose,
reload,
setReload,
user,
isAdd,
}: IUserFormProp) => {
const [reset, setReset] = useState(true); const [reset, setReset] = useState(true);
// prettier-ignore
const initialValue: string = isAdd
? ''
: user!
&& user.id;
// Define your state schema // Define your state schema
const stateSchema: TStateSchema = { const stateSchema: TStateSchema = {
name: { value: '', error: '' }, id: { value: getValueUser(user, 'id') },
identifiedCode: { value: '', error: '' }, name: { value: getValueUser(user, 'name'), error: '' },
phone: { value: '', error: '' }, identifiedCode: { value: getValueUser(user, 'identifiedCode'), error: '' },
roomId: { value: '', error: '' }, phone: { value: getValueUser(user, 'phone'), error: '' },
address: { value: '', error: '' }, roomId: { value: getValueUser(user, 'roomId'), error: '' },
address: { value: getValueUser(user, 'address'), error: '' },
}; };
// prettier-ignore // prettier-ignore
@@ -90,31 +109,51 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => {
// Submit form // Submit form
const onSubmitForm = async (state: TKeyValue) => { const onSubmitForm = async (state: TKeyValue) => {
try { try {
if (isAdd) {
// Add request
const response = await sendRequest( const response = await sendRequest(
'users', USER_PATH,
JSON.stringify(state), JSON.stringify(state),
'POST', 'POST',
); );
if (response.statusCode === STATUS_CODE.CREATE) { if (response.statusCode === STATUS_CODE.CREATE) {
toast.success(ADD_SUCCESS); toast.success(ADD_SUCCESS);
} else {
throw new Error(errorMsg(response.statusCode, response.msg));
}
} else {
// Edit request
const response = await sendRequest(
USER_PATH + `/${user!.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 // Reload table data
setReload(!reload); setReload(!reload);
} else {
toast.error(errorMsg(response.statusCode, response.msg));
}
onResetForm(); onResetForm();
} catch (error) { } catch (error: unknown) {
toast.error( if (error instanceof Error) {
errorMsg((error as TResponse).statusCode, (error as TResponse).msg), toast.error(error.message);
); }
} }
onClose(); onClose();
}; };
// Close and reset form
const closeAndReset = () => {
onClose();
onResetForm();
};
// prettier-ignore // prettier-ignore
const { const {
values, values,
@@ -127,10 +166,12 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => {
stateSchema, stateSchema,
stateValidatorSchema, stateValidatorSchema,
onSubmitForm, onSubmitForm,
initialValue
); );
// prettier-ignore // prettier-ignore
const { const {
id,
name, name,
identifiedCode, identifiedCode,
phone, phone,
@@ -138,6 +179,12 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => {
address address
} = values; } = values;
useEffect(() => {
if (isAdd) {
Object.keys(values).forEach((key) => (values[key] = ''));
}
}, [isAdd]); // eslint-disable-line
// Reset form // Reset form
const onResetForm = () => { const onResetForm = () => {
setReset(!reset); setReset(!reset);
@@ -146,7 +193,7 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => {
return ( return (
<Form onSubmit={handleOnSubmit}> <Form onSubmit={handleOnSubmit}>
<Input type="hidden" id="id" /> <Input type="hidden" name="id" value={id as string} />
<FormRow <FormRow
label="Full Name" label="Full Name"
error={ error={
@@ -235,7 +282,7 @@ const UserForm = ({ onClose, reload, setReload }: IUserFormProp) => {
<FormBtn type="submit" name="submit" disabled={disable}> <FormBtn type="submit" name="submit" disabled={disable}>
Add Add
</FormBtn> </FormBtn>
<FormBtn type="button" styled="secondary" onClick={onClose}> <FormBtn type="button" styled="secondary" onClick={closeAndReset}>
Close Close
</FormBtn> </FormBtn>
</Form.Action> </Form.Action>
+62 -8
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
// Components // Components
import { HiSquare2Stack } from 'react-icons/hi2'; import { HiSquare2Stack } from 'react-icons/hi2';
@@ -14,15 +15,51 @@ import { TUser } from '../../globals/types';
// Constants // Constants
import { useFetch } from '../../hooks/useFetch'; import { useFetch } from '../../hooks/useFetch';
import { USER_PATH } from '../../constants/path';
import { STATUS_CODE } from '../../constants/statusCode';
import { CONFIRM_DELETE, DELETE_SUCCESS } from '../../constants/messages';
// Styled // Styled
import Spinner from '../../commons/styles/Spinner'; import Spinner from '../../commons/styles/Spinner';
type TUserModal = { user: TUser }; // Utils
import { sendRequest } from '../../helpers/sendRequest';
const UserRow = ({ user }: TUserModal) => { interface IUserRow {
const handleOnClick = (id: string) => { user: TUser;
alert(`Id: ${id}`); openFormDialog: () => void;
setUser: React.Dispatch<React.SetStateAction<TUser | null>>;
reload: boolean;
setReload: React.Dispatch<React.SetStateAction<boolean>>;
}
const UserRow = ({
user,
openFormDialog,
setUser,
reload,
setReload,
}: IUserRow) => {
const handleOnEdit = (user: TUser) => {
setUser(user);
openFormDialog();
};
const handleOnDelete = async (user: TUser) => {
if (confirm(CONFIRM_DELETE)) {
const response = await sendRequest(
USER_PATH + `/${user.id}`,
null,
'DELETE',
);
if (response.statusCode === STATUS_CODE.OK) {
toast.success(DELETE_SUCCESS);
// Reload table
setReload(!reload);
}
}
}; };
const { id, name, identifiedCode, phone, roomId } = user; const { id, name, identifiedCode, phone, roomId } = user;
@@ -41,11 +78,11 @@ const UserRow = ({ user }: TUserModal) => {
<Menus.List id={id}> <Menus.List id={id}>
<Menus.Button <Menus.Button
icon={<HiSquare2Stack />} icon={<HiSquare2Stack />}
onClick={() => handleOnClick(id)} onClick={() => handleOnEdit(user)}
> >
Edit Edit
</Menus.Button> </Menus.Button>
<Menus.Button icon={<HiTrash />} onClick={() => handleOnClick(id)}> <Menus.Button icon={<HiTrash />} onClick={() => handleOnDelete(user)}>
Delete Delete
</Menus.Button> </Menus.Button>
</Menus.List> </Menus.List>
@@ -56,9 +93,17 @@ const UserRow = ({ user }: TUserModal) => {
interface IUserTable { interface IUserTable {
reload: boolean; reload: boolean;
setReload: React.Dispatch<React.SetStateAction<boolean>>;
openFormDialog: () => void;
setUser?: React.Dispatch<React.SetStateAction<TUser | null>>;
} }
const UserTable = ({ reload }: IUserTable) => { const UserTable = ({
reload,
setReload,
openFormDialog,
setUser,
}: IUserTable) => {
const { data, isPending, errorMsg } = useFetch('users', reload); const { data, isPending, errorMsg } = useFetch('users', reload);
const [users, setUsers] = useState<TUser[]>([]); const [users, setUsers] = useState<TUser[]>([]);
@@ -94,7 +139,16 @@ const UserTable = ({ reload }: IUserTable) => {
</Table.Header> </Table.Header>
<Table.Body<TUser> <Table.Body<TUser>
data={users} data={users}
render={(user: TUser) => <UserRow user={user} key={user.id} />} render={(user: TUser) => (
<UserRow
user={user}
key={user.id}
reload={reload}
setReload={setReload}
openFormDialog={openFormDialog}
setUser={setUser!}
/>
)}
/> />
</Table> </Table>
</Menus> </Menus>
+18 -5
View File
@@ -9,15 +9,21 @@ import UserTable from './Table';
import { StyledUser, Title } from './styled'; import { StyledUser, Title } from './styled';
import UserDialog from './Dialog'; import UserDialog from './Dialog';
// Types
import { TUser } from '../../globals/types';
const User = () => { const User = () => {
const dialogRef = useRef<HTMLDialogElement>(); const dialogRef = useRef<HTMLDialogElement>();
const [reload, setReload] = useState(true); const [reload, setReload] = useState(true);
const [user, setUser] = useState<TUser | null>(null);
const [isAdd, setIsAdd] = useState(false);
const openDialog = () => { const openFormDialog = (isAddForm: boolean = false) => {
setIsAdd(isAddForm);
dialogRef.current?.showModal(); dialogRef.current?.showModal();
}; };
const closeDialog = () => { const closeFormDialog = () => {
dialogRef.current?.close(); dialogRef.current?.close();
}; };
@@ -26,17 +32,24 @@ const User = () => {
<StyledUser> <StyledUser>
<Direction type="horizontal"> <Direction type="horizontal">
<Title>List User</Title> <Title>List User</Title>
<Button onClick={openDialog}>Add user</Button> <Button onClick={() => openFormDialog(true)}>Add user</Button>
</Direction> </Direction>
<UserTable reload={reload} /> <UserTable
reload={reload}
setReload={setReload}
openFormDialog={() => openFormDialog()}
setUser={setUser}
/>
</StyledUser> </StyledUser>
<UserDialog <UserDialog
onClose={closeDialog} onClose={closeFormDialog}
ref={dialogRef} ref={dialogRef}
setReload={setReload} setReload={setReload}
reload={reload} reload={reload}
user={user}
isAdd={isAdd}
/> />
</> </>
); );