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>