diff --git a/public/locale/i18n/de/common.json b/public/locale/i18n/de/common.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/locale/i18n/de/common.json @@ -0,0 +1 @@ +{} diff --git a/public/locale/i18n/de/resource.json b/public/locale/i18n/de/resource.json index 2502ddb..9b24372 100644 --- a/public/locale/i18n/de/resource.json +++ b/public/locale/i18n/de/resource.json @@ -53,4 +53,25 @@ "Support us !" : "Unterstützen Sie uns!", "PayPal - The safer, easier way to pay online!" : "PayPal - Die sicherere und einfachere Art, online zu bezahlen!", "Number of votes:" : "Anzahl der Stimmen:" + "Unknown error. Try again please.": "__STRING_NOT_TRANSLATED__", + "Ending date:": "__STRING_NOT_TRANSLATED__", + "If you list voters' emails, only them will be able to access the election": "__STRING_NOT_TRANSLATED__", + "Dates": "__STRING_NOT_TRANSLATED__", + "The election will take place from": "__STRING_NOT_TRANSLATED__", + "at": "__STRING_NOT_TRANSLATED__", + "to": "__STRING_NOT_TRANSLATED__", + "Grades": "__STRING_NOT_TRANSLATED__", + "Voters' list": "__STRING_NOT_TRANSLATED__", + "Voters received a link to vote by email. Each link can be used only once!": "__STRING_NOT_TRANSLATED__", + "Results of the election:": "__STRING_NOT_TRANSLATED__", + "Graph": "__STRING_NOT_TRANSLATED__", + "Preference profile": "__STRING_NOT_TRANSLATED__", + "Oops... The election is unknown.": "__STRING_NOT_TRANSLATED__", + "The election is still going on. You can't access now to the results.": "__STRING_NOT_TRANSLATED__", + "No votes have been recorded yet. Come back later.": "__STRING_NOT_TRANSLATED__", + "The election has not started yet.": "__STRING_NOT_TRANSLATED__", + "The election is over. You can't vote anymore": "__STRING_NOT_TRANSLATED__", + "You need a token to vote in this election": "__STRING_NOT_TRANSLATED__", + "You seem to have already voted.": "__STRING_NOT_TRANSLATED__", + "The parameters of the election are incorrect.": "__STRING_NOT_TRANSLATED__", } diff --git a/src/Util.jsx b/src/Util.jsx index e3efb93..d3f5c31 100644 --- a/src/Util.jsx +++ b/src/Util.jsx @@ -1,33 +1,33 @@ import i18n from './i18n.jsx'; const colors = [ - "#015411", - "#019812", - "#6bca24", - "#ffb200", - "#ff5d00", - "#b20616", - "#6f0214" + '#6f0214', + '#b20616', + '#ff5d00', + '#ffb200', + '#6bca24', + '#019812', + '#015411', ]; const gradeNames = [ - "Excellent", - "Very good", - "Good", - "Fair", - "Passable", - "Insufficient", - "To reject", + 'To reject', + 'Insufficient', + 'Passable', + 'Fair', + 'Good', + 'Very good', + 'Excellent', ]; export const grades = gradeNames.map((name, i) => ({ label: name, - color: colors[i] + color: colors[i], })); export const i18nGrades = () => { return gradeNames.map((name, i) => ({ label: i18n.t(name), - color: colors[i] - })); + color: colors[i], + })); }; diff --git a/src/components/layouts/Footer.jsx b/src/components/layouts/Footer.jsx index 0cdfa2f..d28f34a 100644 --- a/src/components/layouts/Footer.jsx +++ b/src/components/layouts/Footer.jsx @@ -1,4 +1,4 @@ -import React, { Component } from "react"; +import React, { Component, Fragment } from "react"; import {withTranslation} from 'react-i18next'; import { Link } from "react-router-dom"; import Paypal from "../banner/Paypal"; @@ -23,6 +23,17 @@ class Footer extends Component { {t("Source code")} - {t("Who are we?")} + - + { + countries.map(({l, flag}, i) => ( + + + {" "} + + )) + }
diff --git a/src/components/views/CreateElection.jsx b/src/components/views/CreateElection.jsx index e5c3773..d6559da 100644 --- a/src/components/views/CreateElection.jsx +++ b/src/components/views/CreateElection.jsx @@ -40,6 +40,11 @@ import ButtonWithConfirm from '../form/ButtonWithConfirm'; import Loader from '../wait'; import i18n from '../../i18n' + +// Error messages +const AT_LEAST_2_CANDIDATES_ERROR = 'Please add at least 2 candidates.' +const NO_TITLE_ERROR = 'Please add a title.' + // Convert a Date object into YYYY-MM-DD const dateToISO = date => date.toISOString().substring(0, 10); @@ -157,7 +162,6 @@ class CreateElection extends Component { this.state = { candidates: [{label: ''}, {label: ''}], - numCandidatesWithLabel: 0, title: title || '', isVisibleTipsDragAndDropCandidate: true, numGrades: 7, @@ -213,7 +217,6 @@ class CreateElection extends Component { }); this.setState({ candidates: candidates, - numCandidatesWithLabel: numLabels, }); }; @@ -242,6 +245,28 @@ class CreateElection extends Component { this.setState({isAdvancedOptionsOpen: !this.state.isAdvancedOptionsOpen}); }; + checkFields() { + const { candidates, title } = this.state; + + if (!candidates) { + return {ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR}; + } + + let numCandidates = 0; + candidates.forEach(c => { + if (c !== "") numCandidates += 1; + }) + if (numCandidates < 2) { + return {ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR}; + } + + if (!title || title === "") { + return {ok: false, msg: NO_TITLE_ERROR}; + } + + return {ok: true, msg: "OK"}; + } + handleSubmit() { const { candidates, @@ -259,6 +284,15 @@ class CreateElection extends Component { const {t} = this.props; const locale=i18n.language.substring(0,2).toLowerCase()==="fr"?"fr":"en"; + + const check = this.checkFields(); + if (!check.ok) { + toast.error(t(check.msg), { + position: toast.POSITION.TOP_CENTER, + }); + return + } + this.setState({waiting: true}); fetch(endpoint, { @@ -301,9 +335,9 @@ class CreateElection extends Component { .catch(error => error); } - handleSendWithoutCandidate = () => { + handleSendNotReady = (msg) => { const {t} = this.props; - toast.error(t('Please add at least 2 candidates.'), { + toast.error(t(msg), { position: toast.POSITION.TOP_CENTER, }); }; @@ -319,12 +353,12 @@ class CreateElection extends Component { candidates, numGrades, isAdvancedOptionsOpen, - numCandidatesWithLabel, - electorEmails + electorEmails, } = this.state; const {t} = this.props; const grades = i18nGrades(); + const check = this.checkFields(); if (successCreate) return ; @@ -579,7 +613,7 @@ class CreateElection extends Component { - {numCandidatesWithLabel >= 2 ? ( + {check.ok ? ( diff --git a/src/components/views/CreateSuccess.jsx b/src/components/views/CreateSuccess.jsx index c798c98..910c623 100644 --- a/src/components/views/CreateSuccess.jsx +++ b/src/components/views/CreateSuccess.jsx @@ -85,6 +85,7 @@ class CreateSuccess extends Component {

{t('Keep these links carefully')} +

diff --git a/src/components/views/Result.jsx b/src/components/views/Result.jsx index 450ed9a..90d07ed 100644 --- a/src/components/views/Result.jsx +++ b/src/components/views/Result.jsx @@ -1,7 +1,7 @@ -import React, { Component } from "react"; -import { Redirect } from "react-router-dom"; -import { withTranslation } from "react-i18next"; -import { resolve } from "url"; +import React, {Component} from 'react'; +import {Redirect} from 'react-router-dom'; +import {withTranslation} from 'react-i18next'; +import {resolve} from 'url'; import { Container, Row, @@ -10,11 +10,20 @@ import { Card, CardHeader, CardBody, - Table -} from "reactstrap"; -import { i18nGrades } from "../../Util"; -import { AppContext } from "../../AppContext"; -import { errorMessage, Error } from "../../Errors"; + Table, +} from 'reactstrap'; +import {i18nGrades} from '../../Util'; +import {AppContext} from '../../AppContext'; +import {errorMessage, Error} from '../../Errors'; + +const meritProfileFromVotes = votes => { + const numGrades = Math.max(...votes) - Math.min(...votes); + const profile = Array(numGrades).fill(0); + votes.forEach(vote => { + profile[vote] += 1; + }); + return profile; +}; class Result extends Component { static contextType = AppContext; @@ -34,8 +43,7 @@ class Result extends Component { collapseGraphics: false, collapseProfiles: false, electionGrades: i18nGrades(), - errorMessage: "", - numVotes:"..." + errorMessage: '', }; } @@ -43,7 +51,7 @@ class Result extends Component { if (!response.ok) { response.json().then(response => { this.setState(state => ({ - errorMessage: errorMessage(response, this.props.t) + errorMessage: errorMessage(response, this.props.t), })); }); throw Error(response); @@ -57,24 +65,21 @@ class Result extends Component { name: c.name, profile: c.profile, grade: c.grade, - score: c.score, - numVotes: c.num_votes })); - console.log(response); - this.setState(state => ({ candidates: candidates, numVotes : candidates[0].numVotes })); + this.setState(state => ({candidates: candidates})); return response; }; detailsToState = response => { const numGrades = response.num_grades; const colSizeGradeLg = Math.floor( - (12 - this.state.colSizeCandidateLg) / numGrades + (12 - this.state.colSizeCandidateLg) / numGrades, ); const colSizeGradeMd = Math.floor( - (12 - this.state.colSizeCandidateMd) / numGrades + (12 - this.state.colSizeCandidateMd) / numGrades, ); const colSizeGradeXs = Math.floor( - (12 - this.state.colSizeCandidateXs) / numGrades + (12 - this.state.colSizeCandidateXs) / numGrades, ); this.setState(state => ({ title: response.title, @@ -94,7 +99,7 @@ class Result extends Component { 12 - colSizeGradeXs * numGrades > 0 ? 12 - colSizeGradeXs * numGrades : 12, - electionGrades: i18nGrades().slice(0, numGrades) + electionGrades: i18nGrades().slice(0, numGrades), })); return response; }; @@ -102,39 +107,38 @@ class Result extends Component { componentDidMount() { // get details of the election const electionSlug = this.props.match.params.slug; - if (electionSlug === "dev") { + if (electionSlug === 'dev') { const dataTest = [ { - name: "BB", + name: 'BB', id: 1, score: 1.0, profile: [1, 1, 0, 0, 0, 0, 0], - grade: 1 + grade: 1, }, { - name: "CC", + name: 'CC', id: 2, score: 1.0, profile: [0, 0, 2, 0, 0, 0, 0], - grade: 2 + grade: 2, }, { - name: "AA", + name: 'AA', id: 0, score: 1.0, profile: [1, 1, 0, 0, 0, 0, 0], - grade: 1 - } + grade: 1, + }, ]; - this.setState({ candidates: dataTest }); - console.log(this.state.candidates); + this.setState({candidates: dataTest}); } else { const detailsEndpoint = resolve( this.context.urlServer, this.context.routesServer.getElection.replace( - new RegExp(":slug", "g"), - electionSlug - ) + new RegExp(':slug', 'g'), + electionSlug, + ), ); fetch(detailsEndpoint) @@ -147,9 +151,9 @@ class Result extends Component { const resultsEndpoint = resolve( this.context.urlServer, this.context.routesServer.getResultsElection.replace( - new RegExp(":slug", "g"), - electionSlug - ) + new RegExp(':slug', 'g'), + electionSlug, + ), ); fetch(resultsEndpoint) @@ -161,30 +165,30 @@ class Result extends Component { } toggleGraphics = () => { - this.setState(state => ({ collapseGraphics: !state.collapseGraphics })); + this.setState(state => ({collapseGraphics: !state.collapseGraphics})); }; toggleProfiles = () => { - this.setState(state => ({ collapseProfiles: !state.collapseProfiles })); + this.setState(state => ({collapseProfiles: !state.collapseProfiles})); }; render() { - const { errorMessage, candidates, electionGrades } = this.state; - const { t } = this.props; + const {errorMessage, candidates, electionGrades} = this.state; + const {t} = this.props; const grades = i18nGrades(); - - if (errorMessage && errorMessage !== "") { + + if (errorMessage && errorMessage !== '') { return ; } - let totalOfVote = 0; - - //based on the first candidate - if (candidates.length > 0) { - candidates[0].profile.map((value, i) => (totalOfVote += value)); - } else { - totalOfVote = 1; - } + const sum = seq => Object.values(seq).reduce((a, b) => a + b, 0); + const numVotes = + candidates && candidates.length > 0 ? sum(candidates[0].profile) : 1; + const gradeIds = + candidates && candidates.length > 0 + ? Object.keys(candidates[0].profile) + : []; + console.log(gradeIds); return ( @@ -196,27 +200,30 @@ class Result extends Component { -

{t("Results of the election:")}

-
{t("Number of votes:")}{" "+this.state.numVotes}
-
+

{t('Results of the election:')}

+
+ + {t('Number of votes:')} + {' ' + numVotes} + +
+
    {candidates.map((candidate, i) => { - console.log(candidate); return (
  1. - {candidate.name} + {candidate.name} + color: '#fff', + }}> {grades[candidate.grade].label} - + {/* {(100 * candidate.score).toFixed(1)}% - + */}
  2. ); })} @@ -230,11 +237,10 @@ class Result extends Component {

    - {t("Graph")} + 'm-0 panel-title ' + + (this.state.collapseGraphics ? 'collapsed' : '') + }> + {t('Graph')}

    @@ -242,27 +248,26 @@ class Result extends Component {
    - +
    {candidates.map((candidate, i) => { return ( - + {/*candidate.label*/}
    {i + 1}{i + 1} - +
    - {candidate.profile.map((value, i) => { + {gradeIds.map((id, i) => { + const value = candidate.profile[id]; if (value > 0) { let percent = - Math.round( - (value * 100) / totalOfVote - ) + "%"; + (value * 100) / numVotes + '%'; if (i === 0) { - percent = "auto"; + percent = 'auto'; } return ( ); @@ -295,7 +299,7 @@ class Result extends Component { {candidates.map((candidate, i) => { return ( - {i > 0 ? ", " : ""} + {i > 0 ? ', ' : ''} {i + 1}: {candidate.name} ); @@ -311,9 +315,8 @@ class Result extends Component { className="badge badge-light mr-2 mt-2" style={{ backgroundColor: grade.color, - color: "#fff" - }} - > + color: '#fff', + }}> {grade.label} ); @@ -331,11 +334,10 @@ class Result extends Component {

    - {t("Preference profile")} + 'm-0 panel-title ' + + (this.state.collapseProfiles ? 'collapsed' : '') + }> + {t('Preference profile')}

    @@ -352,10 +354,9 @@ class Result extends Component { className="badge badge-light" style={{ backgroundColor: grade.color, - color: "#fff" - }} - > - {grade.label}{" "} + color: '#fff', + }}> + {grade.label}{' '} ); @@ -367,13 +368,13 @@ class Result extends Component { return ( - {/*candidate.label*/} - {candidate.profile.map((value, i) => { - let percent = - Math.round( - ((value * 100) / totalOfVote) * 100 - ) / 100; - return ; + {gradeIds.map((id, i) => { + const value = candidate.profile[id]; + const percent = ( + (value / numVotes) * + 100 + ).toFixed(1); + return ; })} ); @@ -385,7 +386,7 @@ class Result extends Component { {candidates.map((candidate, i) => { return ( - {i > 0 ? ", " : ""} + {i > 0 ? ', ' : ''} {i + 1}: {candidate.name} ); diff --git a/src/components/views/Vote.jsx b/src/components/views/Vote.jsx index 6ceca6a..30e38e5 100644 --- a/src/components/views/Vote.jsx +++ b/src/components/views/Vote.jsx @@ -190,13 +190,13 @@ class Vote extends Component { lg={this.state.colSizeCandidateLg}>
     
    - {electionGrades.map((grade, j) => { - return j < this.state.numGrades ? ( + {electionGrades.map((grade, gradeId) => { + return gradeId < this.state.numGrades ? ( - {candidates.map((candidate, i) => { + {candidates.map((candidate, candidateId) => { return ( - + {candidate.label}
    - {this.state.electionGrades.map((grade, j) => { - return j < this.state.numGrades ? ( + {this.state.electionGrades.map((grade, gradeId) => { + console.assert(gradeId < this.state.numGrades) + return (
    - ) : null; + + ) })} );
    + .electionGrades[i].color, + }}>  
    {i + 1}{percent}%{percent} %