Skip to content
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
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const SharedItemLanding = React.lazy(() => import(/* webpackChunkName: 'SharedIt
const SignInEmailProcess = React.lazy(() => import(/* webpackChunkName: 'SignInEmailProcess' */ './js/pages/Process/SignInEmailProcess'));
const SignInJumpProcess = React.lazy(() => import(/* webpackChunkName: 'SignInJumpProcess' */ './js/pages/Process/SignInJumpProcess'));
const Squads = React.lazy(() => import(/* webpackChunkName: 'Squads' */ './js/pages/Squads'));
const SupporterLandingChooseCandidate = React.lazy(() => import(/* webpackChunkName: 'SupporterLandingChooseCandidate' */ './js/pages/SupporterLanding/SupporterLandingChooseCandidate'));
const SuperSharingAddContacts = React.lazy(() => import(/* webpackChunkName: 'SuperSharingAddContacts' */ './js/common/pages/SuperSharing/SuperSharingAddContacts'));
const SuperSharingChooseRecipients = React.lazy(() => import(/* webpackChunkName: 'SuperSharingChooseRecipients' */ './js/common/pages/SuperSharing/SuperSharingChooseRecipients'));
const SuperSharingComposeEmailMessage = React.lazy(() => import(/* webpackChunkName: 'SuperSharingComposeEmailMessage' */ './js/common/pages/SuperSharing/SuperSharingComposeEmailMessage'));
Expand Down Expand Up @@ -663,6 +664,7 @@ class App extends Component {
<Route path="/setupaccount/:set_up_page" exact component={SetUpAccountRoot} />
<Route path="/setupaccount" exact><SetUpAccountRoot /></Route>
<Route path="/squads" exact><Squads /></Route>
<Route path="/supporterlanding" exact component={SupporterLandingChooseCandidate} />
<Route path="/standards-against-child-sexual-abuse-and-exploitation-csae" component={ChildSafety} />
<Route exact path="/start-a-campaign"><CampaignStartIntro /></Route>
<Route exact path="/start-a-challenge"><ChallengeStartIntro /></Route>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/js/components/SupporterLanding/ActionCountLabel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';

// Small muted count shown under an action button, e.g. "143 have chosen".
export default function ActionCountLabel ({ count, suffix }) {
return (
<CountText>
{count.toLocaleString()}
{' '}
{suffix}
</CountText>
);
}
ActionCountLabel.propTypes = {
count: PropTypes.number.isRequired,
suffix: PropTypes.string.isRequired,
};

const CountText = styled.span`
color: ${DesignTokenColors.neutral600};
font-size: 14px;
font-weight: 400;
`;
183 changes: 183 additions & 0 deletions src/js/components/SupporterLanding/CandidateInfoCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { OpenInNew as OpenInNewIcon } from '@mui/icons-material';
import PropTypes from 'prop-types';
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
import normalizedImagePath from '../../common/utils/normalizedImagePath';

const avatarGeneric = '../../../img/global/svg-icons/avatar-generic.svg';

// Candidate identity block. Desktop/tablet: name over party, a vertical divider, then
// "Learn more". Mobile: name on its own row, then "Independent | Learn more about Jane"
// on a single row beneath it.
export default function CandidateInfoCard ({ candidate }) {
const { name, party, officeDescription, learnMoreUrl } = candidate;
const firstName = candidate.firstName || name.split(' ')[0];

const learnMore = (
<LearnMoreLink to={learnMoreUrl}>
Learn more about
{' '}
{firstName}
<OpenInNewIcon style={{ fontSize: 16 }} />
</LearnMoreLink>
);

return (
<CardWrapper>
<CandidateAvatar src={normalizedImagePath(avatarGeneric)} alt={name} />
<CandidateDetails>
<IdentityRow className="u-show-desktop-tablet">
<NameStack>
<CandidateName to={learnMoreUrl}>{name}</CandidateName>
<Party>{party}</Party>
</NameStack>
<RowDivider />
{learnMore}
</IdentityRow>

<MobileIdentity className="u-show-mobile">
<CandidateName to={learnMoreUrl}>{name}</CandidateName>
<MobileMeta>
<Party>{party}</Party>
<MobileDivider />
{learnMore}
</MobileMeta>
</MobileIdentity>

<OfficeDescription>{officeDescription}</OfficeDescription>
</CandidateDetails>
</CardWrapper>
);
}
CandidateInfoCard.propTypes = {
candidate: PropTypes.shape({
name: PropTypes.string,
firstName: PropTypes.string,
party: PropTypes.string,
officeDescription: PropTypes.string,
learnMoreUrl: PropTypes.string,
}).isRequired,
};

const CandidateAvatar = styled.img`
border: 2px solid ${DesignTokenColors.neutralUI200};
border-radius: 50%;
flex: 0 0 auto;
height: 72px;
width: 72px;

@media (max-width: 575px) {
height: 52px;
width: 52px;
}
`;

const CandidateDetails = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
`;

const CandidateName = styled(Link)`
color: ${DesignTokenColors.primary600};
font-size: 22px;
font-weight: 400;
line-height: 1.15;
text-decoration: none;

&:hover {
text-decoration: underline;
}

@media (max-width: 575px) {
font-size: 20px;
}
`;

const CardWrapper = styled.div`
align-items: center;
display: flex;
gap: 16px;

@media (max-width: 575px) {
align-items: flex-start;
gap: 10px;
}
`;

const IdentityRow = styled.div`
align-items: center;
column-gap: 12px;
display: flex;
flex-wrap: wrap;
row-gap: 4px;
`;

const LearnMoreLink = styled(Link)`
align-items: center;
color: ${DesignTokenColors.primary600};
display: inline-flex;
font-size: 16px;
font-weight: 400;
gap: 4px;
text-decoration: none;
white-space: nowrap;

&:hover {
text-decoration: underline;
}

@media (max-width: 575px) {
font-size: 14px;
}
`;

const MobileDivider = styled.span`
background: ${DesignTokenColors.neutralUI200};
flex: 0 0 auto;
height: 16px;
width: 1px;
`;

const MobileIdentity = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
`;

const MobileMeta = styled.div`
align-items: center;
display: flex;
gap: 6px;
`;

const NameStack = styled.div`
display: flex;
flex-direction: column;
`;

const OfficeDescription = styled.p`
color: ${DesignTokenColors.neutral600};
font-size: 14px;
line-height: 1.4;
margin: 0;
`;

const Party = styled.span`
color: ${DesignTokenColors.neutral800};
font-size: 17px;
white-space: nowrap;

@media (max-width: 575px) {
font-size: 14px;
}
`;

const RowDivider = styled.span`
background: ${DesignTokenColors.neutralUI200};
height: 40px;
width: 1px;
`;
47 changes: 47 additions & 0 deletions src/js/components/SupporterLanding/ChooseButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Check as CheckIcon } from '@mui/icons-material';
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
import GreenCheckBadge from './GreenCheckBadge';

