committed by
GitHub
33 changed files with 472 additions and 313 deletions
@ -1,42 +0,0 @@ |
|||||
#root { |
|
||||
max-width: 1280px; |
|
||||
margin: 0 auto; |
|
||||
padding: 2rem; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.logo { |
|
||||
height: 6em; |
|
||||
padding: 1.5em; |
|
||||
will-change: filter; |
|
||||
transition: filter 300ms; |
|
||||
} |
|
||||
.logo:hover { |
|
||||
filter: drop-shadow(0 0 2em #646cffaa); |
|
||||
} |
|
||||
.logo.react:hover { |
|
||||
filter: drop-shadow(0 0 2em #61dafbaa); |
|
||||
} |
|
||||
|
|
||||
@keyframes logo-spin { |
|
||||
from { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
to { |
|
||||
transform: rotate(360deg); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@media (prefers-reduced-motion: no-preference) { |
|
||||
a:nth-of-type(2) .logo { |
|
||||
animation: logo-spin infinite 20s linear; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.card { |
|
||||
padding: 2em; |
|
||||
} |
|
||||
|
|
||||
.read-the-docs { |
|
||||
color: #888; |
|
||||
} |
|
@ -1,61 +0,0 @@ |
|||||
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; |
|
||||
|
|
||||
import { ChakraProvider, extendTheme } from '@chakra-ui/react'; |
|
||||
|
|
||||
import Layout from './pages/Layout'; |
|
||||
import NotFound from './pages/NotFound'; |
|
||||
import Login from './pages/auth/Login'; |
|
||||
import RecoverPassword from './pages/auth/RecoverPassword'; |
|
||||
import Admin from './pages/main/Admin'; |
|
||||
import Dashboard from './pages/main/Dashboard'; |
|
||||
import Items from './pages/main/Items'; |
|
||||
import Profile from './pages/main/Profile'; |
|
||||
|
|
||||
// Theme
|
|
||||
const theme = extendTheme({ |
|
||||
colors: { |
|
||||
ui: { |
|
||||
main: "#009688", |
|
||||
secondary: "#EDF2F7", |
|
||||
success: '#48BB78', |
|
||||
danger: '#E53E3E', |
|
||||
} |
|
||||
}, |
|
||||
components: { |
|
||||
Tabs: { |
|
||||
variants: { |
|
||||
enclosed: { |
|
||||
tab: { |
|
||||
_selected: { |
|
||||
color: 'ui.main', |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
function App() { |
|
||||
return ( |
|
||||
<> |
|
||||
<Router> |
|
||||
<ChakraProvider theme={theme}> |
|
||||
<Routes> |
|
||||
<Route path="/login" element={<Login />} /> |
|
||||
<Route path="/recover-password" element={<RecoverPassword />} /> |
|
||||
<Route element={<Layout />}> |
|
||||
<Route path="/" element={<Dashboard />} /> |
|
||||
<Route path="/settings" element={<Profile />} /> |
|
||||
<Route path="/items" element={<Items />} /> |
|
||||
<Route path="/admin" element={<Admin />} /> |
|
||||
</Route> |
|
||||
<Route path="*" element={<NotFound />} /> |
|
||||
</Routes> |
|
||||
</ ChakraProvider> |
|
||||
</Router> |
|
||||
</> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default App |
|
Before Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 4.9 KiB |
@ -1,29 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Avatar, Flex, Skeleton, Text } from '@chakra-ui/react'; |
|
||||
import { FaUserAstronaut } from 'react-icons/fa'; |
|
||||
|
|
||||
import { useUserStore } from '../store/user-store'; |
|
||||
|
|
||||
|
|
||||
const UserInfo: React.FC = () => { |
|
||||
const { user } = useUserStore(); |
|
||||
|
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
{user ? ( |
|
||||
<Flex gap={2} maxW="180px"> |
|
||||
<Avatar bg="ui.main" icon={<FaUserAstronaut fontSize="18px" />} size='sm' alignSelf="center" /> |
|
||||
{/* TODO: Conditional tooltip based on email length */} |
|
||||
<Text color='gray' alignSelf={"center"} noOfLines={1} fontSize="14px">{user.email}</Text> |
|
||||
</Flex> |
|
||||
) : |
|
||||
<Skeleton height='20px' /> |
|
||||
} |
|
||||
</> |
|
||||
); |
|
||||
|
|
||||
} |
|
||||
|
|
||||
export default UserInfo; |
|
@ -0,0 +1,45 @@ |
|||||
|
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 { FaUserAstronaut } from 'react-icons/fa'; |
||||
|
import { FiLogOut, FiUser } from 'react-icons/fi'; |
||||
|
import { useNavigate } from 'react-router'; |
||||
|
import { Link } from 'react-router-dom'; |
||||
|
|
||||
|
const UserMenu: React.FC = () => { |
||||
|
const navigate = useNavigate(); |
||||
|
|
||||
|
const handleLogout = async () => { |
||||
|
localStorage.removeItem("access_token"); |
||||
|
navigate("/login"); |
||||
|
// TODO: reset all Zustand states
|
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Box 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"> |
||||
|
Log out |
||||
|
</MenuItem> |
||||
|
</MenuList> |
||||
|
</Menu> |
||||
|
</Box> |
||||
|
</> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default UserMenu; |
@ -0,0 +1,22 @@ |
|||||
|
import React from 'react'; |
||||
|
|
||||
|
import { Box, Text } from '@chakra-ui/react'; |
||||
|
|
||||
|
import { useUserStore } from '../store/user-store'; |
||||
|
|
||||
|
|
||||
|
const Dashboard: React.FC = () => { |
||||
|
const { user } = useUserStore(); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Box width="100%" p={8}> |
||||
|
<Text fontSize="2xl">Hi, {user?.full_name || user?.email} 👋🏼</Text> |
||||
|
<Text>Welcome back, nice to see you again!</Text> |
||||
|
</Box> |
||||
|
</> |
||||
|
|
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Dashboard; |
@ -0,0 +1,26 @@ |
|||||
|
import { Button, Container, Text } from "@chakra-ui/react"; |
||||
|
|
||||
|
import { Link, useRouteError } from "react-router-dom"; |
||||
|
|
||||
|
const ErrorPage: React.FC = () => { |
||||
|
const error = useRouteError(); |
||||
|
console.log(error); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Container h="100vh" |
||||
|
alignItems="stretch" |
||||
|
justifyContent="center" textAlign="center" maxW="xs" centerContent> |
||||
|
<Text fontSize="8xl" color="ui.main" fontWeight="bold" lineHeight="1" mb={4}>Oops!</Text> |
||||
|
<Text fontSize="md">Houston, we have a problem.</Text> |
||||
|
<Text fontSize="md">An unexpected error has occurred.</Text> |
||||
|
<Text color="ui.danger"><i>{error.statusText || error.message}</i></Text> |
||||
|
<Button as={Link} to="/" color="ui.main" borderColor="ui.main" variant="outline" mt={4}>Go back to Home</Button> |
||||
|
</Container> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default ErrorPage; |
||||
|
|
||||
|
|
@ -1,15 +0,0 @@ |
|||||
import { Outlet } from 'react-router-dom'; |
|
||||
import Sidebar from '../components/Sidebar'; |
|
||||
|
|
||||
import { Flex } from '@chakra-ui/react'; |
|
||||
|
|
||||
const Layout = () => { |
|
||||
return ( |
|
||||
<Flex maxW="large" h="auto" position="relative"> |
|
||||
<Sidebar /> |
|
||||
<Outlet /> |
|
||||
</Flex> |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export default Layout; |
|
@ -1,18 +0,0 @@ |
|||||
import { Button, Container, Text } from "@chakra-ui/react"; |
|
||||
|
|
||||
import { Link } from "react-router-dom"; |
|
||||
|
|
||||
const NotFound = () => ( |
|
||||
<> |
|
||||
<Container h="100vh" |
|
||||
alignItems="stretch" |
|
||||
justifyContent="center" textAlign="center" maxW="xs" centerContent> |
|
||||
<Text fontSize="8xl" color="ui.main" fontWeight="bold" lineHeight="1" mb={4}>404</Text> |
|
||||
<Text fontSize="md">Houston, we have a problem.</Text> |
|
||||
<Text fontSize="md">It looks like the page you're looking for doesn't exist.</Text> |
|
||||
<Button as={Link} to="/" color="ui.main" borderColor="ui.main" variant="outline" mt={4}>Go back to Home</Button> |
|
||||
</Container> |
|
||||
</> |
|
||||
); |
|
||||
|
|
||||
export default NotFound; |
|
@ -0,0 +1,63 @@ |
|||||
|
import React from "react"; |
||||
|
|
||||
|
import { Button, Container, FormControl, Heading, Input, Text, useToast } from "@chakra-ui/react"; |
||||
|
import { SubmitHandler, useForm } from "react-hook-form"; |
||||
|
|
||||
|
import { LoginService } from "../client"; |
||||
|
|
||||
|
interface FormData { |
||||
|
email: string; |
||||
|
} |
||||
|
|
||||
|
const RecoverPassword: React.FC = () => { |
||||
|
const { register, handleSubmit } = useForm<FormData>(); |
||||
|
const toast = useToast(); |
||||
|
|
||||
|
const onSubmit: SubmitHandler<FormData> = async (data) => { |
||||
|
const response = await LoginService.recoverPassword({ |
||||
|
email: data.email, |
||||
|
}); |
||||
|
console.log(response); |
||||
|
|
||||
|
toast({ |
||||
|
title: "Email sent.", |
||||
|
description: "We sent an email with a link to get back into your account.", |
||||
|
status: "success", |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Container |
||||
|
as="form" |
||||
|
onSubmit={handleSubmit(onSubmit)} |
||||
|
h="100vh" |
||||
|
maxW="sm" |
||||
|
alignItems="stretch" |
||||
|
justifyContent="center" |
||||
|
gap={4} |
||||
|
centerContent |
||||
|
> |
||||
|
<Heading size="xl" color="ui.main" textAlign="center" mb={2}> |
||||
|
Password Recovery |
||||
|
</Heading> |
||||
|
<FormControl id="username"> |
||||
|
<Text align="center" color="gray.600"> |
||||
|
A password recovery email will be sent to the registered account. |
||||
|
</Text> |
||||
|
<Input |
||||
|
{...register("email")} |
||||
|
|
||||
|
mt={4} |
||||
|
placeholder="Enter your email" |
||||
|
type="text" |
||||
|
/> |
||||
|
</FormControl> |
||||
|
<Button bg="ui.main" color="white" _hover={{ opacity: 0.8 }} type="submit"> |
||||
|
Continue |
||||
|
</Button> |
||||
|
</Container> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default RecoverPassword; |
@ -0,0 +1,42 @@ |
|||||
|
import { useEffect } from 'react'; |
||||
|
|
||||
|
import { Outlet } from 'react-router-dom'; |
||||
|
import Sidebar from '../components/Sidebar'; |
||||
|
|
||||
|
import { Flex, useToast } from '@chakra-ui/react'; |
||||
|
import { useUserStore } from '../store/user-store'; |
||||
|
import UserMenu from '../components/UserMenu'; |
||||
|
|
||||
|
const Root: React.FC = () => { |
||||
|
const toast = useToast(); |
||||
|
const { getUser } = useUserStore(); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const fetchUser = async () => { |
||||
|
const token = localStorage.getItem('access_token'); |
||||
|
if (token) { |
||||
|
try { |
||||
|
await getUser(); |
||||
|
} catch (err) { |
||||
|
toast({ |
||||
|
title: 'Something went wrong.', |
||||
|
description: 'Failed to fetch user. Please try again.', |
||||
|
status: 'error', |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
fetchUser(); |
||||
|
}, []); |
||||
|
|
||||
|
return ( |
||||
|
<Flex maxW="large" h="auto" position="relative"> |
||||
|
<Sidebar /> |
||||
|
<Outlet /> |
||||
|
<UserMenu /> |
||||
|
</Flex> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default Root; |
@ -1,24 +0,0 @@ |
|||||
import React from 'react'; |
|
||||
|
|
||||
import { Box, Text } from '@chakra-ui/react'; |
|
||||
|
|
||||
import { useUserStore } from '../../store/user-store'; |
|
||||
|
|
||||
|
|
||||
const Dashboard: React.FC = () => { |
|
||||
const { user } = useUserStore(); |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
{user ? ( |
|
||||
<Box width="100%" p={8}> |
|
||||
<Text fontSize="24px">Hi, {user.full_name || user.email} 👋🏼</Text> |
|
||||
<Text>Welcome back, nice to see you again!</Text> |
|
||||
</Box> |
|
||||
) : null} |
|
||||
</> |
|
||||
|
|
||||
) |
|
||||
} |
|
||||
|
|
||||
export default Dashboard; |
|
@ -0,0 +1,37 @@ |
|||||
|
import { extendTheme } from "@chakra-ui/react" |
||||
|
|
||||
|
const theme = extendTheme({ |
||||
|
colors: { |
||||
|
ui: { |
||||
|
main: "#009688", |
||||
|
secondary: "#EDF2F7", |
||||
|
success: '#48BB78', |
||||
|
danger: '#E53E3E', |
||||
|
focus: 'red', |
||||
|
} |
||||
|
}, |
||||
|
components: { |
||||
|
Tabs: { |
||||
|
variants: { |
||||
|
enclosed: { |
||||
|
tab: { |
||||
|
_selected: { |
||||
|
color: 'ui.main', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
Input: { |
||||
|
baseStyle: { |
||||
|
field: { |
||||
|
_focus: { |
||||
|
borderColor: 'ui.focus', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default theme; |
Loading…
Reference in new issue