Skip to content

Commit

Permalink
register page (#18)
Browse files Browse the repository at this point in the history
* register page

* fix default date

* max is today

* new js package
  • Loading branch information
SKairinos authored Jul 19, 2024
1 parent 0bb4eb5 commit 5b11644
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 22 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"✅ Do add `devDependencies` below that are `peerDependencies` in the CFL package."
],
"dependencies": {
"codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v2.1.2",
"codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v2.1.3",
"crypto-js": "^4.2.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const userApi = api.injectEndpoints({
createIndependentUser: build.mutation<
CreateResult<User>,
CreateArg<User, "first_name" | "last_name" | "email" | "password"> & {
date_of_birth: Date
date_of_birth: string
add_to_newsletter: boolean
}
>({
Expand Down
27 changes: 27 additions & 0 deletions src/components/form/LastNameField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type FC } from "react"
import { string as YupString } from "yup"

import { TextField, type TextFieldProps } from "codeforlife/components/form"

export type LastNameFieldProps = Omit<
TextFieldProps,
"type" | "name" | "schema"
> &
Partial<Pick<TextFieldProps, "name">>

const LastNameField: FC<LastNameFieldProps> = ({
name = "last_name",
label = "Last name",
placeholder = "Enter your last name",
...otherTextFieldProps
}) => (
<TextField
schema={YupString().max(150)}
name={name}
label={label}
placeholder={placeholder}
{...otherTextFieldProps}
/>
)

export default LastNameField
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { Button, Dialog, Typography } from "@mui/material"
import { type FC, useState } from "react"

import { PasswordField } from "codeforlife/components/form"
import {
PasswordField,
type PasswordFieldProps,
} from "codeforlife/components/form"

import {
indyPasswordSchema,
pwnedPasswordSchema,
studentPasswordSchema,
teacherPasswordSchema,
} from "../app/schemas"
} from "../../app/schemas"

export interface NewPasswordFieldProps {
export interface NewPasswordFieldProps
extends Omit<
PasswordFieldProps,
"required" | "withRepeatField" | "schema" | "validateOptions"
> {
userType: "teacher" | "independent" | "student"
}

const NewPasswordField: FC<NewPasswordFieldProps> = ({ userType }) => {
const NewPasswordField: FC<NewPasswordFieldProps> = ({
userType,
...passwordFieldProps
}) => {
const [pwnedPasswords, setPwnedPasswords] = useState<{
online: boolean
dialogOpen: boolean
Expand Down Expand Up @@ -46,6 +56,7 @@ const NewPasswordField: FC<NewPasswordFieldProps> = ({ userType }) => {
withRepeatField
schema={schema}
validateOptions={{ abortEarly: false }}
{...passwordFieldProps}
/>
<Dialog open={!pwnedPasswords.online && pwnedPasswords.dialogOpen}>
<Typography variant="h5" className="no-override">
Expand Down
11 changes: 11 additions & 0 deletions src/components/form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import LastNameField, { type LastNameFieldProps } from "./LastNameField"
import NewPasswordField, {
type NewPasswordFieldProps,
} from "./NewPasswordField"

export {
LastNameField,
NewPasswordField,
type LastNameFieldProps,
type NewPasswordFieldProps,
}
10 changes: 1 addition & 9 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import NewPasswordField, {
type NewPasswordFieldProps,
} from "./NewPasswordField"
import OpenInEmailButtons, {
type OpenInEmailButtonsProps,
} from "./OpenInEmailButtons"

export {
NewPasswordField,
OpenInEmailButtons,
type NewPasswordFieldProps,
type OpenInEmailButtonsProps,
}
export { OpenInEmailButtons, type OpenInEmailButtonsProps }
33 changes: 33 additions & 0 deletions src/pages/register/BaseForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FormHelperText, Typography } from "@mui/material"
import { type FC } from "react"

import { ThemedBox, type ThemedBoxProps } from "codeforlife/theme"

import { themeOptions } from "../../app/theme"

export interface BaseFormProps extends ThemedBoxProps {
header: string
subheader: string
description: string
}

const BaseForm: FC<BaseFormProps> = ({
header,
subheader,
description,
userType,
children,
}) => (
<ThemedBox options={themeOptions} userType={userType} height="100%" p={5}>
<Typography variant="h4" textAlign="center">
{header}
</Typography>
<Typography>{subheader}</Typography>
<FormHelperText sx={({ spacing }) => ({ marginBottom: spacing(3.5) })}>
{description}
</FormHelperText>
{children}
</ThemedBox>
)

export default BaseForm
146 changes: 146 additions & 0 deletions src/pages/register/IndyForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { ChevronRight as ChevronRightIcon } from "@mui/icons-material"
import { FormHelperText, Stack } from "@mui/material"
import dayjs from "dayjs"
import { type FC } from "react"
import { useNavigate } from "react-router-dom"

import * as forms from "codeforlife/components/form"
import { Link } from "codeforlife/components/router"
import { submitForm } from "codeforlife/utils/form"

import { useCreateIndependentUserMutation } from "../../api/user"
import { LastNameField, NewPasswordField } from "../../components/form"
import { paths } from "../../router"
import BaseForm from "./BaseForm"

export interface IndyFormProps {}

const IndyForm: FC<IndyFormProps> = () => {
const navigate = useNavigate()
const [createIndependentUser] = useCreateIndependentUserMutation()

const EmailApplicableAge = 13
const ReceiveUpdateAge = 18

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."
userType="independent"
>
<forms.Form
initialValues={{
date_of_birth: "",
first_name: "",
last_name: "",
email: "",
meets_criteria: false,
add_to_newsletter: false,
password: "",
password_repeat: "",
}}
onSubmit={submitForm(createIndependentUser, {
then: () => {
navigate(paths.register.emailVerification.userType.indy._)
},
})}
>
{form => {
const yearsOfAge = form.values.date_of_birth
? Math.floor(
(new Date().getTime() -
new Date(form.values.date_of_birth).getTime()) /
(1000 * 60 * 60 * 24 * 365.25),
)
: undefined

return (
<>
<FormHelperText sx={{ fontWeight: "bold" }}>
Please enter your date of birth (we do not store this
information).
</FormHelperText>
<forms.DatePickerField
name="date_of_birth"
maxDate={dayjs()}
required
/>
{yearsOfAge !== undefined && (
<>
<forms.FirstNameField required />
<LastNameField />
<forms.EmailField
required
label={
yearsOfAge >= EmailApplicableAge
? "Email address"
: "Parent's email address"
}
placeholder={
yearsOfAge >= EmailApplicableAge
? "Enter your email address"
: "Enter your parent's email address"
}
/>
{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 && (
<forms.CheckboxField
required
name="meets_criteria"
formControlLabelProps={{
label: (
<>
I have read and understood the &nbsp;
<Link
to={paths.termsOfUse.tab.termsOfUse._}
target="_blank"
>
Terms of use
</Link>
&nbsp;and the&nbsp;
<Link
to={paths.privacyNotice.tab.privacyNotice._}
target="_blank"
>
Privacy notice
</Link>
.
</>
),
}}
/>
)}
{yearsOfAge >= ReceiveUpdateAge && (
<forms.CheckboxField
name="add_to_newsletter"
formControlLabelProps={{
label:
"Sign up to receive updates about Code for Life games and teaching resources.",
}}
/>
)}
<NewPasswordField userType="independent" />
<Stack alignItems="end">
<forms.SubmitButton endIcon={<ChevronRightIcon />}>
Register
</forms.SubmitButton>
</Stack>
</>
)}
</>
)
}}
</forms.Form>
</BaseForm>
)
}

export default IndyForm
53 changes: 53 additions & 0 deletions src/pages/register/Register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Unstable_Grid2 as Grid } from "@mui/material"
import { type FC, useEffect } from "react"

import * as page from "codeforlife/components/page"
import { useNavigate, useSessionMetadata } from "codeforlife/hooks"

import { paths } from "../../router"
import IndyForm from "./IndyForm"
import TeacherForm from "./TeacherForm"

export interface RegisterProps {}

const Register: FC<RegisterProps> = () => {
const navigate = useNavigate()
const sessionMetadata = useSessionMetadata()

useEffect(() => {
if (sessionMetadata) {
navigate(
sessionMetadata.auth_factors.length
? // Send user to login page if they need to finish authenticating.
{
teacher: paths.login.teacher._,
student: paths.login.student._,
indy: paths.login.indy._,
}[sessionMetadata.user_type]
: // Send user to dashboard page if they finished authenticating.
{
teacher: paths.teacher.dashboard.tab.school._,
student: paths.student.dashboard._,
indy: paths.indy.dashboard._,
}[sessionMetadata.user_type],
)
}
}, [sessionMetadata, navigate])

return (
<page.Page>
<page.Section>
<Grid container spacing={2}>
<Grid xs={12} md={6}>
<TeacherForm />
</Grid>
<Grid xs={12} md={6}>
<IndyForm />
</Grid>
</Grid>
</page.Section>
</page.Page>
)
}

export default Register
Loading

0 comments on commit 5b11644

Please sign in to comment.