Skip to content

Commit

Permalink
Portal frontend 31 (#32)
Browse files Browse the repository at this point in the history
* export results and args

* new js package

* add icon

* table components

* quick save

* extract id from body

* school name field

* ready for review

* only teachers

* fix rendering issue and create placeholder tabs

* merge from dev

* working order

* create sub view for leaving school

* merge from dev

* student dashboard

* Merge branch 'development' into portal-frontend-25

* new js package

* quick save

* improve form cleaning

* delete account form

* merge from dev

* exclude repeat password

* Merge branch 'development' into portal-frontend-31

* review findings

* new js package

* feedback

* sync with config

* hard install

* simplify
  • Loading branch information
SKairinos authored Aug 14, 2024
1 parent 9c39634 commit 50b4fa9
Show file tree
Hide file tree
Showing 12 changed files with 1,101 additions and 828 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"pipenv"
],
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
"source.fixAll.eslint": "always",
"source.organizeImports": "never"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
Expand Down
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.2.1",
"codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v2.2.2",
"crypto-js": "^4.2.0"
},
"devDependencies": {
Expand Down
10 changes: 2 additions & 8 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
import Cookies from "js-cookie"

import { getCsrfCookie, logout } from "codeforlife/utils/auth"
import { tagTypes } from "codeforlife/api"

// https://docs.djangoproject.com/en/3.2/ref/csrf/
const getCsrfCookie = () =>
Cookies.get(`${import.meta.env.VITE_SERVICE_NAME}_csrftoken`)

const fetch = fetchBaseQuery({
baseUrl: import.meta.env.VITE_API_BASE_URL,
credentials: "include",
Expand Down Expand Up @@ -58,8 +53,7 @@ const api = createApi({
} catch (error) {
console.error("Failed to log out...", error)
} finally {
Cookies.remove("session_key")
Cookies.remove("session_metadata")
logout()
dispatch(api.util.resetApiState())
}
},
Expand Down
26 changes: 19 additions & 7 deletions src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
type Arg,
type CreateArg,
type CreateResult,
type DestroyArg,
type DestroyResult,
type UpdateArg,
type UpdateResult,
Expand Down Expand Up @@ -41,10 +40,7 @@ export type ResetPasswordArg = UpdateArg<User, "password", never> & {
}

export type VerifyEmailAddressResult = UpdateResult<User>
export type VerifyEmailAddressArg = {
id: User["id"]
token: string
}
export type VerifyEmailAddressArg = Pick<User, "id"> & { token: string }

export type UpdateUserResult = UpdateResult<User>
export type UpdateUserArg = UpdateArg<
Expand All @@ -54,7 +50,9 @@ export type UpdateUserArg = UpdateArg<
> & { current_password?: string }

export type DestroyIndependentUserResult = DestroyResult
export type DestroyIndependentUserArg = DestroyArg<User>
export type DestroyIndependentUserArg = Pick<User, "id" | "password"> & {
remove_from_newsletter: boolean
}

export type CreateIndependentUserResult = CreateResult<User>
export type CreateIndependentUserArg = CreateArg<
Expand All @@ -65,6 +63,9 @@ export type CreateIndependentUserArg = CreateArg<
add_to_newsletter: boolean
}

export type ValidatePasswordResult = null
export type ValidatePasswordArg = Pick<User, "id" | "password">

const userApi = api.injectEndpoints({
endpoints: build => ({
...getReadUserEndpoints(build),
Expand Down Expand Up @@ -122,9 +123,10 @@ const userApi = api.injectEndpoints({
DestroyIndependentUserResult,
DestroyIndependentUserArg
>({
query: id => ({
query: ({ id, ...body }) => ({
url: buildUrl(urls.user.detail, { url: { id } }),
method: "DELETE",
body,
}),
invalidatesTags: tagData(USER_TAG),
}),
Expand All @@ -138,6 +140,14 @@ const userApi = api.injectEndpoints({
body,
}),
}),
// TODO: create action on the backend.
validatePassword: build.query<ValidatePasswordResult, ValidatePasswordArg>({
query: ({ id, ...body }) => ({
url: buildUrl(urls.user.detail, { url: { id } }),
method: "POST",
body,
}),
}),
}),
})

Expand All @@ -155,4 +165,6 @@ export const {
useLazyRetrieveUserQuery,
useListUsersQuery,
useLazyListUsersQuery,
useValidatePasswordQuery,
useLazyValidatePasswordQuery,
} = userApi
1 change: 1 addition & 0 deletions src/pages/register/IndyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const IndyForm: FC<IndyFormProps> = () => {
password_repeat: "",
}}
onSubmit={submitForm(createIndependentUser, {
exclude: ["password_repeat", "meets_criteria"],
then: () => {
navigate(paths.register.emailVerification.userType.indy._)
},
Expand Down
1 change: 1 addition & 0 deletions src/pages/register/TeacherForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const TeacherForm: FC<TeacherFormProps> = () => {
},
}}
onSubmit={submitForm(createTeacher, {
exclude: ["user.password_repeat", "user.meets_criteria"],
then: () => {
navigate(paths.register.emailVerification.userType.teacher._)
},
Expand Down
4 changes: 3 additions & 1 deletion src/pages/resetPassword/PasswordForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ const PasswordForm: FC<PasswordFormProps> = ({ userType, userId, token }) => {
password: "",
password_repeat: "",
}}
onSubmit={submitForm(resetPassword)}
onSubmit={submitForm(resetPassword, {
exclude: ["password_repeat"],
})}
>
<NewPasswordField userType={userType} />
<Stack mt={3} direction="row" gap={5} justifyContent="center">
Expand Down
144 changes: 144 additions & 0 deletions src/pages/studentAccount/DeleteAccountForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import * as forms from "codeforlife/components/form"
import {
Button,
Dialog,
Unstable_Grid2 as Grid,
Stack,
Typography,
} from "@mui/material"
import { type FC, useState } from "react"
import { DeleteOutline as DeleteOutlineIcon } from "@mui/icons-material"
import { logout } from "codeforlife/utils/auth"
import { useNavigate } from "codeforlife/hooks"

import {
type DestroyIndependentUserArg,
type RetrieveUserResult,
useDestroyIndependentUserMutation,
useLazyValidatePasswordQuery,
} from "../../api/user"
import { paths } from "../../router"

const ConfirmDialog: FC<{
open: boolean
onClose: () => void
destroyIndyUserArg?: DestroyIndependentUserArg
}> = ({ open, onClose, destroyIndyUserArg }) => {
const [destroyIndyUser] = useDestroyIndependentUserMutation()
const navigate = useNavigate()

if (!destroyIndyUserArg) return <></>

return (
<Dialog open={open}>
<Typography variant="h5" textAlign="center">
You are about to delete your account
</Typography>
<Typography>
This action is not reversible. Are you sure you wish to proceed?
</Typography>
<Stack direction={{ xs: "column", sm: "row" }} spacing={3}>
<Button variant="outlined" onClick={onClose}>
Cancel
</Button>
<Button
className="alert"
endIcon={<DeleteOutlineIcon />}
onClick={() => {
destroyIndyUser(destroyIndyUserArg)
.unwrap()
.then(() => {
logout()
navigate(paths._, {
state: {
notifications: [
{
props: {
children: "Your account was successfully deleted.",
},
},
],
},
})
})
}}
>
Delete
</Button>
</Stack>
</Dialog>
)
}

export interface DeleteAccountFormProps {
user: RetrieveUserResult
}

const DeleteAccountForm: FC<DeleteAccountFormProps> = ({ user }) => {
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean
destroyIndyUserArg?: DestroyIndependentUserArg
}>({ open: false })
const [validatePassword] = useLazyValidatePasswordQuery()

return (
<>
<ConfirmDialog
open={confirmDialog.open}
onClose={() => {
setConfirmDialog({ open: false })
}}
destroyIndyUserArg={confirmDialog.destroyIndyUserArg}
/>
<Typography variant="h5">Delete account</Typography>
<Typography>
If you no longer wish to have a Code for Life account, you can delete it
by confirming below. You will receive an email to confirm this decision.
</Typography>
<Typography fontWeight="bold">This can&apos;t be reversed.</Typography>
<forms.Form
initialValues={{
id: user.id,
password: "",
remove_from_newsletter: false,
}}
onSubmit={values => {
validatePassword({ id: values.id, password: values.password })
.unwrap()
.then(() => {
setConfirmDialog({ open: true, destroyIndyUserArg: values })
})
}}
>
<Grid container columnSpacing={4}>
<Grid xs={12} sm={6}>
<forms.PasswordField
required
label="Current password"
placeholder="Enter your current password"
/>
</Grid>
<Grid xs={12} sm={6}>
{/* TODO: only display this checkbox if the user has been added to the newsletter. */}
<forms.CheckboxField
name="remove_from_newsletter"
formControlLabelProps={{
label:
"Please remove me from the newsletter and marketing emails too.",
}}
/>
</Grid>
</Grid>
<forms.SubmitButton
className="alert"
endIcon={<DeleteOutlineIcon />}
sx={theme => ({ marginTop: theme.spacing(3) })}
>
Delete account
</forms.SubmitButton>
</forms.Form>
</>
)
}

export default DeleteAccountForm
49 changes: 49 additions & 0 deletions src/pages/studentAccount/StudentAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as page from "codeforlife/components/page"
import { type SessionMetadata, useQueryManager } from "codeforlife/hooks"
import { type FC } from "react"
import { LinkButton } from "codeforlife/components/router"
import { Typography } from "@mui/material"

import DeleteAccountForm from "./DeleteAccountForm"
import UpdateAccountForm from "./UpdateAccountForm"
import { paths } from "../../router"
import { useRetrieveUserQuery } from "../../api/user"

export interface StudentAccountProps {
userType: "student" | "indy"
}

const _StudentAccount: FC<SessionMetadata> = ({ user_type, user_id }) =>
useQueryManager(useRetrieveUserQuery, user_id, user => (
<>
<page.Banner
header={`Welcome, ${user.first_name}`}
textAlign="center"
bgcolor={user_type === "student" ? "tertiary" : "secondary"}
/>
<page.Section>
<UpdateAccountForm user={user} />
</page.Section>
{user_type === "indy" && (
<>
<page.Section boxProps={{ bgcolor: "info.main" }}>
<Typography variant="h5">Join a school or club</Typography>
<Typography>
To find out about linking your Code For Life account with a school
or club, click &apos;Join&apos;.
</Typography>
<LinkButton to={paths.indy.dashboard.joinClass._}>Join</LinkButton>
</page.Section>
<page.Section>
<DeleteAccountForm user={user} />
</page.Section>
</>
)}
</>
))

const StudentAccount: FC<StudentAccountProps> = ({ userType }) => (
<page.Page session={{ userType }}>{_StudentAccount}</page.Page>
)

export default StudentAccount
Loading

0 comments on commit 50b4fa9

Please sign in to comment.