Skip to content
Draft
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
192 changes: 192 additions & 0 deletions src/js/components/Ballot/OfficeInfoModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/* eslint-disable react/jsx-one-expression-per-line */
/* eslint-disable react/no-unescaped-entities */
/* eslint-disable react/jsx-closing-tag-location */
import React from 'react';
import PropTypes from 'prop-types';
import styled, { createGlobalStyle } from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
import ModalDisplayTemplateA from '../Widgets/ModalDisplayTemplateA';

const OfficeInfoModal = ({ isOpen, onClose, officeName }) => {
const dialogTitleJSX = (
<HeaderRow>
<Title id="office-info-title">
About the office of <strong>{officeName}</strong>
</Title>
</HeaderRow>
);

const textFieldJSX = (
<div style={{ padding: '18px 18px 28px' }}>
<ModalBody>
<FlexContainer>
<LeftColumn>
<VideoPlaceholder>
<PlayIcon>▶</PlayIcon>
<p>Watch video<br />(25 seconds)</p>
</VideoPlaceholder>
</LeftColumn>

<RightColumn>
<p>
An <strong>{officeName}</strong> is the chief legal officer of a state,
responsible for enforcing laws, protecting citizens' rights, and representing
the public interest in legal matters.
</p>
<p>
They oversee investigations, defend state laws in court, and provide legal
guidance to government agencies.
</p>
{/* eslint-disable react/jsx-indent */}
<p>
Because they influence everything from consumer protections to civil rights
and election integrity, <strong>choosing an {officeName} in an election is
crucial</strong>—it determines who will uphold justice, safeguard democratic
processes, and hold powerful entities accountable on behalf of the public.
</p>
{/* eslint-enable react/jsx-indent */}
</RightColumn>
</FlexContainer>
</ModalBody>

<ModalFooter>
<CloseButton type="button" onClick={onClose}>
Close
</CloseButton>
</ModalFooter>
</div>
);

return (
<>
<WidenPreviewModal />
<SoftenCorners />
<ModalDisplayTemplateA
show={isOpen}
toggleModal={onClose}
externalUniqueId="officeInfoModal"
dialogTitleJSX={dialogTitleJSX}
tallMode={false}
textFieldJSX={textFieldJSX}
/>
</>
);
};

OfficeInfoModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
officeName: PropTypes.string.isRequired,
};

// Global Styles
const WidenPreviewModal = createGlobalStyle`
.MuiDialog-paper:has(#closeModalDisplayTemplateAofficeInfoModal) {
max-width: 860px !important;
width: 96% !important;
}
`;

const SoftenCorners = createGlobalStyle`
.MuiDialog-paper:has(#closeModalDisplayTemplateAofficeInfoModal) {
border-radius: 14px !important;
}
`;

// Styles
const HeaderRow = styled.div`
padding: 0px 12px 0 18px;
`;

const Title = styled.h3`
font-size: 28px;
font-weight: 400;
`;

const ModalBody = styled.div`
background: ${DesignTokenColors.whiteUI};
border: 1px solid ${DesignTokenColors.neutralUI200};
border-radius: 10px;
padding: 14px;
`;

// FLEXBOX LAYOUT
const FlexContainer = styled.div`
display: flex;
gap: 20px;
align-items: flex-start;

/* Stack vertically on mobile */
@media (max-width: 768px) {
flex-direction: column;
}
`;

const LeftColumn = styled.div`
flex: 0 0 180px;

@media (max-width: 768px) {
flex: 1;
width: 100%;
}
`;

const RightColumn = styled.div`
flex: 1;

p {
margin-bottom: 12px;
line-height: 1.6;
}
`;

// Video placeholder
const VideoPlaceholder = styled.div`
width: 100%;
aspect-ratio: 4/3;
background: ${DesignTokenColors.neutral200};
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: ${DesignTokenColors.neutral700};
text-align: center;
cursor: pointer;

&:hover {
background: ${DesignTokenColors.neutral300};
}

p {
margin: 8px 0 0 0;
font-size: 14px;
}
`;

const PlayIcon = styled.div`
font-size: 32px;
color: ${DesignTokenColors.primary700}
`;

const ModalFooter = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 12px;
`;

const CloseButton = styled.button`
background: ${DesignTokenColors.primary700};
border: 1px solid ${DesignTokenColors.primary700};
border-radius: 9999px;
color: ${DesignTokenColors.whiteUI};
cursor: pointer;
padding: 10px 18px;

