committed by
GitHub
29 changed files with 2081 additions and 1376 deletions
@ -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> |
|||
<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> |
|||
<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} /> |
|||
{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,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 |
|||
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 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}> |
|||
<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'> |
|||
<DrawerContent maxW="250px"> |
|||
<DrawerCloseButton /> |
|||
<DrawerBody py={8}> |
|||
<Flex flexDir='column' justify='space-between'> |
|||
<Flex flexDir="column" justify="space-between"> |
|||
<Box> |
|||
<Image src={Logo} alt='logo' p={6} /> |
|||
<Image src={Logo} alt="logo" p={6} /> |
|||
<SidebarItems onClose={onClose} /> |
|||
<Flex as='button' onClick={handleLogout} p={2} color='ui.danger' fontWeight='bold' alignItems='center'> |
|||
<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> |
|||
} |
|||
{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 |
|||
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} /> |
|||
<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> |
|||
} |
|||
{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,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() |
|||
}; |
|||
} |
|||
|
|||
return ( |
|||
<> |
|||
{/* Desktop */} |
|||
<Box display={{ base: 'none', md: 'block' }} position='fixed' top={4} right={4}> |
|||
<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' |
|||
aria-label="Options" |
|||
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> |
|||
</Menu> |
|||
</Box> |
|||
</> |
|||
); |
|||
}; |
|||
) |
|||
} |
|||
|
|||
export default UserMenu; |
|||
export default UserMenu |
|||
|
@ -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}> |
|||
<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 value="light" colorScheme="teal"> |
|||
Light mode |
|||
<Badge ml="1" colorScheme="teal"> |
|||
Default |
|||
</Badge> |
|||
</Radio> |
|||
<Radio value='dark' colorScheme='teal'> |
|||
<Radio value="dark" colorScheme="teal"> |
|||
Dark mode |
|||
</Radio> |
|||
</Stack> |
|||
</RadioGroup> |
|||
</Container> |
|||
</> |
|||
); |
|||
) |
|||
} |
|||
export default Appearance; |
|||
export default Appearance |
|||
|
@ -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}> |
|||
<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. |
|||
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}> |
|||
<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> |
|||
<DeleteConfirmation |
|||
isOpen={confirmationModal.isOpen} |
|||
onClose={confirmationModal.onClose} |
|||
/> |
|||
</Container> |
|||
</> |
|||
); |
|||
) |
|||
} |
|||
export default DeleteAccount; |
|||
export default DeleteAccount |
|||
|
@ -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, { |
|||
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: '/' }); |
|||
}; |
|||
}) |
|||
localStorage.setItem('access_token', response.access_token) |
|||
navigate({ to: '/' }) |
|||
} |
|||
|
|||
const logout = () => { |
|||
localStorage.removeItem('access_token'); |
|||
navigate({ to: '/login' }); |
|||
}; |
|||
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') => { |
|||
const showToast = useCallback( |
|||
(title: string, description: string, status: 'success' | 'error') => { |
|||
toast({ |
|||
title, |
|||
description, |
|||
status, |
|||
isClosable: true, |
|||
position: 'bottom-right' |
|||
}); |
|||
}, [toast]); |
|||
position: 'bottom-right', |
|||
}) |
|||
}, |
|||
[toast], |
|||
) |
|||
|
|||
return showToast; |
|||
}; |
|||
return showToast |
|||
} |
|||
|
|||
export default useCustomToast; |
|||
export default useCustomToast |
|||
|
@ -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, |
|||
}) |
|||
|
|||
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> |
|||
<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 |
|||
|
Loading…
Reference in new issue