diff --git a/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx b/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx index 09d22b69c..b66668317 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx @@ -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, @@ -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( @@ -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 ( <> @@ -205,14 +208,19 @@ function ElectionFinderForElection () { - {electionName} + + {electionName} + {' '} + {`(${officeCount})`} + {nextReleaseFeaturesEnabled && ( @@ -258,6 +266,10 @@ function ElectionFinderForElection () { )} + {electionDayText && ( + {formatDateLong(electionDayText)} + )} + {filteredOffices.length > 0 && ( @@ -304,7 +316,11 @@ function ElectionFinderForElection () { )} {filteredOffices.length === 0 && ( - {ballotLoaded ? 'No results found.' : 'Loading...'} + + {!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.'} + )} diff --git a/src/js/pages/ElectionFinder/ElectionFinderForState.jsx b/src/js/pages/ElectionFinder/ElectionFinderForState.jsx index 1b3beab37..a2fcd26c5 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderForState.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderForState.jsx @@ -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 { @@ -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 || ''); } diff --git a/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx b/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx index 2996e747c..389804964 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx @@ -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 && {stateLabel}} Election Finder {breadcrumbs && breadcrumbs.length > 0 && ( @@ -28,6 +27,7 @@ function ElectionFinderHeader ({ breadcrumbs, stateLabel, subtitle }) { )} {subtitle && {subtitle}} + {stateLabel && {stateLabel}} ); } diff --git a/src/js/pages/ElectionFinder/ElectionFinderHome.jsx b/src/js/pages/ElectionFinder/ElectionFinderHome.jsx index ba0ae843b..ce0256923 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderHome.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderHome.jsx @@ -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 { @@ -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 || ''); } diff --git a/src/js/pages/ElectionFinder/dateHelpers.js b/src/js/pages/ElectionFinder/dateHelpers.js new file mode 100644 index 000000000..4bc8c3cba --- /dev/null +++ b/src/js/pages/ElectionFinder/dateHelpers.js @@ -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}`; +} diff --git a/src/js/pages/ElectionFinder/electionFinderStyles.js b/src/js/pages/ElectionFinder/electionFinderStyles.js index 514eaeeb0..0e406c3cc 100644 --- a/src/js/pages/ElectionFinder/electionFinderStyles.js +++ b/src/js/pages/ElectionFinder/electionFinderStyles.js @@ -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; @@ -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; @@ -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')`