Skip to content

Commit

Permalink
Login and agreement signatures page
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Sep 6, 2024
1 parent 35e75b9 commit a7449f2
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 288 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# Service.
VITE_SERVICE_NAME=contributor

# Links.
VITE_LINK_GH_LOGIN=https://github.com/login/oauth/authorize?client_id=Ov23liBErSabQFqROeMg
VITE_LINK_GH_CONTRIBUTING=https://github.com/ocadotechnology/codeforlife-workspace/blob/{commitId}/CONTRIBUTING.md
3 changes: 0 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
"type": "node"
},
{
"env": {
"VITE_SERVICE_NAME": "contributor"
},
"name": "Vite Server",
"preLaunchTask": "run",
"request": "launch",
Expand Down
8 changes: 4 additions & 4 deletions src/api/session.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { type SessionMetadata } from "../app/hooks"

export type LoginWithGitHubResult = SessionMetadata
export type LoginWithGitHubArg = { code: string }
export type LoginResult = SessionMetadata
export type LoginArg = { code: string }

import api from "."

const sessionApi = api.injectEndpoints({
endpoints: build => ({
loginWithGitHub: build.mutation<LoginWithGitHubResult, LoginWithGitHubArg>({
login: build.mutation<LoginResult, LoginArg>({
query: body => ({
url: "session/login/",
method: "POST",
Expand All @@ -18,4 +18,4 @@ const sessionApi = api.injectEndpoints({
})

export default sessionApi
export const { useLoginWithGitHubMutation } = sessionApi
export const { useLoginMutation } = sessionApi
3 changes: 2 additions & 1 deletion src/app/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import env from "codeforlife/env"

export * from "codeforlife/env"

export const GH_CLIENT_ID = env.GITHUB_CLIENT_ID ?? "REPLACE_ME"
export const LINK_GH_LOGIN = env.VITE_LINK_GH_LOGIN
export const LINK_GH_CONTRIBUTING = env.VITE_LINK_GH_CONTRIBUTING
39 changes: 38 additions & 1 deletion src/app/hooks.ts → src/app/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
// We disable the ESLint rule here because this is the designated place
// for importing and re-exporting the typed versions of hooks.
/* eslint-disable @typescript-eslint/no-restricted-imports */
import { type ReactNode, useEffect } from "react"
import { createSearchParams, useLocation, useNavigate } from "react-router-dom"
import { useDispatch, useSelector } from "react-redux"
import Cookies from "js-cookie"

import type { AppDispatch, RootState } from "./store"
import Cookies from "js-cookie"
import { paths } from "../routes"

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
Expand All @@ -24,3 +27,37 @@ export function useSessionMetadata(): SessionMetadata | undefined {
? (JSON.parse(sessionMetadata) as SessionMetadata)
: undefined
}

export type UseSessionChildren =
| ReactNode
| ((metadata: SessionMetadata) => ReactNode)

export type UseSessionOptions = Partial<{ next: boolean }>

export function useSession(
children: UseSessionChildren,
options: UseSessionOptions = {},
) {
const { next = true } = options

const { pathname } = useLocation()
const navigate = useNavigate()
const sessionMetadata = useSessionMetadata()

useEffect(() => {
if (!sessionMetadata) {
navigate({
pathname: paths._,
search: next
? createSearchParams({ next: pathname }).toString()
: undefined,
})
}
}, [sessionMetadata, navigate, next, pathname])

if (!sessionMetadata) return <></>

if (typeof children === "function") return children(sessionMetadata)

return children
}
71 changes: 0 additions & 71 deletions src/pages/agreementSignatureDetails/AgreementSignatureDetails.tsx

This file was deleted.

61 changes: 19 additions & 42 deletions src/pages/agreementSignatureList/AgreementSignatureList.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,32 @@
import * as pages from "codeforlife/components/page"
import { Stack, Typography } from "@mui/material"
import { type FC } from "react"
import { LinkIconButton } from "codeforlife/components/router"
import { TablePagination } from "codeforlife/components"
import { generatePath } from "react-router"
import { paths } from "../../routes"
import { useLazyListAgreementSignaturesQuery } from "../../api/agreementSignature"

import AgreementSignatureTable from "./AgreementSignatureTable"
import SignLatestAgreementForm from "./SignLatestAgreementForm"
import { useSession } from "../../app/hooks"

export interface AgreementSignatureListProps {}

const AgreementSignatureList: FC<AgreementSignatureListProps> = () => {
return (
const signedLatestAgreement = false // TODO: call endpoint
const latestAgreementId = "" // TODO: call endpoint

return useSession(({ contributor_id }) => (
<pages.Page>
<pages.Banner header="Agreement Signatures" />
{!signedLatestAgreement && (
<pages.Section>
<SignLatestAgreementForm
contributor={contributor_id}
agreementId={latestAgreementId}
/>
</pages.Section>
)}
<pages.Section>
<Typography variant="h1">Agreement Signature List</Typography>
<TablePagination
useLazyListQuery={useLazyListAgreementSignaturesQuery}
preferCacheValue
>
{agreementSignatures => (
<>
<Stack direction="row" gap={5}>
<Typography fontWeight="bold">ID</Typography>
<Typography fontWeight="bold">
Contributor (Agreement ID)
</Typography>
</Stack>
{agreementSignatures.map(agreementSignature => (
<Stack direction="row" key={agreementSignature.id} gap={5}>
<Typography fontWeight="bold">
{agreementSignature.id}
</Typography>
<Typography>
{agreementSignature.contributor} (
{agreementSignature.agreement_id})
</Typography>
<LinkIconButton
to={generatePath(paths.agreementSignatures.id._, {
id: agreementSignature.id,
})}
>
<Typography>View</Typography>
</LinkIconButton>
</Stack>
))}
</>
)}
</TablePagination>
<AgreementSignatureTable />
</pages.Section>
</pages.Page>
)
))
}

export default AgreementSignatureList
64 changes: 64 additions & 0 deletions src/pages/agreementSignatureList/AgreementSignatureTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material"
import { type FC } from "react"
import { LinkIconButton } from "codeforlife/components/router"
import { OpenInNew as OpenInNewIcon } from "@mui/icons-material"
import { TablePagination } from "codeforlife/components"

import { LINK_GH_CONTRIBUTING } from "../../app/env"
import { useLazyListAgreementSignaturesQuery } from "../../api/agreementSignature"

export interface AgreementSignatureTableProps {}

const AgreementSignatureTable: FC<AgreementSignatureTableProps> = () => {
return (
<TablePagination
useLazyListQuery={useLazyListAgreementSignaturesQuery}
preferCacheValue
>
{agreementSignatures => (
<TableContainer component={Paper}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Signed at</TableCell>
<TableCell>Link</TableCell>
</TableRow>
</TableHead>
<TableBody>
{agreementSignatures.map(agreementSignature => (
<TableRow key={agreementSignature.id}>
<TableCell>{agreementSignature.agreement_id}</TableCell>
<TableCell>
{agreementSignature.signed_at.toLocaleString()}
</TableCell>
<TableCell>
<LinkIconButton
to={LINK_GH_CONTRIBUTING.replace(
"{commitId}",
agreementSignature.agreement_id,
)}
target="_blank"
>
<OpenInNewIcon />
</LinkIconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</TablePagination>
)
}

export default AgreementSignatureTable
66 changes: 66 additions & 0 deletions src/pages/agreementSignatureList/SignLatestAgreementForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as forms from "codeforlife/components/form"
import { type FC } from "react"
import { Link } from "codeforlife/components/router"
import { PriorityHigh as PriorityHighIcon } from "@mui/icons-material"
import { Typography } from "@mui/material"
import { submitForm } from "codeforlife/utils/form"

import { type AgreementSignature } from "../../api/agreementSignature"
import { type Contributor } from "../../api/contributor"
import { LINK_GH_CONTRIBUTING } from "../../app/env"
import { useCreateAgreementSignatureMutation } from "../../api/agreementSignature"

export interface SignLatestAgreementFormProps {
contributor: Contributor["id"]
agreementId: AgreementSignature["agreement_id"]
}

const SignLatestAgreementForm: FC<SignLatestAgreementFormProps> = ({
contributor,
agreementId,
}) => {
const [createAgreementSignature] = useCreateAgreementSignatureMutation()

return (
<>
<Typography color="error.main">
You have not signed the latest agreement!
</Typography>
<forms.Form
initialValues={{
read_and_understood: false,
contributor,
agreement_id: agreementId,
signed_at: new Date(),
}}
onSubmit={submitForm(createAgreementSignature, {
exclude: ["read_and_understood"],
clean: values => ({ ...values, signed_at: new Date() }),
})}
>
<forms.CheckboxField
name="read_and_understood"
required
formControlLabelProps={{
label: (
<>
I have read and understood the{" "}
<Link
to={LINK_GH_CONTRIBUTING.replace("{commitId}", agreementId)}
>
agreement
</Link>
.
</>
),
}}
/>
<forms.SubmitButton className="alert" startIcon={<PriorityHighIcon />}>
Sign agreement
</forms.SubmitButton>
</forms.Form>
</>
)
}

export default SignLatestAgreementForm
Loading

0 comments on commit a7449f2

Please sign in to comment.