diff --git a/.gitignore b/.gitignore
index c7b36789..d311079b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,3 +36,4 @@ yarn-error.log*
 /backend/static
 *.sqlite3
 /frontend/build
+node_modules
diff --git a/frontend/package.json b/frontend/package.json
index 80255680..63593101 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,14 +28,16 @@
     "@mui/icons-material": "^5.11.11",
     "@mui/material": "^5.11.12",
     "@reduxjs/toolkit": "^1.9.3",
-    "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v1.5.5",
+    "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v1.7.1",
+    "formik": "^2.2.9",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-redux": "^8.0.5",
     "react-router-dom": "^6.8.2",
     "react-scripts": "^5.0.1",
     "sass": "^1.58.3",
-    "web-vitals": "^2.1.4"
+    "web-vitals": "^2.1.4",
+    "yup": "^1.1.1"
   },
   "devDependencies": {
     "@cypress/code-coverage": "^3.10.0",
@@ -60,4 +62,4 @@
   "nyc": {
     "exclude": []
   }
-}
+}
\ No newline at end of file
diff --git a/frontend/public/handlebars_template.html b/frontend/public/handlebars_template.html
index a6826c3a..07b7b26a 100644
--- a/frontend/public/handlebars_template.html
+++ b/frontend/public/handlebars_template.html
@@ -3,6 +3,7 @@
 
 <head>
   <title>Code for Life</title>
+  <base href="/">
   <link rel="icon" type="image/x-icon" href="{% static '{{ faviconUrl }}' %}">
   <style>
     html,
diff --git a/frontend/public/index.html b/frontend/public/index.html
index cd62b183..83523cc4 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -20,6 +20,7 @@
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
   <title>Code for Life</title>
+  <base href="/">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
   <style>
     html,
diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx
index 2ef71ae1..a0c25a8e 100644
--- a/frontend/src/app/router.tsx
+++ b/frontend/src/app/router.tsx
@@ -15,12 +15,14 @@ import Newsletter from '../pages/newsletter/Newsletter';
 import Forbidden from '../pages/forbidden/Forbidden';
 import PageNotFound from '../pages/pageNotFound/PageNotFound';
 import InternalServerError from '../pages/internalServerError/InternalServerError';
