From f993fce54401c90f92973d38ec41483df5e45e9a Mon Sep 17 00:00:00 2001 From: Arend Peter Date: Sat, 14 Sep 2024 00:04:57 -0700 Subject: [PATCH 01/19] Apply feedback after discussing w/ Sara --- packages/backend/src/Util.ts | 2 +- packages/frontend/src/App.tsx | 7 +- .../Election/Voting/GenericBallotView.jsx | 2 +- .../ElectionForm/CreateElectionDialog.tsx | 6 +- .../ElectionForm/Details/ElectionAuthForm.tsx | 16 ++--- .../src/components/ElectionForm/QuickPoll.tsx | 4 +- packages/frontend/src/components/Footer.tsx | 2 +- .../frontend/src/components/LandingPage.tsx | 5 +- .../LandingPage/LandingPageHero.tsx | 2 +- .../LandingPage/LandingPageSignUpBar.tsx | 12 ++-- packages/frontend/src/components/styles.tsx | 11 +-- packages/frontend/src/components/util.tsx | 3 +- packages/frontend/src/i18n/en.yaml | 72 +++++++++++++------ packages/frontend/src/index.css | 37 ++++++++++ 14 files changed, 129 insertions(+), 52 deletions(-) diff --git a/packages/backend/src/Util.ts b/packages/backend/src/Util.ts index ffb45f70..c51ca89b 100644 --- a/packages/backend/src/Util.ts +++ b/packages/backend/src/Util.ts @@ -93,7 +93,7 @@ export async function getMetaTags(req: any) : Promise { let n_cropped = (len > 5) ? 0 : (5-len); return { - __META_TITLE__: election?.title ?? 'BetterVoting | Create polls & elections that don\'t spoil the vote', + __META_TITLE__: election?.title ?? 'BetterVoting | Create elections & polls that don\'t spoil the vote', __META_DESCRIPTION__: election?.description ?? "Create secure elections with voting methods that don't spoil the vote.", __META_IMAGE__: election == null ? 'https://assets.nationbuilder.com/unifiedprimary/pages/1470/attachments/original/1702692040/Screenshot_2023-12-15_at_6.00.24_PM.png?1702692040' : diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 86bea73d..cf5e4ac7 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -5,7 +5,7 @@ import Header from './components/Header' import Election from './components/Election/Election' import Sandbox from './components/Sandbox' import LandingPage from './components/LandingPage' -import { Box, Button, CssBaseline, Dialog } from '@mui/material' +import { Box, Button, CssBaseline, Dialog, Typography } from '@mui/material' import { SnackbarContextProvider } from './components/SnackbarContext' import Footer from './components/Footer' import { ConfirmDialogProvider } from './components/ConfirmationDialogProvider' @@ -20,8 +20,10 @@ import CreateElectionDialog, { CreateElectionContextProvider } from './component import ComposeContextProviders from './components/ComposeContextProviders' import './i18n/i18n' import ReturnToClassicDialog, { ReturnToClassicContextProvider } from './components/ReturnToClassicDialog' +import { useSubstitutedTranslation } from './components/util' const App = () => { + const {t} = useSubstitutedTranslation(); return ( {
+ + {t('nav.beta_warning')} + - + )} {/* HEADING TITLES (i.e. worst best for STAR )*/} diff --git a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx index 481cee4d..11fcb81f 100644 --- a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx @@ -76,7 +76,7 @@ const templateMappers = { 'demo': (election:NewElection):NewElection => ({ ...election, }), - 'public': (election:NewElection):NewElection => ({ + /*'public': (election:NewElection):NewElection => ({ ...election, is_public: true, settings: { @@ -86,7 +86,7 @@ const templateMappers = { voter_id: true }, } - }), + }),*/ 'unlisted': (election:NewElection):NewElection => ({ ...election, is_public: false, @@ -317,7 +317,7 @@ export default () => { {t('election_creation.template_prompt')} {/*hacky padding*/} - {(election.settings.voter_access === 'closed'? ['email_list', 'id_list'] : ['demo', 'public', 'unlisted']).map((name, i) => + {(election.settings.voter_access === 'closed'? ['email_list', 'id_list'] : ['demo', 'unlisted']).map((name, i) => {t('admin_home.voter_authentication.help_text')} - handleUpdate({})} - value="none" - />} - label={t('admin_home.voter_authentication.no_limit_label')} /> } label={t('admin_home.voter_authentication.email_label')} /> + handleUpdate({})} + value="none" + />} + label={t('admin_home.voter_authentication.no_limit_label')} /> diff --git a/packages/frontend/src/components/ElectionForm/QuickPoll.tsx b/packages/frontend/src/components/ElectionForm/QuickPoll.tsx index 4b307daf..1ce63fe0 100644 --- a/packages/frontend/src/components/ElectionForm/QuickPoll.tsx +++ b/packages/frontend/src/components/ElectionForm/QuickPoll.tsx @@ -68,6 +68,7 @@ const QuickPoll = ({ authSession, methodName, methodKey, grow }) => { public_results: true, random_candidate_order: false, require_instruction_confirmation: true, + term_type: 'poll', } } @@ -173,7 +174,8 @@ const QuickPoll = ({ authSession, methodName, methodKey, grow }) => { borderRadius: '20px', minWidth: {xs: '0px', md: '400px'} }}> - {t('landing_page.quick_poll.title')} diff --git a/packages/frontend/src/components/LandingPage.tsx b/packages/frontend/src/components/LandingPage.tsx index 90f7a466..75ce15cc 100644 --- a/packages/frontend/src/components/LandingPage.tsx +++ b/packages/frontend/src/components/LandingPage.tsx @@ -39,8 +39,9 @@ const LandingPage = () => { //apparently box doesn't have onScroll return (
- - diff --git a/packages/frontend/src/components/LandingPage/LandingPageHero.tsx b/packages/frontend/src/components/LandingPage/LandingPageHero.tsx index 9a127c3f..38324099 100644 --- a/packages/frontend/src/components/LandingPage/LandingPageHero.tsx +++ b/packages/frontend/src/components/LandingPage/LandingPageHero.tsx @@ -131,7 +131,7 @@ export default ({}) => { sx={{width: '100%'}} > {imgIndex != methodKeys.length-1 ? <> - + {imgIndex == 0 && } diff --git a/packages/frontend/src/components/LandingPage/LandingPageSignUpBar.tsx b/packages/frontend/src/components/LandingPage/LandingPageSignUpBar.tsx index e285befa..e5a2a61a 100644 --- a/packages/frontend/src/components/LandingPage/LandingPageSignUpBar.tsx +++ b/packages/frontend/src/components/LandingPage/LandingPageSignUpBar.tsx @@ -20,25 +20,27 @@ export default () => { p: { xs: 2}, }}> - {t('landing_page.sign_up.text')} + {t('landing_page.sign_up.text')} {/*I just copied styled button but removed the full width*/ } - {pages.map((page, n) => ( - + {pages.map((page, pageIndex) => ( + setCurrentPage(n)} + onClick={() => setCurrentPage(pageIndex)} style={{ fontSize: "16px", width: "auto", minWidth: "0px", marginTop: "10px", paddingLeft: "0px", paddingRight: "0px" }} > {/*TODO: I can probably do this in css using the :selected property*/} - - {page.candidates.some((c) => (c.score > 0)) ? : } + + {page.candidates.some((candidate) => (candidate.score > 0)) ? : } diff --git a/packages/shared/src/domain_model/Candidate.ts b/packages/shared/src/domain_model/Candidate.ts index 74fa1eeb..f93f1fc3 100644 --- a/packages/shared/src/domain_model/Candidate.ts +++ b/packages/shared/src/domain_model/Candidate.ts @@ -10,4 +10,5 @@ export interface Candidate { candidate_url?: string; // link to info about candidate partyUrl?: string; // link to info about party photo_filename?:string; // link to info about party + score?: number; // score for the candidate } From 7fc257831380c038732332f8a6a72a1b75939422 Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Wed, 4 Sep 2024 06:09:58 -0600 Subject: [PATCH 09/19] finished with warning messages and blocking submit --- .../GenericBallotView/GenericBallotView.tsx | 6 +-- .../Election/Voting/RankedBallotView.tsx | 52 ++++++++++++------- .../components/Election/Voting/VotePage.tsx | 24 ++++++--- packages/frontend/src/i18n/en.yaml | 2 + 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx index cffce6bf..c2a52fcd 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx @@ -93,8 +93,8 @@ export default function GenericBallotView({ {t('ballot.this_election_uses', {voting_method: methodName})} - {t(`ballot.methods.${methodKey}.instruction_bullets`).map(bullet => - + {t(`ballot.methods.${methodKey}.instruction_bullets`).map((bullet, bulletIndex) => + {bullet} )} @@ -137,7 +137,7 @@ export default function GenericBallotView({ { warnings.map((warning, i) => - ballotContext.candidates.map(candidate => candidate.score), [ballotContext]); - - const skippedColumns = useMemo(() => { - const skippedColumns = []; + const findSkippedColumns = useCallback((scores: number[]): number[] => { + const skippedColumns: number[] = []; for (let i = 1; i <= maxRankings; i++) { if (!scores.includes(i) && scores.some(score => score > i)) { skippedColumns.push(i); } } return skippedColumns; - }, [scores, maxRankings]); - const matchingScores = useMemo(() => { + }, [maxRankings]); + const findMatchingScores = useCallback((scores: number[]): [number, number][] => { const scoreMap = new Map(); // Populate the map with indexes for each score @@ -53,7 +53,7 @@ export default function RankedBallotView({ onlyGrid = false }) { }); // Filter out entries with only one index - const matchingScores = [] + const matchingScores= [] scoreMap.forEach((indexes, score) => { if (indexes.length > 1) { indexes.map(index => matchingScores.push([index, score])) @@ -61,24 +61,40 @@ export default function RankedBallotView({ onlyGrid = false }) { }); return matchingScores - }, [scores]); - const warnings = useMemo(() => { + }, []); + const getWarnings = useCallback((skippedColumns, matchingScores):{color: string, message:string}[] => { const warnings = []; if (skippedColumns.length) { - warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), color: 'brand.goldTransparent20' }); + warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), color: "brand.goldTransparent20" }); } if (matchingScores.length) { - warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), color: 'brand.red' }); + warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), color: "brand.red" }); } return warnings; - }, [skippedColumns, matchingScores, t]); + }, [t]); + const race = useMemo(() => ballotContext.race, [ballotContext.race]); + const [skippedColumns, setSkippedColumns] = useState(findSkippedColumns(ballotContext.candidates.map((c) => c.score))); + const [matchingScores, setMatchingScores] = useState(findMatchingScores(ballotContext.candidates.map((c) => c.score))); + const [warnings, setWarnings] = useState(getWarnings(skippedColumns, matchingScores)); + + const onClick = useCallback((candidateIndex, columnValue) => { // If the candidate already has the score, remove it. Otherwise, set it with the new score. + const scores = ballotContext.candidates.map((candidate) => candidate.score); scores[candidateIndex] = scores[candidateIndex] === columnValue ? null : columnValue; - + const skippedColumns = findSkippedColumns(scores); + const matchingScores = findMatchingScores(scores); + const warnings = getWarnings(skippedColumns, matchingScores); + setSkippedColumns(skippedColumns); + setMatchingScores(matchingScores); + setWarnings(warnings); + if (warnings.length) { + ballotContext.setHasAlert(true); + } else { + ballotContext.setHasAlert(false); + } ballotContext.onUpdate(scores); - }, [scores, race.voting_method, ballotContext]); - + }, [race.voting_method, ballotContext]); const columnValues = useMemo(() => { diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 194bd067..199fd71e 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -26,7 +26,7 @@ const INFO_ICON = "M 11 7 h 2 v 2 h -2 Z m 0 4 h 2 v 6 h -2 Z m 1 -9 C 6.48 2 2 const CHECKED_BOX = "M 19 3 H 5 c -1.11 0 -2 0.9 -2 2 v 14 c 0 1.1 0.89 2 2 2 h 14 c 1.11 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.89 -2 -2 -2 Z m -9 14 l -5 -5 l 1.41 -1.41 L 10 14.17 l 7.59 -7.59 L 19 8 l -9 9 Z" //const UNCHECKED_BOX = "M 19 5 v 14 H 5 V 5 h 14 m 0 -2 H 5 c -1.1 0 -2 0.9 -2 2 v 14 c 0 1.1 0.9 2 2 2 h 14 c 1.1 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.9 -2 -2 -2 Z" const DOT_ICON = "M12 6c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6 2.69-6 6-6m0-2c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z" - +const WARNING_ICON = "M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" type receiptEmail = { sendReceipt: boolean, email: string @@ -40,6 +40,8 @@ export interface IBallotContext { receiptEmail: receiptEmail, setReceiptEmail: React.Dispatch maxRankings?: number, + hasAlert: boolean, + setHasAlert: (boolean) => void } export const BallotContext = createContext(null); @@ -73,7 +75,7 @@ const VotePage = () => { candidates: (flags.isSet('FORCE_DISABLE_RANDOM_CANDIDATES') || !election.settings.random_candidate_order) ? candidates : shuffle(candidates), voting_method: race.voting_method, race_index: raceIndex, - hasWarning: false + hasAlert: false } }) return pages @@ -89,6 +91,11 @@ const VotePage = () => { // shallow copy to trigger a refresh setPages([...pages]) } + const setHasAlert = (hasAlert) => { + pages[currentPage].hasAlert = hasAlert; + // shallow copy to trigger a refresh + setPages([...pages]) + } const [isOpen, setIsOpen] = useState(false) const { data, isPending, error, makeRequest: postBallot } = usePostBallot(election.election_id) @@ -146,7 +153,9 @@ const VotePage = () => { onUpdate: newRankings => onUpdate(currentPage, newRankings), receiptEmail: receiptEmail, setReceiptEmail: setReceiptEmail, - maxRankings: election.settings.max_rankings + maxRankings: election.settings.max_rankings, + hasAlert: pages[currentPage].hasAlert, + setHasAlert: setHasAlert }}> @@ -169,8 +178,8 @@ const VotePage = () => { > {/*TODO: I can probably do this in css using the :selected property*/} - - {page.candidates.some((candidate) => (candidate.score > 0)) ? : } + + {page.hasAlert ? : page.candidates.some((candidate) => (candidate.score > 0)) ? : } @@ -191,7 +200,7 @@ const VotePage = () => { @@ -229,8 +238,9 @@ const VotePage = () => { }} onChange={(e) => setReceiptEmail({...receiptEmail, email: e.target.value})} /> - {pages.map((page) => ( + {pages.map((page, pageIndex) => ( <> + key={pageIndex} {election.races[page.race_index].title} diff --git a/packages/frontend/src/i18n/en.yaml b/packages/frontend/src/i18n/en.yaml index 4cd6f311..d82152e2 100644 --- a/packages/frontend/src/i18n/en.yaml +++ b/packages/frontend/src/i18n/en.yaml @@ -232,6 +232,8 @@ ballot: duplicate_rank_warning: Giving multiple candidates the same ranking is not recommended in IRV. This could result in your ballot getting exhausted early. skipped_rank_warning: Skipping a rank is not recommended in IRV. This could create some abiguity in your ballot. heading_prefix: 'Rank {{candidates}}:' + left_title: 'Best' + right_title: 'Worst' ranked_robin: instruction_bullets: - Rank the {{lowercase_candidates}} in order of preference. From e4b6ff9b5541f98b24f68ffdf5ff9672eb33e11a Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Wed, 4 Sep 2024 20:07:36 -0600 Subject: [PATCH 10/19] updated warning to use alert component and warnings are only added with RCV --- .../components/ConfirmationDialogProvider.tsx | 4 +-- .../GenericBallotView/GenericBallotView.tsx | 16 +++++----- .../Election/Voting/RankedBallotView.tsx | 30 ++++++++++--------- .../components/Election/Voting/VotePage.tsx | 7 +++-- .../ElectionForm/Candidates/AddCandidate.tsx | 2 +- .../ElectionForm/CreateElectionDialog.tsx | 6 ++-- .../Details/ElectionDetailsInlineForm.tsx | 4 +-- .../components/ElectionForm/Races/AddRace.tsx | 2 +- .../ElectionForm/Races/RaceDialog.tsx | 4 +-- .../LandingPage/FeaturedElection.tsx | 17 +++++++++-- packages/frontend/src/i18n/en.yaml | 7 ++--- 11 files changed, 58 insertions(+), 41 deletions(-) diff --git a/packages/frontend/src/components/ConfirmationDialogProvider.tsx b/packages/frontend/src/components/ConfirmationDialogProvider.tsx index bf25ce47..6e3d61ec 100644 --- a/packages/frontend/src/components/ConfirmationDialogProvider.tsx +++ b/packages/frontend/src/components/ConfirmationDialogProvider.tsx @@ -62,14 +62,14 @@ export function ConfirmDialogProvider({ children }) { type='button' variant="contained" width="100%" - fullWidth={false} + fullWidth="false" onClick={() => fn.current(false)}> Cancel fn.current(true)}> Submit diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx index c2a52fcd..dd8fbbb8 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx @@ -1,7 +1,7 @@ import { useContext } from 'react' import Grid from "@mui/material/Grid"; import Typography from '@mui/material/Typography'; -import { Checkbox, FormControlLabel, FormGroup, Link, Paper } from "@mui/material"; +import { Alert, Checkbox, FormControlLabel, FormGroup, Link, Paper } from "@mui/material"; import Box from '@mui/material/Box'; import { BallotContext } from "../VotePage"; import useElection from "../../../ElectionContextProvider"; @@ -15,7 +15,7 @@ interface GenericBallotViewProps { methodKey: string; columnValues?: number[]; starHeadings?: boolean; - warnings?: {color: string, message: string}[]; + warnings?: {severity: 'warning' | 'error', message: string}[]; onlyGrid?: boolean; warningColumns?: number[]; alertBubbles?: [number, number][]; @@ -136,17 +136,19 @@ export default function GenericBallotView({ {learnLink != '' && {t('ballot.learn_more', {voting_method: methodName})}} - { warnings.map((warning, i) => - + {/* ⚠️ */} - {warning.message} - + {message} + )} diff --git a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx index f3cfa296..0135c9b0 100644 --- a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx @@ -62,13 +62,13 @@ export default function RankedBallotView({ onlyGrid = false }) { return matchingScores }, []); - const getWarnings = useCallback((skippedColumns, matchingScores):{color: string, message:string}[] => { - const warnings = []; + const getWarnings = useCallback((skippedColumns, matchingScores):{severity: 'warning' | 'error', message:string}[] => { + const warnings: {severity: 'warning' | 'error', message:string}[] = []; if (skippedColumns.length) { - warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), color: "brand.goldTransparent20" }); + warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), severity: "warning" }); } if (matchingScores.length) { - warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), color: "brand.red" }); + warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), severity: "error" }); } return warnings; }, [t]); @@ -82,16 +82,18 @@ export default function RankedBallotView({ onlyGrid = false }) { // If the candidate already has the score, remove it. Otherwise, set it with the new score. const scores = ballotContext.candidates.map((candidate) => candidate.score); scores[candidateIndex] = scores[candidateIndex] === columnValue ? null : columnValue; - const skippedColumns = findSkippedColumns(scores); - const matchingScores = findMatchingScores(scores); - const warnings = getWarnings(skippedColumns, matchingScores); - setSkippedColumns(skippedColumns); - setMatchingScores(matchingScores); - setWarnings(warnings); - if (warnings.length) { - ballotContext.setHasAlert(true); - } else { - ballotContext.setHasAlert(false); + if (ballotContext.race.voting_method === 'IRV') { + const skippedColumns = findSkippedColumns(scores); + const matchingScores = findMatchingScores(scores); + const warnings = getWarnings(skippedColumns, matchingScores); + setSkippedColumns(skippedColumns); + setMatchingScores(matchingScores); + setWarnings(warnings); + if (warnings.length) { + ballotContext.setHasAlert(true); + } else { + ballotContext.setHasAlert(false); + } } ballotContext.onUpdate(scores); }, [race.voting_method, ballotContext]); diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 199fd71e..397ac6f3 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -26,7 +26,7 @@ const INFO_ICON = "M 11 7 h 2 v 2 h -2 Z m 0 4 h 2 v 6 h -2 Z m 1 -9 C 6.48 2 2 const CHECKED_BOX = "M 19 3 H 5 c -1.11 0 -2 0.9 -2 2 v 14 c 0 1.1 0.89 2 2 2 h 14 c 1.11 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.89 -2 -2 -2 Z m -9 14 l -5 -5 l 1.41 -1.41 L 10 14.17 l 7.59 -7.59 L 19 8 l -9 9 Z" //const UNCHECKED_BOX = "M 19 5 v 14 H 5 V 5 h 14 m 0 -2 H 5 c -1.1 0 -2 0.9 -2 2 v 14 c 0 1.1 0.9 2 2 2 h 14 c 1.1 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.9 -2 -2 -2 Z" const DOT_ICON = "M12 6c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6 2.69-6 6-6m0-2c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z" -const WARNING_ICON = "M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" +const WARNING_ICON = "M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" type receiptEmail = { sendReceipt: boolean, email: string @@ -258,14 +258,14 @@ const VotePage = () => { type='button' variant="contained" width="100%" - fullWidth={false} + fullWidth="false" onClick={() => setIsOpen(false)}> {t('ballot.dialog_cancel')} submit()}> {t('ballot.dialog_submit')} @@ -276,3 +276,4 @@ const VotePage = () => { } export default VotePage + diff --git a/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx b/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx index 367c5ce6..bb4523d2 100644 --- a/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx +++ b/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx @@ -301,7 +301,7 @@ const CandidateDialog = ({ onEditCandidate, candidate, index, onSave, open, hand onSave()}> Close diff --git a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx index 11fcb81f..25450a66 100644 --- a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx @@ -126,7 +126,7 @@ const templateMappers = { const StepButtons = ({activeStep, setActiveStep, canContinue}) => <> {activeStep < 3 && // hard coding this for now setActiveStep(i => i+1)} @@ -137,7 +137,7 @@ const StepButtons = ({activeStep, setActiveStep, canContinue}) => <> } {activeStep > 0 && setActiveStep(i => i-1)} sx={{ mt: 1, mr: 1 }} @@ -336,7 +336,7 @@ export default () => { type='button' variant="contained" width="100%" - fullWidth={false} + fullWidth="false" onClick={onClose}> Cancel diff --git a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx index 878db329..4965812e 100644 --- a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx +++ b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx @@ -90,7 +90,7 @@ export default function ElectionDetailsInlineForm() { type='button' variant="contained" width="100%" - fullWidth={false} + fullWidth="false" onClick={handleClose} disabled={election.title.length==0}> {t('keyword.cancel')} @@ -100,7 +100,7 @@ export default function ElectionDetailsInlineForm() { handleSave()}> {t('keyword.save')} diff --git a/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx b/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx index 485c4b5e..b3540b83 100644 --- a/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx @@ -29,7 +29,7 @@ export default function AddRace() { diff --git a/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx b/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx index a89991f9..73d8cd53 100644 --- a/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx @@ -35,14 +35,14 @@ export default function RaceDialog({ onSaveRace, open, handleClose, children, ed type='button' variant="contained" width="100%" - fullWidth={false} + fullWidth="false" onClick={handleClose}> Cancel handleSave()} disabled={election.state!=='draft'}> Save diff --git a/packages/frontend/src/components/LandingPage/FeaturedElection.tsx b/packages/frontend/src/components/LandingPage/FeaturedElection.tsx index 20dae6a5..1eba63d2 100644 --- a/packages/frontend/src/components/LandingPage/FeaturedElection.tsx +++ b/packages/frontend/src/components/LandingPage/FeaturedElection.tsx @@ -17,8 +17,21 @@ export default ({electionId}) => { const { data, isPending, error, makeRequest: fetchElections } = useGetElection(electionId); useEffect(() => { - fetchElections() - }, []); + //isMounted is used to prevent memory leaks by ensuring that the component is still mounted before updating the state + let isMounted = true; + + const fetchData = async () => { + + if (isMounted) { + await fetchElections(); } + }; + + fetchData(); + + return () => { + isMounted = false; + }; + }, [fetchElections]); return navigate(`/${electionId}`)} elevation={8} sx={{ width: '100%', diff --git a/packages/frontend/src/i18n/en.yaml b/packages/frontend/src/i18n/en.yaml index d82152e2..c87d9aa2 100644 --- a/packages/frontend/src/i18n/en.yaml +++ b/packages/frontend/src/i18n/en.yaml @@ -229,11 +229,10 @@ ballot: Each round the candidate with the least first-choice votes is eliminated and their voters will be distributed to their next preference. - duplicate_rank_warning: Giving multiple candidates the same ranking is not recommended in IRV. This could result in your ballot getting exhausted early. - skipped_rank_warning: Skipping a rank is not recommended in IRV. This could create some abiguity in your ballot. + duplicate_rank_warning: Giving multiple candidates the same ranking is not recommended in RCV. This could result in your ballot getting exhausted early. + skipped_rank_warning: Skipping a rank is not recommended in RCV. This could create some abiguity in your ballot. heading_prefix: 'Rank {{candidates}}:' - left_title: 'Best' - right_title: 'Worst' + left_title: ballot.methods.rcv.left_title ranked_robin: instruction_bullets: - Rank the {{lowercase_candidates}} in order of preference. From ffe9651f697d101b8960639f4bee801ccf4dbb89 Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Thu, 5 Sep 2024 15:25:16 -0600 Subject: [PATCH 11/19] moved warning columns into ballot context --- .../GenericBallotView/GenericBallotGrid.tsx | 4 +-- .../GenericBallotView/GenericBallotView.tsx | 4 --- .../Election/Voting/RankedBallotView.tsx | 12 ++++----- .../components/Election/Voting/VotePage.tsx | 27 ++++++++++++++++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx index d71f5294..403a15a7 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx @@ -19,7 +19,6 @@ interface GenericBallotGridProps { columnValues: number[]; leftTitle: string; rightTitle: string; - warningColumns: number[]; alertBubbles: [number, number][]; } @@ -32,7 +31,6 @@ export default function GenericBallotGrid({ columnValues, leftTitle, rightTitle, - warningColumns, alertBubbles, }: GenericBallotGridProps) { @@ -119,7 +117,7 @@ export default function GenericBallotGrid({ {/* Row Backgrounds */} {rowBackgrounds} {/* Column Warnings */} - {warningColumns.map((columnValue, columnIndex) => + {ballotContext.warningColumns && ballotContext.warningColumns.map((columnValue, columnIndex) => @@ -124,7 +121,6 @@ export default function GenericBallotView({ headingPrefix={headingPrefix} onClick={onClick} columnValues={columnValues} - warningColumns={warningColumns} alertBubbles={alertBubbles} /> diff --git a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx index 0135c9b0..add00e51 100644 --- a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx @@ -31,14 +31,14 @@ export default function RankedBallotView({ onlyGrid = false }) { return Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS); } }, [ballotContext.maxRankings]); - const findSkippedColumns = useCallback((scores: number[]): number[] => { + const findSkippedColumns = useCallback((scores: number[]): number[] | undefined => { const skippedColumns: number[] = []; for (let i = 1; i <= maxRankings; i++) { if (!scores.includes(i) && scores.some(score => score > i)) { skippedColumns.push(i); } } - return skippedColumns; + return skippedColumns.length ? skippedColumns : undefined; }, [maxRankings]); const findMatchingScores = useCallback((scores: number[]): [number, number][] => { const scoreMap = new Map(); @@ -64,7 +64,7 @@ export default function RankedBallotView({ onlyGrid = false }) { }, []); const getWarnings = useCallback((skippedColumns, matchingScores):{severity: 'warning' | 'error', message:string}[] => { const warnings: {severity: 'warning' | 'error', message:string}[] = []; - if (skippedColumns.length) { + if (skippedColumns) { warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), severity: "warning" }); } if (matchingScores.length) { @@ -73,9 +73,8 @@ export default function RankedBallotView({ onlyGrid = false }) { return warnings; }, [t]); const race = useMemo(() => ballotContext.race, [ballotContext.race]); - const [skippedColumns, setSkippedColumns] = useState(findSkippedColumns(ballotContext.candidates.map((c) => c.score))); const [matchingScores, setMatchingScores] = useState(findMatchingScores(ballotContext.candidates.map((c) => c.score))); - const [warnings, setWarnings] = useState(getWarnings(skippedColumns, matchingScores)); + const [warnings, setWarnings] = useState(getWarnings(ballotContext.warningColumns, matchingScores)); const onClick = useCallback((candidateIndex, columnValue) => { @@ -86,7 +85,7 @@ export default function RankedBallotView({ onlyGrid = false }) { const skippedColumns = findSkippedColumns(scores); const matchingScores = findMatchingScores(scores); const warnings = getWarnings(skippedColumns, matchingScores); - setSkippedColumns(skippedColumns); + ballotContext.setWarningColumns(skippedColumns); setMatchingScores(matchingScores); setWarnings(warnings); if (warnings.length) { @@ -119,7 +118,6 @@ export default function RankedBallotView({ onlyGrid = false }) { onClick={onClick} warnings={warnings} onlyGrid={onlyGrid} - warningColumns={skippedColumns} alertBubbles={matchingScores} /> ); diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 397ac6f3..95ef7447 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -16,7 +16,7 @@ import useAuthSession from "../../AuthSessionContextProvider"; import { StyledButton } from "../../styles"; import useFeatureFlags from "../../FeatureFlagContextProvider"; import { Candidate } from "@equal-vote/star-vote-shared/domain_model/Candidate"; -import { Race } from "@equal-vote/star-vote-shared/domain_model/Race"; +import { Race, VotingMethod } from "@equal-vote/star-vote-shared/domain_model/Race"; import { useSubstitutedTranslation } from "~/components/util"; import DraftWarning from "../DraftWarning"; @@ -40,10 +40,22 @@ export interface IBallotContext { receiptEmail: receiptEmail, setReceiptEmail: React.Dispatch maxRankings?: number, + warningColumns?: number[], + setWarningColumns: (warningColumns: number[]) => void, hasAlert: boolean, setHasAlert: (boolean) => void } +export interface IPage { + instructionsRead: boolean, + candidates: Candidate[], + voting_method: VotingMethod, + race_index: number, + hasAlert: boolean, + warningColumns?: number[], +} + + export const BallotContext = createContext(null); function shuffle(array: T[]): T[] { @@ -66,7 +78,7 @@ const VotePage = () => { const flags = useFeatureFlags(); const { election } = useElection() const authSession = useAuthSession() - const makePages = () => { + const makePages = ():IPage[] => { // generate ballot pages let pages = election.races.map((race, raceIndex) => { let candidates = race.candidates.map(candidate => ({ ...candidate, score: null })) @@ -96,6 +108,12 @@ const VotePage = () => { // shallow copy to trigger a refresh setPages([...pages]) } + + const setWarningColumns = (warningColumns: number[]) => { + pages[currentPage].warningColumns = warningColumns; + //shallow copy to trigger a refresh + setPages([...pages]) + } const [isOpen, setIsOpen] = useState(false) const { data, isPending, error, makeRequest: postBallot } = usePostBallot(election.election_id) @@ -155,7 +173,10 @@ const VotePage = () => { setReceiptEmail: setReceiptEmail, maxRankings: election.settings.max_rankings, hasAlert: pages[currentPage].hasAlert, - setHasAlert: setHasAlert + setHasAlert: setHasAlert, + warningColumns: pages[currentPage].warningColumns, + setWarningColumns: setWarningColumns + }}> From 5e4d5d4b399a03389c7dfdf1e212b1d889e527fb Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Thu, 5 Sep 2024 15:41:48 -0600 Subject: [PATCH 12/19] moved warnings into ballot context --- .../GenericBallotView/GenericBallotView.tsx | 2 +- .../Election/Voting/RankedBallotView.tsx | 11 ++----- .../components/Election/Voting/VotePage.tsx | 31 ++++++++++--------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx index 61253dfe..58c9ae0f 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx @@ -132,7 +132,7 @@ export default function GenericBallotView({ {learnLink != '' && {t('ballot.learn_more', {voting_method: methodName})}} - { warnings.map(({message, severity}, warningIndex) => + { ballotContext.warnings && ballotContext.warnings.map(({message, severity}, warningIndex) => { + const getWarnings = useCallback((skippedColumns, matchingScores):{severity: 'warning' | 'error', message:string}[] | undefined=> { const warnings: {severity: 'warning' | 'error', message:string}[] = []; if (skippedColumns) { warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), severity: "warning" }); @@ -70,7 +70,7 @@ export default function RankedBallotView({ onlyGrid = false }) { if (matchingScores.length) { warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), severity: "error" }); } - return warnings; + return warnings.length ? warnings : undefined; }, [t]); const race = useMemo(() => ballotContext.race, [ballotContext.race]); const [matchingScores, setMatchingScores] = useState(findMatchingScores(ballotContext.candidates.map((c) => c.score))); @@ -87,12 +87,7 @@ export default function RankedBallotView({ onlyGrid = false }) { const warnings = getWarnings(skippedColumns, matchingScores); ballotContext.setWarningColumns(skippedColumns); setMatchingScores(matchingScores); - setWarnings(warnings); - if (warnings.length) { - ballotContext.setHasAlert(true); - } else { - ballotContext.setHasAlert(false); - } + ballotContext.setWarnings(warnings); } ballotContext.onUpdate(scores); }, [race.voting_method, ballotContext]); diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 95ef7447..73547d09 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -1,4 +1,4 @@ -import { createContext, useMemo, useState } from "react" +import { createContext, useCallback, useMemo, useState } from "react" import BallotPageSelector from "./BallotPageSelector"; import { useParams } from "react-router"; import React from 'react' @@ -19,6 +19,7 @@ import { Candidate } from "@equal-vote/star-vote-shared/domain_model/Candidate"; import { Race, VotingMethod } from "@equal-vote/star-vote-shared/domain_model/Race"; import { useSubstitutedTranslation } from "~/components/util"; import DraftWarning from "../DraftWarning"; +import { set } from "date-fns"; // I'm using the icon codes instead of an import because there was padding I couldn't get rid of // https://stackoverflow.com/questions/65721218/remove-material-ui-icon-margin @@ -42,8 +43,8 @@ export interface IBallotContext { maxRankings?: number, warningColumns?: number[], setWarningColumns: (warningColumns: number[]) => void, - hasAlert: boolean, - setHasAlert: (boolean) => void + warnings?: {severity: 'warning' | 'error', message: string}[], + setWarnings: (warnings: {severity: 'warning' | 'error', message: string}[]) => void, } export interface IPage { @@ -51,8 +52,8 @@ export interface IPage { candidates: Candidate[], voting_method: VotingMethod, race_index: number, - hasAlert: boolean, warningColumns?: number[], + warnings?: {severity: 'warning' | 'error', message: string}[] } @@ -103,17 +104,17 @@ const VotePage = () => { // shallow copy to trigger a refresh setPages([...pages]) } - const setHasAlert = (hasAlert) => { - pages[currentPage].hasAlert = hasAlert; - // shallow copy to trigger a refresh - setPages([...pages]) - } const setWarningColumns = (warningColumns: number[]) => { pages[currentPage].warningColumns = warningColumns; //shallow copy to trigger a refresh setPages([...pages]) } + const setWarnings = useCallback((warnings: {severity: 'warning' | 'error', message: string}[]) => { + pages[currentPage].warnings = warnings; + //shallow copy to trigger a refresh + setPages([...pages]) + }, [pages, currentPage]) const [isOpen, setIsOpen] = useState(false) const { data, isPending, error, makeRequest: postBallot } = usePostBallot(election.election_id) @@ -172,10 +173,10 @@ const VotePage = () => { receiptEmail: receiptEmail, setReceiptEmail: setReceiptEmail, maxRankings: election.settings.max_rankings, - hasAlert: pages[currentPage].hasAlert, - setHasAlert: setHasAlert, warningColumns: pages[currentPage].warningColumns, - setWarningColumns: setWarningColumns + setWarningColumns: setWarningColumns, + warnings: pages[currentPage].warnings, + setWarnings: setWarnings }}> @@ -199,8 +200,8 @@ const VotePage = () => { > {/*TODO: I can probably do this in css using the :selected property*/} - - {page.hasAlert ? : page.candidates.some((candidate) => (candidate.score > 0)) ? : } + + {page.warnings ? : page.candidates.some((candidate) => (candidate.score > 0)) ? : } @@ -221,7 +222,7 @@ const VotePage = () => { From ffa7d8c5e1d14fc85690cf9a6f40f7d84ba15793 Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Thu, 5 Sep 2024 16:07:34 -0600 Subject: [PATCH 13/19] moved alert bubbles into ballot context --- .../Voting/GenericBallotView/BubbleGrid.tsx | 11 +++++------ .../GenericBallotView/GenericBallotGrid.tsx | 7 ++----- .../GenericBallotView/GenericBallotView.tsx | 6 ------ .../Election/Voting/RankedBallotView.tsx | 12 ++++-------- .../src/components/Election/Voting/VotePage.tsx | 15 +++++++++++++-- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/BubbleGrid.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/BubbleGrid.tsx index d4737302..b9031a79 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/BubbleGrid.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/BubbleGrid.tsx @@ -1,22 +1,21 @@ import React, { useCallback, useMemo } from 'react'; import { Box, Typography } from '@mui/material'; import { Candidate } from '@equal-vote/star-vote-shared/domain_model/Candidate'; +import { IBallotContext } from '../VotePage'; interface BubbleGridProps { - candidates: Candidate[]; + ballotContext: IBallotContext; columnValues: number[]; columns: string[]; numHeaderRows: number; - instructionsRead: boolean; onClick: (candidateIndex: number, columnValue: number) => void; makeArea: (row: number, column: number, width?: number) => string; fontSX: object; - alertBubbles: [number, number][]; } -const BubbleGrid: React.FC = ({ candidates, columnValues, columns, numHeaderRows, instructionsRead, onClick, makeArea, fontSX, alertBubbles }) => { - +const BubbleGrid: React.FC = ({ ballotContext, columnValues, columns, numHeaderRows, onClick, makeArea, fontSX }) => { + const { candidates, instructionsRead, alertBubbles } = ballotContext; // Step 1: Create a triplet of candidateIndex, columnIndex, and columnValue for each candidate const candidateColumnPairsNested = useMemo(() => { @@ -34,7 +33,7 @@ const BubbleGrid: React.FC = ({ candidates, columnValues, colum if (instructionsRead) { className = className + ' unblurred'; } - if (alertBubbles.length && alertBubbles.some(([alertCandidateIndex, alertColumnValue]) => alertCandidateIndex === candidateIndex && alertColumnValue === columnValue)) { + if (alertBubbles && alertBubbles.some(([alertCandidateIndex, alertColumnValue]) => alertCandidateIndex === candidateIndex && alertColumnValue === columnValue)) { className = className + ' alert'; } else if (columnValue === candidates[candidateIndex].score) { className = className + ' filled'; diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx index 403a15a7..b7aa2472 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx @@ -19,7 +19,6 @@ interface GenericBallotGridProps { columnValues: number[]; leftTitle: string; rightTitle: string; - alertBubbles: [number, number][]; } export default function GenericBallotGrid({ @@ -31,7 +30,7 @@ export default function GenericBallotGrid({ columnValues, leftTitle, rightTitle, - alertBubbles, + }: GenericBallotGridProps) { const numHeaderRows = Number(leftTitle != '') + Number(columns.length > 1); @@ -166,15 +165,13 @@ export default function GenericBallotGrid({ candidate={candidate} gridArea={makeArea(numHeaderRows + 1 + 2 * candidateIndex + 1, 1)} /> )} diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx index 58c9ae0f..05c07307 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx @@ -15,9 +15,7 @@ interface GenericBallotViewProps { methodKey: string; columnValues?: number[]; starHeadings?: boolean; - warnings?: {severity: 'warning' | 'error', message: string}[]; onlyGrid?: boolean; - alertBubbles?: [number, number][]; } export default function GenericBallotView({ @@ -26,9 +24,7 @@ export default function GenericBallotView({ methodKey, columnValues=null, starHeadings=false, - warnings=[], onlyGrid=false, - alertBubbles=[], }: GenericBallotViewProps) { if(columnValues == null){ columnValues = columns.map(Number); @@ -65,7 +61,6 @@ export default function GenericBallotView({ headingPrefix={headingPrefix} onClick={onClick} columnValues={columnValues} - alertBubbles={alertBubbles} /> @@ -121,7 +116,6 @@ export default function GenericBallotView({ headingPrefix={headingPrefix} onClick={onClick} columnValues={columnValues} - alertBubbles={alertBubbles} /> diff --git a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx index 57b2b2c6..9a6f78bc 100644 --- a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx @@ -40,7 +40,7 @@ export default function RankedBallotView({ onlyGrid = false }) { } return skippedColumns.length ? skippedColumns : undefined; }, [maxRankings]); - const findMatchingScores = useCallback((scores: number[]): [number, number][] => { + const findMatchingScores = useCallback((scores: number[]): [number, number][] | undefined => { const scoreMap = new Map(); // Populate the map with indexes for each score @@ -60,21 +60,19 @@ export default function RankedBallotView({ onlyGrid = false }) { } }); - return matchingScores + return matchingScores.length ? matchingScores : undefined; }, []); const getWarnings = useCallback((skippedColumns, matchingScores):{severity: 'warning' | 'error', message:string}[] | undefined=> { const warnings: {severity: 'warning' | 'error', message:string}[] = []; if (skippedColumns) { warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), severity: "warning" }); } - if (matchingScores.length) { + if (matchingScores) { warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), severity: "error" }); } return warnings.length ? warnings : undefined; }, [t]); const race = useMemo(() => ballotContext.race, [ballotContext.race]); - const [matchingScores, setMatchingScores] = useState(findMatchingScores(ballotContext.candidates.map((c) => c.score))); - const [warnings, setWarnings] = useState(getWarnings(ballotContext.warningColumns, matchingScores)); const onClick = useCallback((candidateIndex, columnValue) => { @@ -86,7 +84,7 @@ export default function RankedBallotView({ onlyGrid = false }) { const matchingScores = findMatchingScores(scores); const warnings = getWarnings(skippedColumns, matchingScores); ballotContext.setWarningColumns(skippedColumns); - setMatchingScores(matchingScores); + ballotContext.setAlertBubbles(matchingScores); ballotContext.setWarnings(warnings); } ballotContext.onUpdate(scores); @@ -111,9 +109,7 @@ export default function RankedBallotView({ onlyGrid = false }) { columnValues={columnValues} columns={columns} onClick={onClick} - warnings={warnings} onlyGrid={onlyGrid} - alertBubbles={matchingScores} /> ); } \ No newline at end of file diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 73547d09..e3ee288f 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -43,6 +43,8 @@ export interface IBallotContext { maxRankings?: number, warningColumns?: number[], setWarningColumns: (warningColumns: number[]) => void, + alertBubbles?: [number, number][], + setAlertBubbles: (alertBubbles: [number, number][]) => void, warnings?: {severity: 'warning' | 'error', message: string}[], setWarnings: (warnings: {severity: 'warning' | 'error', message: string}[]) => void, } @@ -53,7 +55,8 @@ export interface IPage { voting_method: VotingMethod, race_index: number, warningColumns?: number[], - warnings?: {severity: 'warning' | 'error', message: string}[] + warnings?: {severity: 'warning' | 'error', message: string}[], + alertBubbles?: [number, number][], } @@ -117,6 +120,12 @@ const VotePage = () => { }, [pages, currentPage]) const [isOpen, setIsOpen] = useState(false) + const setAlertBubbles = useCallback((alertBubbles: [number, number][]) => { + pages[currentPage].alertBubbles = alertBubbles; + //shallow copy to trigger a refresh + setPages([...pages]) + }, [pages, currentPage]) + const { data, isPending, error, makeRequest: postBallot } = usePostBallot(election.election_id) const onUpdate = (pageIndex, newRaceScores) => { var newPages = [...pages] @@ -176,7 +185,9 @@ const VotePage = () => { warningColumns: pages[currentPage].warningColumns, setWarningColumns: setWarningColumns, warnings: pages[currentPage].warnings, - setWarnings: setWarnings + setWarnings: setWarnings, + alertBubbles: pages[currentPage].alertBubbles, + setAlertBubbles: setAlertBubbles, }}> From 38d36cb103f1e152ddaafe109d46c0f43c5f561e Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Thu, 5 Sep 2024 16:38:43 -0600 Subject: [PATCH 14/19] cleaning up errors --- .../src/components/ConfirmationDialogProvider.tsx | 4 ++-- .../Voting/GenericBallotView/GenericBallotView.tsx | 6 +++--- .../src/components/Election/Voting/VotePage.tsx | 4 ++-- .../ElectionForm/Candidates/AddCandidate.tsx | 2 +- .../components/ElectionForm/CreateElectionDialog.tsx | 6 +++--- .../Details/ElectionDetailsInlineForm.tsx | 4 ++-- .../src/components/ElectionForm/Races/AddRace.tsx | 2 +- .../src/components/ElectionForm/Races/RaceDialog.tsx | 4 ++-- packages/frontend/src/i18n/en.yaml | 12 +++++++++++- 9 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/frontend/src/components/ConfirmationDialogProvider.tsx b/packages/frontend/src/components/ConfirmationDialogProvider.tsx index 6e3d61ec..bf25ce47 100644 --- a/packages/frontend/src/components/ConfirmationDialogProvider.tsx +++ b/packages/frontend/src/components/ConfirmationDialogProvider.tsx @@ -62,14 +62,14 @@ export function ConfirmDialogProvider({ children }) { type='button' variant="contained" width="100%" - fullWidth="false" + fullWidth={false} onClick={() => fn.current(false)}> Cancel fn.current(true)}> Submit diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx index 05c07307..60e4d29b 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotView.tsx @@ -40,8 +40,8 @@ export default function GenericBallotView({ const methodName = t(`methods.${methodKey}.full_name`); const leftKey = `ballot.methods.${methodKey}.left_title`; - const leftTitle = (t(leftKey) == leftKey)? '' : t(leftKey); - const rightTitle = (t(leftKey) == leftKey)? '' : t(`ballot.methods.${methodKey}.right_title`); + const leftTitle = t(leftKey); + const rightTitle = t(`ballot.methods.${methodKey}.right_title`); const headingPrefixKey = `ballot.methods.${methodKey}.heading_prefix`; const headingPrefix = (t(headingPrefixKey) == headingPrefixKey)? '' : t(headingPrefixKey); @@ -128,9 +128,9 @@ export default function GenericBallotView({ { ballotContext.warnings && ballotContext.warnings.map(({message, severity}, warningIndex) => { type='button' variant="contained" width="100%" - fullWidth="false" + fullWidth={false} onClick={() => setIsOpen(false)}> {t('ballot.dialog_cancel')} submit()}> {t('ballot.dialog_submit')} diff --git a/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx b/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx index bb4523d2..367c5ce6 100644 --- a/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx +++ b/packages/frontend/src/components/ElectionForm/Candidates/AddCandidate.tsx @@ -301,7 +301,7 @@ const CandidateDialog = ({ onEditCandidate, candidate, index, onSave, open, hand onSave()}> Close diff --git a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx index 25450a66..11fcb81f 100644 --- a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx @@ -126,7 +126,7 @@ const templateMappers = { const StepButtons = ({activeStep, setActiveStep, canContinue}) => <> {activeStep < 3 && // hard coding this for now setActiveStep(i => i+1)} @@ -137,7 +137,7 @@ const StepButtons = ({activeStep, setActiveStep, canContinue}) => <> } {activeStep > 0 && setActiveStep(i => i-1)} sx={{ mt: 1, mr: 1 }} @@ -336,7 +336,7 @@ export default () => { type='button' variant="contained" width="100%" - fullWidth="false" + fullWidth={false} onClick={onClose}> Cancel diff --git a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx index 4965812e..878db329 100644 --- a/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx +++ b/packages/frontend/src/components/ElectionForm/Details/ElectionDetailsInlineForm.tsx @@ -90,7 +90,7 @@ export default function ElectionDetailsInlineForm() { type='button' variant="contained" width="100%" - fullWidth="false" + fullWidth={false} onClick={handleClose} disabled={election.title.length==0}> {t('keyword.cancel')} @@ -100,7 +100,7 @@ export default function ElectionDetailsInlineForm() { handleSave()}> {t('keyword.save')} diff --git a/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx b/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx index b3540b83..485c4b5e 100644 --- a/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/AddRace.tsx @@ -29,7 +29,7 @@ export default function AddRace() { diff --git a/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx b/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx index 73d8cd53..a89991f9 100644 --- a/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/Races/RaceDialog.tsx @@ -35,14 +35,14 @@ export default function RaceDialog({ onSaveRace, open, handleClose, children, ed type='button' variant="contained" width="100%" - fullWidth="false" + fullWidth={false} onClick={handleClose}> Cancel handleSave()} disabled={election.state!=='draft'}> Save diff --git a/packages/frontend/src/i18n/en.yaml b/packages/frontend/src/i18n/en.yaml index c87d9aa2..ce4b975f 100644 --- a/packages/frontend/src/i18n/en.yaml +++ b/packages/frontend/src/i18n/en.yaml @@ -204,6 +204,9 @@ ballot: - You can select as many {{lowercase_candidates}} as you like footer_single_winner: The {{lowercase_candidate}} with the most votes wins footer_multi_winner: The {{n}} {{lowercase_candidates}} with the most votes wins + left_title: '' + right_title: '' + heading_prefix: '' star: instruction_bullets: - Give your favorite(s) five stars. @@ -219,6 +222,7 @@ ballot: # These show above the star columns left_title: 'Worst' right_title: 'Best' + heading_prefix: '' rcv: instruction_bullets: - Rank the {{lowercase_candidates}} in order of preference. @@ -232,7 +236,8 @@ ballot: duplicate_rank_warning: Giving multiple candidates the same ranking is not recommended in RCV. This could result in your ballot getting exhausted early. skipped_rank_warning: Skipping a rank is not recommended in RCV. This could create some abiguity in your ballot. heading_prefix: 'Rank {{candidates}}:' - left_title: ballot.methods.rcv.left_title + left_title: '' + right_title: '' ranked_robin: instruction_bullets: - Rank the {{lowercase_candidates}} in order of preference. @@ -242,11 +247,16 @@ ballot: {{candidates}} are compared in 1-on-1 match-ups. A {{lowercase_candidate}} wins a match-up if they are ranked higher than the opponent by more voters heading_prefix: 'Rank {{candidates}}:' + left_title: '' + right_title: '' choose_one: instruction_bullets: - Fill in the bubble next to your favorite footer_single_winner: The {{lowercase_candidate}} with the most votes wins footer_multi_winner: The {{n}} {{lowercase_candidates}} with the most votes win + left_title: '' + right_title: '' + heading_prefix: '' # Confirmation (after you've submitted a ballot) ballot_submitted: From 122fe2e5b2409d133d997fc8f793fb38f3ec299e Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Fri, 6 Sep 2024 23:21:06 -0600 Subject: [PATCH 15/19] fixed landing page error from redefinition of Ballot Context --- .../src/components/Election/Voting/RankedBallotView.tsx | 7 +++++-- .../frontend/src/components/Election/Voting/VotePage.tsx | 6 +++--- .../src/components/LandingPage/LandingPageHero.tsx | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx index 9a6f78bc..c8930d3c 100644 --- a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx @@ -25,11 +25,14 @@ export default function RankedBallotView({ onlyGrid = false }) { // } const maxRankings = useMemo(() => { - if (ballotContext.maxRankings) { + if (ballotContext.maxRankings && Number(process.env.REACT_APP_MAX_BALLOT_RANKS)){ return Math.min(ballotContext.maxRankings, Number(process.env.REACT_APP_MAX_BALLOT_RANKS)); + } else if (Number(process.env.REACT_APP_MAX_BALLOT_RANKS)) { + return Number(process.env.REACT_APP_MAX_BALLOT_RANKS); } else { - return Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS); + return undefined; } + }, [ballotContext.maxRankings]); const findSkippedColumns = useCallback((scores: number[]): number[] | undefined => { const skippedColumns: number[] = []; diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index c5927d8f..9a154cab 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -42,11 +42,11 @@ export interface IBallotContext { setReceiptEmail: React.Dispatch maxRankings?: number, warningColumns?: number[], - setWarningColumns: (warningColumns: number[]) => void, + setWarningColumns?: (warningColumns: number[]) => void, alertBubbles?: [number, number][], - setAlertBubbles: (alertBubbles: [number, number][]) => void, + setAlertBubbles?: (alertBubbles: [number, number][]) => void, warnings?: {severity: 'warning' | 'error', message: string}[], - setWarnings: (warnings: {severity: 'warning' | 'error', message: string}[]) => void, + setWarnings?: (warnings: {severity: 'warning' | 'error', message: string}[]) => void, } export interface IPage { diff --git a/packages/frontend/src/components/LandingPage/LandingPageHero.tsx b/packages/frontend/src/components/LandingPage/LandingPageHero.tsx index 38324099..53400cd3 100644 --- a/packages/frontend/src/components/LandingPage/LandingPageHero.tsx +++ b/packages/frontend/src/components/LandingPage/LandingPageHero.tsx @@ -10,7 +10,7 @@ import { useLocalStorage } from '../../hooks/useLocalStorage' import ArrowBackIosRoundedIcon from '@mui/icons-material/ArrowBackIosRounded'; import ArrowForwardIosRoundedIcon from '@mui/icons-material/ArrowForwardIosRounded'; import { StyledButton, Tip } from '../styles' -import { BallotContext } from '../Election/Voting/VotePage' +import { BallotContext, IBallotContext } from '../Election/Voting/VotePage' import StarBallotView from '../Election/Voting/StarBallotView' import { ElectionContextProvider } from '../ElectionContextProvider' import { VotingMethod } from '@equal-vote/star-vote-shared/domain_model/Race' @@ -71,7 +71,7 @@ export default ({}) => { let quickPollIndex = transitionStep < 4 ? prevMethodIndex : methodIndex; if(quickPollIndex == methodKeys.length-1) quickPollIndex = methodKeys.length-2; - const makeBallotContext = (scores, onUpdate) => { + const makeBallotContext = (scores, onUpdate):IBallotContext => { const candidateNames = t('landing_page.hero.candidates') return { instructionsRead: true, @@ -98,7 +98,7 @@ export default ({}) => { }, setReceiptEmail: () => {}, onUpdate: onUpdate, - maxRankings: undefined + maxRankings: undefined, } } From 44ada87d8395599242df46706d959c4b3140f482 Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Sat, 14 Sep 2024 14:38:12 -0600 Subject: [PATCH 16/19] apply feedback from Sarah and Arend --- .../GenericBallotView/GenericBallotGrid.tsx | 9 +------ .../GenericBallotView/GenericBallotView.tsx | 4 ---- .../Election/Voting/RankedBallotView.tsx | 24 ++++++++++--------- .../ElectionForm/ElectionSettings.tsx | 4 ++-- packages/frontend/src/i18n/en.yaml | 21 +++++++--------- packages/frontend/src/index.css | 6 ++--- packages/frontend/src/theme.tsx | 3 +++ 7 files changed, 31 insertions(+), 40 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx index b7aa2472..d91398c8 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx @@ -14,7 +14,6 @@ interface GenericBallotGridProps { ballotContext: IBallotContext; starHeadings: boolean; columns: string[]; - headingPrefix: string; onClick: (candidateIndex: number, columnValue: number) => void; columnValues: number[]; leftTitle: string; @@ -25,7 +24,6 @@ export default function GenericBallotGrid({ ballotContext, starHeadings, columns, - headingPrefix, onClick, columnValues, leftTitle, @@ -121,7 +119,7 @@ export default function GenericBallotGrid({ sx={{ gridArea: makeArea(1, 1 + columnValue, 1, numHeaderRows + 1 + ballotContext.candidates.length * 2), height: '100%', - backgroundColor: "brand.goldTransparent20" + backgroundColor: "brand.warningColumn" }} /> )} @@ -139,11 +137,6 @@ export default function GenericBallotGrid({ {/* HEADINGS (i.e. stars, ranks, etc) */} {columns.length > 1 && <> - - - {headingPrefix} - - {columns.map((columnTitle, columnIndex) => @@ -113,7 +110,6 @@ export default function GenericBallotView({ columns={columns} leftTitle={leftTitle} rightTitle={rightTitle} - headingPrefix={headingPrefix} onClick={onClick} columnValues={columnValues} /> diff --git a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx index c8930d3c..d30d1330 100644 --- a/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx +++ b/packages/frontend/src/components/Election/Voting/RankedBallotView.tsx @@ -25,14 +25,13 @@ export default function RankedBallotView({ onlyGrid = false }) { // } const maxRankings = useMemo(() => { - if (ballotContext.maxRankings && Number(process.env.REACT_APP_MAX_BALLOT_RANKS)){ - return Math.min(ballotContext.maxRankings, Number(process.env.REACT_APP_MAX_BALLOT_RANKS)); - } else if (Number(process.env.REACT_APP_MAX_BALLOT_RANKS)) { - return Number(process.env.REACT_APP_MAX_BALLOT_RANKS); + const MAX_BALLOT_RANKS = Number(process.env.REACT_APP_MAX_BALLOT_RANKS) ? Number(process.env.REACT_APP_MAX_BALLOT_RANKS) : 8; + const DEFAULT_BALLOT_RANKS = Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS) ? Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS) : 6; + if (ballotContext.maxRankings) { + return Math.min(ballotContext.maxRankings, MAX_BALLOT_RANKS); } else { - return undefined; + return DEFAULT_BALLOT_RANKS; } - }, [ballotContext.maxRankings]); const findSkippedColumns = useCallback((scores: number[]): number[] | undefined => { const skippedColumns: number[] = []; @@ -68,10 +67,10 @@ export default function RankedBallotView({ onlyGrid = false }) { const getWarnings = useCallback((skippedColumns, matchingScores):{severity: 'warning' | 'error', message:string}[] | undefined=> { const warnings: {severity: 'warning' | 'error', message:string}[] = []; if (skippedColumns) { - warnings.push({ message: t('ballot.methods.rcv.skipped_rank_warning'), severity: "warning" }); + warnings.push({ message: t('ballot.warnings.skipped_rank'), severity: "warning" }); } if (matchingScores) { - warnings.push({ message: t('ballot.methods.rcv.duplicate_rank_warning'), severity: "error" }); + warnings.push({ message: t('ballot.warnings.duplicate_rank'), severity: "error" }); } return warnings.length ? warnings : undefined; }, [t]); @@ -82,14 +81,17 @@ export default function RankedBallotView({ onlyGrid = false }) { // If the candidate already has the score, remove it. Otherwise, set it with the new score. const scores = ballotContext.candidates.map((candidate) => candidate.score); scores[candidateIndex] = scores[candidateIndex] === columnValue ? null : columnValue; - if (ballotContext.race.voting_method === 'IRV') { const skippedColumns = findSkippedColumns(scores); - const matchingScores = findMatchingScores(scores); + let matchingScores: [number, number][] | undefined = undefined; + if (ballotContext.race.voting_method === 'IRV') { + matchingScores = findMatchingScores(scores); + } const warnings = getWarnings(skippedColumns, matchingScores); ballotContext.setWarningColumns(skippedColumns); ballotContext.setAlertBubbles(matchingScores); ballotContext.setWarnings(warnings); - } + // } + ballotContext.onUpdate(scores); }, [race.voting_method, ballotContext]); diff --git a/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx b/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx index 15e3ef34..1e6ab3d2 100644 --- a/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx +++ b/packages/frontend/src/components/ElectionForm/ElectionSettings.tsx @@ -14,8 +14,8 @@ export default function ElectionSettings() { const { election, refreshElection, permissions, updateElection } = useElection() const min_rankings = 3; - const max_rankings = Number(process.env.REACT_APP_MAX_BALLOT_RANKS); - const default_rankings = Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS); + const max_rankings = Number(process.env.REACT_APP_MAX_BALLOT_RANKS) ? Number(process.env.REACT_APP_MAX_BALLOT_RANKS) : 8; + const default_rankings = Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS) ? Number(process.env.REACT_APP_DEFAULT_BALLOT_RANKS) : 6; const {t} = useSubstitutedTranslation(election.settings.term_type, {min_rankings, max_rankings}); diff --git a/packages/frontend/src/i18n/en.yaml b/packages/frontend/src/i18n/en.yaml index ce4b975f..81a7cca7 100644 --- a/packages/frontend/src/i18n/en.yaml +++ b/packages/frontend/src/i18n/en.yaml @@ -196,6 +196,9 @@ ballot: dialog_email_placeholder: Receipt Email dialog_cancel: Cancel dialog_submit: Submit + warnings: + skipped_rank: Do not skip rankings. Rank candidates in order to clearly show preferences. Candidates left blank are ranked last. + duplicate_rank: Do not rank multiple candidates equally. (Ranking candidates equally can void your ballot.) methods: approval: @@ -222,20 +225,16 @@ ballot: # These show above the star columns left_title: 'Worst' right_title: 'Best' - heading_prefix: '' rcv: instruction_bullets: - - Rank the {{lowercase_candidates}} in order of preference. - - Equal ranks are not recommended, since they risk your vote being exhausted early + - Rank the {{lowercase_candidates}} in order of preference. (1st, 2nd, 3rd, etc.) + - Ranking {{lowercase_candidates}} equally is not allowed. - '{{candidates}} left blank are ranked last' footer_single_winner: > - The winner is selected after a series of elimination rounds. - - Each round the candidate with the least first-choice votes is eliminated and - their voters will be distributed to their next preference. - duplicate_rank_warning: Giving multiple candidates the same ranking is not recommended in RCV. This could result in your ballot getting exhausted early. - skipped_rank_warning: Skipping a rank is not recommended in RCV. This could create some abiguity in your ballot. - heading_prefix: 'Rank {{candidates}}:' + How RCV is counted: Ballots are counted in elimination rounds. In each round, your vote goes to the candidate you ranked highest, + if possible. If no candidate has a majority of remaining votes the candidate with the fewest votes is eliminated. + If your vote is unable to transfer, it will not be counted in later rounds. If a candidate has a majority of remaining votes + in a round, they are elected. left_title: '' right_title: '' ranked_robin: @@ -246,7 +245,6 @@ ballot: footer_single_winner: | {{candidates}} are compared in 1-on-1 match-ups. A {{lowercase_candidate}} wins a match-up if they are ranked higher than the opponent by more voters - heading_prefix: 'Rank {{candidates}}:' left_title: '' right_title: '' choose_one: @@ -256,7 +254,6 @@ ballot: footer_multi_winner: The {{n}} {{lowercase_candidates}} with the most votes win left_title: '' right_title: '' - heading_prefix: '' # Confirmation (after you've submitted a ballot) ballot_submitted: diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css index 8e80792a..48daf63b 100644 --- a/packages/frontend/src/index.css +++ b/packages/frontend/src/index.css @@ -298,7 +298,7 @@ code { .circle.alert, .oval.alert { - background-color: var(--brand-red); + background-color: var(--ltbrand-red); transition: background-color 400ms; } @@ -318,8 +318,8 @@ code { transition: color 200ms; } -.circle.alert:hover { - background-color: var(--brand-red); +.circle.alert:hover, .oval.alert:hover { + background-color: var(--ltbrand-red); transition: background-color 400ms; } diff --git a/packages/frontend/src/theme.tsx b/packages/frontend/src/theme.tsx index 9f451cd7..7ba5057f 100644 --- a/packages/frontend/src/theme.tsx +++ b/packages/frontend/src/theme.tsx @@ -77,6 +77,7 @@ declare module '@mui/material/styles' { gray3Transparent20?: string; gray2Transparent20?: string; gray1Transparent20?: string; + warningColumn?: string; } interface SimplePaletteColorOptions { @@ -110,6 +111,7 @@ declare module '@mui/material/styles' { gray3Transparent20?: string; gray2Transparent20?: string; gray1Transparent20?: string; + warningColumn?: string; } } @@ -147,6 +149,7 @@ const brandPalette: PaletteOptions = { gray3Transparent20: '#99999933', gray2Transparent20: '#CCCCCC33', gray1Transparent20: '#ECECEC33', + warningColumn: '#FFDD0080', } } From 1b6508ed6be5b8b71aaba682d1fb01dc066c9ece Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Sat, 14 Sep 2024 14:54:27 -0600 Subject: [PATCH 17/19] removed score from CandidateInterface and put it in an extension on VotePage instead --- .../Voting/GenericBallotView/GenericBallotGrid.tsx | 2 ++ .../frontend/src/components/Election/Voting/VotePage.tsx | 9 +++++++-- packages/shared/src/domain_model/Candidate.ts | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx index d91398c8..4407dc05 100644 --- a/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx +++ b/packages/frontend/src/components/Election/Voting/GenericBallotView/GenericBallotGrid.tsx @@ -74,6 +74,7 @@ export default function GenericBallotGrid({ rowBackgrounds.push( ); } + return rowBackgrounds; }, [numHeaderRows, columns.length, ballotContext.candidates.length]); diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 9a154cab..06f39f0e 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -32,10 +32,15 @@ type receiptEmail = { sendReceipt: boolean, email: string } + +export interface BallotCandidate extends Candidate { + score: number +} + export interface IBallotContext { instructionsRead: boolean, setInstructionsRead: () => void, - candidates: Candidate[], + candidates: BallotCandidate[], race: Race, onUpdate: (any) => void, receiptEmail: receiptEmail, @@ -51,7 +56,7 @@ export interface IBallotContext { export interface IPage { instructionsRead: boolean, - candidates: Candidate[], + candidates: BallotCandidate[], voting_method: VotingMethod, race_index: number, warningColumns?: number[], diff --git a/packages/shared/src/domain_model/Candidate.ts b/packages/shared/src/domain_model/Candidate.ts index f93f1fc3..74fa1eeb 100644 --- a/packages/shared/src/domain_model/Candidate.ts +++ b/packages/shared/src/domain_model/Candidate.ts @@ -10,5 +10,4 @@ export interface Candidate { candidate_url?: string; // link to info about candidate partyUrl?: string; // link to info about party photo_filename?:string; // link to info about party - score?: number; // score for the candidate } From e5fc2fd50da7dcbd816035852763f8e1990810b1 Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Sat, 14 Sep 2024 15:11:30 -0600 Subject: [PATCH 18/19] fixed an error with tip component on CreateElectionDialog where termtype wasn't defined --- .../src/components/ElectionForm/CreateElectionDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx index 11fcb81f..cfc08be2 100644 --- a/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx +++ b/packages/frontend/src/components/ElectionForm/CreateElectionDialog.tsx @@ -243,7 +243,7 @@ export default () => { } {t('election_creation.term_question')} - + {['poll', 'election'].map( (type, i) => @@ -290,7 +290,7 @@ export default () => { {t('election_creation.restricted_question')} - + From e7920443589350f00ebd18c86b2cc1cb94cd81a0 Mon Sep 17 00:00:00 2001 From: Samuel Coleman Date: Sat, 14 Sep 2024 15:40:55 -0600 Subject: [PATCH 19/19] switched warning icon to yellow triangle --- .../src/components/Election/Voting/VotePage.tsx | 11 +++++++++-- packages/frontend/src/theme.tsx | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/Election/Voting/VotePage.tsx b/packages/frontend/src/components/Election/Voting/VotePage.tsx index 06f39f0e..9c26297a 100644 --- a/packages/frontend/src/components/Election/Voting/VotePage.tsx +++ b/packages/frontend/src/components/Election/Voting/VotePage.tsx @@ -27,7 +27,7 @@ const INFO_ICON = "M 11 7 h 2 v 2 h -2 Z m 0 4 h 2 v 6 h -2 Z m 1 -9 C 6.48 2 2 const CHECKED_BOX = "M 19 3 H 5 c -1.11 0 -2 0.9 -2 2 v 14 c 0 1.1 0.89 2 2 2 h 14 c 1.11 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.89 -2 -2 -2 Z m -9 14 l -5 -5 l 1.41 -1.41 L 10 14.17 l 7.59 -7.59 L 19 8 l -9 9 Z" //const UNCHECKED_BOX = "M 19 5 v 14 H 5 V 5 h 14 m 0 -2 H 5 c -1.1 0 -2 0.9 -2 2 v 14 c 0 1.1 0.9 2 2 2 h 14 c 1.1 0 2 -0.9 2 -2 V 5 c 0 -1.1 -0.9 -2 -2 -2 Z" const DOT_ICON = "M12 6c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6 2.69-6 6-6m0-2c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8z" -const WARNING_ICON = "M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z" +const WARNING_ICON = "M12,5.99L19.53,19H4.47L12,5.99 M12,2L1,21h22L12,2L12,2z" type receiptEmail = { sendReceipt: boolean, email: string @@ -216,8 +216,15 @@ const VotePage = () => { > {/*TODO: I can probably do this in css using the :selected property*/} - + {page.warnings ? : page.candidates.some((candidate) => (candidate.score > 0)) ? : } + {page.warnings && <> + + + } + diff --git a/packages/frontend/src/theme.tsx b/packages/frontend/src/theme.tsx index 7ba5057f..cca239f9 100644 --- a/packages/frontend/src/theme.tsx +++ b/packages/frontend/src/theme.tsx @@ -51,6 +51,7 @@ declare module '@mui/material/styles' { ltblue?: string; blue?: string; gold?: string; + warning?: string; red?: string; orange?: string; green?: string; @@ -85,6 +86,7 @@ declare module '@mui/material/styles' { ltblue?: string; blue?: string; gold?: string; + warning?: string; red?: string; orange?: string; green?: string; @@ -123,6 +125,7 @@ const brandPalette: PaletteOptions = { ltblue: '#2AA2B3', blue: '#02627C', gold: '#FFE156', + warning: '#ed6c02', red: '#EE2C53', orange: '#FF9900', green: '#60B33C',