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

Portal frontend 31 #32

Merged
merged 29 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading