🚚 Move new-frontend to frontend (#652)

This commit is contained in:
Alejandra
2024-03-08 19:23:54 +01:00
committed by GitHub
parent 3b44537361
commit 9d703df254
97 changed files with 8 additions and 8 deletions

View File

@@ -0,0 +1,14 @@
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import NotFound from '../components/Common/NotFound'
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
<TanStackRouterDevtools />
</>
),
notFoundComponent: () => <NotFound />,
})

View File

@@ -0,0 +1,37 @@
import { Flex, Spinner } from '@chakra-ui/react'
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'
import Sidebar from '../components/Common/Sidebar'
import UserMenu from '../components/Common/UserMenu'
import useAuth, { isLoggedIn } from '../hooks/useAuth'
export const Route = createFileRoute('/_layout')({
component: Layout,
beforeLoad: async () => {
if (!isLoggedIn()) {
throw redirect({
to: '/login',
})
}
},
})
function Layout() {
const { isLoading } = useAuth()
return (
<Flex maxW="large" h="auto" position="relative">
<Sidebar />
{isLoading ? (
<Flex justify="center" align="center" height="100vh" width="full">
<Spinner size="xl" color="ui.main" />
</Flex>
) : (
<Outlet />
)}
<UserMenu />
</Flex>
)
}
export default Layout

View File