+import EmailVerification from 'pages/emailVerification/EmailVerification';
 
 export const paths = {
   home: '/',
   teachers: '/teachers',
   students: '/students',
   register: '/register',
+  emailVerification: '/register/email-verification',
   aboutUs: '/about-us',
   codingClubs: '/coding-clubs',
   getInvolved: '/get-involved',
@@ -90,6 +92,10 @@ const router = createBrowserRouter([
   {
     path: paths.internalServerError,
     element: <InternalServerError />
+  },
+  {
+    path: paths.emailVerification,
+    element: <EmailVerification />
   }
 ]);
 
diff --git a/frontend/src/app/theme.ts b/frontend/src/app/theme.ts
index cec24ab6..4e4ccb4b 100644
--- a/frontend/src/app/theme.ts
+++ b/frontend/src/app/theme.ts
@@ -2,51 +2,59 @@ import { createTheme, ThemeOptions } from '@mui/material/styles';
 
 import { theme as cflTheme } from 'codeforlife';
 
+// Styles shared by all form components.
+export const formStyleOverrides = {
+  fontFamily: '"Inter"',
+  fontSize: '14px',
+  fontWeight: 500,
+  marginBottom: '12px'
+};
+
 const options: ThemeOptions = {
   typography: {
     h1: {
       fontFamily: '"SpaceGrotesk"',
       fontWeight: 500,
-      marginBottom: 24
+      marginBottom: '24px'
     },
     h2: {
       fontFamily: '"SpaceGrotesk"',
       fontWeight: 500,
-      marginBottom: 22
+      marginBottom: '22px'
     },
     h3: {
       fontFamily: '"SpaceGrotesk"',
       fontWeight: 500,
-      marginBottom: 20
+      marginBottom: '20px'
     },
     h4: {
       fontFamily: '"SpaceGrotesk"',
       fontWeight: 500,
-      marginBottom: 18
+      marginBottom: '18px'
     },
     h5: {
       fontFamily: '"SpaceGrotesk"',
       fontWeight: 500,
-      marginBottom: 16
+      marginBottom: '16px'
     },
     h6: {
       fontFamily: '"SpaceGrotesk"',
       fontWeight: 500,
-      marginBottom: 14
+      marginBottom: '14px'
     },
     body1: {
       fontFamily: '"Inter"',
-      fontSize: 18,
-      marginBottom: 16
+      fontSize: '18px',
+      marginBottom: '16px'
     },
     body2: {
       fontFamily: '"Inter"',
-      fontSize: 16,
-      marginBottom: 14
+      fontSize: '16px',
+      marginBottom: '14px'
     },
     button: {
       fontFamily: '"Inter"',
-      fontSize: 14,
+      fontSize: '14px',
       fontWeight: 550
     }
   },
@@ -80,13 +88,17 @@ const options: ThemeOptions = {
     MuiFormControlLabel: {
       defaultProps: {
         sx: {
-          '.MuiTypography-root': { m: 0 }
+          '.MuiTypography-root': {
+            ...formStyleOverrides,
+            m: 0
+          }
         }
       }
     },
     MuiInputBase: {
       styleOverrides: {
         root: {
+          background: 'white',
           margin: 0
         }
       }
@@ -114,6 +126,22 @@ const options: ThemeOptions = {
           padding: 0
         })
       }
+    },
+    MuiFormHelperText: {
+      styleOverrides: {
+        root: {
+          ...formStyleOverrides,
+          marginTop: 4,
+          marginLeft: 4
+        }
+      }
+    },
+    MuiCheckbox: {
+      styleOverrides: {
+        root: {
+          color: 'white'
+        }
+      }
     }
     // MuiToolbar: {
     //   styleOverrides: {
diff --git a/frontend/src/components/DatePicker.tsx b/frontend/src/components/DatePicker.tsx
new file mode 100644
index 00000000..66b8992e
--- /dev/null
+++ b/frontend/src/components/DatePicker.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import {
+  Unstable_Grid2 as Grid,
+  Select,
+  SelectProps,
+  MenuItem,
+  FormHelperText,
+  FormHelperTextProps
+} from '@mui/material';
+
+import { formStyleOverrides } from '../app/theme';
+
+const monthOptions = [
+  'January',
+  'February',
+  'March',
+  'April',
+  'May',
+  'June',
+  'July',
+  'August',
+  'September',
+  'October',
+  'November',
+  'December'
+];
+
+export interface DatePickerProps {
+  defaultsToToday?: boolean,
+  previousYears?: number,
+  helperText?: string,
+  formHelperTextProps?: FormHelperTextProps,
+  onChange: (date: Date | undefined) => void
+}
+
+const DatePicker: React.FC<DatePickerProps> = ({
+  defaultsToToday = false,
+  previousYears = 150,
+  helperText,
+  formHelperTextProps,
+  onChange
+}) => {
+  const now = new Date();
+
+  const [date, setDate] = React.useState((defaultsToToday)
+    ? { day: now.getDay(), month: now.getMonth(), year: now.getFullYear() }
+    : { day: 0, month: 0, year: 0 }
+  );
+
+  const dayIsDisabled = date.month === 0 || date.year === 0;
+
+  if ([date.day, date.month, date.year].every(n => n !== 0)) {
+    onChange(new Date(date.year, date.month, date.day));
+  }
+
+  function getLastDay(month: number, year: number): number {
+    return new Date(year, month, 0).getDate();
+  }
+
+  function _onChange(
+    key: 'day' | 'month' | 'year',
+    value: number | string
+  ): void {
+    const newDate = { ...date };
+    newDate[key] = Number(value);
+
+    if (key !== 'day' &&
+      !dayIsDisabled &&
+      newDate.day > getLastDay(newDate.month, newDate.year)
+    ) {
+      newDate.day = 0;
+      onChange(undefined);
+    }
+
+    setDate(newDate);
+  }
+
+  function getDayOptions(): number[] {
+    return Array
+      .from(Array(getLastDay(date.month, date.year)).keys())
+      .map(day => day + 1);
+  }
+
+  const yearOptions = Array
+    .from(Array(previousYears).keys())
+    .map(year => year + 1 - previousYears + now.getFullYear())
+    .reverse();
+
+  const commonSelectProps: SelectProps<number> = {
+    style: { backgroundColor: 'white', width: '100%' },
+    size: 'small'
+  };
+
+  return (
+    <Grid
+      container
+      columnSpacing={2}
+      marginBottom={formStyleOverrides.marginBottom}
+    >
+      {helperText !== undefined && helperText !== '' &&
+        <Grid xs={12}>
+          <FormHelperText {...formHelperTextProps}>
+            {helperText}
+          </FormHelperText>
+        </Grid>
+      }
+      <Grid xs={4}>
+        <Select
+          id='select-day'
+          value={date.day}
+          onChange={(event) => { _onChange('day', event.target.value); }}
+          disabled={dayIsDisabled}
+          {...commonSelectProps}
+        >
+          <MenuItem className='header' value={0}>
+            Day
+          </MenuItem>
+          {!dayIsDisabled && getDayOptions().map((day) =>
+            <MenuItem key={`day-${day}`} value={day} dense>
+              {day}
+            </MenuItem>
+          )}
+        </Select>
+      </Grid>
+      <Grid xs={4}>
+        <Select
+          id='select-month'
+          value={date.month}
+          onChange={(event) => { _onChange('month', event.target.value); }}
+          {...commonSelectProps}
+        >
+          <MenuItem className='header' value={0}>
+            Month
+          </MenuItem>
+          {monthOptions.map((month, index) =>
+            <MenuItem key={`month-${month}`} value={index + 1} dense>
+              {month}
+            </MenuItem>
+          )}
+        </Select>
+      </Grid>
+      <Grid xs={4}>
+        <Select
+          id='select-year'
+          value={date.year}
+          onChange={(event) => { _onChange('year', event.target.value); }}
+          {...commonSelectProps}
+        >
+          <MenuItem className='header' value={0}>
+            Year
+          </MenuItem>
+          {yearOptions.map((year) =>
+            <MenuItem key={`year-${year}`} value={year} dense>
+              {year}
+            </MenuItem>
+          )}
+        </Select>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default DatePicker;
diff --git a/frontend/src/components/PageSection.tsx b/frontend/src/components/PageSection.tsx
index 86fecd9e..ba6bb003 100644
--- a/frontend/src/components/PageSection.tsx
+++ b/frontend/src/components/PageSection.tsx
@@ -14,10 +14,17 @@ export interface PageSectionProps extends Pick<Grid2Props, (
   py?: boolean
   background?: string
   className?: string
+  maxWidth?: Breakpoint
 }
 
 const PageSection: React.FC<PageSectionProps> = ({
-  bgcolor, children, px = true, py = true, background, className
+  bgcolor,
+  children,
+  px = true,
+  py = true,
+  background,
+  className,
+  maxWidth = process.env.REACT_APP_CONTAINER_MAX_WIDTH as Breakpoint
 }) => {
   return <>
     <Grid
@@ -27,7 +34,7 @@ const PageSection: React.FC<PageSectionProps> = ({
       sx={{ background, bgcolor }}
     >
       <Container
-        maxWidth={process.env.REACT_APP_CONTAINER_MAX_WIDTH as Breakpoint}
+        maxWidth={maxWidth}
         className={className}
       >
         {children}
diff --git a/frontend/src/components/formik/CflCheckboxField.tsx b/frontend/src/components/formik/CflCheckboxField.tsx
new file mode 100644
index 00000000..7f1627ee
--- /dev/null
+++ b/frontend/src/components/formik/CflCheckboxField.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import {
+  FormControlLabel,
+  FormControlLabelProps,
+  Checkbox,
+  CheckboxProps,
+  IconProps
+} from '@mui/material';
+import {
+  Error as ErrorIcon
+} from '@mui/icons-material';
+
+import { formStyleOverrides } from '../../app/theme';
+import CflField, { CflFieldProps } from './CflField';
+
+export type CflCheckboxFieldProps = (
+  Omit<CflFieldProps, (
+    'type' |
+    'as' |
+    'asProps' |
+    'component' |
+    'render' |
+    'children' |
+    'errorIconProps' |
+    'errorMessageProps' |
+    'stackProps'
+  )> &
+  CheckboxProps & {
+    errorIconProps?: Omit<IconProps, 'children'>,
+    formControlLabelProps: Omit<FormControlLabelProps, 'control'>
+  }
+);
+
+const CflCheckboxField: React.FC<CflCheckboxFieldProps> = ({
+  name,
+  tooltipProps,
+  errorIconProps = { style: { color: 'white' } },
+  formControlLabelProps,
+  ...checkboxProps
+}) => {
+  return (
+    <CflField
+      name={name}
+      type='checkbox'
+      // @ts-expect-error prematurely complains about missing props.
+      as={FormControlLabel}
+      asProps={{
+        control: <Checkbox {...checkboxProps} />,
+        ...formControlLabelProps
+      }}
+      stackProps={{
+        direction: 'row',
+        style: {
+          marginBottom: formStyleOverrides.marginBottom
+        }
+      }}
+      tooltipProps={tooltipProps}
+      errorIconProps={{
+        style: {
+          margin: 'auto 12px auto 0px',
+          ...errorIconProps.style
+        },
+        children: <ErrorIcon />,
+        ...errorIconProps
+      }}
+    />
+  );
+};
+
+export default CflCheckboxField;
diff --git a/frontend/src/components/formik/CflField.tsx b/frontend/src/components/formik/CflField.tsx
new file mode 100644
index 00000000..9d12ae82
--- /dev/null
+++ b/frontend/src/components/formik/CflField.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import {
+  Tooltip,
+  TooltipProps,
+  Stack,
+  StackProps,
+  Icon,
+  IconProps
+} from '@mui/material';
+import {
+  ErrorOutline as ErrorOutlineIcon
+} from '@mui/icons-material';
+import {
+  Field,
+  FieldConfig,
+  ErrorMessage,
+  ErrorMessageProps
+} from 'formik';
+
+export interface CflFieldProps extends FieldConfig<any> {
+  stackProps?: StackProps,
+  tooltipProps?: Omit<TooltipProps, 'title' | 'children'>,
+  errorIconProps?: IconProps
+  errorMessageProps?: Omit<ErrorMessageProps, (
+    'name' |
+    'children'
+  )> & {
+    afterField?: boolean
+  },
+  asProps?: Record<string, any>
+}
+
+const CflField: React.FC<CflFieldProps> = ({
+  name,
+  stackProps,
+  tooltipProps,
+  errorIconProps = { color: 'error' },
+  errorMessageProps = {
+    afterField: true,
+    render: undefined,
+    children: undefined
+  },
+  asProps,
+  ...otherFieldProps
+}) => {
+  let {
+    afterField,
+    render,
+    ...otherErrorMessageProps
+  } = errorMessageProps;
+
+  if (render === undefined) {
+    const {
+      children = <ErrorOutlineIcon />,
+      ...otherErrorIconProps
+    } = errorIconProps;
+
+    render = (errorMessage: string) => (
+      <Tooltip title={errorMessage} {...tooltipProps}>
+        <Icon {...otherErrorIconProps}>
+          {children}
+        </Icon>
+      </Tooltip>
+    );
+  }
+
+  const errorMessage = (
+    <ErrorMessage
+      name={name}
+      render={render}
+      {...otherErrorMessageProps}
+    />
+  );
+
+  return (
+    <Stack {...stackProps}>
+      {!afterField && errorMessage}
+      <Field name={name} {...otherFieldProps} {...asProps} />
+      {afterField && errorMessage}
+    </Stack>
+  );
+};
+
+export default CflField;
diff --git a/frontend/src/components/formik/CflPasswordFields.tsx b/frontend/src/components/formik/CflPasswordFields.tsx
new file mode 100644
index 00000000..4203018f
--- /dev/null
+++ b/frontend/src/components/formik/CflPasswordFields.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import {
+  Typography,
+  Stack,
+  InputAdornment
+} from '@mui/material';
+import {
+  Circle as CircleIcon,
+  Security as SecurityIcon
+} from '@mui/icons-material';
+
+import CflTextField, { CflTextFieldProps } from './CflTextField';
+
+export function isStrongPassword(
+  password: string,
+  { forTeacher }: { forTeacher: boolean }
+): boolean {
+  return (forTeacher)
+    ? (password.length >= 10 &&
+      !(
+        password.search(/[A-Z]/) === -1 ||
+        password.search(/[a-z]/) === -1 ||
+        password.search(/[0-9]/) === -1 ||
+        password.search(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/) === -1
+      ))
+    : (password.length >= 8 &&
+      !(
+        password.search(/[A-Z]/) === -1 ||
+        password.search(/[a-z]/) === -1 ||
+        password.search(/[0-9]/) === -1
+      ));
+}
+
+interface CflPasswordFieldProps extends Omit<CflTextFieldProps, (
+  'type' |
+  'onKeyUp' |
+  'InputProps'
+)> { }
+
+export interface CflPasswordFieldsProps
+  extends Omit<CflPasswordFieldProps, 'name'> {
+  forTeacher: boolean,
+  passwordFieldProps?: CflPasswordFieldProps,
+  repeatPasswordFieldProps?: CflPasswordFieldProps
+}
+
+const CflPasswordFields: React.FC<CflPasswordFieldsProps> = ({
+  forTeacher,
+  passwordFieldProps = {
+    name: 'password',
+    placeholder: 'Password',
+    helperText: 'Enter a password'
+  },
+  repeatPasswordFieldProps = {
+    name: 'repeatPassword',
+    placeholder: 'Repeat password',
+    helperText: 'Repeat password'
+  },
+  ...commonPasswordFieldProps
+}) => {
+  const [password, setPassword] = React.useState('');
+
+  // TODO: Load from central storage.
+  const mostUsed = ['Abcd1234', 'Password1', 'Qwerty123'];
+
+  let status: { name: string, color: string };
+  if (password === '') {
+    status = { name: 'No password!', color: '#FF0000' };
+  } else if (mostUsed.includes(password)) {
+    status = { name: 'Password too common!', color: '#DBA901' };
+  } else if (isStrongPassword(password, { forTeacher })) {
+    status = { name: 'Strong password!', color: '#088A08' };
+  } else {
+    status = { name: 'Password too weak!', color: '#DBA901' };
+  }
+
+  const inputProps: CflTextFieldProps['InputProps'] = {
+    endAdornment: (
+      <InputAdornment position='end'>
+        <SecurityIcon />
+      </InputAdornment>
+    )
+  };
+
+  return <>
+    <CflTextField
+      type='password'
+      InputProps={inputProps}
+      onKeyUp={(event) => {
+        setPassword((event.target as HTMLTextAreaElement).value);
+      }}
+      {...passwordFieldProps}
+      {...commonPasswordFieldProps}
+    />
+    <CflTextField
+      type='password'
+      InputProps={inputProps}
+      {...repeatPasswordFieldProps}
+      {...commonPasswordFieldProps}
+    />
+    <Stack direction='row' justifyContent='center'>
+      <CircleIcon
+        htmlColor={status.color}
+        stroke='white'
+        strokeWidth={1}
+      />
+      <Typography
+        fontSize={18}
+        fontWeight={500}
+        margin={0}
+      >
+        &nbsp;&nbsp;{status.name}
+      </Typography>
+    </Stack>
+  </>;
+};
+
+export default CflPasswordFields;
diff --git a/frontend/src/components/formik/CflTextField.tsx b/frontend/src/components/formik/CflTextField.tsx
new file mode 100644
index 00000000..946b49e5
--- /dev/null
+++ b/frontend/src/components/formik/CflTextField.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import {
+  TextField,
+  TextFieldProps,
+  InputAdornment,
+  Tooltip,
+  Icon
+} from '@mui/material';
+import {
+  ErrorOutline as ErrorOutlineIcon
+} from '@mui/icons-material';
+
+import CflField, { CflFieldProps } from './CflField';
+
+export type CflTextFieldProps = (
+  Omit<TextFieldProps, 'name'> &
+  Omit<CflFieldProps, (
+    'as' |
+    'asProps' |
+    'component' |
+    'render' |
+    'children' |
+    'errorMessageProps' |
+    'stackProps'
+  )>
+);
+
+const CflTextField: React.FC<CflTextFieldProps> = ({
+  name,
+  type,
+  tooltipProps,
+  errorIconProps = { color: 'error' },
+  InputProps = {},
+  onKeyUp,
+  ...otherTextFieldProps
+}) => {
+  const [errorMessage, setErrorMessage] = React.useState('');
+  const [valueChanged, setValueChanged] = React.useState(false);
+
+  const resetErrorMessage = (): void => {
+    if (valueChanged) {
+      setErrorMessage('');
+    } else {
+      setValueChanged(true);
+    }
+  };
+
+  let {
+    endAdornment,
+    ...otherInputProps
+  } = InputProps;
+
+  if (errorMessage !== '') {
+    endAdornment = (
+      <>
+        {endAdornment}
+        <InputAdornment position='end'>
+          <Tooltip title={errorMessage} {...tooltipProps}>
+            <Icon {...errorIconProps}>
+              <ErrorOutlineIcon />
+            </Icon>
+          </Tooltip>
+        </InputAdornment>
+      </>
+    );
+  }
+
+  if (onKeyUp === undefined) {
+    onKeyUp = resetErrorMessage;
+  } else {
+    const originalOnKeyUp = onKeyUp;
+    onKeyUp = (event) => {
+      originalOnKeyUp(event);
+      resetErrorMessage();
+    };
+  }
+
+  const textFieldProps: TextFieldProps = {
+    name,
+    type,
+    onKeyUp,
+    InputProps: {
+      endAdornment,
+      ...otherInputProps
+    },
+    ...otherTextFieldProps
+  };
+
+  return (
+    <CflField
+      name={name}
+      type={type}
+      as={TextField}
+      asProps={textFieldProps}
+      errorMessageProps={{
+        render: (errorMessage) => {
+          setErrorMessage(errorMessage);
+          setValueChanged(false);
+          return <></>;
+        }
+      }}
+    />
+  );
+};
+
+export default CflTextField;
diff --git a/frontend/src/images/paper_plane.png b/frontend/src/images/paper_plane.png
new file mode 100644
index 00000000..6324bdf7
Binary files /dev/null and b/frontend/src/images/paper_plane.png differ
diff --git a/frontend/src/images/sadface.png b/frontend/src/images/sadface.png
new file mode 100644
index 00000000..e9b86779
Binary files /dev/null and b/frontend/src/images/sadface.png differ
diff --git a/frontend/src/pages/emailVerification/EmailVerification.tsx b/frontend/src/pages/emailVerification/EmailVerification.tsx
new file mode 100644
index 00000000..49b00bfb
--- /dev/null
+++ b/frontend/src/pages/emailVerification/EmailVerification.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import {
+  getSearchParams,
+  stringToBoolean
+} from 'codeforlife/lib/esm/helpers';
+
+import SadFaceImg from '../../images/sadface.png';
+import PaperPlaneImg from '../../images/paper_plane.png';
+import { paths } from '../../app/router';
+import BasePage from '../../pages/BasePage';
+import PageSection from '../../components/PageSection';
+import Status from './Status';
+
+const EmailVerification: React.FC = () => {
+  const navigate = useNavigate();
+
+  const params = getSearchParams({
+    success: stringToBoolean,
+    forTeacher: stringToBoolean
+  });
+
+  React.useEffect(() => {
+    if (params === null) {
+      navigate(paths.internalServerError);
+    }
+  }, []);
+
+  return (
+    <BasePage>
+      <PageSection maxWidth='md' className='flex-center'>
+        {params !== null && (params.success
+          ? <Status
+            forTeacher={params.forTeacher}
+            header='We need to verify your email address'
+            body={[
+              'An email has been sent to the address you provided.',
+              'Please follow the link within the email to verify your details. This will expire in 1 hour.',
+              'If you don\'t receive the email within the next few minutes, check your spam folder.'
+            ]}
+            imageProps={{
+              alt: 'PaperPlane',
+              src: PaperPlaneImg
+            }}
+          />
+          : <Status
+            forTeacher={params.forTeacher}
+            header='Your email address verification failed'
+            body={[
+              'You used an invalid link, either you mistyped the URL or that link is expired.',
+              'When you next attempt to log in, you will be sent a new verification email.'
+            ]}
+            imageProps={{
+              alt: 'SadFace',
+              src: SadFaceImg
+            }}
+          />
+        )}
+      </PageSection>
+    </BasePage>
+  );
+};
+
+export default EmailVerification;
diff --git a/frontend/src/pages/emailVerification/Status.tsx b/frontend/src/pages/emailVerification/Status.tsx
new file mode 100644
index 00000000..dbfd2b6e
--- /dev/null
+++ b/frontend/src/pages/emailVerification/Status.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import {
+  Button,
+  Stack,
+  Typography,
+  createTheme,
+  useTheme,
+  ThemeProvider,
+  ThemeOptions,
+  SxProps
+} from '@mui/material';
+import {
+  Circle as CircleIcon,
+  Hexagon as HexagonIcon
+} from '@mui/icons-material';
+
+import {
+  Image,
+  ImageProps
+} from 'codeforlife/lib/esm/components';
+
+import { paths } from '../../app/router';
+
+const Status: React.FC<{
+  forTeacher: boolean,
+  header: string,
+  body: string[],
+  imageProps: ImageProps
+}> = ({ forTeacher, header, body, imageProps }) => {
+  const themeOptions: ThemeOptions = {
+    components: {
+      MuiTypography: {
+        styleOverrides: {
+          root: {
+            color: forTeacher ? 'white' : 'black'
+          }
+        }
+      }
+    }
+  };
+
+  const commonIconSxProps: SxProps = {
+    display: { xs: 'none', md: 'block' },
+    fontSize: '200px',
+    position: 'absolute'
+  };
+
+  return (
+    <ThemeProvider theme={createTheme(useTheme(), themeOptions)}>
+      <Stack
+        paddingY={{ xs: 2, sm: 3, md: 5 }}
+        paddingX={{ xs: 2, sm: 5, md: 10 }}
+        alignItems='center'
+        bgcolor={forTeacher ? '#ee0857' : '#ffc709'}
+        position='relative'
+      >
+        <CircleIcon
+          color={forTeacher ? 'secondary' : 'primary'}
+          sx={{
+            ...commonIconSxProps,
+            top: '5%',
+            left: '0%',
+            transform: 'translate(-60%, 0%)'
+          }}
+        />
+        <HexagonIcon
+          color={forTeacher ? 'tertiary' : 'secondary'}
+          sx={{
+            ...commonIconSxProps,
+            bottom: '5%',
+            right: '0%',
+            transform: 'translate(60%, 0%)'
+          }}
+        />
+        <Typography variant='h4' paddingY={1} textAlign='center'>
+          {header}
+        </Typography>
+        <Image
+          maxWidth='100px'
+          marginY={5}
+          {...imageProps}
+        />
+        {body.map((text, index) =>
+          <Typography key={index}>
+            {text}
+          </Typography>
+        )}
+        <Button
+          href={paths.home}
+          color={forTeacher ? 'tertiary' : 'white'}
+          style={{ marginTop: 30 }}
+        >
+          Back to homepage
+        </Button>
+      </Stack>
+    </ThemeProvider>
+  );
+};
+
+export default Status;
diff --git a/frontend/src/pages/register/BaseForm.tsx b/frontend/src/pages/register/BaseForm.tsx
new file mode 100644
index 00000000..01604249
--- /dev/null
+++ b/frontend/src/pages/register/BaseForm.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import {
+  useTheme,
+  createTheme,
+  ThemeProvider,
+  Stack,
+  StackProps,
+  Typography,
+  FormHelperText
+} from '@mui/material';
+
+const BaseForm: React.FC<{
+  header: string,
+  subheader: string,
+  description: string,
+  bgcolor: StackProps['bgcolor'],
+  children: StackProps['children'],
+  color: string
+}> =
+  ({
+    header,
+    subheader,
+    description,
+    bgcolor,
+    children,
+    color
+  }) => (
+    <ThemeProvider theme={createTheme(useTheme(), {
+      components: {
+        MuiTypography: {
+          styleOverrides: {
+            root: { color, fontWeight: 500 }
+          }
+        },
+        MuiTextField: {
+          styleOverrides: {
+            root: { background: 'transparent' }
+          }
+        },
+        MuiFormHelperText: {
+          styleOverrides: {
+            root: { color }
+          }
+        }
+      }
+    })}>
+      <Stack bgcolor={bgcolor} p={3} height='100%'>
+        <Typography variant='h4' textAlign='center'>
+          {header}
+        </Typography>
+        <Typography>
+          {subheader}
+        </Typography>
+        <FormHelperText style={{ marginBottom: 30 }}>
+          {description}
+        </FormHelperText>
+        {children}
+      </Stack>
+    </ThemeProvider>
+  );
+
+export default BaseForm;
diff --git a/frontend/src/pages/register/IndependentForm.tsx b/frontend/src/pages/register/IndependentForm.tsx
new file mode 100644
index 00000000..90acd19e
--- /dev/null
+++ b/frontend/src/pages/register/IndependentForm.tsx
@@ -0,0 +1,189 @@
+import React from 'react';
+import {
+  Stack,
+  Link,
+  Button,
+  FormHelperText
+} from '@mui/material';
+import {
+  ChevronRight as ChevronRightIcon
+} from '@mui/icons-material';
+import {
+  Formik,
+  Form,
+  FormikHelpers
+} from 'formik';
+import * as Yup from 'yup';
+
+import { paths } from '../../app/router';
+import BaseForm from './BaseForm';
+import DatePicker from '../../components/DatePicker';
+import CflTextField from '../../components/formik/CflTextField';
+import CflCheckboxField from 'components/formik/CflCheckboxField';
+import CflPasswordFields, { isStrongPassword } from '../../components/formik/CflPasswordFields';
+
+interface IndependentFormValues {
+  fullName: string;
+  email: string;
+  termsOfUse: boolean;
+  receiveUpdates: boolean;
+  password: string;
+  repeatPassword: string;
+}
+
+const initialValues: IndependentFormValues = {
+  fullName: '',
+  email: '',
+  termsOfUse: false,
+  receiveUpdates: false,
+  password: '',
+  repeatPassword: ''
+};
+
+const validationSchema: { [K in keyof IndependentFormValues]: Yup.Schema } = {
+  fullName: Yup
+    .string()
+    .required('This field is required'),
+  email: Yup
+    .string()
+    .email('Invalid email address')
+    .required('This field is required'),
+  termsOfUse: Yup
+    .bool()
+    .oneOf([true], 'You need to accept the terms and conditions'),
+  receiveUpdates: Yup
+    .bool(),
+  password: Yup
+    .string()
+    .required('This field is required')
+    .test(
+      'independent-password-strength-check',
+      'Invalid password',
+      (password) => isStrongPassword(password, { forTeacher: false })
+    ),
+  repeatPassword: Yup
+    .string()
+    .oneOf([Yup.ref('password'), undefined], "Passwords don't match")
+    .required('This field is required')
+};
+
+const IndependentForm: React.FC = () => {
+  const [yearsOfAge, setYearsOfAge] = React.useState<number>();
+
+  const EmailApplicableAge = 13;
+  const ReceiveUpdateAge = 18;
+
+  function onDateOfBirthChange(dob: Date | undefined): void {
+    setYearsOfAge((dob === undefined)
+      ? undefined
+      : Math.floor(
+        (new Date().getTime() - dob.getTime()) /
+        (1000 * 60 * 60 * 24 * 365)
+      )
+    );
+  }
+
+  return (
+    <BaseForm
+      header='Independent learner'
+      subheader='Register below if you are not part of a school or club and wish to set up a home learning account.'
+      description='You will have access to learning resources for Rapid Router.'
+      bgcolor='#ffc709' // TODO: use theme.palette
+      color='black'
+    >
+      <DatePicker
+        helperText='Please enter your date of birth (we do not store this information).'
+        onChange={onDateOfBirthChange}
+      />
+      {yearsOfAge !== undefined &&
+        <Formik
+          initialValues={initialValues}
+          validationSchema={Yup.object(validationSchema)}
+          onSubmit={(
+            values: IndependentFormValues,
+            { setSubmitting }: FormikHelpers<IndependentFormValues>
+          ) => {
+            // TODO: to call backend
+            setSubmitting(false);
+          }}
+        >
+          {(formik) => (
+            <Form>
+              <CflTextField
+                name='fullName'
+                placeholder='Full name'
+                helperText='Enter your full name'
+                size='small'
+              />
+              <CflTextField
+                name='email'
+                placeholder='Email address'
+                helperText={(yearsOfAge >= EmailApplicableAge)
+                  ? 'Enter your email address'
+                  : 'Please enter your parent\'s email address'
+                }
+                size='small'
+              />
+              {yearsOfAge < EmailApplicableAge &&
+                <FormHelperText style={{ fontWeight: 'bold' }}>
+                  We will send your parent/guardian an email to ask them to activate the account for you. Once they&apos;ve done this you&apos;ll be able to log in using your name and password.
+                </FormHelperText>
+              }
+              {yearsOfAge >= EmailApplicableAge &&
+                <CflCheckboxField
+                  name='termsOfUse'
+                  formControlLabelProps={{
+                    label: <>
+                      I have read and understood the &nbsp;
+                      <Link
+                        href={paths.termsOfUse}
+                        target='_blank'
+                        color='inherit'
+                        className='body'
+                      >
+                        Terms of use
+                      </Link>
+                      &nbsp;and the&nbsp;
+                      <Link
+                        href={paths.privacyNotice}
+                        target='_blank'
+                        color='inherit'
+                        className='body'
+                      >
+                        Privacy notice
+                      </Link>
+                      .
+                    </>
+                  }}
+                />
+              }
+              {yearsOfAge >= ReceiveUpdateAge &&
+                <CflCheckboxField
+                  name='receiveUpdates'
+                  formControlLabelProps={{
+                    label: 'Sign up to receive updates about Code for Life games and teaching resources.'
+                  }}
+                />
+              }
+              <CflPasswordFields
+                forTeacher={false}
+                size='small'
+              />
+              <Stack direction='row' justifyContent='end'>
+                <Button
+                  type='submit'
+                  endIcon={<ChevronRightIcon />}
+                  disabled={!formik.dirty}
+                >
+                  Register
+                </Button>
+              </Stack>
+            </Form>
+          )}
+        </Formik>
+      }
+    </BaseForm>
+  );
+};
+
+export default IndependentForm;
diff --git a/frontend/src/pages/register/Register.tsx b/frontend/src/pages/register/Register.tsx
index 749c4647..d71536a2 100644
--- a/frontend/src/pages/register/Register.tsx
+++ b/frontend/src/pages/register/Register.tsx
@@ -4,13 +4,23 @@ import {
 } from '@mui/material';
 
 import BasePage from '../../pages/BasePage';
+import PageSection from '../../components/PageSection';
+import TeacherForm from './TeacherForm';
+import IndependentForm from './IndependentForm';
 
 const Register: React.FC = () => {
   return (
     <BasePage>
-      <Grid xs={12}>
-        TODO
-      </Grid>
+      <PageSection>
+        <Grid container spacing={2}>
+          <Grid xs={12} md={6}>
+            <TeacherForm />
+          </Grid>
+          <Grid xs={12} md={6}>
+            <IndependentForm />
+          </Grid>
+        </Grid>
+      </PageSection>
     </BasePage>
   );
 };
diff --git a/frontend/src/pages/register/TeacherForm.tsx b/frontend/src/pages/register/TeacherForm.tsx
new file mode 100644
index 00000000..7a7a17b1
--- /dev/null
+++ b/frontend/src/pages/register/TeacherForm.tsx
@@ -0,0 +1,174 @@
+import React from 'react';
+import {
+  Stack,
+  Link,
+  Button,
+  InputAdornment
+} from '@mui/material';
+import {
+  EmailOutlined as EmailOutlinedIcon,
+  ChevronRight as ChevronRightIcon
+} from '@mui/icons-material';
+import {
+  Formik,
+  FormikHelpers,
+  Form
+} from 'formik';
+import * as Yup from 'yup';
+
+import { paths } from 'app/router';
+import BaseForm from './BaseForm';
+import CflTextField from '../../components/formik/CflTextField';
+import CflCheckboxField from 'components/formik/CflCheckboxField';
+import CflPasswordFields, { isStrongPassword } from '../../components/formik/CflPasswordFields';
+
+interface TeacherFormValues {
+  firstName: string;
+  lastName: string;
+  email: string;
+  termsOfUse: boolean;
+  receiveUpdates: boolean;
+  password: string;
+  repeatPassword: string;
+}
+
+const initialValues: TeacherFormValues = {
+  firstName: '',
+  lastName: '',
+  email: '',
+  termsOfUse: false,
+  receiveUpdates: false,
+  password: '',
+  repeatPassword: ''
+};
+
+const validationSchema: { [K in keyof TeacherFormValues]: Yup.Schema } = {
+  firstName: Yup
+    .string()
+    .required('This field is required'),
+  lastName: Yup
+    .string()
+    .required('This field is required'),
+  email: Yup
+    .string()
+    .email('Invalid email address')
+    .required('This field is required'),
+  termsOfUse: Yup
+    .bool()
+    .oneOf([true], 'You need to accept the terms and conditions'),
+  receiveUpdates: Yup
+    .bool(),
+  password: Yup
+    .string()
+    .required('This field is required')
+    .test(
+      'teacher-password-strength-check',
+      'Invalid password',
+      (password) => isStrongPassword(password, { forTeacher: true })
+    ),
+  repeatPassword: Yup
+    .string()
+    .oneOf([Yup.ref('password'), undefined], "Passwords don't match")
+    .required('This field is required')
+};
+
+const TeacherForm: React.FC = () => {
+  return (
+    <BaseForm
+      header='Teacher/Tutor'
+      subheader='Register below to create your school or club.'
+      description='You will have access to teaching resources, progress tracking and lesson plans for both Rapid Router and Kurono.'
+      bgcolor='#ee0857' // TODO: use theme.palette
+      color='white'
+    >
+      <Formik
+        initialValues={initialValues}
+        validationSchema={Yup.object(validationSchema)}
+        onSubmit={(
+          values: TeacherFormValues,
+          { setSubmitting }: FormikHelpers<TeacherFormValues>
+        ) => {
+          // TODO: to call backend
+          setSubmitting(false);
+        }}
+      >
+        {(formik) => (
+          <Form>
+            <CflTextField
+              name='firstName'
+              placeholder='First name'
+              helperText='Enter your first name'
+              size='small'
+            />
+            <CflTextField
+              name='lastName'
+              placeholder='Last name'
+              helperText='Enter your last name'
+              size='small'
+            />
+            <CflTextField
+              name='email'
+              placeholder='Email address'
+              helperText='Enter your email address'
+              size='small'
+              InputProps={{
+                endAdornment: (
+                  <InputAdornment position='end'>
+                    <EmailOutlinedIcon />
+                  </InputAdornment>
+                )
+              }}
+            />
+            <CflCheckboxField
+              name='termsOfUse'
+              formControlLabelProps={{
+                label: <>
+                  I am over 18 years old have read and understood the&nbsp;
+                  <Link
+                    href={paths.termsOfUse}
+                    target='_blank'
+                    color='inherit'
+                    className='body'
+                  >
+                    Terms of use
+                  </Link>
+                  &nbsp;and the&nbsp;
+                  <Link
+                    href={paths.privacyNotice}
+                    target='_blank'
+                    color='inherit'
+                    className='body'
+                  >
+                    Privacy notice
+                  </Link>
+                  .
+                </>
+              }}
+            />
+            <CflCheckboxField
+              name='receiveUpdates'
+              formControlLabelProps={{
+                label: 'Sign up to receive updates about Code for Life games and teaching resources.'
+              }}
+            />
+            <CflPasswordFields
+              forTeacher={true}
+              size='small'
+            />
+            <Stack direction='row' justifyContent='end'>
+              <Button
+                type='submit'
+                endIcon={<ChevronRightIcon />}
+                disabled={!formik.dirty}
+              >
+                Register
+              </Button>
+            </Stack>
+          </Form>
+        )}
+      </Formik>
+    </BaseForm>
+  );
+};
+
+export default TeacherForm;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 42446b98..405b6920 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2269,14 +2269,14 @@
   integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
 
 "@types/node@*":
-  version "18.16.3"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.3.tgz#6bda7819aae6ea0b386ebc5b24bdf602f1b42b01"
-  integrity sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==
+  version "20.0.0"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-20.0.0.tgz#081d9afd28421be956c1a47ced1c9a0034b467e2"
+  integrity sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw==
 
 "@types/node@^14.14.31":
-  version "14.18.43"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.43.tgz#679e000d9f1d914132ea295b4a1ffdf20370ec49"
-  integrity sha512-n3eFEaoem0WNwLux+k272P0+aq++5o05bA9CfiwKPdYPB5ZambWKdWoeHy7/OJiizMhzg27NLaZ6uzjLTzXceQ==
+  version "14.18.44"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.44.tgz#1d42ba325c5b434ee78437378ef0b7589f32c151"
+  integrity sha512-Sg79dXC3jrRlG0QOLrK5eq2hRzpU4pkD7xBiYNYJ6r9OitJMxkpTpWf6m3qa2AWzb76uMHx+6x5T1Y/WAiS3nw==
 
 "@types/node@^17.0.45":
   version "17.0.45"
@@ -2335,16 +2335,16 @@
     "@types/react" "^17"
 
 "@types/react-transition-group@^4.4.5":
-  version "4.4.5"
-  resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
-  integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==
+  version "4.4.6"
+  resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e"
+  integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==
   dependencies:
     "@types/react" "*"
 
 "@types/react@*", "@types/react@^18.0.28":
-  version "18.2.4"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.4.tgz#970e6d56f6d3fd8bd2cb1d1f042aef1d0426d08e"
-  integrity sha512-IvAIhJTmKAAJmCIcaa6+5uagjyh+9GvcJ/thPZcw+i+vx+22eHlTy2Q1bJg/prES57jehjebq9DnIhOTtIhmLw==
+  version "18.2.5"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.5.tgz#f9403e1113b12b53f7edcdd9a900c10dd4b49a59"
+  integrity sha512-RuoMedzJ5AOh23Dvws13LU9jpZHIc/k90AgmK7CecAYeWmSr3553L4u5rk4sWAPBuQosfT7HmTfG4Rg5o4nGEA==
   dependencies:
     "@types/prop-types" "*"
     "@types/scheduler" "*"
@@ -4078,9 +4078,9 @@ coa@^2.0.2:
     chalk "^2.4.1"
     q "^1.1.2"
 
-"codeforlife@github:ocadotechnology/codeforlife-package-javascript#v1.5.5":
+"codeforlife@github:ocadotechnology/codeforlife-package-javascript#v1.7.1":
   version "1.0.0"
-  resolved "https://codeload.github.com/ocadotechnology/codeforlife-package-javascript/tar.gz/52217abca0badc2b047dd632a6b7ee272d5d0e44"
+  resolved "https://codeload.github.com/ocadotechnology/codeforlife-package-javascript/tar.gz/05cfa24eb87061de1ed7e85e0d0a5d81e7981ab5"
   dependencies:
     "@emotion/react" "^11.10.6"
     "@emotion/styled" "^11.10.6"
@@ -4916,6 +4916,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
   integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
 
+deepmerge@^2.1.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+  integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
 deepmerge@^4.2.2:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
@@ -5240,9 +5245,9 @@ ejs@^3.1.6:
     jake "^10.8.5"
 
 electron-to-chromium@^1.4.284:
-  version "1.4.382"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.382.tgz#87e659b0f0d5f7b19759038871bac0a327191f82"
-  integrity sha512-czMavlW52VIPgutbVL9JnZIZuFijzsG1ww/1z2Otu1r1q+9Qe2bTsH3My3sZarlvwyqHM6+mnZfEnt2Vr4dsIg==
+  version "1.4.384"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.384.tgz#5c23b5579930dec9af2a93edafddbe991542eace"
+  integrity sha512-I97q0MmRAAqj53+a8vZsDkEXBZki+ehYAOPzwtQzALip52aEp2+BJqHFtTlsfjoqVZYwPpHC8wM6MbsSZQ/Eqw==
 
 elliptic@^6.5.3:
   version "6.5.4"
@@ -6285,6 +6290,19 @@ form-data@~2.3.2:
     combined-stream "^1.0.6"
     mime-types "^2.1.12"
 
+formik@^2.2.9:
+  version "2.2.9"
+  resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
+  integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
+  dependencies:
+    deepmerge "^2.1.1"
+    hoist-non-react-statics "^3.3.0"
+    lodash "^4.17.21"
+    lodash-es "^4.17.21"
+    react-fast-compare "^2.0.1"
+    tiny-warning "^1.0.2"
+    tslib "^1.10.0"
+
 forwarded@0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -8492,6 +8510,11 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash.clone@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
@@ -10620,6 +10643,11 @@ prop-types@^15.6.2, prop-types@^15.8.1:
     object-assign "^4.1.1"
     react-is "^16.13.1"
 
+property-expr@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
+  integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
+
 proxy-addr@~2.0.7:
   version "2.0.7"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@@ -10843,6 +10871,11 @@ react-error-overlay@^6.0.11:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
   integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
 
+react-fast-compare@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
+  integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+
 react-is@^16.13.1, react-is@^16.7.0:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -12486,11 +12519,21 @@ timsort@^0.3.0:
   resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
   integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
 
+tiny-case@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
+  integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
+
 tiny-inflate@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
   integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
 
+tiny-warning@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
 tmp@~0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -12555,6 +12598,11 @@ toidentifier@1.0.1:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
+toposort@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+  integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
+
 tough-cookie@^2.3.3, tough-cookie@^2.5.0, tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -12607,7 +12655,7 @@ tsconfig-paths@^3.14.1:
     minimist "^1.2.6"
     strip-bom "^3.0.0"
 
-tslib@^1.8.1:
+tslib@^1.10.0, tslib@^1.8.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@@ -12680,7 +12728,7 @@ type-fest@^0.8.0:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
   integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
 
-type-fest@^2.13.0:
+type-fest@^2.13.0, type-fest@^2.19.0:
   version "2.19.0"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
   integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
@@ -13622,3 +13670,13 @@ yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+yup@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/yup/-/yup-1.1.1.tgz#49dbcf5ae7693ed0a36ed08a9e9de0a09ac18e6b"
+  integrity sha512-KfCGHdAErqFZWA5tZf7upSUnGKuTOnsI3hUsLr7fgVtx+DK04NPV01A68/FslI4t3s/ZWpvXJmgXhd7q6ICnag==
+  dependencies:
+    property-expr "^2.0.5"
+    tiny-case "^1.0.3"
+    toposort "^2.0.2"
+    type-fest "^2.19.0"