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