From 696758e8e732755ee500a8ea77a578714a6bf981 Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Wed, 9 Nov 2022 14:29:24 +0100 Subject: [PATCH 1/7] Begin splitting up the results into two tabs --- web/frontend/src/pages/form/GroupedResult.tsx | 235 +++++++++++++++++ web/frontend/src/pages/form/Result.tsx | 246 +++--------------- 2 files changed, 269 insertions(+), 212 deletions(-) create mode 100644 web/frontend/src/pages/form/GroupedResult.tsx diff --git a/web/frontend/src/pages/form/GroupedResult.tsx b/web/frontend/src/pages/form/GroupedResult.tsx new file mode 100644 index 000000000..73e0f1075 --- /dev/null +++ b/web/frontend/src/pages/form/GroupedResult.tsx @@ -0,0 +1,235 @@ +import React, { FC, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { DownloadedResults, RankResults, SelectResults, TextResults } from 'types/form'; +import SelectResult from './components/SelectResult'; +import RankResult from './components/RankResult'; +import TextResult from './components/TextResult'; +import { + ID, + RANK, + RankQuestion, + SELECT, + SUBJECT, + SelectQuestion, + Subject, + SubjectElement, + TEXT, +} from 'types/configuration'; +import DownloadButton from 'components/buttons/DownloadButton'; +import { useTranslation } from 'react-i18next'; +import saveAs from 'file-saver'; +import { useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router'; +import useForm from 'components/utils/useForm'; +import { useConfigurationOnly } from 'components/utils/useConfiguration'; +import { + countRankResult, + countSelectResult, + countTextResult, +} from './components/utils/countResult'; + +// Functional component that displays the result of the votes +const GroupedResult: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { formId } = useParams(); + + const { loading, result, configObj } = useForm(formId); + const configuration = useConfigurationOnly(configObj); + + const [rankResult, setRankResult] = useState<RankResults>(null); + const [selectResult, setSelectResult] = useState<SelectResults>(null); + const [textResult, setTextResult] = useState<TextResults>(null); + + // Group the different results by the ID of the question, + const groupByID = ( + resultMap: Map<ID, number[][] | string[][]>, + IDs: ID[], + results: boolean[][] | number[][] | string[][], + toNumber: boolean = false + ) => { + IDs.forEach((id, index) => { + let updatedRes = []; + let res = results[index]; + + // SelectResult are mapped to 0 or 1s, such that ballots can be + // counted more efficiently + if (toNumber) { + res = (results[index] as boolean[]).map((r: boolean) => (r ? 1 : 0)); + } + + if (resultMap.has(id)) { + updatedRes = resultMap.get(id); + } + + updatedRes.push(res); + resultMap.set(id, updatedRes); + }); + }; + + const groupResultsByID = () => { + let selectRes: SelectResults = new Map<ID, number[][]>(); + let rankRes: RankResults = new Map<ID, number[][]>(); + let textRes: TextResults = new Map<ID, string[][]>(); + + result.forEach((res) => { + if ( + res.SelectResultIDs !== null && + res.RankResultIDs !== null && + res.TextResultIDs !== null + ) { + groupByID(selectRes, res.SelectResultIDs, res.SelectResult, true); + groupByID(rankRes, res.RankResultIDs, res.RankResult); + groupByID(textRes, res.TextResultIDs, res.TextResult); + } + }); + + return { rankRes, selectRes, textRes }; + }; + + useEffect(() => { + if (result !== null) { + const { rankRes, selectRes, textRes } = groupResultsByID(); + + setRankResult(rankRes); + setSelectResult(selectRes); + setTextResult(textRes); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { + dataToDownload.push({ Title: subject.Title }); + + subject.Order.forEach((id: ID) => { + const element = subject.Elements.get(id); + let res = undefined; + + switch (element.Type) { + case RANK: + const rank = element as RankQuestion; + + if (rankResult.has(id)) { + res = countRankResult(rankResult.get(id), element as RankQuestion).resultsInPercent.map( + (percent, index) => { + return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; + } + ); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + + case SELECT: + const select = element as SelectQuestion; + + if (selectResult.has(id)) { + res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { + return { Candidate: select.Choices[index], Percentage: `${percent}%` }; + }); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + + case SUBJECT: + getResultData(element as Subject, dataToDownload); + break; + + case TEXT: + if (textResult.has(id)) { + res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { + return { Candidate: r[0], Percentage: `${r[1]}%` }; + }); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + } + }); + }; + + const exportJSONData = () => { + const fileName = 'result.json'; + + const dataToDownload: DownloadedResults[] = []; + + configuration.Scaffold.forEach((subject: Subject) => { + getResultData(subject, dataToDownload); + }); + + const data = { + Title: configuration.MainTitle, + NumberOfVotes: result.length, + Results: dataToDownload, + }; + + const fileToSave = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }); + + saveAs(fileToSave, fileName); + }; + + const SubjectElementResultDisplay = (element: SubjectElement) => { + return ( + <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> + <h2 className="text-lg pb-2">{element.Title}</h2> + {element.Type === RANK && rankResult.has(element.ID) && ( + <RankResult rank={element as RankQuestion} rankResult={rankResult.get(element.ID)} /> + )} + {element.Type === SELECT && selectResult.has(element.ID) && ( + <SelectResult + select={element as SelectQuestion} + selectResult={selectResult.get(element.ID)} + /> + )} + {element.Type === TEXT && textResult.has(element.ID) && ( + <TextResult textResult={textResult.get(element.ID)} /> + )} + </div> + ); + }; + + const displayResults = (subject: Subject) => { + return ( + <div key={subject.ID}> + <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> + {subject.Title} + </h2> + {subject.Order.map((id: ID) => ( + <div key={id}> + {subject.Elements.get(id).Type === SUBJECT ? ( + <div className="pl-4 sm:pl-6"> + {displayResults(subject.Elements.get(id) as Subject)} + </div> + ) : ( + SubjectElementResultDisplay(subject.Elements.get(id)) + )} + </div> + ))} + </div> + ); + }; + + return ( + <div> + <div className="flex flex-col"> + {configuration.Scaffold.map((subject: Subject) => displayResults(subject))} + </div> + <div className="flex my-4"> + <button + type="button" + onClick={() => navigate(-1)} + className="text-gray-700 my-2 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500"> + {t('back')} + </button> + + <DownloadButton exportData={exportJSONData}>{t('exportJSON')}</DownloadButton> + </div> + </div> + ); +}; + +GroupedResult.propTypes = { + location: PropTypes.any, +}; + +export default GroupedResult; diff --git a/web/frontend/src/pages/form/Result.tsx b/web/frontend/src/pages/form/Result.tsx index 8fb79e389..a0d8d1e36 100644 --- a/web/frontend/src/pages/form/Result.tsx +++ b/web/frontend/src/pages/form/Result.tsx @@ -1,216 +1,25 @@ -import React, { FC, useEffect, useState } from 'react'; +import { FC } from 'react'; import PropTypes from 'prop-types'; -import { DownloadedResults, RankResults, SelectResults, TextResults } from 'types/form'; -import SelectResult from './components/SelectResult'; -import RankResult from './components/RankResult'; -import TextResult from './components/TextResult'; -import { - ID, - RANK, - RankQuestion, - SELECT, - SUBJECT, - SelectQuestion, - Subject, - SubjectElement, - TEXT, -} from 'types/configuration'; -import DownloadButton from 'components/buttons/DownloadButton'; + import { useTranslation } from 'react-i18next'; -import saveAs from 'file-saver'; + import { useParams } from 'react-router-dom'; -import { useNavigate } from 'react-router'; + import useForm from 'components/utils/useForm'; import { useConfigurationOnly } from 'components/utils/useConfiguration'; -import { - countRankResult, - countSelectResult, - countTextResult, -} from './components/utils/countResult'; + import Loading from 'pages/Loading'; import ResultExplanation from './components/ResultExplanation'; +import { Tab } from '@headlessui/react'; +import GroupedResult from './GroupedResult'; // Functional component that displays the result of the votes const FormResult: FC = () => { const { t } = useTranslation(); - const navigate = useNavigate(); const { formId } = useParams(); const { loading, result, configObj } = useForm(formId); const configuration = useConfigurationOnly(configObj); - - const [rankResult, setRankResult] = useState<RankResults>(null); - const [selectResult, setSelectResult] = useState<SelectResults>(null); - const [textResult, setTextResult] = useState<TextResults>(null); - - // Group the different results by the ID of the question, - const groupByID = ( - resultMap: Map<ID, number[][] | string[][]>, - IDs: ID[], - results: boolean[][] | number[][] | string[][], - toNumber: boolean = false - ) => { - IDs.forEach((id, index) => { - let updatedRes = []; - let res = results[index]; - - // SelectResult are mapped to 0 or 1s, such that ballots can be - // counted more efficiently - if (toNumber) { - res = (results[index] as boolean[]).map((r: boolean) => (r ? 1 : 0)); - } - - if (resultMap.has(id)) { - updatedRes = resultMap.get(id); - } - - updatedRes.push(res); - resultMap.set(id, updatedRes); - }); - }; - - const groupResultsByID = () => { - let selectRes: SelectResults = new Map<ID, number[][]>(); - let rankRes: RankResults = new Map<ID, number[][]>(); - let textRes: TextResults = new Map<ID, string[][]>(); - - result.forEach((res) => { - if ( - res.SelectResultIDs !== null && - res.RankResultIDs !== null && - res.TextResultIDs !== null - ) { - groupByID(selectRes, res.SelectResultIDs, res.SelectResult, true); - groupByID(rankRes, res.RankResultIDs, res.RankResult); - groupByID(textRes, res.TextResultIDs, res.TextResult); - } - }); - - return { rankRes, selectRes, textRes }; - }; - - useEffect(() => { - if (result !== null) { - const { rankRes, selectRes, textRes } = groupResultsByID(); - - setRankResult(rankRes); - setSelectResult(selectRes); - setTextResult(textRes); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [result]); - - const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { - dataToDownload.push({ Title: subject.Title }); - - subject.Order.forEach((id: ID) => { - const element = subject.Elements.get(id); - let res = undefined; - - switch (element.Type) { - case RANK: - const rank = element as RankQuestion; - - if (rankResult.has(id)) { - res = countRankResult(rankResult.get(id), element as RankQuestion).resultsInPercent.map( - (percent, index) => { - return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; - } - ); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - - case SELECT: - const select = element as SelectQuestion; - - if (selectResult.has(id)) { - res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { - return { Candidate: select.Choices[index], Percentage: `${percent}%` }; - }); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - - case SUBJECT: - getResultData(element as Subject, dataToDownload); - break; - - case TEXT: - if (textResult.has(id)) { - res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { - return { Candidate: r[0], Percentage: `${r[1]}%` }; - }); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - } - }); - }; - - const exportJSONData = () => { - const fileName = 'result.json'; - - const dataToDownload: DownloadedResults[] = []; - - configuration.Scaffold.forEach((subject: Subject) => { - getResultData(subject, dataToDownload); - }); - - const data = { - Title: configuration.MainTitle, - NumberOfVotes: result.length, - Results: dataToDownload, - }; - - const fileToSave = new Blob([JSON.stringify(data, null, 2)], { - type: 'application/json', - }); - - saveAs(fileToSave, fileName); - }; - - const SubjectElementResultDisplay = (element: SubjectElement) => { - return ( - <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> - <h2 className="text-lg pb-2">{element.Title}</h2> - {element.Type === RANK && rankResult.has(element.ID) && ( - <RankResult rank={element as RankQuestion} rankResult={rankResult.get(element.ID)} /> - )} - {element.Type === SELECT && selectResult.has(element.ID) && ( - <SelectResult - select={element as SelectQuestion} - selectResult={selectResult.get(element.ID)} - /> - )} - {element.Type === TEXT && textResult.has(element.ID) && ( - <TextResult textResult={textResult.get(element.ID)} /> - )} - </div> - ); - }; - - const displayResults = (subject: Subject) => { - return ( - <div key={subject.ID}> - <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> - {subject.Title} - </h2> - {subject.Order.map((id: ID) => ( - <div key={id}> - {subject.Elements.get(id).Type === SUBJECT ? ( - <div className="pl-4 sm:pl-6"> - {displayResults(subject.Elements.get(id) as Subject)} - </div> - ) : ( - SubjectElementResultDisplay(subject.Elements.get(id)) - )} - </div> - ))} - </div> - ); - }; - return ( <div className="w-[60rem] font-sans px-4 pt-8 pb-4"> {!loading ? ( @@ -223,29 +32,42 @@ const FormResult: FC = () => { <ResultExplanation /> </div> </div> - <div className="w-full pb-4 my-0 sm:my-4"> <h2 className="text-lg mt-2 sm:mt-4 sm:mb-6 mb-4"> {t('totalNumberOfVotes', { votes: result.length })} </h2> - <h3 className="py-6 border-t text-2xl text-center text-gray-700"> + <h3 className="py-6 border-y text-2xl text-center text-gray-700"> {configuration.MainTitle} </h3> - <div className="flex flex-col"> - {configuration.Scaffold.map((subject: Subject) => displayResults(subject))} + <div> + <Tab.Group> + <Tab.List className="flex space-x-1 rounded-xl p-1"> + <Tab + key="grouped" + className={({ selected }) => + selected + ? 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-white bg-indigo-500 shadow' + : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-gray-700 hover:bg-indigo-100 hover:text-indigo-500' + }> + Tab 1 + </Tab> + <Tab + key="individual" + className={({ selected }) => + selected + ? 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-white bg-indigo-500 shadow' + : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-indigo-500 text-gray-600 hover:bg-indigo-100 hover:text-indigo-500' + }> + Tab 2 + </Tab> + </Tab.List> + <Tab.Panel> + <GroupedResult /> + </Tab.Panel> + </Tab.Group> </div> </div> - <div className="flex my-4"> - <button - type="button" - onClick={() => navigate(-1)} - className="text-gray-700 my-2 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500"> - {t('back')} - </button> - - <DownloadButton exportData={exportJSONData}>{t('exportJSON')}</DownloadButton> - </div> </div> ) : ( <Loading /> From 35d8a7375d2ff62ea43c049b6e856a032f295c02 Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Wed, 9 Nov 2022 14:32:12 +0100 Subject: [PATCH 2/7] Add i18nable tab titles --- web/frontend/src/language/en.json | 4 +++- web/frontend/src/pages/form/Result.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/frontend/src/language/en.json b/web/frontend/src/language/en.json index 100dd0f42..9e7d844d4 100644 --- a/web/frontend/src/language/en.json +++ b/web/frontend/src/language/en.json @@ -257,6 +257,8 @@ "actionNotAvailable": "Action not available", "uninitialized": "Uninitialized", "actionTextVoter1": "The form is not open yet, you can come back later to vote once it is open.", - "actionTextVoter2": "The results of the form are not available yet." + "actionTextVoter2": "The results of the form are not available yet.", + "resIndiv": "Individual", + "resGroup": "Grouped" } } diff --git a/web/frontend/src/pages/form/Result.tsx b/web/frontend/src/pages/form/Result.tsx index a0d8d1e36..da635f0ff 100644 --- a/web/frontend/src/pages/form/Result.tsx +++ b/web/frontend/src/pages/form/Result.tsx @@ -50,7 +50,7 @@ const FormResult: FC = () => { ? 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-white bg-indigo-500 shadow' : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-gray-700 hover:bg-indigo-100 hover:text-indigo-500' }> - Tab 1 + {t('resIndiv')} </Tab> <Tab key="individual" @@ -59,7 +59,7 @@ const FormResult: FC = () => { ? 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-white bg-indigo-500 shadow' : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-indigo-500 text-gray-600 hover:bg-indigo-100 hover:text-indigo-500' }> - Tab 2 + {t('resGroup')} </Tab> </Tab.List> <Tab.Panel> From 1b221c310e387e81ef63f3c7b7c2caef1fabef53 Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Tue, 22 Nov 2022 11:23:49 +0100 Subject: [PATCH 3/7] Add he two result tabs and individualize answers --- web/frontend/src/pages/form/GroupedResult.tsx | 1 + .../src/pages/form/IndividualResult.tsx | 239 ++++++++++++++++++ web/frontend/src/pages/form/Result.tsx | 10 +- 3 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 web/frontend/src/pages/form/IndividualResult.tsx diff --git a/web/frontend/src/pages/form/GroupedResult.tsx b/web/frontend/src/pages/form/GroupedResult.tsx index 73e0f1075..4016f81cc 100644 --- a/web/frontend/src/pages/form/GroupedResult.tsx +++ b/web/frontend/src/pages/form/GroupedResult.tsx @@ -189,6 +189,7 @@ const GroupedResult: FC = () => { }; const displayResults = (subject: Subject) => { + console.log(rankResult); return ( <div key={subject.ID}> <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> diff --git a/web/frontend/src/pages/form/IndividualResult.tsx b/web/frontend/src/pages/form/IndividualResult.tsx new file mode 100644 index 000000000..35cc75172 --- /dev/null +++ b/web/frontend/src/pages/form/IndividualResult.tsx @@ -0,0 +1,239 @@ +import React, { FC, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { DownloadedResults, RankResults, SelectResults, TextResults } from 'types/form'; +import SelectResult from './components/SelectResult'; +import RankResult from './components/RankResult'; +import TextResult from './components/TextResult'; +import { + ID, + RANK, + RankQuestion, + SELECT, + SUBJECT, + SelectQuestion, + Subject, + SubjectElement, + TEXT, +} from 'types/configuration'; +import DownloadButton from 'components/buttons/DownloadButton'; +import { useTranslation } from 'react-i18next'; +import saveAs from 'file-saver'; +import { useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router'; +import useForm from 'components/utils/useForm'; +import { useConfigurationOnly } from 'components/utils/useConfiguration'; +import { + countRankResult, + countSelectResult, + countTextResult, +} from './components/utils/countResult'; + +// Functional component that displays the result of the votes +const IndividualResult: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { formId } = useParams(); + + const { loading, result, configObj } = useForm(formId); + const configuration = useConfigurationOnly(configObj); + + const [rankResult, setRankResult] = useState<RankResults>(null); + const [selectResult, setSelectResult] = useState<SelectResults>(null); + const [textResult, setTextResult] = useState<TextResults>(null); + const [currentID, setCurrentID] = useState<number>(0); + + // Group the different results by the ID of the question, + const groupByID = ( + resultMap: Map<ID, number[][] | string[][]>, + IDs: ID[], + results: boolean[][] | number[][] | string[][], + toNumber: boolean = false + ) => { + IDs.forEach((id, index) => { + let updatedRes = []; + let res = results[index]; + + // SelectResult are mapped to 0 or 1s, such that ballots can be + // counted more efficiently + if (toNumber) { + res = (results[index] as boolean[]).map((r: boolean) => (r ? 1 : 0)); + } + + if (resultMap.has(id)) { + updatedRes = resultMap.get(id); + } + + updatedRes.push(res); + resultMap.set(id, updatedRes); + }); + }; + + const groupResultsByID = () => { + let selectRes: SelectResults = new Map<ID, number[][]>(); + let rankRes: RankResults = new Map<ID, number[][]>(); + let textRes: TextResults = new Map<ID, string[][]>(); + + result.forEach((res) => { + if ( + res.SelectResultIDs !== null && + res.RankResultIDs !== null && + res.TextResultIDs !== null + ) { + groupByID(selectRes, res.SelectResultIDs, res.SelectResult, true); + groupByID(rankRes, res.RankResultIDs, res.RankResult); + groupByID(textRes, res.TextResultIDs, res.TextResult); + } + }); + + return { rankRes, selectRes, textRes }; + }; + + useEffect(() => { + if (result !== null) { + const { rankRes, selectRes, textRes } = groupResultsByID(); + + setRankResult(rankRes); + setSelectResult(selectRes); + setTextResult(textRes); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { + dataToDownload.push({ Title: subject.Title }); + + subject.Order.forEach((id: ID) => { + const element = subject.Elements.get(id); + let res = undefined; + + switch (element.Type) { + case RANK: + const rank = element as RankQuestion; + + if (rankResult.has(id)) { + res = countRankResult(rankResult.get(id), element as RankQuestion).resultsInPercent.map( + (percent, index) => { + return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; + } + ); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + + case SELECT: + const select = element as SelectQuestion; + + if (selectResult.has(id)) { + res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { + return { Candidate: select.Choices[index], Percentage: `${percent}%` }; + }); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + + case SUBJECT: + getResultData(element as Subject, dataToDownload); + break; + + case TEXT: + if (textResult.has(id)) { + res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { + return { Candidate: r[0], Percentage: `${r[1]}%` }; + }); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + } + }); + }; + + const exportJSONData = () => { + const fileName = 'result.json'; + + const dataToDownload: DownloadedResults[] = []; + + configuration.Scaffold.forEach((subject: Subject) => { + getResultData(subject, dataToDownload); + }); + + const data = { + Title: configuration.MainTitle, + NumberOfVotes: result.length, + Results: dataToDownload, + }; + + const fileToSave = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }); + + saveAs(fileToSave, fileName); + }; + + const SubjectElementResultDisplay = (element: SubjectElement) => { + return ( + <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> + <h2 className="text-lg pb-2">{element.Title}</h2> + {element.Type === RANK && rankResult.has(element.ID) && ( + <RankResult + rank={element as RankQuestion} + rankResult={[rankResult.get(element.ID)[currentID]]} + /> + )} + {element.Type === SELECT && selectResult.has(element.ID) && ( + <SelectResult + select={element as SelectQuestion} + selectResult={[selectResult.get(element.ID)[currentID]]} + /> + )} + {element.Type === TEXT && textResult.has(element.ID) && ( + <TextResult textResult={[textResult.get(element.ID)[currentID]]} /> + )} + </div> + ); + }; + + const displayResults = (subject: Subject) => { + return ( + <div key={subject.ID}> + <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> + {subject.Title} + </h2> + {subject.Order.map((id: ID) => ( + <div key={id}> + {subject.Elements.get(id).Type === SUBJECT ? ( + <div className="pl-4 sm:pl-6"> + {displayResults(subject.Elements.get(id) as Subject)} + </div> + ) : ( + SubjectElementResultDisplay(subject.Elements.get(id)) + )} + </div> + ))} + </div> + ); + }; + + return ( + <div> + <div className="flex flex-col"> + {configuration.Scaffold.map((subject: Subject) => displayResults(subject))} + </div> + <div className="flex my-4"> + <button + type="button" + onClick={() => navigate(-1)} + className="text-gray-700 my-2 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500"> + {t('back')} + </button> + + <DownloadButton exportData={exportJSONData}>{t('exportJSON')}</DownloadButton> + </div> + </div> + ); +}; + +IndividualResult.propTypes = { + location: PropTypes.any, +}; + +export default IndividualResult; diff --git a/web/frontend/src/pages/form/Result.tsx b/web/frontend/src/pages/form/Result.tsx index da635f0ff..156d8f304 100644 --- a/web/frontend/src/pages/form/Result.tsx +++ b/web/frontend/src/pages/form/Result.tsx @@ -12,6 +12,7 @@ import Loading from 'pages/Loading'; import ResultExplanation from './components/ResultExplanation'; import { Tab } from '@headlessui/react'; import GroupedResult from './GroupedResult'; +import IndividualResult from './IndividualResult'; // Functional component that displays the result of the votes const FormResult: FC = () => { @@ -50,21 +51,24 @@ const FormResult: FC = () => { ? 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-white bg-indigo-500 shadow' : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-gray-700 hover:bg-indigo-100 hover:text-indigo-500' }> - {t('resIndiv')} + {t('resGroup')} </Tab> <Tab key="individual" className={({ selected }) => selected ? 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-white bg-indigo-500 shadow' - : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-indigo-500 text-gray-600 hover:bg-indigo-100 hover:text-indigo-500' + : 'w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-gray-600 hover:bg-indigo-100 hover:text-indigo-500' }> - {t('resGroup')} + {t('resIndiv')} </Tab> </Tab.List> <Tab.Panel> <GroupedResult /> </Tab.Panel> + <Tab.Panel> + <IndividualResult /> + </Tab.Panel> </Tab.Group> </div> </div> From 5a162b3f69a5582a18b22f8d34ecadbe113a914d Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Tue, 22 Nov 2022 11:24:58 +0100 Subject: [PATCH 4/7] add initial value to counting select results --- web/frontend/src/pages/form/components/utils/countResult.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/frontend/src/pages/form/components/utils/countResult.ts b/web/frontend/src/pages/form/components/utils/countResult.ts index 7517b4f2d..7c7973955 100644 --- a/web/frontend/src/pages/form/components/utils/countResult.ts +++ b/web/frontend/src/pages/form/components/utils/countResult.ts @@ -54,7 +54,7 @@ const countSelectResult = (selectResult: number[][]) => { } return current; }); - }); + }, new Array(selectResult[0].length).fill(0)); results.forEach((count, index) => { if (count === max) { From 58f2ccf94233a5e4d217e4f125c247f47e83f7bc Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Wed, 23 Nov 2022 10:49:49 +0100 Subject: [PATCH 5/7] Refactor code to have the least duplication possible --- web/frontend/src/pages/form/GroupedResult.tsx | 174 ++--------------- .../src/pages/form/IndividualResult.tsx | 170 +---------------- web/frontend/src/pages/form/Result.tsx | 180 +++++++++++++++++- 3 files changed, 194 insertions(+), 330 deletions(-) diff --git a/web/frontend/src/pages/form/GroupedResult.tsx b/web/frontend/src/pages/form/GroupedResult.tsx index 4016f81cc..2665ce240 100644 --- a/web/frontend/src/pages/form/GroupedResult.tsx +++ b/web/frontend/src/pages/form/GroupedResult.tsx @@ -1,6 +1,5 @@ -import React, { FC, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { DownloadedResults, RankResults, SelectResults, TextResults } from 'types/form'; +import { FC } from 'react'; +import { RankResults, SelectResults, TextResults } from 'types/form'; import SelectResult from './components/SelectResult'; import RankResult from './components/RankResult'; import TextResult from './components/TextResult'; @@ -15,159 +14,23 @@ import { SubjectElement, TEXT, } from 'types/configuration'; -import DownloadButton from 'components/buttons/DownloadButton'; -import { useTranslation } from 'react-i18next'; -import saveAs from 'file-saver'; import { useParams } from 'react-router-dom'; -import { useNavigate } from 'react-router'; import useForm from 'components/utils/useForm'; import { useConfigurationOnly } from 'components/utils/useConfiguration'; -import { - countRankResult, - countSelectResult, - countTextResult, -} from './components/utils/countResult'; + +type GroupedResultProps = { + rankResult: RankResults; + selectResult: SelectResults; + textResult: TextResults; +}; // Functional component that displays the result of the votes -const GroupedResult: FC = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); +const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textResult }) => { const { formId } = useParams(); - const { loading, result, configObj } = useForm(formId); + const { result, configObj } = useForm(formId); const configuration = useConfigurationOnly(configObj); - const [rankResult, setRankResult] = useState<RankResults>(null); - const [selectResult, setSelectResult] = useState<SelectResults>(null); - const [textResult, setTextResult] = useState<TextResults>(null); - - // Group the different results by the ID of the question, - const groupByID = ( - resultMap: Map<ID, number[][] | string[][]>, - IDs: ID[], - results: boolean[][] | number[][] | string[][], - toNumber: boolean = false - ) => { - IDs.forEach((id, index) => { - let updatedRes = []; - let res = results[index]; - - // SelectResult are mapped to 0 or 1s, such that ballots can be - // counted more efficiently - if (toNumber) { - res = (results[index] as boolean[]).map((r: boolean) => (r ? 1 : 0)); - } - - if (resultMap.has(id)) { - updatedRes = resultMap.get(id); - } - - updatedRes.push(res); - resultMap.set(id, updatedRes); - }); - }; - - const groupResultsByID = () => { - let selectRes: SelectResults = new Map<ID, number[][]>(); - let rankRes: RankResults = new Map<ID, number[][]>(); - let textRes: TextResults = new Map<ID, string[][]>(); - - result.forEach((res) => { - if ( - res.SelectResultIDs !== null && - res.RankResultIDs !== null && - res.TextResultIDs !== null - ) { - groupByID(selectRes, res.SelectResultIDs, res.SelectResult, true); - groupByID(rankRes, res.RankResultIDs, res.RankResult); - groupByID(textRes, res.TextResultIDs, res.TextResult); - } - }); - - return { rankRes, selectRes, textRes }; - }; - - useEffect(() => { - if (result !== null) { - const { rankRes, selectRes, textRes } = groupResultsByID(); - - setRankResult(rankRes); - setSelectResult(selectRes); - setTextResult(textRes); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [result]); - - const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { - dataToDownload.push({ Title: subject.Title }); - - subject.Order.forEach((id: ID) => { - const element = subject.Elements.get(id); - let res = undefined; - - switch (element.Type) { - case RANK: - const rank = element as RankQuestion; - - if (rankResult.has(id)) { - res = countRankResult(rankResult.get(id), element as RankQuestion).resultsInPercent.map( - (percent, index) => { - return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; - } - ); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - - case SELECT: - const select = element as SelectQuestion; - - if (selectResult.has(id)) { - res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { - return { Candidate: select.Choices[index], Percentage: `${percent}%` }; - }); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - - case SUBJECT: - getResultData(element as Subject, dataToDownload); - break; - - case TEXT: - if (textResult.has(id)) { - res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { - return { Candidate: r[0], Percentage: `${r[1]}%` }; - }); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - } - }); - }; - - const exportJSONData = () => { - const fileName = 'result.json'; - - const dataToDownload: DownloadedResults[] = []; - - configuration.Scaffold.forEach((subject: Subject) => { - getResultData(subject, dataToDownload); - }); - - const data = { - Title: configuration.MainTitle, - NumberOfVotes: result.length, - Results: dataToDownload, - }; - - const fileToSave = new Blob([JSON.stringify(data, null, 2)], { - type: 'application/json', - }); - - saveAs(fileToSave, fileName); - }; - const SubjectElementResultDisplay = (element: SubjectElement) => { return ( <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> @@ -189,7 +52,7 @@ const GroupedResult: FC = () => { }; const displayResults = (subject: Subject) => { - console.log(rankResult); + console.log(result); return ( <div key={subject.ID}> <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> @@ -215,22 +78,9 @@ const GroupedResult: FC = () => { <div className="flex flex-col"> {configuration.Scaffold.map((subject: Subject) => displayResults(subject))} </div> - <div className="flex my-4"> - <button - type="button" - onClick={() => navigate(-1)} - className="text-gray-700 my-2 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500"> - {t('back')} - </button> - - <DownloadButton exportData={exportJSONData}>{t('exportJSON')}</DownloadButton> - </div> + <div className="flex my-4"></div> </div> ); }; -GroupedResult.propTypes = { - location: PropTypes.any, -}; - export default GroupedResult; diff --git a/web/frontend/src/pages/form/IndividualResult.tsx b/web/frontend/src/pages/form/IndividualResult.tsx index 35cc75172..d186e9f02 100644 --- a/web/frontend/src/pages/form/IndividualResult.tsx +++ b/web/frontend/src/pages/form/IndividualResult.tsx @@ -1,6 +1,5 @@ -import React, { FC, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { DownloadedResults, RankResults, SelectResults, TextResults } from 'types/form'; +import { FC, useState } from 'react'; +import { RankResults, SelectResults, TextResults } from 'types/form'; import SelectResult from './components/SelectResult'; import RankResult from './components/RankResult'; import TextResult from './components/TextResult'; @@ -15,160 +14,23 @@ import { SubjectElement, TEXT, } from 'types/configuration'; -import DownloadButton from 'components/buttons/DownloadButton'; -import { useTranslation } from 'react-i18next'; -import saveAs from 'file-saver'; import { useParams } from 'react-router-dom'; -import { useNavigate } from 'react-router'; import useForm from 'components/utils/useForm'; import { useConfigurationOnly } from 'components/utils/useConfiguration'; -import { - countRankResult, - countSelectResult, - countTextResult, -} from './components/utils/countResult'; - +type IndividualResultProps = { + rankResult: RankResults; + selectResult: SelectResults; + textResult: TextResults; +}; // Functional component that displays the result of the votes -const IndividualResult: FC = () => { - const { t } = useTranslation(); - const navigate = useNavigate(); +const IndividualResult: FC<IndividualResultProps> = ({ rankResult, selectResult, textResult }) => { const { formId } = useParams(); - const { loading, result, configObj } = useForm(formId); + const { configObj } = useForm(formId); const configuration = useConfigurationOnly(configObj); - const [rankResult, setRankResult] = useState<RankResults>(null); - const [selectResult, setSelectResult] = useState<SelectResults>(null); - const [textResult, setTextResult] = useState<TextResults>(null); const [currentID, setCurrentID] = useState<number>(0); - // Group the different results by the ID of the question, - const groupByID = ( - resultMap: Map<ID, number[][] | string[][]>, - IDs: ID[], - results: boolean[][] | number[][] | string[][], - toNumber: boolean = false - ) => { - IDs.forEach((id, index) => { - let updatedRes = []; - let res = results[index]; - - // SelectResult are mapped to 0 or 1s, such that ballots can be - // counted more efficiently - if (toNumber) { - res = (results[index] as boolean[]).map((r: boolean) => (r ? 1 : 0)); - } - - if (resultMap.has(id)) { - updatedRes = resultMap.get(id); - } - - updatedRes.push(res); - resultMap.set(id, updatedRes); - }); - }; - - const groupResultsByID = () => { - let selectRes: SelectResults = new Map<ID, number[][]>(); - let rankRes: RankResults = new Map<ID, number[][]>(); - let textRes: TextResults = new Map<ID, string[][]>(); - - result.forEach((res) => { - if ( - res.SelectResultIDs !== null && - res.RankResultIDs !== null && - res.TextResultIDs !== null - ) { - groupByID(selectRes, res.SelectResultIDs, res.SelectResult, true); - groupByID(rankRes, res.RankResultIDs, res.RankResult); - groupByID(textRes, res.TextResultIDs, res.TextResult); - } - }); - - return { rankRes, selectRes, textRes }; - }; - - useEffect(() => { - if (result !== null) { - const { rankRes, selectRes, textRes } = groupResultsByID(); - - setRankResult(rankRes); - setSelectResult(selectRes); - setTextResult(textRes); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [result]); - - const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { - dataToDownload.push({ Title: subject.Title }); - - subject.Order.forEach((id: ID) => { - const element = subject.Elements.get(id); - let res = undefined; - - switch (element.Type) { - case RANK: - const rank = element as RankQuestion; - - if (rankResult.has(id)) { - res = countRankResult(rankResult.get(id), element as RankQuestion).resultsInPercent.map( - (percent, index) => { - return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; - } - ); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - - case SELECT: - const select = element as SelectQuestion; - - if (selectResult.has(id)) { - res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { - return { Candidate: select.Choices[index], Percentage: `${percent}%` }; - }); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - - case SUBJECT: - getResultData(element as Subject, dataToDownload); - break; - - case TEXT: - if (textResult.has(id)) { - res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { - return { Candidate: r[0], Percentage: `${r[1]}%` }; - }); - dataToDownload.push({ Title: element.Title, Results: res }); - } - break; - } - }); - }; - - const exportJSONData = () => { - const fileName = 'result.json'; - - const dataToDownload: DownloadedResults[] = []; - - configuration.Scaffold.forEach((subject: Subject) => { - getResultData(subject, dataToDownload); - }); - - const data = { - Title: configuration.MainTitle, - NumberOfVotes: result.length, - Results: dataToDownload, - }; - - const fileToSave = new Blob([JSON.stringify(data, null, 2)], { - type: 'application/json', - }); - - saveAs(fileToSave, fileName); - }; - const SubjectElementResultDisplay = (element: SubjectElement) => { return ( <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> @@ -218,22 +80,8 @@ const IndividualResult: FC = () => { <div className="flex flex-col"> {configuration.Scaffold.map((subject: Subject) => displayResults(subject))} </div> - <div className="flex my-4"> - <button - type="button" - onClick={() => navigate(-1)} - className="text-gray-700 my-2 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500"> - {t('back')} - </button> - - <DownloadButton exportData={exportJSONData}>{t('exportJSON')}</DownloadButton> - </div> </div> ); }; -IndividualResult.propTypes = { - location: PropTypes.any, -}; - export default IndividualResult; diff --git a/web/frontend/src/pages/form/Result.tsx b/web/frontend/src/pages/form/Result.tsx index 156d8f304..2715a33bc 100644 --- a/web/frontend/src/pages/form/Result.tsx +++ b/web/frontend/src/pages/form/Result.tsx @@ -1,26 +1,174 @@ -import { FC } from 'react'; +import { FC, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; - import { useTranslation } from 'react-i18next'; - +import { DownloadedResults, RankResults, SelectResults, TextResults } from 'types/form'; +import { + ID, + RANK, + RankQuestion, + SELECT, + SUBJECT, + SelectQuestion, + Subject, + TEXT, +} from 'types/configuration'; import { useParams } from 'react-router-dom'; - +import { useNavigate } from 'react-router'; import useForm from 'components/utils/useForm'; import { useConfigurationOnly } from 'components/utils/useConfiguration'; - +import DownloadButton from 'components/buttons/DownloadButton'; import Loading from 'pages/Loading'; +import saveAs from 'file-saver'; import ResultExplanation from './components/ResultExplanation'; import { Tab } from '@headlessui/react'; import GroupedResult from './GroupedResult'; import IndividualResult from './IndividualResult'; +import { + countRankResult, + countSelectResult, + countTextResult, +} from './components/utils/countResult'; // Functional component that displays the result of the votes const FormResult: FC = () => { const { t } = useTranslation(); + const navigate = useNavigate(); const { formId } = useParams(); const { loading, result, configObj } = useForm(formId); const configuration = useConfigurationOnly(configObj); + + const [rankResult, setRankResult] = useState<RankResults>(null); + const [selectResult, setSelectResult] = useState<SelectResults>(null); + const [textResult, setTextResult] = useState<TextResults>(null); + + const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { + dataToDownload.push({ Title: subject.Title }); + + subject.Order.forEach((id: ID) => { + const element = subject.Elements.get(id); + let res = undefined; + + switch (element.Type) { + case RANK: + const rank = element as RankQuestion; + + if (rankResult.has(id)) { + res = countRankResult(rankResult.get(id), element as RankQuestion).resultsInPercent.map( + (percent, index) => { + return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; + } + ); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + + case SELECT: + const select = element as SelectQuestion; + + if (selectResult.has(id)) { + res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { + return { Candidate: select.Choices[index], Percentage: `${percent}%` }; + }); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + + case SUBJECT: + getResultData(element as Subject, dataToDownload); + break; + + case TEXT: + if (textResult.has(id)) { + res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { + return { Candidate: r[0], Percentage: `${r[1]}%` }; + }); + dataToDownload.push({ Title: element.Title, Results: res }); + } + break; + } + }); + }; + + // Group the different results by the ID of the question, + const groupByID = ( + resultMap: Map<ID, number[][] | string[][]>, + IDs: ID[], + results: boolean[][] | number[][] | string[][], + toNumber: boolean = false + ) => { + IDs.forEach((id, index) => { + let updatedRes = []; + let res = results[index]; + + // SelectResult are mapped to 0 or 1s, such that ballots can be + // counted more efficiently + if (toNumber) { + res = (results[index] as boolean[]).map((r: boolean) => (r ? 1 : 0)); + } + + if (resultMap.has(id)) { + updatedRes = resultMap.get(id); + } + + updatedRes.push(res); + resultMap.set(id, updatedRes); + }); + }; + + const groupResultsByID = () => { + let selectRes: SelectResults = new Map<ID, number[][]>(); + let rankRes: RankResults = new Map<ID, number[][]>(); + let textRes: TextResults = new Map<ID, string[][]>(); + + result.forEach((res) => { + if ( + res.SelectResultIDs !== null && + res.RankResultIDs !== null && + res.TextResultIDs !== null + ) { + groupByID(selectRes, res.SelectResultIDs, res.SelectResult, true); + groupByID(rankRes, res.RankResultIDs, res.RankResult); + groupByID(textRes, res.TextResultIDs, res.TextResult); + } + }); + + return { rankRes, selectRes, textRes }; + }; + + useEffect(() => { + if (result !== null) { + const { rankRes, selectRes, textRes } = groupResultsByID(); + + setRankResult(rankRes); + setSelectResult(selectRes); + setTextResult(textRes); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [result]); + + const exportJSONData = () => { + const fileName = 'result.json'; + + const dataToDownload: DownloadedResults[] = []; + + configuration.Scaffold.forEach((subject: Subject) => { + getResultData(subject, dataToDownload); + }); + + const data = { + Title: configuration.MainTitle, + NumberOfVotes: result.length, + Results: dataToDownload, + }; + + const fileToSave = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }); + + saveAs(fileToSave, fileName); + }; + return ( <div className="w-[60rem] font-sans px-4 pt-8 pb-4"> {!loading ? ( @@ -64,14 +212,32 @@ const FormResult: FC = () => { </Tab> </Tab.List> <Tab.Panel> - <GroupedResult /> + <GroupedResult + rankResult={rankResult} + selectResult={selectResult} + textResult={textResult} + /> </Tab.Panel> <Tab.Panel> - <IndividualResult /> + <IndividualResult + rankResult={rankResult} + selectResult={selectResult} + textResult={textResult} + /> </Tab.Panel> </Tab.Group> </div> </div> + <div className="flex my-4"> + <button + type="button" + onClick={() => navigate(-1)} + className="text-gray-700 my-2 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500"> + {t('back')} + </button> + + <DownloadButton exportData={exportJSONData}>{t('exportJSON')}</DownloadButton> + </div> </div> ) : ( <Loading /> From 0d9a57b6b2c51dd5aa074f367cd1513446e728fe Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Thu, 24 Nov 2022 17:32:27 +0100 Subject: [PATCH 6/7] Add a pagination mechanism (unstyled) --- .../src/pages/form/IndividualResult.tsx | 37 +++++++++++++++++-- web/frontend/src/pages/form/Result.tsx | 1 + 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/web/frontend/src/pages/form/IndividualResult.tsx b/web/frontend/src/pages/form/IndividualResult.tsx index d186e9f02..f7d7a10ed 100644 --- a/web/frontend/src/pages/form/IndividualResult.tsx +++ b/web/frontend/src/pages/form/IndividualResult.tsx @@ -1,8 +1,9 @@ -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { RankResults, SelectResults, TextResults } from 'types/form'; import SelectResult from './components/SelectResult'; import RankResult from './components/RankResult'; import TextResult from './components/TextResult'; +import { useTranslation } from 'react-i18next'; import { ID, RANK, @@ -17,15 +18,22 @@ import { import { useParams } from 'react-router-dom'; import useForm from 'components/utils/useForm'; import { useConfigurationOnly } from 'components/utils/useConfiguration'; + type IndividualResultProps = { rankResult: RankResults; selectResult: SelectResults; textResult: TextResults; + ballotNumber: number; }; // Functional component that displays the result of the votes -const IndividualResult: FC<IndividualResultProps> = ({ rankResult, selectResult, textResult }) => { +const IndividualResult: FC<IndividualResultProps> = ({ + rankResult, + selectResult, + textResult, + ballotNumber, +}) => { const { formId } = useParams(); - + const { t } = useTranslation(); const { configObj } = useForm(formId); const configuration = useConfigurationOnly(configObj); @@ -74,10 +82,33 @@ const IndividualResult: FC<IndividualResultProps> = ({ rankResult, selectResult, </div> ); }; + useEffect(() => { + configuration.Scaffold.map((subject: Subject) => displayResults(subject)); + }, [currentID]); + const handleNext = (): void => { + setCurrentID((currentID + 1) % ballotNumber); + }; + + const handlePrevious = (): void => { + setCurrentID((currentID - 1) % ballotNumber); + }; return ( <div> <div className="flex flex-col"> + <div className="flex-1 flex justify-between sm:justify-end"> + <button + onClick={handlePrevious} + className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> + {t('previous')} + </button> + {'Ballot ' + (currentID + 1)} + <button + onClick={handleNext} + className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> + {t('next')} + </button> + </div> {configuration.Scaffold.map((subject: Subject) => displayResults(subject))} </div> </div> diff --git a/web/frontend/src/pages/form/Result.tsx b/web/frontend/src/pages/form/Result.tsx index 2715a33bc..bd0b742ef 100644 --- a/web/frontend/src/pages/form/Result.tsx +++ b/web/frontend/src/pages/form/Result.tsx @@ -223,6 +223,7 @@ const FormResult: FC = () => { rankResult={rankResult} selectResult={selectResult} textResult={textResult} + ballotNumber={result.length} /> </Tab.Panel> </Tab.Group> From e6616b3bba1867f63cdc07ff80500bec79ad53ca Mon Sep 17 00:00:00 2001 From: Ahmed Kamal Elalamy <ahmed.elalamy@epfl.ch> Date: Fri, 25 Nov 2022 19:35:37 +0100 Subject: [PATCH 7/7] Improve styling of the pagination mechanism --- web/frontend/src/pages/form/IndividualResult.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web/frontend/src/pages/form/IndividualResult.tsx b/web/frontend/src/pages/form/IndividualResult.tsx index f7d7a10ed..a19a7b9c9 100644 --- a/web/frontend/src/pages/form/IndividualResult.tsx +++ b/web/frontend/src/pages/form/IndividualResult.tsx @@ -91,21 +91,22 @@ const IndividualResult: FC<IndividualResultProps> = ({ }; const handlePrevious = (): void => { - setCurrentID((currentID - 1) % ballotNumber); + setCurrentID((currentID - 1 + ballotNumber) % ballotNumber); }; + return ( <div> <div className="flex flex-col"> - <div className="flex-1 flex justify-between sm:justify-end"> + <div className="grid grid-cols-9 font-medium rounded-md border-t stext-sm text-center align-center justify-middle text-gray-700 bg-white py-2"> <button onClick={handlePrevious} - className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> + className="items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> {t('previous')} </button> - {'Ballot ' + (currentID + 1)} + <div className="grow col-span-7 p-2">{'Ballot ' + (currentID + 1)}</div> <button onClick={handleNext} - className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> + className="ml-3 relative align-right items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> {t('next')} </button> </div>