diff --git a/src/App.tsx b/src/App.tsx index c2462a8..6bc14f4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,13 +6,35 @@ import {CssBaseline, ThemeProvider} from "@mui/material" import {queryClient} from "~/constants/react-query" import {lightTheme} from "~/constants/themes" -import LoadCriticalContent from "~/LoadCriticalContent" +import {getServerSettings} from "~/apis" import RootRoute from "~/routes/Root" +import SignupRoute from "~/routes/SignupRoute" +import SingleElementRoute from "~/routes/SingleElementRoute" +import VerifyEmailRoute from "~/routes/VerifyEmailRoute" const router = createBrowserRouter([ { path: "/", element: , + errorElement:
, + children: [ + { + path: "/", + element: , + children: [ + { + loader: getServerSettings, + path: "/verify-email", + element: , + }, + { + loader: getServerSettings, + path: "/signup", + element: , + }, + ], + }, + ], }, ]) @@ -22,9 +44,7 @@ export default function App(): ReactElement { - - - + diff --git a/src/LoadCriticalContent.tsx b/src/LoadCriticalContent.tsx deleted file mode 100644 index 212cde6..0000000 --- a/src/LoadCriticalContent.tsx +++ /dev/null @@ -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( - ["server-settings"], - async () => { - const settings = await getServerSettings() - - return { - serverSettings: settings, - } - }, - { - staleTime: inSeconds.days(20), - }, - ) - - if (!data) { - return - } - - return ( - - {children} - - ) -} diff --git a/src/LoadingScreen.tsx b/src/LoadingScreen.tsx index 389c5fb..a141400 100644 --- a/src/LoadingScreen.tsx +++ b/src/LoadingScreen.tsx @@ -2,14 +2,10 @@ import React, {ReactElement} from "react" import {Typography} from "@mui/material" -import {SingleElementWrapper} from "~/components" - export default function LoadingScreen(): ReactElement { return ( - - - Loading... - - + + Loading... + ) } diff --git a/src/ServerSettingsContext.ts b/src/ServerSettingsContext.ts deleted file mode 100644 index 6520a04..0000000 --- a/src/ServerSettingsContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {createContext} from "react" - -import {ServerSettings} from "~/types" - -export interface ServerSettingsContextType { - serverSettings: ServerSettings -} - -// @ts-ignore -const ServerSettingsContext = createContext() - -export default ServerSettingsContext diff --git a/src/apis/index.ts b/src/apis/index.ts index 36b6f00..8bba37a 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -4,3 +4,5 @@ export * from "./get-server-settings" export {default as getServerSettings} from "./get-server-settings" export * from "./signup" export {default as signup} from "./signup" +export * from "./validate-email" +export {default as validateEmail} from "./validate-email" diff --git a/src/apis/validate-token.ts b/src/apis/validate-email.ts similarity index 66% rename from src/apis/validate-token.ts rename to src/apis/validate-email.ts index 36e15e6..e73a345 100644 --- a/src/apis/validate-token.ts +++ b/src/apis/validate-email.ts @@ -2,15 +2,15 @@ import axios from "axios" import {AuthenticationDetails} from "~/types" -export interface ValidateTokenData { +export interface ValidateEmailData { email: string token: string } -export default async function validateToken({ +export default async function validateEmail({ email, token, -}: ValidateTokenData): Promise { +}: ValidateEmailData): Promise { const {data} = await axios.post( `${import.meta.env.VITE_SERVER_BASE_URL}/auth/verify-email`, { diff --git a/src/components/SingleElementWrapper.tsx b/src/components/SingleElementWrapper.tsx deleted file mode 100644 index 4488dd2..0000000 --- a/src/components/SingleElementWrapper.tsx +++ /dev/null @@ -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 ( - - {children} - - ) -} diff --git a/src/components/index.ts b/src/components/index.ts index d6b1efc..f8b5312 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,12 +1,10 @@ export * from "./MultiStepForm" -export { default as MultiStepForm } from "./MultiStepForm" +export {default as MultiStepForm} from "./MultiStepForm" export * from "./MultiStepFormElement" -export { default as MultiStepFormElement } from "./MultiStepFormElement" +export {default as MultiStepFormElement} from "./MultiStepFormElement" export * from "./OpenMailButton" -export { default as OpenMailButton } from "./OpenMailButton" +export {default as OpenMailButton} from "./OpenMailButton" export * from "./PasswordField" -export { default as PasswordField } from "./PasswordField" +export {default as PasswordField} from "./PasswordField" export * from "./SimpleForm" -export { default as SimpleForm } from "./SimpleForm" -export * from "./SingleElementWrapper" -export { default as SingleElementWrapper } from "./SingleElementWrapper" +export {default as SimpleForm} from "./SimpleForm" diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 91eaee6..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./use-server-settings" -export {default as useServerSettings} from "./use-server-settings" diff --git a/src/hooks/use-server-settings.ts b/src/hooks/use-server-settings.ts deleted file mode 100644 index 3e2b7d0..0000000 --- a/src/hooks/use-server-settings.ts +++ /dev/null @@ -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 -} diff --git a/src/route-widgets/root/EmailForm/EmailForm.tsx b/src/route-widgets/root/EmailForm/EmailForm.tsx index a434753..b36dbdb 100644 --- a/src/route-widgets/root/EmailForm/EmailForm.tsx +++ b/src/route-widgets/root/EmailForm/EmailForm.tsx @@ -7,17 +7,20 @@ import {InputAdornment, TextField} from "@mui/material" import {MultiStepFormElement, SimpleForm} from "~/components" import {signup} from "~/apis" import {handleErrors} from "~/utils" -import {useServerSettings} from "~/hooks" +import {ServerSettings} from "~/types" import DetectEmailAutofillService from "./DetectEmailAutofillService" import useSchema, {Form} from "./use-schema" interface EmailFormProps { + serverSettings: ServerSettings onSignUp: (email: string) => void } -export default function EmailForm({onSignUp}: EmailFormProps): ReactElement { - const serverSettings = useServerSettings() +export default function EmailForm({ + onSignUp, + serverSettings, +}: EmailFormProps): ReactElement { const schema = useSchema(serverSettings) const formik = useFormik
({ validationSchema: schema, diff --git a/src/routes/Root.tsx b/src/routes/Root.tsx index 8e0bf4f..1dd294a 100644 --- a/src/routes/Root.tsx +++ b/src/routes/Root.tsx @@ -1,30 +1,6 @@ -import {useLocalStorage} from "react-use" +import {Outlet} from "react-router-dom" 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 { - const [email, setEmail] = useLocalStorage( - "signup-form-state-email", - "", - ) - - const index = email ? 1 : 0 - - return ( - - , - , - ]} - index={index} - /> - - ) + return } diff --git a/src/routes/SignupRoute.tsx b/src/routes/SignupRoute.tsx new file mode 100644 index 0000000..7808767 --- /dev/null +++ b/src/routes/SignupRoute.tsx @@ -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( + "signup-form-state-email", + "", + ) + + const index = email ? 1 : 0 + + return ( + , + , + ]} + index={index} + /> + ) +} diff --git a/src/routes/SingleElementRoute.tsx b/src/routes/SingleElementRoute.tsx new file mode 100644 index 0000000..bbf5d69 --- /dev/null +++ b/src/routes/SingleElementRoute.tsx @@ -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 ( + +
+ + + + + + + + + + + ) +} diff --git a/src/routes/VerifyEmailRoute.tsx b/src/routes/VerifyEmailRoute.tsx new file mode 100644 index 0000000..093e6ad --- /dev/null +++ b/src/routes/VerifyEmailRoute.tsx @@ -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 ( + + + + + Verify your email + + + {loading ? ( + + + Verifying your email... + + + ) : ( + <> + + + + + + Sorry, but this token is invalid. + + + + )} + + + ) +}