Merge remote-tracking branch 'origin/master' into feature/clement-release

# Conflicts:
#	public/locale/i18n/es/resource.json
#	src/components/layouts/Footer.jsx
#	src/components/views/CreateElection.jsx
#	src/components/views/CreateSuccess.jsx
pull/73/head
Clement G 4 years ago
commit 429c0f755b

@ -9,8 +9,11 @@ jobs:
name: update-yarn
command: 'curl --compressed -o- -L https://yarnpkg.com/install.sh | bash'
- run:
name: install-yarn
name: install-dependencies
command: yarn install
- run:
name: compile-translations
command: yarn translate
- run:
name: test
command: yarn test

@ -15,14 +15,20 @@ $ cd mvfront-react
$ yarn install
```
## Translation
We are welcoming translations of the application in any language.
To add a new language, copy a [language folder](./public/locale/i18n/en/) into a new folder with your language as a name.
Then, replace values in the JSON files.
To compile them, launch: `$ yarn translate`.
## Starting
In dev:
`yarn start`
In development, you might want to copy `.env` into `.env.local` and set the environment variables. Then launch `$ yarn start`
For production, see our [CI/CD configuration](https://github.com/MieuxVoter/continuous-integration).
## Testing
`yarn test`
Launch `$ yarn test`

@ -30,7 +30,7 @@ module.exports = {
// Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
}
},
lngs: ["en", "fr", "es"],
lngs: ["en", "fr", "es", "de"],
ns: ["resource", "common"],
defaultLng: "en",
defaultNs: "resource",

@ -1 +0,0 @@
i18next-scanner --config i18n.config.js 'src/**/*.{js,jsx}'

@ -85,7 +85,8 @@
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
"test": "node scripts/test.js",
"translate": "i18next-scanner --config i18n.config.js 'src/**/*.{js,jsx}'"
},
"eslintConfig": {
"extends": "react-app"

@ -0,0 +1,53 @@
{
"Homepage": " Homepage ",
"Source code": "Quellcode",
"Who are we": "Wer wir sind",
"BetterVote": " BetterVote",
"Voting platform": "Wahlplattform",
"Majority Judgment": " Mehrheitswahl ",
"Start an election": "Eine Wahl beginnen",
"Candidate/proposal name...": "Name des Kandidaten/Abstimmungsvorschlags",
"Delete?": "Löschen?",
"Are you sure to delete": "Sind Sie sich sicher, dass Sie dies löschen möchten?",
"the row": "die Zeile",
"Write here your question or introduce simple your election (250 characters max.)": "Schreiben Sie hier Ihre Frage oder erklären Sie kurz ihre Wahl (bis 250 Zeichen)",
"Please add at least 2 candidates.": "Bitte geben Sie mindestens zwei Kandidaten vor. ",
"Question of the election": "Zur Wahl stehende Frage",
"Write here the question of your election": "Schreiben Sie hier die zur Wahl stehenden Frage",
"For example:": "Zum Beispiel",
"For the role of my representative, I judge this candidate...": "Meine Einschätzung des Kandidaten als meinen Repräsentanten ist …",
"Candidates/Proposals": "Kandidaten/Abstimmungsvorschlag ",
"Add a proposal": "Einen Abstimmungsvorschlag hinzufügen",
"Advanced options": "Weitere Optionen",
"Starting date:": "Anfangsdatum:",
"Ending date: ": "Enddatum: ",
"Grades:": "Note",
"You can select here the number of grades for your election": " Sie können hier die Anzahl der Noten für Ihre Wahl auswählen ",
"5 = Excellent, Very good, Good, Fair, Passable": "5 = hervorragend, sehr gut, gut, befriedigend, ausreichend",
"Participants:": "Teilnehmer :",
"Add here participants' emails": "Fügen Sie hier die Email Adressen der Teilnehmer hinzu.",
"List voters' emails in case the election is not opened": "Falls die Wahl noch nicht sofort geöffnet werden soll, fügen Sie die Email Adressen der Teilnehmer hier zu.",
"Validate": "Ok",
"Confirm your vote": "Bestätigen Sie Ihre Wahl",
"The form contains no address.": "Keine Email Adresse wurde hinzugefügt.",
"The election will be opened to anyone with the link": "Die Wahl ist offen afür jeden, der diesen Link hat.",
"Start the election": "Mit der Wahl beginnen.",
"Cancel": "Abbrechen",
"Confirm": "Ok",
"Successful election creation!": "Die Wahl wurde erfolgreich erstellt!",
"You can now share the election link to participants:": "Sie können nun den Teilnehmern den Link zukommen lassen.",
"Copy": "Kopieren",
"Here is the link for the results in real time:": " Hier ist der Link für die Ergebnisse in Echtzeit:",
"Keep these links carefully": "Speichern Sie diesen Link an einem sicheren Ort.",
"Participate now!": "Machen Sie jetzt mit!",
"t": "<0>Achtung</0> : Sie werden zu einem späteren Zeitpunkt keine Möglichkeit haben, diese Links abzurufen, auch wir haben keinen Zugriff darauf. Sie können aber beispielsweise diese Seite in Ihrem Browser als Lesezeichen speichern.",
"Simple and free: organize an election with Majority Judgment.": "Einfach und kostenlos: Organisation von Mehrheitswahlen.",
"Start": "Start",
"No advertising or ad cookies": "Keine Werbung und auch keine Cookies zu Werbezwecken.",
"Oops! This election does not exist or it is not available anymore.": "Ups! Diese Wahl existiert nicht oder ist nicht mehr verfügbar. ",
"You can start another election.": "Sie können eine neue Umfrage starten.",
"Go back to homepage": "Zurück zur Hompage",
"You have to judge every candidate/proposal!": "Sie müssen jeden Kandidaten/Abstimmungsvorschlag bewerten!",
"Your participation was recorded with success!": " Ihre Teilnahme wurde gespeichert!",
"Thanks for your participation.": " Vielen Dank für Ihre Teilnahme."
}

@ -66,5 +66,16 @@
"Voters' list": "Voters' list",
"Graph": "Graph",
"Preference profile": "Merit profile",
"Results of the election:": "Results of the election:"
"Results of the election:": "Results of the election:",
"Unknown error. Try again please.": "Unknown error. Try again please.",
"If you list voters' emails, only them will be able to access the election": "If you list voters' emails, only them will be able to access the election",
"Voters received a link to vote by email. Each link can be used only once!": "Voters received a link to vote by email. Each link can be used only once!",
"Oops... The election is unknown.": "Oops... The election is unknown.",
"The election is still going on. You can't access now to the results.": "The election is still going on. You can't access now to the results.",
"No votes have been recorded yet. Come back later.": "No votes have been recorded yet. Come back later.",
"The election has not started yet.": "The election has not started yet.",
"The election is over. You can't vote anymore": "The election is over. You can't vote anymore",
"You need a token to vote in this election": "You need a token to vote in this election",
"You seem to have already voted.": "You seem to have already voted.",
"The parameters of the election are incorrect.": "The parameters of the election are incorrect."
}

@ -66,5 +66,16 @@
"Voters' list": "Lista de votantes",
"Graph": "Gráfico",
"Preference profile": "Perfil de preferencia",
"Results of the election:": "Resultados de la elección"
"Results of the election:": "Resultados de la elección",
"Unknown error. Try again please.": "__STRING_NOT_TRANSLATED__",
"If you list voters' emails, only them will be able to access the election": "__STRING_NOT_TRANSLATED__",
"Voters received a link to vote by email. Each link can be used only once!": "__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__"
}

@ -66,5 +66,16 @@
"Voters' list": "Listes des électeurs",
"Graph": "Graphique",
"Preference profile": "Profil de mérites",
"Results of the election:": "Résultats de l'élection"
"Results of the election:": "Résultats de l'élection",
"Unknown error. Try again please.": "Erreur inconnue. Merci de ré-essayer plus tard.",
"If you list voters' emails, only them will be able to access the election": "Si vous ajoutez des emails, seulement ceux-là seront capables d'accéder à l'élection",
"Voters received a link to vote by email. Each link can be used only once!": "Les électeurs ont reçu un lien par courriel pour voter. Chaque lien ne peut être utilisé qu'une seule fois.",
"Oops... The election is unknown.": "Oups... Le serveur ne retrouve pas l'élection.",
"The election is still going on. You can't access now to the results.": "L'élection est encore en cours. Vous ne pouvez pas encore accéder aux résultats.",
"No votes have been recorded yet. Come back later.": "Aucun vote n'a été enregistré. Merci de revenir plus tard.",
"The election has not started yet.": "L'élection n'a pas encore commencé.",
"The election is over. You can't vote anymore": "L'élection est terminée. Vous ne pouvez plus voter.",
"You need a token to vote in this election": "Vous avez besoin d'un jeton pour voter dans cette élection",
"You seem to have already voted.": "Il semble que vous ayez déjà voté.",
"The parameters of the election are incorrect.": "Les paramètres de l'élection sont incorrects."
}

@ -1,7 +1,4 @@
import React, {Suspense} from 'react';
import {BrowserRouter as Router} from 'react-router-dom';
import Loader from './components/loader';
import Routes from './Routes';
import Header from './components/layouts/Header';
@ -10,17 +7,13 @@ import AppContextProvider from './AppContext';
function App() {
return (
<Suspense fallback={<Loader/>} >
<AppContextProvider>
<Router>
<div>
<Header />
<Routes />
<Footer />
</div>
</Router>
</AppContextProvider>
</Suspense>
);
}

@ -1,4 +1,7 @@
import React, { createContext } from "react";
import React, { createContext, Suspense } from "react";
import {BrowserRouter as Router} from 'react-router-dom';
import Loader from './components/loader';
export const AppContext = createContext();
@ -13,7 +16,13 @@ const AppContextProvider = ({ children }) => {
}
};
return (
<AppContext.Provider value={defaultState}>{children}</AppContext.Provider>
<Suspense fallback={<Loader/>} >
<Router>
<AppContext.Provider value={defaultState}>
{children}
</AppContext.Provider>
</Router>
</Suspense>
);
};
export default AppContextProvider;

@ -0,0 +1,50 @@
import React from 'react';
import {
Container,
Row,
Col,
} from "reactstrap";
export const UNKNOWN_ELECTION_ERROR = 'E1';
export const ONGOING_ELECTION_ERROR = 'E2';
export const NO_VOTE_ERROR = 'E3';
export const ELECTION_NOT_STARTED_ERROR = 'E4';
export const ELECTION_FINISHED_ERROR = 'E5';
export const INVITATION_ONLY_ERROR = 'E6';
export const UNKNOWN_TOKEN_ERROR = 'E7';
export const USED_TOKEN_ERROR = 'E8';
export const WRONG_ELECTION_ERROR = 'E9';
export const redirectError = (errorMsg, history) => {};
export const errorMessage = (error, t) => {
if (error.startsWith(UNKNOWN_ELECTION_ERROR)) {
return t('Oops... The election is unknown.');
} else if (error.startsWith(ONGOING_ELECTION_ERROR)) {
return t(
"The election is still going on. You can't access now to the results.",
);
} else if (error.startsWith(NO_VOTE_ERROR)) {
return t('No votes have been recorded yet. Come back later.');
} else if (error.startsWith(ELECTION_NOT_STARTED_ERROR)) {
return t('The election has not started yet.');
} else if (error.startsWith(ELECTION_FINISHED_ERROR)) {
return t('The election is over. You can\'t vote anymore');
} else if (error.startsWith(INVITATION_ONLY_ERROR)) {
return t('You need a token to vote in this election');
} else if (error.startsWith(USED_TOKEN_ERROR)) {
return t('You seem to have already voted.');
} else if (error.startsWith(WRONG_ELECTION_ERROR)) {
return t('The parameters of the election are incorrect.');
}
};
export const Error = (props) => (
<Container>
<Row>
<Col xs="12">
<h1>{props.value}</h1>
</Col>
</Row>
</Container>
);

@ -19,7 +19,8 @@ function Routes() {
<Route path="/create-election" component={CreateElection} />
<Route path="/vote/:slug" component={Vote} />
<Route path="/result/:slug" component={Result} />
<Route path="/create-success/:slug" component={CreateSuccess} />
<Route path="/link/:slug" component={props => <CreateSuccess invitationOnly={true} {...props} />} />
<Route path="/links/:slug" component={props => <CreateSuccess invitationOnly={false} {...props} />} />
<Route path="/vote-success/:slug" component={VoteSuccess} />
<Route path="/unknown-election/:slug" component={UnknownElection} />
<Route component={UnknownView} />

@ -0,0 +1,45 @@
import React, {Component} from 'react';
import {Button} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
const CopyField = props => {
const ref = React.createRef();
const handleClickOnField = event => {
event.target.focus();
event.target.select();
};
const handleClickOnButton = event => {
const input = ref.current;
input.focus();
input.select();
document.execCommand('copy');
};
const {t, value, icon} = props;
return (
<div className="input-group ">
<input
type="text"
className="form-control"
ref={ref}
value={value}
readOnly
onClick={handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-outline-light"
onClick={handleClickOnButton}
type="button">
<FontAwesomeIcon icon={icon} className="mr-2" />
{t('Copy')}
</Button>
</div>
</div>
);
};
export default CopyField;

@ -17,7 +17,6 @@ class Footer extends Component {
render() {
const buttonStyle = {backgroundColor: "black", padding: "0px", border: "0px",};
const linkStyle = {whiteSpace: "nowrap"};
const {t} = this.props;
return (

@ -268,7 +268,7 @@ class CreateElection extends Component {
},
body: JSON.stringify({
title: title,
candidates: candidates.map(c => c.label),
candidates: candidates.map(c => c.label).filter( c => c !== ""),
on_invitation_only: electorEmails.length > 0,
num_grades: numGrades,
elector_emails: electorEmails,
@ -278,24 +278,27 @@ class CreateElection extends Component {
front_url : window.location.origin
}),
})
.then(response => response.json())
.then(result => {
console.log(result);
if (result.id) {
this.setState(state => ({
redirectTo: '/create-success/' + result.id,
successCreate: true,
waiting: false
}))
}
else {
toast.error(t('Unknown error. Try again please.'), {
position: toast.POSITION.TOP_CENTER,
});
this.setState({waiting: false});
}
})
.catch(error => error);
.then(response => response.json())
.then(result => {
console.log(result);
if (result.id) {
const nextPage =
electorEmails && electorEmails.length
? `/link/${result.id}`
: `/links/${result.id}`;
this.setState(state => ({
redirectTo: nextPage,
successCreate: true,
waiting: false,
}));
} else {
toast.error(t('Unknown error. Try again please.'), {
position: toast.POSITION.TOP_CENTER,
});
this.setState({waiting: false});
}
})
.catch(error => error);
}
handleSendWithoutCandidate = () => {
@ -557,88 +560,88 @@ class CreateElection extends Component {
onClick={() => removeEmail(index)}>
×
</span>
</div>
);
}}
/>
<div>
<small className="text-muted">
{t(
"List voters' emails in case the election is not opened",
)}
</small>
</div>
);
}}
/>
<div>
<small className="text-muted">
{t(
"If you list voters' emails, only them will be able to access the election",
)}
</small>
</div>
</Col>
</Row>
<hr className="mt-2 mb-2" />
</CardBody>
</Card>
</Collapse>
<Row className="justify-content-end mt-2">
<Col xs="12" md="3">
{numCandidatesWithLabel >= 2 ? (
<ButtonWithConfirm
className="btn btn-success float-right btn-block"
tabIndex={candidates.length + 4}>
<div key="button">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t('Validate')}
</div>
<div key="modal-title">{t('Confirm your vote')}</div>
<div key="modal-body">
<div className="mt-1 mb-1">
<div className="text-white bg-primary p-1">
{t('Question of the election')}
</div>
</Col>
</Row>
<hr className="mt-2 mb-2" />
</CardBody>
</Card>
</Collapse>
<Row className="justify-content-end mt-2">
<Col xs="12" md="3">
{numCandidatesWithLabel >= 2 ? (
<ButtonWithConfirm
className="btn btn-success float-right btn-block"
tabIndex={candidates.length + 4}>
<div key="button">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t('Validate')}
<div className="p-1 pl-3">
<em>{title}</em>
</div>
<div key="modal-title">{t('Confirm your vote')}</div>
<div key="modal-body">
<div className="mt-1 mb-1">
<div className="text-white bg-primary p-1">
{t('Question of the election')}
</div>
<div className="p-1 pl-3">
<em>{title}</em>
</div>
<div className="text-white bg-primary p-1">
{t('Candidates/Proposals')}
</div>
<div className="p-1 pl-0">
<ul className="m-0 pl-4">
{candidates.map((candidate, i) => {
if (candidate.label !== '') {
return (
<li key={i} className="m-0">
{candidate.label}
</li>
);
} else {
return <li key={i} className="d-none" />;
}
})}
</ul>
</div>
<div className="text-white bg-primary p-1 mt-1">
{t('Dates')}
</div>
<p className="p-1 pl-3">
{t('The election will take place from')}{' '}
<b>
{start.toLocaleDateString()}, {t('at')}{' '}
{start.toLocaleTimeString()}
</b>{' '}
{t('to')}{' '}
<b>
{finish.toLocaleDateString()}, {t('at')}{' '}
{finish.toLocaleTimeString()}
</b>
</p>
<div className="text-white bg-primary p-1">
{t('Grades')}
</div>
<div className="p-1 pl-3">
{grades.map((mention, i) => {
return i < numGrades ? (
<span
key={i}
className="badge badge-light mr-2 mt-2"
style={{
backgroundColor: mention.color,
color: '#fff',
}}>
<div className="text-white bg-primary p-1">
{t('Candidates/Proposals')}
</div>
<div className="p-1 pl-0">
<ul className="m-0 pl-4">
{candidates.map((candidate, i) => {
if (candidate.label !== '') {
return (
<li key={i} className="m-0">
{candidate.label}
</li>
);
} else {
return <li key={i} className="d-none" />;
}
})}
</ul>
</div>
<div className="text-white bg-primary p-1 mt-1">
{t('Dates')}
</div>
<p className="p-1 pl-3">
{t('The election will take place from')}{' '}
<b>
{start.toLocaleDateString()}, {t('at')}{' '}
{start.toLocaleTimeString()}
</b>{' '}
{t('to')}{' '}
<b>
{finish.toLocaleDateString()}, {t('at')}{' '}
{finish.toLocaleTimeString()}
</b>
</p>
<div className="text-white bg-primary p-1">
{t('Grades')}
</div>
<div className="p-1 pl-3">
{grades.map((mention, i) => {
return i < numGrades ? (
<span
key={i}
className="badge badge-light mr-2 mt-2"
style={{
backgroundColor: mention.color,
color: '#fff',
}}>
{mention.label}
</span>
) : (

@ -6,11 +6,16 @@ import {faCopy, faUsers, faExclamationTriangle} from '@fortawesome/free-solid-sv
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import logoLine from '../../logos/logo-line-white.svg';
import {AppContext} from '../../AppContext';
import CopyField from '../CopyField';
class CreateSuccess extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
console.log(props);
const electionSlug = this.props.match.params.slug;
this.state = {
urlOfVote:
@ -22,18 +27,6 @@ class CreateSuccess extends Component {
this.urlResultField = React.createRef();
}
handleClickOnField = event => {
event.target.focus();
event.target.select();
};
handleClickOnCopyVote = event => {
const input = this.urlVoteField.current;
input.focus();
input.select();
document.execCommand('copy');
};
handleClickOnCopyResult = event => {
const input = this.urlResultField.current;
input.focus();
@ -43,6 +36,26 @@ class CreateSuccess extends Component {
render() {
const {t} = this.props;
console.log(this.props)
const electionLink = this.props.invitationOnly ? (
<>
<p className="mt-4 mb-1">
{t('Voters received a link to vote by email. Each link can be used only once!')}
</p>
</>
) : (
<>
<p className="mt-4 mb-1">
{t('You can now share the election link to participants:')}
</p>
<CopyField
value={this.state.urlOfVote}
icon={faCopy}
t={t}
/>
</>
);
return (
<Container>
<Row>
@ -53,54 +66,17 @@ class CreateSuccess extends Component {
<Row className="mt-4">
<Col className="text-center offset-lg-3" lg="6">
<h2>{t('Successful election creation!')}</h2>
<p className="mt-4 mb-1">
{t('You can now share the election link to participants:')}
</p>
<div className="input-group ">
<input
type="text"
className="form-control"
ref={this.urlVoteField}
value={this.state.urlOfVote}
readOnly
onClick={this.handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-outline-light"
onClick={this.handleClickOnCopyVote}
type="button">
<FontAwesomeIcon icon={faCopy} className="mr-2" />
{t('Copy')}
</Button>
</div>
</div>
{electionLink}
<p className="mt-4 mb-1">
{t('Here is the link for the results in real time:')}
</p>
<div className="input-group ">
<input
type="text"
className="form-control"
ref={this.urlResultField}
value={this.state.urlOfResult}
readOnly
onClick={this.handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-outline-light"
onClick={this.handleClickOnCopyResult}
type="button">
<FontAwesomeIcon icon={faCopy} className="mr-2" />
{t('Copy')}
</Button>
</div>
</div>
<CopyField
value={this.state.urlOfResult}
icon={faCopy}
t={t}
/>
</Col>
</Row>
<Row className="mt-4 mb-4">
@ -127,7 +103,7 @@ class CreateSuccess extends Component {
to={'/vote/' + this.props.match.params.slug}
className="btn btn-success">
<FontAwesomeIcon icon={faUsers} className="mr-2" />
{t("Participate now!")}
{t('Participate now!')}
</Link>
</Col>
</Row>

@ -14,6 +14,7 @@ import {
} from "reactstrap";
import { i18nGrades } from "../../Util";
import { AppContext } from "../../AppContext";
import { errorMessage, Error } from "../../Errors";
class Result extends Component {
static contextType = AppContext;
@ -32,8 +33,8 @@ class Result extends Component {
colSizeGradeXs: 1,
collapseGraphics: false,
collapseProfiles: false,
redirectLost: false,
electionGrades: i18nGrades()
electionGrades: i18nGrades(),
errorMessage: "",
};
}
@ -41,7 +42,7 @@ class Result extends Component {
if (!response.ok) {
response.json().then(response => {
this.setState(state => ({
redirectLost: "/unknown-election/" + encodeURIComponent(response)
errorMessage: errorMessage(response, this.props.t)
}));
});
throw Error(response);
@ -167,12 +168,12 @@ class Result extends Component {
};
render() {
const { redirectLost, candidates, electionGrades } = this.state;
const { errorMessage, candidates, electionGrades } = this.state;
const { t } = this.props;
const grades = i18nGrades();
if (redirectLost) {
return <Redirect to={redirectLost} />;
if (errorMessage && errorMessage !== "") {
return <Error value={errorMessage} />;
}
let totalOfVote = 0;

@ -1,13 +1,16 @@
import React, { Component } from "react";
import { Redirect } from "react-router-dom";
import { withTranslation } from 'react-i18next';
import { Button, Col, Container, Row } from "reactstrap";
import { toast, ToastContainer } from "react-toastify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { resolve } from "url";
import { i18nGrades } from "../../Util";
import { AppContext } from "../../AppContext";
import React, {Component} from 'react';
import {Redirect} from 'react-router-dom';
import {withTranslation} from 'react-i18next';
import {Button, Col, Container, Row} from 'reactstrap';
import {toast, ToastContainer} from 'react-toastify';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons';
import {resolve} from 'url';
import {i18nGrades} from '../../Util';
import {AppContext} from '../../AppContext';
import {errorMessage} from '../../Errors';
const shuffle = array => array.sort(() => Math.random() - 0.5);
class Vote extends Component {
static contextType = AppContext;
@ -25,7 +28,7 @@ class Vote extends Component {
colSizeGradeMd: 1,
colSizeGradeXs: 1,
redirectTo: null,
electionGrades: i18nGrades()
electionGrades: i18nGrades(),
};
}
@ -33,9 +36,11 @@ class Vote extends Component {
if (!response.ok) {
response.json().then(response => {
console.log(response);
this.setState(state => ({
redirectTo: "/unknown-election/" + encodeURIComponent(response)
}));
const {t} = this.props;
toast.error(errorMessage(response, t));
// this.setState(state => ({
// redirectTo: "/unknown-election/" + encodeURIComponent(response)
// }));
});
throw Error(response);
}
@ -46,24 +51,18 @@ class Vote extends Component {
const numGrades = response.num_grades;
const candidates = response.candidates.map((c, i) => ({
id: i,
label: c
label: c,
}));
//shuffle candidates
let i, j, temp;
for (i = candidates.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = candidates[i];
candidates[i] = candidates[j];
candidates[j] = temp;
}
shuffle(candidates);
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 => ({
@ -85,7 +84,7 @@ class Vote extends Component {
12 - colSizeGradeXs * numGrades > 0
? 12 - colSizeGradeXs * numGrades
: 12,
electionGrades: i18nGrades().slice(0, numGrades)
electionGrades: i18nGrades().slice(0, numGrades),
}));
return response;
};
@ -96,9 +95,9 @@ class Vote extends Component {
const detailsEndpoint = resolve(
this.context.urlServer,
this.context.routesServer.getElection.replace(
new RegExp(":slug", "g"),
electionSlug
)
new RegExp(':slug', 'g'),
electionSlug,
),
);
fetch(detailsEndpoint)
.then(this.handleErrors)
@ -109,32 +108,33 @@ class Vote extends Component {
handleGradeClick = event => {
let data = {
id: parseInt(event.currentTarget.getAttribute("data-id")),
value: parseInt(event.currentTarget.value)
id: parseInt(event.currentTarget.getAttribute('data-id')),
value: parseInt(event.currentTarget.value),
};
//remove candidate
let ratedCandidates = this.state.ratedCandidates.filter(
ratedCandidate => ratedCandidate.id !== data.id
ratedCandidate => ratedCandidate.id !== data.id,
);
ratedCandidates.push(data);
this.setState({ ratedCandidates: ratedCandidates });
this.setState({ratedCandidates});
};
handleSubmitWithoutAllRate = () => {
const {t} = this.props;
toast.error(t("You have to judge every candidate/proposal!"), {
position: toast.POSITION.TOP_CENTER
toast.error(t('You have to judge every candidate/proposal!'), {
position: toast.POSITION.TOP_CENTER,
});
};
handleSubmit = event => {
event.preventDefault();
const { ratedCandidates } = this.state;
const {ratedCandidates} = this.state;
const electionSlug = this.props.match.params.slug;
const token = this.props.location.search.substr(7);
const endpoint = resolve(
this.context.urlServer,
this.context.routesServer.voteElection
this.context.routesServer.voteElection,
);
const gradesById = {};
@ -142,30 +142,33 @@ class Vote extends Component {
gradesById[c.id] = c.value;
});
const gradesByCandidate = [];
Object.keys(gradesById)
.sort()
.forEach(id => {
gradesByCandidate.push(gradesById[id]);
});
Object.keys(gradesById).forEach(id => {
gradesByCandidate.push(gradesById[id]);
});
const payload = {
election: electionSlug,
grades_by_candidate: gradesByCandidate,
};
if (token !== '') {
payload['token'] = token;
}
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
election: electionSlug,
grades_by_candidate: gradesByCandidate
})
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
})
.then(this.handleErrors)
.then(result =>
this.setState({ redirectTo: "/vote-success/" + electionSlug })
this.setState({redirectTo: '/vote-success/' + electionSlug}),
)
.catch(error => error);
};
render() {
const {t} = this.props;
const { redirectTo, candidates, electionGrades } = this.state;
const {redirectTo, candidates, electionGrades} = this.state;
if (redirectTo) {
return <Redirect to={redirectTo} />;
@ -184,8 +187,7 @@ class Vote extends Component {
<Col
xs={this.state.colSizeCandidateXs}
md={this.state.colSizeCandidateMd}
lg={this.state.colSizeCandidateLg}
>
lg={this.state.colSizeCandidateLg}>
<h5>&nbsp;</h5>
</Col>
{electionGrades.map((grade, j) => {
@ -196,12 +198,10 @@ class Vote extends Component {
lg={this.state.colSizeGradeLg}
key={j}
className="text-center p-0"
style={{ lineHeight: 2 }}
>
style={{lineHeight: 2}}>
<small
className="nowrap bold badge"
style={{ backgroundColor: grade.color, color: "#fff" }}
>
style={{backgroundColor: grade.color, color: '#fff'}}>
{grade.label}
</small>
</Col>
@ -215,8 +215,7 @@ class Vote extends Component {
<Col
xs={this.state.colSizeCandidateXs}
md={this.state.colSizeCandidateMd}
lg={this.state.colSizeCandidateLg}
>
lg={this.state.colSizeCandidateLg}>
<h5 className="m-0">{candidate.label}</h5>
<hr className="d-lg-none" />
</Col>
@ -227,36 +226,33 @@ class Vote extends Component {
md={this.state.colSizeGradeMd}
lg={this.state.colSizeGradeLg}
key={j}
className="text-lg-center"
>
className="text-lg-center">
<label
htmlFor={"candidateGrade" + i + "-" + j}
className="check"
>
htmlFor={'candidateGrade' + i + '-' + j}
className="check">
<small
className="nowrap d-lg-none ml-2 bold badge"
style={
this.state.ratedCandidates.find(function(
ratedCandidat
ratedCandidat,
) {
return (
JSON.stringify(ratedCandidat) ===
JSON.stringify({ id: candidate.id, value: j })
JSON.stringify({id: candidate.id, value: j})
);
})
? { backgroundColor: grade.color, color: "#fff" }
? {backgroundColor: grade.color, color: '#fff'}
: {
backgroundColor: "transparent",
color: "#000"
backgroundColor: 'transparent',
color: '#000',
}
}
>
}>
{grade.label}
</small>
<input
type="radio"
name={"candidate" + i}
id={"candidateGrade" + i + "-" + j}
name={'candidate' + i}
id={'candidateGrade' + i + '-' + j}
data-index={i}
data-id={candidate.id}
value={j}
@ -265,26 +261,26 @@ class Vote extends Component {
function(element) {
return (
JSON.stringify(element) ===
JSON.stringify({ id: candidate.id, value: j })
JSON.stringify({id: candidate.id, value: j})
);
}
},
)}
/>
<span
className="checkmark"
style={
this.state.ratedCandidates.find(function(
ratedCandidat
ratedCandidat,
) {
return (
JSON.stringify(ratedCandidat) ===
JSON.stringify({ id: candidate.id, value: j })
JSON.stringify({id: candidate.id, value: j})
);
})
? { backgroundColor: grade.color, color: "#fff" }
? {backgroundColor: grade.color, color: '#fff'}
: {
backgroundColor: "transparent",
color: "#000"
backgroundColor: 'transparent',
color: '#000',
}
}
/>
@ -303,15 +299,14 @@ class Vote extends Component {
<Button
type="button"
onClick={this.handleSubmitWithoutAllRate}
className="btn btn-dark "
>
className="btn btn-dark ">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Validate")}
{t('Validate')}
</Button>
) : (
<Button type="submit" className="btn btn-success ">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Validate")}
{t('Validate')}
</Button>
)}
</Col>

@ -0,0 +1,35 @@
import React from 'react';
export const UNKNOWN_ELECTION_ERROR = 'E1';
export const ONGOING_ELECTION_ERROR = 'E2';
export const NO_VOTE_ERROR = 'E3';
export const ELECTION_NOT_STARTED_ERROR = 'E4';
export const ELECTION_FINISHED_ERROR = 'E5';
export const INVITATION_ONLY_ERROR = 'E6';
export const UNKNOWN_TOKEN_ERROR = 'E7';
export const USED_TOKEN_ERROR = 'E8';
export const WRONG_ELECTION_ERROR = 'E9';
export const redirectError = (errorMsg, history) => {};
export const errorMessage = (error, t) => {
if (error.startsWith(UNKNOWN_ELECTION_ERROR)) {
return t('Oops... The election is unknown.');
} else if (error.startsWith(ONGOING_ELECTION_ERROR)) {
return t(
"The election is still going on. You can't access now to the results.",
);
} else if (error.startsWith(NO_VOTE_ERROR)) {
return t('No votes have been recorded yet. Come back later.');
} else if (error.startsWith(ELECTION_NOT_STARTED_ERROR)) {
return t('The election has not started yet.');
} else if (error.startsWith(ELECTION_FINISHED_ERROR)) {
return t('The election is over. You can\'t vote anymore');
} else if (error.startsWith(INVITATION_ONLY_ERROR)) {
return t('You need a token to vote in this election');
} else if (error.startsWith(USED_TOKEN_ERROR)) {
return t('You seem to have already voted.');
} else if (error.startsWith(WRONG_ELECTION_ERROR)) {
return t('The parameters of the election are incorrect.');
}
};
Loading…
Cancel
Save