Browse Source
Connection to MV API (#1)
Connection to MV API (#1)
* change url * fix bug in API * add .env config * rename mentions to grades * connect Result.jsx to API * connect votemaster
22 changed files with 388 additions and 287 deletions
-
3.gitignore
-
5package.json
-
2public/index.html
-
2src/App.jsx
-
0src/App.test.jsx
-
14src/Routes.jsx
-
5src/Util.jsx
-
0src/components/form/ButtonWithConfirm.jsx
-
0src/components/form/HelpButton.jsx
-
0src/components/form/ModalConfirm.jsx
-
0src/components/layouts/Footer.jsx
-
0src/components/layouts/Header.jsx
-
82src/components/views/CreateElection.jsx
-
9src/components/views/CreateSuccess.js
-
2src/components/views/Home.jsx
-
161src/components/views/Result.jsx
-
0src/components/views/UnknownView.jsx
-
160src/components/views/Vote.js
-
213src/components/views/Vote.jsx
-
17src/components/views/VoteSuccess.js
-
0src/index.jsx
-
0src/serviceWorker.jsx
@ -1,6 +1,6 @@ |
|||
import React from 'react'; |
|||
import { BrowserRouter as Router} from "react-router-dom"; |
|||
import Routes from "./Routes.js"; |
|||
import Routes from "./Routes"; |
|||
|
|||
import Header from "./components/layouts/Header"; |
|||
import Footer from "./components/layouts/Footer"; |
@ -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,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 > </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 > </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; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue