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

add signup form on the constituency page #88

Merged
merged 3 commits into from
Mar 27, 2024
Merged
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
251 changes: 251 additions & 0 deletions app/constituencies/[slug]/SignupShare.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
"use client";

import {
Form,
Button,
ButtonGroup,
Spinner,
InputGroup,
} from "react-bootstrap";

import {
FaShare,
FaPuzzlePiece,
FaCopy,
FaHandHoldingHeart,
} from "react-icons/fa6";
import { submitANForm } from "@/utils/AnApiSubmission";
import { rubik } from "@/utils/Fonts";
import { useRef, useState, useEffect } from "react";
import ConstituencyLookup from "@/components/forms/constituencyLookup";

const emailErrorMessage = (code: EmailErrorCode) => {
switch (code) {
case "EMAIL_INVALID":
return "Please add a valid email address.";
case "SERVER_ERROR":
return "Something went wrong signing you up. Please try again?";
default:
return "";
}
};

type FormData = {
emailOptIn: boolean;
email: string;
};

const initialFormState: FormData = {
emailOptIn: false,
email: "",
};

// Variables used to track state of the ConstituencyLookup components

export default function SignupShare({
constituencyData,
}: {
constituencyData: ConstituencyData;
}) {
const [subscribed, setSubscribed] = useState<string | null | false>(false);
const [formState, setFormState] = useState<FormData>(initialFormState);
const [emailError, setEmailError] = useState<EmailErrorCode | null>(null);
const validPostcode = useRef("");
const [constituency, setConstituency] = useState<Constituency | null>(null);
const [constituencyApiLoading, setConstituencyApiLoading] = useState(false);

// Variables used to track state of the email opt-in components
const formRef = useRef<HTMLFormElement | null>(null);

// Track whether the user is already subscribed via localStorage
useEffect(() => {
//string = subscription Date.now()
//null = not subscribed on client
//false = on server
setSubscribed(window.localStorage.getItem("fwd-subscribed"));
}, []);

const submitForm = async () => {
if (
constituency &&
formState.email &&
formRef.current &&
!formRef.current.email.validity.typeMismatch
) {
//TODO set source codes from current url params.
const anResponse = await submitANForm(
formState.email,
validPostcode.current,
constituency,
process.env.NEXT_PUBLIC_AN_POSTCODE_FORM || "",
["stop the tories", "movement forward", "election reminders", "join"],
"", // source codes,
);

if (anResponse.ok) {
window.localStorage.setItem("fwd-subscribed", Date.now().toString());
setSubscribed("Subscribed");
} else {
setEmailError("SERVER_ERROR"); //AN doesn't give error codes on failure
}
}

// VALIDATION
// Invalid email
if (
formState.emailOptIn &&
(!formState.email ||
(formRef.current && formRef.current.email.validity.typeMismatch))
) {
setEmailError("EMAIL_INVALID");
}

// // no postcode or invalid postcode or constituency/address not selected
// if (
// !validPostcode.current ||
// !apiResponse ||
// !apiResponse.constituencies ||
// apiResponse.constituencies.length == 0
// ) {
// // User hasn't input anything or invalid postcode
// setPostError("POSTCODE_INVALID");
// return;
// }

// //not selected constituency or address
// if (
// apiResponse.constituencies.length > 1 &&
// formState.constituencyIndex === false
// ) {
// setPostError("UNCLEAR_CONSTITUENCY");
// }
};

if (!subscribed) {
return (
<Form
className="form-search"
ref={formRef}
action={submitForm}
noValidate
>
<h3>Join the movement forward</h3>
<ul>
<li>
<strong>Be counted</strong>, I&apos;m voting tactically!
</li>
<li>Get a voting plan</li>
<li>Get reminders and actions</li>
</ul>
{/* Renders the postcode box, makes API calls, and if necessary shows an address/constituency picker */}
<ConstituencyLookup
validPostcode={validPostcode}
constituency={constituency}
setConstituency={setConstituency}
loading={constituencyApiLoading}
setLoading={setConstituencyApiLoading}
filterConstituencySlug={constituencyData.constituencyIdentifiers.slug}
/>

<div className="my-3">
<>
<InputGroup hasValidation className="my-3">
<Form.Control
name="email"
size="lg"
type="email"
placeholder="Your Email"
value={formState.email}
isInvalid={!!emailError}
onChange={(e) => {
setFormState({ ...formState, email: e.target.value });
if (!e.target.validity.typeMismatch) {
setEmailError(null);
}
}}
className="invalid-text-greyed"
/>
<Form.Control.Feedback
className="fw-bold fst-italic px-2 pt-1 text-white"
type="invalid"
>
{emailError ? emailErrorMessage(emailError) : ""}
</Form.Control.Feedback>
</InputGroup>
<p className="small">
You&apos;re opting in to receive emails. We store your email
address, postcode, and constituency, so we can send you exactly
the information you need.
</p>
</>
</div>

<div className="d-flex justify-content-between mt-3">
<Button
variant="light"
size="lg"
type="submit"
disabled={!constituency}
aria-disabled={!constituency}
style={{ width: "66%" }}
>
{constituencyApiLoading && (
<>
<Spinner
as="span"
animation="border"
size="sm"
role="status"
area-hidden="true"
/>
<span className="visually-hidden">Loading...</span>{" "}
</>
)}
<span className={`${rubik.className} fw-bold`}>Join</span>
</Button>
<a
href="https://themovementforward.com/privacy/"
target="_blank"
rel="noreferrer"
className="btn btn-link btn-sm"
role="button"
>
<span className={`${rubik.className} fw-bold`}>Privacy Policy</span>
</a>
</div>
</Form>
);
} else {
return (
<div className="form-search">
<h3>Grow this movement</h3>
<p>You&apos;re in! Now let&apos;s build our numbers</p>
<ButtonGroup size="lg" vertical className="w-100 mb-0">
{/* TODO share link */}
<Button href="#" variant="light">
<FaShare /> Share with friends &amp; family
</Button>
<Button
variant="light"
onClick={() =>
window.navigator.clipboard.writeText(window.location.href)
}
>
<FaCopy />
Copy link to this page
</Button>
<Button
href="https://themovementforward.com/volunteer/"
variant="light"
>
<FaPuzzlePiece /> Volunteer
</Button>

<Button href="/donate" variant="light">
<FaHandHoldingHeart /> Support our crowdfunder
</Button>
</ButtonGroup>
</div>
);
}
}
37 changes: 4 additions & 33 deletions app/constituencies/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Col, Container, Row, ButtonGroup, Button } from "react-bootstrap";
import { Col, Container, Row } from "react-bootstrap";
import Link from "next/link";
import Header from "@/components/Header";
import ActionBox from "@/components/info_box/ActionBox";
import ImpliedChart from "@/components/info_box/ImpliedChart";
import MRPChart from "@/components/info_box/MRPChart";
import PlanToVoteBox from "@/components/info_box/PlanToVoteBox";
Expand All @@ -12,13 +11,7 @@ import {
getConstituencySlugs,
} from "@/utils/constituencyData";
import { notFound } from "next/navigation";
import {
FaShare,
FaPuzzlePiece,
FaCopy,
FaHandHoldingHeart,
} from "react-icons/fa6";
import PostcodeLookup from "@/components/constituency_lookup/ConstituencyLookup";
import SignupShare from "./SignupShare";

