Skip to content

Commit 4c6c038

Browse files
committed
frontend changes
1 parent e8ba1f1 commit 4c6c038

14 files changed

Lines changed: 463 additions & 21 deletions

File tree

components/EditProfilePage/EditProfilePage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { TestimoniesTab } from "./TestimoniesTab"
2828
import { useFlags } from "components/featureFlags"
2929
import LoginPage from "components/Login/Login"
3030
import { PendingUpgradeBanner } from "components/PendingUpgradeBanner"
31+
import { PendingLegislatorBanner } from "components/PendingLegislatorBanner"
3132
import { FollowersTab } from "./FollowersTab"
3233

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

115116
const { claims, user } = useAuth()
116117
const isPendingUpgrade = claims?.role === "pendingUpgrade"
118+
const isPendingLegislator = claims?.role === "pendingLegislator"
117119

118120
isOrg = isOrg || isPendingUpgrade
119121

@@ -175,6 +177,7 @@ export function EditProfileForm({
175177
return (
176178
<>
177179
{isPendingUpgrade && <PendingUpgradeBanner />}
180+
{isPendingLegislator && <PendingLegislatorBanner />}
178181

179182
<Container>
180183
<EditProfileHeader
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { MessageBanner } from "./shared/MessageBanner"
2+
import { useTranslation } from "next-i18next"
3+
4+
export const PendingLegislatorBanner = () => {
5+
const { t } = useTranslation("common")
6+
return (
7+
<MessageBanner
8+
icon={"/Clock.svg"}
9+
heading={t("pending_legislator_warning.header")}
10+
content={t("pending_legislator_warning.content")}
11+
/>
12+
)
13+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { mapleClient } from "./maple-client"
2+
3+
/**
4+
* Changes the user's role to "legislator", approving their legislator account request.
5+
*
6+
* Requires the logged-in user to be an admin.
7+
*/
8+
export async function acceptLegislatorRequest(userId: string) {
9+
return mapleClient.patch(`/api/users/${userId}`, { role: "legislator" })
10+
}
11+
12+
/**
13+
* Rejects a pending legislator request by reverting the user's role to "user".
14+
* Also releases the claimed member code so it can be claimed by others.
15+
*
16+
* Requires the logged-in user to be an admin.
17+
*/
18+
export async function rejectLegislatorRequest(userId: string) {
19+
return mapleClient.patch(`/api/users/${userId}`, { role: "user" })
20+
}

components/auth/AuthModal.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import SignInModal from "./SignInModal"
22
import UserSignUpModal from "./UserSignUpModal"
33
import OrgSignUpModal from "./OrgSignUpModal"
4+
import LegislatorSignUpModal from "./LegislatorSignUpModal"
45
import StartModal from "./StartModal"
56
import ForgotPasswordModal from "./ForgotPasswordModal"
67
import VerifyEmailModal from "./VerifyEmailModal"
@@ -28,6 +29,7 @@ export default function AuthModal() {
2829
onHide={close}
2930
onIndividualUserClick={() => setCurrentModal("userSignUp")}
3031
onOrgUserClick={() => setCurrentModal("orgSignUp")}
32+
onLegislatorUserClick={() => setCurrentModal("legislatorSignUp")}
3133
/>
3234
<SignInModal
3335
show={currentModal === "signIn"}
@@ -44,6 +46,11 @@ export default function AuthModal() {
4446
onHide={close}
4547
onSuccessfulSubmit={() => setCurrentModal("verifyEmail")}
4648
/>
49+
<LegislatorSignUpModal
50+
show={currentModal === "legislatorSignUp"}
51+
onHide={close}
52+
onSuccessfulSubmit={() => setCurrentModal("verifyEmail")}
53+
/>
4754
<VerifyEmailModal show={currentModal === "verifyEmail"} onHide={close} />
4855
<ForgotPasswordModal
4956
show={currentModal === "forgotPassword"}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { useEffect, useMemo, useState } from "react"
2+
import clsx from "clsx"
3+
import type { ModalProps } from "react-bootstrap"
4+
import { useForm } from "react-hook-form"
5+
import {
6+
Alert,
7+
Button,
8+
Col,
9+
Form,
10+
Modal,
11+
Row,
12+
Stack
13+
} from "../bootstrap"
14+
import { LoadingButton } from "../buttons"
15+
import Input from "../forms/Input"
16+
import PasswordInput from "../forms/PasswordInput"
17+
import {
18+
CreateLegislatorWithEmailAndPasswordData,
19+
useCreateLegislatorWithEmailAndPassword
20+
} from "./hooks"
21+
import TermsOfServiceModal from "./TermsOfServiceModal"
22+
import { useTranslation } from "next-i18next"
23+
import { Search } from "../legislatorSearch"
24+
import { useClaimedMemberCodes, useMemberSearch } from "../db/members"
25+
import { ProfileMember } from "../db"
26+
27+
export default function LegislatorSignUpModal({
28+
show,
29+
onHide,
30+
onSuccessfulSubmit
31+
}: Pick<ModalProps, "show" | "onHide"> & {
32+
onSuccessfulSubmit: () => void
33+
onHide: () => void
34+
}) {
35+
const {
36+
register,
37+
handleSubmit,
38+
reset,
39+
getValues,
40+
trigger,
41+
formState: { errors }
42+
} = useForm<CreateLegislatorWithEmailAndPasswordData>()
43+
44+
const [tosStep, setTosStep] = useState<"not-agreed" | "reading" | "agreed">(
45+
"not-agreed"
46+
)
47+
const [selectedMember, setSelectedMember] = useState<ProfileMember | null>(
48+
null
49+
)
50+
const [memberError, setMemberError] = useState<string | undefined>()
51+
52+
const showTos = tosStep === "reading"
53+
54+
const createLegislatorWithEmailAndPassword =
55+
useCreateLegislatorWithEmailAndPassword()
56+
57+
const { index } = useMemberSearch()
58+
const { claimedCodes } = useClaimedMemberCodes()
59+
60+
const memberIndex = useMemo(() => {
61+
const all = [
62+
...(index?.representatives ?? []),
63+
...(index?.senators ?? [])
64+
]
65+
if (!claimedCodes) return all
66+
return all.filter(m => !claimedCodes.has(m.MemberCode))
67+
}, [index, claimedCodes])
68+
69+
const { t } = useTranslation("auth")
70+
71+
useEffect(() => {
72+
if (!show) {
73+
reset()
74+
setTosStep("not-agreed")
75+
setSelectedMember(null)
76+
setMemberError(undefined)
77+
createLegislatorWithEmailAndPassword.reset()
78+
}
79+
// eslint-disable-next-line react-hooks/exhaustive-deps
80+
}, [show, reset])
81+
82+
const onSubmit = handleSubmit(newUser => {
83+
if (!selectedMember) {
84+
setMemberError(
85+
t("legislatorRequired") ?? "Please select your legislator profile."
86+
)
87+
return
88+
}
89+
setMemberError(undefined)
90+
const promise = createLegislatorWithEmailAndPassword.execute({
91+
...newUser,
92+
memberCode: selectedMember.id
93+
})
94+
promise.then(onSuccessfulSubmit).catch(() => {})
95+
})
96+
97+
async function handleContinueClick() {
98+
if (!selectedMember) {
99+
setMemberError(
100+
t("legislatorRequired") ?? "Please select your legislator profile."
101+
)
102+
return
103+
}
104+
setMemberError(undefined)
105+
const isValid = await trigger()
106+
if (isValid) {
107+
setTosStep("reading")
108+
}
109+
}
110+
111+
useEffect(() => {
112+
if (tosStep === "agreed") {
113+
const loadingbtn = document.getElementById("legislator-loading-button")
114+
loadingbtn?.click()
115+
}
116+
}, [tosStep])
117+
118+
return (
119+
<>
120+
<Modal
121+
show={show}
122+
onHide={onHide}
123+
aria-labelledby="legislator-sign-up-modal"
124+
centered
125+
size="lg"
126+
className={clsx(showTos && "opacity-0")}
127+
>
128+
<Modal.Header closeButton>
129+
<Modal.Title id="legislator-sign-up-modal">
130+
{t("signUpAsLegislator") ?? "Sign Up as Legislator"}
131+
</Modal.Title>
132+
</Modal.Header>
133+
<Modal.Body>
134+
<Col md={12} className="mx-auto">
135+
{createLegislatorWithEmailAndPassword.error ? (
136+
<Alert variant="danger">
137+
{createLegislatorWithEmailAndPassword.error.message}
138+
</Alert>
139+
) : null}
140+
141+
<Form noValidate onSubmit={onSubmit}>
142+
<TermsOfServiceModal
143+
show={showTos}
144+
onHide={() => setTosStep("not-agreed")}
145+
onAgree={() => setTosStep("agreed")}
146+
/>
147+
148+
<Stack gap={3} className="mb-4">
149+
<Input
150+
label={t("email") ?? "Email"}
151+
type="email"
152+
{...register("email", {
153+
required: t("emailIsRequired") ?? "An email is required."
154+
})}
155+
error={errors.email?.message}
156+
/>
157+
158+
<Input
159+
label={t("fullName") ?? "Full Name"}
160+
type="text"
161+
{...register("fullName", {
162+
validate: value =>
163+
value.trim().length >= 2 ||
164+
t("errEmptyAndMinLength").toString(),
165+
required: t("nameIsRequired") ?? "A full name is required."
166+
})}
167+
error={errors.fullName?.message}
168+
/>
169+
170+
<Form.Group controlId="legislatorSearch">
171+
<Form.Label>
172+
{t("selectLegislator") ?? "Select your legislator profile"}
173+
</Form.Label>
174+
<Search
175+
index={memberIndex}
176+
update={member => {
177+
setSelectedMember(member)
178+
if (member) setMemberError(undefined)
179+
}}
180+
memberId={selectedMember?.id}
181+
placeholder={
182+
t("searchLegislators") ?? "Search by name or district..."
183+
}
184+
menuPortalTarget={document.body}
185+
styles={{ menuPortal: base => ({ ...base, zIndex: 9999 }) }}
186+
/>
187+
{memberError && (
188+
<Form.Text className="text-danger">{memberError}</Form.Text>
189+
)}
190+
</Form.Group>
191+
192+
<Row className="g-3">
193+
<Col md={6}>
194+
<PasswordInput
195+
label={t("password") ?? "Password"}
196+
{...register("password", {
197+
required:
198+
t("passwordRequired") ?? "A password is required.",
199+
minLength: {
200+
value: 8,
201+
message:
202+
t("passwordLength") ??
203+
"Your password must be 8 characters or longer."
204+
},
205+
deps: ["confirmedPassword"]
206+
})}
207+
error={errors.password?.message}
208+
/>
209+
</Col>
210+
211+
<Col md={6}>
212+
<PasswordInput
213+
label={t("confirmPassword") ?? "Confirm Password"}
214+
{...register("confirmedPassword", {
215+
required:
216+
t("mustConfirmPassword") ??
217+
"You must confirm your password.",
218+
validate: confirmedPassword => {
219+
const password = getValues("password")
220+
return confirmedPassword !== password
221+
? t("mustMatch") ??
222+
"Confirmed password must match password."
223+
: undefined
224+
}
225+
})}
226+
error={errors.confirmedPassword?.message}
227+
/>
228+
</Col>
229+
</Row>
230+
</Stack>
231+
{tosStep === "agreed" ? (
232+
<LoadingButton
233+
id="legislator-loading-button"
234+
type="submit"
235+
className="w-100"
236+
loading={createLegislatorWithEmailAndPassword.loading}
237+
>
238+
{t("signUp") ?? "Sign Up"}
239+
</LoadingButton>
240+
) : (
241+
<Button
242+
className="w-100"
243+
type="button"
244+
onClick={handleContinueClick}
245+
>
246+
{t("continue") ?? "Continue"}
247+
</Button>
248+
)}
249+
</Form>
250+
</Col>
251+
</Modal.Body>
252+
</Modal>
253+
</>
254+
)
255+
}

0 commit comments

Comments
 (0)