From a167b5e353d296e06b498696ff0a201fd4542d0d Mon Sep 17 00:00:00 2001 From: Ricardo Reynoso Date: Tue, 12 May 2026 14:29:25 -0700 Subject: [PATCH] [WV-2660] Election Finder: National filter fix, date-below-title layout, About office on candidates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix /election-finder/na: match elections explicitly tagged state_code='NA' (or 'NA' in state_code_list) instead of treating empty state_code_list as national, which was sweeping in state-level elections that just have unpopulated lists. - Add "National" option to the state dropdown (top of list, above alphabetical states) on Home and ForState. - Stack title and date vertically: title on top, date below in "Mon D, YYYY" format. On Home, non-national rows prefix the election name with " - " (en dash). - Add "About office" entry to the candidate row in both the desktop action bar and the kebab menu, gated behind nextReleaseFeaturesEnabled, stub onClick — mirroring the office row exactly. --- .../ElectionFinderForElection.jsx | 6 ++++ .../ElectionFinder/ElectionFinderForState.jsx | 35 +++++++++++-------- .../ElectionFinder/ElectionFinderHome.jsx | 23 +++++++----- .../ElectionFinder/electionFinderStyles.js | 15 ++++---- 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx b/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx index 9f1440322..09d22b69c 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx @@ -381,6 +381,11 @@ function OfficeSectionItemInner ({ // eslint-disable-line react/no-multi-comp candidateName} /> `${window.location.origin}${getCandidatePath(candidate)}`} /> + {nextReleaseFeaturesEnabled && ( + + + + )} window.open(getCandidatePath(candidate), '_blank')}> @@ -392,6 +397,7 @@ function OfficeSectionItemInner ({ // eslint-disable-line react/no-multi-comp items={[ { key: 'copy-name', icon: ContentCopy, label: 'Copy candidate name', onClick: () => copyAndToast(candidateName) }, { key: 'copy-link', icon: ContentCopy, label: 'Copy link', onClick: () => copyAndToast(`${window.location.origin}${getCandidatePath(candidate)}`) }, + ...(nextReleaseFeaturesEnabled ? [{ key: 'about', icon: InfoOutlined, label: 'About office', onClick: () => {} }] : []), { key: 'open', icon: Launch, label: 'Open in new tab', onClick: () => window.open(getCandidatePath(candidate), '_blank') }, ]} /> diff --git a/src/js/pages/ElectionFinder/ElectionFinderForState.jsx b/src/js/pages/ElectionFinder/ElectionFinderForState.jsx index d70258ba5..1b3beab37 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderForState.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderForState.jsx @@ -17,7 +17,7 @@ import ElectionFinderHeader from './ElectionFinderHeader'; import RowKebabMenu from './RowKebabMenu'; import { ActionDivider, DarkTooltip, - ElectionDatePill, ElectionLink, ElectionList, ElectionRow, ElectionRowActions, + ElectionDateText, ElectionLink, ElectionList, ElectionRow, ElectionRowActions, ElectionRowText, FilterTab, FilterTabsRow, InlineSearchField, NoResults, SearchIconButton, SectionTitle, SectionTitleRow, ShowMoreButton, StateSelectWrapper, StateSelectNative, StateSelectLabel, StateSelectCaret, @@ -30,16 +30,27 @@ const SORTED_STATES = Object.entries(stateCodeMap) .filter(([code]) => code !== 'NA') .sort((a, b) => a[1].localeCompare(b[1])); -function formatDateUS (dateString) { +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 `${m}-${d}-${y}`; + 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 || ''); } +// 'NA' must be matched explicitly: an empty state_code_list does not imply national, +// since some state-level elections come back without state_code_list populated. +function matchesStateCode (election, stateCode) { + if (stateCode === 'NA') { + return election.state_code === 'NA' || (election.state_code_list && election.state_code_list.includes('NA')); + } + return election.state_code_list && election.state_code_list.includes(stateCode); +} + function getBreadcrumbTabLabel (filterTab) { if (filterTab === 'all') return 'All'; if (filterTab === 'past') return 'Past'; @@ -82,9 +93,7 @@ function ElectionFinderForState () { // Auto-switch from "upcoming" to "all" when there are no upcoming elections for this state useEffect(() => { if (electionList.length > 0 && filterTab === 'upcoming') { - const stateElections = electionList.filter( - (el) => el.state_code_list && el.state_code_list.includes(selectedStateCode), - ); + const stateElections = electionList.filter((el) => matchesStateCode(el, selectedStateCode)); const hasUpcoming = stateElections.some((el) => el.election_is_upcoming); if (!hasUpcoming && stateElections.length > 0) { setFilterTab('all'); @@ -110,10 +119,7 @@ function ElectionFinderForState () { historyPush(`/election-finder/${selectedStateCode.toLowerCase()}/${googleCivicElectionId}`); }, [selectedStateCode]); - // Filter elections for this state - let stateElections = electionList.filter( - (el) => el.state_code_list && el.state_code_list.includes(selectedStateCode), - ); + let stateElections = electionList.filter((el) => matchesStateCode(el, selectedStateCode)); // Apply search filter before splitting upcoming/past so counts reflect the search if (searchText) { @@ -167,6 +173,7 @@ function ElectionFinderForState () { + {SORTED_STATES.map(([code, name]) => ( ))} @@ -239,12 +246,12 @@ function ElectionFinderForState () { key={googleCivicElectionId} onClick={() => onElectionSelect(googleCivicElectionId)} > - + + {election.election_name || ''} {election.election_day_text && ( - {formatDateUS(election.election_day_text)} + {formatDateLong(election.election_day_text)} )} - {election.election_name || ''} - + e.stopPropagation()}> + {SORTED_STATES.map(([code, name]) => ( ))} @@ -213,12 +216,16 @@ function ElectionFinderHome () { key={googleCivicElectionId} onClick={() => onElectionSelect(election)} > - + + + {election.state_code && election.state_code !== 'NA' ? + `${convertStateCodeToStateText(election.state_code)} – ${election.election_name || ''}` : + (election.election_name || '')} + {election.election_day_text && ( - {formatDateUS(election.election_day_text)} + {formatDateLong(election.election_day_text)} )} - {election.election_name || ''} - + e.stopPropagation()}> `${window.location.origin}${electionUrl}`} /> diff --git a/src/js/pages/ElectionFinder/electionFinderStyles.js b/src/js/pages/ElectionFinder/electionFinderStyles.js index cbaaea697..514eaeeb0 100644 --- a/src/js/pages/ElectionFinder/electionFinderStyles.js +++ b/src/js/pages/ElectionFinder/electionFinderStyles.js @@ -117,13 +117,11 @@ export const DetailTitle = styled('h2')` margin: 0; `; -export const ElectionDatePill = styled('span')` - display: inline-block; +export const ElectionDateText = styled('span')` color: #848484; - font-size: 13px; - font-weight: 500; - margin-right: 8px; - vertical-align: middle; + font-size: 14px; + font-weight: 400; + margin-top: 2px; white-space: nowrap; `; @@ -136,6 +134,11 @@ export const ElectionLink = styled('span')` } `; +export const ElectionRowText = styled('div')` + display: flex; + flex-direction: column; +`; + export const ElectionList = styled('div')` display: flex; flex-direction: column;