export const dynamicParams = false; // Don't allow params not in generateStaticParams

Expand Down Expand Up @@ -86,6 +79,7 @@ export default async function ConstituencyPage({
}
}

// NO ADVICE OVERRIDE (NI & Speaker)
if (constituencyData.recommendation.partySlug === "None") {
return (
<>
Expand Down Expand Up @@ -157,30 +151,7 @@ export default async function ConstituencyPage({
</Row>
<Row xs={1} lg={3}>
<Col md={7} className="pb-3">
<div className="form-search">
<h3>Grow this movement</h3>
<p>You&apos;re in! Now let&apos;s build our numbers</p>
<ButtonGroup size="lg" vertical className="w-100 mb-0">
{/* TODO share link and clipboard copy */}
<Button href="#" variant="light">
<FaShare /> Share with friends &amp; family
</Button>
<Button href="#" variant="light">
<FaCopy />
Copy link to this page
</Button>
<Button
href="https://themovementforward.com/volunteer/"
variant="light"
>
<FaPuzzlePiece /> Volunteer
</Button>

<Button href="/donate" variant="light">
<FaHandHoldingHeart /> Support our crowdfunder
</Button>
</ButtonGroup>
</div>
<SignupShare constituencyData={constituencyData} />
</Col>
<Col md={7} className="pb-3">
<p style={{ fontSize: "26px" }}>
Expand Down
6 changes: 5 additions & 1 deletion components/forms/constituencyLookup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ interface IProps {
setConstituency: Dispatch<SetStateAction<Constituency | null>>;
loading: boolean;
setLoading: Dispatch<SetStateAction<boolean>>;
//Prevent the selected constituency
//displaying on the constituencies own page.
filterConstituencySlug?: string;
}

const ConstituencyLookup = ({
Expand All @@ -150,6 +153,7 @@ const ConstituencyLookup = ({
setConstituency,
loading,
setLoading,
filterConstituencySlug,
}: IProps) => {
const [formState, setFormState] = useState<FormData>(initialFormState);
const [apiResponse, setApiResponse] = useState<
Expand Down Expand Up @@ -275,7 +279,7 @@ const ConstituencyLookup = ({
</Form.Control.Feedback>
</InputGroup>

{constituency && (
{constituency && constituency.slug != filterConstituencySlug && (
<InputGroup className="my-3">
<Form.Control
name="constituency-display"
Expand Down
Loading