23 changed files with 2057 additions and 1549 deletions
-
1src/App.css
-
18src/App.jsx
-
148src/App.test.jsx
-
29src/Routes.jsx
-
18src/Util.jsx
-
104src/components/form/ButtonWithConfirm.jsx
-
105src/components/form/HelpButton.jsx
-
78src/components/form/ModalConfirm.jsx
-
42src/components/layouts/Footer.jsx
-
110src/components/layouts/Header.jsx
-
1102src/components/views/CreateElection.jsx
-
219src/components/views/CreateSuccess.js
-
131src/components/views/Home.jsx
-
545src/components/views/Result.jsx
-
59src/components/views/UnknownElection.js
-
57src/components/views/UnknownView.jsx
-
489src/components/views/Vote.jsx
-
55src/components/views/VoteSuccess.js
-
13src/index.jsx
-
216src/scss/_app.scss
-
5src/scss/_bootstrap.scss
-
32src/scss/config.scss
-
30src/serviceWorker.jsx
@ -1 +0,0 @@ |
|||
|
@ -1,5 +1,13 @@ |
|||
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]})); |
|||
|
|||
|
|||
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,55 +1,61 @@ |
|||
import React, {Component} from "react"; |
|||
import React, { Component } from "react"; |
|||
import ModalConfirm from "./ModalConfirm"; |
|||
|
|||
|
|||
class ButtonWithConfirm extends Component { |
|||
constructor(props) { |
|||
super(props); |
|||
this._modalConfirm=React.createRef(); |
|||
this.state={ |
|||
focused:false |
|||
} |
|||
} |
|||
|
|||
getComponent= (key) => { |
|||
return this.props.children.filter( (comp) => { |
|||
return comp.key === key; |
|||
}); |
|||
constructor(props) { |
|||
super(props); |
|||
this._modalConfirm = React.createRef(); |
|||
this.state = { |
|||
focused: false |
|||
}; |
|||
|
|||
render() { |
|||
const classNames=this.props.className.split(" "); |
|||
|
|||
let classNameForDiv=""; |
|||
let classNameForButton=""; |
|||
classNames.forEach(function(className){ |
|||
if(className==="input-group-prepend" || className==="input-group-append" ){ |
|||
classNameForDiv+=" "+className; |
|||
}else{ |
|||
classNameForButton+=" "+className; |
|||
} |
|||
}); |
|||
|
|||
|
|||
|
|||
return ( |
|||
<div className={classNameForDiv}> |
|||
<button |
|||
type="button" |
|||
className={classNameForButton} |
|||
onClick={() => { this._modalConfirm.current.toggle() }} |
|||
tabIndex={this.props.tabIndex} |
|||
>{this.getComponent("button")} |
|||
</button> |
|||
<ModalConfirm className={this.props.modalClassName} ref={this._modalConfirm}> |
|||
<div key="title">{this.getComponent("modal-title")}</div> |
|||
<div key="body">{this.getComponent("modal-body")}</div> |
|||
<div key="confirm">{this.getComponent("modal-confirm")}</div> |
|||
<div key="cancel">{this.getComponent("modal-cancel")}</div> |
|||
</ModalConfirm> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
getComponent = key => { |
|||
return this.props.children.filter(comp => { |
|||
return comp.key === key; |
|||
}); |
|||
}; |
|||
|
|||
render() { |
|||
const classNames = this.props.className.split(" "); |
|||
|
|||
let classNameForDiv = ""; |
|||
let classNameForButton = ""; |
|||
classNames.forEach(function(className) { |
|||
if ( |
|||
className === "input-group-prepend" || |
|||
className === "input-group-append" |
|||
) { |
|||
classNameForDiv += " " + className; |
|||
} else { |
|||
classNameForButton += " " + className; |
|||
} |
|||
}); |
|||
|
|||
return ( |
|||
<div className={classNameForDiv}> |
|||
<button |
|||
type="button" |
|||
className={classNameForButton} |
|||
onClick={() => { |
|||
this._modalConfirm.current.toggle(); |
|||
}} |
|||
tabIndex={this.props.tabIndex} |
|||
> |
|||
{this.getComponent("button")} |
|||
</button> |
|||
<ModalConfirm |
|||
className={this.props.modalClassName} |
|||
ref={this._modalConfirm} |
|||
> |
|||
<div key="title">{this.getComponent("modal-title")}</div> |
|||
<div key="body">{this.getComponent("modal-body")}</div> |
|||
<div key="confirm">{this.getComponent("modal-confirm")}</div> |
|||
<div key="cancel">{this.getComponent("modal-cancel")}</div> |
|||
</ModalConfirm> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ButtonWithConfirm; |
|||
export default ButtonWithConfirm; |
@ -1,45 +1,76 @@ |
|||
import React, {Component} from "react"; |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; |
|||
|
|||
import React, { Component } from "react"; |
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
|||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; |
|||
|
|||
class HelpButton extends Component { |
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
|
|||
this.state = { |
|||
tooltipOpen: false |
|||
}; |
|||
} |
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
showTooltip = () => { |
|||
console.log("show"); |
|||
this.setState({ |
|||
tooltipOpen: true |
|||
}); |
|||
this.state = { |
|||
tooltipOpen: false |
|||
}; |
|||
} |
|||
|
|||
hideTooltip = () => { |
|||
console.log("hide"); |
|||
this.setState({ |
|||
tooltipOpen: false |
|||
}); |
|||
}; |
|||
showTooltip = () => { |
|||
console.log("show"); |
|||
this.setState({ |
|||
tooltipOpen: true |
|||
}); |
|||
}; |
|||
|
|||
render() { |
|||
return ( |
|||
<span> |
|||
<span> |
|||
{this.state.tooltipOpen?<span style={{position:"absolute", zIndex:10, fontSize:"12px", color: "#000",backgroundColor:"#fff",display:"inline-block",borderRadius:"0.25rem",boxShadow:"-5px 0 5px rgba(0,0,0,0.5)",maxWidth:"200px",padding:"10px",marginLeft:"-215px", marginTop:"-25px"}}> |
|||
<span style={{ position:"absolute", width: 0,height: 0, borderTop: "10px solid transparent", borderBottom: "10px solid transparent", borderLeft: "10px solid #fff", marginLeft:"190px", marginTop:"15px"}}></span> |
|||
{this.props.children} |
|||
</span>:<span />} |
|||
</span> |
|||
<FontAwesomeIcon icon={faQuestionCircle} onMouseOver={this.showTooltip} onMouseOut={this.hideTooltip}/> |
|||
</span> |
|||
hideTooltip = () => { |
|||
console.log("hide"); |
|||
this.setState({ |
|||
tooltipOpen: false |
|||
}); |
|||
}; |
|||
|
|||
); |
|||
} |
|||
render() { |
|||
return ( |
|||
<span> |
|||
<span> |
|||
{this.state.tooltipOpen ? ( |
|||
<span |
|||
style={{ |
|||
position: "absolute", |
|||
zIndex: 10, |
|||
fontSize: "12px", |
|||
color: "#000", |
|||
backgroundColor: "#fff", |
|||
display: "inline-block", |
|||
borderRadius: "0.25rem", |
|||
boxShadow: "-5px 0 5px rgba(0,0,0,0.5)", |
|||
maxWidth: "200px", |
|||
padding: "10px", |
|||
marginLeft: "-215px", |
|||
marginTop: "-25px" |
|||
}} |
|||
> |
|||
<span |
|||
style={{ |
|||
position: "absolute", |
|||
width: 0, |
|||
height: 0, |
|||
borderTop: "10px solid transparent", |
|||
borderBottom: "10px solid transparent", |
|||
borderLeft: "10px solid #fff", |
|||
marginLeft: "190px", |
|||
marginTop: "15px" |
|||
}} |
|||
></span> |
|||
{this.props.children} |
|||
</span> |
|||
) : ( |
|||
<span /> |
|||
)} |
|||
</span> |
|||
<FontAwesomeIcon |
|||
icon={faQuestionCircle} |
|||
onMouseOver={this.showTooltip} |
|||
onMouseOut={this.hideTooltip} |
|||
/> |
|||
</span> |
|||
); |
|||
} |
|||
} |
|||
export default HelpButton; |
|||
export default HelpButton; |
@ -1,40 +1,52 @@ |
|||
import React, {Component} from "react"; |
|||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; |
|||
import React, { Component } from "react"; |
|||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"; |
|||
|
|||
class ModalConfirm extends Component { |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
modal: false |
|||
}; |
|||
} |
|||
|
|||
toggle = () => { |
|||
this.setState({ |
|||
modal: !this.state.modal |
|||
}); |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
modal: false |
|||
}; |
|||
} |
|||
|
|||
getComponent= (key) => { |
|||
return this.props.children.filter( (comp) => { |
|||
return comp.key === key; |
|||
}); |
|||
}; |
|||
toggle = () => { |
|||
this.setState({ |
|||
modal: !this.state.modal |
|||
}); |
|||
}; |
|||
|
|||
getComponent = key => { |
|||
return this.props.children.filter(comp => { |
|||
return comp.key === key; |
|||
}); |
|||
}; |
|||
|
|||
render() { |
|||
return ( |
|||
<Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className+" modal-dialog-centered"} > |
|||
<ModalHeader toggle={this.toggle}>{this.getComponent("title")}</ModalHeader> |
|||
<ModalBody> |
|||
{this.getComponent("body")} |
|||
</ModalBody> |
|||
<ModalFooter> |
|||
<Button color="primary-outline" className="text-primary border-primary" onClick={this.toggle}>{this.getComponent("cancel")}</Button> |
|||
<Button color="primary" onClick={this.toggle}>{this.getComponent("confirm")}</Button> |
|||
</ModalFooter> |
|||
</Modal> |
|||
); |
|||
} |
|||
render() { |
|||
return ( |
|||
<Modal |
|||
isOpen={this.state.modal} |
|||
toggle={this.toggle} |
|||
className={this.props.className + " modal-dialog-centered"} |
|||
> |
|||
<ModalHeader toggle={this.toggle}> |
|||
{this.getComponent("title")} |
|||
</ModalHeader> |
|||
<ModalBody>{this.getComponent("body")}</ModalBody> |
|||
<ModalFooter> |
|||
<Button |
|||
color="primary-outline" |
|||
className="text-primary border-primary" |
|||
onClick={this.toggle} |
|||
> |
|||
{this.getComponent("cancel")} |
|||
</Button> |
|||
<Button color="primary" onClick={this.toggle}> |
|||
{this.getComponent("confirm")} |
|||
</Button> |
|||
</ModalFooter> |
|||
</Modal> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ModalConfirm; |
|||
export default ModalConfirm; |
@ -1,27 +1,23 @@ |
|||
import React, {Component} from "react"; |
|||
import {Link} from "react-router-dom"; |
|||
import React, { Component } from "react"; |
|||
import { Link } from "react-router-dom"; |
|||
|
|||
class Footer extends Component { |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = {}; |
|||
} |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
} |
|||
} |
|||
|
|||
render(){ |
|||
return( |
|||
<footer className="text-center"> |
|||
<Link to="/">Accueil</Link> |
|||
<span className="m-2">-</span> |
|||
<a href="https://github.com/MieuxVoter">Code source</a> |
|||
<span className="m-2">-</span> |
|||
<a href="https://mieuxvoter.fr/">Qui sommes nous ?</a> |
|||
<div className="mt-2"> |
|||
MieuxVoter © |
|||
</div> |
|||
</footer> |
|||
) |
|||
} |
|||
render() { |
|||
return ( |
|||
<footer className="text-center"> |
|||
<Link to="/">Accueil</Link> |
|||
<span className="m-2">-</span> |
|||
<a href="https://github.com/MieuxVoter">Code source</a> |
|||
<span className="m-2">-</span> |
|||
<a href="https://mieuxvoter.fr/">Qui sommes nous ?</a> |
|||
<div className="mt-2">MieuxVoter ©</div> |
|||
</footer> |
|||
); |
|||
} |
|||
} |
|||
export default Footer; |
|||
export default Footer; |
@ -1,64 +1,60 @@ |
|||
import React, {Component} from "react"; |
|||
import { |
|||
Collapse, |
|||
Navbar, |
|||
NavbarToggler, |
|||
Nav, |
|||
NavItem |
|||
} from 'reactstrap' |
|||
import {Link} from "react-router-dom"; |
|||
import React, { Component } from "react"; |
|||
import { Collapse, Navbar, NavbarToggler, Nav, NavItem } from "reactstrap"; |
|||
import { Link } from "react-router-dom"; |
|||
|
|||
import logo from '../../logos/logo-color.svg'; |
|||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; |
|||
import {faRocket} from "@fortawesome/free-solid-svg-icons"; |
|||
import logo from "../../logos/logo-color.svg"; |
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
|||
import { faRocket } from "@fortawesome/free-solid-svg-icons"; |
|||
|
|||
class Header extends Component { |
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.toggle = this.toggle.bind(this); |
|||
this.state = { |
|||
isOpen: false |
|||
}; |
|||
} |
|||
|
|||
this.toggle = this.toggle.bind(this); |
|||
this.state = { |
|||
isOpen: false |
|||
}; |
|||
} |
|||
toggle() { |
|||
this.setState({ |
|||
isOpen: !this.state.isOpen |
|||
}); |
|||
} |
|||
|
|||
toggle() { |
|||
this.setState({ |
|||
isOpen: !this.state.isOpen |
|||
}); |
|||
} |
|||
|
|||
render(){ |
|||
return ( |
|||
<header> |
|||
<Navbar color="light" light expand="md"> |
|||
<Link to="/" className="navbar-brand"> |
|||
<div className="d-flex flex-row"> |
|||
<div className="align-self-center"> |
|||
<img src={logo} alt="logo" height="32"/> |
|||
</div> |
|||
<div className="align-self-center ml-2"> |
|||
<div className="logo-text"> |
|||
<h1>Plateforme de vote |
|||
<small>Jugement Majoritaire</small> |
|||
</h1> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Link> |
|||
<NavbarToggler onClick={this.toggle} /> |
|||
<Collapse isOpen={this.state.isOpen} navbar> |
|||
<Nav className="ml-auto" navbar> |
|||
<NavItem> |
|||
<Link className="text-primary nav-link" to="/create-election/"><FontAwesomeIcon icon={faRocket} className="mr-2"/> Démarrer un vote</Link> |
|||
</NavItem> |
|||
</Nav> |
|||
</Collapse> |
|||
</Navbar> |
|||
</header> |
|||
); |
|||
} |
|||
render() { |
|||
return ( |
|||
<header> |
|||
<Navbar color="light" light expand="md"> |
|||
<Link to="/" className="navbar-brand"> |
|||
<div className="d-flex flex-row"> |
|||
<div className="align-self-center"> |
|||
<img src={logo} alt="logo" height="32" /> |
|||
</div> |
|||
<div className="align-self-center ml-2"> |
|||
<div className="logo-text"> |
|||
<h1> |
|||
Plateforme de vote |
|||
<small>Jugement Majoritaire</small> |
|||
</h1> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Link> |
|||
<NavbarToggler onClick={this.toggle} /> |
|||
<Collapse isOpen={this.state.isOpen} navbar> |
|||
<Nav className="ml-auto" navbar> |
|||
<NavItem> |
|||
<Link className="text-primary nav-link" to="/create-election/"> |
|||
<FontAwesomeIcon icon={faRocket} className="mr-2" /> Démarrer |
|||
un vote |
|||
</Link> |
|||
</NavItem> |
|||
</Nav> |
|||
</Collapse> |
|||
</Navbar> |
|||
</header> |
|||
); |
|||
} |
|||
} |
|||
export default Header; |
|||
export default Header; |
1102
src/components/views/CreateElection.jsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,97 +1,134 @@ |
|||
import React, {Component} from "react"; |
|||
import {Button, Col, Container, Row} from "reactstrap"; |
|||
import {Link} from 'react-router-dom'; |
|||
import { faCopy, faUsers } from '@fortawesome/free-solid-svg-icons'; |
|||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; |
|||
import React, { Component } from "react"; |
|||
import { Button, Col, Container, Row } from "reactstrap"; |
|||
import { Link } from "react-router-dom"; |
|||
import { faCopy, faUsers } from "@fortawesome/free-solid-svg-icons"; |
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
|||
import logoLine from "../../logos/logo-line-white.svg"; |
|||
|
|||
|
|||
class UnknownView extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
const electionSlug = this.props.match.params.handle; |
|||
this.state = { |
|||
urlOfVote: "https://" + window.location.hostname + "/vote/" + electionSlug, |
|||
urlOfResult: "https://" + window.location.hostname + "/result/" + electionSlug |
|||
}; |
|||
this.urlVoteField = React.createRef(); |
|||
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"); |
|||
constructor(props) { |
|||
super(props); |
|||
const electionSlug = this.props.match.params.handle; |
|||
this.state = { |
|||
urlOfVote: |
|||
"https://" + window.location.hostname + "/vote/" + electionSlug, |
|||
urlOfResult: |
|||
"https://" + window.location.hostname + "/result/" + electionSlug |
|||
}; |
|||
|
|||
handleClickOnCopyResult=(event)=>{ |
|||
const input = this.urlResultField.current; |
|||
input.focus(); |
|||
input.select(); |
|||
document.execCommand("copy"); |
|||
}; |
|||
|
|||
render(){ |
|||
return( |
|||
<Container> |
|||
<Row> |
|||
<Link to="/" className="d-block ml-auto mr-auto mb-4"><img src={logoLine} alt="logo" height="128" /></Link> |
|||
</Row> |
|||
<Row className="mt-4"> |
|||
<Col className="text-center offset-lg-3" lg="6"><h2>Vote créé avec succès !</h2> |
|||
<p className="mt-4 mb-1">Vous pouvez maintenant partager le lien du vote aux 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"/>Copier</Button> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<p className="mt-4 mb-1">Voici le lien vers les résultats du vote en temps réel :</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"/>Copier</Button> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
|
|||
</Col> |
|||
</Row> |
|||
<Row className="mt-4 mb-4" > |
|||
<Col > |
|||
<div className=" bg-warning text-white p-2 "><p className="m-0 p-0 text-center">Conservez ces liens précieusement !</p> |
|||
<p className="small m-2 p-0"><b>ATTENTION</b> : Vous ne les retrouverez pas ailleurs et nous ne serons pas capable de vous les communiquer. Vous pouvez par exemple les enregistrer dans les favoris de votre |
|||
navigateur.</p></div> |
|||
</Col> |
|||
</Row> |
|||
|
|||
<Row className="mt-4 mb-4" > |
|||
<Col className="text-center"> |
|||
<Link to={ "/vote/" + this.props.match.params.handle} className="btn btn-success"><FontAwesomeIcon icon={faUsers} className="mr-2"/>Participer maintenant !</Link> |
|||
</Col> |
|||
</Row> |
|||
</Container> |
|||
) |
|||
} |
|||
this.urlVoteField = React.createRef(); |
|||
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(); |
|||
input.select(); |
|||
document.execCommand("copy"); |
|||
}; |
|||
|
|||
render() { |
|||
return ( |
|||
<Container> |
|||
<Row> |
|||
<Link to="/" className="d-block ml-auto mr-auto mb-4"> |
|||
<img src={logoLine} alt="logo" height="128" /> |
|||
</Link> |
|||
</Row> |
|||
<Row className="mt-4"> |
|||
<Col className="text-center offset-lg-3" lg="6"> |
|||
<h2>Vote créé avec succès !</h2> |
|||
<p className="mt-4 mb-1"> |
|||
Vous pouvez maintenant partager le lien du vote aux 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" /> |
|||
Copier |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
<p className="mt-4 mb-1"> |
|||
Voici le lien vers les résultats du vote en temps réel : |
|||
</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" /> |
|||
Copier |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
<Row className="mt-4 mb-4"> |
|||
<Col> |
|||
<div className=" bg-warning text-white p-2 "> |
|||
<p className="m-0 p-0 text-center"> |
|||
Conservez ces liens précieusement ! |
|||
</p> |
|||
<p className="small m-2 p-0"> |
|||
<b>ATTENTION</b> : Vous ne les retrouverez pas ailleurs et nous |
|||
ne serons pas capable de vous les communiquer. Vous pouvez par |
|||
exemple les enregistrer dans les favoris de votre navigateur. |
|||
</p> |
|||
</div> |
|||
</Col> |
|||
</Row> |
|||
|
|||
<Row className="mt-4 mb-4"> |
|||
<Col className="text-center"> |
|||
<Link |
|||
to={"/vote/" + this.props.match.params.handle} |
|||
className="btn btn-success" |
|||
> |
|||
<FontAwesomeIcon icon={faUsers} className="mr-2" /> |
|||
Participer maintenant ! |
|||
</Link> |
|||
</Col> |
|||
</Row> |
|||
</Container> |
|||
); |
|||
} |
|||
} |
|||
export default UnknownView; |
@ -1,59 +1,90 @@ |
|||
import React, {Component} from "react"; |
|||
import { Container, Row, Col,Button, Input } from 'reactstrap'; |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faRocket } from '@fortawesome/free-solid-svg-icons'; |
|||
import { Redirect } from 'react-router-dom'; |
|||
import React, { Component } from "react"; |
|||
import { Container, Row, Col, Button, Input } from "reactstrap"; |
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; |
|||
import { faRocket } from "@fortawesome/free-solid-svg-icons"; |
|||
import { Redirect } from "react-router-dom"; |
|||
import logoLine from "../../logos/logo-line-white.svg"; |
|||
|
|||
class Home extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
title:null, |
|||
redirect:false, |
|||
}; |
|||
this.focusInput= React.createRef(); |
|||
} |
|||
|
|||
handleSubmit = (event) => { |
|||
event.preventDefault(); |
|||
this.setState({redirect: true}); |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
title: null, |
|||
redirect: false |
|||
}; |
|||
this.focusInput = React.createRef(); |
|||
} |
|||
|
|||
handleChangeTitle= (event) => { |
|||
this.setState({title: event.target.value}); |
|||
}; |
|||
handleSubmit = event => { |
|||
event.preventDefault(); |
|||
this.setState({ redirect: true }); |
|||
}; |
|||
|
|||
render(){ |
|||
const redirect = this.state.redirect; |
|||
handleChangeTitle = event => { |
|||
this.setState({ title: event.target.value }); |
|||
}; |
|||
|
|||
if (redirect) { |
|||
return <Redirect to={ '/create-election/?title='+encodeURIComponent(this.state.title)} />; |
|||
} |
|||
return( |
|||
<Container> |
|||
<form onSubmit={this.handleSubmit} autoComplete="off"> |
|||
<Row> |
|||
<img src={logoLine} alt="logo" height="128" className="d-block ml-auto mr-auto mb-4"/> |
|||
</Row> |
|||
<Row> |
|||
<Col className="text-center"><h3>Simple et gratuit : organisez un vote à l'aide du Jugement Majoritaire.</h3></Col> |
|||
</Row> |
|||
<Row className="mt-2"> |
|||
<Col xs="12" md="9" xl="6" className="offset-xl-2"> |
|||
<Input placeholder="Saisissez ici la question de votre vote" innerRef={this.focusInput} autoFocus required className="mt-2" name="title" value={this.state.title?this.state.title:""} onChange={this.handleChangeTitle} maxLength="250" /> |
|||
</Col> |
|||
<Col xs="12" md="3" xl="2"> |
|||
<Button type="submit" className="btn btn-block btn-secondary mt-2" ><FontAwesomeIcon icon={faRocket} className="mr-2"/>Lancer</Button> |
|||
</Col> |
|||
</Row> |
|||
<Row className="mt-4"> |
|||
<Col className="text-center"><p>Pas de publicité et pas de cookie publicitaire.</p></Col> |
|||
</Row> |
|||
</form> |
|||
</Container> |
|||
) |
|||
}; |
|||
render() { |
|||
const redirect = this.state.redirect; |
|||
|
|||
if (redirect) { |
|||
return ( |
|||
<Redirect |
|||
to={"/create-election/?title=" + encodeURIComponent(this.state.title)} |
|||
/> |
|||
); |
|||
} |
|||
return ( |
|||
<Container> |
|||
<form onSubmit={this.handleSubmit} autoComplete="off"> |
|||
<Row> |
|||
<img |
|||
src={logoLine} |
|||
alt="logo" |
|||
height="128" |
|||
className="d-block ml-auto mr-auto mb-4" |
|||
/> |
|||
</Row> |
|||
<Row> |
|||
<Col className="text-center"> |
|||
<h3> |
|||
Simple et gratuit : organisez un vote à l'aide du Jugement |
|||
Majoritaire. |
|||
</h3> |
|||
</Col> |
|||
</Row> |
|||
<Row className="mt-2"> |
|||
<Col xs="12" md="9" xl="6" className="offset-xl-2"> |
|||
<Input |
|||
placeholder="Saisissez ici la question de votre vote" |
|||
innerRef={this.focusInput} |
|||
autoFocus |
|||
required |
|||
className="mt-2" |
|||
name="title" |
|||
value={this.state.title ? this.state.title : ""} |
|||
onChange={this.handleChangeTitle} |
|||
maxLength="250" |
|||
/> |
|||
</Col> |
|||
<Col xs="12" md="3" xl="2"> |
|||
<Button |
|||
type="submit" |
|||
className="btn btn-block btn-secondary mt-2" |
|||
> |
|||
<FontAwesomeIcon icon={faRocket} className="mr-2" /> |
|||
Lancer |
|||
</Button> |
|||
</Col> |
|||
</Row> |
|||
<Row className="mt-4"> |
|||
<Col className="text-center"> |
|||
<p>Pas de publicité et pas de cookie publicitaire.</p> |
|||
</Col> |
|||
</Row> |
|||
</form> |
|||
</Container> |
|||
); |
|||
} |
|||
} |
|||
export default Home; |
@ -1,243 +1,344 @@ |
|||
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 { grades } from '../../Util'; |
|||
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 { grades } from "../../Util"; |
|||
|
|||
class Result extends Component { |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
candidates: [], |
|||
title: null, |
|||
numGrades: 0, |
|||
colSizeCandidateLg: 4, |
|||
colSizeCandidateMd: 6, |
|||
colSizeCandidateXs: 12, |
|||
colSizeGradeLg: 1, |
|||
colSizeGradeMd: 1, |
|||
colSizeGradeXs: 1, |
|||
collapseGraphics: false, |
|||
collapseProfiles: false, |
|||
redirectLost: false, |
|||
electionGrades: grades |
|||
}; |
|||
} |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
candidates: [], |
|||
title: null, |
|||
numGrades: 0, |
|||
colSizeCandidateLg: 4, |
|||
colSizeCandidateMd: 6, |
|||
colSizeCandidateXs: 12, |
|||
colSizeGradeLg: 1, |
|||
colSizeGradeMd: 1, |
|||
colSizeGradeXs: 1, |
|||
collapseGraphics: 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; |
|||
}; |
|||
|
|||
handleErrors = (response) => { |
|||
if (!response.ok) { |
|||
response.json().then( response => { |
|||
this.setState(state => ({ |
|||
redirectLost: '/unknown-election/' + encodeURIComponent(response)})); |
|||
}) |
|||
throw Error(response); |
|||
} |
|||
return response; |
|||
} |
|||
resultsToState = response => { |
|||
const candidates = response.map(c => ({ |
|||
id: c.id, |
|||
label: c.name, |
|||
profile: c.profile, |
|||
grade: c.grade, |
|||
score: c.score |
|||
})); |
|||
this.setState(state => ({ candidates: candidates })); |
|||
return response; |
|||
}; |
|||
|
|||
resultsToState = (response) => { |
|||
const candidates = response.map(c => ({ |
|||
id: c.id, label: c.name, profile: c.profile, grade:c.grade, score: c.score |
|||
})); |
|||
this.setState(state => ({candidates: candidates})); |
|||
return response; |
|||
} |
|||
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; |
|||
}; |
|||
|
|||
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) |
|||
); |
|||
|
|||
componentDidMount() { |
|||
// FIXME we should better handling logs |
|||
fetch(detailsEndpoint) |
|||
.then(this.handleErrors) |
|||
.then(response => response.json()) |
|||
.then(this.detailsToState) |
|||
.catch(error => console.log(error)); |
|||
|
|||
const electionSlug = this.props.match.params.handle; |
|||
// get results of the election |
|||
const resultsEndpoint = resolve( |
|||
process.env.REACT_APP_SERVER_URL, |
|||
"election/results/".concat(electionSlug) |
|||
); |
|||
|
|||
// get details of the election |
|||
const detailsEndpoint = resolve(process.env.REACT_APP_SERVER_URL, |
|||
'election/get/'.concat(electionSlug)); |
|||
fetch(resultsEndpoint) |
|||
.then(this.handleErrors) |
|||
.then(response => response.json()) |
|||
.then(this.resultsToState) |
|||
.catch(error => console.log(error)); |
|||
} |
|||
|
|||
fetch(detailsEndpoint) |
|||
.then(this.handleErrors) |
|||
.then(response => response.json()) |
|||
.then(this.detailsToState) |
|||
.catch(error => console.log(error)); |
|||
toggleGraphics = () => { |
|||
this.setState(state => ({ collapseGraphics: !state.collapseGraphics })); |
|||
}; |
|||
|
|||
// get results of the election |
|||
const resultsEndpoint = resolve(process.env.REACT_APP_SERVER_URL, |
|||
'election/results/'.concat(electionSlug)); |
|||
toggleProfiles = () => { |
|||
this.setState(state => ({ collapseProfiles: !state.collapseProfiles })); |
|||
}; |
|||
|
|||
fetch(resultsEndpoint) |
|||
.then(this.handleErrors) |
|||
.then(response => response.json()) |
|||
.then(this.resultsToState) |
|||
.catch(error => console.log(error)); |
|||
render() { |
|||
const { redirectLost, candidates, electionGrades } = this.state; |
|||
|
|||
if (redirectLost) { |
|||
return <Redirect to={redirectLost} />; |
|||
} |
|||
|
|||
toggleGraphics = () => { |
|||
this.setState(state => ({collapseGraphics: !state.collapseGraphics})); |
|||
}; |
|||
return ( |
|||
<Container> |
|||
<Row> |
|||
<Col xs="12"> |
|||
<h1>{this.state.title}</h1> |
|||
</Col> |
|||
</Row> |
|||
|
|||
toggleProfiles = () => { |
|||
this.setState(state => ({collapseProfiles: !state.collapseProfiles})); |
|||
}; |
|||
<Row className="mt-5"> |
|||
<Col> |
|||
<h1>Résultat du vote :</h1> |
|||
<ol> |
|||
{candidates.map((candidate, i) => { |
|||
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-light mr-2 mt-2" |
|||
style={{ |
|||
backgroundColor: electionGrades[candidate.grade].color, |
|||
color: "#fff" |
|||
}} |
|||
> |
|||
{grades[candidate.grade].label} |
|||
</span> |
|||
</li> |
|||
); |
|||
})} |
|||
</ol> |
|||
</Col> |
|||
</Row> |
|||
|
|||
render() { |
|||
|
|||
const { redirectLost, |
|||
candidates, |
|||
electionGrades |
|||
} = this.state; |
|||
|
|||
if (redirectLost) { |
|||
return (<Redirect to={redirectLost}/>) |
|||
} |
|||
|
|||
return ( |
|||
<Container> |
|||
<Row> |
|||
<Col xs="12"><h1>{this.state.title}</h1></Col> |
|||
</Row> |
|||
|
|||
<Row className="mt-5"> |
|||
<Col><h1>Résultat du vote :</h1> |
|||
<ol>{candidates.map((candidate, i) => { |
|||
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-light mr-2 mt-2" style={{ |
|||
backgroundColor: electionGrades[candidate.grade].color, |
|||
color: "#fff" |
|||
}}>{grades[candidate.grade].label}</span></li>); |
|||
})}</ol> |
|||
</Col> |
|||
</Row> |
|||
|
|||
<Row className="mt-5"> |
|||
<Col> |
|||
<Card className="bg-light text-primary"> |
|||
<CardHeader className="pointer" onClick={this.toggleGraphics}> |
|||
<h4 className={"m-0 panel-title " + (this.state.collapseGraphics ? "collapsed" : "")}>Graphique</h4> |
|||
</CardHeader> |
|||
<Collapse isOpen={this.state.collapseGraphics}> |
|||
<CardBody className="pt-5"> |
|||
<div> |
|||
<div className="median" |
|||
style={{height: (candidates.length * 28) + 30}}/> |
|||
<table style={{width: "100%"}}><tbody> |
|||
{candidates.map((candidate, i) => { |
|||
return (<tr key={i}> |
|||
<td style={{width: "30px"}}>{i + 1}</td> |
|||
{/*candidate.label*/} |
|||
<td> |
|||
<table style={{width: "100%"}}> |
|||
<tbody> |
|||
<tr> |
|||
{candidate.profile.map((value, i) => { |
|||
if (value > 0) { |
|||
let percent = value + "%"; |
|||
if (i === 0) { |
|||
percent = "auto"; |
|||
} |
|||
return (<td key={i} style={{ |
|||
width: percent, |
|||
backgroundColor: this.state.electionGrades[i].color |
|||
}}> </td>); |
|||
}else{ |
|||
return null |
|||
} |
|||
|
|||
})}</tr> |
|||
</tbody> |
|||
</table> |
|||
</td> |
|||
</tr>) |
|||
})}</tbody></table> |
|||
|
|||
</div> |
|||
<div className="mt-4"> |
|||
<small> |
|||
{candidates.map((candidate, i) => { |
|||
return ( |
|||
<span key={i}>{(i > 0) ? ", " : ""}<b>{i + 1}</b>: {candidate.label}</span>); |
|||
})} |
|||
</small> |
|||
</div> |
|||
<div className="mt-2"> |
|||
<small> |
|||
{electionGrades.map((grade, i) => { |
|||
return ( |
|||
<span key={i} className="badge badge-light mr-2 mt-2" style={{ |
|||
backgroundColor: grade.color, |
|||
color: "#fff" |
|||
}}>{grade.label}</span> |
|||
)})} |
|||
</small> |
|||
</div> |
|||
</CardBody> |
|||
</Collapse> |
|||
</Card> |
|||
</Col> |
|||
</Row> |
|||
<Row className="mt-3"> |
|||
<Col> |
|||
<Card className="bg-light text-primary"> |
|||
<CardHeader className="pointer" onClick={this.toggleProfiles}> |
|||
<h4 className={"m-0 panel-title " + (this.state.collapseProfiles ? "collapsed" : "")}>Profils |
|||
de mérites</h4> |
|||
</CardHeader> |
|||
<Collapse isOpen={this.state.collapseProfiles}> |
|||
<CardBody> |
|||
<div className="table-responsive"> |
|||
<Table className="profiles"> |
|||
<thead> |
|||
<tr> |
|||
<th>#</th> |
|||
{electionGrades.map((grade, i) => { |
|||
return (<th key={i}><span className="badge badge-light" style={{ |
|||
backgroundColor: grade.color, |
|||
color: "#fff" |
|||
}}>{grade.label} </span></th>); |
|||
})}</tr> |
|||
</thead> |
|||
<tbody>{candidates.map((candidate, i) => { |
|||
return (<tr key={i}> |
|||
<td>{i + 1 |