From 01159043bc93d00a834c4fe9f40525bc5317e567 Mon Sep 17 00:00:00 2001 From: Ricardo Reynoso Date: Tue, 7 Apr 2026 19:37:08 -0700 Subject: [PATCH] [WV-2660] Election Finder: tweaks, date pills, tooltips, search, header, copy link - Date pill before election name (MM-DD-YYYY format, grey label) - Upcoming elections sorted chronologically (soonest first) - Copy link action added to election row hover actions (Home + State) - DarkTooltip styled component matching bootstrap tooltip style - ElectionFinderHeader: stateLabel prop, removed TitleWrapper, breadcrumb spacing - State dropdown "All" tab shows state-specific download tooltip - Search placeholder: "Search..." on mobile, "Search elections..." on desktop - Search field flex-wrap for mobile responsiveness - Removed unused TitleWrapper export from styles - Functional components with isMobileScreenSize for responsive placeholder --- .../ElectionFinderForElection.jsx | 43 +++++++------ .../ElectionFinder/ElectionFinderForState.jsx | 60 ++++++++++++++----- .../ElectionFinder/ElectionFinderHeader.jsx | 12 ++-- .../ElectionFinder/ElectionFinderHome.jsx | 53 ++++++++++++---- .../ElectionFinder/electionFinderStyles.js | 41 ++++++++++--- 5 files changed, 144 insertions(+), 65 deletions(-) diff --git a/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx b/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx index 5bd99c01e..053d530de 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderForElection.jsx @@ -1,5 +1,5 @@ import { ContentCopy, FileDownloadOutlined, InfoOutlined, Launch, Search, Close, ExpandMore, UnfoldMore, UnfoldLess } from '@mui/icons-material'; -import { IconButton, InputAdornment, Tooltip } from '@mui/material'; +import { IconButton, InputAdornment } from '@mui/material'; import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; @@ -11,14 +11,13 @@ import SupportActions from '../../actions/SupportActions'; import { renderLog } from '../../common/utils/logging'; import { convertStateCodeToStateText } from '../../common/utils/addressFunctions'; import AppObservableStore from '../../common/stores/AppObservableStore'; -import { ElectionStateLabel } from '../../components/Style/BallotTitleHeaderStyles'; import { PageContentContainer } from '../../components/Style/pageLayoutStyles'; import BallotStore from '../../stores/BallotStore'; import CandidateStore from '../../stores/CandidateStore'; import ElectionStore from '../../stores/ElectionStore'; import ElectionFinderHeader from './ElectionFinderHeader'; import { - ActionChip, ActionDivider, + ActionChip, ActionDivider, DarkTooltip, CandidateActions as CandidateActionsRow, CandidateInfo, CandidateList, CandidateName, CandidateParty, CandidateRow, DetailTitle, ElectionTitleRow, ExpandCollapseButton, ExpandCollapseRow, ExpandMoreIcon, @@ -201,14 +200,14 @@ function ElectionFinderForElection () { { label: `${stateName} ${isUpcoming ? 'Upcoming' : 'Past'} Elections (${isUpcoming ? upcomingCount : pastCount})`, href: `/election-finder/${selectedStateCode.toLowerCase()}` }, { label: electionName }, ]} + stateLabel={stateName} /> - {stateName} {electionName} - + - + {electionSearchOpen ? ( {`${totalResults} results for \u201C${electionSearchText}\u201D`} - + - + )} @@ -323,27 +322,27 @@ function OfficeSectionItemInner ({ // eslint-disable-line react/no-multi-comp e.stopPropagation()}> - + copyToClipboard(officeName)}> Copy office name - - + + copyToClipboard(`${window.location.origin}/office/${officeWeVoteId}`)}> Copy link - + - + - - + + window.open(`/office/${officeWeVoteId}`, '_blank')}> - + {isExpanded && ( @@ -361,24 +360,24 @@ function OfficeSectionItemInner ({ // eslint-disable-line react/no-multi-comp {candidateParty} - + copyToClipboard(candidateName)}> Copy candidate name - - + + copyToClipboard(`${window.location.origin}${getCandidatePath(candidate)}`)}> Copy link - + - + window.open(getCandidatePath(candidate), '_blank')}> - + ); diff --git a/src/js/pages/ElectionFinder/ElectionFinderForState.jsx b/src/js/pages/ElectionFinder/ElectionFinderForState.jsx index 7d032c6d0..7af5da126 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderForState.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderForState.jsx @@ -1,17 +1,19 @@ -import { ExpandMore, FileDownloadOutlined, Launch, Search, Close } from '@mui/icons-material'; -import { IconButton, InputAdornment, Tooltip } from '@mui/material'; +import { ContentCopy, ExpandMore, FileDownloadOutlined, Launch, Search, Close } from '@mui/icons-material'; +import { IconButton, InputAdornment } from '@mui/material'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; import ElectionActions from '../../actions/ElectionActions'; import { renderLog } from '../../common/utils/logging'; import { stateCodeMap, convertStateCodeToStateText } from '../../common/utils/addressFunctions'; +import isMobileScreenSize from '../../common/utils/isMobileScreenSize'; import { PageContentContainer } from '../../components/Style/pageLayoutStyles'; import historyPush from '../../common/utils/historyPush'; import ElectionStore from '../../stores/ElectionStore'; import ElectionFinderHeader from './ElectionFinderHeader'; import { - ElectionLink, ElectionList, ElectionRow, ElectionRowActions, + ActionChip, ActionDivider, DarkTooltip, + ElectionDatePill, ElectionLink, ElectionList, ElectionRow, ElectionRowActions, FilterTab, FilterTabsRow, InlineSearchField, NoResults, SearchIconButton, SectionTitle, SectionTitleRow, ShowMoreButton, StateSelectWrapper, StateSelectNative, StateSelectLabel, StateSelectCaret, @@ -21,6 +23,16 @@ const SORTED_STATES = Object.entries(stateCodeMap) .filter(([code]) => code !== 'NA') .sort((a, b) => a[1].localeCompare(b[1])); +function formatDateUS (dateString) { + if (!dateString) return ''; + const [y, m, d] = dateString.split('-'); + return `${m}-${d}-${y}`; +} + +function sortByDateAsc (a, b) { + return (a.election_day_text || '').localeCompare(b.election_day_text || ''); +} + function getBreadcrumbTabLabel (filterTab) { if (filterTab === 'all') return 'All'; if (filterTab === 'past') return 'Past'; @@ -105,7 +117,7 @@ function ElectionFinderForState () { ); } - const upcomingElections = stateElections.filter((el) => el.election_is_upcoming); + const upcomingElections = stateElections.filter((el) => el.election_is_upcoming).sort(sortByDateAsc); const pastElections = stateElections.filter((el) => !el.election_is_upcoming); const upcomingCount = upcomingElections.length; const pastCount = pastElections.length; @@ -123,9 +135,14 @@ function ElectionFinderForState () { sectionTitle = `${stateName} \u2013 Past Elections`; } - const sectionDownloadLabel = filterTab === 'past' ? - 'Download data for all past elections' : - 'Download data for all upcoming elections'; + let sectionDownloadLabel; + if (filterTab === 'all') { + sectionDownloadLabel = `Download data for all ${stateName} elections`; + } else if (filterTab === 'past') { + sectionDownloadLabel = 'Download data for all past elections'; + } else { + sectionDownloadLabel = 'Download data for all upcoming elections'; + } return ( <> @@ -162,7 +179,7 @@ function ElectionFinderForState () { { @@ -199,32 +216,43 @@ function ElectionFinderForState () { {sectionTitle} - + - + {(searchText ? displayElections : displayElections.slice(0, visibleCount)).map((election) => { const googleCivicElectionId = election.google_civic_election_id; - const electionLabel = `${election.election_name || ''} \u2013 ${election.election_day_text || ''}`; return ( onElectionSelect(googleCivicElectionId)} > - {electionLabel} + {election.election_day_text && ( + {formatDateUS(election.election_day_text)} + )} + {election.election_name || ''} e.stopPropagation()}> - + + navigator.clipboard.writeText(`${window.location.origin}/election-finder/${selectedStateCode.toLowerCase()}/${googleCivicElectionId}`)} + > + + Copy link + + + + - - + + window.open(`/election-finder/${selectedStateCode.toLowerCase()}/${googleCivicElectionId}`, '_blank')}> - + ); diff --git a/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx b/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx index db34a8693..57b362962 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderHeader.jsx @@ -1,14 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ElectionNameH1 } from '../../components/Style/BallotTitleHeaderStyles'; -import { Breadcrumb, BreadcrumbAnchor, TitleWrapper } from './electionFinderStyles'; +import { ElectionNameH1, ElectionStateLabel } from '../../components/Style/BallotTitleHeaderStyles'; +import { Breadcrumb, BreadcrumbAnchor } from './electionFinderStyles'; -function ElectionFinderHeader ({ breadcrumbs, subtitle }) { +function ElectionFinderHeader ({ breadcrumbs, stateLabel, subtitle }) { return ( <> - - Election Finder - + {stateLabel && {stateLabel}} + Election Finder {breadcrumbs && breadcrumbs.length > 0 && ( {breadcrumbs.map((crumb, idx) => { @@ -38,6 +37,7 @@ ElectionFinderHeader.propTypes = { label: PropTypes.string.isRequired, href: PropTypes.string, })), + stateLabel: PropTypes.string, subtitle: PropTypes.string, }; diff --git a/src/js/pages/ElectionFinder/ElectionFinderHome.jsx b/src/js/pages/ElectionFinder/ElectionFinderHome.jsx index 9695ddb33..4701b9219 100644 --- a/src/js/pages/ElectionFinder/ElectionFinderHome.jsx +++ b/src/js/pages/ElectionFinder/ElectionFinderHome.jsx @@ -1,21 +1,33 @@ -import { ExpandMore, FileDownloadOutlined, Launch, Search, Close } from '@mui/icons-material'; -import { IconButton, InputAdornment, Tooltip } from '@mui/material'; +import { ContentCopy, ExpandMore, FileDownloadOutlined, Launch, Search, Close } from '@mui/icons-material'; +import { IconButton, InputAdornment } from '@mui/material'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import ElectionActions from '../../actions/ElectionActions'; import { renderLog } from '../../common/utils/logging'; import { stateCodeMap } from '../../common/utils/addressFunctions'; +import isMobileScreenSize from '../../common/utils/isMobileScreenSize'; import { PageContentContainer } from '../../components/Style/pageLayoutStyles'; import historyPush from '../../common/utils/historyPush'; import ElectionStore from '../../stores/ElectionStore'; import ElectionFinderHeader from './ElectionFinderHeader'; import { - ElectionLink, ElectionList, ElectionRow, ElectionRowActions, + ActionChip, ActionDivider, DarkTooltip, + ElectionDatePill, ElectionLink, ElectionList, ElectionRow, ElectionRowActions, FilterTab, FilterTabsRow, InlineSearchField, NoResults, SearchIconButton, SectionTitle, SectionTitleRow, ShowMoreButton, StateSelectWrapper, StateSelectNative, StateSelectLabel, StateSelectCaret, } from './electionFinderStyles'; +function formatDateUS (dateString) { + if (!dateString) return ''; + const [y, m, d] = dateString.split('-'); + return `${m}-${d}-${y}`; +} + +function sortByDateAsc (a, b) { + return (a.election_day_text || '').localeCompare(b.election_day_text || ''); +} + const SORTED_STATES = Object.entries(stateCodeMap) .filter(([code]) => code !== 'NA') .sort((a, b) => a[1].localeCompare(b[1])); @@ -87,7 +99,7 @@ function ElectionFinderHome () { ); } - const upcomingElections = filteredByState.filter((el) => el.election_is_upcoming); + const upcomingElections = filteredByState.filter((el) => el.election_is_upcoming).sort(sortByDateAsc); const pastElections = filteredByState.filter((el) => !el.election_is_upcoming); const upcomingCount = upcomingElections.length; const pastCount = pastElections.length; @@ -137,7 +149,7 @@ function ElectionFinderHome () { { @@ -174,28 +186,43 @@ function ElectionFinderHome () { {sectionTitle} - + - + {(searchText ? displayElections : displayElections.slice(0, visibleCount)).map((election) => { const googleCivicElectionId = election.google_civic_election_id; - const electionLabel = `${election.election_name || ''} \u2013 ${election.election_day_text || ''}`; return ( onElectionSelect(election)} > - {electionLabel} + {election.election_day_text && ( + {formatDateUS(election.election_day_text)} + )} + {election.election_name || ''} e.stopPropagation()}> - + + { + const sc = (election.state_code_list && election.state_code_list.length > 0) ? + election.state_code_list[0].toLowerCase() : + 'na'; + navigator.clipboard.writeText(`${window.location.origin}/election-finder/${sc}/${googleCivicElectionId}`); + }} + > + + Copy link + + + + - - + + { @@ -207,7 +234,7 @@ function ElectionFinderHome () { > - + ); diff --git a/src/js/pages/ElectionFinder/electionFinderStyles.js b/src/js/pages/ElectionFinder/electionFinderStyles.js index b5904c391..85f560d5e 100644 --- a/src/js/pages/ElectionFinder/electionFinderStyles.js +++ b/src/js/pages/ElectionFinder/electionFinderStyles.js @@ -1,6 +1,25 @@ -import { TextField } from '@mui/material'; +import { TextField, Tooltip, tooltipClasses } from '@mui/material'; +import React from 'react'; // eslint-disable-line no-unused-vars import styled from 'styled-components'; +// eslint-disable-next-line react/jsx-props-no-spreading, react/react-in-jsx-scope +export const DarkTooltip = styled(({ className, ...props }) => ( + // eslint-disable-line react/jsx-props-no-spreading +))` + & .${tooltipClasses.tooltip} { + background-color: rgba(0, 0, 0, 0.9); + color: #fff; + font-family: "Poppins", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 400; + letter-spacing: 0.15px; + padding: 4px 8px; + border-radius: 4px; + max-width: 200px; + text-align: center; + } +`; + export const ActionChip = styled('button')` display: inline-flex; align-items: center; @@ -27,7 +46,8 @@ export const ActionDivider = styled('span')` export const Breadcrumb = styled('div')` font-size: 14px; color: #555; - margin-bottom: 4px; + margin-top: -14px; + margin-bottom: 8px; `; export const BreadcrumbAnchor = styled('a')` @@ -95,6 +115,16 @@ export const DetailTitle = styled('h2')` margin: 0; `; +export const ElectionDatePill = styled('span')` + display: inline-block; + color: #848484; + font-size: 13px; + font-weight: 500; + margin-right: 8px; + vertical-align: middle; + white-space: nowrap; +`; + export const ElectionLink = styled('span')` font-size: 17px; color: #206bc4; @@ -199,7 +229,7 @@ export const HighlightSpan = styled('span')` `; export const InlineSearchField = styled(TextField)` - flex: 1; + flex: 1 1 150px; max-width: 280px; & .MuiOutlinedInput-root { height: 26px; @@ -348,8 +378,3 @@ export const StateSelectCaret = styled('span')` color: #555; font-size: 18px; `; - - -export const TitleWrapper = styled('div')` - margin-top: 20px; -`;