improvements; developed verify email page

This commit is contained in:
Myzel394 2022-10-14 21:11:23 +02:00
parent f35a07efc1
commit c8c774de54
15 changed files with 222 additions and 145 deletions

View File

@ -6,13 +6,35 @@ import {CssBaseline, ThemeProvider} from "@mui/material"
import {queryClient} from "~/constants/react-query" import {queryClient} from "~/constants/react-query"
import {lightTheme} from "~/constants/themes" import {lightTheme} from "~/constants/themes"
import LoadCriticalContent from "~/LoadCriticalContent" import {getServerSettings} from "~/apis"
import RootRoute from "~/routes/Root" import RootRoute from "~/routes/Root"
import SignupRoute from "~/routes/SignupRoute"
import SingleElementRoute from "~/routes/SingleElementRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",
element: <RootRoute />, element: <RootRoute />,
errorElement: <div></div>,
children: [
{
path: "/",
element: <SingleElementRoute />,
children: [
{
loader: getServerSettings,
path: "/verify-email",
element: <VerifyEmailRoute />,
},
{
loader: getServerSettings,
path: "/signup",
element: <SignupRoute />,
},
],
},
],
}, },
]) ])
@ -22,9 +44,7 @@ export default function App(): ReactElement {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<CssBaseline /> <CssBaseline />
<LoadCriticalContent>
<RouterProvider router={router} /> <RouterProvider router={router} />
</LoadCriticalContent>
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>
</React.StrictMode> </React.StrictMode>

View File

@ -1,44 +0,0 @@
import * as inSeconds from "in-seconds"
import {AxiosError} from "axios"
import React, {ReactElement} from "react"
import {useQuery} from "@tanstack/react-query"
import {getServerSettings} from "~/apis"
import LoadingScreen from "~/LoadingScreen"
import ServerSettingsContext, {
ServerSettingsContextType,
} from "./ServerSettingsContext"
export interface LoadCriticalContentProps {
children: ReactElement
}
export default function LoadCriticalContent({
children,
}: LoadCriticalContentProps): ReactElement {
const {data} = useQuery<ServerSettingsContextType, AxiosError>(
["server-settings"],
async () => {
const settings = await getServerSettings()
return {
serverSettings: settings,
}
},
{
staleTime: inSeconds.days(20),
},
)
if (!data) {
return <LoadingScreen />
}
return (
<ServerSettingsContext.Provider value={data}>
{children}
</ServerSettingsContext.Provider>
)
}

View File

@ -2,14 +2,10 @@ import React, {ReactElement} from "react"
import {Typography} from "@mui/material" import {Typography} from "@mui/material"
import {SingleElementWrapper} from "~/components"
export default function LoadingScreen(): ReactElement { export default function LoadingScreen(): ReactElement {
return ( return (
<SingleElementWrapper>
<Typography variant="caption" component="p"> <Typography variant="caption" component="p">
Loading... Loading...
</Typography> </Typography>
</SingleElementWrapper>
) )
} }

View File

@ -1,12 +0,0 @@
import {createContext} from "react"
import {ServerSettings} from "~/types"
export interface ServerSettingsContextType {
serverSettings: ServerSettings
}
// @ts-ignore
const ServerSettingsContext = createContext<ServerSettingsContextType>()
export default ServerSettingsContext

View File

@ -4,3 +4,5 @@ export * from "./get-server-settings"
export {default as getServerSettings} from "./get-server-settings" export {default as getServerSettings} from "./get-server-settings"
export * from "./signup" export * from "./signup"
export {default as signup} from "./signup" export {default as signup} from "./signup"
export * from "./validate-email"
export {default as validateEmail} from "./validate-email"

View File

@ -2,15 +2,15 @@ import axios from "axios"
import {AuthenticationDetails} from "~/types" import {AuthenticationDetails} from "~/types"
export interface ValidateTokenData { export interface ValidateEmailData {
email: string email: string
token: string token: string
} }
export default async function validateToken({ export default async function validateEmail({
email, email,
token, token,
}: ValidateTokenData): Promise<AuthenticationDetails> { }: ValidateEmailData): Promise<AuthenticationDetails> {
const {data} = await axios.post( const {data} = await axios.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/verify-email`, `${import.meta.env.VITE_SERVER_BASE_URL}/auth/verify-email`,
{ {

View File

@ -1,27 +0,0 @@
import React, {ReactElement} from "react"
import {Box} from "@mui/material"
export interface SingleElementWrapperProps {
children: ReactElement
}
const style = {
minHeight: "100vh",
}
export default function SingleElementWrapper({
children,
}: SingleElementWrapperProps): ReactElement {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
sx={style}
>
{children}
</Box>
)
}

View File

@ -8,5 +8,3 @@ export * from "./PasswordField"
export {default as PasswordField} from "./PasswordField" export {default as PasswordField} from "./PasswordField"
export * from "./SimpleForm" export * from "./SimpleForm"
export {default as SimpleForm} from "./SimpleForm" export {default as SimpleForm} from "./SimpleForm"
export * from "./SingleElementWrapper"
export { default as SingleElementWrapper } from "./SingleElementWrapper"

View File

@ -1,2 +0,0 @@
export * from "./use-server-settings"
export {default as useServerSettings} from "./use-server-settings"

View File

@ -1,10 +0,0 @@
import {useContext} from "react"
import {ServerSettings} from "~/types"
import ServerSettingsContext from "~/ServerSettingsContext"
export default function useServerSettings(): ServerSettings {
const {serverSettings} = useContext(ServerSettingsContext)
return serverSettings
}

View File

@ -7,17 +7,20 @@ import {InputAdornment, TextField} from "@mui/material"
import {MultiStepFormElement, SimpleForm} from "~/components" import {MultiStepFormElement, SimpleForm} from "~/components"
import {signup} from "~/apis" import {signup} from "~/apis"
import {handleErrors} from "~/utils" import {handleErrors} from "~/utils"
import {useServerSettings} from "~/hooks" import {ServerSettings} from "~/types"
import DetectEmailAutofillService from "./DetectEmailAutofillService" import DetectEmailAutofillService from "./DetectEmailAutofillService"
import useSchema, {Form} from "./use-schema" import useSchema, {Form} from "./use-schema"
interface EmailFormProps { interface EmailFormProps {
serverSettings: ServerSettings
onSignUp: (email: string) => void onSignUp: (email: string) => void
} }
export default function EmailForm({onSignUp}: EmailFormProps): ReactElement { export default function EmailForm({
const serverSettings = useServerSettings() onSignUp,
serverSettings,
}: EmailFormProps): ReactElement {
const schema = useSchema(serverSettings) const schema = useSchema(serverSettings)
const formik = useFormik<Form>({ const formik = useFormik<Form>({
validationSchema: schema, validationSchema: schema,

View File

@ -1,30 +1,6 @@
import {useLocalStorage} from "react-use" import {Outlet} from "react-router-dom"
import React, {ReactElement} from "react" import React, {ReactElement} from "react"
import {MultiStepForm, SingleElementWrapper} from "~/components"
import EmailForm from "~/route-widgets/root/EmailForm"
import YouGotMail from "~/route-widgets/root/YouGotMail"
export default function RootRoute(): ReactElement { export default function RootRoute(): ReactElement {
const [email, setEmail] = useLocalStorage<string>( return <Outlet />
"signup-form-state-email",
"",
)
const index = email ? 1 : 0
return (
<SingleElementWrapper>
<MultiStepForm
steps={[
<EmailForm onSignUp={setEmail} key="email" />,
<YouGotMail
domain={(email || "").split("@")[1]}
key="you_got_mail"
/>,
]}
index={index}
/>
</SingleElementWrapper>
)
} }

View File

@ -0,0 +1,35 @@
import {ReactElement} from "react"
import {useLocalStorage} from "react-use"
import {useLoaderData} from "react-router-dom"
import {MultiStepForm} from "~/components"
import {ServerSettings} from "~/types"
import EmailForm from "~/route-widgets/root/EmailForm"
import YouGotMail from "~/route-widgets/root/YouGotMail"
export default function SignupRoute(): ReactElement {
const serverSettings = useLoaderData() as ServerSettings
const [email, setEmail] = useLocalStorage<string>(
"signup-form-state-email",
"",
)
const index = email ? 1 : 0
return (
<MultiStepForm
steps={[
<EmailForm
serverSettings={serverSettings}
onSignUp={setEmail}
key="email"
/>,
<YouGotMail
domain={(email || "").split("@")[1]}
key="you_got_mail"
/>,
]}
index={index}
/>
)
}

View File

@ -0,0 +1,49 @@
import {ReactElement} from "react"
import {Link as RouterLink, Outlet} from "react-router-dom"
import {MdAdd, MdLogin} from "react-icons/md"
import {Box, Button, Grid} from "@mui/material"
export default function SingleElementRoute(): ReactElement {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="space-between"
height="100vh"
>
<div />
<Outlet />
<Grid
container
spacing={2}
justifyContent="center"
marginBottom={2}
>
<Grid item>
<Button
component={RouterLink}
to="/signup"
color="inherit"
size="small"
startIcon={<MdAdd />}
>
Sign Up
</Button>
</Grid>
<Grid item>
<Button
component={RouterLink}
to="/login"
color="inherit"
size="small"
startIcon={<MdLogin />}
>
Login
</Button>
</Grid>
</Grid>
</Box>
)
}

View File

@ -0,0 +1,93 @@
import * as yup from "yup"
import {useLoaderData, useParams} from "react-router-dom"
import {useAsync} from "react-use"
import {MdCancel} from "react-icons/md"
import React, {ReactElement} from "react"
import {Grid, Paper, Typography, useTheme} from "@mui/material"
import {ServerSettings} from "~/types"
import {validateEmail} from "~/apis"
const emailSchema = yup.string().email()
export default function VerifyEmailRoute(): ReactElement {
const theme = useTheme()
const {email, token} = useParams<{
email: string
token: string
}>()
const serverSettings = useLoaderData() as ServerSettings
const tokenSchema = yup
.string()
.length(serverSettings.email_verification_length)
.test("token", "Invalid token", token => {
if (!token) {
return false
}
// Check token only contains chars from `serverSettings.email_verification_chars`
const chars = serverSettings.email_verification_chars.split("")
return token.split("").every(chars.includes)
})
const {error, loading} = useAsync(async () => {
await emailSchema.validate(email)
await tokenSchema.validate(token)
await validateEmail({
token: token as string,
email: email as string,
})
return true
}, [email, token])
return (
<Paper>
<Grid
container
spacing={4}
padding={4}
alignItems="center"
justifyContent="center"
flexDirection="column"
>
<Grid item>
<Typography variant="h5" component="h1" align="center">
Verify your email
</Typography>
</Grid>
{loading ? (
<Grid item>
<Typography
variant="subtitle1"
component="p"
align="center"
>
Verifying your email...
</Typography>
</Grid>
) : (
<>
<Grid item>
<MdCancel
size={100}
color={theme.palette.error.main}
/>
</Grid>
<Grid item>
<Typography
variant="subtitle1"
component="p"
align="center"
>
Sorry, but this token is invalid.
</Typography>
</Grid>
</>
)}
</Grid>
</Paper>
)
}