@@ -0,0 +1,117 @@
import {
Badge,
Box,
Container,
Flex,
Heading,
Spinner,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from '@chakra-ui/react'
import { createFileRoute } from '@tanstack/react-router'
import { useQuery, useQueryClient } from 'react-query'
import { ApiError, UserOut, UsersService } from '../../client'
import ActionsMenu from '../../components/Common/ActionsMenu'
import Navbar from '../../components/Common/Navbar'
import useCustomToast from '../../hooks/useCustomToast'
export const Route = createFileRoute('/_layout/admin')({
component: Admin,
})
function Admin() {
const queryClient = useQueryClient()
const showToast = useCustomToast()
const currentUser = queryClient.getQueryData<UserOut>('currentUser')
const {
data: users,
isLoading,
isError,
error,
} = useQuery('users', () => UsersService.readUsers({}))
if (isError) {
const errDetail = (error as ApiError).body?.detail
showToast('Something went wrong.', `${errDetail}`, 'error')
}
return (
<>
{isLoading ? (
// TODO: Add skeleton
<Flex justify="center" align="center" height="100vh" width="full">
<Spinner size="xl" color="ui.main" />
</Flex>
) : (
users && (
<Container maxW="full">
<Heading
size="lg"
textAlign={{ base: 'center', md: 'left' }}
pt={12}
>
User Management
</Heading>
<Navbar type={'User'} />
<TableContainer>
<Table fontSize="md" size={{ base: 'sm', md: 'md' }}>
<Thead>
<Tr>
<Th>Full name</Th>
<Th>Email</Th>
<Th>Role</Th>
<Th>Status</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{users.data.map((user) => (
<Tr key={user.id}>
<Td color={!user.full_name ? 'gray.600' : 'inherit'}>
{user.full_name || 'N/A'}
{currentUser?.id === user.id && (
<Badge ml="1" colorScheme="teal">
You
</Badge>
)}
</Td>
<Td>{user.email}</Td>
<Td>{user.is_superuser ? 'Superuser' : 'User'}</Td>
<Td>
<Flex gap={2}>
<Box
w="2"
h="2"
borderRadius="50%"
bg={user.is_active ? 'ui.success' : 'ui.danger'}
alignSelf="center"
/>
{user.is_active ? 'Active' : 'Inactive'}
</Flex>
</Td>
<Td>
<ActionsMenu
type="User"
value={user}
disabled={currentUser?.id === user.id ? true : false}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Container>
)
)}
</>
)
}
export default Admin

View File

@@ -0,0 +1,28 @@
import { Container, Text } from '@chakra-ui/react'
import { useQueryClient } from 'react-query'
import { createFileRoute } from '@tanstack/react-router'
import { UserOut } from '../../client'
export const Route = createFileRoute('/_layout/')({
component: Dashboard,
})
function Dashboard() {
const queryClient = useQueryClient()
const currentUser = queryClient.getQueryData<UserOut>('currentUser')
return (
<>
<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

View File

@@ -0,0 +1,91 @@
import {
Container,
Flex,
Heading,
Spinner,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
} from '@chakra-ui/react'
import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from 'react-query'
import { ApiError, ItemsService } from '../../client'
import ActionsMenu from '../../components/Common/ActionsMenu'
import Navbar from '../../components/Common/Navbar'
import useCustomToast from '../../hooks/useCustomToast'
export const Route = createFileRoute('/_layout/items')({
component: Items,
})
function Items() {
const showToast = useCustomToast()
const {
data: items,
isLoading,
isError,
error,
} = useQuery('items', () => ItemsService.readItems({}))
if (isError) {
const errDetail = (error as ApiError).body?.detail
showToast('Something went wrong.', `${errDetail}`, 'error')
}
return (
<>
{isLoading ? (
// TODO: Add skeleton
<Flex justify="center" align="center" height="100vh" width="full">
<Spinner size="xl" color="ui.main" />
</Flex>
) : (
items && (
<Container maxW="full">
<Heading
size="lg"
textAlign={{ base: 'center', md: 'left' }}
pt={12}
>
Items Management
</Heading>
<Navbar type={'Item'} />
<TableContainer>
<Table size={{ base: 'sm', md: 'md' }}>
<Thead>
<Tr>
<Th>ID</Th>
<Th>Title</Th>
<Th>Description</Th>
<Th>Actions</Th>
</Tr>
</Thead>
<Tbody>
{items.data.map((item) => (
<Tr key={item.id}>
<Td>{item.id}</Td>
<Td>{item.title}</Td>
<Td color={!item.description ? 'gray.600' : 'inherit'}>
{item.description || 'N/A'}
</Td>
<Td>
<ActionsMenu type={'Item'} value={item} />
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Container>
)
)}
</>
)
}
export default Items

View File

@@ -0,0 +1,60 @@
import {
Container,
Heading,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from '@chakra-ui/react'
import { createFileRoute } from '@tanstack/react-router'
import { useQueryClient } from 'react-query'
import { UserOut } from '../../client'
import Appearance from '../../components/UserSettings/Appearance'
import ChangePassword from '../../components/UserSettings/ChangePassword'
import DeleteAccount from '../../components/UserSettings/DeleteAccount'
import UserInformation from '../../components/UserSettings/UserInformation'
const tabsConfig = [
{ title: 'My profile', component: UserInformation },
{ title: 'Password', component: ChangePassword },
{ title: 'Appearance', component: Appearance },
{ title: 'Danger zone', component: DeleteAccount },
]
export const Route = createFileRoute('/_layout/settings')({
component: UserSettings,
})
function UserSettings() {
const queryClient = useQueryClient()
const currentUser = queryClient.getQueryData<UserOut>('currentUser')
const finalTabs = currentUser?.is_superuser
? tabsConfig.slice(0, 3)
: tabsConfig
return (
<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>
))}
</TabPanels>
</Tabs>
</Container>
)
}
export default UserSettings

View File

@@ -0,0 +1,144 @@
import React from 'react'
import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons'
import {
Button,
Center,
Container,
FormControl,
FormErrorMessage,
Icon,
Image,
Input,
InputGroup,
InputRightElement,
Link,
useBoolean,
} from '@chakra-ui/react'
import {
Link as RouterLink,
createFileRoute,
redirect,
} from '@tanstack/react-router'
import { SubmitHandler, useForm } from 'react-hook-form'
import Logo from '../assets/images/fastapi-logo.svg'
import { ApiError } from '../client'
import { Body_login_login_access_token as AccessToken } from '../client/models/Body_login_login_access_token'
import useAuth, { isLoggedIn } from '../hooks/useAuth'
export const Route = createFileRoute('/login')({
component: Login,
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
to: '/',
})
}
},
})
function Login() {
const [show, setShow] = useBoolean()
const { login } = useAuth()
const [error, setError] = React.useState<string | null>(null)
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<AccessToken>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
username: '',
password: '',
},
})
const onSubmit: SubmitHandler<AccessToken> = async (data) => {
try {
await login(data)
} catch (err) {
const errDetail = (err as ApiError).body.detail
setError(errDetail)
}
}
return (
<>
<Container
as="form"
onSubmit={handleSubmit(onSubmit)}
h="100vh"
maxW="sm"
alignItems="stretch"
justifyContent="center"
gap={4}
centerContent
>
<Image
src={Logo}
alt="FastAPI logo"
height="auto"
maxW="2xs"
alignSelf="center"
mb={4}
/>
<FormControl id="username" isInvalid={!!errors.username || !!error}>
<Input
id="username"
{...register('username', {
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'Invalid email address',
},
})}
placeholder="Email"
type="text"
/>
{errors.username && (
<FormErrorMessage>{errors.username.message}</FormErrorMessage>
)}
</FormControl>
<FormControl id="password" isInvalid={!!error}>
<InputGroup>
<Input
{...register('password')}
type={show ? 'text' : 'password'}
placeholder="Password"
/>
<InputRightElement
color="gray.400"
_hover={{
cursor: 'pointer',
}}
>
<Icon
onClick={setShow.toggle}
aria-label={show ? 'Hide password' : 'Show password'}
>
{show ? <ViewOffIcon /> : <ViewIcon />}
</Icon>
</InputRightElement>
</InputGroup>
{error && <FormErrorMessage>{error}</FormErrorMessage>}
</FormControl>
<Center>
<Link as={RouterLink} to="/recover-password" color="blue.500">
Forgot password?
</Link>
</Center>
<Button
bg="ui.main"
color="white"
_hover={{ opacity: 0.8 }}
type="submit"
isLoading={isSubmitting}
>
Log In
</Button>
</Container>
</>
)
}
export default Login