&:hover{
background: ${DesignTokenColors.primary800};
border-color: ${DesignTokenColors.primary800};
}
`;

export default OfficeInfoModal;

39 changes: 38 additions & 1 deletion src/js/components/Ballot/OfficeItemCompressed.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { InfoOutlined } from '@mui/icons-material';
import withStyles from '@mui/styles/withStyles';
import withTheme from '@mui/styles/withTheme';
import PropTypes from 'prop-types';
import React, { Component, Suspense } from 'react';
import styled from 'styled-components';
import DesignTokenColors from '../../common/components/Style/DesignTokenColors';
import OfficeActions from '../../actions/OfficeActions';
import BallotScrollingContainer from './BallotScrollingContainer';
import { BallotScrollingOuterWrapper } from '../../common/components/Style/ScrollingStyles';
Expand All @@ -16,6 +18,7 @@ import { sortCandidateList } from '../../utils/positionFunctions';
import { OfficeItemCompressedWrapper, OfficeNameH2 } from '../Style/BallotStyles';

const ShowMoreButtons = React.lazy(() => import(/* webpackChunkName: 'ShowMoreButtons' */ '../Widgets/ShowMoreButtons'));
const OfficeInfoModal = React.lazy(() => import(/* webpackChunkName: 'OfficeInfoModal' */ './OfficeInfoModal'));
const NUMBER_OF_CANDIDATES_TO_DISPLAY = 5;

// This is related to components/VoterGuide/VoterGuideOfficeItemCompressed
Expand All @@ -29,6 +32,8 @@ class OfficeItemCompressed extends Component {
candidateListForDisplay: [],
showAllCandidates: false,
totalNumberOfCandidates: 0,
officeInfoModalOpen: false,
moreInfoIconHovered: false,
};
}

Expand Down Expand Up @@ -87,6 +92,22 @@ class OfficeItemCompressed extends Component {
});
};

openOfficeInfoModal = () => {
this.setState({ officeInfoModalOpen: true });
};

closeOfficeInfoModal = () => {
this.setState({ officeInfoModalOpen: false });
};

handleMoreInfoIconHover = () => {
this.setState({ moreInfoIconHovered: true });
}

handleMoreInfoIconLeave = () => {
this.setState({ moreInfoIconHovered: false });
}

goToCandidateLink (candidateWeVoteId) {
const { organizationWeVoteId } = this.props;
const candidateLink = organizationWeVoteId ?
Expand All @@ -101,10 +122,11 @@ class OfficeItemCompressed extends Component {

let { ballotItemDisplayName } = this.props;
const { isFirstBallotItem, officeWeVoteId, primaryParty, useHelpDefeatOrHelpWin } = this.props; // classes
const { candidateListLength, showAllCandidates, totalNumberOfCandidates } = this.state;
const { candidateListLength, showAllCandidates, totalNumberOfCandidates, moreInfoIconHovered } = this.state;
ballotItemDisplayName = toTitleCase(ballotItemDisplayName).replace('(Unexpired)', '(Remainder)');
const moreCandidatesToDisplay = candidateListLength > NUMBER_OF_CANDIDATES_TO_DISPLAY;


return (
<OfficeItemCompressedWrapper>
<div
Expand All @@ -116,6 +138,12 @@ class OfficeItemCompressed extends Component {
/>
<OfficeNameH2>
{ballotItemDisplayName}
<InfoOutlined
style={{ color: moreInfoIconHovered ? DesignTokenColors.primary500 : DesignTokenColors.neutral600, cursor: 'pointer', marginLeft: '8px' }}
onMouseEnter={this.handleMoreInfoIconHover}
onMouseLeave={this.handleMoreInfoIconLeave}
onClick={this.openOfficeInfoModal}
/>
{!!primaryParty && (
<PrimaryPartyWrapper>
{' '}
Expand Down Expand Up @@ -144,6 +172,15 @@ class OfficeItemCompressed extends Component {
/>
</Suspense>
)}
{this.state.officeInfoModalOpen && (
<Suspense fallback={<div>Loading...</div>}>
<OfficeInfoModal
isOpen={this.state.officeInfoModalOpen}
onClose={this.closeOfficeInfoModal}
officeName={ballotItemDisplayName}
/>
</Suspense>
)}
</OfficeItemCompressedWrapper>
);
}
Expand Down