Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 0 deletions components/EditProfilePage/EditProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { TestimoniesTab } from "./TestimoniesTab"
import { useFlags } from "components/featureFlags"
import LoginPage from "components/Login/Login"
import { PendingUpgradeBanner } from "components/PendingUpgradeBanner"
import { PendingLegislatorBanner } from "components/PendingLegislatorBanner"
import { FollowersTab } from "./FollowersTab"

const tabTitle = ["about-you", "testimonies", "following", "followers"] as const
Expand Down Expand Up @@ -114,6 +115,7 @@ export function EditProfileForm({

const { claims, user } = useAuth()
const isPendingUpgrade = claims?.role === "pendingUpgrade"
const isPendingLegislator = claims?.role === "pendingLegislator"

isOrg = isOrg || isPendingUpgrade

Expand Down Expand Up @@ -175,6 +177,7 @@ export function EditProfileForm({
return (
<>
{isPendingUpgrade && <PendingUpgradeBanner />}
{isPendingLegislator && <PendingLegislatorBanner />}

<Container>
<EditProfileHeader
Expand Down
13 changes: 13 additions & 0 deletions components/PendingLegislatorBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MessageBanner } from "./shared/MessageBanner"
import { useTranslation } from "next-i18next"

export const PendingLegislatorBanner = () => {
const { t } = useTranslation("common")
return (
<MessageBanner
icon={"/Clock.svg"}
heading={t("pending_legislator_warning.header")}
content={t("pending_legislator_warning.content")}
/>
)
}
20 changes: 20 additions & 0 deletions components/api/upgrade-legislator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { mapleClient } from "./maple-client"

/**
* Changes the user's role to "legislator", approving their legislator account request.
*
* Requires the logged-in user to be an admin.
*/
export async function acceptLegislatorRequest(userId: string) {
return mapleClient.patch(`/api/users/${userId}`, { role: "legislator" })
}

/**
* Rejects a pending legislator request by reverting the user's role to "user".
* Also releases the claimed member code so it can be claimed by others.
*
* Requires the logged-in user to be an admin.
*/
export async function rejectLegislatorRequest(userId: string) {
return mapleClient.patch(`/api/users/${userId}`, { role: "user" })
}
7 changes: 7 additions & 0 deletions components/auth/AuthModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SignInModal from "./SignInModal"
import UserSignUpModal from "./UserSignUpModal"
import OrgSignUpModal from "./OrgSignUpModal"
import LegislatorSignUpModal from "./LegislatorSignUpModal"
import StartModal from "./StartModal"
import ForgotPasswordModal from "./ForgotPasswordModal"
import VerifyEmailModal from "./VerifyEmailModal"
Expand Down Expand Up @@ -28,6 +29,7 @@ export default function AuthModal() {
onHide={close}
onIndividualUserClick={() => setCurrentModal("userSignUp")}
onOrgUserClick={() => setCurrentModal("orgSignUp")}
onLegislatorUserClick={() => setCurrentModal("legislatorSignUp")}
/>
<SignInModal
show={currentModal === "signIn"}
Expand All @@ -44,6 +46,11 @@ export default function AuthModal() {
onHide={close}
onSuccessfulSubmit={() => setCurrentModal("verifyEmail")}
/>
<LegislatorSignUpModal
show={currentModal === "legislatorSignUp"}
onHide={close}
onSuccessfulSubmit={() => setCurrentModal("verifyEmail")}
/>
<VerifyEmailModal show={currentModal === "verifyEmail"} onHide={close} />
<ForgotPasswordModal
show={currentModal === "forgotPassword"}
Expand Down
242 changes: 242 additions & 0 deletions components/auth/LegislatorSignUpModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { useEffect, useMemo, useState } from "react"
import clsx from "clsx"
import type { ModalProps } from "react-bootstrap"
import { useForm } from "react-hook-form"
import { Alert, Button, Col, Form, Modal, Row, Stack } from "../bootstrap"
import { LoadingButton } from "../buttons"
import Input from "../forms/Input"
import PasswordInput from "../forms/PasswordInput"
import {
CreateLegislatorWithEmailAndPasswordData,
useCreateLegislatorWithEmailAndPassword
} from "./hooks"
import TermsOfServiceModal from "./TermsOfServiceModal"
import { useTranslation } from "next-i18next"
import { Search } from "../legislatorSearch"
import { useClaimedMemberCodes, useMemberSearch } from "../db/members"
import { ProfileMember } from "../db"

export default function LegislatorSignUpModal({
show,
onHide,
onSuccessfulSubmit
}: Pick<ModalProps, "show" | "onHide"> & {
onSuccessfulSubmit: () => void
onHide: () => void
}) {
const {
register,
handleSubmit,
reset,
getValues,
trigger,
formState: { errors }
} = useForm<CreateLegislatorWithEmailAndPasswordData>()

const [tosStep, setTosStep] = useState<"not-agreed" | "reading" | "agreed">(
"not-agreed"
)
const [selectedMember, setSelectedMember] = useState<ProfileMember | null>(
null
)
const [memberError, setMemberError] = useState<string | undefined>()

const showTos = tosStep === "reading"

const createLegislatorWithEmailAndPassword =
useCreateLegislatorWithEmailAndPassword()

const { index } = useMemberSearch()
const { claimedCodes } = useClaimedMemberCodes()

const memberIndex = useMemo(() => {
const all = [...(index?.representatives ?? []), ...(index?.senators ?? [])]
if (!claimedCodes) return all
return all.filter(m => !claimedCodes.has(m.MemberCode))
}, [index, claimedCodes])

const { t } = useTranslation("auth")

useEffect(() => {
if (!show) {
reset()
setTosStep("not-agreed")
setSelectedMember(null)
setMemberError(undefined)
createLegislatorWithEmailAndPassword.reset()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [show, reset])

const onSubmit = handleSubmit(newUser => {
if (!selectedMember) {
setMemberError(
t("legislatorRequired") ?? "Please select your legislator profile."
)
return
}
setMemberError(undefined)
const promise = createLegislatorWithEmailAndPassword.execute({
...newUser,
memberCode: selectedMember.id
})
promise.then(onSuccessfulSubmit).catch(() => {})
})

async function handleContinueClick() {
if (!selectedMember) {
setMemberError(
t("legislatorRequired") ?? "Please select your legislator profile."
)
return
}
setMemberError(undefined)
const isValid = await trigger()
if (isValid) {
setTosStep("reading")
}
}

useEffect(() => {
if (tosStep === "agreed") {
const loadingbtn = document.getElementById("legislator-loading-button")
loadingbtn?.click()
}
}, [tosStep])

return (
<>
<Modal
show={show}
onHide={onHide}
aria-labelledby="legislator-sign-up-modal"
centered
size="lg"
className={clsx(showTos && "opacity-0")}
>
<Modal.Header closeButton>
<Modal.Title id="legislator-sign-up-modal">
{t("signUpAsLegislator") ?? "Sign Up as Legislator"}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Col md={12} className="mx-auto">
{createLegislatorWithEmailAndPassword.error ? (
<Alert variant="danger">
{createLegislatorWithEmailAndPassword.error.message}
</Alert>
) : null}

<Form noValidate onSubmit={onSubmit}>
<TermsOfServiceModal
show={showTos}
onHide={() => setTosStep("not-agreed")}
onAgree={() => setTosStep("agreed")}
/>

<Stack gap={3} className="mb-4">
<Input
label={t("email") ?? "Email"}
type="email"
{...register("email", {
required: t("emailIsRequired") ?? "An email is required."
})}
error={errors.email?.message}
/>

<Input
label={t("fullName") ?? "Full Name"}
type="text"
{...register("fullName", {
validate: value =>
value.trim().length >= 2 ||
t("errEmptyAndMinLength").toString(),
required: t("nameIsRequired") ?? "A full name is required."
})}
error={errors.fullName?.message}
/>

<Form.Group controlId="legislatorSearch">
<Form.Label>{t("selectLegislatorHeader")}</Form.Label>
<Search
index={memberIndex}
update={member => {
setSelectedMember(member)
if (member) setMemberError(undefined)
}}
memberId={selectedMember?.id}
placeholder={t("searchLegislatorsPlaceholder")}
menuPortalTarget={document.body}
styles={{
menuPortal: (base: any) => ({ ...base, zIndex: 9999 })
}}
/>
{memberError && (
<Form.Text className="text-danger">{memberError}</Form.Text>
)}
</Form.Group>

<Row className="g-3">
<Col md={6}>
<PasswordInput
label={t("password") ?? "Password"}
{...register("password", {
required:
t("passwordRequired") ?? "A password is required.",
minLength: {
value: 8,
message:
t("passwordLength") ??
"Your password must be 8 characters or longer."
},
deps: ["confirmedPassword"]
})}
error={errors.password?.message}
/>
</Col>

<Col md={6}>
<PasswordInput
label={t("confirmPassword") ?? "Confirm Password"}
{...register("confirmedPassword", {
required:
t("mustConfirmPassword") ??
"You must confirm your password.",
validate: confirmedPassword => {
const password = getValues("password")
return confirmedPassword !== password
? t("mustMatch") ??
"Confirmed password must match password."
: undefined
}
})}
error={errors.confirmedPassword?.message}
/>
</Col>
</Row>
</Stack>
{tosStep === "agreed" ? (
<LoadingButton
id="legislator-loading-button"
type="submit"
className="w-100"
loading={createLegislatorWithEmailAndPassword.loading}
>
{t("signUp") ?? "Sign Up"}
</LoadingButton>
) : (
<Button
className="w-100"
type="button"
onClick={handleContinueClick}
>
{t("continue") ?? "Continue"}
</Button>
)}
</Form>
</Col>
</Modal.Body>
</Modal>
</>
)
}
Loading
Loading