// Primary "Choose" action. Toggles to a "Chosen" pill (lighter blue + a green badge
// with a white check) once the voter has chosen this candidate.
export default function ChooseButton ({ chosen = false, onClick }) {
return (
<Button type="button" onClick={onClick} $chosen={chosen} aria-pressed={chosen}>
{chosen ? (
<GreenCheckBadge />
) : (
<CheckIcon style={{ fontSize: 22, margin: '-3px 0' }} />
)}
{chosen ? 'Chosen' : 'Choose'}
</Button>
);
}
ChooseButton.propTypes = {
chosen: PropTypes.bool,
onClick: PropTypes.func,
};

const Button = styled.button`
align-items: center;
background: ${({ $chosen }) => ($chosen ? DesignTokenColors.primary400 : DesignTokenColors.primary600)};
border: 1.5px solid ${({ $chosen }) => ($chosen ? DesignTokenColors.primary400 : DesignTokenColors.primary600)};
border-radius: 9999px;
box-sizing: border-box;
color: ${DesignTokenColors.whiteUI};
cursor: pointer;
display: inline-flex;
font-size: 18px;
font-weight: 400;
gap: 8px;
justify-content: center;
padding: 4px 20px;
width: 150px;

&:hover {
background: ${({ $chosen }) => ($chosen ? DesignTokenColors.primary400 : DesignTokenColors.primary700)};
border-color: ${({ $chosen }) => ($chosen ? DesignTokenColors.primary400 : DesignTokenColors.primary700)};
}
`;
84 changes: 84 additions & 0 deletions src/js/components/SupporterLanding/ChooseExplainerColumns.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import PropTypes from 'prop-types';
import React from 'react';
import styled from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';

// The two-column explainer under the buttons (only shown before choosing):
// left — what "Choose" does
// right — the "Not ready to choose?" follow pitch, split by a vertical divider.
export default function ChooseExplainerColumns ({ candidateFirstName }) {
return (
<ColumnsWrapper>
<ExplainerColumn>
Choosing marks
{' '}
{candidateFirstName}
{' '}
on your WeVote digital ballot for the most impact.
</ExplainerColumn>
<ColumnDivider />
<ExplainerColumn>
<strong>Not ready to choose?</strong>
{' '}
Following
{' '}
{candidateFirstName}
{' '}
helps build public trust and lets WeVote suggest similar
candidates and measures to help you complete your ballot.
{' '}
<InlineLink type="button">Learn more</InlineLink>
</ExplainerColumn>
</ColumnsWrapper>
);
}
ChooseExplainerColumns.propTypes = {
candidateFirstName: PropTypes.string,
};

const ColumnDivider = styled.span`
align-self: stretch;
background: ${DesignTokenColors.neutralUI200};
justify-self: center;
width: 1px;
`;

// The divider sits under the "-OR-" above it (centre at 196px = 160 action column + 16
// gap + 20 half of the 40px "-OR-"). Zero grid gap + fixed columns: left column 194px so
// the 4px divider column centres at 196; the right paragraph aligns with the Follow
// column via padding-left (track starts at 198, + 36 = 234). Only the left paragraph's
// width changes when tuning.
const ColumnsWrapper = styled.div`
align-items: stretch;
column-gap: 0;
display: grid;
grid-template-columns: 194px 4px 1fr;

& > p:first-of-type {
padding-right: 6px;
}

& > p:last-of-type {
padding-left: 36px;
}
`;

const ExplainerColumn = styled.p`
color: ${DesignTokenColors.neutral700};
font-size: 15px;
line-height: 1.5;
margin: 0;
`;

const InlineLink = styled.button`
background: none;
border: none;
color: ${DesignTokenColors.primary600};
cursor: pointer;
font-size: 15px;
padding: 0;

&:hover {
text-decoration: underline;
}
`;
Loading
Loading