🚚 Move new-frontend to frontend (#652)
This commit is contained in:
76
frontend/src/components/Common/ActionsMenu.tsx
Normal file
76
frontend/src/components/Common/ActionsMenu.tsx
Normal file
@@ -0,0 +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'
|
||||
|
||||
interface ActionsMenuProps {
|
||||
type: string
|
||||
value: ItemOut | UserOut
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ActionsMenu: React.FC<ActionsMenuProps> = ({ type, value, disabled }) => {
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsMenu
|
116
frontend/src/components/Common/DeleteAlert.tsx
Normal file
116
frontend/src/components/Common/DeleteAlert.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
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 { ItemsService, UsersService } from '../../client'
|
||||
import useCustomToast from '../../hooks/useCustomToast'
|
||||
|
||||
interface DeleteProps {
|
||||
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 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 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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Delete
|
43
frontend/src/components/Common/Navbar.tsx
Normal file
43
frontend/src/components/Common/Navbar.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
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'
|
||||
|
||||
interface NavbarProps {
|
||||
type: string
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ type }) => {
|
||||
const addUserModal = useDisclosure()
|
||||
const addItemModal = useDisclosure()
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
42
frontend/src/components/Common/NotFound.tsx
Normal file
42
frontend/src/components/Common/NotFound.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
117
frontend/src/components/Common/Sidebar.tsx
Normal file
117
frontend/src/components/Common/Sidebar.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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 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()
|
||||
}
|
||||
|
||||
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>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
57
frontend/src/components/Common/SidebarItems.tsx
Normal file
57
frontend/src/components/Common/SidebarItems.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
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 { UserOut } from '../../client'
|
||||
|
||||
const items = [
|
||||
{ icon: FiHome, title: 'Dashboard', path: '/' },
|
||||
{ icon: FiBriefcase, title: 'Items', path: '/items' },
|
||||
{ icon: FiSettings, title: 'User Settings', path: '/settings' },
|
||||
]
|
||||
|
||||
interface SidebarItemsProps {
|
||||
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
|
59
frontend/src/components/Common/UserMenu.tsx
Normal file
59
frontend/src/components/Common/UserMenu.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
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 useAuth from '../../hooks/useAuth'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
const UserMenu: React.FC = () => {
|
||||
const { logout } = useAuth()
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserMenu
|
Reference in New Issue
Block a user