Skip to content

Commit 7ed2f27

Browse files
authored
Merge pull request #128 from dedis/frontend-result
Fix the UI of the result and vote page
2 parents 6f7b5e6 + 907a497 commit 7ed2f27

23 files changed

+377
-241
lines changed

web/frontend/public/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
work correctly both with client-side routing and a non-root public URL.
2222
Learn how to configure a non-root public URL by running `npm run build`.
2323
-->
24-
<title>React App</title>
24+
<title>D-Voting</title>
2525
</head>
2626
<body>
2727
<noscript>You need to enable JavaScript to run this app.</noscript>

web/frontend/src/components/buttons/DownloadButton.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const DownloadButton: FC<DownloadButtonProps> = ({ exportData, children }) => {
1010
return (
1111
<button
1212
type="button"
13-
className="flex inline-flex my-2 mx-2 items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm shadow-md font-medium hover:text-indigo-500"
13+
className="flex my-2 mx-2 items-center px-4 py-2 border rounded-md text-sm font-medium hover:text-indigo-500"
1414
onClick={exportData}>
1515
<CloudDownloadIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
1616
{children}

web/frontend/src/components/buttons/TextButton.tsx

-18
This file was deleted.

web/frontend/src/components/utils/useConfiguration.tsx

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { useEffect, useState } from 'react';
22
import { Answers, Configuration } from 'types/configuration';
3-
import { emptyConfiguration } from 'types/getObjectType';
4-
import { unmarshalConfig, unmarshalConfigAndCreateAnswers } from 'types/JSONparser';
3+
import { emptyConfiguration, newAnswer } from 'types/getObjectType';
4+
import {
5+
unmarshalConfig,
6+
unmarshalConfigAndCreateAnswers,
7+
unmarshalSubjectAndCreateAnswers,
8+
} from 'types/JSONparser';
59

610
// Returns a Configuration and the initialized Answers
711
const useConfiguration = (configObj: any) => {
812
const [configuration, setConfiguration] = useState<Configuration>(emptyConfiguration());
9-
10-
const [answers, setAnswers]: [Answers, React.Dispatch<React.SetStateAction<Answers>>] =
11-
useState(null);
13+
const [answers, setAnswers] = useState<Answers>(null);
1214

1315
useEffect(() => {
1416
if (configObj !== null) {
@@ -38,4 +40,23 @@ const useConfigurationOnly = (configObj: any) => {
3840
return configuration;
3941
};
4042

41-
export { useConfiguration, useConfigurationOnly };
43+
// Custom hook to create answers from a Configuration.
44+
const useAnswers = (configuration: Configuration) => {
45+
const [answers, setAnswers] = useState<Answers>(null);
46+
47+
useEffect(() => {
48+
if (configuration !== null) {
49+
const newAnswers: Answers = newAnswer();
50+
51+
for (const subjectObj of configuration.Scaffold) {
52+
unmarshalSubjectAndCreateAnswers(subjectObj, newAnswers);
53+
}
54+
55+
setAnswers(newAnswers);
56+
}
57+
}, [configuration]);
58+
59+
return { answers, setAnswers };
60+
};
61+
62+
export { useConfiguration, useConfigurationOnly, useAnswers };

web/frontend/src/language/en.json

+7-6
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,17 @@
2828
"loginText": "You need to login to access the content of {{from}}",
2929
"loginCallback": "We are proceeding with the authentication. You should be redirected...",
3030
"logout": "Logout",
31-
"elecName": "Election title",
3231
"namePlaceHolder": "Enter the name",
3332
"addCandidate": "Add a candidate",
3433
"addUser": "Add user",
3534
"role": "Role",
3635
"edit": "Edit",
37-
"addCandPlaceHolder": "candidate's name",
3836
"nothingToAdd": "There is nothing to add.",
3937
"duplicateCandidate": "This candidate has already been added.",
4038
"add": "Add",
41-
"exportElecJSON": "Export as JSON",
39+
"exportJSON": "Export as JSON",
4240
"delete": "Delete",
43-
"createElec": "Create election",
44-
"clearElec": "Clear election",
41+
"clearElection": "Clear election",
4542
"upload": "Choose a json file from your computer:",
4643
"notJson": "The file needs to have the .json extension.",
4744
"noFile": "No file found",
@@ -138,14 +135,18 @@
138135
"pluralAnswers": "answers",
139136
"rankRange": "Answer must be between 1 and {{max}}. ",
140137
"castVote": "Cast vote",
138+
"voteExplanation": "You may cast a ballot as many time as you want while the election is open. Only your last vote will be taken into account.",
141139
"noVote": "There is currently nothing to vote on.",
142140
"voteAllowed": "You are allowed to vote on the election(s) below. Click on an election title to display its ballot and vote.",
143141
"displayResults": "The results of the election(s) listed below are available. Click on an election title to access them.",
144142
"noResultsAvailable": "There is currently no available results.",
145-
"exportResJSON": "Export as JSON",
143+
"resultExplanation1": "Results for select and text question are given in percentage of the number of votes for a candidate divided by the number of ballots cast. ",
144+
"resultExplanation2": "Results of rank question corresponds to the percentage of the score a candidate has. Each voter gives candidates points by ranking them (lower is better). ",
145+
"resultExplanation3": "The score corresponds to the sum of the points a candidate got and is divided by the total number of points attributed across all ballots.",
146146
"shuffle": "Shuffle",
147147
"decrypt": "Decrypt",
148148
"seeResult": "See results",
149+
"totalNumberOfVotes": "Total number of votes : {{votes}}",
149150
"notEnoughBallot": "The operation failed because less than two ballots have been casted.",
150151
"operationFailure": "The operation failed. Try refreshing the page.",
151152
"shuffleFail": "The shuffle operation failed.",

web/frontend/src/layout/NavBar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ const RightSideNavBar = ({ authCtx, handleLogout, fctx, t }) => (
150150
<div className="absolute hidden inset-y-0 right-0 flex items-center pr-2 md:static md:inset-auto md:flex md:ml-6 md:pr-0">
151151
{authCtx.isLogged && (authCtx.role === 'admin' || authCtx.role === 'operator') && (
152152
<NavLink title={t('navBarCreateElection')} to={ROUTE_ELECTION_CREATE}>
153-
<div className="whitespace-nowrap inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-indigo-600 hover:bg-indigo-700">
153+
<div className="whitespace-nowrap inline-flex items-center justify-center px-4 py-2 border-2 border-indigo-500 rounded-md shadow-sm text-base font-medium text-indigo-500 bg-white hover:bg-indigo-500 hover:text-white">
154154
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
155155
{t('navBarCreateElection')}
156156
</div>

web/frontend/src/pages/About.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const About: FC = () => {
66

77
return (
88
<Fragment>
9-
<div className="w-[60rem] font-sans px-4 py-4">
9+
<div className="w-[60rem] font-sans px-4 py-4 text-justify">
1010
<div>
1111
<br />
1212
{t('about1')}

web/frontend/src/pages/ballot/New.tsx

-1
This file was deleted.

web/frontend/src/pages/ballot/Show.tsx

+51-79
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,22 @@ import kyber from '@dedis/kyber';
55
import PropTypes from 'prop-types';
66
import { Buffer } from 'buffer';
77

8+
import SpinnerIcon from 'components/utils/SpinnerIcon';
9+
import { MailIcon } from '@heroicons/react/outline';
10+
import { useNavigate } from 'react-router';
11+
812
import useElection from 'components/utils/useElection';
913
import usePostCall from 'components/utils/usePostCall';
1014
import * as endpoints from 'components/utils/Endpoints';
1115
import { encryptVote } from './components/VoteEncrypt';
1216
import { voteEncode } from './components/VoteEncode';
1317
import { useConfiguration } from 'components/utils/useConfiguration';
14-
import * as types from 'types/configuration';
15-
import { ID, RANK, SELECT, SUBJECT, TEXT } from 'types/configuration';
16-
import { DragDropContext } from 'react-beautiful-dnd';
17-
import RedirectToModal from 'components/modal/RedirectToModal';
18-
import Select from './components/Select';
19-
import Rank, { handleOnDragEnd } from './components/Rank';
20-
import Text from './components/Text';
21-
import { ballotIsValid } from './components/ValidateAnswers';
2218
import { Status } from 'types/election';
19+
import { ballotIsValid } from './components/ValidateAnswers';
20+
import BallotDisplay from './components/BallotDisplay';
2321
import ElectionClosed from './components/ElectionClosed';
2422
import Loading from 'pages/Loading';
25-
import { CloudUploadIcon } from '@heroicons/react/solid';
26-
import SpinnerIcon from 'components/utils/SpinnerIcon';
23+
import RedirectToModal from 'components/modal/RedirectToModal';
2724

2825
const Ballot: FC = () => {
2926
const { t } = useTranslation();
@@ -33,6 +30,7 @@ const Ballot: FC = () => {
3330
const { loading, configObj, electionID, status, pubKey, ballotSize, chunksPerBallot } =
3431
useElection(electionId);
3532
const { configuration, answers, setAnswers } = useConfiguration(configObj);
33+
3634
const [userErrors, setUserErrors] = useState('');
3735
const edCurve = kyber.curve.newCurve('edwards25519');
3836
const [postError, setPostError] = useState('');
@@ -42,6 +40,8 @@ const Ballot: FC = () => {
4240
const [castVoteLoading, setCastVoteLoading] = useState(false);
4341
const sendFetchRequest = usePostCall(setPostError);
4442

43+
const navigate = useNavigate();
44+
4545
useEffect(() => {
4646
if (postError !== null) {
4747
if (postError.includes('ECONNREFUSED')) {
@@ -115,72 +115,8 @@ const Ballot: FC = () => {
115115
sendBallot();
116116
};
117117

118-
const SubjectElementDisplay = (element: types.SubjectElement) => {
119-
return (
120-
<div className="pl-4">
121-
{element.Type === RANK && <Rank rank={element as types.RankQuestion} answers={answers} />}
122-
{element.Type === SELECT && (
123-
<Select
124-
select={element as types.SelectQuestion}
125-
answers={answers}
126-
setAnswers={setAnswers}
127-
/>
128-
)}
129-
{element.Type === TEXT && (
130-
<Text text={element as types.TextQuestion} answers={answers} setAnswers={setAnswers} />
131-
)}
132-
</div>
133-
);
134-
};
135-
136-
const SubjectTree = (subject: types.Subject) => {
137-
return (
138-
<div className="sm:px-8 pl-2" key={subject.ID}>
139-
<h3 className="text-lg font-bold text-gray-600">{subject.Title}</h3>
140-
{subject.Order.map((id: ID) => (
141-
<div key={id}>
142-
{subject.Elements.get(id).Type === SUBJECT ? (
143-
<div>{SubjectTree(subject.Elements.get(id) as types.Subject)}</div>
144-
) : (
145-
SubjectElementDisplay(subject.Elements.get(id))
146-
)}
147-
</div>
148-
))}
149-
</div>
150-
);
151-
};
152-
153-
const ballotDisplay = () => {
154-
return (
155-
<DragDropContext onDragEnd={(dropRes) => handleOnDragEnd(dropRes, answers, setAnswers)}>
156-
<div className="shadow-lg rounded-md my-0 sm:my-4 py-8 w-full">
157-
<h3 className="font-bold uppercase py-4 text-2xl text-center text-gray-600">
158-
{configuration.MainTitle}
159-
</h3>
160-
<div>
161-
{configuration.Scaffold.map((subject: types.Subject) => SubjectTree(subject))}
162-
<div className="sm:mx-8 mx-4 text-red-600 text-sm pt-3 pb-5">{userErrors}</div>
163-
<div className="flex sm:mx-8 mx-4">
164-
<button
165-
type="button"
166-
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-500 hover:bg-indigo-600"
167-
onClick={handleClick}>
168-
{castVoteLoading ? (
169-
<SpinnerIcon />
170-
) : (
171-
<CloudUploadIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
172-
)}
173-
{t('castVote')}
174-
</button>
175-
</div>
176-
</div>
177-
</div>
178-
</DragDropContext>
179-
);
180-
};
181-
182118
return (
183-
<div>
119+
<>
184120
<RedirectToModal
185121
showModal={showModal}
186122
setShowModal={setShowModal}
@@ -192,12 +128,48 @@ const Ballot: FC = () => {
192128
{loading ? (
193129
<Loading />
194130
) : (
195-
<div>
196-
{status === Status.Open && ballotDisplay()}
131+
<>
132+
{status === Status.Open && (
133+
<div className="w-[60rem] font-sans px-4 pt-8 pb-4">
134+
<div className="pb-2">
135+
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
136+
{t('vote')}
137+
</h2>
138+
<div className="mt-2 text-sm text-gray-500">{t('voteExplanation')}</div>
139+
</div>
140+
141+
<BallotDisplay
142+
configuration={configuration}
143+
answers={answers}
144+
setAnswers={setAnswers}
145+
userErrors={userErrors}
146+
/>
147+
148+
<div className="flex mb-4 sm:mb-6">
149+
<button
150+
type="button"
151+
className="inline-flex items-center mr-2 sm:mr-4 px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-500 hover:bg-indigo-600"
152+
onClick={handleClick}>
153+
{castVoteLoading ? (
154+
<SpinnerIcon />
155+
) : (
156+
<MailIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
157+
)}
158+
{t('castVote')}
159+
</button>
160+
<button
161+
type="button"
162+
onClick={() => navigate(-1)}
163+
className=" text-gray-700 mr-2 items-center px-4 py-2 border rounded-md text-sm hover:text-indigo-500">
164+
{t('back')}
165+
</button>
166+
</div>
167+
</div>
168+
)}
197169
{status !== Status.Open && <ElectionClosed />}
198-
</div>
170+
</>
199171
)}
200-
</div>
172+
</>
201173
);
202174
};
203175

0 commit comments

Comments
 (0)