From 9df53f1971a2f51d4b7a29071bc386719bd81d19 Mon Sep 17 00:00:00 2001 From: James Southern Date: Sat, 23 Mar 2024 18:04:03 +0000 Subject: [PATCH] update the home page & constituency lookup ui (#83) * update the navbar, footer, home page & constituency page ui in line with Josh's designs * Update the css to match * add back in form-check class which seems to be sometimes added by bootstrap-react and sometimes not? * fix css for footer svgs * update tactical voting advice to handle tbc, vote with heart * remove flex from svg div container for safari * rename non voters to didn't vote * bring updates back into the refactored front page form --- app/constituencies/[slug]/page.tsx | 161 ++++- app/globals.scss | 601 +++++++++++++++--- app/page.tsx | 15 +- .../ConstituencyLookup.tsx | 350 +++++----- components/footer/Footer.tsx | 129 ++-- components/footer/FooterLink.tsx | 5 +- .../forms/ConstituencyFormWithSignup.tsx | 245 ++++--- components/forms/constituencyLookup.tsx | 123 ++-- components/info_box/ImpliedChart.tsx | 17 +- components/info_box/MRPChart.tsx | 20 +- components/info_box/PlanToVoteBox.tsx | 108 ++-- components/info_box/TacticalReasoningBox.tsx | 23 +- .../info_box/ToryCantWinReasoningBox.tsx | 92 +++ components/navigation/Navigation.tsx | 87 +-- components/sections/MovementSection.tsx | 114 ++-- utils/Echarts.ts | 12 +- utils/Party.ts | 14 +- 17 files changed, 1367 insertions(+), 749 deletions(-) create mode 100644 components/info_box/ToryCantWinReasoningBox.tsx diff --git a/app/constituencies/[slug]/page.tsx b/app/constituencies/[slug]/page.tsx index d5f683e..a608f64 100644 --- a/app/constituencies/[slug]/page.tsx +++ b/app/constituencies/[slug]/page.tsx @@ -1,4 +1,5 @@ -import { Col, Container, Row } from "react-bootstrap"; +import { Col, Container, Row, ButtonGroup, Button } 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"; @@ -11,6 +12,13 @@ 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"; export const dynamicParams = false; // Don't allow params not in generateStaticParams @@ -54,6 +62,30 @@ export default async function ConstituencyPage({ (a, b) => b.votePercent - a.votePercent, ); + let tacticalVoteHeader = ""; + let tacticalVoteAdvice = ""; + let tacticalVoteClass = ""; + + if (constituencyData.otherVoteData.conservativeWinUnlikely) { + tacticalVoteHeader = "Tories unlikely to win here"; + tacticalVoteAdvice = "Vote with your heart"; + tacticalVoteClass = "party-your-heart"; + } else { + tacticalVoteHeader = "The Tactical Vote is"; + + if (constituencyData.recommendation.partySlug) { + tacticalVoteAdvice = partyNameFromSlug( + constituencyData.recommendation.partySlug, + ); + tacticalVoteClass = partyCssClassFromSlug( + constituencyData.recommendation.partySlug, + ); + } else { + tacticalVoteClass = "party-too-soon"; + tacticalVoteAdvice = "Too Soon to call"; + } + } + if (constituencyData.recommendation.partySlug === "None") { return ( <> @@ -92,50 +124,139 @@ export default async function ConstituencyPage({

{constituencyData.constituencyIdentifiers.name}

Bookmark this page and check back before the election for updated - info. + info. Not your constituency?{" "} + + Search here. +

-
+
-

The tactical vote is

+

{tacticalVoteHeader}

+

+ {tacticalVoteAdvice} +

+

+ Why? +

+
+
+
+ - -

- {partyNameFromSlug(constituencyData.recommendation.partySlug)} -

+ +

be counted, stick together!

- - - + +
+

Grow this movement

+

You're in! Now let's build our numbers

+ + {/* TODO share link and clipboard copy */} + + + + + + +
- - + +

+ Reasons to be counted +

+

+ 1. Show how many of us are voting tactically and not just for + the party we're lending our vote to, and that we want our + votes to count next time. +

+

+ 2. Our large numbers show that the country is rejecting the + narrative the right wing media and think tanks spin. +

+

+ 3. Together we can be a huge independent influence on the next + government, for Proportional Representation, and other + crucial, common sense, policies. +

- +
+
+
+
+ + + + {constituencyData.otherVoteData.conservativeWinUnlikely ? ( + <> +

Why Tories Won't Win Here

+

({constituencyData.constituencyIdentifiers.name})

+ + ) : constituencyData.recommendation.partySlug ? ( + <> +

+ Why vote{" "} + + {partyNameFromSlug( + constituencyData.recommendation.partySlug, + )} + {" "} + here? +

+

({constituencyData.constituencyIdentifiers.name})

+ + ) : ( + <> +

Why it's too soon to call here

+

({constituencyData.constituencyIdentifiers.name})

+ + )} + +
+ - + + + + + Read more about our process + + + + -
diff --git a/app/globals.scss b/app/globals.scss index e984707..328b9cc 100644 --- a/app/globals.scss +++ b/app/globals.scss @@ -91,7 +91,21 @@ $bs-link-hover-color-rgb: 238, 102, 238; /***** GLOBAL CSS VARIABLES *****/ :root { - --mvtfwd-pink-strong: #ff66ff; + --mf-pink: #ff99ff; + --mf-pink-light: #ffccff; + --mf-pink-strong: #ff66ff; + --mf-pink-dark: #ee66ee; + --bs-link-color: var(--bs-black); + --bs-link-color-rgb: 0, 0, 0; + --bs-link-decoration: underline; + --bs-link-hover-color: var(--mf-pink-dark); + --bs-link-hover-color-rgb: 238, 102, 238; + --bs-red-rgb: 220, 53, 69; + --bs-orange-rgb: 253, 126, 20; + --bs-green-rgb: 25, 135, 84; + --bs-blue-rgb: 13, 110, 253; + --bs-gray-150: #f1f3f5; + --con-party-color: #0087dc; --lab-party-color: #e4003b; --ld-party-color: #faa61a; @@ -103,6 +117,40 @@ $bs-link-hover-color-rgb: 238, 102, 238; --other-party-color: var(--bs-gray-600); } +.btn-primary { + --bs-btn-color: #fff; + --bs-btn-bg: var(--mf-pink-strong); + --bs-btn-border-color: var(--mf-pink-strong); + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: var(--mf-pink-dark); + --bs-btn-hover-border-color: var(--mf-pink-dark); + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: var(--mf-pink-dark); + --bs-btn-active-border-color: var(--mf-pink-dark); + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: var(--mf-pink-strong); + --bs-btn-disabled-border-color: var(--mf-pink-strong); +} + +.btn-link { + --bs-btn-font-weight: 400; + --bs-btn-color: var(--bs-link-color); + --bs-btn-bg: transparent; + --bs-btn-border-color: transparent; + --bs-btn-hover-color: var(--bs-link-hover-color); + --bs-btn-hover-bg: #fff; + --bs-btn-hover-border-color: transparent; + --bs-btn-active-color: var(--bs-link-hover-color); + --bs-btn-active-border-color: transparent; + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-border-color: transparent; + --bs-btn-box-shadow: 0 0 0 #000; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + text-decoration: underline; +} + /***** GLOBAL DEFAULT STYLES *****/ // TODO: Many of these could be Next.js CSS Modules as they only affect a single component @@ -121,6 +169,7 @@ $bs-link-hover-color-rgb: 238, 102, 238; /* TYPOGRAPHY */ body { font-size: 18px; + line-height: 1.3; } html, @@ -128,8 +177,122 @@ body { background-color: var(--bs-gray-300); } +H1, +H2, +H3, +H4, +H5, +H6, +navbar, +nav, +.btn { + font-family: var(--font-rubik); + line-height: 1; +} + +H1, +H2, +H3, +H4, +H5, +H6 { + font-weight: bold; + text-transform: uppercase; +} + +.btn { + line-height: 1.3; + font-weight: bold; +} + +header h6 { + color: var(--bs-gray-900); + background-color: var(--bs-white); + display: inline-block; + padding: 0.2rem 0.4rem; +} + +.small, +small { + font-size: 0.75em; + line-height: 1.2; +} + +/* LAYOUT */ + +.alignwide { + margin: 0px calc(25% - 25vw); + max-width: 100vw; + /*width: 100vw;*/ +} + +.alignfull { + margin: 0px calc(50% - 50vw); + max-width: 100vw; + width: 100vw; +} + +.py-6 { + padding-top: 6rem !important; + padding-bottom: 6rem !important; +} + +@include media-breakpoint-up(md) { + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.6666%; + } +} + +/* LINKS */ + +p a:link, +p a:visited, +p a:hover, +p a:active { + font-weight: bold; +} + +a.dropdown-item:link, +a.dropdown-item:visited, +a.dropdown-item:hover, +a.dropdown-item:active, +a.dropdown-item i { + color: bs var(--bs-dark); +} + +main .section-light p a:link, +main .section-light p a:visited, +main .section-light p a:hover, +main .section-light p a:active { + color: var(--bs-black); +} + +main .section-dark p a:link, +main .section-dark p a:visited, +main .section-dark p a:hover, +main .section-dark p a:active { + color: var(--bs-white); +} + +main .section-darker p a:link, +main .section-darker p a:visited, +main .section-darker p a:hover, +main .section-darker p a:active { + color: var(--bs-white); +} + +footer ul svg { + color: var(--bs-black); +} + +footer ul svg:hover { + color: var(--mf-pink-strong); +} + /* NAVBAR */ /* Below 360px, the Navigation component won't naturally fit, so start to scale down the text */ +/* Not In Josh CSS .brand-text { @media (max-width: 359.98px) { font-size: 5.25vw; @@ -137,16 +300,44 @@ body { } /* Below 385px, the NavWithHamburger component won't naturally fit, so start to scale down the text */ +/* .hamburger-brand-text { @media (max-width: 384.98px) { font-size: 4vw; } } +*/ + +/* FORMS - CUSTOM CHECKBOX */ + +.custom-checkbox .form-check-input { + width: 22px; + height: 22px; + position: relative; + top: -2px; +} + +.custom-checkbox .form-check-input:checked { + background-color: var(--bs-gray-900); + border-color: var(--bs-gray-900); +} +// Set validation error style on all inputs +input[type="text"].is-invalid, +input[type="email"].is-invalid { + box-shadow: 0 0 0px 2px rgba(var(--bs-black-rgb), 0.5) !important; + border-color: var(--bs-gray-900); +} + +// Custom validation for postcode & email text inputs +.invalid-text-greyed:invalid { + color: var(--bs-gray-600); +} /* HEADER */ header { background-color: var(--bs-black); color: var(--bs-white); + font-size: 18px; } header h1 { @@ -159,123 +350,250 @@ header h1 { } } -/* TEXT HEADINGS */ -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: var(--font-rubik); - font-weight: bold; - text-transform: uppercase; - line-height: 1; +header p { + color: var(--bs-white); + font-size: 18px; } -/* FORM INPUTS */ -// Set checkbox size & style -input[type="checkbox"] { - width: 22px; - height: 22px; +/* SECTIONS */ +section { + padding-top: 2rem; + padding-bottom: 1rem; } -input[type="checkbox"]:checked { + +@include media-breakpoint-up(lg) { + section { + padding-top: 3rem; + padding-bottom: 2rem; + } +} + +section h2 { + font-size: 28px; +} + +@include media-breakpoint-up(md) { + section h2 { + font-size: 42px; + } +} + +section h3 { + font-size: 20px; +} + +section h4 { + font-size: 32px; +} + +section h3.position-sticky { background-color: var(--bs-gray-900); - border-color: var(--bs-gray-900); + color: var(--bs-white); + top: 74px; } -// Set validation error style on all inputs -input[type="text"]:invalid, -input[type="email"]:invalid, -select:invalid, -select option:invalid { - box-shadow: 0 0 0px 4px rgba(196, 0, 0, 0.5) !important; +section h2.position-sticky { + top: 200px; } -// Set focus style on all inputs -input[type="text"]:focus, -input[type="email"]:focus, -input[type="checkbox"]:focus, -select:focus, -select option:focus, -button:focus { - box-shadow: 0 0 0px 4px rgba(255, 255, 255, 0.5) !important; - border-color: var(--bs-gray-600) !important; +.section-light { + background-color: var(--bs-gray-100); + /*box-shadow: inset 0px -1px 0px 0px var(--bs-gray-300);*/ + border-top: 2px solid var(--bs-gray-100); + border-bottom: 2px solid var(--bs-gray-400); } -// Custom validation for postcode & email text inputs -.invalid-text-greyed:invalid { - color: var(--bs-gray-600); +.section-light h2, +.section-light h3, +.section-light h4, +.section-light p { + color: var(--bs-body-color); } -/* PARTY STYLES */ -h3.party { - font-size: 8vmax; - font-weight: 800; - text-transform: uppercase; +.section-light h3 { + /*color: var(--mf-pink-strong);*/ } -h3.party-labour, -svg.party-labour, -i.party-labour { - color: var(--bs-red); +/* SECTION DARK */ + +.section-dark { + background-color: var(--bs-gray-900); + /*box-shadow: inset 0px -2px 0px 0px var(--bs-gray-900);*/ + /*border-top: 2px solid var(--bs-gray-700);*/ + /*border-bottom: 2px solid var(--bs-gray-900);*/ } -div.party-labour { - background-color: rgba(var(--bs-red-rgb), 1); - background: linear-gradient( - rgba(var(--bs-red-rgb), 1), - rgba(var(--bs-red-rgb), 0.85) - ); - box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); - /*border: solid 1px var(--bs-gray-300);*/ +.section-dark h2, +.section-dark h3, +.section-dark h4, +.section-dark p { + color: var(--bs-body-bg); } -h3.party-libdem, -svg.party-libdem, -i.party-libdem { - color: var(--bs-orange); +.section-dark h3 { + /*color: var(--mf-pink-strong);*/ } -div.party-libdem { - background-color: rgba(var(--bs-orange-rgb), 1); +/* SECTION DARKER */ + +.section-darker { + background-color: var(--bs-black); + color: var(--bs-white); +} + +.section-darker h2, +.section-darker h3, +.section-darker h4, +.section-darker p { + color: var(--bs-body-bg); +} + +/* SEARCH */ + +.form-search { + max-width: 475px; + background: var(--mf-pink-strong); + border-radius: 12px; + padding: 18px; + margin-bottom: 24px; + box-shadow: 0px 0px 24px rgba(0, 0, 0, 0.25); +} + +div.alert { + margin-left: 12px; + margin-right: 12px; +} + +/* SECTION INFO AREAS */ + +.rounded-box { + padding: 1rem 1rem 0.1rem 1rem; + margin-bottom: 1rem; + border-radius: 1rem; +} + +.rounded-box h3, +.rounded-box h4, +.rounded-box p { + color: white; +} + +.info-area a:link, +.info-area a:visited, +.info-area a:hover, +.info-area a:active, +.action-area a:link, +.action-area a:visited, +.action-area a:hover, +.action-area a:active { + color: var(--bs-black); +} + +.info-area p, +.action-area p { + color: var(--bs-black); +} + +.info-area h3, +.action-area h3 { + color: var(--bs-black); +} + +.section-light .action-area { + /*background-color: var(--bs-gray-200);*/ + background: linear-gradient(var(--bs-gray-100), var(--bs-gray-150)); + box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); + /*border: solid 1px var(--bs-gray-300);*/ +} + +.section-light .info-area { + /*background-color: var(--bs-gray-200);*/ + background: linear-gradient(var(--bs-gray-100), var(--bs-gray-150)); background: linear-gradient( - rgba(var(--bs-orange-rgb), 1), - rgba(var(--bs-orange-rgb), 0.85) + var(--bs-warning-bg-subtle), + var(--bs-warning-border-subtle) ); box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); /*border: solid 1px var(--bs-gray-300);*/ } -h3.party-green, -svg.party-green, -i.party-green { - color: var(--bs-green); +.section-dark .action-area { + background-color: var(--bs-gray-400); + background: linear-gradient(var(--bs-gray-200), var(--bs-gray-400)); + box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.5); + /*border: solid 1px var(--bs-gray-300);*/ } -div.party-green { - background-color: rgba(var(--bs-green-rgb), 1); +.section-dark .info-area { + background-color: var(--bs-gray-400); + background: linear-gradient(var(--bs-gray-200), var(--bs-gray-400)); background: linear-gradient( - rgba(var(--bs-green-rgb), 1), - rgba(var(--bs-green-rgb), 0.85) + var(--bs-warning-bg-subtle), + var(--bs-warning-border-subtle) ); - box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); + box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.5); /*border: solid 1px var(--bs-gray-300);*/ } -h3.party-conservative, -svg.party-conservative, -i.party-conservative { - color: var(--bs-blue); +/* FOOTER */ + +footer { + background: var(--bs-gray-300); + padding-top: 2rem; + padding-bottom: 4rem; + border-top: 2px solid var(--bs-gray-100); } -div.party-conservative { - background-color: rgba(var(--bs-blue-rgb), 1); +@media (min-width: 992px) { + footer { + background: var(--bs-gray-300); + padding-top: 6rem; + padding-bottom: 4rem; + border-top: 2px solid var(--bs-gray-100); + } } -/* EXTRA PADDING CLASSES (py-6 / py-sm-6 / etc) */ -.py-6 { - padding-top: 6rem !important; - padding-bottom: 6rem !important; +.brand-tag { + position: fixed; + bottom: 0px; + right: 0px; + color: var(--mf-pink-dark) !important; + background-color: rgba(255, 255, 255, 0.9) !important; + border: 0; + border-radius: 0; +} + +/* BUTTON GROUPS */ + +.btn-group-vertical { + margin-bottom: 24px; +} + +.btn-group-vertical .btn { + margin-bottom: 4px; + text-align: left; +} + +.fa, +.fas, +.far, +.btn svg, +.btn img { + margin-right: 0.5rem; + width: 1.2rem; + display: inline-flex; + justify-content: center; +} + +.dropdown-toggle::after { + margin-top: 0.4555em; + float: right; +} + +/* RESPONSIVE */ + +[id] { + scroll-margin-top: 48px; } @include media-breakpoint-up(sm) { @@ -313,34 +631,111 @@ div.party-conservative { } } -/* SECTIONS */ -section { - padding-top: 3rem; - padding-bottom: 2rem; +@include media-breakpoint-up(lg) { + div.two-columns { + column-count: 2; + } } -.section-light { - background-color: var(--bs-gray-200); - /*box-shadow: inset 0px -1px 0px 0px var(--bs-gray-300);*/ - border-top: 2px solid var(--bs-gray-100); - border-bottom: 2px solid var(--bs-gray-400); +@include media-breakpoint-up(xxl) { + div.three-columns { + column-count: 3; + } } -.section-dark { - background-color: var(--bs-gray-800); - /*box-shadow: inset 0px -2px 0px 0px var(--bs-gray-900);*/ - border-top: 2px solid var(--bs-gray-700); - border-bottom: 2px solid var(--bs-gray-900); +/* PARTIES */ + +section h3.party { + font-size: 6vmax; + font-weight: 800; + text-transform: uppercase; } -/* FOOTER */ -.footer { - background-color: var(--bs-gray-300); - border-top: 2px solid var(--bs-gray-100); - border-bottom: 2px solid var(--bs-gray-400); +span.party-too-soon, +h3.party-too-soon, +svg.party-too-soon, +i.party-too-soon { + color: var(--bs-gray-600); } -.footer-dark { - background-color: var(--bs-gray-400); - border-top: 2px solid var(--bs-gray-300); +span.party-your-heart, +h3.party-your-heart, +svg.party-your-heart, +i.party-your-heart { + color: var(--mf-pink); +} + +span.party-labour, +h3.party-labour, +svg.party-labour, +i.party-labour { + color: var(--bs-red); +} + +div.party-labour { + background-color: rgba(var(--bs-red-rgb), 1); + background: linear-gradient( + rgba(var(--bs-red-rgb), 1), + rgba(var(--bs-red-rgb), 0.85) + ); + box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); + /*border: solid 1px var(--bs-gray-300);*/ +} + +span.party-libdem, +h3.party-libdem, +svg.party-libdem, +i.party-libdem { + color: var(--bs-orange); +} + +div.party-libdem { + background-color: rgba(var(--bs-orange-rgb), 1); + background: linear-gradient( + rgba(var(--bs-orange-rgb), 1), + rgba(var(--bs-orange-rgb), 0.85) + ); + box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); + /*border: solid 1px var(--bs-gray-300);*/ +} + +span.party-plaid, +h3.party-plaid, +svg.party-plaid, +i.party-plaid { + color: var(--pc-party-color); +} + +span.party-snp, +h3.party-snp, +svg.party-snp, +i.party-snp { + color: var(--snp-party-color); +} + +span.party-green, +h3.party-green, +svg.party-green, +i.party-green { + color: var(--bs-green); +} + +div.party-green { + background-color: rgba(var(--bs-green-rgb), 1); + background: linear-gradient( + rgba(var(--bs-green-rgb), 1), + rgba(var(--bs-green-rgb), 0.85) + ); + box-shadow: 0px 5px 10px 0px rgba(var(--bs-black-rgb), 0.075); + /*border: solid 1px var(--bs-gray-300);*/ +} + +h3.party-conservative, +svg.party-conservative, +i.party-conservative { + color: var(--bs-blue); +} + +div.party-conservative { + background-color: rgba(var(--bs-blue-rgb), 1); } diff --git a/app/page.tsx b/app/page.tsx index 1a2ba20..ace4196 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,15 +10,24 @@ export default async function Index() {
- +

Your vote IS
your power

- Join the movement that sticks together using it's votes - collectively for change. + 1. Show up + + {" "} + to get the Tories out. + +

+

+ 2. Join up{" "} + + to lobby the next gov for PR. +

diff --git a/components/constituency_lookup/ConstituencyLookup.tsx b/components/constituency_lookup/ConstituencyLookup.tsx index 66adcc1..a390f7a 100644 --- a/components/constituency_lookup/ConstituencyLookup.tsx +++ b/components/constituency_lookup/ConstituencyLookup.tsx @@ -171,6 +171,7 @@ const PostcodeLookup = () => { const [subscribed, setSubscribed] = useState(false); const [formState, setFormState] = useState(initialFormState); + const [formPostcode, setFormPostcode] = useState(""); const [apiResponse, setApiResponse] = useState< ConstituencyLookupResponse | false | null >(null); @@ -247,6 +248,8 @@ const PostcodeLookup = () => { }; const postcodeChanged = async (userPostcode: string) => { + setFormPostcode(userPostcode); + setPostError(null); const normalizedPostcode = normalizePostcode(userPostcode); if ( @@ -330,81 +333,82 @@ const PostcodeLookup = () => { }; return ( - -
-

Vote the Tories out

-

- Vote tactically at the General Election -

- + +

How to vote your Tory out

+ + postcodeChanged(e.target.value)} + className="invalid-text-greyed" + onBlur={(e) => { + if (!validatePostcode.test(normalizePostcode(e.target.value))) + setPostError("POSTCODE_INVALID"); + }} + /> + + {postError ? postcodeErrorToErrorMessage(postError) : ""} + + + + {lastSelectedConstituency && ( + postcodeChanged(e.target.value)} - className="invalid-text-greyed" + value={lastSelectedConstituency.name} + readOnly /> - {lastSelectedConstituency && ( - - {!lastSelectedConstituency?.name - ? "" - : lastSelectedConstituency.name.length < 31 - ? lastSelectedConstituency.name - : lastSelectedConstituency.name.substring(0, 27) + "..."} - - )} - { + setApiResponse(null); + setFormPostcode(""); + }} > - {postError ? postcodeErrorToErrorMessage(postError) : ""} - + CLEAR + - - {apiResponse && apiResponse.constituencies.length > 1 && ( - <> - {apiResponse.addresses ? ( -
-

- We can't work out exactly which constituency you're - in - please select your address: -

- - lookupConstituency(validPostcode.current, e.target.value) - } - > - + )} + + {apiResponse && apiResponse.constituencies.length > 1 && ( + <> + {apiResponse.addresses ? ( +
+

Select your exact address

+ + lookupConstituency(validPostcode.current, e.target.value) + } + > + + {apiResponse.addresses.map((c) => ( ))} - -
- ) : ( + +
+
+ ) : ( + !lastSelectedConstituency && (
-

- We can't work out exactly which constituency you're - in - please select one of the{" "} - {apiResponse.constituencies.length} options: -

+

Select your constituency

{ > Select Constituency - {apiResponse.constituencies.map((c, idx) => ( - - ))} + + {apiResponse.constituencies.map((c, idx) => ( + + ))} +
- )} - - )} - {subscribed ? ( -
- ) : ( -
- -
- - setFormState({ - ...formState, - emailOptIn: !formState.emailOptIn, - }) - } - className="me-2" + ) + )} + + )} + {subscribed ? ( +
+ ) : ( +
+ + + setFormState({ + ...formState, + emailOptIn: !formState.emailOptIn, + }) + } + /> + + setFormState({ + ...formState, + emailOptIn: !formState.emailOptIn, + }) + } + > + Join up, be counted, stick together + + + + {formState.emailOptIn && ( + <> + + { + setFormState({ ...formState, email: e.target.value }); + if (!e.target.validity.typeMismatch) { + setEmailError(null); + } + }} + className="invalid-text-greyed" /> - - setFormState({ - ...formState, - emailOptIn: !formState.emailOptIn, - }) - } + - Join with your email to stick together - -
- - - {formState.emailOptIn && ( - <> - - { - setFormState({ ...formState, email: e.target.value }); - if (!e.target.validity.typeMismatch) { - setEmailError(null); - } - }} - className="my-2 invalid-text-greyed" - /> - - {emailError ? emailErrorToErrorMessage(emailError) : ""} - - -

- We store your email address, postcode, and constituency, so we - can send you exactly the information you need, and the actions - to take. -

- - )} -
- )} - - - - - - - - - Privacy Policy - - - - - - + {emailError ? emailErrorToErrorMessage(emailError) : ""} + + +

