Browse Source
* Reorganize project directory structure * Allow edit users/items, add useAuth and useCustomToast, password confirmation * Minor improvements for consistency * Add 'Cancel' button to UserInformation in editMode * Refactor UserSettings * Enable user password changes and improve error handling * Enable user information update * Add logout to Sidebar in mobile devices, conditional tabs depending on role and other improvements * Add badges * Remove comment * Appearance tab updates * Change badge color * Reset inputs when clicking on 'Cancel' button * Disable actions menu for Superuser when logged in * Modify Logout and update storespull/13907/head
committed by
GitHub
35 changed files with 802 additions and 621 deletions
@ -0,0 +1,93 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form'; |
||||
|
|
||||
|
import { UserCreate } from '../../client'; |
||||
|
import useCustomToast from '../../hooks/useCustomToast'; |
||||
|
import { useUsersStore } from '../../store/users-store'; |
||||
|
import { ApiError } from '../../client/core/ApiError'; |
||||
|
|
||||
|
interface AddUserProps { |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
} |
||||
|
|
||||
|
interface UserCreateForm extends UserCreate { |
||||
|
confirmPassword: string; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => { |
||||
|
const showToast = useCustomToast(); |
||||
|
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserCreateForm>(); |
||||
|
const { addUser } = useUsersStore(); |
||||
|
|
||||
|
const onSubmit: SubmitHandler<UserCreateForm> = async (data) => { |
||||
|
if (data.password === data.confirmPassword) { |
||||
|
try { |
||||
|
await addUser(data); |
||||
|
showToast('Success!', 'User created successfully.', 'success'); |
||||
|
reset(); |
||||
|
onClose(); |
||||
|
} catch (err) { |
||||
|
const errDetail = (err as ApiError).body.detail; |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error'); |
||||
|
} |
||||
|
} else { |
||||
|
// TODO: Complete when form validation is implemented
|
||||
|
console.log("Passwords don't match") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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> |
||||
|
<FormLabel htmlFor='email'>Email</FormLabel> |
||||
|
<Input id='email' {...register('email')} placeholder='Email' type='email' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='name'>Full name</FormLabel> |
||||
|
<Input id='name' {...register('full_name')} placeholder='Full name' type='text' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='password'>Set Password</FormLabel> |
||||
|
<Input id='password' {...register('password')} placeholder='Password' type='password' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='confirmPassword'>Confirm Password</FormLabel> |
||||
|
<Input id='confirmPassword' {...register('confirmPassword')} placeholder='Password' type='password' /> |
||||
|
</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' type='submit' isLoading={isSubmitting}> |
||||
|
Save |
||||
|
</Button> |
||||
|
<Button onClick={onClose}>Cancel</Button> |
||||
|
</ModalFooter> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default AddUser; |
@ -0,0 +1,100 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form'; |
||||
|
|
||||
|
import { ApiError, UserUpdate } from '../../client'; |
||||
|
import useCustomToast from '../../hooks/useCustomToast'; |
||||
|
import { useUsersStore } from '../../store/users-store'; |
||||
|
|
||||
|
interface EditUserProps { |
||||
|
user_id: number; |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
} |
||||
|
|
||||
|
interface UserUpdateForm extends UserUpdate { |
||||
|
confirm_password: string; |
||||
|
} |
||||
|
|
||||
|
const EditUser: React.FC<EditUserProps> = ({ user_id, isOpen, onClose }) => { |
||||
|
const showToast = useCustomToast(); |
||||
|
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserUpdateForm>(); |
||||
|
const { editUser, users } = useUsersStore(); |
||||
|
|
||||
|
const currentUser = users.find((user) => user.id === user_id); |
||||
|
|
||||
|
const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => { |
||||
|
if (data.password === data.confirm_password) { |
||||
|
try { |
||||
|
await editUser(user_id, data); |
||||
|
showToast('Success!', 'User updated successfully.', 'success'); |
||||
|
reset(); |
||||
|
onClose(); |
||||
|
} catch (err) { |
||||
|
const errDetail = (err as ApiError).body.detail; |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error'); |
||||
|
} |
||||
|
} else { |
||||
|
// TODO: Complete when form validation is implemented
|
||||
|
console.log("Passwords don't match") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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> |
||||
|
<FormLabel htmlFor='email'>Email</FormLabel> |
||||
|
<Input id="email" {...register('email')} defaultValue={currentUser?.email} type='email' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='name'>Full name</FormLabel> |
||||
|
<Input id="name" {...register('full_name')} defaultValue={currentUser?.full_name} type='text' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='password'>Password</FormLabel> |
||||
|
<Input id="password" {...register('password')} placeholder='••••••••' type='password' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='confirmPassword'>Confirmation Password</FormLabel> |
||||
|
<Input id='confirmPassword' {...register('confirm_password')} placeholder='••••••••' type='password' /> |
||||
|
</FormControl> |
||||
|
<Flex> |
||||
|
<FormControl mt={4}> |
||||
|
<Checkbox {...register('is_superuser')} defaultChecked={currentUser?.is_superuser} colorScheme='teal'>Is superuser?</Checkbox> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<Checkbox {...register('is_active')} defaultChecked={currentUser?.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={onCancel}>Cancel</Button> |
||||
|
</ModalFooter> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default EditUser; |
@ -0,0 +1,71 @@ |
|||||
|
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 { useNavigate } from 'react-router-dom'; |
||||
|
|
||||
|
import Logo from '../../assets/images/fastapi-logo.svg'; |
||||
|
import useAuth from '../../hooks/useAuth'; |
||||
|
import { useUserStore } from '../../store/user-store'; |
||||
|
import SidebarItems from './SidebarItems'; |
||||
|
|
||||
|
const Sidebar: React.FC = () => { |
||||
|
const bgColor = useColorModeValue('white', '#1a202c'); |
||||
|
const textColor = useColorModeValue('gray', 'white'); |
||||
|
const secBgColor = useColorModeValue('ui.secondary', '#252d3d'); |
||||
|
const { isOpen, onOpen, onClose } = useDisclosure(); |
||||
|
const { user } = useUserStore(); |
||||
|
const { logout } = useAuth(); |
||||
|
const navigate = useNavigate(); |
||||
|
|
||||
|
const handleLogout = async () => { |
||||
|
logout() |
||||
|
navigate('/login'); |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
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> |
||||
|
{ |
||||
|
user?.email && |
||||
|
<Text color={textColor} noOfLines={2} fontSize='sm' p={2}>Logged in as: {user.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> |
||||
|
{ |
||||
|
user?.email && |
||||
|
<Text color={textColor} noOfLines={2} fontSize='sm' p={2} maxW='180px'>Logged in as: {user.email}</Text> |
||||
|
} |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default Sidebar; |
@ -1,38 +1,38 @@ |
|||||
import React from 'react'; |
import React from 'react'; |
||||
|
|
||||
import { IconButton } from '@chakra-ui/button'; |
import { Box, IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react'; |
||||
import { Box } from '@chakra-ui/layout'; |
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; |
|
||||
import { FaUserAstronaut } from 'react-icons/fa'; |
import { FaUserAstronaut } from 'react-icons/fa'; |
||||
import { FiLogOut, FiUser } from 'react-icons/fi'; |
import { FiLogOut, FiUser } from 'react-icons/fi'; |
||||
import { useNavigate } from 'react-router'; |
import { Link, useNavigate } from 'react-router-dom'; |
||||
import { Link } from 'react-router-dom'; |
|
||||
|
import useAuth from '../../hooks/useAuth'; |
||||
|
|
||||
const UserMenu: React.FC = () => { |
const UserMenu: React.FC = () => { |
||||
const navigate = useNavigate(); |
const navigate = useNavigate(); |
||||
|
const { logout } = useAuth(); |
||||
|
|
||||
const handleLogout = async () => { |
const handleLogout = async () => { |
||||
localStorage.removeItem("access_token"); |
logout() |
||||
navigate("/login"); |
navigate('/login'); |
||||
// TODO: reset all Zustand states
|
|
||||
}; |
}; |
||||
|
|
||||
return ( |
return ( |
||||
<> |
<> |
||||
<Box position="fixed" top={4} right={4}> |
{/* Desktop */} |
||||
|
<Box display={{ base: 'none', md: 'block' }} position='fixed' top={4} right={4}> |
||||
<Menu> |
<Menu> |
||||
<MenuButton |
<MenuButton |
||||
as={IconButton} |
as={IconButton} |
||||
aria-label='Options' |
aria-label='Options' |
||||
icon={<FaUserAstronaut color="white" fontSize="18px" />} |
icon={<FaUserAstronaut color='white' fontSize='18px' />} |
||||
bg="ui.main" |
bg='ui.main' |
||||
isRound |
isRound |
||||
/> |
/> |
||||
<MenuList> |
<MenuList> |
||||
<MenuItem icon={<FiUser fontSize="18px" />} as={Link} to="settings"> |
<MenuItem icon={<FiUser fontSize='18px' />} as={Link} to='settings'> |
||||
My profile |
My profile |
||||
</MenuItem> |
</MenuItem> |
||||
<MenuItem icon={<FiLogOut fontSize="18px" />} onClick={handleLogout} color="ui.danger" fontWeight="bold"> |
<MenuItem icon={<FiLogOut fontSize='18px' />} onClick={handleLogout} color='ui.danger' fontWeight='bold'> |
||||
Log out |
Log out |
||||
</MenuItem> |
</MenuItem> |
||||
</MenuList> |
</MenuList> |
@ -0,0 +1,74 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form'; |
||||
|
|
||||
|
import { ApiError, ItemUpdate } from '../../client'; |
||||
|
import useCustomToast from '../../hooks/useCustomToast'; |
||||
|
import { useItemsStore } from '../../store/items-store'; |
||||
|
|
||||
|
interface EditItemProps { |
||||
|
id: number; |
||||
|
isOpen: boolean; |
||||
|
onClose: () => void; |
||||
|
} |
||||
|
|
||||
|
const EditItem: React.FC<EditItemProps> = ({ id, isOpen, onClose }) => { |
||||
|
const showToast = useCustomToast(); |
||||
|
const { register, handleSubmit, reset, formState: { isSubmitting }, } = useForm<ItemUpdate>(); |
||||
|
const { editItem, items } = useItemsStore(); |
||||
|
|
||||
|
const currentItem = items.find((item) => item.id === id); |
||||
|
|
||||
|
const onSubmit: SubmitHandler<ItemUpdate> = async (data) => { |
||||
|
try { |
||||
|
await editItem(id, data); |
||||
|
showToast('Success!', 'Item updated successfully.', 'success'); |
||||
|
reset(); |
||||
|
onClose(); |
||||
|
} catch (err) { |
||||
|
const errDetail = (err as ApiError).body.detail; |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 Item</ModalHeader> |
||||
|
<ModalCloseButton /> |
||||
|
<ModalBody pb={6}> |
||||
|
<FormControl> |
||||
|
<FormLabel htmlFor='title'>Title</FormLabel> |
||||
|
<Input id='title' {...register('title')} defaultValue={currentItem?.title} type='text' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel htmlFor='description'>Description</FormLabel> |
||||
|
<Input id='description' {...register('description')} defaultValue={currentItem?.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={onCancel}>Cancel</Button> |
||||
|
</ModalFooter> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
</> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default EditItem; |
@ -1,59 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Box, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerOverlay, Flex, IconButton, Image, useDisclosure, Text, useColorModeValue } from '@chakra-ui/react'; |
|
||||
import { FiMenu } from 'react-icons/fi'; |
|
||||
|
|
||||
import Logo from "../assets/images/fastapi-logo.svg"; |
|
||||
import SidebarItems from './SidebarItems'; |
|
||||
import { useUserStore } from '../store/user-store'; |
|
||||
|
|
||||
|
|
||||
const Sidebar: React.FC = () => { |
|
||||
const bgColor = useColorModeValue("white", "#1a202c"); |
|
||||
const textColor = useColorModeValue("gray", "white"); |
|
||||
const secBgColor = useColorModeValue("ui.secondary", "#252d3d"); |
|
||||
|
|
||||
const { isOpen, onOpen, onClose } = useDisclosure(); |
|
||||
const { user } = useUserStore(); |
|
||||
|
|
||||
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} /> |
|
||||
</Box> |
|
||||
{ |
|
||||
user?.email && |
|
||||
<Text color={textColor} noOfLines={2} fontSize="sm" p={2}>Logged in as: {user.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> |
|
||||
{ |
|
||||
user?.email && |
|
||||
<Text color={textColor} noOfLines={2} fontSize="sm" p={2} maxW="180px">Logged in as: {user.email}</Text> |
|
||||
} |
|
||||
</Flex> |
|
||||
</Box> |
|
||||
</> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
export default Sidebar; |
|
@ -0,0 +1,29 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Badge, Container, Heading, Radio, RadioGroup, Stack, useColorMode } from '@chakra-ui/react'; |
||||
|
|
||||
|
const Appearance: React.FC = () => { |
||||
|
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> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
export default Appearance; |
@ -0,0 +1,58 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Box, Button, Container, FormControl, FormLabel, Heading, Input, useColorModeValue } from '@chakra-ui/react'; |
||||
|
import { SubmitHandler, useForm } from 'react-hook-form'; |
||||
|
import { ApiError, UpdatePassword } from '../../client'; |
||||
|
import useCustomToast from '../../hooks/useCustomToast'; |
||||
|
import { useUserStore } from '../../store/user-store'; |
||||
|
|
||||
|
interface UpdatePasswordForm extends UpdatePassword { |
||||
|
confirm_password: string; |
||||
|
} |
||||
|
|
||||
|
const ChangePassword: React.FC = () => { |
||||
|
const color = useColorModeValue('gray.700', 'white'); |
||||
|
const showToast = useCustomToast(); |
||||
|
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UpdatePasswordForm>(); |
||||
|
const { editPassword } = useUserStore(); |
||||
|
|
||||
|
const onSubmit: SubmitHandler<UpdatePasswordForm> = async (data) => { |
||||
|
try { |
||||
|
await editPassword(data); |
||||
|
showToast('Success!', 'Password updated.', 'success'); |
||||
|
reset(); |
||||
|
} catch (err) { |
||||
|
const errDetail = (err as ApiError).body.detail; |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error'); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Container maxW='full' as='form' onSubmit={handleSubmit(onSubmit)}> |
||||
|
<Heading size='sm' py={4}> |
||||
|
Change Password |
||||
|
</Heading> |
||||
|
<Box w={{ 'sm': 'full', 'md': '50%' }}> |
||||
|
<FormControl> |
||||
|
<FormLabel color={color} htmlFor='currentPassword'>Current password</FormLabel> |
||||
|
<Input id='currentPassword' {...register('current_password')} placeholder='••••••••' type='password' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel color={color} htmlFor='newPassword'>New password</FormLabel> |
||||
|
<Input id='newPassword' {...register('new_password')} placeholder='••••••••' type='password' /> |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel color={color} htmlFor='confirmPassword'>Confirm new password</FormLabel> |
||||
|
<Input id='confirmPassword' {...register('confirm_password')} placeholder='••••••••' type='password' /> |
||||
|
</FormControl> |
||||
|
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} mt={4} type='submit' isLoading={isSubmitting}> |
||||
|
Save |
||||
|
</Button> |
||||
|
</Box> |
||||
|
</ Container> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
export default ChangePassword; |
@ -0,0 +1,88 @@ |
|||||
|
import React, { useState } from 'react'; |
||||
|
|
||||
|
import { Box, Button, Container, Flex, FormControl, FormLabel, Heading, Input, Text, useColorModeValue } from '@chakra-ui/react'; |
||||
|
|
||||
|
import { SubmitHandler, useForm } from 'react-hook-form'; |
||||
|
import { ApiError, UserOut, UserUpdateMe } from '../../client'; |
||||
|
import useCustomToast from '../../hooks/useCustomToast'; |
||||
|
import { useUserStore } from '../../store/user-store'; |
||||
|
import { useUsersStore } from '../../store/users-store'; |
||||
|
|
||||
|
const UserInformation: React.FC = () => { |
||||
|
const color = useColorModeValue('gray.700', 'white'); |
||||
|
const showToast = useCustomToast(); |
||||
|
const [editMode, setEditMode] = useState(false); |
||||
|
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserOut>(); |
||||
|
const { user, editUser } = useUserStore(); |
||||
|
const { getUsers } = useUsersStore(); |
||||
|
|
||||
|
const toggleEditMode = () => { |
||||
|
setEditMode(!editMode); |
||||
|
}; |
||||
|
|
||||
|
const onSubmit: SubmitHandler<UserUpdateMe> = async (data) => { |
||||
|
try { |
||||
|
await editUser(data); |
||||
|
await getUsers() |
||||
|
showToast('Success!', 'User updated successfully.', 'success'); |
||||
|
} catch (err) { |
||||
|
const errDetail = (err as ApiError).body.detail; |
||||
|
showToast('Something went wrong.', `${errDetail}`, 'error'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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')} defaultValue={user?.full_name} type='text' size='md' /> : |
||||
|
<Text size='md' py={2}> |
||||
|
{user?.full_name || 'N/A'} |
||||
|
</Text> |
||||
|
} |
||||
|
</FormControl> |
||||
|
<FormControl mt={4}> |
||||
|
<FormLabel color={color} htmlFor='email'>Email</FormLabel> |
||||
|
{ |
||||
|
editMode ? |
||||
|
<Input id='email' {...register('email')} defaultValue={user?.email} type='text' size='md' /> : |
||||
|
<Text size='md' py={2}> |
||||
|
{user?.email || 'N/A'} |
||||
|
</Text> |
||||
|
} |
||||
|
</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} |
||||
|
> |
||||
|
{editMode ? 'Save' : 'Edit'} |
||||
|
</Button> |
||||
|
{editMode && |
||||
|
<Button onClick={onCancel}> |
||||
|
Cancel |
||||
|
</Button>} |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
</ Container> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default UserInformation; |
@ -0,0 +1,33 @@ |
|||||
|
import { useUserStore } from '../store/user-store'; |
||||
|
import { Body_login_login_access_token as AccessToken, LoginService } from '../client'; |
||||
|
import { useUsersStore } from '../store/users-store'; |
||||
|
import { useItemsStore } from '../store/items-store'; |
||||
|
|
||||
|
const useAuth = () => { |
||||
|
const { user, getUser, resetUser } = useUserStore(); |
||||
|
const { resetUsers } = useUsersStore(); |
||||
|
const { resetItems } = useItemsStore(); |
||||
|
|
||||
|
const login = async (data: AccessToken) => { |
||||
|
const response = await LoginService.loginAccessToken({ |
||||
|
formData: data, |
||||
|
}); |
||||
|
localStorage.setItem('access_token', response.access_token); |
||||
|
await getUser(); |
||||
|
}; |
||||
|
|
||||
|
const logout = () => { |
||||
|
localStorage.removeItem('access_token'); |
||||
|
resetUser(); |
||||
|
resetUsers(); |
||||
|
resetItems(); |
||||
|
}; |
||||
|
|
||||
|
const isLoggedIn = () => { |
||||
|
return user !== null; |
||||
|
}; |
||||
|
|
||||
|
return { login, logout, isLoggedIn }; |
||||
|
} |
||||
|
|
||||
|
export default useAuth; |
@ -0,0 +1,20 @@ |
|||||
|
import { useCallback } from 'react'; |
||||
|
|
||||
|
import { useToast } from '@chakra-ui/react'; |
||||
|
|
||||
|
const useCustomToast = () => { |
||||
|
const toast = useToast(); |
||||
|
|
||||
|
const showToast = useCallback((title: string, description: string, status: 'success' | 'error') => { |
||||
|
toast({ |
||||
|
title, |
||||
|
description, |
||||
|
status, |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
}, [toast]); |
||||
|
|
||||
|
return showToast; |
||||
|
}; |
||||
|
|
||||
|
export default useCustomToast; |
@ -1,97 +0,0 @@ |
|||||
import React, { useState } from 'react'; |
|
||||
|
|
||||
import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useToast } from '@chakra-ui/react'; |
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'; |
|
||||
|
|
||||
import { UserCreate } from '../client'; |
|
||||
import { useUsersStore } from '../store/users-store'; |
|
||||
|
|
||||
interface AddUserProps { |
|
||||
isOpen: boolean; |
|
||||
onClose: () => void; |
|
||||
} |
|
||||
|
|
||||
const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => { |
|
||||
const toast = useToast(); |
|
||||
const [isLoading, setIsLoading] = useState(false); |
|
||||
const { register, handleSubmit, reset } = useForm<UserCreate>(); |
|
||||
const { addUser } = useUsersStore(); |
|
||||
|
|
||||
const onSubmit: SubmitHandler<UserCreate> = async (data) => { |
|
||||
setIsLoading(true); |
|
||||
try { |
|
||||
await addUser(data); |
|
||||
toast({ |
|
||||
title: 'Success!', |
|
||||
description: 'User created successfully.', |
|
||||
status: 'success', |
|
||||
isClosable: true, |
|
||||
}); |
|
||||
reset(); |
|
||||
onClose(); |
|
||||
|
|
||||
} catch (err) { |
|
||||
toast({ |
|
||||
title: 'Something went wrong.', |
|
||||
description: 'Failed to create user. Please try again.', |
|
||||
status: 'error', |
|
||||
isClosable: true, |
|
||||
}); |
|
||||
} finally { |
|
||||
setIsLoading(false); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<Modal |
|
||||
isOpen={isOpen} |
|
||||
onClose={onClose} |
|
||||
size={{ base: "sm", md: "md" }} |
|
||||
isCentered |
|
||||
> |
|
||||
<ModalOverlay /> |
|
||||
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}> |
|
||||
{/* TODO: Check passwords */} |
|
||||
<ModalHeader>Add User</ModalHeader> |
|
||||
<ModalCloseButton /> |
|
||||
<ModalBody pb={6}> |
|
||||
<FormControl> |
|
||||
<FormLabel>Email</FormLabel> |
|
||||
<Input {...register('email')} placeholder='Email' type="email" /> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel>Full name</FormLabel> |
|
||||
<Input {...register('full_name')} placeholder='Full name' type="text" /> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel>Set Password</FormLabel> |
|
||||
<Input {...register('password')} placeholder='Password' type="password" /> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel>Confirm Password</FormLabel> |
|
||||
<Input {...register('confirmPassword')} placeholder='Password' type="password" /> |
|
||||
</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" type="submit" isLoading={isLoading}> |
|
||||
Save |
|
||||
</Button> |
|
||||
<Button onClick={onClose}>Cancel</Button> |
|
||||
</ModalFooter> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default AddUser; |
|
@ -1,48 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; |
|
||||
|
|
||||
interface EditItemProps { |
|
||||
isOpen: boolean; |
|
||||
onClose: () => void; |
|
||||
} |
|
||||
|
|
||||
const EditItem: React.FC<EditItemProps> = ({ isOpen, onClose }) => { |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<Modal |
|
||||
isOpen={isOpen} |
|
||||
onClose={onClose} |
|
||||
size={{ base: "sm", md: "md" }} |
|
||||
isCentered |
|
||||
> |
|
||||
<ModalOverlay /> |
|
||||
<ModalContent> |
|
||||
<ModalHeader>Edit Item</ModalHeader> |
|
||||
<ModalCloseButton /> |
|
||||
<ModalBody pb={6}> |
|
||||
<FormControl> |
|
||||
<FormLabel>Item</FormLabel> |
|
||||
<Input placeholder='Item' type="text" /> |
|
||||
</FormControl> |
|
||||
|
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel>Description</FormLabel> |
|
||||
<Input placeholder='Description' type="text" /> |
|
||||
</FormControl> |
|
||||
</ModalBody> |
|
||||
|
|
||||
<ModalFooter gap={3}> |
|
||||
<Button colorScheme='teal'> |
|
||||
Save |
|
||||
</Button> |
|
||||
<Button onClick={onClose}>Cancel</Button> |
|
||||
</ModalFooter> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default EditItem; |
|
@ -1,60 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'; |
|
||||
|
|
||||
interface EditUserProps { |
|
||||
isOpen: boolean; |
|
||||
onClose: () => void; |
|
||||
} |
|
||||
|
|
||||
const EditUser: React.FC<EditUserProps> = ({ isOpen, onClose }) => { |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<Modal |
|
||||
isOpen={isOpen} |
|
||||
onClose={onClose} |
|
||||
size={{ base: "sm", md: "md" }} |
|
||||
isCentered |
|
||||
> |
|
||||
<ModalOverlay /> |
|
||||
<ModalContent> |
|
||||
<ModalHeader>Edit User</ModalHeader> |
|
||||
<ModalCloseButton /> |
|
||||
<ModalBody pb={6}> |
|
||||
<FormControl> |
|
||||
<FormLabel>Email</FormLabel> |
|
||||
<Input placeholder='Email' type="email" /> |
|
||||
</FormControl> |
|
||||
|
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel>Full name</FormLabel> |
|
||||
<Input placeholder='Full name' type="text" /> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel>Password</FormLabel> |
|
||||
<Input placeholder='Password' type="password" /> |
|
||||
</FormControl> |
|
||||
<Flex> |
|
||||
<FormControl mt={4}> |
|
||||
<Checkbox colorScheme='teal'>Is superuser?</Checkbox> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<Checkbox colorScheme='teal'>Is active?</Checkbox> |
|
||||
</FormControl> |
|
||||
</Flex> |
|
||||
</ModalBody> |
|
||||
|
|
||||
<ModalFooter gap={3}> |
|
||||
<Button bg="ui.main" color="white" _hover={{ opacity: 0.8 }}> |
|
||||
Save |
|
||||
</Button> |
|
||||
<Button onClick={onClose}>Cancel</Button> |
|
||||
</ModalFooter> |
|
||||
</ModalContent> |
|
||||
</Modal> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default EditUser; |
|
@ -1,28 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Container, Heading, Radio, RadioGroup, Stack, useColorMode } from '@chakra-ui/react'; |
|
||||
|
|
||||
const Appearance: React.FC = () => { |
|
||||
const { colorMode, toggleColorMode } = useColorMode(); |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<Container maxW="full"> |
|
||||
<Heading size="sm" py={4}> |
|
||||
Appearance |
|
||||
</Heading> |
|
||||
<RadioGroup onChange={toggleColorMode} value={colorMode}> |
|
||||
<Stack> |
|
||||
<Radio value="light" colorScheme="teal"> |
|
||||
Light <i>(default)</i> |
|
||||
</Radio> |
|
||||
<Radio value="dark" colorScheme="teal"> |
|
||||
Dark |
|
||||
</Radio> |
|
||||
</Stack> |
|
||||
</RadioGroup> |
|
||||
</Container> |
|
||||
</> |
|
||||
); |
|
||||
} |
|
||||
export default Appearance; |
|
@ -1,35 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Box, Button, Container, FormControl, FormLabel, Heading, Input, useColorModeValue } from '@chakra-ui/react'; |
|
||||
|
|
||||
const ChangePassword: React.FC = () => { |
|
||||
const color = useColorModeValue("gray.700", "white"); |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<Container maxW="full"> |
|
||||
<Heading size="sm" py={4}> |
|
||||
Change Password |
|
||||
</Heading> |
|
||||
<Box as="form" display="flex" flexDirection="column" alignItems="start"> |
|
||||
<FormControl> |
|
||||
<FormLabel color={color}>Old password</FormLabel> |
|
||||
<Input placeholder='Password' type="password" /> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel color={color}>New password</FormLabel> |
|
||||
<Input placeholder='Password' type="password" /> |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel color={color}>Confirm new password</FormLabel> |
|
||||
<Input placeholder='Password' type="password" /> |
|
||||
</FormControl> |
|
||||
<Button bg="ui.main" color="white" _hover={{ opacity: 0.8 }} mt={4} type="submit"> |
|
||||
Save |
|
||||
</Button> |
|
||||
</Box> |
|
||||
</ Container> |
|
||||
</> |
|
||||
); |
|
||||
} |
|
||||
export default ChangePassword; |
|
@ -1,50 +0,0 @@ |
|||||
import React, { useState } from 'react'; |
|
||||
|
|
||||
import { Button, Container, FormControl, FormLabel, Heading, Input, Text, useColorModeValue } from '@chakra-ui/react'; |
|
||||
|
|
||||
import { useUserStore } from '../store/user-store'; |
|
||||
|
|
||||
const UserInformation: React.FC = () => { |
|
||||
const color = useColorModeValue("gray.700", "white"); |
|
||||
const [editMode, setEditMode] = useState(false); |
|
||||
const { user } = useUserStore(); |
|
||||
|
|
||||
|
|
||||
const toggleEditMode = () => { |
|
||||
setEditMode(!editMode); |
|
||||
}; |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<Container maxW="full"> |
|
||||
<Heading size="sm" py={4}> |
|
||||
User Information |
|
||||
</Heading> |
|
||||
<FormControl> |
|
||||
<FormLabel color={color}>Full name</FormLabel> |
|
||||
{ |
|
||||
editMode ? |
|
||||
<Input placeholder={user?.full_name || "Full name"} type="text" size="md" /> : |
|
||||
<Text size="md" py={2}> |
|
||||
{user?.full_name || "N/A"} |
|
||||
</Text> |
|
||||
} |
|
||||
</FormControl> |
|
||||
<FormControl mt={4}> |
|
||||
<FormLabel color={color}>Email</FormLabel> |
|
||||
{ |
|
||||
editMode ? |
|
||||
<Input placeholder={user?.email} type="text" size="md" /> : |
|
||||
<Text size="md" py={2}> |
|
||||
{user?.email || "N/A"} |
|
||||
</Text> |
|
||||
} |
|
||||
</FormControl> |
|
||||
<Button bg="ui.main" color="white" _hover={{ opacity: 0.8 }} mt={4} onClick={toggleEditMode}> |
|
||||
{editMode ? "Save" : "Edit"} |
|
||||
</Button> |
|
||||
</ Container> |
|
||||
</> |
|
||||
); |
|
||||
} |
|
||||
export default UserInformation; |
|
Loading…
Reference in new issue