View File

@@ -0,0 +1,98 @@
import {
Button,
Container,
FormControl,
FormErrorMessage,
Heading,
Input,
Text,
} from '@chakra-ui/react'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { SubmitHandler, useForm } from 'react-hook-form'
import { LoginService } from '../client'
import useCustomToast from '../hooks/useCustomToast'
import { isLoggedIn } from '../hooks/useAuth'
interface FormData {
email: string
}
export const Route = createFileRoute('/recover-password')({
component: RecoverPassword,
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
to: '/',
})
}
},
})
function RecoverPassword() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>()
const showToast = useCustomToast()
const onSubmit: SubmitHandler<FormData> = async (data) => {
await LoginService.recoverPassword({
email: data.email,
})
showToast(
'Email sent.',
'We sent an email with a link to get back into your account.',
'success',
)
}
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>
<Text align="center">
A password recovery email will be sent to the registered account.
</Text>
<FormControl isInvalid={!!errors.email}>
<Input
id="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: 'Invalid email address',
},
})}
placeholder="Email"
type="email"
/>
{errors.email && (
<FormErrorMessage>{errors.email.message}</FormErrorMessage>
)}
</FormControl>
<Button
bg="ui.main"
color="white"
_hover={{ opacity: 0.8 }}
type="submit"
isLoading={isSubmitting}
>
Continue
</Button>
</Container>
)
}
export default RecoverPassword

View File

@@ -0,0 +1,134 @@
import {
Button,
Container,
FormControl,
FormErrorMessage,
FormLabel,
Heading,
Input,
Text,
} from '@chakra-ui/react'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useMutation } from 'react-query'
import { ApiError, LoginService, NewPassword } from '../client'
import { isLoggedIn } from '../hooks/useAuth'
import useCustomToast from '../hooks/useCustomToast'
interface NewPasswordForm extends NewPassword {
confirm_password: string
}
export const Route = createFileRoute('/reset-password')({
component: ResetPassword,
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
to: '/',
})
}
},
})
function ResetPassword() {
const {
register,
handleSubmit,
getValues,
formState: { errors },
} = useForm<NewPasswordForm>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
new_password: '',
},
})
const showToast = useCustomToast()
const resetPassword = async (data: NewPassword) => {
const token = new URLSearchParams(window.location.search).get('token')
await LoginService.resetPassword({
requestBody: { new_password: data.new_password, token: token! },
})
}
const mutation = useMutation(resetPassword, {
onSuccess: () => {
showToast('Success!', 'Password updated.', 'success')
},
onError: (err: ApiError) => {
const errDetail = err.body.detail
showToast('Something went wrong.', `${errDetail}`, 'error')
},
})
const onSubmit: SubmitHandler<NewPasswordForm> = async (data) => {
mutation.mutate(data)
}
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}>
Reset Password
</Heading>
<Text textAlign="center">
Please enter your new password and confirm it to reset your password.
</Text>
<FormControl mt={4} isInvalid={!!errors.new_password}>
<FormLabel htmlFor="password">Set Password</FormLabel>
<Input
id="password"
{...register('new_password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters',
},
})}
placeholder="Password"
type="password"
/>
{errors.new_password && (
<FormErrorMessage>{errors.new_password.message}</FormErrorMessage>
)}
</FormControl>
<FormControl mt={4} isInvalid={!!errors.confirm_password}>
<FormLabel htmlFor="confirm_password">Confirm Password</FormLabel>
<Input
id="confirm_password"
{...register('confirm_password', {
required: 'Please confirm your password',
validate: (value) =>
value === getValues().new_password ||
'The passwords do not match',
})}
placeholder="Password"
type="password"
/>
{errors.confirm_password && (
<FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>
)}
</FormControl>
<Button
bg="ui.main"
color="white"
_hover={{ opacity: 0.8 }}
type="submit"
>
Reset Password
</Button>
</Container>
)
}
export default ResetPassword