Connection to MV API (#1)

* change url

* fix bug in API

* add .env config

* rename mentions to grades

* connect Result.jsx to API

* connect vote
pull/73/head
guhur 5 years ago
parent ea2d783f3c
commit e49ad0bde8

3
.gitignore vendored

@ -1,4 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.tern-port
.#*
*.env.local
# dependencies # dependencies
/node_modules /node_modules

@ -41,5 +41,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
} },
"jshintConfig": {
"esversion": 6
}
} }

@ -30,7 +30,7 @@
<meta name="msapplication-TileColor" content="#ffffff"> <meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="%PUBLIC_URL%/favicon/ms-icon-144x144.png"> <meta name="msapplication-TileImage" content="%PUBLIC_URL%/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<title>Platerforme de vote : Jugement Majoritaire</title> <title>Plateforme de vote : Jugement Majoritaire</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router} from "react-router-dom"; import { BrowserRouter as Router} from "react-router-dom";
import Routes from "./Routes.js"; import Routes from "./Routes";
import Header from "./components/layouts/Header"; import Header from "./components/layouts/Header";
import Footer from "./components/layouts/Footer"; import Footer from "./components/layouts/Footer";

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import {Switch, Route } from "react-router-dom"; import {Switch, Route } from "react-router-dom";
import Home from "./components/views/Home.js"; import Home from "./components/views/Home";
import CreateElection from "./components/views/CreateElection.js"; import CreateElection from "./components/views/CreateElection";
import Vote from "./components/views/Vote"; import Vote from "./components/views/Vote";
import Result from "./components/views/Result"; import Result from "./components/views/Result";
import UnknownView from "./components/views/UnknownView"; import UnknownView from "./components/views/UnknownView";
@ -15,11 +15,11 @@ function Routes() {
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route path="/create-election" component={CreateElection} /> <Route path="/create-election" component={CreateElection} />
<Route path="/create-success" component={CreateSuccess} /> <Route path="/vote/:handle" component={Vote} />
<Route path="/vote" component={Vote} /> <Route path="/result/:handle" component={Result} />
<Route path="/vote-success" component={VoteSuccess} /> <Route path="/create-success/:handle" component={CreateSuccess} />
<Route path="/result" component={Result} /> <Route path="/vote-success/:handle" component={VoteSuccess} />
<Route path="/unknown-election" component={UnknownElection} /> <Route path="/unknown-election/:handle" component={UnknownElection} />
<Route component={UnknownView} /> <Route component={UnknownView} />
</Switch> </Switch>
</main> </main>

@ -0,0 +1,5 @@
const colors = ["#015411", "#019812", "#6bca24", "#ffb200", "#ff5d00", "#b20616", "#6f0214"];
export const grades = process.env.REACT_APP_GRADES.split(', ').map(
(e, i) => ({label: e, color:colors[i]}));

@ -1,5 +1,5 @@
import React, {Component} from "react"; import React, {Component} from "react";
import { Redirect } from 'react-router-dom';
import { import {
Container, Container,
Row, Row,
@ -13,27 +13,16 @@ import {
import {toast, ToastContainer} from 'react-toastify'; import {toast, ToastContainer} from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { resolve } from 'url';
import HelpButton from "../form/HelpButton"; import HelpButton from "../form/HelpButton";
import {arrayMove, sortableContainer, sortableElement, sortableHandle} from 'react-sortable-hoc'; import {arrayMove, sortableContainer, sortableElement, sortableHandle} from 'react-sortable-hoc';
import ButtonWithConfirm from "../form/ButtonWithConfirm"; import ButtonWithConfirm from "../form/ButtonWithConfirm";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {faPlus, faTrashAlt, faCheck } from '@fortawesome/free-solid-svg-icons'; import {faPlus, faTrashAlt, faCheck } from '@fortawesome/free-solid-svg-icons';
import { grades } from '../../Util';
//TODO : variable de config dans un fichier à part (avec les mentions, le min/max de mentions, le nombre max de candidats, les maxlength,l'url api, etc ...)
const mentions = [
{label:"Excellent", color:"#015411"},
{label:"Trés Bien", color:"#019812"},
{label:"Bien", color:"#6bca24"},
{label:"Assez Bien", color:"#ffb200"},
{label:"Passable", color:"#ff5d00"},
{label:"Insuffisant", color:"#b20616"},
{label:"A Rejeter", color:"#6f0214"},
];
const PATH_API = '/api/';
const PATH_CREATE_ELECTION = 'create';
const DragHandle = sortableHandle(({children}) => <span className="input-group-text indexNumber">{children}</span>); const DragHandle = sortableHandle(({children}) => <span className="input-group-text indexNumber">{children}</span>);
@ -85,10 +74,12 @@ class CreateElection extends Component {
super(props); super(props);
this.state = { this.state = {
candidates:[{label:""},{label:""}], candidates:[{label:""},{label:""}],
nbCandidatesWithLabel:0, numCandidatesWithLabel:0,
title:null, title:null,
isVisibleTipsDragAndDropCandidate:true, isVisibleTipsDragAndDropCandidate:true,
nbMentions:7 numGrades:7,
successCreate: false,
redirectTo: null
}; };
this.candidateInputs = []; this.candidateInputs = [];
this.focusInput= React.createRef(); this.focusInput= React.createRef();
@ -108,7 +99,7 @@ class CreateElection extends Component {
this.setState({ candidates: candidates}); this.setState({ candidates: candidates});
} }
if(event.type === 'keypress'){ if(event.type === 'keypress'){
setTimeout(()=>{ this.candidateInputs[this.state.candidates.length-1].focus()},250); setTimeout(()=>{ this.candidateInputs[this.state.candidates.length-1].focus();},250);
} }
}; };
@ -125,15 +116,15 @@ class CreateElection extends Component {
editCandidateLabel = (event, index) => { editCandidateLabel = (event, index) => {
let candidates = this.state.candidates; let candidates = this.state.candidates;
let nbLabels = 0; let numLabels = 0;
candidates[index].label = event.currentTarget.value; candidates[index].label = event.currentTarget.value;
candidates.map((candidate,i)=>{ candidates.map((candidate,i)=>{
if(candidate.label!==""){ if(candidate.label!==""){
nbLabels++; numLabels++;
} }
return candidate.label; return candidate.label;
}); });
this.setState({candidates: candidates, nbCandidatesWithLabel:nbLabels}); this.setState({candidates: candidates, numCandidatesWithLabel:numLabels});
}; };
@ -156,8 +147,8 @@ class CreateElection extends Component {
this.setState({candidates: candidates}); this.setState({candidates: candidates});
}; };
handleChangeNbMentions= (event) => { handleChangeNumGrades= (event) => {
this.setState({nbMentions: event.target.value}); this.setState({numGrades: event.target.value});
}; };
componentWillMount() { componentWillMount() {
@ -168,26 +159,35 @@ class CreateElection extends Component {
handleSubmit () { handleSubmit () {
const { const {
candidates, candidates,
title, title,
nbMentions numGrades
} = this.state; } = this.state;
fetch(`${PATH_API}${PATH_CREATE_ELECTION}`, {
const endpoint = resolve(
process.env.REACT_APP_SERVER_URL,
'election/'
);
fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
title: title, title: title,
candidates: candidates, candidates: candidates.map(c => c.label),
on_invitation_only: false, on_invitation_only: false,
num_grades: nbMentions, num_grades: numGrades,
elector_emails: [] elector_emails: []
}) })
} })
).then(response => response.json()) .then(response => response.json())
.then(result => alert(result)) .then(result => this.setState(state => ({
.catch(error => error); redirectTo: '/create-success/' + result.id,
successCreate: true
})))
.catch(error => error);
}; };
handleSendWithoutCandidate = () => { handleSendWithoutCandidate = () => {
@ -197,7 +197,11 @@ class CreateElection extends Component {
}; };
render(){ render(){
const { successCreate, redirectTo } = this.state;
const params = new URLSearchParams(this.props.location.search); const params = new URLSearchParams(this.props.location.search);
if (successCreate) return <Redirect to={redirectTo} />;
return( return(
<Container> <Container>
<ToastContainer/> <ToastContainer/>
@ -244,7 +248,7 @@ class CreateElection extends Component {
<Label for="title">Nombre de mentions :</Label> <Label for="title">Nombre de mentions :</Label>
</Col> </Col>
<Col xs="" md="2" > <Col xs="" md="2" >
<select className="form-control" tabIndex={this.state.candidates.length+3} onChange={this.handleChangeNbMentions} defaultValue="7"> <select className="form-control" tabIndex={this.state.candidates.length+3} onChange={this.handleChangeNumGrades} defaultValue="7">
<option value="5">5</option> <option value="5">5</option>
<option value="6" >6</option> <option value="6" >6</option>
<option value="7">7</option> <option value="7">7</option>
@ -257,8 +261,8 @@ class CreateElection extends Component {
</HelpButton> </HelpButton>
</Col> </Col>
<Col xs="12" md="" > <Col xs="12" md="" >
{ mentions.map((mention,i) => { { grades.map((mention,i) => {
return <span key={i} className="badge badge-light mr-2 mt-2" style={{backgroundColor:mention.color,color:"#fff",opacity:(i<this.state.nbMentions)?1:0.3}} >{mention.label}</span> return <span key={i} className="badge badge-light mr-2 mt-2" style={{backgroundColor:mention.color,color:"#fff",opacity:(i<this.state.numGrades)?1:0.3}} >{mention.label}</span>
}) })
} }
</Col> </Col>
@ -267,7 +271,7 @@ class CreateElection extends Component {
<hr /> <hr />
<Row className="mt-4 justify-content-md-center"> <Row className="mt-4 justify-content-md-center">
<Col xs="12" md="3" > <Col xs="12" md="3" >
{this.state.nbCandidatesWithLabel>=2?<ButtonWithConfirm className="btn btn-success float-right btn-block" tabIndex={this.state.candidates.length+4}> {this.state.numCandidatesWithLabel>=2?<ButtonWithConfirm className="btn btn-success float-right btn-block" tabIndex={this.state.candidates.length+4}>
<div key="button"><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</div> <div key="button"><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</div>
<div key="modal-title">Confirmez votre vote</div> <div key="modal-title">Confirmez votre vote</div>
<div key="modal-body"> <div key="modal-body">
@ -288,8 +292,8 @@ class CreateElection extends Component {
} }
</ul></div> </ul></div>
<div className="text-white bg-primary p-1">Mentions :</div> <div className="text-white bg-primary p-1">Mentions :</div>
<div className="p-1 pl-3">{ mentions.map((mention,i) => { <div className="p-1 pl-3">{ grades.map((mention,i) => {
return (i<this.state.nbMentions)?<span key={i} className="badge badge-light mr-2 mt-2" style={{backgroundColor:mention.color,color:"#fff"}}>{mention.label}</span>:<span key={i}/> return (i<this.state.numGrades)?<span key={i} className="badge badge-light mr-2 mt-2" style={{backgroundColor:mention.color,color:"#fff"}}>{mention.label}</span>:<span key={i}/>
}) })
}</div> }</div>
</div> </div>

@ -1,18 +1,19 @@
import React, {Component} from "react"; import React, {Component} from "react";
import {Button, Col, Container, Row} from "reactstrap"; import {Button, Col, Container, Row} from "reactstrap";
import logoLine from "../../logos/logo-line-white.svg";
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import { faCopy, faUsers } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faUsers } from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import logoLine from "../../logos/logo-line-white.svg";
class UnknownView extends Component { class UnknownView extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
const electionSlug = this.props.match.params.handle;
this.state = { this.state = {
urlOfVote:"http://localhost/vote", urlOfVote: "https://" + window.location.hostname + "/vote/" + electionSlug,
urlOfResult:"http://localhost/result" urlOfResult: "https://" + window.location.hostname + "/result/" + electionSlug
}; };
this.urlVoteField = React.createRef(); this.urlVoteField = React.createRef();
this.urlResultField = React.createRef(); this.urlResultField = React.createRef();
@ -86,7 +87,7 @@ class UnknownView extends Component {
<Row className="mt-4 mb-4" > <Row className="mt-4 mb-4" >
<Col className="text-center"> <Col className="text-center">
<Link to={this.state.urlOfVote} className="btn btn-success"><FontAwesomeIcon icon={faUsers} className="mr-2"/>Participer maintenant !</Link> <Link to={ "/vote/" + this.props.match.params.handle} className="btn btn-success"><FontAwesomeIcon icon={faUsers} className="mr-2"/>Participer maintenant !</Link>
</Col> </Col>
</Row> </Row>
</Container> </Container>

@ -54,6 +54,6 @@ class Home extends Component {
</form> </form>
</Container> </Container>
) )
} };
} }
export default Home; export default Home;

@ -1,17 +1,8 @@
import React, {Component} from "react"; import React, {Component} from "react";
import { Redirect } from 'react-router-dom';
import { resolve } from 'url';
import {Container, Row, Col, Collapse, Card, CardHeader, CardBody, Table} from "reactstrap"; import {Container, Row, Col, Collapse, Card, CardHeader, CardBody, Table} from "reactstrap";
import { grades } from '../../Util';
//TODO : variable de config dans un fichier à part (avec les mentions, le min/max de mentions, le nombre max de candidats, les maxlength,l'url api, etc ...)
const mentions = [
{label: "Excellent", color: "#015411"},
{label: "Trés Bien", color: "#019812"},
{label: "Bien", color: "#6bca24"},
{label: "Assez Bien", color: "#ffb200"},
{label: "Passable", color: "#ff5d00"},
{label: "Insuffisant", color: "#b20616"},
{label: "A Rejeter", color: "#6f0214"},
];
class Result extends Component { class Result extends Component {
@ -20,49 +11,84 @@ class Result extends Component {
this.state = { this.state = {
candidates: [], candidates: [],
title: null, title: null,
nbMentions: 0, numGrades: 0,
colSizeCandidateLg: 4, colSizeCandidateLg: 4,
colSizeCandidateMd: 6, colSizeCandidateMd: 6,
colSizeCandidateXs: 12, colSizeCandidateXs: 12,
colSizeMentionLg: 1, colSizeGradeLg: 1,
colSizeMentionMd: 1, colSizeGradeMd: 1,
colSizeMentionXs: 1, colSizeGradeXs: 1,
collapseGraphics: false, collapseGraphics: false,
collapseProfiles: false collapseProfiles: false,
redirectLost: false,
electionGrades: grades
}; };
}
handleErrors = (response) => {
if (!response.ok) {
response.json().then( response => {
this.setState(state => ({
redirectLost: '/unknown-election/' + encodeURIComponent(response)}));
})
throw Error(response);
}
return response;
} }
componentDidMount() { resultsToState = (response) => {
//todo fetch data from API const candidates = response.map(c => ({
let fetchedData = { id: c.id, label: c.name, profile: c.profile, grade:c.grade, score: c.score
title: "Merci d'évaluer les candidats suivants", }));
candidates: [ this.setState(state => ({candidates: candidates}));
{id: 0, label: "Mme ABCD", mention: 2, profile: [20, 20, 20, 10, 10, 20, 0], score: "55.28"}, return response;
{id: 2, label: "M. EFGH", mention: 3, profile: [0, 20, 20, 10, 10, 30, 10], score: "43.10"},
{id: 3, label: "M. IJKL", mention: 4, profile: [0, 0, 20, 25, 15, 20, 20], score: "22.82"},
{id: 4, label: "M. MNOP", mention: 4, profile: [0, 0, 15, 15, 30, 10, 30], score: "12.72"}
],//ordered by result
nbMentions: 7,
};
let data = {
title: fetchedData.title,
candidates: fetchedData.candidates,
nbMentions: fetchedData.nbMentions,
colSizeCandidateLg: 0,
colSizeCandidateMd: 0,
colSizeCandidateXs: 0,
colSizeMentionLg: Math.floor((12 - this.state.colSizeCandidateLg) / fetchedData.nbMentions),
colSizeMentionMd: Math.floor((12 - this.state.colSizeCandidateMd) / fetchedData.nbMentions),
colSizeMentionXs: Math.floor((12 - this.state.colSizeCandidateXs) / fetchedData.nbMentions),
};
data.colSizeCandidateLg = ((12 - data.colSizeMentionLg * data.nbMentions) > 0) ? (12 - data.colSizeMentionLg * data.nbMentions) : 12;
data.colSizeCandidateMd = ((12 - data.colSizeMentionMd * data.nbMentions) > 0) ? (12 - data.colSizeMentionMd * data.nbMentions) : 12;
data.colSizeCandidateXs = ((12 - data.colSizeMentionXs * data.nbMentions) > 0) ? (12 - data.colSizeMentionXs * data.nbMentions) : 12;
this.setState(data);
} }
detailsToState = (response) => {
const numGrades = response.num_grades;
const colSizeGradeLg = Math.floor((12 - this.state.colSizeCandidateLg) / numGrades);
const colSizeGradeMd = Math.floor((12 - this.state.colSizeCandidateMd) / numGrades);
const colSizeGradeXs = Math.floor((12 - this.state.colSizeCandidateXs) / numGrades);
this.setState(state => ({
title: response.title,
numGrades: numGrades,
colSizeGradeLg: colSizeGradeLg,
colSizeGradeMd: colSizeGradeMd,
colSizeGradeXs: colSizeGradeXs,
colSizeCandidateLg: ((12 - colSizeGradeLg * numGrades) > 0) ? (12 - colSizeGradeLg * numGrades) : 12,
colSizeCandidateMd: ((12 - colSizeGradeMd * numGrades) > 0) ? (12 - colSizeGradeMd * numGrades) : 12,
colSizeCandidateXs: ((12 - colSizeGradeXs * numGrades) > 0) ? (12 - colSizeGradeXs * numGrades) : 12,
electionGrades: grades.slice(0, numGrades)
}));
return response;
}
componentDidMount() {
// FIXME we should better handling logs
const electionSlug = this.props.match.params.handle;
// get details of the election
const detailsEndpoint = resolve(process.env.REACT_APP_SERVER_URL,
'election/get/'.concat(electionSlug));
fetch(detailsEndpoint)
.then(this.handleErrors)
.then(response => response.json())
.then(this.detailsToState)
.catch(error => console.log(error));
// get results of the election
const resultsEndpoint = resolve(process.env.REACT_APP_SERVER_URL,
'election/results/'.concat(electionSlug));
fetch(resultsEndpoint)
.then(this.handleErrors)
.then(response => response.json())
.then(this.resultsToState)
.catch(error => console.log(error));
}
toggleGraphics = () => { toggleGraphics = () => {
this.setState(state => ({collapseGraphics: !state.collapseGraphics})); this.setState(state => ({collapseGraphics: !state.collapseGraphics}));
@ -73,6 +99,16 @@ class Result extends Component {
}; };
render() { render() {
const { redirectLost,
candidates,
electionGrades
} = this.state;
if (redirectLost) {
return (<Redirect to={redirectLost}/>)
}
return ( return (
<Container> <Container>
<Row> <Row>
@ -81,13 +117,13 @@ class Result extends Component {
<Row className="mt-5"> <Row className="mt-5">
<Col><h1>Résultat du vote :</h1> <Col><h1>Résultat du vote :</h1>
<ol>{this.state.candidates.map((candidate, i) => { <ol>{candidates.map((candidate, i) => {
return (<li key={i} className="mt-2">{candidate.label}<span return (<li key={i} className="mt-2">{candidate.label}<span
className="badge badge-dark mr-2 mt-2">{candidate.score}%</span><span className="badge badge-dark mr-2 mt-2">{candidate.score}%</span><span
className="badge badge-light mr-2 mt-2" style={{ className="badge badge-light mr-2 mt-2" style={{
backgroundColor: mentions[candidate.mention].color, backgroundColor: electionGrades[candidate.grade].color,
color: "#fff" color: "#fff"
}}>{mentions[candidate.mention].label}</span></li>); }}>{grades[candidate.grade].label}</span></li>);
})}</ol> })}</ol>
</Col> </Col>
</Row> </Row>
@ -102,9 +138,9 @@ class Result extends Component {
<CardBody className="pt-5"> <CardBody className="pt-5">
<div> <div>
<div className="median" <div className="median"
style={{height: (this.state.candidates.length * 28) + 30}}/> style={{height: (candidates.length * 28) + 30}}/>
<table style={{width: "100%"}}><tbody> <table style={{width: "100%"}}><tbody>
{this.state.candidates.map((candidate, i) => { {candidates.map((candidate, i) => {
return (<tr key={i}> return (<tr key={i}>
<td style={{width: "30px"}}>{i + 1}</td> <td style={{width: "30px"}}>{i + 1}</td>
{/*candidate.label*/} {/*candidate.label*/}
@ -120,7 +156,7 @@ class Result extends Component {
} }
return (<td key={i} style={{ return (<td key={i} style={{
width: percent, width: percent,
backgroundColor: mentions[i].color backgroundColor: this.state.electionGrades[i].color
}}>&nbsp;</td>); }}>&nbsp;</td>);
}else{ }else{
return null return null
@ -136,7 +172,7 @@ class Result extends Component {
</div> </div>
<div className="mt-4"> <div className="mt-4">
<small> <small>
{this.state.candidates.map((candidate, i) => { {candidates.map((candidate, i) => {
return ( return (
<span key={i}>{(i > 0) ? ", " : ""}<b>{i + 1}</b>: {candidate.label}</span>); <span key={i}>{(i > 0) ? ", " : ""}<b>{i + 1}</b>: {candidate.label}</span>);
})} })}
@ -144,14 +180,13 @@ class Result extends Component {
</div> </div>
<div className="mt-2"> <div className="mt-2">
<small> <small>
{mentions.map((mention, i) => { {electionGrades.map((grade, i) => {
return (i < this.state.nbMentions) ? return (
<span key={i} className="badge badge-light mr-2 mt-2" style={{ <span key={i} className="badge badge-light mr-2 mt-2" style={{
backgroundColor: mention.color, backgroundColor: grade.color,
color: "#fff" color: "#fff"
}}>{mention.label}</span> : <span key={i}/> }}>{grade.label}</span>
}) )})}
}
</small> </small>
</div> </div>
</CardBody> </CardBody>
@ -173,14 +208,14 @@ class Result extends Component {
<thead> <thead>
<tr> <tr>
<th>#</th> <th>#</th>
{mentions.map((mention, i) => { {electionGrades.map((grade, i) => {
return (<th key={i}><span className="badge badge-light" style={{ return (<th key={i}><span className="badge badge-light" style={{
backgroundColor: mention.color, backgroundColor: grade.color,
color: "#fff" color: "#fff"
}}>{mention.label} </span></th>); }}>{grade.label} </span></th>);
})}</tr> })}</tr>
</thead> </thead>
<tbody>{this.state.candidates.map((candidate, i) => { <tbody>{candidates.map((candidate, i) => {
return (<tr key={i}> return (<tr key={i}>
<td>{i + 1}</td> <td>{i + 1}</td>
{/*candidate.label*/} {/*candidate.label*/}
@ -191,7 +226,7 @@ class Result extends Component {
})}</tbody> })}</tbody>
</Table> </Table>
</div> </div>
<small>{this.state.candidates.map((candidate, i) => { <small>{candidates.map((candidate, i) => {
return (<span return (<span
key={i}>{(i > 0) ? ", " : ""}<b>{i + 1}</b>: {candidate.label}</span>); key={i}>{(i > 0) ? ", " : ""}<b>{i + 1}</b>: {candidate.label}</span>);
})}</small> })}</small>

@ -1,160 +0,0 @@
import React, {Component} from "react";
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";
//TODO : variable de config dans un fichier à part (avec les mentions, le min/max de mentions, le nombre max de candidats, les maxlength,l'url api, etc ...)
const mentions = [
{label:"Excellent", color:"#015411"},
{label:"Trés Bien", color:"#019812"},
{label:"Bien", color:"#6bca24"},
{label:"Assez Bien", color:"#ffb200"},
{label:"Passable", color:"#ff5d00"},
{label:"Insuffisant", color:"#b20616"},
{label:"A Rejeter", color:"#6f0214"},
];
class Vote extends Component {
constructor(props) {
super(props);
this.state = {
candidates:[],
title:null,
nbMentions:0,
ratedCandidates:[],
colSizeCandidateLg:4,
colSizeCandidateMd:6,
colSizeCandidateXs:12,
colSizeMentionLg:1,
colSizeMentionMd:1,
colSizeMentionXs:1,
};
}
componentDidMount() {
//todo fetch data from API
let fetchedData={
title:"Merci d'évaluer les candidats suivants",
candidates:[ {id:0, label:"Mme ABCD"}, {id:2, label:"M. EFGH"}, {id:3, label:"M. IJKL"}, {id:4, label:"M. MNOP"} ],
nbMentions:7,
};
let data={
title:fetchedData.title,
candidates:fetchedData.candidates,
nbMentions:fetchedData.nbMentions,
colSizeCandidateLg:0,
colSizeCandidateMd:0,
colSizeCandidateXs:0,
colSizeMentionLg:Math.floor((12-this.state.colSizeCandidateLg)/fetchedData.nbMentions),
colSizeMentionMd:Math.floor((12-this.state.colSizeCandidateMd)/fetchedData.nbMentions),
colSizeMentionXs:Math.floor((12-this.state.colSizeCandidateXs)/fetchedData.nbMentions),
};
data.colSizeCandidateLg=((12-data.colSizeMentionLg*data.nbMentions)>0)?(12-data.colSizeMentionLg*data.nbMentions):12;
data.colSizeCandidateMd=((12-data.colSizeMentionMd*data.nbMentions)>0)?(12-data.colSizeMentionMd*data.nbMentions):12;
data.colSizeCandidateXs=((12-data.colSizeMentionXs*data.nbMentions)>0)?(12-data.colSizeMentionXs*data.nbMentions):12;
//shuffle candidates
let i,
j,
temp;
for (i = data.candidates.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = data.candidates[i];
data.candidates[i] = data.candidates[j];
data.candidates[j] = temp;
}
this.setState(data);
}
handleMentionClick= (event) => {
let data={
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);
ratedCandidates.push(data);
this.setState({ratedCandidates:ratedCandidates});
};
handleSubmitWithoutAllRate = () => {
toast.error("Vous devez évaluer l'ensemble des propositions/candidats !", {
position: toast.POSITION.TOP_CENTER
});
};
handleSubmit= (event) => {
event.preventDefault();
};
render(){
return(
<Container>
<ToastContainer/>
<form onSubmit={this.handleSubmit} autoComplete="off" >
<Row>
<Col ><h3>{ this.state.title }</h3></Col>
</Row>
<Row className="cardVote d-none d-lg-flex" >
<Col xs={this.state.colSizeCandidateXs} md={this.state.colSizeCandidateMd} lg={this.state.colSizeCandidateLg} ><h5 >&nbsp;</h5></Col>
{ mentions.map((mention,j) => {
return (j<this.state.nbMentions)?<Col xs={this.state.colSizeMentionXs} md={this.state.colSizeMentionMd} lg={this.state.colSizeMentionLg} key={j} className="text-center p-0" style={{lineHeight:2}}><small className="nowrap bold badge" style={{backgroundColor:mention.color,color:"#fff"}}>{mention.label}</small></Col>:null
})
}
</Row>
{
this.state.candidates.map((candidate,i) => {
return <Row key={i} className="cardVote">
<Col xs={this.state.colSizeCandidateXs} md={this.state.colSizeCandidateMd} lg={this.state.colSizeCandidateLg} >
<h5 className="m-0">{candidate.label}</h5><hr className="d-lg-none" /></Col>
{ mentions.map((mention,j) => {
return (j<this.state.nbMentions)?<Col
xs={this.state.colSizeMentionXs} md={this.state.colSizeMentionMd} lg={this.state.colSizeMentionLg} key={j}
className="text-lg-center"
>
<label htmlFor={"candidateMention"+i+"-"+j} className="check"
>
<small className="nowrap d-lg-none ml-2 bold badge"
style={
this.state.ratedCandidates.find(function(ratedCandidat){return JSON.stringify(ratedCandidat) === JSON.stringify({id:candidate.id,value:j})})?
{backgroundColor:mention.color,color:"#fff"}:{backgroundColor:'transparent',color:"#000"}
}
>{mention.label}</small>
<input type="radio" name={"candidate"+i} id={"candidateMention"+i+"-"+j} data-index={i} data-id={candidate.id} value={j} onClick={this.handleMentionClick} defaultChecked={this.state.ratedCandidates.find(function(element) { return JSON.stringify(element) === JSON.stringify({id:candidate.id,value:j})})} />
<span className="checkmark" style={
this.state.ratedCandidates.find(function(ratedCandidat){return JSON.stringify(ratedCandidat) === JSON.stringify({id:candidate.id,value:j})})?
{backgroundColor:mention.color,color:"#fff"}:{backgroundColor:'transparent',color:"#000"}
}/>
</label></Col>:null
})
}
</Row>
})
}
<Row>
<Col className="text-center" >
{(this.state.ratedCandidates.length!==this.state.candidates.length)?<Button type="button" onClick={this.handleSubmitWithoutAllRate} className="btn btn-dark "><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</Button>:<Button type="submit" className="btn btn-success "><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</Button>}
</Col>
</Row>
</form>
</Container>
)
}
}
export default Vote;

@ -0,0 +1,213 @@
import React, {Component} from "react";
import { Redirect } from 'react-router-dom';
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 { grades } from '../../Util';
class Vote extends Component {
constructor(props) {
super(props);
this.state = {
candidates:[],
title:null,
numGrades:0,
ratedCandidates:[],
colSizeCandidateLg:4,
colSizeCandidateMd:6,
colSizeCandidateXs:12,
colSizeGradeLg:1,
colSizeGradeMd:1,
colSizeGradeXs:1,
redirectTo: null,
electionGrades: grades
};
}
handleErrors = (response) => {
if (!response.ok) {
response.json().then( response => {
console.log(response);
this.setState(state => ({
redirectTo: '/unknown-election/' + encodeURIComponent(response)}));
})
throw Error(response);
}
return response;
}
detailsToState = (response) => {
const numGrades = response.num_grades;
const candidates = response.candidates.map((c, i) => ({
id: i,
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;
}
const colSizeGradeLg = Math.floor((12 - this.state.colSizeCandidateLg) / numGrades);
const colSizeGradeMd = Math.floor((12 - this.state.colSizeCandidateMd) / numGrades);
const colSizeGradeXs = Math.floor((12 - this.state.colSizeCandidateXs) / numGrades);
this.setState(state => ({
title: response.title,
candidates: candidates,
numGrades: numGrades,
colSizeGradeLg: colSizeGradeLg,
colSizeGradeMd: colSizeGradeMd,
colSizeGradeXs: colSizeGradeXs,
colSizeCandidateLg: ((12 - colSizeGradeLg * numGrades) > 0) ?
(12 - colSizeGradeLg * numGrades) : 12,
colSizeCandidateMd: ((12 - colSizeGradeMd * numGrades) > 0) ?
(12 - colSizeGradeMd * numGrades) : 12,
colSizeCandidateXs: ((12 - colSizeGradeXs * numGrades) > 0) ?
(12 - colSizeGradeXs * numGrades) : 12,
electionGrades: grades.slice(0, numGrades)
}));
return response;
}
componentDidMount() {
// FIXME we should better handling logs
const electionSlug = this.props.match.params.handle;
const detailsEndpoint = resolve(process.env.REACT_APP_SERVER_URL,
'election/get/'.concat(electionSlug));
fetch(detailsEndpoint)
.then(this.handleErrors)
.then(response => response.json())
.then(this.detailsToState)
.catch(error => console.log(error));
}
handleGradeClick = (event) => {
let data={
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);
ratedCandidates.push(data);
this.setState({ratedCandidates:ratedCandidates});
};
handleSubmitWithoutAllRate = () => {
toast.error("Vous devez évaluer l'ensemble des propositions/candidats !", {
position: toast.POSITION.TOP_CENTER
});
};
handleSubmit = (event) => {
event.preventDefault();
const { ratedCandidates } = this.state;
const electionSlug = this.props.match.params.handle;
const endpoint = resolve(process.env.REACT_APP_SERVER_URL,
'election/vote/');
const gradesById = {};
ratedCandidates.forEach(c => { gradesById[c.id] = c.value; });
const gradesByCandidate = [];
Object.keys(gradesById)
.sort()
.forEach(id => {gradesByCandidate.push(gradesById[id]);});
fetch(endpoint, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
election: electionSlug,
grades_by_candidate: gradesByCandidate,
})
})
.then(this.handleErrors)
.then(result => this.setState({redirectTo: '/vote-success/' + electionSlug}))
.catch(error => error);
};
render(){
const { redirectTo,
candidates,
electionGrades
} = this.state;
if (redirectTo) {
return (<Redirect to={redirectTo}/>);
}
return(
<Container>
<ToastContainer/>
<form onSubmit={this.handleSubmit} autoComplete="off" >
<Row>
<Col ><h3>{ this.state.title }</h3></Col>
</Row>
<Row className="cardVote d-none d-lg-flex" >
<Col xs={this.state.colSizeCandidateXs} md={this.state.colSizeCandidateMd} lg={this.state.colSizeCandidateLg} ><h5 >&nbsp;</h5></Col>
{ electionGrades.map((grade,j) => {
return (j<this.state.numGrades)?<Col xs={this.state.colSizeGradeXs} md={this.state.colSizeGradeMd} lg={this.state.colSizeGradeLg} key={j} className="text-center p-0" style={{lineHeight:2}}><small className="nowrap bold badge" style={{backgroundColor:grade.color,color:"#fff"}}>{grade.label}</small></Col>:null;
})
}
</Row>
{
candidates.map((candidate,i) => {
return <Row key={i} className="cardVote">
<Col xs={this.state.colSizeCandidateXs} md={this.state.colSizeCandidateMd} lg={this.state.colSizeCandidateLg} >
<h5 className="m-0">{candidate.label}</h5><hr className="d-lg-none" /></Col>
{ this.state.electionGrades.map((grade,j) => {
return (j<this.state.numGrades)?<Col
xs={this.state.colSizeGradeXs} md={this.state.colSizeGradeMd} lg={this.state.colSizeGradeLg} key={j}
className="text-lg-center"
>
<label htmlFor={"candidateGrade"+i+"-"+j} className="check"
>
<small className="nowrap d-lg-none ml-2 bold badge"
style={
this.state.ratedCandidates.find(function(ratedCandidat){return JSON.stringify(ratedCandidat) === JSON.stringify({id:candidate.id,value:j})})?
{backgroundColor:grade.color,color:"#fff"}:{backgroundColor:'transparent',color:"#000"}
}
>{grade.label}</small>
<input type="radio" name={"candidate"+i} id={"candidateGrade"+i+"-"+j} data-index={i} data-id={candidate.id} value={j} onClick={this.handleGradeClick} defaultChecked={this.state.ratedCandidates.find(function(element) { return JSON.stringify(element) === JSON.stringify({id:candidate.id,value:j})})} />
<span className="checkmark" style={
this.state.ratedCandidates.find(function(ratedCandidat){return JSON.stringify(ratedCandidat) === JSON.stringify({id:candidate.id,value:j})})?
{backgroundColor:grade.color,color:"#fff"}:{backgroundColor:'transparent',color:"#000"}
}/>
</label></Col>:null
})
}
</Row>
})
}
<Row>
<Col className="text-center" >
{(this.state.ratedCandidates.length!==this.state.candidates.length)?<Button type="button" onClick={this.handleSubmitWithoutAllRate} className="btn btn-dark "><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</Button>:<Button type="submit" className="btn btn-success "><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</Button>}
</Col>
</Row>
</form>
</Container>
)
}
}
export default Vote;

@ -1,26 +1,23 @@
import React, {Component} from "react"; import React, {Component} from "react";
import {Button, Col, Container, Row} from "reactstrap"; import {Col, Container, Row} from "reactstrap";
import logoLine from "../../logos/logo-line-white.svg"; import logoLine from "../../logos/logo-line-white.svg";
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import { faCopy, faUsers } from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
class UnknownView extends Component { class UnknownView extends Component {
constructor(props) {
super(props);
}
render(){ render(){
return( return(
<Container> <Container>
<Row> <Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4"><img src={logoLine} alt="logo" height="128" /></Link> <Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row> </Row>
<Row className="mt-4"> <Row className="mt-4">
<Col className="text-center offset-lg-3" lg="6"><h2>Participation enregistrée avec succès !</h2> <Col className="text-center offset-lg-3" lg="6">
<p>Merci pour votre participation.</p> <h2>Participation enregistrée avec succès !</h2>
<p>Merci pour votre participation.</p>
</Col> </Col>
</Row> </Row>

Loading…
Cancel
Save