+ You're opting in to receive emails. We store your email + address, postcode, and constituency, so we can send you exactly + the information you need. +

+ + )} +
+ )} + +
+ + +
+ ); }; diff --git a/components/footer/Footer.tsx b/components/footer/Footer.tsx index e268132..a37af33 100644 --- a/components/footer/Footer.tsx +++ b/components/footer/Footer.tsx @@ -26,8 +26,8 @@ import { ButtonGroup } from "react-bootstrap"; import { rubik } from "@/utils/Fonts"; const Footer = ({ blok }: { blok: FooterStoryblok }) => ( -
-
+ <> +
{/* FOOTER CALL TO ACTION BUTTONS */} @@ -143,73 +143,76 @@ const Footer = ({ blok }: { blok: FooterStoryblok }) => ( A Movement Forward initiative
© 2024 Forward Democracy Limited

- - {/* FOOTER LINKS */} - +

+ {/* FOOTER LINKS */} {blok.links.map((link) => ( - - - + ))} - - - - -

- -
- - - - {/* POSTCODE LOOKUP ATTRIBUTION */} -

- Postcode lookup contains data from{" "} - - DemocracyClub - - , the{" "} - - ONS - {" "} - &{" "} - - MySociety -

-
    -
  • - Contains OS data © Crown copyright and database right 2024 -
  • -
  • - Contains Royal Mail data © Royal Mail copyright and Database - right 2024 -
  • -
  • - Contains GeoPlace data © Local Government Information House - Limited copyright and database right 2024 -
  • -
  • - Source: Office for National Statistics licensed under the Open - Government Licence v.3.0 -
  • -
+

+ We don't use cookies on this website. +

-
-
+ +
+ + + + {/* POSTCODE LOOKUP ATTRIBUTION */} +

+ Postcode lookup contains data from{" "} + + DemocracyClub + + , the{" "} + + ONS + {" "} + &{" "} + + MySociety + +

+
    +
  • + Contains OS data © Crown copyright and database right 2024 +
  • +
  • + Contains Royal Mail data © Royal Mail copyright and Database + right 2024 +
  • +
  • + Contains GeoPlace data © Local Government Information House + Limited copyright and database right 2024 +
  • +
  • + Source: Office for National Statistics licensed under the Open + Government Licence v.3.0 +
  • +
+ +
+
+
+ + + @MVTFWD + + ); export default Footer; diff --git a/components/footer/FooterLink.tsx b/components/footer/FooterLink.tsx index 34b5b09..da3aa68 100644 --- a/components/footer/FooterLink.tsx +++ b/components/footer/FooterLink.tsx @@ -5,13 +5,12 @@ import { } from "@/storyblok/types/storyblok-types"; import Link from "next/link"; -import Col from "react-bootstrap/Col"; - +// TODO we only want links to Donate About Data Privacy const FooterLink = ({ blok, }: { blok: FooterInternalLinkStoryblok | FooterExternalLinkStoryblok; -}) => {link(blok)}; +}) => <>{link(blok)}; const link = ( blok: FooterInternalLinkStoryblok | FooterExternalLinkStoryblok, diff --git a/components/forms/ConstituencyFormWithSignup.tsx b/components/forms/ConstituencyFormWithSignup.tsx index 2fe606c..b9e6433 100644 --- a/components/forms/ConstituencyFormWithSignup.tsx +++ b/components/forms/ConstituencyFormWithSignup.tsx @@ -2,27 +2,13 @@ import { useRouter } from "next/navigation"; -import { - Container, - Form, - Button, - FormCheck, - Row, - Col, - Spinner, - InputGroup, -} from "react-bootstrap"; +import { Form, Button, FormCheck, Spinner, InputGroup } from "react-bootstrap"; -import { - normalizePostcode, - postcodeInputPattern, - validatePostcode, -} from "@/utils/Postcodes"; import { submitANForm } from "@/utils/AnApiSubmission"; import { rubik } from "@/utils/Fonts"; import FormCheckInput from "react-bootstrap/esm/FormCheckInput"; import FormCheckLabel from "react-bootstrap/esm/FormCheckLabel"; -import { useMemo, useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect } from "react"; import ConstituencyLookup from "./constituencyLookup"; const emailErrorMessage = (code: EmailErrorCode) => { @@ -133,129 +119,114 @@ const ConstituencyFormWithSignup = () => { }; return ( - -
-

Vote the Tories out

-

- Vote tactically at the General Election -

- - {/* Renders the postcode box, makes API calls, and if necessary shows an address/constituency picker */} - - - {subscribed ? ( -
- ) : ( -
- -
- - setFormState({ - ...formState, - emailOptIn: !formState.emailOptIn, - }) - } - className="me-2" + +

How to vote your Tory out

+ {/* Renders the postcode box, makes API calls, and if necessary shows an address/constituency picker */} + + + {subscribed ? ( +
+ ) : ( +
+ + + setFormState({ + ...formState, + emailOptIn: !formState.emailOptIn, + }) + } + /> + + setFormState({ + ...formState, + emailOptIn: !formState.emailOptIn, + }) + } + > + Join up, be counted, stick together + + + + {formState.emailOptIn && ( + <> + + { + setFormState({ ...formState, email: e.target.value }); + if (!e.target.validity.typeMismatch) { + setEmailError(null); + } + }} + className="invalid-text-greyed" /> - - setFormState({ - ...formState, - emailOptIn: !formState.emailOptIn, - }) - } + - Join with your email to stick together - -
- - - {formState.emailOptIn && ( - <> - - { - setFormState({ ...formState, email: e.target.value }); - if (!e.target.validity.typeMismatch) { - setEmailError(null); - } - }} - className="my-2 invalid-text-greyed" - /> - - {emailError ? emailErrorMessage(emailError) : ""} - - -

- We store your email address, postcode, and constituency, so we - can send you exactly the information you need, and the actions - to take. -

- - )} -
- )} - - - - - - - - - Privacy Policy - - - - - - + {emailError ? emailErrorMessage(emailError) : ""} + + +

