committed by
GitHub
29 changed files with 2081 additions and 1376 deletions
@ -1,117 +1,194 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Button, |
||||
|
Checkbox, |
||||
|
Flex, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Input, |
||||
|
Modal, |
||||
|
ModalBody, |
||||
|
ModalCloseButton, |
||||
|
ModalContent, |
||||
|
ModalFooter, |
||||
|
ModalHeader, |
||||
|
ModalOverlay, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
import { useMutation, useQueryClient } from 'react-query' |
||||
|
|
||||
import { Button, Checkbox, Flex, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
import { UserCreate, UsersService } from '../../client' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import { ApiError } from '../../client/core/ApiError' |
||||
import { useMutation, useQueryClient } from 'react-query'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
|
|
||||
import { UserCreate, UsersService } from '../../client'; |
|
||||
import { ApiError } from '../../client/core/ApiError'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
interface AddUserProps { |
interface AddUserProps { |
||||
isOpen: boolean; |
isOpen: boolean |
||||
onClose: () => void; |
onClose: () => void |
||||
} |
} |
||||
|
|
||||
interface UserCreateForm extends UserCreate { |
interface UserCreateForm extends UserCreate { |
||||
confirm_password: string; |
confirm_password: string |
||||
|
|
||||
} |
} |
||||
|
|
||||
const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => { |
const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting } } = useForm<UserCreateForm>({ |
const { |
||||
mode: 'onBlur', |
register, |
||||
criteriaMode: 'all', |
handleSubmit, |
||||
defaultValues: { |
reset, |
||||
email: '', |
getValues, |
||||
full_name: '', |
formState: { errors, isSubmitting }, |
||||
password: '', |
} = useForm<UserCreateForm>({ |
||||
confirm_password: '', |
mode: 'onBlur', |
||||
is_superuser: false, |
criteriaMode: 'all', |
||||
is_active: false |
defaultValues: { |
||||
} |
email: '', |
||||
}); |
full_name: '', |
||||
|
password: '', |
||||
|
confirm_password: '', |
||||
|
is_superuser: false, |
||||
|
is_active: false, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
const addUser = async (data: UserCreate) => { |
const addUser = async (data: UserCreate) => { |
||||
await UsersService.createUser({ requestBody: data }) |
await UsersService.createUser({ requestBody: data }) |
||||
} |
} |
||||
|
|
||||
const mutation = useMutation(addUser, { |
const mutation = useMutation(addUser, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success!', 'User created successfully.', 'success'); |
showToast('Success!', 'User created successfully.', 'success') |
||||
reset(); |
reset() |
||||
onClose(); |
onClose() |
||||
}, |
}, |
||||
onError: (err: ApiError) => { |
onError: (err: ApiError) => { |
||||
const errDetail = err.body.detail; |
const errDetail = err.body.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
}, |
}, |
||||
onSettled: () => { |
onSettled: () => { |
||||
queryClient.invalidateQueries('users'); |
queryClient.invalidateQueries('users') |
||||
} |
}, |
||||
}); |
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<UserCreateForm> = (data) => { |
const onSubmit: SubmitHandler<UserCreateForm> = (data) => { |
||||
mutation.mutate(data); |
mutation.mutate(data) |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Modal |
<Modal |
||||
isOpen={isOpen} |
isOpen={isOpen} |
||||
onClose={onClose} |
onClose={onClose} |
||||
size={{ base: 'sm', md: 'md' }} |
size={{ base: 'sm', md: 'md' }} |
||||
isCentered |
isCentered |
||||
|
> |
||||
|
<ModalOverlay /> |
||||
|
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
|
<ModalHeader>Add User</ModalHeader> |
||||
|
<ModalCloseButton /> |
||||
|
<ModalBody pb={6}> |
||||
|
<FormControl isRequired isInvalid={!!errors.email}> |
||||
|
<FormLabel htmlFor="email">Email</FormLabel> |
||||
|
<Input |
||||
|
id="email" |
||||
|
{...register('email', { |
||||
|
required: 'Email is required', |
||||
|
pattern: { |
||||
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, |
||||
|
message: 'Invalid email address', |
||||
|
}, |
||||
|
})} |
||||
|
placeholder="Email" |
||||
|
type="email" |
||||
|
/> |
||||
|
{errors.email && ( |
||||
|
<FormErrorMessage>{errors.email.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<FormControl mt={4} isInvalid={!!errors.full_name}> |
||||
|
<FormLabel htmlFor="name">Full name</FormLabel> |
||||
|
<Input |
||||
|
id="name" |
||||
|
{...register('full_name')} |
||||
|
placeholder="Full name" |
||||
|
type="text" |
||||
|
/> |
||||
|
{errors.full_name && ( |
||||
|
<FormErrorMessage>{errors.full_name.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<FormControl mt={4} isRequired isInvalid={!!errors.password}> |
||||
|
<FormLabel htmlFor="password">Set Password</FormLabel> |
||||
|
<Input |
||||
|
id="password" |
||||
|
{...register('password', { |
||||
|
required: 'Password is required', |
||||
|
minLength: { |
||||
|
value: 8, |
||||
|
message: 'Password must be at least 8 characters', |
||||
|
}, |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.password && ( |
||||
|
<FormErrorMessage>{errors.password.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<FormControl |
||||
|
mt={4} |
||||
|
isRequired |
||||
|
isInvalid={!!errors.confirm_password} |
||||
|
> |
||||
|
<FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> |
||||
|
<Input |
||||
|
id="confirm_password" |
||||
|
{...register('confirm_password', { |
||||
|
required: 'Please confirm your password', |
||||
|
validate: (value) => |
||||
|
value === getValues().password || |
||||
|
'The passwords do not match', |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.confirm_password && ( |
||||
|
<FormErrorMessage> |
||||
|
{errors.confirm_password.message} |
||||
|
</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<Flex mt={4}> |
||||
|
<FormControl> |
||||
|
<Checkbox {...register('is_superuser')} colorScheme="teal"> |
||||
|
Is superuser? |
||||
|
</Checkbox> |
||||
|
</FormControl> |
||||
|
<FormControl> |
||||
|
<Checkbox {...register('is_active')} colorScheme="teal"> |
||||
|
Is active? |
||||
|
</Checkbox> |
||||
|
</FormControl> |
||||
|
</Flex> |
||||
|
</ModalBody> |
||||
|
<ModalFooter gap={3}> |
||||
|
<Button |
||||
|
bg="ui.main" |
||||
|
color="white" |
||||
|
_hover={{ opacity: 0.8 }} |
||||
|
type="submit" |
||||
|
isLoading={isSubmitting} |
||||
> |
> |
||||
<ModalOverlay /> |
Save |
||||
<ModalContent as='form' onSubmit={handleSubmit(onSubmit)}> |
</Button> |
||||
<ModalHeader>Add User</ModalHeader> |
<Button onClick={onClose}>Cancel</Button> |
||||
<ModalCloseButton /> |
</ModalFooter> |
||||
<ModalBody pb={6} > |
</ModalContent> |
||||
<FormControl isRequired isInvalid={!!errors.email}> |
</Modal> |
||||
<FormLabel htmlFor='email'>Email</FormLabel> |
</> |
||||
<Input id='email' {...register('email', { required: 'Email is required', pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: 'Invalid email address' } })} placeholder='Email' type='email' /> |
) |
||||
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>} |
|
||||
</FormControl> |
|
||||
<FormControl mt={4} isInvalid={!!errors.full_name}> |
|
||||
<FormLabel htmlFor='name'>Full name</FormLabel> |
|
||||
<Input id='name' {...register('full_name')} placeholder='Full name' type='text' /> |
|
||||
{errors.full_name && <FormErrorMessage>{errors.full_name.message}</FormErrorMessage>} |
|
||||
</FormControl> |
|
||||
<FormControl mt={4} isRequired isInvalid={!!errors.password}> |
|
||||
<FormLabel htmlFor='password'>Set Password</FormLabel> |
|
||||
<Input id='password' {...register('password', { required: 'Password is required', minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='Password' type='password' /> |
|
||||
{errors.password && <FormErrorMessage>{errors.password.message}</FormErrorMessage>} |
|
||||
</FormControl> |
|
||||
<FormControl mt={4} isRequired isInvalid={!!errors.confirm_password}> |
|
||||
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel> |
|
||||
<Input id='confirm_password' {...register('confirm_password', { |
|
||||
required: 'Please confirm your password', |
|
||||
validate: value => value === getValues().password || 'The passwords do not match' |
|
||||
})} placeholder='Password' type='password' /> |
|
||||
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>} |
|
||||
</FormControl> |
|
||||
<Flex mt={4}> |
|
||||
<FormControl> |
|
||||
<Checkbox {...register('is_superuser')} colorScheme='teal'>Is superuser?</Checkbox> |
|
||||
</FormControl> |
|
||||
<FormControl> |
|
||||
<Checkbox {...register('is_active')} colorScheme='teal'>Is active?</Checkbox> |
|
||||
</FormControl> |
|
||||
</Flex> |
|
||||
</ModalBody> |
|
||||
<ModalFooter gap={3}> |
|
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}> |
|
||||
Save |
|
||||
</Button> |
|
||||
<Button onClick={onClose}>Cancel</Button> |
|
||||
</ModalFooter> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
</> |
|
||||
) |
|
||||
} |
} |
||||
|
|
||||
export default AddUser; |
export default AddUser |
||||
|
@ -1,116 +1,183 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Button, |
||||
|
Checkbox, |
||||
|
Flex, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Input, |
||||
|
Modal, |
||||
|
ModalBody, |
||||
|
ModalCloseButton, |
||||
|
ModalContent, |
||||
|
ModalFooter, |
||||
|
ModalHeader, |
||||
|
ModalOverlay, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
import { useMutation, useQueryClient } from 'react-query' |
||||
|
|
||||
import { Button, Checkbox, Flex, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
import { ApiError, UserOut, UserUpdate, UsersService } from '../../client' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
import { useMutation, useQueryClient } from 'react-query'; |
|
||||
|
|
||||
import { ApiError, UserOut, UserUpdate, UsersService } from '../../client'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
interface EditUserProps { |
interface EditUserProps { |
||||
user: UserOut; |
user: UserOut |
||||
isOpen: boolean; |
isOpen: boolean |
||||
onClose: () => void; |
onClose: () => void |
||||
} |
} |
||||
|
|
||||
interface UserUpdateForm extends UserUpdate { |
interface UserUpdateForm extends UserUpdate { |
||||
confirm_password: string; |
confirm_password: string |
||||
} |
} |
||||
|
|
||||
const EditUser: React.FC<EditUserProps> = ({ user, isOpen, onClose }) => { |
const EditUser: React.FC<EditUserProps> = ({ user, isOpen, onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
|
|
||||
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting, isDirty } } = useForm<UserUpdateForm>({ |
const { |
||||
mode: 'onBlur', |
register, |
||||
criteriaMode: 'all', |
handleSubmit, |
||||
defaultValues: user |
reset, |
||||
}); |
getValues, |
||||
|
formState: { errors, isSubmitting, isDirty }, |
||||
|
} = useForm<UserUpdateForm>({ |
||||
|
mode: 'onBlur', |
||||
|
criteriaMode: 'all', |
||||
|
defaultValues: user, |
||||
|
}) |
||||
|
|
||||
const updateUser = async (data: UserUpdateForm) => { |
const updateUser = async (data: UserUpdateForm) => { |
||||
await UsersService.updateUser({ userId: user.id, requestBody: data }); |
await UsersService.updateUser({ userId: user.id, requestBody: data }) |
||||
} |
} |
||||
|
|
||||
const mutation = useMutation(updateUser, { |
const mutation = useMutation(updateUser, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success!', 'User updated successfully.', 'success'); |
showToast('Success!', 'User updated successfully.', 'success') |
||||
onClose(); |
onClose() |
||||
}, |
}, |
||||
onError: (err: ApiError) => { |
onError: (err: ApiError) => { |
||||
const errDetail = err.body.detail; |
const errDetail = err.body.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
}, |
}, |
||||
onSettled: () => { |
onSettled: () => { |
||||
queryClient.invalidateQueries('users'); |
queryClient.invalidateQueries('users') |
||||
} |
}, |
||||
}); |
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => { |
const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => { |
||||
if (data.password === '') { |
if (data.password === '') { |
||||
delete data.password; |
delete data.password |
||||
} |
|
||||
mutation.mutate(data) |
|
||||
} |
} |
||||
|
mutation.mutate(data) |
||||
|
} |
||||
|
|
||||
const onCancel = () => { |
const onCancel = () => { |
||||
reset(); |
reset() |
||||
onClose(); |
onClose() |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Modal |
<Modal |
||||
isOpen={isOpen} |
isOpen={isOpen} |
||||
onClose={onClose} |
onClose={onClose} |
||||
size={{ base: 'sm', md: 'md' }} |
size={{ base: 'sm', md: 'md' }} |
||||
isCentered |
isCentered |
||||
> |
> |
||||
<ModalOverlay /> |
<ModalOverlay /> |
||||
<ModalContent as='form' onSubmit={handleSubmit(onSubmit)}> |
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
<ModalHeader>Edit User</ModalHeader> |
<ModalHeader>Edit User</ModalHeader> |
||||
<ModalCloseButton /> |
<ModalCloseButton /> |
||||
<ModalBody pb={6}> |
<ModalBody pb={6}> |
||||
<FormControl isInvalid={!!errors.email}> |
<FormControl isInvalid={!!errors.email}> |
||||
<FormLabel htmlFor='email'>Email</FormLabel> |
<FormLabel htmlFor="email">Email</FormLabel> |
||||
<Input id='email' {...register('email', { required: 'Email is required', pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: 'Invalid email address' } })} placeholder='Email' type='email' /> |
<Input |
||||
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>} |
id="email" |
||||
</FormControl> |
{...register('email', { |
||||
<FormControl mt={4}> |
required: 'Email is required', |
||||
<FormLabel htmlFor='name'>Full name</FormLabel> |
pattern: { |
||||
<Input id='name' {...register('full_name')} type='text' /> |
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, |
||||
</FormControl> |
message: 'Invalid email address', |
||||
<FormControl mt={4} isInvalid={!!errors.password}> |
}, |
||||
<FormLabel htmlFor='password'>Set Password</FormLabel> |
})} |
||||
<Input id='password' {...register('password', { minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='••••••••' type='password' /> |
placeholder="Email" |
||||
{errors.password && <FormErrorMessage>{errors.password.message}</FormErrorMessage>} |
type="email" |
||||
</FormControl> |
/> |
||||
<FormControl mt={4} isInvalid={!!errors.confirm_password}> |
{errors.email && ( |
||||
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel> |
<FormErrorMessage>{errors.email.message}</FormErrorMessage> |
||||
<Input id='confirm_password' {...register('confirm_password', { |
)} |
||||
validate: value => value === getValues().password || 'The passwords do not match' |
</FormControl> |
||||
})} placeholder='••••••••' type='password' /> |
<FormControl mt={4}> |
||||
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>} |
<FormLabel htmlFor="name">Full name</FormLabel> |
||||
</FormControl> |
<Input id="name" {...register('full_name')} type="text" /> |
||||
<Flex> |
</FormControl> |
||||
<FormControl mt={4}> |
<FormControl mt={4} isInvalid={!!errors.password}> |
||||
<Checkbox {...register('is_superuser')} colorScheme='teal'>Is superuser?</Checkbox> |
<FormLabel htmlFor="password">Set Password</FormLabel> |
||||
</FormControl> |
<Input |
||||
<FormControl mt={4}> |
id="password" |
||||
<Checkbox {...register('is_active')} colorScheme='teal'>Is active?</Checkbox> |
{...register('password', { |
||||
</FormControl> |
minLength: { |
||||
</Flex> |
value: 8, |
||||
</ModalBody> |
message: 'Password must be at least 8 characters', |
||||
|
}, |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.password && ( |
||||
|
<FormErrorMessage>{errors.password.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<FormControl mt={4} isInvalid={!!errors.confirm_password}> |
||||
|
<FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> |
||||
|
<Input |
||||
|
id="confirm_password" |
||||
|
{...register('confirm_password', { |
||||
|
validate: (value) => |
||||
|
value === getValues().password || |
||||
|
'The passwords do not match', |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.confirm_password && ( |
||||
|
<FormErrorMessage> |
||||
|
{errors.confirm_password.message} |
||||
|
</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<Flex> |
||||
|
<FormControl mt={4}> |
||||
|
<Checkbox {...register('is_superuser')} colorScheme="teal"> |
||||
|
Is superuser? |
||||
|
</Checkbox> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<Checkbox {...register('is_active')} colorScheme="teal"> |
||||
|
Is active? |
||||
|
</Checkbox> |
||||
|
</FormControl> |
||||
|
</Flex> |
||||
|
</ModalBody> |
||||
|
|
||||
<ModalFooter gap={3}> |
<ModalFooter gap={3}> |
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting} isDisabled={!isDirty}> |
<Button |
||||
Save |
bg="ui.main" |
||||
</Button> |
color="white" |
||||
<Button onClick={onCancel}>Cancel</Button> |
_hover={{ opacity: 0.8 }} |
||||
</ModalFooter> |
type="submit" |
||||
</ModalContent> |
isLoading={isSubmitting} |
||||
</Modal> |
isDisabled={!isDirty} |
||||
</> |
> |
||||
) |
Save |
||||
|
</Button> |
||||
|
<Button onClick={onCancel}>Cancel</Button> |
||||
|
</ModalFooter> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default EditUser; |
export default EditUser |
||||
|
@ -1,42 +1,76 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
import { Button, Menu, MenuButton, MenuItem, MenuList, useDisclosure } from '@chakra-ui/react'; |
Button, |
||||
import { BsThreeDotsVertical } from 'react-icons/bs'; |
Menu, |
||||
import { FiEdit, FiTrash } from 'react-icons/fi'; |
MenuButton, |
||||
|
MenuItem, |
||||
import EditUser from '../Admin/EditUser'; |
MenuList, |
||||
import EditItem from '../Items/EditItem'; |
useDisclosure, |
||||
import Delete from './DeleteAlert'; |
} from '@chakra-ui/react' |
||||
import { ItemOut, UserOut } from '../../client'; |
import { BsThreeDotsVertical } from 'react-icons/bs' |
||||
|
import { FiEdit, FiTrash } from 'react-icons/fi' |
||||
|
|
||||
|
import EditUser from '../Admin/EditUser' |
||||
|
import EditItem from '../Items/EditItem' |
||||
|
import Delete from './DeleteAlert' |
||||
|
import { ItemOut, UserOut } from '../../client' |
||||
|
|
||||
interface ActionsMenuProps { |
interface ActionsMenuProps { |
||||
type: string; |
type: string |
||||
value: ItemOut | UserOut; |
value: ItemOut | UserOut |
||||
disabled?: boolean; |
disabled?: boolean |
||||
} |
} |
||||
|
|
||||
const ActionsMenu: React.FC<ActionsMenuProps> = ({ type, value, disabled }) => { |
const ActionsMenu: React.FC<ActionsMenuProps> = ({ type, value, disabled }) => { |
||||
const editUserModal = useDisclosure(); |
const editUserModal = useDisclosure() |
||||
const deleteModal = useDisclosure(); |
const deleteModal = useDisclosure() |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Menu> |
<Menu> |
||||
<MenuButton isDisabled={disabled} as={Button} rightIcon={<BsThreeDotsVertical />} variant='unstyled'> |
<MenuButton |
||||
</MenuButton> |
isDisabled={disabled} |
||||
<MenuList> |
as={Button} |
||||
<MenuItem onClick={editUserModal.onOpen} icon={<FiEdit fontSize='16px' />}>Edit {type}</MenuItem> |
rightIcon={<BsThreeDotsVertical />} |
||||
<MenuItem onClick={deleteModal.onOpen} icon={<FiTrash fontSize='16px' />} color='ui.danger'>Delete {type}</MenuItem> |
variant="unstyled" |
||||
</MenuList> |
></MenuButton> |
||||
{ |
<MenuList> |
||||
type === 'User' ? <EditUser user={value as UserOut} isOpen={editUserModal.isOpen} onClose={editUserModal.onClose} /> |
<MenuItem |
||||
: <EditItem item={value as ItemOut} isOpen={editUserModal.isOpen} onClose={editUserModal.onClose} /> |
onClick={editUserModal.onOpen} |
||||
} |
icon={<FiEdit fontSize="16px" />} |
||||
<Delete type={type} id={value.id} isOpen={deleteModal.isOpen} onClose={deleteModal.onClose} /> |
> |
||||
</Menu> |
Edit {type} |
||||
</> |
</MenuItem> |
||||
); |
<MenuItem |
||||
}; |
onClick={deleteModal.onOpen} |
||||
|
icon={<FiTrash fontSize="16px" />} |
||||
|
color="ui.danger" |
||||
|
> |
||||
|
Delete {type} |
||||
|
</MenuItem> |
||||
|
</MenuList> |
||||
|
{type === 'User' ? ( |
||||
|
<EditUser |
||||
|
user={value as UserOut} |
||||
|
isOpen={editUserModal.isOpen} |
||||
|
onClose={editUserModal.onClose} |
||||
|
/> |
||||
|
) : ( |
||||
|
<EditItem |
||||
|
item={value as ItemOut} |
||||
|
isOpen={editUserModal.isOpen} |
||||
|
onClose={editUserModal.onClose} |
||||
|
/> |
||||
|
)} |
||||
|
<Delete |
||||
|
type={type} |
||||
|
id={value.id} |
||||
|
isOpen={deleteModal.isOpen} |
||||
|
onClose={deleteModal.onClose} |
||||
|
/> |
||||
|
</Menu> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default ActionsMenu; |
export default ActionsMenu |
||||
|
@ -1,85 +1,116 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
AlertDialog, |
||||
|
AlertDialogBody, |
||||
|
AlertDialogContent, |
||||
|
AlertDialogFooter, |
||||
|
AlertDialogHeader, |
||||
|
AlertDialogOverlay, |
||||
|
Button, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { useForm } from 'react-hook-form' |
||||
|
import { useMutation, useQueryClient } from 'react-query' |
||||
|
|
||||
import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Button } from '@chakra-ui/react'; |
import { ItemsService, UsersService } from '../../client' |
||||
import { useForm } from 'react-hook-form'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
import { useMutation, useQueryClient } from 'react-query'; |
|
||||
|
|
||||
import { ItemsService, UsersService } from '../../client'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
interface DeleteProps { |
interface DeleteProps { |
||||
type: string; |
type: string |
||||
id: number |
id: number |
||||
isOpen: boolean; |
isOpen: boolean |
||||
onClose: () => void; |
onClose: () => void |
||||
} |
} |
||||
|
|
||||
const Delete: React.FC<DeleteProps> = ({ type, id, isOpen, onClose }) => { |
const Delete: React.FC<DeleteProps> = ({ type, id, isOpen, onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const cancelRef = React.useRef<HTMLButtonElement | null>(null); |
const cancelRef = React.useRef<HTMLButtonElement | null>(null) |
||||
const { handleSubmit, formState: { isSubmitting } } = useForm(); |
const { |
||||
|
handleSubmit, |
||||
|
formState: { isSubmitting }, |
||||
|
} = useForm() |
||||
|
|
||||
const deleteEntity = async (id: number) => { |
const deleteEntity = async (id: number) => { |
||||
if (type === 'Item') { |
if (type === 'Item') { |
||||
await ItemsService.deleteItem({ id: id }); |
await ItemsService.deleteItem({ id: id }) |
||||
} else if (type === 'User') { |
} else if (type === 'User') { |
||||
await UsersService.deleteUser({ userId: id }); |
await UsersService.deleteUser({ userId: id }) |
||||
} else { |
} else { |
||||
throw new Error(`Unexpected type: ${type}`); |
throw new Error(`Unexpected type: ${type}`) |
||||
} |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
const mutation = useMutation(deleteEntity, { |
const mutation = useMutation(deleteEntity, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success', `The ${type.toLowerCase()} was deleted successfully.`, 'success'); |
showToast( |
||||
onClose(); |
'Success', |
||||
}, |
`The ${type.toLowerCase()} was deleted successfully.`, |
||||
onError: () => { |
'success', |
||||
showToast('An error occurred.', `An error occurred while deleting the ${type.toLowerCase()}.`, 'error'); |
) |
||||
}, |
onClose() |
||||
onSettled: () => { |
}, |
||||
queryClient.invalidateQueries(type === 'Item' ? 'items' : 'users'); |
onError: () => { |
||||
} |
showToast( |
||||
}) |
'An error occurred.', |
||||
|
`An error occurred while deleting the ${type.toLowerCase()}.`, |
||||
|
'error', |
||||
|
) |
||||
|
}, |
||||
|
onSettled: () => { |
||||
|
queryClient.invalidateQueries(type === 'Item' ? 'items' : 'users') |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
const onSubmit = async () => { |
const onSubmit = async () => { |
||||
mutation.mutate(id); |
mutation.mutate(id) |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<AlertDialog |
<AlertDialog |
||||
isOpen={isOpen} |
isOpen={isOpen} |
||||
onClose={onClose} |
onClose={onClose} |
||||
leastDestructiveRef={cancelRef} |
leastDestructiveRef={cancelRef} |
||||
size={{ base: 'sm', md: 'md' }} |
size={{ base: 'sm', md: 'md' }} |
||||
isCentered |
isCentered |
||||
> |
> |
||||
<AlertDialogOverlay> |
<AlertDialogOverlay> |
||||
<AlertDialogContent as='form' onSubmit={handleSubmit(onSubmit)}> |
<AlertDialogContent as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
<AlertDialogHeader> |
<AlertDialogHeader>Delete {type}</AlertDialogHeader> |
||||
Delete {type} |
|
||||
</AlertDialogHeader> |
|
||||
|
|
||||
<AlertDialogBody> |
<AlertDialogBody> |
||||
{type === 'User' && <span>All items associated with this user will also be <strong>permantly deleted. </strong></span>} |
{type === 'User' && ( |
||||
Are you sure? You will not be able to undo this action. |
<span> |
||||
</AlertDialogBody> |
All items associated with this user will also be{' '} |
||||
|
<strong>permantly deleted. </strong> |
||||
|
</span> |
||||
|
)} |
||||
|
Are you sure? You will not be able to undo this action. |
||||
|
</AlertDialogBody> |
||||
|
|
||||
<AlertDialogFooter gap={3}> |
<AlertDialogFooter gap={3}> |
||||
<Button bg='ui.danger' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}> |
<Button |
||||
Delete |
bg="ui.danger" |
||||
</Button> |
color="white" |
||||
<Button ref={cancelRef} onClick={onClose} isDisabled={isSubmitting}> |
_hover={{ opacity: 0.8 }} |
||||
Cancel |
type="submit" |
||||
</Button> |
isLoading={isSubmitting} |
||||
</AlertDialogFooter> |
> |
||||
</AlertDialogContent> |
Delete |
||||
</AlertDialogOverlay> |
</Button> |
||||
</AlertDialog> |
<Button |
||||
</> |
ref={cancelRef} |
||||
) |
onClick={onClose} |
||||
|
isDisabled={isSubmitting} |
||||
|
> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
</AlertDialogFooter> |
||||
|
</AlertDialogContent> |
||||
|
</AlertDialogOverlay> |
||||
|
</AlertDialog> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default Delete; |
export default Delete |
||||
|
@ -1,37 +1,43 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { Button, Flex, Icon, useDisclosure } from '@chakra-ui/react' |
||||
|
import { FaPlus } from 'react-icons/fa' |
||||
|
|
||||
import { Button, Flex, Icon, useDisclosure } from '@chakra-ui/react'; |
import AddUser from '../Admin/AddUser' |
||||
import { FaPlus } from 'react-icons/fa'; |
import AddItem from '../Items/AddItem' |
||||
|
|
||||
import AddUser from '../Admin/AddUser'; |
|
||||
import AddItem from '../Items/AddItem'; |
|
||||
|
|
||||
interface NavbarProps { |
interface NavbarProps { |
||||
type: string; |
type: string |
||||
} |
} |
||||
|
|
||||
const Navbar: React.FC<NavbarProps> = ({ type }) => { |
const Navbar: React.FC<NavbarProps> = ({ type }) => { |
||||
const addUserModal = useDisclosure(); |
const addUserModal = useDisclosure() |
||||
const addItemModal = useDisclosure(); |
const addItemModal = useDisclosure() |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Flex py={8} gap={4}> |
<Flex py={8} gap={4}> |
||||
{/* TODO: Complete search functionality */} |
{/* TODO: Complete search functionality */} |
||||
{/* <InputGroup w={{ base: '100%', md: 'auto' }}> |
{/* <InputGroup w={{ base: '100%', md: 'auto' }}> |
||||
<InputLeftElement pointerEvents='none'> |
<InputLeftElement pointerEvents='none'> |
||||
<Icon as={FaSearch} color='gray.400' /> |
<Icon as={FaSearch} color='gray.400' /> |
||||
</InputLeftElement> |
</InputLeftElement> |
||||
<Input type='text' placeholder='Search' fontSize={{ base: 'sm', md: 'inherit' }} borderRadius='8px' /> |
<Input type='text' placeholder='Search' fontSize={{ base: 'sm', md: 'inherit' }} borderRadius='8px' /> |
||||
</InputGroup> */} |
</InputGroup> */} |
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} gap={1} fontSize={{ base: 'sm', md: 'inherit' }} onClick={type === 'User' ? addUserModal.onOpen : addItemModal.onOpen}> |
<Button |
||||
<Icon as={FaPlus} /> Add {type} |
bg="ui.main" |
||||
</Button> |
color="white" |
||||
<AddUser isOpen={addUserModal.isOpen} onClose={addUserModal.onClose} /> |
_hover={{ opacity: 0.8 }} |
||||
<AddItem isOpen={addItemModal.isOpen} onClose={addItemModal.onClose} /> |
gap={1} |
||||
</Flex > |
fontSize={{ base: 'sm', md: 'inherit' }} |
||||
</> |
onClick={type === 'User' ? addUserModal.onOpen : addItemModal.onOpen} |
||||
); |
> |
||||
}; |
<Icon as={FaPlus} /> Add {type} |
||||
|
</Button> |
||||
|
<AddUser isOpen={addUserModal.isOpen} onClose={addUserModal.onClose} /> |
||||
|
<AddItem isOpen={addItemModal.isOpen} onClose={addItemModal.onClose} /> |
||||
|
</Flex> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default Navbar; |
export default Navbar |
||||
|
@ -1,22 +1,42 @@ |
|||||
import { Button, Container, Text } from '@chakra-ui/react'; |
import React from 'react' |
||||
import { Link } from '@tanstack/react-router'; |
import { Button, Container, Text } from '@chakra-ui/react' |
||||
|
import { Link } from '@tanstack/react-router' |
||||
|
|
||||
const NotFound: React.FC = () => { |
const NotFound: React.FC = () => { |
||||
|
return ( |
||||
return ( |
<> |
||||
<> |
<Container |
||||
<Container h='100vh' |
h="100vh" |
||||
alignItems='stretch' |
alignItems="stretch" |
||||
justifyContent='center' textAlign='center' maxW='sm' centerContent> |
justifyContent="center" |
||||
<Text fontSize='8xl' color='ui.main' fontWeight='bold' lineHeight='1' mb={4}>404</Text> |
textAlign="center" |
||||
<Text fontSize='md'>Oops!</Text> |
maxW="sm" |
||||
<Text fontSize='md'>Page not found.</Text> |
centerContent |
||||
<Button as={Link} to='/' color='ui.main' borderColor='ui.main' variant='outline' mt={4}>Go back</Button> |
> |
||||
</Container> |
<Text |
||||
</> |
fontSize="8xl" |
||||
); |
color="ui.main" |
||||
|
fontWeight="bold" |
||||
|
lineHeight="1" |
||||
|
mb={4} |
||||
|
> |
||||
|
404 |
||||
|
</Text> |
||||
|
<Text fontSize="md">Oops!</Text> |
||||
|
<Text fontSize="md">Page not found.</Text> |
||||
|
<Button |
||||
|
as={Link} |
||||
|
to="/" |
||||
|
color="ui.main" |
||||
|
borderColor="ui.main" |
||||
|
variant="outline" |
||||
|
mt={4} |
||||
|
> |
||||
|
Go back |
||||
|
</Button> |
||||
|
</Container> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default NotFound; |
export default NotFound |
||||
|
|
||||
|
|
||||
|
@ -1,70 +1,117 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Box, |
||||
|
Drawer, |
||||
|
DrawerBody, |
||||
|
DrawerCloseButton, |
||||
|
DrawerContent, |
||||
|
DrawerOverlay, |
||||
|
Flex, |
||||
|
IconButton, |
||||
|
Image, |
||||
|
Text, |
||||
|
useColorModeValue, |
||||
|
useDisclosure, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { FiLogOut, FiMenu } from 'react-icons/fi' |
||||
|
import { useQueryClient } from 'react-query' |
||||
|
|
||||
import { Box, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerOverlay, Flex, IconButton, Image, Text, useColorModeValue, useDisclosure } from '@chakra-ui/react'; |
import Logo from '../../assets/images/fastapi-logo.svg' |
||||
import { FiLogOut, FiMenu } from 'react-icons/fi'; |
import { UserOut } from '../../client' |
||||
import { useQueryClient } from 'react-query'; |
import useAuth from '../../hooks/useAuth' |
||||
|
import SidebarItems from './SidebarItems' |
||||
import Logo from '../../assets/images/fastapi-logo.svg'; |
|
||||
import { UserOut } from '../../client'; |
|
||||
import useAuth from '../../hooks/useAuth'; |
|
||||
import SidebarItems from './SidebarItems'; |
|
||||
|
|
||||
const Sidebar: React.FC = () => { |
const Sidebar: React.FC = () => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const bgColor = useColorModeValue('white', '#1a202c'); |
const bgColor = useColorModeValue('white', '#1a202c') |
||||
const textColor = useColorModeValue('gray', 'white'); |
const textColor = useColorModeValue('gray', 'white') |
||||
const secBgColor = useColorModeValue('ui.secondary', '#252d3d'); |
const secBgColor = useColorModeValue('ui.secondary', '#252d3d') |
||||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
const currentUser = queryClient.getQueryData<UserOut>('currentUser') |
||||
const { isOpen, onOpen, onClose } = useDisclosure(); |
const { isOpen, onOpen, onClose } = useDisclosure() |
||||
const { logout } = useAuth(); |
const { logout } = useAuth() |
||||
|
|
||||
const handleLogout = async () => { |
|
||||
logout() |
|
||||
}; |
|
||||
|
|
||||
|
const handleLogout = async () => { |
||||
|
logout() |
||||
|
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
{/* Mobile */} |
{/* Mobile */} |
||||
<IconButton onClick={onOpen} display={{ base: 'flex', md: 'none' }} aria-label='Open Menu' position='absolute' fontSize='20px' m={4} icon={<FiMenu />} /> |
<IconButton |
||||
<Drawer isOpen={isOpen} placement='left' onClose={onClose}> |
onClick={onOpen} |
||||
<DrawerOverlay /> |
display={{ base: 'flex', md: 'none' }} |
||||
<DrawerContent maxW='250px'> |
aria-label="Open Menu" |
||||
<DrawerCloseButton /> |
position="absolute" |
||||
<DrawerBody py={8}> |
fontSize="20px" |
||||
<Flex flexDir='column' justify='space-between'> |
m={4} |
||||
<Box> |
icon={<FiMenu />} |
||||
<Image src={Logo} alt='logo' p={6} /> |
/> |
||||
<SidebarItems onClose={onClose} /> |
<Drawer isOpen={isOpen} placement="left" onClose={onClose}> |
||||
<Flex as='button' onClick={handleLogout} p={2} color='ui.danger' fontWeight='bold' alignItems='center'> |
<DrawerOverlay /> |
||||
<FiLogOut /> |
<DrawerContent maxW="250px"> |
||||
<Text ml={2}>Log out</Text> |
<DrawerCloseButton /> |
||||
</Flex> |
<DrawerBody py={8}> |
||||
</Box> |
<Flex flexDir="column" justify="space-between"> |
||||
{ |
<Box> |
||||
currentUser?.email && |
<Image src={Logo} alt="logo" p={6} /> |
||||
<Text color={textColor} noOfLines={2} fontSize='sm' p={2}>Logged in as: {currentUser.email}</Text> |
<SidebarItems onClose={onClose} /> |
||||
} |
<Flex |
||||
</Flex> |
as="button" |
||||
</DrawerBody> |
onClick={handleLogout} |
||||
</DrawerContent> |
p={2} |
||||
</Drawer> |
color="ui.danger" |
||||
|
fontWeight="bold" |
||||
{/* Desktop */} |
alignItems="center" |
||||
<Box bg={bgColor} p={3} h='100vh' position='sticky' top='0' display={{ base: 'none', md: 'flex' }}> |
> |
||||
<Flex flexDir='column' justify='space-between' bg={secBgColor} p={4} borderRadius={12}> |
<FiLogOut /> |
||||
<Box> |
<Text ml={2}>Log out</Text> |
||||
<Image src={Logo} alt='Logo' w='180px' maxW='2xs' p={6} /> |
|
||||
<SidebarItems /> |
|
||||
</Box> |
|
||||
{ |
|
||||
currentUser?.email && |
|
||||
<Text color={textColor} noOfLines={2} fontSize='sm' p={2} maxW='180px'>Logged in as: {currentUser.email}</Text> |
|
||||
} |
|
||||
</Flex> |
</Flex> |
||||
</Box> |
</Box> |
||||
</> |
{currentUser?.email && ( |
||||
); |
<Text color={textColor} noOfLines={2} fontSize="sm" p={2}> |
||||
|
Logged in as: {currentUser.email} |
||||
|
</Text> |
||||
|
)} |
||||
|
</Flex> |
||||
|
</DrawerBody> |
||||
|
</DrawerContent> |
||||
|
</Drawer> |
||||
|
|
||||
|
{/* Desktop */} |
||||
|
<Box |
||||
|
bg={bgColor} |
||||
|
p={3} |
||||
|
h="100vh" |
||||
|
position="sticky" |
||||
|
top="0" |
||||
|
display={{ base: 'none', md: 'flex' }} |
||||
|
> |
||||
|
<Flex |
||||
|
flexDir="column" |
||||
|
justify="space-between" |
||||
|
bg={secBgColor} |
||||
|
p={4} |
||||
|
borderRadius={12} |
||||
|
> |
||||
|
<Box> |
||||
|
<Image src={Logo} alt="Logo" w="180px" maxW="2xs" p={6} /> |
||||
|
<SidebarItems /> |
||||
|
</Box> |
||||
|
{currentUser?.email && ( |
||||
|
<Text |
||||
|
color={textColor} |
||||
|
noOfLines={2} |
||||
|
fontSize="sm" |
||||
|
p={2} |
||||
|
maxW="180px" |
||||
|
> |
||||
|
Logged in as: {currentUser.email} |
||||
|
</Text> |
||||
|
)} |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default Sidebar; |
export default Sidebar |
||||
|
@ -1,60 +1,57 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { Box, Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react' |
||||
|
import { FiBriefcase, FiHome, FiSettings, FiUsers } from 'react-icons/fi' |
||||
|
import { Link } from '@tanstack/react-router' |
||||
|
import { useQueryClient } from 'react-query' |
||||
|
|
||||
import { Box, Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react'; |
import { UserOut } from '../../client' |
||||
import { FiBriefcase, FiHome, FiSettings, FiUsers } from 'react-icons/fi'; |
|
||||
import { Link } from '@tanstack/react-router'; |
|
||||
import { useQueryClient } from 'react-query'; |
|
||||
|
|
||||
import { UserOut } from '../../client'; |
|
||||
|
|
||||
const items = [ |
const items = [ |
||||
{ icon: FiHome, title: 'Dashboard', path: '/' }, |
{ icon: FiHome, title: 'Dashboard', path: '/' }, |
||||
{ icon: FiBriefcase, title: 'Items', path: '/items' }, |
{ icon: FiBriefcase, title: 'Items', path: '/items' }, |
||||
{ icon: FiSettings, title: 'User Settings', path: '/settings' }, |
{ icon: FiSettings, title: 'User Settings', path: '/settings' }, |
||||
]; |
] |
||||
|
|
||||
interface SidebarItemsProps { |
interface SidebarItemsProps { |
||||
onClose?: () => void; |
onClose?: () => void |
||||
} |
} |
||||
|
|
||||
const SidebarItems: React.FC<SidebarItemsProps> = ({ onClose }) => { |
const SidebarItems: React.FC<SidebarItemsProps> = ({ onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const textColor = useColorModeValue('ui.main', '#E2E8F0'); |
const textColor = useColorModeValue('ui.main', '#E2E8F0') |
||||
const bgActive = useColorModeValue('#E2E8F0', '#4A5568'); |
const bgActive = useColorModeValue('#E2E8F0', '#4A5568') |
||||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
const currentUser = queryClient.getQueryData<UserOut>('currentUser') |
||||
|
|
||||
|
const finalItems = currentUser?.is_superuser |
||||
const finalItems = currentUser?.is_superuser ? [...items, { icon: FiUsers, title: 'Admin', path: '/admin' }] : items; |
? [...items, { icon: FiUsers, title: 'Admin', path: '/admin' }] |
||||
|
: items |
||||
const listItems = finalItems.map((item) => ( |
|
||||
<Flex |
const listItems = finalItems.map((item) => ( |
||||
as={Link} |
<Flex |
||||
to={item.path} |
as={Link} |
||||
w='100%' |
to={item.path} |
||||
p={2} |
w="100%" |
||||
key={item.title} |
p={2} |
||||
activeProps={{ |
key={item.title} |
||||
style: { |
activeProps={{ |
||||
background: bgActive, |
style: { |
||||
borderRadius: '12px', |
background: bgActive, |
||||
}, |
borderRadius: '12px', |
||||
}} |
}, |
||||
color={textColor} |
}} |
||||
onClick={onClose} |
color={textColor} |
||||
> |
onClick={onClose} |
||||
<Icon as={item.icon} alignSelf='center' /> |
> |
||||
<Text ml={2}>{item.title}</Text> |
<Icon as={item.icon} alignSelf="center" /> |
||||
</Flex> |
<Text ml={2}>{item.title}</Text> |
||||
)); |
</Flex> |
||||
|
)) |
||||
return ( |
|
||||
<> |
return ( |
||||
<Box> |
<> |
||||
{listItems} |
<Box>{listItems}</Box> |
||||
</Box> |
</> |
||||
|
) |
||||
</> |
} |
||||
); |
|
||||
}; |
export default SidebarItems |
||||
|
|
||||
export default SidebarItems; |
|
||||
|
@ -1,43 +1,59 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Box, |
||||
|
IconButton, |
||||
|
Menu, |
||||
|
MenuButton, |
||||
|
MenuItem, |
||||
|
MenuList, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { FaUserAstronaut } from 'react-icons/fa' |
||||
|
import { FiLogOut, FiUser } from 'react-icons/fi' |
||||
|
|
||||
import { Box, IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react'; |
import useAuth from '../../hooks/useAuth' |
||||
import { FaUserAstronaut } from 'react-icons/fa'; |
import { Link } from '@tanstack/react-router' |
||||
import { FiLogOut, FiUser } from 'react-icons/fi'; |
|
||||
|
|
||||
import useAuth from '../../hooks/useAuth'; |
|
||||
import { Link } from '@tanstack/react-router'; |
|
||||
|
|
||||
const UserMenu: React.FC = () => { |
const UserMenu: React.FC = () => { |
||||
const { logout } = useAuth(); |
const { logout } = useAuth() |
||||
|
|
||||
const handleLogout = async () => { |
const handleLogout = async () => { |
||||
logout() |
logout() |
||||
}; |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
{/* Desktop */} |
{/* Desktop */} |
||||
<Box display={{ base: 'none', md: 'block' }} position='fixed' top={4} right={4}> |
<Box |
||||
<Menu> |
display={{ base: 'none', md: 'block' }} |
||||
<MenuButton |
position="fixed" |
||||
as={IconButton} |
top={4} |
||||
aria-label='Options' |
right={4} |
||||
icon={<FaUserAstronaut color='white' fontSize='18px' />} |
> |
||||
bg='ui.main' |
<Menu> |
||||
isRound |
<MenuButton |
||||
/> |
as={IconButton} |
||||
<MenuList> |
aria-label="Options" |
||||
<MenuItem icon={<FiUser fontSize='18px' />} as={Link} to='settings'> |
icon={<FaUserAstronaut color="white" fontSize="18px" />} |
||||
My profile |
bg="ui.main" |
||||
</MenuItem> |
isRound |
||||
<MenuItem icon={<FiLogOut fontSize='18px' />} onClick={handleLogout} color='ui.danger' fontWeight='bold'> |
/> |
||||
Log out |
<MenuList> |
||||
</MenuItem> |
<MenuItem icon={<FiUser fontSize="18px" />} as={Link} to="settings"> |
||||
</MenuList> |
My profile |
||||
</Menu> |
</MenuItem> |
||||
</Box> |
<MenuItem |
||||
</> |
icon={<FiLogOut fontSize="18px" />} |
||||
); |
onClick={handleLogout} |
||||
}; |
color="ui.danger" |
||||
|
fontWeight="bold" |
||||
|
> |
||||
|
Log out |
||||
|
</MenuItem> |
||||
|
</MenuList> |
||||
|
</Menu> |
||||
|
</Box> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default UserMenu; |
export default UserMenu |
||||
|
@ -1,98 +1,123 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Button, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Input, |
||||
|
Modal, |
||||
|
ModalBody, |
||||
|
ModalCloseButton, |
||||
|
ModalContent, |
||||
|
ModalFooter, |
||||
|
ModalHeader, |
||||
|
ModalOverlay, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
import { useMutation, useQueryClient } from 'react-query' |
||||
|
|
||||
import { Button, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
import { ApiError, ItemCreate, ItemsService } from '../../client' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
import { useMutation, useQueryClient } from 'react-query'; |
|
||||
|
|
||||
import { ApiError, ItemCreate, ItemsService } from '../../client'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
interface AddItemProps { |
interface AddItemProps { |
||||
isOpen: boolean; |
isOpen: boolean |
||||
onClose: () => void; |
onClose: () => void |
||||
} |
} |
||||
|
|
||||
const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => { |
const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm<ItemCreate>({ |
const { |
||||
mode: 'onBlur', |
register, |
||||
criteriaMode: 'all', |
handleSubmit, |
||||
defaultValues: { |
reset, |
||||
title: '', |
formState: { errors, isSubmitting }, |
||||
description: '', |
} = useForm<ItemCreate>({ |
||||
}, |
mode: 'onBlur', |
||||
}); |
criteriaMode: 'all', |
||||
|
defaultValues: { |
||||
|
title: '', |
||||
|
description: '', |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
const addItem = async (data: ItemCreate) => { |
const addItem = async (data: ItemCreate) => { |
||||
await ItemsService.createItem({ requestBody: data }) |
await ItemsService.createItem({ requestBody: data }) |
||||
} |
} |
||||
|
|
||||
const mutation = useMutation(addItem, { |
const mutation = useMutation(addItem, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success!', 'Item created successfully.', 'success'); |
showToast('Success!', 'Item created successfully.', 'success') |
||||
reset(); |
reset() |
||||
onClose(); |
onClose() |
||||
}, |
}, |
||||
onError: (err: ApiError) => { |
onError: (err: ApiError) => { |
||||
const errDetail = err.body.detail; |
const errDetail = err.body.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
}, |
}, |
||||
onSettled: () => { |
onSettled: () => { |
||||
queryClient.invalidateQueries('items'); |
queryClient.invalidateQueries('items') |
||||
} |
}, |
||||
}); |
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<ItemCreate> = (data) => { |
const onSubmit: SubmitHandler<ItemCreate> = (data) => { |
||||
mutation.mutate(data); |
mutation.mutate(data) |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Modal |
<Modal |
||||
isOpen={isOpen} |
isOpen={isOpen} |
||||
onClose={onClose} |
onClose={onClose} |
||||
size={{ base: 'sm', md: 'md' }} |
size={{ base: 'sm', md: 'md' }} |
||||
isCentered |
isCentered |
||||
> |
> |
||||
<ModalOverlay /> |
<ModalOverlay /> |
||||
<ModalContent as='form' onSubmit={handleSubmit(onSubmit)}> |
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
<ModalHeader>Add Item</ModalHeader> |
<ModalHeader>Add Item</ModalHeader> |
||||
<ModalCloseButton /> |
<ModalCloseButton /> |
||||
<ModalBody pb={6}> |
<ModalBody pb={6}> |
||||
<FormControl isRequired isInvalid={!!errors.title}> |
<FormControl isRequired isInvalid={!!errors.title}> |
||||
<FormLabel htmlFor='title'>Title</FormLabel> |
<FormLabel htmlFor="title">Title</FormLabel> |
||||
<Input |
<Input |
||||
id='title' |
id="title" |
||||
{...register('title', { required: 'Title is required.' })} |
{...register('title', { |
||||
placeholder='Title' |
required: 'Title is required.', |
||||
type='text' |
})} |
||||
/> |
placeholder="Title" |
||||
{errors.title && <FormErrorMessage>{errors.title.message}</FormErrorMessage>} |
type="text" |
||||
</FormControl> |
/> |
||||
<FormControl mt={4}> |
{errors.title && ( |
||||
<FormLabel htmlFor='description'>Description</FormLabel> |
<FormErrorMessage>{errors.title.message}</FormErrorMessage> |
||||
<Input |
)} |
||||
id='description' |
</FormControl> |
||||
{...register('description')} |
<FormControl mt={4}> |
||||
placeholder='Description' |
<FormLabel htmlFor="description">Description</FormLabel> |
||||
type='text' |
<Input |
||||
/> |
id="description" |
||||
</FormControl> |
{...register('description')} |
||||
</ModalBody> |
placeholder="Description" |
||||
|
type="text" |
||||
|
/> |
||||
|
</FormControl> |
||||
|
</ModalBody> |
||||
|
|
||||
<ModalFooter gap={3}> |
<ModalFooter gap={3}> |
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}> |
<Button |
||||
Save |
bg="ui.main" |
||||
</Button> |
color="white" |
||||
<Button onClick={onClose}> |
_hover={{ opacity: 0.8 }} |
||||
Cancel |
type="submit" |
||||
</Button> |
isLoading={isSubmitting} |
||||
</ModalFooter> |
> |
||||
</ModalContent> |
Save |
||||
</Modal> |
</Button> |
||||
</> |
<Button onClick={onClose}>Cancel</Button> |
||||
); |
</ModalFooter> |
||||
}; |
</ModalContent> |
||||
|
</Modal> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default AddItem; |
export default AddItem |
||||
|
@ -1,87 +1,124 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Button, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Input, |
||||
|
Modal, |
||||
|
ModalBody, |
||||
|
ModalCloseButton, |
||||
|
ModalContent, |
||||
|
ModalFooter, |
||||
|
ModalHeader, |
||||
|
ModalOverlay, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
|
||||
import { Button, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
import { useMutation, useQueryClient } from 'react-query' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import { ApiError, ItemOut, ItemUpdate, ItemsService } from '../../client' |
||||
|
import useCustomToast from '../../hooks/useCustomToast' |
||||
import { useMutation, useQueryClient } from 'react-query'; |
|
||||
import { ApiError, ItemOut, ItemUpdate, ItemsService } from '../../client'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
interface EditItemProps { |
interface EditItemProps { |
||||
item: ItemOut; |
item: ItemOut |
||||
isOpen: boolean; |
isOpen: boolean |
||||
onClose: () => void; |
onClose: () => void |
||||
} |
} |
||||
|
|
||||
const EditItem: React.FC<EditItemProps> = ({ item, isOpen, onClose }) => { |
const EditItem: React.FC<EditItemProps> = ({ item, isOpen, onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const { register, handleSubmit, reset, formState: { isSubmitting, errors, isDirty } } = useForm<ItemUpdate>({ |
const { |
||||
mode: 'onBlur', |
register, |
||||
criteriaMode: 'all', |
handleSubmit, |
||||
defaultValues: item |
reset, |
||||
}); |
formState: { isSubmitting, errors, isDirty }, |
||||
|
} = useForm<ItemUpdate>({ |
||||
|
mode: 'onBlur', |
||||
|
criteriaMode: 'all', |
||||
|
defaultValues: item, |
||||
|
}) |
||||
|
|
||||
const updateItem = async (data: ItemUpdate) => { |
const updateItem = async (data: ItemUpdate) => { |
||||
await ItemsService.updateItem({ id: item.id, requestBody: data }); |
await ItemsService.updateItem({ id: item.id, requestBody: data }) |
||||
} |
} |
||||
|
|
||||
const mutation = useMutation(updateItem, { |
const mutation = useMutation(updateItem, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success!', 'Item updated successfully.', 'success'); |
showToast('Success!', 'Item updated successfully.', 'success') |
||||
onClose(); |
onClose() |
||||
}, |
}, |
||||
onError: (err: ApiError) => { |
onError: (err: ApiError) => { |
||||
const errDetail = err.body.detail; |
const errDetail = err.body.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
}, |
}, |
||||
onSettled: () => { |
onSettled: () => { |
||||
queryClient.invalidateQueries('items'); |
queryClient.invalidateQueries('items') |
||||
} |
}, |
||||
}); |
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<ItemUpdate> = async (data) => { |
const onSubmit: SubmitHandler<ItemUpdate> = async (data) => { |
||||
mutation.mutate(data) |
mutation.mutate(data) |
||||
} |
} |
||||
|
|
||||
const onCancel = () => { |
const onCancel = () => { |
||||
reset(); |
reset() |
||||
onClose(); |
onClose() |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Modal |
<Modal |
||||
isOpen={isOpen} |
isOpen={isOpen} |
||||
onClose={onClose} |
onClose={onClose} |
||||
size={{ base: 'sm', md: 'md' }} |
size={{ base: 'sm', md: 'md' }} |
||||
isCentered |
isCentered |
||||
|
> |
||||
|
<ModalOverlay /> |
||||
|
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
|
<ModalHeader>Edit Item</ModalHeader> |
||||
|
<ModalCloseButton /> |
||||
|
<ModalBody pb={6}> |
||||
|
<FormControl isInvalid={!!errors.title}> |
||||
|
<FormLabel htmlFor="title">Title</FormLabel> |
||||
|
<Input |
||||
|
id="title" |
||||
|
{...register('title', { |
||||
|
required: 'Title is required', |
||||
|
})} |
||||
|
type="text" |
||||
|
/> |
||||
|
{errors.title && ( |
||||
|
<FormErrorMessage>{errors.title.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor="description">Description</FormLabel> |
||||
|
<Input |
||||
|
id="description" |
||||
|
{...register('description')} |
||||
|
placeholder="Description" |
||||
|
type="text" |
||||
|
/> |
||||
|
</FormControl> |
||||
|
</ModalBody> |
||||
|
<ModalFooter gap={3}> |
||||
|
<Button |
||||
|
bg="ui.main" |
||||
|
color="white" |
||||
|
_hover={{ opacity: 0.8 }} |
||||
|
type="submit" |
||||
|
isLoading={isSubmitting} |
||||
|
isDisabled={!isDirty} |
||||
> |
> |
||||
<ModalOverlay /> |
Save |
||||
<ModalContent as='form' onSubmit={handleSubmit(onSubmit)}> |
</Button> |
||||
<ModalHeader>Edit Item</ModalHeader> |
<Button onClick={onCancel}>Cancel</Button> |
||||
<ModalCloseButton /> |
</ModalFooter> |
||||
<ModalBody pb={6}> |
</ModalContent> |
||||
<FormControl isInvalid={!!errors.title}> |
</Modal> |
||||
<FormLabel htmlFor='title'>Title</FormLabel> |
</> |
||||
<Input id='title' {...register('title', { required: 'Title is required' })} type='text' /> |
) |
||||
{errors.title && <FormErrorMessage>{errors.title.message}</FormErrorMessage>} |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel htmlFor='description'>Description</FormLabel> |
|
||||
<Input id='description' {...register('description')} placeholder='Description' type='text' /> |
|
||||
</FormControl> |
|
||||
</ModalBody> |
|
||||
<ModalFooter gap={3}> |
|
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting} isDisabled={!isDirty}> |
|
||||
Save |
|
||||
</Button> |
|
||||
<Button onClick={onCancel}>Cancel</Button> |
|
||||
</ModalFooter> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
</> |
|
||||
) |
|
||||
} |
} |
||||
|
|
||||
export default EditItem; |
export default EditItem |
||||
|
@ -1,29 +1,39 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
import { Badge, Container, Heading, Radio, RadioGroup, Stack, useColorMode } from '@chakra-ui/react'; |
Badge, |
||||
|
Container, |
||||
|
Heading, |
||||
|
Radio, |
||||
|
RadioGroup, |
||||
|
Stack, |
||||
|
useColorMode, |
||||
|
} from '@chakra-ui/react' |
||||
|
|
||||
const Appearance: React.FC = () => { |
const Appearance: React.FC = () => { |
||||
const { colorMode, toggleColorMode } = useColorMode(); |
const { colorMode, toggleColorMode } = useColorMode() |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Container maxW='full'> |
<Container maxW="full"> |
||||
<Heading size='sm' py={4}> |
<Heading size="sm" py={4}> |
||||
Appearance |
Appearance |
||||
</Heading> |
</Heading> |
||||
<RadioGroup onChange={toggleColorMode} value={colorMode}> |
<RadioGroup onChange={toggleColorMode} value={colorMode}> |
||||
<Stack> |
<Stack> |
||||
{/* TODO: Add system default option */} |
{/* TODO: Add system default option */} |
||||
<Radio value='light' colorScheme='teal'> |
<Radio value="light" colorScheme="teal"> |
||||
Light mode<Badge ml='1' colorScheme='teal'>Default</Badge> |
Light mode |
||||
</Radio> |
<Badge ml="1" colorScheme="teal"> |
||||
<Radio value='dark' colorScheme='teal'> |
Default |
||||
Dark mode |
</Badge> |
||||
</Radio> |
</Radio> |
||||
</Stack> |
<Radio value="dark" colorScheme="teal"> |
||||
</RadioGroup> |
Dark mode |
||||
</Container> |
</Radio> |
||||
</> |
</Stack> |
||||
); |
</RadioGroup> |
||||
|
</Container> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
export default Appearance; |
export default Appearance |
||||
|
@ -1,74 +1,137 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Box, |
||||
|
Button, |
||||
|
Container, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Heading, |
||||
|
Input, |
||||
|
useColorModeValue, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
import { useMutation } from 'react-query' |
||||
|
|
||||
import { Box, Button, Container, FormControl, FormErrorMessage, FormLabel, Heading, Input, useColorModeValue } from '@chakra-ui/react'; |
import { ApiError, UpdatePassword, UsersService } from '../../client' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
import { useMutation } from 'react-query'; |
|
||||
|
|
||||
import { ApiError, UpdatePassword, UsersService } from '../../client'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
interface UpdatePasswordForm extends UpdatePassword { |
interface UpdatePasswordForm extends UpdatePassword { |
||||
confirm_password: string; |
confirm_password: string |
||||
} |
} |
||||
|
|
||||
const ChangePassword: React.FC = () => { |
const ChangePassword: React.FC = () => { |
||||
const color = useColorModeValue('gray.700', 'white'); |
const color = useColorModeValue('gray.700', 'white') |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting } } = useForm<UpdatePasswordForm>({ |
const { |
||||
mode: 'onBlur', |
register, |
||||
criteriaMode: 'all' |
handleSubmit, |
||||
}); |
reset, |
||||
|
getValues, |
||||
|
formState: { errors, isSubmitting }, |
||||
|
} = useForm<UpdatePasswordForm>({ |
||||
|
mode: 'onBlur', |
||||
|
criteriaMode: 'all', |
||||
|
}) |
||||
|
|
||||
const UpdatePassword = async (data: UpdatePassword) => { |
const UpdatePassword = async (data: UpdatePassword) => { |
||||
await UsersService.updatePasswordMe({ requestBody: data }) |
await UsersService.updatePasswordMe({ requestBody: data }) |
||||
} |
} |
||||
|
|
||||
const mutation = useMutation(UpdatePassword, { |
const mutation = useMutation(UpdatePassword, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success!', 'Password updated.', 'success'); |
showToast('Success!', 'Password updated.', 'success') |
||||
reset(); |
reset() |
||||
}, |
}, |
||||
onError: (err: ApiError) => { |
onError: (err: ApiError) => { |
||||
const errDetail = err.body.detail; |
const errDetail = err.body.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
} |
}, |
||||
}) |
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<UpdatePasswordForm> = async (data) => { |
const onSubmit: SubmitHandler<UpdatePasswordForm> = async (data) => { |
||||
mutation.mutate(data); |
mutation.mutate(data) |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Container maxW='full' as='form' onSubmit={handleSubmit(onSubmit)}> |
<Container maxW="full" as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
<Heading size='sm' py={4}> |
<Heading size="sm" py={4}> |
||||
Change Password |
Change Password |
||||
</Heading> |
</Heading> |
||||
<Box w={{ 'sm': 'full', 'md': '50%' }}> |
<Box w={{ sm: 'full', md: '50%' }}> |
||||
<FormControl isRequired isInvalid={!!errors.current_password}> |
<FormControl isRequired isInvalid={!!errors.current_password}> |
||||
<FormLabel color={color} htmlFor='current_password'>Current password</FormLabel> |
<FormLabel color={color} htmlFor="current_password"> |
||||
<Input id='current_password' {...register('current_password', { required: 'Password is required', minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='Password' type='password' /> |
Current password |
||||
{errors.current_password && <FormErrorMessage>{errors.current_password.message}</FormErrorMessage>} |
</FormLabel> |
||||
</FormControl> |
<Input |
||||
<FormControl mt={4} isRequired isInvalid={!!errors.new_password}> |
id="current_password" |
||||
<FormLabel htmlFor='password'>Set Password</FormLabel> |
{...register('current_password', { |
||||
<Input id='password' {...register('new_password', { required: 'Password is required', minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='Password' type='password' /> |
required: 'Password is required', |
||||
{errors.new_password && <FormErrorMessage>{errors.new_password.message}</FormErrorMessage>} |
minLength: { |
||||
</FormControl> |
value: 8, |
||||
<FormControl mt={4} isRequired isInvalid={!!errors.confirm_password}> |
message: 'Password must be at least 8 characters', |
||||
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel> |
}, |
||||
<Input id='confirm_password' {...register('confirm_password', { |
})} |
||||
required: 'Please confirm your password', |
placeholder="Password" |
||||
validate: value => value === getValues().new_password || 'The passwords do not match' |
type="password" |
||||
})} placeholder='Password' type='password' /> |
/> |
||||
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>} |
{errors.current_password && ( |
||||
</FormControl> |
<FormErrorMessage> |
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} mt={4} type='submit' isLoading={isSubmitting}> |
{errors.current_password.message} |
||||
Save |
</FormErrorMessage> |
||||
</Button> |
)} |
||||
</Box> |
</FormControl> |
||||
</ Container> |
<FormControl mt={4} isRequired isInvalid={!!errors.new_password}> |
||||
</> |
<FormLabel htmlFor="password">Set Password</FormLabel> |
||||
); |
<Input |
||||
|
id="password" |
||||
|
{...register('new_password', { |
||||
|
required: 'Password is required', |
||||
|
minLength: { |
||||
|
value: 8, |
||||
|
message: 'Password must be at least 8 characters', |
||||
|
}, |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.new_password && ( |
||||
|
<FormErrorMessage>{errors.new_password.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<FormControl mt={4} isRequired isInvalid={!!errors.confirm_password}> |
||||
|
<FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> |
||||
|
<Input |
||||
|
id="confirm_password" |
||||
|
{...register('confirm_password', { |
||||
|
required: 'Please confirm your password', |
||||
|
validate: (value) => |
||||
|
value === getValues().new_password || |
||||
|
'The passwords do not match', |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.confirm_password && ( |
||||
|
<FormErrorMessage> |
||||
|
{errors.confirm_password.message} |
||||
|
</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<Button |
||||
|
bg="ui.main" |
||||
|
color="white" |
||||
|
_hover={{ opacity: 0.8 }} |
||||
|
mt={4} |
||||
|
type="submit" |
||||
|
isLoading={isSubmitting} |
||||
|
> |
||||
|
Save |
||||
|
</Button> |
||||
|
</Box> |
||||
|
</Container> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
export default ChangePassword; |
export default ChangePassword |
||||
|
@ -1,27 +1,42 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
|
Button, |
||||
|
Container, |
||||
|
Heading, |
||||
|
Text, |
||||
|
useDisclosure, |
||||
|
} from '@chakra-ui/react' |
||||
|
|
||||
import { Button, Container, Heading, Text, useDisclosure } from '@chakra-ui/react'; |
import DeleteConfirmation from './DeleteConfirmation' |
||||
|
|
||||
import DeleteConfirmation from './DeleteConfirmation'; |
|
||||
|
|
||||
const DeleteAccount: React.FC = () => { |
const DeleteAccount: React.FC = () => { |
||||
const confirmationModal = useDisclosure(); |
const confirmationModal = useDisclosure() |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Container maxW='full'> |
<Container maxW="full"> |
||||
<Heading size='sm' py={4}> |
<Heading size="sm" py={4}> |
||||
Delete Account |
Delete Account |
||||
</Heading> |
</Heading> |
||||
<Text> |
<Text> |
||||
Are you sure you want to delete your account? This action cannot be undone. |
Are you sure you want to delete your account? This action cannot be |
||||
</Text> |
undone. |
||||
<Button bg='ui.danger' color='white' _hover={{ opacity: 0.8 }} mt={4} onClick={confirmationModal.onOpen}> |
</Text> |
||||
Delete |
<Button |
||||
</Button> |
bg="ui.danger" |
||||
<DeleteConfirmation isOpen={confirmationModal.isOpen} onClose={confirmationModal.onClose} /> |
color="white" |
||||
</ Container> |
_hover={{ opacity: 0.8 }} |
||||
</> |
mt={4} |
||||
); |
onClick={confirmationModal.onOpen} |
||||
|
> |
||||
|
Delete |
||||
|
</Button> |
||||
|
<DeleteConfirmation |
||||
|
isOpen={confirmationModal.isOpen} |
||||
|
onClose={confirmationModal.onClose} |
||||
|
/> |
||||
|
</Container> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
export default DeleteAccount; |
export default DeleteAccount |
||||
|
@ -1,86 +1,105 @@ |
|||||
import React from 'react'; |
import React from 'react' |
||||
|
import { |
||||
import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Button } from '@chakra-ui/react'; |
AlertDialog, |
||||
import { useForm } from 'react-hook-form'; |
AlertDialogBody, |
||||
import { useMutation, useQueryClient } from 'react-query'; |
AlertDialogContent, |
||||
|
AlertDialogFooter, |
||||
import { ApiError, UserOut, UsersService } from '../../client'; |
AlertDialogHeader, |
||||
import useAuth from '../../hooks/useAuth'; |
AlertDialogOverlay, |
||||
import useCustomToast from '../../hooks/useCustomToast'; |
Button, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { useForm } from 'react-hook-form' |
||||
|
import { useMutation, useQueryClient } from 'react-query' |
||||
|
|
||||
|
import { ApiError, UserOut, UsersService } from '../../client' |
||||
|
import useAuth from '../../hooks/useAuth' |
||||
|
import useCustomToast from '../../hooks/useCustomToast' |
||||
|
|
||||
interface DeleteProps { |
interface DeleteProps { |
||||
isOpen: boolean; |
isOpen: boolean |
||||
onClose: () => void; |
onClose: () => void |
||||
} |
} |
||||
|
|
||||
const DeleteConfirmation: React.FC<DeleteProps> = ({ isOpen, onClose }) => { |
const DeleteConfirmation: React.FC<DeleteProps> = ({ isOpen, onClose }) => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const cancelRef = React.useRef<HTMLButtonElement | null>(null); |
const cancelRef = React.useRef<HTMLButtonElement | null>(null) |
||||
const { handleSubmit, formState: { isSubmitting } } = useForm(); |
const { |
||||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
handleSubmit, |
||||
const { logout } = useAuth(); |
formState: { isSubmitting }, |
||||
|
} = useForm() |
||||
const deleteCurrentUser = async (id: number) => { |
const currentUser = queryClient.getQueryData<UserOut>('currentUser') |
||||
await UsersService.deleteUser({ userId: id }); |
const { logout } = useAuth() |
||||
} |
|
||||
|
const deleteCurrentUser = async (id: number) => { |
||||
const mutation = useMutation(deleteCurrentUser, { |
await UsersService.deleteUser({ userId: id }) |
||||
onSuccess: () => { |
} |
||||
showToast('Success', 'Your account has been successfully deleted.', 'success'); |
|
||||
logout(); |
const mutation = useMutation(deleteCurrentUser, { |
||||
onClose(); |
onSuccess: () => { |
||||
}, |
showToast( |
||||
onError: (err: ApiError) => { |
'Success', |
||||
const errDetail = err.body.detail; |
'Your account has been successfully deleted.', |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
'success', |
||||
}, |
) |
||||
onSettled: () => { |
logout() |
||||
queryClient.invalidateQueries('currentUser'); |
onClose() |
||||
} |
}, |
||||
}) |
onError: (err: ApiError) => { |
||||
|
const errDetail = err.body.detail |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
const onSubmit = async () => { |
}, |
||||
mutation.mutate(currentUser!.id); |
onSettled: () => { |
||||
} |
queryClient.invalidateQueries('currentUser') |
||||
|
}, |
||||
return ( |
}) |
||||
<> |
|
||||
<AlertDialog |
const onSubmit = async () => { |
||||
isOpen={isOpen} |
mutation.mutate(currentUser!.id) |
||||
onClose={onClose} |
} |
||||
leastDestructiveRef={cancelRef} |
|
||||
size={{ base: 'sm', md: 'md' }} |
return ( |
||||
isCentered |
<> |
||||
> |
<AlertDialog |
||||
<AlertDialogOverlay> |
isOpen={isOpen} |
||||
<AlertDialogContent as='form' onSubmit={handleSubmit(onSubmit)}> |
onClose={onClose} |
||||
<AlertDialogHeader> |
leastDestructiveRef={cancelRef} |
||||
Confirmation Required |
size={{ base: 'sm', md: 'md' }} |
||||
</AlertDialogHeader> |
isCentered |
||||
|
> |
||||
<AlertDialogBody> |
<AlertDialogOverlay> |
||||
All your account data will be <strong>permanently deleted.</strong> If you are sure, please click <strong>'Confirm'</strong> to proceed. |
<AlertDialogContent as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
</AlertDialogBody> |
<AlertDialogHeader>Confirmation Required</AlertDialogHeader> |
||||
|
|
||||
<AlertDialogFooter gap={3}> |
<AlertDialogBody> |
||||
<Button bg='ui.danger' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}> |
All your account data will be{' '} |
||||
Confirm |
<strong>permanently deleted.</strong> If you are sure, please |
||||
</Button> |
click <strong>'Confirm'</strong> to proceed. |
||||
<Button ref={cancelRef} onClick={onClose} isDisabled={isSubmitting}> |
</AlertDialogBody> |
||||
Cancel |
|
||||
</Button> |
<AlertDialogFooter gap={3}> |
||||
</AlertDialogFooter> |
<Button |
||||
</AlertDialogContent> |
bg="ui.danger" |
||||
</AlertDialogOverlay> |
color="white" |
||||
</AlertDialog > |
_hover={{ opacity: 0.8 }} |
||||
</> |
type="submit" |
||||
) |
isLoading={isSubmitting} |
||||
|
> |
||||
|
Confirm |
||||
|
</Button> |
||||
|
<Button |
||||
|
ref={cancelRef} |
||||
|
onClick={onClose} |
||||
|
isDisabled={isSubmitting} |
||||
|
> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
</AlertDialogFooter> |
||||
|
</AlertDialogContent> |
||||
|
</AlertDialogOverlay> |
||||
|
</AlertDialog> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default DeleteConfirmation; |
export default DeleteConfirmation |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@ -1,106 +1,147 @@ |
|||||
import React, { useState } from 'react'; |
import React, { useState } from 'react' |
||||
|
import { |
||||
|
Box, |
||||
|
Button, |
||||
|
Container, |
||||
|
Flex, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Heading, |
||||
|
Input, |
||||
|
Text, |
||||
|
useColorModeValue, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
import { useMutation, useQueryClient } from 'react-query' |
||||
|
|
||||
import { Box, Button, Container, Flex, FormControl, FormErrorMessage, FormLabel, Heading, Input, Text, useColorModeValue } from '@chakra-ui/react'; |
import { ApiError, UserOut, UserUpdateMe, UsersService } from '../../client' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import useAuth from '../../hooks/useAuth' |
||||
import { useMutation, useQueryClient } from 'react-query'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
|
|
||||
import { ApiError, UserOut, UserUpdateMe, UsersService } from '../../client'; |
|
||||
import useAuth from '../../hooks/useAuth'; |
|
||||
import useCustomToast from '../../hooks/useCustomToast'; |
|
||||
|
|
||||
const UserInformation: React.FC = () => { |
const UserInformation: React.FC = () => { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const color = useColorModeValue('gray.700', 'white'); |
const color = useColorModeValue('gray.700', 'white') |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const [editMode, setEditMode] = useState(false); |
const [editMode, setEditMode] = useState(false) |
||||
const { user: currentUser } = useAuth(); |
const { user: currentUser } = useAuth() |
||||
const { register, handleSubmit, reset, formState: { isSubmitting, errors, isDirty } } = useForm<UserOut>({ |
const { |
||||
mode: 'onBlur', criteriaMode: 'all', defaultValues: { |
register, |
||||
full_name: currentUser?.full_name, |
handleSubmit, |
||||
email: currentUser?.email |
reset, |
||||
} |
formState: { isSubmitting, errors, isDirty }, |
||||
}) |
} = useForm<UserOut>({ |
||||
|
mode: 'onBlur', |
||||
|
criteriaMode: 'all', |
||||
|
defaultValues: { |
||||
|
full_name: currentUser?.full_name, |
||||
|
email: currentUser?.email, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
const toggleEditMode = () => { |
const toggleEditMode = () => { |
||||
setEditMode(!editMode); |
setEditMode(!editMode) |
||||
}; |
} |
||||
|
|
||||
const updateInfo = async (data: UserUpdateMe) => { |
const updateInfo = async (data: UserUpdateMe) => { |
||||
await UsersService.updateUserMe({ requestBody: data }) |
await UsersService.updateUserMe({ requestBody: data }) |
||||
} |
} |
||||
|
|
||||
const mutation = useMutation(updateInfo, { |
const mutation = useMutation(updateInfo, { |
||||
onSuccess: () => { |
onSuccess: () => { |
||||
showToast('Success!', 'User updated successfully.', 'success'); |
showToast('Success!', 'User updated successfully.', 'success') |
||||
}, |
}, |
||||
onError: (err: ApiError) => { |
onError: (err: ApiError) => { |
||||
const errDetail = err.body.detail; |
const errDetail = err.body.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
}, |
}, |
||||
onSettled: () => { |
onSettled: () => { |
||||
queryClient.invalidateQueries('users'); |
queryClient.invalidateQueries('users') |
||||
queryClient.invalidateQueries('currentUser'); |
queryClient.invalidateQueries('currentUser') |
||||
} |
}, |
||||
}); |
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<UserUpdateMe> = async (data) => { |
const onSubmit: SubmitHandler<UserUpdateMe> = async (data) => { |
||||
mutation.mutate(data) |
mutation.mutate(data) |
||||
} |
} |
||||
|
|
||||
const onCancel = () => { |
const onCancel = () => { |
||||
reset(); |
reset() |
||||
toggleEditMode(); |
toggleEditMode() |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Container maxW='full' as='form' onSubmit={handleSubmit(onSubmit)}> |
<Container maxW="full" as="form" onSubmit={handleSubmit(onSubmit)}> |
||||
<Heading size='sm' py={4}> |
<Heading size="sm" py={4}> |
||||
User Information |
User Information |
||||
</Heading> |
</Heading> |
||||
<Box w={{ 'sm': 'full', 'md': '50%' }}> |
<Box w={{ sm: 'full', md: '50%' }}> |
||||
<FormControl> |
<FormControl> |
||||
<FormLabel color={color} htmlFor='name'>Full name</FormLabel> |
<FormLabel color={color} htmlFor="name"> |
||||
{ |
Full name |
||||
editMode ? |
</FormLabel> |
||||
<Input id='name' {...register('full_name', { maxLength: 30 })} type='text' size='md' /> : |
{editMode ? ( |
||||
<Text size='md' py={2}> |
<Input |
||||
{currentUser?.full_name || 'N/A'} |
id="name" |
||||
</Text> |
{...register('full_name', { maxLength: 30 })} |
||||
} |
type="text" |
||||
</FormControl> |
size="md" |
||||
<FormControl mt={4} isInvalid={!!errors.email}> |
/> |
||||
<FormLabel color={color} htmlFor='email'>Email</FormLabel> |
) : ( |
||||
{ |
<Text size="md" py={2}> |
||||
editMode ? |
{currentUser?.full_name || 'N/A'} |
||||
<Input id='email' {...register('email', { required: 'Email is required', pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: 'Invalid email address' } })} type='text' size='md' /> : |
</Text> |
||||
<Text size='md' py={2}> |
)} |
||||
{currentUser!.email} |
</FormControl> |
||||
</Text> |
<FormControl mt={4} isInvalid={!!errors.email}> |
||||
} |
<FormLabel color={color} htmlFor="email"> |
||||
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>} |
Email |
||||
</FormControl> |
</FormLabel> |
||||
<Flex mt={4} gap={3}> |
{editMode ? ( |
||||
<Button |
<Input |
||||
bg='ui.main' |
id="email" |
||||
color='white' |
{...register('email', { |
||||
_hover={{ opacity: 0.8 }} |
required: 'Email is required', |
||||
onClick={toggleEditMode} |
pattern: { |
||||
type={editMode ? 'button' : 'submit'} |
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, |
||||
isLoading={editMode ? isSubmitting : false} |
message: 'Invalid email address', |
||||
isDisabled={editMode ? !isDirty : false} |
}, |
||||
> |
})} |
||||
{editMode ? 'Save' : 'Edit'} |
type="text" |
||||
</Button> |
size="md" |
||||
{editMode && |
/> |
||||
<Button onClick={onCancel} isDisabled={isSubmitting}> |
) : ( |
||||
Cancel |
<Text size="md" py={2}> |
||||
</Button>} |
{currentUser!.email} |
||||
</Flex> |
</Text> |
||||
</Box> |
)} |
||||
</ Container> |
{errors.email && ( |
||||
</> |
<FormErrorMessage>{errors.email.message}</FormErrorMessage> |
||||
); |
)} |
||||
|
</FormControl> |
||||
|
<Flex mt={4} gap={3}> |
||||
|
<Button |
||||
|
bg="ui.main" |
||||
|
color="white" |
||||
|
_hover={{ opacity: 0.8 }} |
||||
|
onClick={toggleEditMode} |
||||
|
type={editMode ? 'button' : 'submit'} |
||||
|
isLoading={editMode ? isSubmitting : false} |
||||
|
isDisabled={editMode ? !isDirty : false} |
||||
|
> |
||||
|
{editMode ? 'Save' : 'Edit'} |
||||
|
</Button> |
||||
|
{editMode && ( |
||||
|
<Button onClick={onCancel} isDisabled={isSubmitting}> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
)} |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
</Container> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default UserInformation; |
export default UserInformation |
||||
|
@ -1,33 +1,42 @@ |
|||||
import { useQuery } from 'react-query'; |
import { useQuery } from 'react-query' |
||||
import { useNavigate } from '@tanstack/react-router'; |
import { useNavigate } from '@tanstack/react-router' |
||||
|
|
||||
import { Body_login_login_access_token as AccessToken, LoginService, UserOut, UsersService } from '../client'; |
import { |
||||
|
Body_login_login_access_token as AccessToken, |
||||
|
LoginService, |
||||
|
UserOut, |
||||
|
UsersService, |
||||
|
} from '../client' |
||||
|
|
||||
const isLoggedIn = () => { |
const isLoggedIn = () => { |
||||
return localStorage.getItem('access_token') !== null; |
return localStorage.getItem('access_token') !== null |
||||
}; |
} |
||||
|
|
||||
const useAuth = () => { |
const useAuth = () => { |
||||
const navigate = useNavigate(); |
const navigate = useNavigate() |
||||
const { data: user, isLoading } = useQuery<UserOut | null, Error>('currentUser', UsersService.readUserMe, { |
const { data: user, isLoading } = useQuery<UserOut | null, Error>( |
||||
enabled: isLoggedIn(), |
'currentUser', |
||||
}); |
UsersService.readUserMe, |
||||
|
{ |
||||
|
enabled: isLoggedIn(), |
||||
|
}, |
||||
|
) |
||||
|
|
||||
const login = async (data: AccessToken) => { |
const login = async (data: AccessToken) => { |
||||
const response = await LoginService.loginAccessToken({ |
const response = await LoginService.loginAccessToken({ |
||||
formData: data, |
formData: data, |
||||
}); |
}) |
||||
localStorage.setItem('access_token', response.access_token); |
localStorage.setItem('access_token', response.access_token) |
||||
navigate({ to: '/' }); |
navigate({ to: '/' }) |
||||
}; |
} |
||||
|
|
||||
const logout = () => { |
const logout = () => { |
||||
localStorage.removeItem('access_token'); |
localStorage.removeItem('access_token') |
||||
navigate({ to: '/login' }); |
navigate({ to: '/login' }) |
||||
}; |
} |
||||
|
|
||||
return { login, logout, user, isLoading }; |
return { login, logout, user, isLoading } |
||||
} |
} |
||||
|
|
||||
export { isLoggedIn }; |
export { isLoggedIn } |
||||
export default useAuth; |
export default useAuth |
||||
|
@ -1,21 +1,23 @@ |
|||||
import { useCallback } from 'react'; |
import { useCallback } from 'react' |
||||
|
import { useToast } from '@chakra-ui/react' |
||||
import { useToast } from '@chakra-ui/react'; |
|
||||
|
|
||||
const useCustomToast = () => { |
const useCustomToast = () => { |
||||
const toast = useToast(); |
const toast = useToast() |
||||
|
|
||||
const showToast = useCallback((title: string, description: string, status: 'success' | 'error') => { |
const showToast = useCallback( |
||||
toast({ |
(title: string, description: string, status: 'success' | 'error') => { |
||||
title, |
toast({ |
||||
description, |
title, |
||||
status, |
description, |
||||
isClosable: true, |
status, |
||||
position: 'bottom-right' |
isClosable: true, |
||||
}); |
position: 'bottom-right', |
||||
}, [toast]); |
}) |
||||
|
}, |
||||
|
[toast], |
||||
|
) |
||||
|
|
||||
return showToast; |
return showToast |
||||
}; |
} |
||||
|
|
||||
export default useCustomToast; |
export default useCustomToast |
||||
|
@ -1,13 +1,14 @@ |
|||||
import { createRootRoute, Outlet } from '@tanstack/react-router' |
import { createRootRoute, Outlet } from '@tanstack/react-router' |
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools' |
import { TanStackRouterDevtools } from '@tanstack/router-devtools' |
||||
|
|
||||
import NotFound from '../components/Common/NotFound' |
import NotFound from '../components/Common/NotFound' |
||||
|
|
||||
export const Route = createRootRoute({ |
export const Route = createRootRoute({ |
||||
component: () => ( |
component: () => ( |
||||
<> |
<> |
||||
<Outlet /> |
<Outlet /> |
||||
<TanStackRouterDevtools /> |
<TanStackRouterDevtools /> |
||||
</> |
</> |
||||
), |
), |
||||
notFoundComponent: () => <NotFound />, |
notFoundComponent: () => <NotFound />, |
||||
}) |
}) |
||||
|
@ -1,38 +1,37 @@ |
|||||
import { Flex, Spinner } from '@chakra-ui/react'; |
import { Flex, Spinner } from '@chakra-ui/react' |
||||
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; |
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router' |
||||
|
|
||||
import Sidebar from '../components/Common/Sidebar'; |
|
||||
import UserMenu from '../components/Common/UserMenu'; |
|
||||
import useAuth, { isLoggedIn } from '../hooks/useAuth'; |
|
||||
|
|
||||
|
import Sidebar from '../components/Common/Sidebar' |
||||
|
import UserMenu from '../components/Common/UserMenu' |
||||
|
import useAuth, { isLoggedIn } from '../hooks/useAuth' |
||||
|
|
||||
export const Route = createFileRoute('/_layout')({ |
export const Route = createFileRoute('/_layout')({ |
||||
component: Layout, |
component: Layout, |
||||
beforeLoad: async () => { |
beforeLoad: async () => { |
||||
if (!isLoggedIn()) { |
if (!isLoggedIn()) { |
||||
throw redirect({ |
throw redirect({ |
||||
to: '/login', |
to: '/login', |
||||
}) |
}) |
||||
} |
|
||||
} |
} |
||||
|
}, |
||||
}) |
}) |
||||
|
|
||||
function Layout() { |
function Layout() { |
||||
const { isLoading } = useAuth(); |
const { isLoading } = useAuth() |
||||
|
|
||||
return ( |
return ( |
||||
<Flex maxW='large' h='auto' position='relative'> |
<Flex maxW="large" h="auto" position="relative"> |
||||
<Sidebar /> |
<Sidebar /> |
||||
{isLoading ? ( |
{isLoading ? ( |
||||
<Flex justify='center' align='center' height='100vh' width='full'> |
<Flex justify="center" align="center" height="100vh" width="full"> |
||||
<Spinner size='xl' color='ui.main' /> |
<Spinner size="xl" color="ui.main" /> |
||||
</Flex> |
|
||||
) : ( |
|
||||
<Outlet /> |
|
||||
)} |
|
||||
<UserMenu /> |
|
||||
</Flex> |
</Flex> |
||||
); |
) : ( |
||||
}; |
<Outlet /> |
||||
|
)} |
||||
|
<UserMenu /> |
||||
|
</Flex> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default Layout; |
export default Layout |
||||
|
@ -1,82 +1,117 @@ |
|||||
import { Badge, Box, Container, Flex, Heading, Spinner, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; |
import { |
||||
import { createFileRoute } from '@tanstack/react-router'; |
Badge, |
||||
import { useQuery, useQueryClient } from 'react-query'; |
Box, |
||||
|
Container, |
||||
|
Flex, |
||||
|
Heading, |
||||
|
Spinner, |
||||
|
Table, |
||||
|
TableContainer, |
||||
|
Tbody, |
||||
|
Td, |
||||
|
Th, |
||||
|
Thead, |
||||
|
Tr, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { createFileRoute } from '@tanstack/react-router' |
||||
|
import { useQuery, useQueryClient } from 'react-query' |
||||
|
|
||||
import { ApiError, UserOut, UsersService } from '../../client'; |
import { ApiError, UserOut, UsersService } from '../../client' |
||||
import ActionsMenu from '../../components/Common/ActionsMenu'; |
import ActionsMenu from '../../components/Common/ActionsMenu' |
||||
import Navbar from '../../components/Common/Navbar'; |
import Navbar from '../../components/Common/Navbar' |
||||
import useCustomToast from '../../hooks/useCustomToast'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
|
|
||||
export const Route = createFileRoute('/_layout/admin')({ |
export const Route = createFileRoute('/_layout/admin')({ |
||||
component: Admin, |
component: Admin, |
||||
}) |
}) |
||||
|
|
||||
function Admin() { |
function Admin() { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
const currentUser = queryClient.getQueryData<UserOut>('currentUser') |
||||
const { data: users, isLoading, isError, error } = useQuery('users', () => UsersService.readUsers({})) |
const { |
||||
|
data: users, |
||||
|
isLoading, |
||||
|
isError, |
||||
|
error, |
||||
|
} = useQuery('users', () => UsersService.readUsers({})) |
||||
|
|
||||
if (isError) { |
if (isError) { |
||||
const errDetail = (error as ApiError).body?.detail; |
const errDetail = (error as ApiError).body?.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
{isLoading ? ( |
{isLoading ? ( |
||||
// TODO: Add skeleton
|
// TODO: Add skeleton
|
||||
<Flex justify='center' align='center' height='100vh' width='full'> |
<Flex justify="center" align="center" height="100vh" width="full"> |
||||
<Spinner size='xl' color='ui.main' /> |
<Spinner size="xl" color="ui.main" /> |
||||
</Flex> |
</Flex> |
||||
) : ( |
) : ( |
||||
users && |
users && ( |
||||
<Container maxW='full'> |
<Container maxW="full"> |
||||
<Heading size='lg' textAlign={{ base: 'center', md: 'left' }} pt={12}> |
<Heading |
||||
User Management |
size="lg" |
||||
</Heading> |
textAlign={{ base: 'center', md: 'left' }} |
||||
<Navbar type={'User'} /> |
pt={12} |
||||
<TableContainer> |
> |
||||
<Table fontSize='md' size={{ base: 'sm', md: 'md' }}> |
User Management |
||||
<Thead> |
</Heading> |
||||
<Tr> |
<Navbar type={'User'} /> |
||||
<Th>Full name</Th> |
<TableContainer> |
||||
<Th>Email</Th> |
<Table fontSize="md" size={{ base: 'sm', md: 'md' }}> |
||||
<Th>Role</Th> |
<Thead> |
||||
<Th>Status</Th> |
<Tr> |
||||
<Th>Actions</Th> |
<Th>Full name</Th> |
||||
</Tr> |
<Th>Email</Th> |
||||
</Thead> |
<Th>Role</Th> |
||||
<Tbody> |
<Th>Status</Th> |
||||
{users.data.map((user) => ( |
<Th>Actions</Th> |
||||
<Tr key={user.id}> |
</Tr> |
||||
<Td color={!user.full_name ? 'gray.600' : 'inherit'}>{user.full_name || 'N/A'}{currentUser?.id === user.id && <Badge ml='1' colorScheme='teal'>You</Badge>}</Td> |
</Thead> |
||||
<Td>{user.email}</Td> |
<Tbody> |
||||
<Td>{user.is_superuser ? 'Superuser' : 'User'}</Td> |
{users.data.map((user) => ( |
||||
<Td> |
<Tr key={user.id}> |
||||
<Flex gap={2}> |
<Td color={!user.full_name ? 'gray.600' : 'inherit'}> |
||||
<Box |
{user.full_name || 'N/A'} |
||||
w='2' |
{currentUser?.id === user.id && ( |
||||
h='2' |
<Badge ml="1" colorScheme="teal"> |
||||
borderRadius='50%' |
You |
||||
bg={user.is_active ? 'ui.success' : 'ui.danger'} |
</Badge> |
||||
alignSelf='center' |
)} |
||||
/> |
</Td> |
||||
{user.is_active ? 'Active' : 'Inactive'} |
<Td>{user.email}</Td> |
||||
</Flex> |
<Td>{user.is_superuser ? 'Superuser' : 'User'}</Td> |
||||
</Td> |
<Td> |
||||
<Td> |
<Flex gap={2}> |
||||
<ActionsMenu type='User' value={user} disabled={currentUser?.id === user.id ? true : false} /> |
<Box |
||||
</Td> |
w="2" |
||||
</Tr> |
h="2" |
||||
))} |
borderRadius="50%" |
||||
</Tbody> |
bg={user.is_active ? 'ui.success' : 'ui.danger'} |
||||
</Table> |
alignSelf="center" |
||||
</TableContainer> |
/> |
||||
</Container> |
{user.is_active ? 'Active' : 'Inactive'} |
||||
)} |
</Flex> |
||||
</> |
</Td> |
||||
) |
<Td> |
||||
|
<ActionsMenu |
||||
|
type="User" |
||||
|
value={user} |
||||
|
disabled={currentUser?.id === user.id ? true : false} |
||||
|
/> |
||||
|
</Td> |
||||
|
</Tr> |
||||
|
))} |
||||
|
</Tbody> |
||||
|
</Table> |
||||
|
</TableContainer> |
||||
|
</Container> |
||||
|
) |
||||
|
)} |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default Admin; |
export default Admin |
||||
|
@ -1,27 +1,28 @@ |
|||||
|
import { Container, Text } from '@chakra-ui/react' |
||||
|
import { useQueryClient } from 'react-query' |
||||
|
import { createFileRoute } from '@tanstack/react-router' |
||||
|
|
||||
import { Container, Text } from '@chakra-ui/react'; |
import { UserOut } from '../../client' |
||||
import { useQueryClient } from 'react-query'; |
|
||||
import { createFileRoute } from '@tanstack/react-router'; |
|
||||
|
|
||||
import { UserOut } from '../../client'; |
|
||||
|
|
||||
export const Route = createFileRoute('/_layout/')({ |
export const Route = createFileRoute('/_layout/')({ |
||||
component: Dashboard, |
component: Dashboard, |
||||
}) |
}) |
||||
|
|
||||
function Dashboard() { |
function Dashboard() { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
|
|
||||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
const currentUser = queryClient.getQueryData<UserOut>('currentUser') |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Container maxW='full' pt={12}> |
<Container maxW="full" pt={12}> |
||||
<Text fontSize='2xl'>Hi, {currentUser?.full_name || currentUser?.email} 👋🏼</Text> |
<Text fontSize="2xl"> |
||||
<Text>Welcome back, nice to see you again!</Text> |
Hi, {currentUser?.full_name || currentUser?.email} 👋🏼 |
||||
</Container> |
</Text> |
||||
</> |
<Text>Welcome back, nice to see you again!</Text> |
||||
) |
</Container> |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default Dashboard; |
export default Dashboard |
||||
|
@ -1,67 +1,91 @@ |
|||||
import { Container, Flex, Heading, Spinner, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; |
import { |
||||
import { createFileRoute } from '@tanstack/react-router'; |
Container, |
||||
import { useQuery } from 'react-query'; |
Flex, |
||||
|
Heading, |
||||
|
Spinner, |
||||
|
Table, |
||||
|
TableContainer, |
||||
|
Tbody, |
||||
|
Td, |
||||
|
Th, |
||||
|
Thead, |
||||
|
Tr, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { createFileRoute } from '@tanstack/react-router' |
||||
|
import { useQuery } from 'react-query' |
||||
|
|
||||
import { ApiError, ItemsService } from '../../client'; |
import { ApiError, ItemsService } from '../../client' |
||||
import ActionsMenu from '../../components/Common/ActionsMenu'; |
import ActionsMenu from '../../components/Common/ActionsMenu' |
||||
import Navbar from '../../components/Common/Navbar'; |
import Navbar from '../../components/Common/Navbar' |
||||
import useCustomToast from '../../hooks/useCustomToast'; |
import useCustomToast from '../../hooks/useCustomToast' |
||||
|
|
||||
export const Route = createFileRoute('/_layout/items')({ |
export const Route = createFileRoute('/_layout/items')({ |
||||
component: Items, |
component: Items, |
||||
}) |
}) |
||||
|
|
||||
function Items() { |
function Items() { |
||||
const showToast = useCustomToast(); |
const showToast = useCustomToast() |
||||
const { data: items, isLoading, isError, error } = useQuery('items', () => ItemsService.readItems({})) |
const { |
||||
|
data: items, |
||||
|
isLoading, |
||||
|
isError, |
||||
|
error, |
||||
|
} = useQuery('items', () => ItemsService.readItems({})) |
||||
|
|
||||
if (isError) { |
if (isError) { |
||||
const errDetail = (error as ApiError).body?.detail; |
const errDetail = (error as ApiError).body?.detail |
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
} |
} |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
{isLoading ? ( |
{isLoading ? ( |
||||
// TODO: Add skeleton
|
// TODO: Add skeleton
|
||||
<Flex justify='center' align='center' height='100vh' width='full'> |
<Flex justify="center" align="center" height="100vh" width="full"> |
||||
<Spinner size='xl' color='ui.main' /> |
<Spinner size="xl" color="ui.main" /> |
||||
</Flex> |
</Flex> |
||||
) : ( |
) : ( |
||||
items && |
items && ( |
||||
<Container maxW='full'> |
<Container maxW="full"> |
||||
<Heading size='lg' textAlign={{ base: 'center', md: 'left' }} pt={12}> |
<Heading |
||||
Items Management |
size="lg" |
||||
</Heading> |
textAlign={{ base: 'center', md: 'left' }} |
||||
<Navbar type={'Item'} /> |
pt={12} |
||||
<TableContainer> |
> |
||||
<Table size={{ base: 'sm', md: 'md' }}> |
Items Management |
||||
<Thead> |
</Heading> |
||||
<Tr> |
<Navbar type={'Item'} /> |
||||
<Th>ID</Th> |
<TableContainer> |
||||
<Th>Title</Th> |
<Table size={{ base: 'sm', md: 'md' }}> |
||||
<Th>Description</Th> |
<Thead> |
||||
<Th>Actions</Th> |
<Tr> |
||||
</Tr> |
<Th>ID</Th> |
||||
</Thead> |
<Th>Title</Th> |
||||
<Tbody> |
<Th>Description</Th> |
||||
{items.data.map((item) => ( |
<Th>Actions</Th> |
||||
<Tr key={item.id}> |
</Tr> |
||||
<Td>{item.id}</Td> |
</Thead> |
||||
<Td>{item.title}</Td> |
<Tbody> |
||||
<Td color={!item.description ? 'gray.600' : 'inherit'}>{item.description || 'N/A'}</Td> |
{items.data.map((item) => ( |
||||
<Td> |
<Tr key={item.id}> |
||||
<ActionsMenu type={'Item'} value={item} /> |
<Td>{item.id}</Td> |
||||
</Td> |
<Td>{item.title}</Td> |
||||
</Tr> |
<Td color={!item.description ? 'gray.600' : 'inherit'}> |
||||
))} |
{item.description || 'N/A'} |
||||
</Tbody> |
</Td> |
||||
</Table> |
<Td> |
||||
</TableContainer> |
<ActionsMenu type={'Item'} value={item} /> |
||||
</Container> |
</Td> |
||||
)} |
</Tr> |
||||
</> |
))} |
||||
) |
</Tbody> |
||||
|
</Table> |
||||
|
</TableContainer> |
||||
|
</Container> |
||||
|
) |
||||
|
)} |
||||
|
</> |
||||
|
) |
||||
} |
} |
||||
|
|
||||
export default Items; |
export default Items |
||||
|
@ -1,50 +1,60 @@ |
|||||
import { Container, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; |
import { |
||||
import { createFileRoute } from '@tanstack/react-router'; |
Container, |
||||
import { useQueryClient } from 'react-query'; |
Heading, |
||||
|
Tab, |
||||
|
TabList, |
||||
|
TabPanel, |
||||
|
TabPanels, |
||||
|
Tabs, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { createFileRoute } from '@tanstack/react-router' |
||||
|
import { useQueryClient } from 'react-query' |
||||
|
|
||||
import { UserOut } from '../../client'; |
import { UserOut } from '../../client' |
||||
import Appearance from '../../components/UserSettings/Appearance'; |
import Appearance from '../../components/UserSettings/Appearance' |
||||
import ChangePassword from '../../components/UserSettings/ChangePassword'; |
import ChangePassword from '../../components/UserSettings/ChangePassword' |
||||
import DeleteAccount from '../../components/UserSettings/DeleteAccount'; |
import DeleteAccount from '../../components/UserSettings/DeleteAccount' |
||||
import UserInformation from '../../components/UserSettings/UserInformation'; |
import UserInformation from '../../components/UserSettings/UserInformation' |
||||
|
|
||||
const tabsConfig = [ |
const tabsConfig = [ |
||||
{ title: 'My profile', component: UserInformation }, |
{ title: 'My profile', component: UserInformation }, |
||||
{ title: 'Password', component: ChangePassword }, |
{ title: 'Password', component: ChangePassword }, |
||||
{ title: 'Appearance', component: Appearance }, |
{ title: 'Appearance', component: Appearance }, |
||||
{ title: 'Danger zone', component: DeleteAccount }, |
{ title: 'Danger zone', component: DeleteAccount }, |
||||
]; |
] |
||||
|
|
||||
export const Route = createFileRoute('/_layout/settings')({ |
export const Route = createFileRoute('/_layout/settings')({ |
||||
component: UserSettings, |
component: UserSettings, |
||||
}) |
}) |
||||
|
|
||||
function UserSettings() { |
function UserSettings() { |
||||
const queryClient = useQueryClient(); |
const queryClient = useQueryClient() |
||||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
const currentUser = queryClient.getQueryData<UserOut>('currentUser') |
||||
const finalTabs = currentUser?.is_superuser ? tabsConfig.slice(0, 3) : tabsConfig; |
const finalTabs = currentUser?.is_superuser |
||||
|
? tabsConfig.slice(0, 3) |
||||
|
: tabsConfig |
||||
|
|
||||
return ( |
return ( |
||||
<Container maxW='full'> |
<Container maxW="full"> |
||||
<Heading size='lg' textAlign={{ base: 'center', md: 'left' }} py={12}> |
<Heading size="lg" textAlign={{ base: 'center', md: 'left' }} py={12}> |
||||
User Settings |
User Settings |
||||
</Heading> |
</Heading> |
||||
<Tabs variant='enclosed'> |
<Tabs variant="enclosed"> |
||||
<TabList> |
<TabList> |
||||
{finalTabs.map((tab, index) => ( |
{finalTabs.map((tab, index) => ( |
||||
<Tab key={index}>{tab.title}</Tab> |
<Tab key={index}>{tab.title}</Tab> |
||||
))} |
))} |
||||
</TabList> |
</TabList> |
||||
<TabPanels> |
<TabPanels> |
||||
{finalTabs.map((tab, index) => ( |
{finalTabs.map((tab, index) => ( |
||||
<TabPanel key={index}> |
<TabPanel key={index}> |
||||
<tab.component /> |
<tab.component /> |
||||
</TabPanel> |
</TabPanel> |
||||
))} |
))} |
||||
</TabPanels> |
</TabPanels> |
||||
</Tabs> |
</Tabs> |
||||
</Container> |
</Container> |
||||
); |
) |
||||
} |
} |
||||
|
|
||||
export default UserSettings; |
export default UserSettings |
||||
|
@ -1,95 +1,134 @@ |
|||||
|
import { |
||||
|
Button, |
||||
|
Container, |
||||
|
FormControl, |
||||
|
FormErrorMessage, |
||||
|
FormLabel, |
||||
|
Heading, |
||||
|
Input, |
||||
|
Text, |
||||
|
} from '@chakra-ui/react' |
||||
|
import { createFileRoute, redirect } from '@tanstack/react-router' |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form' |
||||
|
import { useMutation } from 'react-query' |
||||
|
|
||||
import { Button, Container, FormControl, FormErrorMessage, FormLabel, Heading, Input, Text } from '@chakra-ui/react'; |
import { ApiError, LoginService, NewPassword } from '../client' |
||||
import { createFileRoute, redirect } from '@tanstack/react-router'; |
import { isLoggedIn } from '../hooks/useAuth' |
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
import useCustomToast from '../hooks/useCustomToast' |
||||
import { useMutation } from 'react-query'; |
|
||||
|
|
||||
import { ApiError, LoginService, NewPassword } from '../client'; |
|
||||
import { isLoggedIn } from '../hooks/useAuth'; |
|
||||
import useCustomToast from '../hooks/useCustomToast'; |
|
||||
|
|
||||
interface NewPasswordForm extends NewPassword { |
interface NewPasswordForm extends NewPassword { |
||||
confirm_password: string; |
confirm_password: string |
||||
} |
} |
||||
|
|
||||
export const Route = createFileRoute('/reset-password')({ |
export const Route = createFileRoute('/reset-password')({ |
||||
component: ResetPassword, |
component: ResetPassword, |
||||
beforeLoad: async () => { |
beforeLoad: async () => { |
||||
if (isLoggedIn()) { |
if (isLoggedIn()) { |
||||
throw redirect({ |
throw redirect({ |
||||
to: '/', |
to: '/', |
||||
}) |
}) |
||||
} |
|
||||
} |
} |
||||
|
}, |
||||
}) |
}) |
||||
|
|
||||
function ResetPassword() { |
function ResetPassword() { |
||||
const { register, handleSubmit, getValues, formState: { errors } } = useForm<NewPasswordForm>({ |
const { |
||||
mode: 'onBlur', |
register, |
||||
criteriaMode: 'all', |
handleSubmit, |
||||
defaultValues: { |
getValues, |
||||
new_password: '', |
formState: { errors }, |
||||
} |
} = useForm<NewPasswordForm>({ |
||||
}); |
mode: 'onBlur', |
||||
const showToast = useCustomToast(); |
criteriaMode: 'all', |
||||
|
defaultValues: { |
||||
const resetPassword = async (data: NewPassword) => { |
new_password: '', |
||||
const token = new URLSearchParams(window.location.search).get('token'); |
}, |
||||
await LoginService.resetPassword({ |
}) |
||||
requestBody: { new_password: data.new_password, token: token! } |
const showToast = useCustomToast() |
||||
}); |
|
||||
} |
|
||||
|
|
||||
const mutation = useMutation(resetPassword, { |
const resetPassword = async (data: NewPassword) => { |
||||
onSuccess: () => { |
const token = new URLSearchParams(window.location.search).get('token') |
||||
showToast('Success!', 'Password updated.', 'success'); |
await LoginService.resetPassword({ |
||||
}, |
requestBody: { new_password: data.new_password, token: token! }, |
||||
onError: (err: ApiError) => { |
|
||||
const errDetail = err.body.detail; |
|
||||
showToast('Something went wrong.', `${errDetail}`, 'error'); |
|
||||
} |
|
||||
}) |
}) |
||||
|
} |
||||
|
|
||||
|
const mutation = useMutation(resetPassword, { |
||||
|
onSuccess: () => { |
||||
|
showToast('Success!', 'Password updated.', 'success') |
||||
|
}, |
||||
|
onError: (err: ApiError) => { |
||||
|
const errDetail = err.body.detail |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error') |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
const onSubmit: SubmitHandler<NewPasswordForm> = async (data) => { |
const onSubmit: SubmitHandler<NewPasswordForm> = async (data) => { |
||||
mutation.mutate(data); |
mutation.mutate(data) |
||||
}; |
} |
||||
|
|
||||
return ( |
return ( |
||||
<Container |
<Container |
||||
as='form' |
as="form" |
||||
onSubmit={handleSubmit(onSubmit)} |
onSubmit={handleSubmit(onSubmit)} |
||||
h='100vh' |
h="100vh" |
||||
maxW='sm' |
maxW="sm" |
||||
alignItems='stretch' |
alignItems="stretch" |
||||
justifyContent='center' |
justifyContent="center" |
||||
gap={4} |
gap={4} |
||||
centerContent |
centerContent |
||||
> |
> |
||||
<Heading size='xl' color='ui.main' textAlign='center' mb={2}> |
<Heading size="xl" color="ui.main" textAlign="center" mb={2}> |
||||
Reset Password |
Reset Password |
||||
</Heading> |
</Heading> |
||||
<Text textAlign='center'> |
<Text textAlign="center"> |
||||
Please enter your new password and confirm it to reset your password. |
Please enter your new password and confirm it to reset your password. |
||||
</Text> |
</Text> |
||||
<FormControl mt={4} isInvalid={!!errors.new_password}> |
<FormControl mt={4} isInvalid={!!errors.new_password}> |
||||
<FormLabel htmlFor='password'>Set Password</FormLabel> |
<FormLabel htmlFor="password">Set Password</FormLabel> |
||||
<Input id='password' {...register('new_password', { required: 'Password is required', minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='Password' type='password' /> |
<Input |
||||
{errors.new_password && <FormErrorMessage>{errors.new_password.message}</FormErrorMessage>} |
id="password" |
||||
</FormControl> |
{...register('new_password', { |
||||
<FormControl mt={4} isInvalid={!!errors.confirm_password}> |
required: 'Password is required', |
||||
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel> |
minLength: { |
||||
<Input id='confirm_password' {...register('confirm_password', { |
value: 8, |
||||
required: 'Please confirm your password', |
message: 'Password must be at least 8 characters', |
||||
validate: value => value === getValues().new_password || 'The passwords do not match' |
}, |
||||
})} placeholder='Password' type='password' /> |
})} |
||||
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>} |
placeholder="Password" |
||||
</FormControl> |
type="password" |
||||
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit'> |
/> |
||||
Reset Password |
{errors.new_password && ( |
||||
</Button> |
<FormErrorMessage>{errors.new_password.message}</FormErrorMessage> |
||||
</Container> |
)} |
||||
); |
</FormControl> |
||||
}; |
<FormControl mt={4} isInvalid={!!errors.confirm_password}> |
||||
|
<FormLabel htmlFor="confirm_password">Confirm Password</FormLabel> |
||||
|
<Input |
||||
|
id="confirm_password" |
||||
|
{...register('confirm_password', { |
||||
|
required: 'Please confirm your password', |
||||
|
validate: (value) => |
||||
|
value === getValues().new_password || |
||||
|
'The passwords do not match', |
||||
|
})} |
||||
|
placeholder="Password" |
||||
|
type="password" |
||||
|
/> |
||||
|
{errors.confirm_password && ( |
||||
|
<FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage> |
||||
|
)} |
||||
|
</FormControl> |
||||
|
<Button |
||||
|
bg="ui.main" |
||||
|
color="white" |
||||
|
_hover={{ opacity: 0.8 }} |
||||
|
type="submit" |
||||
|
> |
||||
|
Reset Password |
||||
|
</Button> |
||||
|
</Container> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default ResetPassword; |
export default ResetPassword |
||||
|
@ -1,27 +1,27 @@ |
|||||
import { extendTheme } from '@chakra-ui/react' |
import { extendTheme } from '@chakra-ui/react' |
||||
|
|
||||
const theme = extendTheme({ |
const theme = extendTheme({ |
||||
colors: { |
colors: { |
||||
ui: { |
ui: { |
||||
main: '#009688', |
main: '#009688', |
||||
secondary: '#EDF2F7', |
secondary: '#EDF2F7', |
||||
success: '#48BB78', |
success: '#48BB78', |
||||
danger: '#E53E3E', |
danger: '#E53E3E', |
||||
} |
|
||||
}, |
}, |
||||
components: { |
}, |
||||
Tabs: { |
components: { |
||||
variants: { |
Tabs: { |
||||
enclosed: { |
variants: { |
||||
tab: { |
enclosed: { |
||||
_selected: { |
tab: { |
||||
color: 'ui.main', |
_selected: { |
||||
}, |
color: 'ui.main', |
||||
}, |
|
||||
}, |
|
||||
}, |
}, |
||||
|
}, |
||||
}, |
}, |
||||
|
}, |
||||
}, |
}, |
||||
}); |
}, |
||||
|
}) |
||||
|
|
||||
export default theme; |
export default theme |
||||
|
Loading…
Reference in new issue