diff --git a/src/pages/User/List/User.jsx b/src/pages/User/List/User.jsx new file mode 100644 index 0000000000000000000000000000000000000000..54266a29d26141905ed5b6d8ec1d198f3187df5c --- /dev/null +++ b/src/pages/User/List/User.jsx @@ -0,0 +1,99 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import React, { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { RiDeleteBinLine } from 'react-icons/ri'; +import { z } from 'zod'; +import Input from '../../../components/form/Input'; +import Select from '../../../components/form/Select'; +import Submit from '../../../components/form/Submit'; +import { useAuth } from '../../../contexts/Auth/AuthState'; +import { ROLES } from '../UserTypes'; +import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling'; + + +function User({ user, idx, handleDelete }) { + // ################################# + // VALIDATION SCHEMA + // ################################# + const schema = z.object({ + name: z.string().min(2), + username: z.string().min(2), + email: z.string().min(1).email(), + verified: z.string().transform((val) => val === 'true' ? true : false), + role: z.coerce.number() + }); + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { currentUser, update } = useAuth(); + const [updatedUser, setUpdatedUser] = useState(user); + + // ### PREPARE FORM + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onBlur', + defaultValues: user + }); + + + // const id = useId(); + + // ################################# + // FUNCTIONS + // ################################# + // ### HANDLE SUBMITTING FORM + async function handleSendForm(inputs) { + // save to db + try { + // send data + const result = await update(user._id, inputs); + setUpdatedUser(result.data.document); + // set flash msg + setFlashMsg(result.data?.message); + } catch (error) { + // catch the error + mergeBackendValidation(error.response.status, error.response.data, methods.setError); + } + } + + + // ################################# + // OUTPUT + // ################################# + return ( + <div className='flex items-center bg-white border border-UhhLightGrey rounded-lg shadow-lg p-3 m-3'> + <div> + {currentUser?.role >= 2 ? + <> + <h2 className='text-Uhh-Grey font-bold text-2xl flex justify-end'> + <RiDeleteBinLine className='cursor-pointer hover:text-UhhRed' title='delete user' onClick={() => handleDelete(user.id, idx)} /> + </h2> + <FormProvider {...methods} > + <form onSubmit={methods.handleSubmit(handleSendForm)} noValidate> + <Input name='name' type='text' title='name' required={true} /> + + <Input name='username' type='text' title='username' required={true} /> + + <Input name='email' type='email' title='eMail' required={true} /> + + <Select title="verified" name="verified" options={[{ _id: true, title: 'verified' }, { _id: false, title: 'not verified' }]} required={true} /> + + <Select title="Role" name="role" options={ROLES} required={true} /> + + <Submit value='save' /> + </form> + </FormProvider> + </> + : + <> + <h2 className='text-Uhh-Grey font-bold text-2xl'>{user.fullname}</h2> + <p className='text-sm'>{user.email}</p> + </> + } + </div> + </div> + ); +} + +export default React.memo(User); \ No newline at end of file diff --git a/src/pages/User/List/Users.jsx b/src/pages/User/List/Users.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2ddb103b0671d16f5212b770571d57b311086e9e --- /dev/null +++ b/src/pages/User/List/Users.jsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import Heading from '../../../components/font/Heading'; +import api from '../../../utils/AxiosConfig'; +import User from './User'; +import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling'; + + +function Users() { + // ################################# + // HOOKS + // ################################# + // ### USERS + const [users, setUsers] = useState(); + + // ### FETCH USERS + useEffect(() => { + // mount + const controller = new AbortController(); + const getUsers = async () => { + try { + const result = await api.get('/users', { + signal: controller.signal + }); + setUsers(result?.data); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + getUsers(); + + return () => { + // on unmount abort request + controller.abort(); + }; + }, []); + + // ################################# + // FUNCTIONS + // ################################# + // ### DELETE USERS + const handleDelete = async (id, idx) => { + try { + // delete in backend + const result = await api.delete(`/users/${id}`); + // delete in frontend + const list = [...users]; + list.splice(idx, 1); + setUsers(list); + setFlashMsg(result.data?.message); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data, methods.setError); + } + }; + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* render page title */} + <Helmet><title>[{import.meta.env.VITE_APP_NAME}] Users</title></Helmet> + + <Heading level="1" className="col-span-2">Registered Users</Heading> + <div> + {users?.length + ? ( + <div className='flex flex-wrap justify-items-stretch'> + {users.map((user, idx) => + <User key={user._id} idx={idx} user={user} handleDelete={handleDelete} /> + )} + + </div> + ) : <p>No users to display</p> + } + </div> + </> + ); +} + +export default React.memo(Users); diff --git a/src/pages/User/UserTypes.js b/src/pages/User/UserTypes.js new file mode 100644 index 0000000000000000000000000000000000000000..1305abdcc109d7067ec17f5dd7992cf9bda353fb --- /dev/null +++ b/src/pages/User/UserTypes.js @@ -0,0 +1,9 @@ +// the _id represents the order ranking the role +// the lower the _id, the lower the permissions +export const ROLES = [ + { _id: 0, title: 'User' }, + { _id: 1, title: 'Editor' }, + { _id: 2, title: 'Poweruser' }, + { _id: 3, title: 'Lead' }, + { _id: 4, title: 'Admin' } +]; \ No newline at end of file diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx index b8da28fc13b4d6ceac43425ecf2829928fbe20e4..253a4b5e16c59a61f26fe22666ebae9217fc10a1 100644 --- a/src/routes/Sitemap.jsx +++ b/src/routes/Sitemap.jsx @@ -38,6 +38,10 @@ export const sitemap = [{ ] }, + // USER + { + title: 'Users', path: '/users', element: loadComponent('User/List/Users', true, true), handle: { crumb: () => <Link to="/users">Users</Link> } + }, // PROFILE { title: 'Profile', path: '/profile', handle: { crumb: () => <Link to="/profile">Profile</Link> },