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 { IconButton } from '@chakra-ui/button'; |
|||
import { Box } from '@chakra-ui/layout'; |
|||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; |
|||
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 { useNavigate } from 'react-router'; |
|||
import { Link } from 'react-router-dom'; |
|||
import { Link, useNavigate } from 'react-router-dom'; |
|||
|
|||
import useAuth from '../../hooks/useAuth'; |
|||
|
|||
const UserMenu: React.FC = () => { |
|||
const navigate = useNavigate(); |
|||
const { logout } = useAuth(); |
|||
|
|||
const handleLogout = async () => { |
|||
localStorage.removeItem("access_token"); |
|||
navigate("/login"); |
|||
// TODO: reset all Zustand states
|
|||
logout() |
|||
navigate('/login'); |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<Box position="fixed" top={4} right={4}> |
|||
{/* 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" |
|||
icon={<FaUserAstronaut color='white' fontSize='18px' />} |
|||
bg='ui.main' |
|||
isRound |
|||
/> |
|||
<MenuList> |
|||
<MenuItem icon={<FiUser fontSize="18px" />} as={Link} to="settings"> |
|||
<MenuItem icon={<FiUser fontSize='18px' />} as={Link} to='settings'> |
|||
My profile |
|||
</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 |
|||
</MenuItem> |
|||
</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,49 +1,46 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Container, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; |
|||
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 { useUserStore } from '../store/user-store'; |
|||
|
|||
const tabsConfig = [ |
|||
{ title: 'My profile', component: UserInformation }, |
|||
{ title: 'Password', component: ChangePassword }, |
|||
{ title: 'Appearance', component: Appearance }, |
|||
{ title: 'Danger zone', component: DeleteAccount }, |
|||
]; |
|||
|
|||
import Appearance from '../panels/Appearance'; |
|||
import ChangePassword from '../panels/ChangePassword'; |
|||
import DeleteAccount from '../panels/DeleteAccount'; |
|||
import UserInformation from '../panels/UserInformation'; |
|||
|
|||
const UserSettings: React.FC = () => { |
|||
const { user } = useUserStore(); |
|||
|
|||
const finalTabs = user?.is_superuser ? tabsConfig.slice(0, 3) : tabsConfig; |
|||
|
|||
const UserSettings: React.FC = () => { |
|||
|
|||
return ( |
|||
<> |
|||
<Container maxW="full"> |
|||
<Heading size="lg" textAlign={{ base: "center", md: "left" }} py={12}> |
|||
User Settings |
|||
</Heading> |
|||
<Tabs variant='enclosed' > |
|||
<TabList> |
|||
<Tab>My profile</Tab> |
|||
<Tab>Password</Tab> |
|||
<Tab>Appearance</Tab> |
|||
<Tab>Danger zone</Tab> |
|||
</TabList> |
|||
<TabPanels> |
|||
<TabPanel> |
|||
<UserInformation /> |
|||
</TabPanel> |
|||
<TabPanel> |
|||
<ChangePassword /> |
|||
<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> |
|||
<TabPanel> |
|||
<Appearance /> |
|||
</TabPanel> |
|||
<TabPanel> |
|||
<DeleteAccount /> |
|||
</TabPanel> |
|||
|
|||
</TabPanels> |
|||
</Tabs> |
|||
</Container> |
|||
</> |
|||
))} |
|||
</TabPanels> |
|||
</Tabs> |
|||
</Container> |
|||
); |
|||
}; |
|||
|
|||
export default UserSettings; |
|||
|
|||
export default UserSettings; |
@ -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