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
26 changes: 21 additions & 5 deletions src/js/pages/ElectionFinder/ElectionFinderForElection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import SnackNotifier from '../../common/components/Widgets/SnackNotifier';
import ElectionStore from '../../stores/ElectionStore';
import CopyChip from './CopyChip';
import copyAndToast from './copyAndToast';
import formatDateLong from './dateHelpers';
import ElectionFinderHeader from './ElectionFinderHeader';
import RowKebabMenu from './RowKebabMenu';
import {
ActionDivider, DarkTooltip,
CandidateActions as CandidateActionsRow, CandidateInfo, CandidateList, CandidateName,
CandidateParty, CandidateRow, DetailTitle, ElectionTitleRow,
CandidateParty, CandidateRow, DetailTitle, ElectionDetailDate, ElectionTitleRow,
ExpandCollapseButton, ExpandCollapseRow, ExpandMoreIcon,
HighlightSpan, InlineSearchField, NoResults,
OfficeHeader, OfficeHeaderActions, OfficeHeaderLeft, OfficeName, OfficePrimaryPartySpan,
Expand Down Expand Up @@ -70,6 +71,7 @@ function ElectionFinderForElection () {

// Derived from stores — no need to cache in state
const electionName = ElectionStore.getElectionName(googleCivicElectionId) || 'Election';
const electionDayText = ElectionStore.getElectionDayText(googleCivicElectionId);
const isUpcoming = ElectionStore.isElectionUpcoming(googleCivicElectionId);
const electionList = ElectionStore.getElectionList();
const stateElections = electionList.filter(
Expand Down Expand Up @@ -197,6 +199,7 @@ function ElectionFinderForElection () {
const totalResults = electionSearchText ?
filteredOffices.reduce((sum, o) => sum + (o.candidate_list || []).length, 0) :
null;
const officeCount = ballotItems.length;

return (
<>
Expand All @@ -205,14 +208,19 @@ function ElectionFinderForElection () {
<PageContentContainer>
<ElectionFinderHeader
breadcrumbs={[
{ label: `\u2190 ${stateName} ${isUpcoming ? 'Upcoming' : 'Past'} Elections (${isUpcoming ? upcomingCount : pastCount})`, href: `/election-finder/${selectedStateCode.toLowerCase()}` },
{ label: electionName },
{ label: '\u2190 Election Finder Home', href: '/election-finder' },
{ label: `${isUpcoming ? 'Upcoming' : 'Past'} Elections - ${stateName} (${isUpcoming ? upcomingCount : pastCount})`, href: `/election-finder/${selectedStateCode.toLowerCase()}` },
{ label: `${electionName}${electionDayText ? ` - ${formatDateLong(electionDayText)}` : ''} (${officeCount})` },
]}
stateLabel={stateName}
/>

<ElectionTitleRow>
<DetailTitle>{electionName}</DetailTitle>
<DetailTitle>
{electionName}
{' '}
{`(${officeCount})`}
</DetailTitle>
{nextReleaseFeaturesEnabled && (
<DarkTooltip title="Download election data">
<IconButton size="small"><FileDownloadOutlined fontSize="small" /></IconButton>
Expand Down Expand Up @@ -258,6 +266,10 @@ function ElectionFinderForElection () {
)}
</ElectionTitleRow>

{electionDayText && (
<ElectionDetailDate>{formatDateLong(electionDayText)}</ElectionDetailDate>
)}

{filteredOffices.length > 0 && (
<ExpandCollapseRow>
<ExpandCollapseButton onClick={expandAll}>
Expand Down Expand Up @@ -304,7 +316,11 @@ function ElectionFinderForElection () {
)}

{filteredOffices.length === 0 && (
<NoResults>{ballotLoaded ? 'No results found.' : 'Loading...'}</NoResults>
<NoResults>
{!ballotLoaded && 'Loading...'}
{ballotLoaded && electionSearchText && 'No results found.'}
{ballotLoaded && !electionSearchText && 'Our team hasn’t assembled the data for this election yet. We usually have election data 45 days before each election.'}
</NoResults>
)}
</PageContentContainer>
</>
Expand Down
9 changes: 1 addition & 8 deletions src/js/pages/ElectionFinder/ElectionFinderForState.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SnackNotifier from '../../common/components/Widgets/SnackNotifier';
import ElectionStore from '../../stores/ElectionStore';
import CopyChip from './CopyChip';
import copyAndToast from './copyAndToast';
import formatDateLong from './dateHelpers';
import ElectionFinderHeader from './ElectionFinderHeader';
import RowKebabMenu from './RowKebabMenu';
import {
Expand All @@ -30,14 +31,6 @@ const SORTED_STATES = Object.entries(stateCodeMap)
.filter(([code]) => code !== 'NA')
.sort((a, b) => a[1].localeCompare(b[1]));

const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

function formatDateLong (dateString) {
if (!dateString) return '';
const [y, m, d] = dateString.split('-');
return `${MONTH_ABBR[parseInt(m, 10) - 1]} ${parseInt(d, 10)}, ${y}`;
}

function sortByDateAsc (a, b) {
return (a.election_day_text || '').localeCompare(b.election_day_text || '');
}
Expand Down
6 changes: 3 additions & 3 deletions src/js/pages/ElectionFinder/ElectionFinderHeader.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ElectionNameH1, ElectionStateLabel } from '../../components/Style/BallotTitleHeaderStyles';
import { Breadcrumb, BreadcrumbAnchor } from './electionFinderStyles';
import { ElectionNameH1 } from '../../components/Style/BallotTitleHeaderStyles';
import { Breadcrumb, BreadcrumbAnchor, ElectionFinderStateLabel } from './electionFinderStyles';

function ElectionFinderHeader ({ breadcrumbs, stateLabel, subtitle }) {
return (
<>
{stateLabel && <ElectionStateLabel>{stateLabel}</ElectionStateLabel>}
<ElectionNameH1>Election Finder</ElectionNameH1>
{breadcrumbs && breadcrumbs.length > 0 && (
<Breadcrumb>
Expand All @@ -28,6 +27,7 @@ function ElectionFinderHeader ({ breadcrumbs, stateLabel, subtitle }) {
</Breadcrumb>
)}
{subtitle && <Breadcrumb>{subtitle}</Breadcrumb>}
{stateLabel && <ElectionFinderStateLabel>{stateLabel}</ElectionFinderStateLabel>}
</>
);
}
Expand Down
9 changes: 1 addition & 8 deletions src/js/pages/ElectionFinder/ElectionFinderHome.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SnackNotifier from '../../common/components/Widgets/SnackNotifier';
import ElectionStore from '../../stores/ElectionStore';
import CopyChip from './CopyChip';
import copyAndToast from './copyAndToast';
import formatDateLong from './dateHelpers';
import ElectionFinderHeader from './ElectionFinderHeader';
import RowKebabMenu from './RowKebabMenu';
import {
Expand All @@ -25,14 +26,6 @@ import webAppConfig from '../../config';

const nextReleaseFeaturesEnabled = webAppConfig.ENABLE_NEXT_RELEASE_FEATURES === undefined ? false : webAppConfig.ENABLE_NEXT_RELEASE_FEATURES;

const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

function formatDateLong (dateString) {
if (!dateString) return '';
const [y, m, d] = dateString.split('-');
return `${MONTH_ABBR[parseInt(m, 10) - 1]} ${parseInt(d, 10)}, ${y}`;
}

function sortByDateAsc (a, b) {
return (a.election_day_text || '').localeCompare(b.election_day_text || '');
}
Expand Down
8 changes: 8 additions & 0 deletions src/js/pages/ElectionFinder/dateHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// "2026-03-17" -> "Mar 17, 2026"
export default function formatDateLong (dateString) {
if (!dateString) return '';
const [y, m, d] = dateString.split('-');
return `${MONTH_ABBR[parseInt(m, 10) - 1]} ${parseInt(d, 10)}, ${y}`;
}
22 changes: 21 additions & 1 deletion src/js/pages/ElectionFinder/electionFinderStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { TextField, Tooltip, tooltipClasses } from '@mui/material';
import React from 'react'; // eslint-disable-line no-unused-vars
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import colors from '../../common/components/Style/Colors';
import { ElectionStateLabel } from '../../components/Style/BallotTitleHeaderStyles';

export const ActionChip = styled('button')`
display: inline-flex;
Expand Down Expand Up @@ -125,6 +127,23 @@ export const ElectionDateText = styled('span')`
white-space: nowrap;
`;

// Date shown directly under the election title on the ForElection page.
// Color matches ElectionStateLabel so the state line + date frame the title in the same hue.
export const ElectionDetailDate = styled('div')`
color: ${colors.middleGrey};
font-size: 16px;
font-weight: 400;
margin-top: 0;
margin-bottom: 16px;
white-space: nowrap;
`;

// Slightly larger state label used in the Election Finder header.
// Scoped so the shared ElectionStateLabel stays unchanged on Ballot pages.
export const ElectionFinderStateLabel = styled(ElectionStateLabel)`
font-size: 15px;
`;

export const ElectionLink = styled('span')`
font-size: 17px;
color: #206bc4;
Expand Down Expand Up @@ -176,7 +195,8 @@ export const ElectionTitleRow = styled('div')`
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
margin-top: 4px;
margin-bottom: 0;
`;

export const ExpandCollapseButton = styled('button')`
Expand Down
Loading