+ You're opting in to receive emails. We store your email + address, postcode, and constituency, so we can send you exactly + the information you need. +

+ + )} +
+ )} + +
+ + + Privacy Policy + +
+ ); }; diff --git a/components/forms/constituencyLookup.tsx b/components/forms/constituencyLookup.tsx index 753a8d2..0cce09d 100644 --- a/components/forms/constituencyLookup.tsx +++ b/components/forms/constituencyLookup.tsx @@ -1,6 +1,6 @@ "use client"; -import { Form, InputGroup } from "react-bootstrap"; +import { Button, Form, InputGroup } from "react-bootstrap"; import { normalizePostcode, @@ -159,6 +159,7 @@ const ConstituencyLookup = ({ null, ); + const [formPostcode, setFormPostcode] = useState(""); useEffect(() => { if ( apiResponse && @@ -225,6 +226,7 @@ const ConstituencyLookup = ({ }; const postcodeChanged = async (userPostcode: string) => { + setFormPostcode(userPostcode); const normalizedPostcode = normalizePostcode(userPostcode); if ( @@ -251,6 +253,7 @@ const ConstituencyLookup = ({ <> postcodeChanged(e.target.value)} className="invalid-text-greyed" + onBlur={(e) => { + if (!validatePostcode.test(normalizePostcode(e.target.value))) + setPostcodeError("POSTCODE_INVALID"); + }} /> - {constituency && ( - - {!constituency.name - ? "" - : constituency.name.length < 31 - ? constituency.name - : constituency.name.substring(0, 27) + "..."} - - )} {postcodeError ? postcodeErrorMessage(postcodeError) : ""} + {constituency && ( + + + + + )} + {apiResponse && apiResponse.constituencies.length > 1 && ( <> {apiResponse.addresses ? (
-

- We can't work out exactly which constituency you're in - - please select your address: -

+

Select your exact address

Select Address - {apiResponse.addresses.map((c) => ( - - ))} + + {apiResponse.addresses.map((c) => ( + + ))} +
) : ( -
-

- We can't work out exactly which constituency you're in - - please select one of the {apiResponse.constituencies.length}{" "} - options: -

- { - if (e.target.value.length > 2) { - lookupConstituency(validPostcode.current, e.target.value); - } else { - setFormState({ - ...formState, - constituencyIndex: parseInt(e.target.value), - }); - } - }} - > - - {apiResponse.constituencies.map((c, idx) => ( - + {apiResponse.constituencies.map((c, idx) => ( + + ))} + + +
+ ) )} )} diff --git a/components/info_box/ImpliedChart.tsx b/components/info_box/ImpliedChart.tsx index 19bbc29..78327dd 100644 --- a/components/info_box/ImpliedChart.tsx +++ b/components/info_box/ImpliedChart.tsx @@ -1,5 +1,6 @@ -import InfoBox from "./InfoBox"; import { svgChart } from "@/utils/Echarts"; +import metadata from "@/data/metadata.json"; +import Link from "next/link"; const ImpliedChart = ({ constituencyData, @@ -12,16 +13,20 @@ const ImpliedChart = ({ ); return ( - +
<> -

2019 Election Results (Implied)

-

The 2019 implied results from your constituency:

+

2019 Results

+

+ Calculated by{" "} + BBC & Sky using + new boundaries. +

- +
); }; diff --git a/components/info_box/MRPChart.tsx b/components/info_box/MRPChart.tsx index c5996d3..efe8358 100644 --- a/components/info_box/MRPChart.tsx +++ b/components/info_box/MRPChart.tsx @@ -1,5 +1,6 @@ -import InfoBox from "./InfoBox"; import { svgChart } from "@/utils/Echarts"; +import metadata from "@/data/metadata.json"; +import Link from "next/link"; const MRPChart = ({ constituencyData, @@ -9,16 +10,23 @@ const MRPChart = ({ const svgStr = svgChart(constituencyData.pollingResults.partyVoteResults); return ( - +
<> -

Constituency Regression Polls

-

Average of last 6 months MRP models:

+

Local MRP polling

+

+ Aggregate average of:{" "} + {metadata.mrp_poll.map((datum, idx) => ( + <> + Poll {idx + 1}{" "} + + ))}{" "} +

- +
); }; diff --git a/components/info_box/PlanToVoteBox.tsx b/components/info_box/PlanToVoteBox.tsx index 7d1c728..0358427 100644 --- a/components/info_box/PlanToVoteBox.tsx +++ b/components/info_box/PlanToVoteBox.tsx @@ -1,59 +1,71 @@ -import InfoBox from "./InfoBox"; - import { FaPenClip, - FaRegIdCard, + FaIdCard, FaEnvelopeOpenText, FaFileImage, + FaHand, + FaCheckToSlot, + FaTriangleExclamation, } from "react-icons/fa6"; const PlanToVoteBox = () => { return ( - - <> -

Your Plan

-

- - - Register to vote, it takes 5 minutes - -

-

- - - Don't forget your photo Voter ID - -

-

- - - Vote in advance, privately, by post from home - -

-

- - Download and put up some posters -

- -
+ <> +
+ <> +

Get your voting plan

+

+ Be counted! + (get this plan and reminders) +

+

+ + Register to + vote, it takes 5 minutes + +

+

+ + Get your photo + Voter ID + +

+

+ + {" "} + Optionally vote from home by post + +

+

+ Print posters + and flyers +

+

+ Vote + Tactically! +

+

+ Remind + your new MP why you showed up +

+ +
+

+ Join up and we'll take you through this plan. +

+ ); }; diff --git a/components/info_box/TacticalReasoningBox.tsx b/components/info_box/TacticalReasoningBox.tsx index 8415964..8d2bf2a 100644 --- a/components/info_box/TacticalReasoningBox.tsx +++ b/components/info_box/TacticalReasoningBox.tsx @@ -1,4 +1,3 @@ -import InfoBox from "./InfoBox"; import { partyCssClassFromSlug, partyNameFromSlug } from "@/utils/Party"; import { @@ -34,14 +33,12 @@ const TacticalReasoningBox = ({ const closeSeat = !constituencyData.otherVoteData.conservativeWinUnlikely; return ( - +
<> -

Why?

- {/* Always show previous general election winner */}

- {partyNameFromSlug(previousWinner)} won here in 2019 + {partyNameFromSlug(previousWinner)} won in 2019

{/* Show previous biggest progressive if it matches our recommendation AND they weren't the winner */} @@ -52,8 +49,7 @@ const TacticalReasoningBox = ({ className="me-2" style={{ color: "var(--bs-green)" }} /> - {recommendedPartyName} received the most progressive votes here in - 2019 + {recommendedPartyName} were closest in 2019

)} @@ -64,8 +60,7 @@ const TacticalReasoningBox = ({ className="me-2" style={{ color: "var(--bs-green)" }} /> - Polling indicates that {recommendedPartyName} have the best chance - of beating the Tory party here at the next election + Polls favour {recommendedPartyName} here

)} @@ -73,11 +68,13 @@ const TacticalReasoningBox = ({ {recommendedPartyTargetSeat && (

- This is a likely target seat for {recommendedPartyName} + Likely a {recommendedPartyName} target seat

)} - {/* Check if this is a close seat */} + {/* + TODO motivational text based on whats required to get tories out + Check if this is a close seat {closeSeat && (

This is a close seat, tactical voting can work very well here.

- )} + )}*/} - +
); }; diff --git a/components/info_box/ToryCantWinReasoningBox.tsx b/components/info_box/ToryCantWinReasoningBox.tsx new file mode 100644 index 0000000..aa902fc --- /dev/null +++ b/components/info_box/ToryCantWinReasoningBox.tsx @@ -0,0 +1,92 @@ +import { partyCssClassFromSlug, partyNameFromSlug } from "@/utils/Party"; + +import { + FaUser, + FaChartSimple, + FaChartLine, + FaBullseye, + FaTriangleExclamation, +} from "react-icons/fa6"; + +const ToryCantWinReasoningBox = ({ + constituencyData, +}: { + constituencyData: ConstituencyData; +}) => { + const recommendedParty = constituencyData.recommendation.partySlug; + const recommendedPartyName = partyNameFromSlug(recommendedParty); + + const previousWinner = constituencyData.impliedPreviousResult.winningParty; + const previousBiggestProgressive = + constituencyData.impliedPreviousResult.biggestProgressiveParty; + + const pollingWinner = constituencyData.pollingResults.winningParty; + const pollingBiggestProgressive = + constituencyData.pollingResults.biggestProgressiveParty; + + const recommendedPartyTargetSeat = + constituencyData.otherVoteData.targetSeatData.some( + (target) => + target.partySlug == recommendedParty && target.likelyTarget == "YES", + ); + + const closeSeat = !constituencyData.otherVoteData.conservativeWinUnlikely; + + return ( +
+ <> + {/* Always show previous general election winner */} +

+ + {partyNameFromSlug(previousWinner)} won in 2019 +

+ + {/* Show previous biggest progressive if it matches our recommendation AND they weren't the winner */} + {recommendedParty == previousBiggestProgressive && + recommendedParty != previousWinner && ( +

+ + {recommendedPartyName} were closest in 2019 +

+ )} + + {/* Show polling biggest progressive if it matches our recommendation */} + {recommendedParty == pollingBiggestProgressive && ( +

+ + Polls favour {recommendedPartyName} here +

+ )} + + {/* If this is a target seat for our recommended party, show this */} + {recommendedPartyTargetSeat && ( +

+ + Likely a {recommendedPartyName} target seat +

+ )} + + {/* + TODO motivational text based on whats required to get tories out + Check if this is a close seat + {closeSeat && ( +

+ + This is a close seat, tactical voting can work very well here. +

+ )}*/} + +
+ ); +}; + +export default ToryCantWinReasoningBox; diff --git a/components/navigation/Navigation.tsx b/components/navigation/Navigation.tsx index 08fa34a..26c6838 100644 --- a/components/navigation/Navigation.tsx +++ b/components/navigation/Navigation.tsx @@ -3,71 +3,52 @@ // (even tho this navbar doesn't use a collapse, it still fails without use client) import Image from "next/image"; -import Link from "next/link"; -import { Container, Navbar, Row, Col } from "react-bootstrap"; +import { Container, Button, Nav, Navbar } from "react-bootstrap"; import logo from "@/assets/stop-the-tories-logo-transparent.png"; import { rubik } from "@/utils/Fonts"; import { FaBoltLightning, FaMagnifyingGlass } from "react-icons/fa6"; -import styles from "./styles.module.css"; - // Navbar which just includes our main call to actions, which are (currently) the search // & join buttons. These are always displayed on the navbar, and there is no hamburger // menu. Users need to scroll to the footer to see links to non-primary pages. const Navigation = () => { return ( - - - {/* Branding section - left-aligned */} - - StopTheTories.vote logo - StopTheTories - .Vote - - - {/* Right-aligned section - always shown links */} -
- - - - - - Search - - - - - - Join - - - - -
-
-
+ + + + {/* Branding section - left-aligned */} + + + StopTheTories.vote logo + + StopTheTories + .Vote + + + {/* Right-aligned section - always shown links */} + + {/* pushes the buttons to the right */} + + + + + + + ); }; diff --git a/components/sections/MovementSection.tsx b/components/sections/MovementSection.tsx index c6bec19..cd54e95 100644 --- a/components/sections/MovementSection.tsx +++ b/components/sections/MovementSection.tsx @@ -33,66 +33,66 @@ const MovementSection = () => { }; return ( -
- - {/* PEOPLE */} - - -

- Join A Movement building voter power, beyond this election. -

- -
-
+
+
+ + {/* PEOPLE */} + + +

+ Join A Movement building voter power, beyond this election. +

+ +
- - - {Object.keys(people).map((key) => { - // TS isn't smart enough to work out `key` is just the keys from the people object, - // so thinks they might not be a valid key for the object... - // @ts-expect-error - const name: string = people[key]["name"]; - // @ts-expect-error - const image: StaticImageData = people[key]["image"]; + + {Object.keys(people).map((key) => { + // TS isn't smart enough to work out `key` is just the keys from the people object, + // so thinks they might not be a valid key for the object... + // @ts-expect-error + const name: string = people[key]["name"]; + // @ts-expect-error + const image: StaticImageData = people[key]["image"]; - return ( - - {`Photo - - ); - })} - - + return ( + + {`Photo + + ); + })} + - {/* PLAN */} - - - -

We show up

-

To get the tories out

-

- In many places around the country, if we vote together for the - most likely candidate to beat the Tory, out votes demolish them - into tiny numbers. -

- - -

We stick together

-

To influence the next government

-

- A bigger influence on the next government than the media that - currently shape the narrative. We stick together offering our - votes for the changes we want. -

- -
-
-
+ {/* PLAN */} + + +

We show up

+

To get the tories out

+

+ In many places around the country, if we vote together for the + most likely candidate to beat the Tory, out votes demolish them + into tiny numbers. +

+ + +

We stick together

+

+ To influence the next government +

+

+ A bigger influence on the next government than the media that + currently shape the narrative. We stick together offering our + votes for the changes we want. +

+ +
+ +
+
); }; diff --git a/utils/Echarts.ts b/utils/Echarts.ts index 335ed29..245a625 100644 --- a/utils/Echarts.ts +++ b/utils/Echarts.ts @@ -34,16 +34,16 @@ const svgChart = (partyData: PartyVoteResult[], rawVote: boolean = false) => { const chart = echarts.init(null, null, { renderer: "svg", // must use SVG rendering mode ssr: true, // enable SSR - width: 350, // need to specify height and width + width: 416, // need to specify height and width height: 250, }); chart.setOption({ grid: { - left: 40, - right: 10, - top: 10, - bottom: 100, + left: 35, + right: 2, + top: 5, + bottom: 57, }, xAxis: { data: axisArray, @@ -76,7 +76,7 @@ const svgChart = (partyData: PartyVoteResult[], rawVote: boolean = false) => { }); } - return chart.renderToSVGString(); + return chart.renderToSVGString().replace('width="416" height="250" ', ""); }; export { svgChart }; diff --git a/utils/Party.ts b/utils/Party.ts index 3f0d84e..e6945dc 100644 --- a/utils/Party.ts +++ b/utils/Party.ts @@ -7,15 +7,15 @@ const partyNameFromSlug = (slug: PartySlug): string => { case "LD": return "Liberal Democrat"; case "Green": - return "Green"; + return "Green Party"; case "SNP": - return "SNP"; + return "The SNP"; case "PC": return "Plaid Cymru"; case "Reform": return "Reform UK"; case "NonVoter": - return "Non Voters"; + return "Didn't Vote"; default: return "Other"; } @@ -38,7 +38,7 @@ const shortPartyNameFromSlug = (slug: PartySlug): string => { case "Reform": return "Reform"; case "NonVoter": - return "Non Voters"; + return "Didn't Vote"; default: return "Other"; } @@ -61,7 +61,7 @@ const partyColorFromSlug = (slug: PartySlug) => { case "Reform": return "var(--reform-party-color)"; case "NonVoter": - return "var(--mvtfwd-pink-strong)"; + return "var(--mf-pink-strong)"; case "Other": return "var(--other-party-color)"; default: @@ -81,9 +81,9 @@ const partyCssClassFromSlug = (slug: PartySlug) => { case "Green": return "party-green"; case "SNP": - return ""; + return "party-snp"; case "PC": - return ""; + return "party-plaid"; case "Reform": return ""; default: