Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: teacher onboarding #83

Merged
merged 14 commits into from
Jan 17, 2025
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
],
"dependencies": {
"@react-pdf/renderer": "^4.0.0",
"codeforlife": "2.6.3",
"codeforlife": "2.6.4",
"crypto-js": "^4.2.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/form/ClassNameField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type ClassNameFieldProps = Omit<

const ClassNameField: FC<ClassNameFieldProps> = ({
name = "name",
label = "Last name",
label = "Class name",
placeholder = "Enter a class name",
InputProps = {},
...otherTextFieldProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@
import { Stack, Typography } from "@mui/material"
import { type Class } from "codeforlife/api"
import { InputFileButton } from "codeforlife/components"
import { type SubmitFormOptions } from "codeforlife/utils/form"
import { firstNameSchema } from "codeforlife/schemas/user"
import { generatePath } from "react-router-dom"
import { useNavigate } from "codeforlife/hooks"

import {
type CreateStudentsArg,
type CreateStudentsResult,
useCreateStudentsMutation,
} from "../../../../api/student"
import { type StudentsCredentialsState } from "../studentsCredentials/StudentsCredentials"
import { paths } from "../../../../routes"
} from "../../api/student"

export interface CreateStudentsFormProps {
classId: Class["id"]
submitOptions: Omit<
SubmitFormOptions<
{ first_names: string[] },
CreateStudentsArg,
CreateStudentsResult
>,
"clean" | "catch"
>
}

const CreateStudentsForm: FC<CreateStudentsFormProps> = ({ classId }) => {
const CreateStudentsForm: FC<CreateStudentsFormProps> = ({

Check warning on line 29 in src/components/form/CreateStudentsForm.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/form/CreateStudentsForm.tsx#L29

Added line #L29 was not covered by tests
classId,
submitOptions,
}) => {
const fileInput = useRef<HTMLInputElement>()
const navigate = useNavigate()

Expand Down Expand Up @@ -56,23 +66,14 @@
initialValues={{ first_names: [] as string[] }}
useMutation={useCreateStudentsMutation}
submitOptions={{
...submitOptions,
clean: ({ first_names }) =>
first_names.reduce((arg, first_name) => {
first_name = first_name.trim()
return first_name
? [...arg, { klass: classId, user: { first_name } }]
: arg
}, [] as CreateStudentsArg),
then: students => {
navigate<StudentsCredentialsState>(
generatePath(
paths.teacher.dashboard.tab.classes.class.students.credentials
._,
{ classId },
),
{ state: { flow: "create", students } },
)
},
catch: () => {
navigate(".", {
state: {
Expand Down
2 changes: 2 additions & 0 deletions src/components/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export * from "./ClassAutocompleteField"
export { default as ClassAutocompleteField } from "./ClassAutocompleteField"
export * from "./ClassNameField"
export { default as ClassNameField } from "./ClassNameField"
export * from "./CreateStudentsForm"
export { default as CreateStudentsForm } from "./CreateStudentsForm"
export * from "./LastNameField"
export { default as LastNameField } from "./LastNameField"
export * from "./NewPasswordField"
Expand Down
48 changes: 24 additions & 24 deletions src/pages/login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import * as page from "codeforlife/components/page"
import * as yup from "yup"
import { type FC, useEffect } from "react"
import {
useNavigate,
useSearchParams,
useSessionMetadata,
} from "codeforlife/hooks"
import { useSearchParams, useSessionMetadata } from "codeforlife/hooks"
import { type FC } from "react"
import { Navigate } from "codeforlife/components/router"

import * as studentForms from "./studentForms"
import * as teacherForms from "./teacherForms"
Expand All @@ -24,32 +21,35 @@

const Login: FC<LoginProps> = ({ form }) => {
const sessionMetadata = useSessionMetadata()
const navigate = useNavigate()
const searchParams = useSearchParams({
verifyEmail: yup.boolean().default(false),
})

useEffect(() => {
if (sessionMetadata) {
if (
sessionMetadata.user_type === "teacher" &&
sessionMetadata.auth_factors.includes("otp") &&
form !== "teacher-otp" &&
form !== "teacher-otp-bypass-token"
) {
navigate(paths.login.teacher.otp._, { replace: true })
} else {
navigate(
if (sessionMetadata) {
const { user_type, auth_factors } = sessionMetadata

Check warning on line 29 in src/pages/login/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/login/Login.tsx#L29

Added line #L29 was not covered by tests

if (
user_type === "teacher" &&
auth_factors.includes("otp") &&
form !== "teacher-otp" &&
form !== "teacher-otp-bypass-token"
) {
return <Navigate to={paths.login.teacher.otp._} replace />

Check warning on line 37 in src/pages/login/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/login/Login.tsx#L37

Added line #L37 was not covered by tests
}

return (

Check warning on line 40 in src/pages/login/Login.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/login/Login.tsx#L40

Added line #L40 was not covered by tests
<Navigate
to={
{
teacher: paths.teacher.dashboard.tab.school._,
student: paths.student.dashboard._,
indy: paths.indy.dashboard._,
}[sessionMetadata.user_type],
{ replace: true },
)
}
}
}, [sessionMetadata, navigate, form])
}[user_type]
}
replace
/>
)
}

return (
<page.Page>
Expand Down
110 changes: 54 additions & 56 deletions src/pages/teacherDashboard/TeacherDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as page from "codeforlife/components/page"
import { type FC, useEffect } from "react"
import { type SessionMetadata, useNavigate } from "codeforlife/hooks"
import { type FC } from "react"
import { Navigate } from "codeforlife/components/router"
import { type SchoolTeacherUser } from "codeforlife/api"
import { type SessionMetadata } from "codeforlife/hooks"
import { getParam } from "codeforlife/utils/router"
import { handleResultState } from "codeforlife/utils/api"

import Account, { type AccountProps } from "./account/Account"
import Classes, { type ClassesProps } from "./classes/Classes"
import { type RetrieveUserResult, useRetrieveUserQuery } from "../../api/user"
import School, { type SchoolProps } from "./school/School"
import { paths } from "../../routes"
import { useRetrieveUserQuery } from "../../api/user"

export type TeacherDashboardProps =
| {
Expand All @@ -29,64 +30,61 @@
tab,
view,
user_id,
}) => {
const result = useRetrieveUserQuery(user_id)
const navigate = useNavigate()
}) =>
handleResultState(

Check warning on line 34 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L34

Added line #L34 was not covered by tests
useRetrieveUserQuery(user_id, { refetchOnMountOrArgChange: true }),
authUser => {

Check warning on line 36 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L36

Added line #L36 was not covered by tests
if (!authUser.teacher!.school) {
return <Navigate to={paths.teacher.onboarding._} />

Check warning on line 38 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L38

Added line #L38 was not covered by tests
}

const authUser = result.data
const isNonSchoolTeacher = authUser && !authUser.teacher?.school
const authSchoolTeacherUser = authUser as SchoolTeacherUser<

Check warning on line 41 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L41

Added line #L41 was not covered by tests
typeof authUser
>

useEffect(() => {
if (isNonSchoolTeacher) navigate(paths.teacher.onboarding._)
}, [isNonSchoolTeacher, navigate])
const tabs: page.TabBarProps["tabs"] = [

Check warning on line 45 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L45

Added line #L45 was not covered by tests
{
label: "Your school",
children: (
<School
authUser={authSchoolTeacherUser}
view={view as SchoolProps["view"]}
/>
),
path: getParam(paths.teacher.dashboard.tab.school, "tab"),
},
{
label: "Your classes",
children: (
<Classes
authUser={authSchoolTeacherUser}
view={view as ClassesProps["view"]}
/>
),
path: getParam(paths.teacher.dashboard.tab.classes, "tab"),
},
{
label: "Your account",
children: (
<Account
authUser={authSchoolTeacherUser}
view={view as AccountProps["view"]}
/>
),
path: getParam(paths.teacher.dashboard.tab.account, "tab"),
},
]

if (isNonSchoolTeacher) return <></>

const authSchoolTeacherUser =
authUser as SchoolTeacherUser<RetrieveUserResult>

const tabs: page.TabBarProps["tabs"] = [
{
label: "Your school",
children: (
<School
authUser={authSchoolTeacherUser}
view={view as SchoolProps["view"]}
/>
),
path: getParam(paths.teacher.dashboard.tab.school, "tab"),
},
{
label: "Your classes",
children: (
<Classes
authUser={authSchoolTeacherUser}
view={view as ClassesProps["view"]}
return (

Check warning on line 78 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L78

Added line #L78 was not covered by tests
<page.TabBar
header={`Welcome back, ${authUser.first_name} ${authUser.last_name}`}
originalPath={paths.teacher.dashboard.tab._}
value={tabs.findIndex(t => t.path === tab)}

Check warning on line 82 in src/pages/teacherDashboard/TeacherDashboard.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/TeacherDashboard.tsx#L82

Added line #L82 was not covered by tests
tabs={tabs}
/>
),
path: getParam(paths.teacher.dashboard.tab.classes, "tab"),
)
},
{
label: "Your account",
children: (
<Account
authUser={authSchoolTeacherUser}
view={view as AccountProps["view"]}
/>
),
path: getParam(paths.teacher.dashboard.tab.account, "tab"),
},
]

return handleResultState(result, authUser => (
<page.TabBar
header={`Welcome back, ${authUser.first_name} ${authUser.last_name}`}
originalPath={paths.teacher.dashboard.tab._}
value={tabs.findIndex(t => t.path === tab)}
tabs={tabs}
/>
))
}
)

const TeacherDashboard: FC<TeacherDashboardProps> = props => (
<page.Page session={{ userType: "teacher" }}>
Expand Down
24 changes: 21 additions & 3 deletions src/pages/teacherDashboard/classes/class/Class.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import * as pages from "codeforlife/components/page"
import { useNavigate, useParams } from "codeforlife/hooks"
import { type Class } from "codeforlife/api"
import { type FC } from "react"
import { Link } from "codeforlife/components/router"
import { Navigate } from "codeforlife/components/router"
import { Typography } from "@mui/material"
import { generatePath } from "react-router-dom"
import { handleResultState } from "codeforlife/utils/api"
import { useParams } from "codeforlife/hooks"

import AdditionalClassDetails from "./AdditionalClassDetails"
import CreateStudentsForm from "./CreateStudentsForm"
import { CreateStudentsForm } from "../../../../components/form"
import StudentTable from "./StudentTable"
import { type StudentsCredentialsState } from "../studentsCredentials/StudentsCredentials"
import { classIdSchema } from "../../../../app/schemas"
import { paths } from "../../../../routes"
import { useRetrieveClassQuery } from "../../../../api/klass"

const _Class: FC<{ classId: Class["id"] }> = ({ classId }) => {
const navigate = useNavigate()

Check warning on line 20 in src/pages/teacherDashboard/classes/class/Class.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/classes/class/Class.tsx#L20

Added line #L20 was not covered by tests

return handleResultState(useRetrieveClassQuery(classId), klass => (
<>
<pages.Section>
Expand All @@ -35,7 +39,21 @@
<StudentTable classId={classId} />
</pages.Section>
<pages.Section boxProps={{ bgcolor: "info.main" }}>
<CreateStudentsForm classId={classId} />
<CreateStudentsForm
classId={classId}
submitOptions={{
then: students => {
navigate<StudentsCredentialsState>(

Check warning on line 46 in src/pages/teacherDashboard/classes/class/Class.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherDashboard/classes/class/Class.tsx#L45-L46

Added lines #L45 - L46 were not covered by tests
generatePath(
paths.teacher.dashboard.tab.classes.class.students.credentials
._,
{ classId },
),
{ state: { flow: "create", students } },
)
},
}}
/>
</pages.Section>
<pages.Section>
<AdditionalClassDetails classId={classId} />
Expand Down
42 changes: 42 additions & 0 deletions src/pages/teacherOnboarding/CreateClassForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as forms from "codeforlife/components/form"
import { type FC } from "react"
import { type SubmitFormOptions } from "codeforlife/utils/form"
import { Typography } from "@mui/material"

import { ClassNameField, ReadClassmatesDataField } from "../../components/form"
import {
type CreateClassArg,
type CreateClassResult,
useCreateClassMutation,
} from "../../api/klass"

export interface CreateClassFormProps {
submitOptions: SubmitFormOptions<
CreateClassArg,
CreateClassArg,
CreateClassResult
>
}

const CreateClassForm: FC<CreateClassFormProps> = ({ submitOptions }) => (
<>

Check warning on line 22 in src/pages/teacherOnboarding/CreateClassForm.tsx

View check run for this annotation

Codecov / codecov/patch

src/pages/teacherOnboarding/CreateClassForm.tsx#L21-L22

Added lines #L21 - L22 were not covered by tests
<Typography>
When you set up a new class, a unique class access code will automatically
be generated, with you being identified as the teacher for that class.
</Typography>
<forms.Form
initialValues={{
name: "",
read_classmates_data: false,
}}
useMutation={useCreateClassMutation}
submitOptions={submitOptions}
>
<ClassNameField required />
<ReadClassmatesDataField />
<forms.SubmitButton>Create class</forms.SubmitButton>
</forms.Form>
</>
)

export default CreateClassForm
Loading