reformat code

pull/73/head
Pierre-Louis Guhur 4 years ago committed by guhur
parent 75d1c383f5
commit 85b03f3fc0

@ -1,5 +1,5 @@
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"; import Routes from "./Routes";
import Header from "./components/layouts/Header"; import Header from "./components/layouts/Header";

@ -1,11 +1,11 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import {MemoryRouter} from 'react-router-dom'; import { MemoryRouter } from "react-router-dom";
import Routes from './Routes'; import Routes from "./Routes";
import App from './App'; import App from "./App";
import Adapter from 'enzyme-adapter-react-16'; import Adapter from "enzyme-adapter-react-16";
import {mount, configure} from 'enzyme'; import { mount, configure } from "enzyme";
import Home from "./components/views/Home.js"; import Home from "./components/views/Home.js";
import CreateElection from "./components/views/CreateElection.js"; import CreateElection from "./components/views/CreateElection.js";
@ -13,72 +13,76 @@ import Result from "./components/views/Result.js";
import Vote from "./components/views/Vote.js"; import Vote from "./components/views/Vote.js";
import UnknownView from "./components/views/UnknownView"; import UnknownView from "./components/views/UnknownView";
configure({adapter: new Adapter()}); configure({ adapter: new Adapter() });
it("renders without crashing", () => {
it('renders without crashing', () => { const div = document.createElement("div");
const div = document.createElement('div'); ReactDOM.render(<App />, div);
ReactDOM.render(<App/>, div);
ReactDOM.unmountComponentAtNode(div); ReactDOM.unmountComponentAtNode(div);
}); });
describe("open good View component for each route", () => {
describe('open good View component for each route', () => { it("should show Home view component for `/`", () => {
const wrapper = mount(
it('should show Home view component for `/`', () => { <MemoryRouter initialEntries={["/"]}>
const wrapper = mount(<MemoryRouter initialEntries={['/']}> <Routes />
<Routes/>
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find(Home)).toHaveLength(1); expect(wrapper.find(Home)).toHaveLength(1);
expect(wrapper.find(UnknownView)).toHaveLength(0); expect(wrapper.find(UnknownView)).toHaveLength(0);
}); });
it("should show CreateElection view component for `/create-election`", () => {
it('should show CreateElection view component for `/create-election`', () => { const wrapper = mount(
const wrapper = mount(<MemoryRouter initialEntries={['/create-election']}> <MemoryRouter initialEntries={["/create-election"]}>
<Routes/> <Routes />
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find(CreateElection)).toHaveLength(1); expect(wrapper.find(CreateElection)).toHaveLength(1);
expect(wrapper.find(UnknownView)).toHaveLength(0); expect(wrapper.find(UnknownView)).toHaveLength(0);
}); });
it('should show CreateElection view component with title for `/create-election/?title=test%20with%20title`', () => { it("should show CreateElection view component with title for `/create-election/?title=test%20with%20title`", () => {
const wrapper = mount(<MemoryRouter initialEntries={['/create-election/?title=test%20with%20title']}> const wrapper = mount(
<Routes/> <MemoryRouter
initialEntries={["/create-election/?title=test%20with%20title"]}
>
<Routes />
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find(CreateElection)).toHaveLength(1); expect(wrapper.find(CreateElection)).toHaveLength(1);
expect(wrapper.find('input[name="title"]').props().defaultValue).toBe("test with title"); expect(wrapper.find('input[name="title"]').props().defaultValue).toBe(
"test with title"
);
expect(wrapper.find(UnknownView)).toHaveLength(0); expect(wrapper.find(UnknownView)).toHaveLength(0);
}); });
it('should show Vote view component for `/vote`', () => { it("should show Vote view component for `/vote`", () => {
const wrapper = mount(<MemoryRouter initialEntries={['/vote']}> const wrapper = mount(
<Routes/> <MemoryRouter initialEntries={["/vote"]}>
<Routes />
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find(Vote)).toHaveLength(1); expect(wrapper.find(Vote)).toHaveLength(1);
expect(wrapper.find(UnknownView)).toHaveLength(0); expect(wrapper.find(UnknownView)).toHaveLength(0);
}); });
it('should show Result view component for `/result`', () => { it("should show Result view component for `/result`", () => {
const wrapper = mount(<MemoryRouter initialEntries={['/result']}> const wrapper = mount(
<Routes/> <MemoryRouter initialEntries={["/result"]}>
<Routes />
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find(Result)).toHaveLength(1); expect(wrapper.find(Result)).toHaveLength(1);
expect(wrapper.find(UnknownView)).toHaveLength(0); expect(wrapper.find(UnknownView)).toHaveLength(0);
}); });
it("should show UnknownView view component for `/aaabbbcccddd`", () => {
it('should show UnknownView view component for `/aaabbbcccddd`', () => { const wrapper = mount(
const wrapper = mount(<MemoryRouter initialEntries={['/aaabbbcccddd']} initialIndex={0}> <MemoryRouter initialEntries={["/aaabbbcccddd"]} initialIndex={0}>
<Routes/> <Routes />
</MemoryRouter> </MemoryRouter>
); );
expect(wrapper.find(UnknownView)).toHaveLength(1); expect(wrapper.find(UnknownView)).toHaveLength(1);
}); });
}); });

@ -1,5 +1,5 @@
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"; import Home from "./components/views/Home";
import CreateElection from "./components/views/CreateElection"; import CreateElection from "./components/views/CreateElection";
@ -11,7 +11,8 @@ import CreateSuccess from "./components/views/CreateSuccess";
import VoteSuccess from "./components/views/VoteSuccess"; import VoteSuccess from "./components/views/VoteSuccess";
function Routes() { function Routes() {
return (<main className="d-flex flex-column justify-content-center" > return (
<main className="d-flex flex-column justify-content-center">
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route path="/create-election" component={CreateElection} /> <Route path="/create-election" component={CreateElection} />

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

@ -1,47 +1,53 @@
import React, {Component} from "react"; import React, { Component } from "react";
import ModalConfirm from "./ModalConfirm"; import ModalConfirm from "./ModalConfirm";
class ButtonWithConfirm extends Component { class ButtonWithConfirm extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this._modalConfirm=React.createRef(); this._modalConfirm = React.createRef();
this.state={ this.state = {
focused:false focused: false
} };
} }
getComponent= (key) => { getComponent = key => {
return this.props.children.filter( (comp) => { return this.props.children.filter(comp => {
return comp.key === key; return comp.key === key;
}); });
}; };
render() { render() {
const classNames=this.props.className.split(" "); const classNames = this.props.className.split(" ");
let classNameForDiv=""; let classNameForDiv = "";
let classNameForButton=""; let classNameForButton = "";
classNames.forEach(function(className){ classNames.forEach(function(className) {
if(className==="input-group-prepend" || className==="input-group-append" ){ if (
classNameForDiv+=" "+className; className === "input-group-prepend" ||
}else{ className === "input-group-append"
classNameForButton+=" "+className; ) {
classNameForDiv += " " + className;
} else {
classNameForButton += " " + className;
} }
}); });
return ( return (
<div className={classNameForDiv}> <div className={classNameForDiv}>
<button <button
type="button" type="button"
className={classNameForButton} className={classNameForButton}
onClick={() => { this._modalConfirm.current.toggle() }} onClick={() => {
this._modalConfirm.current.toggle();
}}
tabIndex={this.props.tabIndex} tabIndex={this.props.tabIndex}
>{this.getComponent("button")} >
{this.getComponent("button")}
</button> </button>
<ModalConfirm className={this.props.modalClassName} ref={this._modalConfirm}> <ModalConfirm
className={this.props.modalClassName}
ref={this._modalConfirm}
>
<div key="title">{this.getComponent("modal-title")}</div> <div key="title">{this.getComponent("modal-title")}</div>
<div key="body">{this.getComponent("modal-body")}</div> <div key="body">{this.getComponent("modal-body")}</div>
<div key="confirm">{this.getComponent("modal-confirm")}</div> <div key="confirm">{this.getComponent("modal-confirm")}</div>

@ -1,13 +1,11 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
class HelpButton extends Component { class HelpButton extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
tooltipOpen: false tooltipOpen: false
}; };
@ -31,14 +29,47 @@ class HelpButton extends Component {
return ( return (
<span> <span>
<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"}}> {this.state.tooltipOpen ? (
<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> <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} {this.props.children}
</span>:<span />}
</span> </span>
<FontAwesomeIcon icon={faQuestionCircle} onMouseOver={this.showTooltip} onMouseOut={this.hideTooltip}/> ) : (
<span />
)}
</span>
<FontAwesomeIcon
icon={faQuestionCircle}
onMouseOver={this.showTooltip}
onMouseOut={this.hideTooltip}
/>
</span> </span>
); );
} }
} }

@ -1,5 +1,5 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
class ModalConfirm extends Component { class ModalConfirm extends Component {
constructor(props) { constructor(props) {
@ -15,22 +15,34 @@ class ModalConfirm extends Component {
}); });
}; };
getComponent= (key) => { getComponent = key => {
return this.props.children.filter( (comp) => { return this.props.children.filter(comp => {
return comp.key === key; return comp.key === key;
}); });
}; };
render() { render() {
return ( return (
<Modal isOpen={this.state.modal} toggle={this.toggle} className={this.props.className+" modal-dialog-centered"} > <Modal
<ModalHeader toggle={this.toggle}>{this.getComponent("title")}</ModalHeader> isOpen={this.state.modal}
<ModalBody> toggle={this.toggle}
{this.getComponent("body")} className={this.props.className + " modal-dialog-centered"}
</ModalBody> >
<ModalHeader toggle={this.toggle}>
{this.getComponent("title")}
</ModalHeader>
<ModalBody>{this.getComponent("body")}</ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary-outline" className="text-primary border-primary" onClick={this.toggle}>{this.getComponent("cancel")}</Button> <Button
<Button color="primary" onClick={this.toggle}>{this.getComponent("confirm")}</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> </ModalFooter>
</Modal> </Modal>
); );

@ -1,27 +1,23 @@
import React, {Component} from "react"; import React, { Component } from "react";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
class Footer extends Component { class Footer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
}
} }
render(){ render() {
return( return (
<footer className="text-center"> <footer className="text-center">
<Link to="/">Accueil</Link> <Link to="/">Accueil</Link>
<span className="m-2">-</span> <span className="m-2">-</span>
<a href="https://github.com/MieuxVoter">Code source</a> <a href="https://github.com/MieuxVoter">Code source</a>
<span className="m-2">-</span> <span className="m-2">-</span>
<a href="https://mieuxvoter.fr/">Qui sommes nous ?</a> <a href="https://mieuxvoter.fr/">Qui sommes nous ?</a>
<div className="mt-2"> <div className="mt-2">MieuxVoter &copy;</div>
MieuxVoter &copy;
</div>
</footer> </footer>
) );
} }
} }
export default Footer; export default Footer;

@ -1,19 +1,12 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { import { Collapse, Navbar, NavbarToggler, Nav, NavItem } from "reactstrap";
Collapse, import { Link } from "react-router-dom";
Navbar,
NavbarToggler,
Nav,
NavItem
} from 'reactstrap'
import {Link} from "react-router-dom";
import logo from '../../logos/logo-color.svg'; import logo from "../../logos/logo-color.svg";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {faRocket} from "@fortawesome/free-solid-svg-icons"; import { faRocket } from "@fortawesome/free-solid-svg-icons";
class Header extends Component { class Header extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -29,21 +22,21 @@ class Header extends Component {
}); });
} }
render(){ render() {
return ( return (
<header> <header>
<Navbar color="light" light expand="md"> <Navbar color="light" light expand="md">
<Link to="/" className="navbar-brand"> <Link to="/" className="navbar-brand">
<div className="d-flex flex-row"> <div className="d-flex flex-row">
<div className="align-self-center"> <div className="align-self-center">
<img src={logo} alt="logo" height="32"/> <img src={logo} alt="logo" height="32" />
</div> </div>
<div className="align-self-center ml-2"> <div className="align-self-center ml-2">
<div className="logo-text"> <div className="logo-text">
<h1>Plateforme de vote <h1>
Plateforme de vote
<small>Jugement Majoritaire</small> <small>Jugement Majoritaire</small>
</h1> </h1>
</div> </div>
</div> </div>
</div> </div>
@ -52,7 +45,10 @@ class Header extends Component {
<Collapse isOpen={this.state.isOpen} navbar> <Collapse isOpen={this.state.isOpen} navbar>
<Nav className="ml-auto" navbar> <Nav className="ml-auto" navbar>
<NavItem> <NavItem>
<Link className="text-primary nav-link" to="/create-election/"><FontAwesomeIcon icon={faRocket} className="mr-2"/> Démarrer un vote</Link> <Link className="text-primary nav-link" to="/create-election/">
<FontAwesomeIcon icon={faRocket} className="mr-2" /> Démarrer
un vote
</Link>
</NavItem> </NavItem>
</Nav> </Nav>
</Collapse> </Collapse>

@ -1,5 +1,5 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { Redirect } from 'react-router-dom'; import { Redirect } from "react-router-dom";
import { import {
Collapse, Collapse,
Container, Container,
@ -9,208 +9,243 @@ import {
Label, Label,
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
Button, Card, CardBody Button,
} from 'reactstrap'; Card,
CardBody
import {toast, ToastContainer} from 'react-toastify'; } from "reactstrap";
import 'react-toastify/dist/ReactToastify.css';
import { resolve } from 'url'; import { toast, ToastContainer } from "react-toastify";
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, faCogs } from '@fortawesome/free-solid-svg-icons'; import {
faPlus,
import { grades } from '../../Util'; faTrashAlt,
import { ReactMultiEmail, isEmail } from 'react-multi-email'; faCheck,
import 'react-multi-email/style.css'; faCogs
} from "@fortawesome/free-solid-svg-icons";
const DragHandle = sortableHandle(({children}) => <span className="input-group-text indexNumber">{children}</span>); import { grades } from "../../Util";
import { ReactMultiEmail, isEmail } from "react-multi-email";
const SortableCandidate = sortableElement(({candidate, sortIndex, form}) => <li className="sortable"> import "react-multi-email/style.css";
const DragHandle = sortableHandle(({ children }) => (
<span className="input-group-text indexNumber">{children}</span>
));
const SortableCandidate = sortableElement(({ candidate, sortIndex, form }) => (
<li className="sortable">
<Row key={"rowCandidate" + sortIndex}> <Row key={"rowCandidate" + sortIndex}>
<Col> <Col>
<InputGroup > <InputGroup>
<InputGroupAddon addonType="prepend" > <InputGroupAddon addonType="prepend">
<DragHandle > <DragHandle>
<span >{sortIndex + 1}</span> <span>{sortIndex + 1}</span>
</DragHandle> </DragHandle>
</InputGroupAddon> </InputGroupAddon>
<Input type="text" value={candidate.label} <Input
onChange={(event) => form.editCandidateLabel(event, sortIndex)} type="text"
onKeyPress={(event) => form.handleKeypressOnCandidateLabel(event, sortIndex)} value={candidate.label}
onChange={event => form.editCandidateLabel(event, sortIndex)}
onKeyPress={event =>
form.handleKeypressOnCandidateLabel(event, sortIndex)
}
placeholder="Nom du candidat ou de la proposition ..." placeholder="Nom du candidat ou de la proposition ..."
tabIndex={ sortIndex + 1} tabIndex={sortIndex + 1}
innerRef={(ref) => form.candidateInputs[sortIndex] = ref} innerRef={ref => (form.candidateInputs[sortIndex] = ref)}
maxLength="250"/> maxLength="250"
/>
<ButtonWithConfirm className="btn btn-primary input-group-append border-light"> <ButtonWithConfirm className="btn btn-primary input-group-append border-light">
<div key="button"><FontAwesomeIcon icon={faTrashAlt} /></div> <div key="button">
<FontAwesomeIcon icon={faTrashAlt} />
</div>
<div key="modal-title">Suppression ?</div> <div key="modal-title">Suppression ?</div>
<div key="modal-body">Êtes-vous sûr de vouloir supprimer {(candidate.label!=="")?<b>"{candidate.label}"</b>:<span>la ligne {sortIndex+1}</span>} ? <div key="modal-body">
Êtes-vous sûr de vouloir supprimer{" "}
{candidate.label !== "" ? (
<b>"{candidate.label}"</b>
) : (
<span>la ligne {sortIndex + 1}</span>
)}{" "}
?
</div>
<div
key="modal-confirm"
onClick={() => form.removeCandidate(sortIndex)}
>
Oui
</div> </div>
<div key="modal-confirm" onClick={() => form.removeCandidate(sortIndex)}>Oui</div>
<div key="modal-cancel">Non</div> <div key="modal-cancel">Non</div>
</ButtonWithConfirm> </ButtonWithConfirm>
</InputGroup> </InputGroup>
</Col> </Col>
<Col xs="auto" className="align-self-center pl-0"> <Col xs="auto" className="align-self-center pl-0">
<HelpButton> <HelpButton>
Saisissez ici le nom de votre candidat ou de votre proposition (250 caractères max.) Saisissez ici le nom de votre candidat ou de votre proposition (250
caractères max.)
</HelpButton> </HelpButton>
</Col> </Col>
</Row> </Row>
</li>
));
</li>); const SortableCandidatesContainer = sortableContainer(({ items, form }) => {
return (
const SortableCandidatesContainer = sortableContainer(({items, form}) => { <ul className="sortable">
return <ul className="sortable">{items.map((candidate, index) => ( {items.map((candidate, index) => (
<SortableCandidate key={`item-${index}`} index={index} sortIndex={index} candidate={candidate} form={form}/> <SortableCandidate
))}</ul>; key={`item-${index}`}
index={index}
sortIndex={index}
candidate={candidate}
form={form}
/>
))}
</ul>
);
}); });
class CreateElection extends Component { class CreateElection extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
//default value : start now //default value : start now
const startedAt = new Date(); const startedAt = new Date();
const startedHour= startedAt.getHours(); const startedHour = startedAt.getHours();
//default value : finish in one week //default value : finish in one week
const finishedAt = new Date(startedAt.getTime() + 7 * 24 * 60 * 60 * 1000); const finishedAt = new Date(startedAt.getTime() + 7 * 24 * 60 * 60 * 1000);
this.state = { this.state = {
candidates:[{label:""},{label:""}], candidates: [{ label: "" }, { label: "" }],
numCandidatesWithLabel:0, numCandidatesWithLabel: 0,
title:null, title: null,
isVisibleTipsDragAndDropCandidate:true, isVisibleTipsDragAndDropCandidate: true,
numGrades:7, numGrades: 7,
successCreate: false, successCreate: false,
redirectTo: null, redirectTo: null,
isAdvancedOptionsOpen:false, isAdvancedOptionsOpen: false,
startedDayAt:startedAt.toISOString().substring(0, 10), startedDayAt: startedAt.toISOString().substring(0, 10),
finishedDayAt:finishedAt.toISOString().substring(0, 10), finishedDayAt: finishedAt.toISOString().substring(0, 10),
startedTimeAt:(Math.floor(startedHour/2)*2)+":00:00", startedTimeAt: Math.floor(startedHour / 2) * 2 + ":00:00",
finishedTimeAt:"23:59:59", finishedTimeAt: "23:59:59",
electorEmails:[] electorEmails: []
}; };
this.candidateInputs = []; this.candidateInputs = [];
this.focusInput= React.createRef(); this.focusInput = React.createRef();
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }
handleChangeTitle = event => {
handleChangeTitle= (event) => { this.setState({ title: event.target.value });
this.setState({title: event.target.value});
}; };
addCandidate = (event) => { addCandidate = event => {
let candidates = this.state.candidates; let candidates = this.state.candidates;
if (candidates.length < 100) { if (candidates.length < 100) {
candidates.push({label:""}); candidates.push({ label: "" });
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);
} }
}; };
removeCandidate = (index) => { removeCandidate = index => {
let candidates = this.state.candidates; let candidates = this.state.candidates;
candidates.splice(index, 1); candidates.splice(index, 1);
console.log(candidates.length); console.log(candidates.length);
if(candidates.length===0){ if (candidates.length === 0) {
candidates=[{label:""}]; candidates = [{ label: "" }];
} }
this.setState({candidates: candidates}); this.setState({ candidates: candidates });
}; };
editCandidateLabel = (event, index) => { editCandidateLabel = (event, index) => {
let candidates = this.state.candidates; let candidates = this.state.candidates;
let numLabels = 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 !== "") {
numLabels++; numLabels++;
} }
return candidate.label; return candidate.label;
}); });
this.setState({candidates: candidates, numCandidatesWithLabel:numLabels}); this.setState({
candidates: candidates,
numCandidatesWithLabel: numLabels
});
}; };
handleKeypressOnCandidateLabel = (event, index) => { handleKeypressOnCandidateLabel = (event, index) => {
if(event.key === 'Enter'){ if (event.key === "Enter") {
event.preventDefault(); event.preventDefault();
if(index+1===this.state.candidates.length){ if (index + 1 === this.state.candidates.length) {
this.addCandidate(event); this.addCandidate(event);
}else{ } else {
this.candidateInputs[index+1].focus(); this.candidateInputs[index + 1].focus();
} }
} }
}; };
onCandidatesSortEnd = ({oldIndex, newIndex}) => { onCandidatesSortEnd = ({ oldIndex, newIndex }) => {
let candidates = this.state.candidates; let candidates = this.state.candidates;
candidates = arrayMove(candidates, oldIndex, newIndex); candidates = arrayMove(candidates, oldIndex, newIndex);
this.setState({candidates: candidates}); this.setState({ candidates: candidates });
}; };
handleChangeNumGrades= (event) => { handleChangeNumGrades = event => {
this.setState({numGrades: event.target.value}); this.setState({ numGrades: event.target.value });
}; };
toggleAdvancedOptions= () => { toggleAdvancedOptions = () => {
this.setState({isAdvancedOptionsOpen: !this.state.isAdvancedOptionsOpen}); this.setState({ isAdvancedOptionsOpen: !this.state.isAdvancedOptionsOpen });
}; };
componentWillMount() { componentWillMount() {
const params = new URLSearchParams(this.props.location.search); const params = new URLSearchParams(this.props.location.search);
this.setState({title:params.get("title")?params.get("title"):""}); this.setState({ title: params.get("title") ? params.get("title") : "" });
}
}; handleSubmit() {
const { candidates, title, numGrades } = this.state;
handleSubmit () { const endpoint = resolve(process.env.REACT_APP_SERVER_URL, "election/");
const {
candidates,
title,
numGrades
} = this.state;
const endpoint = resolve(
process.env.REACT_APP_SERVER_URL,
'election/'
);
fetch(endpoint, { 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.map(c => c.label), candidates: candidates.map(c => c.label),
on_invitation_only: (this.state.electorEmails.length>0), on_invitation_only: this.state.electorEmails.length > 0,
num_grades: numGrades, num_grades: numGrades,
elector_emails: this.state.electorEmails, elector_emails: this.state.electorEmails,
started_at:this.state.startedDayAt+" "+this.state.startedTimeAt, started_at: this.state.startedDayAt + " " + this.state.startedTimeAt,
finished_at:this.state.finishedDayAt+" "+this.state.finishedTimeAt, finished_at: this.state.finishedDayAt + " " + this.state.finishedTimeAt,
time_offset:new Date().getTimezoneOffset() time_offset: new Date().getTimezoneOffset()
}) })
}) })
.then(response => response.json()) .then(response => response.json())
.then(result => this.setState(state => ({ .then(result =>
redirectTo: '/create-success/' + result.id, this.setState(state => ({
redirectTo: "/create-success/" + result.id,
successCreate: true successCreate: true
}))) }))
)
.catch(error => error); .catch(error => error);
}; }
handleSendWithoutCandidate = () => { handleSendWithoutCandidate = () => {
toast.error("Vous devez saisir au moins deux candidats !", { toast.error("Vous devez saisir au moins deux candidats !", {
@ -218,104 +253,145 @@ class CreateElection extends Component {
}); });
}; };
formatDate = (dateIsoStr,timeIsoStr) => { formatDate = (dateIsoStr, timeIsoStr) => {
let date= new Date(dateIsoStr+"T"+timeIsoStr); let date = new Date(dateIsoStr + "T" + timeIsoStr);
let day = date.getDate(); let day = date.getDate();
let month = date.getMonth() + 1; //Months are zero based let month = date.getMonth() + 1; //Months are zero based
let year = date.getFullYear(); let year = date.getFullYear();
let hours = date.getHours(); let hours = date.getHours();
let minutes = date.getMinutes(); let minutes = date.getMinutes();
if(month<10){ if (month < 10) {
month="0"+month; month = "0" + month;
} }
if(day<10){ if (day < 10) {
day="0"+day; day = "0" + day;
} }
if(hours<10){ if (hours < 10) {
hours="0"+hours; hours = "0" + hours;
} }
if(minutes<10){ if (minutes < 10) {
minutes="0"+minutes; minutes = "0" + minutes;
} }
let hoursString=hours+"h"+minutes; let hoursString = hours + "h" + minutes;
if(hoursString==="23h59"){ if (hoursString === "23h59") {
hoursString="minuit"; hoursString = "minuit";
} }
return day+"/"+month+"/"+year+" à "+hoursString; return day + "/" + month + "/" + year + " à " + hoursString;
}; };
render(){ render() {
const { successCreate, redirectTo } = this.state; const { successCreate, redirectTo } = this.state;
const { electorEmails } = this.state; const { electorEmails } = this.state;
const params = new URLSearchParams(this.props.location.search); const params = new URLSearchParams(this.props.location.search);
if (successCreate) return <Redirect to={redirectTo} />; if (successCreate) return <Redirect to={redirectTo} />;
return( return (
<Container> <Container>
<ToastContainer/> <ToastContainer />
<form onSubmit={this.handleSubmit} autoComplete="off" > <form onSubmit={this.handleSubmit} autoComplete="off">
<Row> <Row>
<Col ><h3>Démarrer un vote</h3></Col> <Col>
<h3>Démarrer un vote</h3>
</Col>
</Row> </Row>
<hr /> <hr />
<Row className="mt-4"> <Row className="mt-4">
<Col xs="12" > <Col xs="12">
<Label for="title">Question du vote :</Label> <Label for="title">Question du vote :</Label>
</Col> </Col>
<Col> <Col>
<Input placeholder="Saisissez ici la question de votre vote" tabIndex="1" name="title" id="title" innerRef={this.focusInput} autoFocus defaultValue={params.get("title")?params.get("title"):""} onChange={this.handleChangeTitle} maxLength="250" /> <Input
placeholder="Saisissez ici la question de votre vote"
tabIndex="1"
name="title"
id="title"
innerRef={this.focusInput}
autoFocus
defaultValue={params.get("title") ? params.get("title") : ""}
onChange={this.handleChangeTitle}
maxLength="250"
/>
</Col> </Col>
<Col xs="auto" className="align-self-center pl-0"> <Col xs="auto" className="align-self-center pl-0">
<HelpButton> <HelpButton>
Posez ici votre question ou introduisez simplement votre vote (250 caractères max.) Posez ici votre question ou introduisez simplement votre vote
<br /><u>Par exemple :</u> <em>Pour être mon représentant, je juge ce candidat ...</em> (250 caractères max.)
<br />
<u>Par exemple :</u>{" "}
<em>Pour être mon représentant, je juge ce candidat ...</em>
</HelpButton> </HelpButton>
</Col> </Col>
</Row> </Row>
<Row className="mt-4"> <Row className="mt-4">
<Col xs="12" > <Col xs="12">
<Label for="title">Candidats / Propositions :</Label> <Label for="title">Candidats / Propositions :</Label>
</Col> </Col>
<Col xs="12" > <Col xs="12">
<SortableCandidatesContainer items={this.state.candidates} onSortEnd={this.onCandidatesSortEnd} <SortableCandidatesContainer
form={this} useDragHandle/> items={this.state.candidates}
onSortEnd={this.onCandidatesSortEnd}
form={this}
useDragHandle
/>
</Col> </Col>
</Row> </Row>
<Row className="justify-content-between"> <Row className="justify-content-between">
<Col xs="12" sm="6" md="5" lg="4" > <Col xs="12" sm="6" md="5" lg="4">
<Button color="secondary" className="btn-block mt-2" <Button
tabIndex={this.state.candidates.length+2} color="secondary"
className="btn-block mt-2"
tabIndex={this.state.candidates.length + 2}
type="button" type="button"
onClick={(event)=>this.addCandidate(event)}> onClick={event => this.addCandidate(event)}
<FontAwesomeIcon icon={faPlus} className="mr-2" />Ajouter une proposition</Button> >
<FontAwesomeIcon icon={faPlus} className="mr-2" />
Ajouter une proposition
</Button>
</Col> </Col>
<Col xs="12" sm="6" md="12"className="text-center text-sm-right text-md-left" > <Col
<Button color="link" xs="12"
sm="6"
md="12"
className="text-center text-sm-right text-md-left"
>
<Button
color="link"
className="text-white mt-3 mb-1" className="text-white mt-3 mb-1"
onClick={this.toggleAdvancedOptions} onClick={this.toggleAdvancedOptions}
><FontAwesomeIcon icon={faCogs} className="mr-2" />Options avancées</Button> >
<FontAwesomeIcon icon={faCogs} className="mr-2" />
Options avancées
</Button>
</Col> </Col>
</Row> </Row>
<Collapse isOpen={this.state.isAdvancedOptionsOpen}> <Collapse isOpen={this.state.isAdvancedOptionsOpen}>
<Card> <Card>
<CardBody className="text-primary"> <CardBody className="text-primary">
<Row > <Row>
<Col xs="12" md="3" lg="2"> <Col xs="12" md="3" lg="2">
<Label for="title">Date de début :</Label> <Label for="title">Date de début :</Label>
</Col> </Col>
<Col xs="6" md="4" lg="3" > <Col xs="6" md="4" lg="3">
<input className="form-control" <input
className="form-control"
type="date" type="date"
value={this.state.startedDayAt} value={this.state.startedDayAt}
onChange={e => this.setState({ startedDayAt: e.target.value })} /> onChange={e =>
this.setState({ startedDayAt: e.target.value })
}
/>
</Col> </Col>
<Col xs="6" md="5" lg="3" > <Col xs="6" md="5" lg="3">
<select className="form-control" value={this.state.startedTimeAt} onChange={e => this.setState({ startedTimeAt: e.target.value })} > <select
className="form-control"
value={this.state.startedTimeAt}
onChange={e =>
this.setState({ startedTimeAt: e.target.value })
}
>
<option value="2:00:00">02h00</option> <option value="2:00:00">02h00</option>
<option value="3:00:00">03h00</option> <option value="3:00:00">03h00</option>
<option value="4:00:00">04h00</option> <option value="4:00:00">04h00</option>
@ -331,20 +407,30 @@ class CreateElection extends Component {
</select> </select>
</Col> </Col>
</Row> </Row>
<hr className="mt-2 mb-2"/> <hr className="mt-2 mb-2" />
<Row > <Row>
<Col xs="12" md="3" lg="2"> <Col xs="12" md="3" lg="2">
<Label for="title">Date de fin :</Label> <Label for="title">Date de fin :</Label>
</Col> </Col>
<Col xs="6" md="4" lg="3" > <Col xs="6" md="4" lg="3">
<input className="form-control" <input
className="form-control"
type="date" type="date"
value={this.state.finishedDayAt} value={this.state.finishedDayAt}
min={this.state.startedDayAt} min={this.state.startedDayAt}
onChange={e => this.setState({ finishedDayAt: e.target.value })} /> onChange={e =>
this.setState({ finishedDayAt: e.target.value })
}
/>
</Col> </Col>
<Col xs="6" md="5" lg="3" > <Col xs="6" md="5" lg="3">
<select className="form-control" value={this.state.finishedTimeAt} onChange={e => this.setState({ finishedTimeAt: e.target.value })} > <select
className="form-control"
value={this.state.finishedTimeAt}
onChange={e =>
this.setState({ finishedTimeAt: e.target.value })
}
>
<option value="2:00:00">02h00</option> <option value="2:00:00">02h00</option>
<option value="3:00:00">03h00</option> <option value="3:00:00">03h00</option>
<option value="4:00:00">04h00</option> <option value="4:00:00">04h00</option>
@ -360,13 +446,18 @@ class CreateElection extends Component {
</select> </select>
</Col> </Col>
</Row> </Row>
<hr className="mt-2 mb-2"/> <hr className="mt-2 mb-2" />
<Row > <Row>
<Col xs="12" md="3" lg="2"> <Col xs="12" md="3" lg="2">
<Label for="title">Mentions :</Label> <Label for="title">Mentions :</Label>
</Col> </Col>
<Col xs="10" sm="11" md="4" lg="3" > <Col xs="10" sm="11" md="4" lg="3">
<select className="form-control" tabIndex={this.state.candidates.length+3} onChange={this.handleChangeNumGrades} 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>
@ -374,23 +465,45 @@ class CreateElection extends Component {
</Col> </Col>
<Col xs="auto" className="align-self-center pl-0 "> <Col xs="auto" className="align-self-center pl-0 ">
<HelpButton> <HelpButton>
Vous pouvez choisir ici le nombre de mentions pour votre vote Vous pouvez choisir ici le nombre de mentions pour votre
<br /><u>Par exemple : </u> <em> 5 = Excellent, Très bien, bien, assez bien, passable</em> vote
<br />
<u>Par exemple : </u>{" "}
<em>
{" "}
5 = Excellent, Très bien, bien, assez bien, passable
</em>
</HelpButton> </HelpButton>
</Col> </Col>
<Col xs="12" md="9" lg="10" className="offset-xs-0 offset-md-3 offset-lg-2" > <Col
{ grades.map((mention,i) => { xs="12"
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> md="9"
}) lg="10"
} className="offset-xs-0 offset-md-3 offset-lg-2"
>
{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.numGrades ? 1 : 0.3
}}
>
{mention.label}
</span>
);
})}
</Col> </Col>
</Row> </Row>
<hr className="mt-2 mb-2"/> <hr className="mt-2 mb-2" />
<Row > <Row>
<Col xs="12" md="3" lg="2"> <Col xs="12" md="3" lg="2">
<Label for="title">Participants :</Label> <Label for="title">Participants :</Label>
</Col> </Col>
<Col xs="12" md="9" lg="10" > <Col xs="12" md="9" lg="10">
<ReactMultiEmail <ReactMultiEmail
placeholder="Saisissez ici les e-mails des participants" placeholder="Saisissez ici les e-mails des participants"
emails={electorEmails} emails={electorEmails}
@ -403,67 +516,148 @@ class CreateElection extends Component {
getLabel={( getLabel={(
email: string, email: string,
index: number, index: number,
removeEmail: (index: number) => void, removeEmail: (index: number) => void
) => { ) => {
return ( return (
<div data-tag key={index}> <div data-tag key={index}>
{email} {email}
<span data-tag-handle onClick={() => removeEmail(index)}>×</span> <span
data-tag-handle
onClick={() => removeEmail(index)}
>
×
</span>
</div> </div>
); );
}} }}
/> />
<div><small className="text-muted">Liste des e-mails à préciser si vous désirez réaliser un vote fermé.</small></div> <div>
<small className="text-muted">
Liste des e-mails à préciser si vous désirez réaliser un
vote fermé.
</small>
</div>
</Col> </Col>
</Row> </Row>
<hr className="mt-2 mb-2"/> <hr className="mt-2 mb-2" />
</CardBody> </CardBody>
</Card> </Card>
</Collapse> </Collapse>
<Row className="justify-content-end mt-2"> <Row className="justify-content-end mt-2">
<Col xs="12" md="3" > <Col xs="12" md="3">
{this.state.numCandidatesWithLabel>=2?<ButtonWithConfirm className="btn btn-success float-right btn-block" tabIndex={this.state.candidates.length+4}> {this.state.numCandidatesWithLabel >= 2 ? (
<div key="button"><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</div> <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="modal-title">Confirmez votre vote</div> <div key="modal-title">Confirmez votre vote</div>
<div key="modal-body"> <div key="modal-body">
<div className="mt-1 mb-1"> <div className="mt-1 mb-1">
<div className="text-white bg-primary p-1">Question du vote</div> <div className="text-white bg-primary p-1">
<div className="p-1 pl-3"><em>{this.state.title}</em></div> Question du vote
<div className="text-white bg-primary p-1">Candidats/Propositions</div> </div>
<div className="p-1 pl-0"><ul className="m-0 pl-4"> <div className="p-1 pl-3">
{ <em>{this.state.title}</em>
this.state.candidates.map((candidate,i) => { </div>
if(candidate.label!==""){ <div className="text-white bg-primary p-1">
return <li key={i} className="m-0">{candidate.label}</li> Candidats/Propositions
}else{ </div>
return <li key={i} className="d-none" /> <div className="p-1 pl-0">
} <ul className="m-0 pl-4">
{this.state.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">Dates</div> </ul>
<p className="p-1 pl-3">Le vote se déroulera du <b>{this.formatDate(this.state.startedDayAt,this.state.startedTimeAt)}</b> au <b>{this.formatDate(this.state.finishedDayAt,this.state.finishedTimeAt)}</b></p> </div>
<div className="text-white bg-primary p-1 mt-1">
Dates
</div>
<p className="p-1 pl-3">
Le vote se déroulera du{" "}
<b>
{this.formatDate(
this.state.startedDayAt,
this.state.startedTimeAt
)}
</b>{" "}
au{" "}
<b>
{this.formatDate(
this.state.finishedDayAt,
this.state.finishedTimeAt
)}
</b>
</p>
<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">{ grades.map((mention,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 className="text-white bg-primary p-1 mt-1">Liste des électeurs</div>
<div className="p-1 pl-3"> <div className="p-1 pl-3">
{(electorEmails.length>0)?electorEmails.join(', '):<p>Aucune adresse e-mail précisée.<br /><em>Le vote sera ouvert à tous les utilisateurs ayant le lien du vote</em></p>} {grades.map((mention, 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 className="text-white bg-primary p-1 mt-1">
Liste des électeurs
</div>
<div className="p-1 pl-3">
{electorEmails.length > 0 ? (
electorEmails.join(", ")
) : (
<p>
Aucune adresse e-mail précisée.
<br />
<em>
Le vote sera ouvert à tous les utilisateurs ayant
le lien du vote
</em>
</p>
)}
</div>
</div> </div>
</div> </div>
<div key="modal-confirm" onClick={this.handleSubmit}>
Lancer le vote
</div> </div>
<div key="modal-confirm" onClick={this.handleSubmit}>Lancer le vote</div>
<div key="modal-cancel">Annuler</div> <div key="modal-cancel">Annuler</div>
</ButtonWithConfirm>:<Button type="button" className="btn btn-dark float-right btn-block" onClick={this.handleSendWithoutCandidate}><FontAwesomeIcon icon={faCheck} className="mr-2" />Valider</Button>} </ButtonWithConfirm>
) : (
<Button
type="button"
className="btn btn-dark float-right btn-block"
onClick={this.handleSendWithoutCandidate}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
Valider
</Button>
)}
</Col> </Col>
</Row> </Row>
</form> </form>
</Container> </Container>
) );
} }
} }
export default CreateElection; export default CreateElection;

@ -1,97 +1,134 @@
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 {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"; 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; const electionSlug = this.props.match.params.handle;
this.state = { this.state = {
urlOfVote: "https://" + window.location.hostname + "/vote/" + electionSlug, urlOfVote:
urlOfResult: "https://" + window.location.hostname + "/result/" + electionSlug "https://" + window.location.hostname + "/vote/" + electionSlug,
urlOfResult:
"https://" + window.location.hostname + "/result/" + electionSlug
}; };
this.urlVoteField = React.createRef(); this.urlVoteField = React.createRef();
this.urlResultField = React.createRef(); this.urlResultField = React.createRef();
} }
handleClickOnField=(event)=>{ handleClickOnField = event => {
event.target.focus(); event.target.focus();
event.target.select(); event.target.select();
}; };
handleClickOnCopyVote=(event)=>{ handleClickOnCopyVote = event => {
const input = this.urlVoteField.current; const input = this.urlVoteField.current;
input.focus(); input.focus();
input.select(); input.select();
document.execCommand("copy"); document.execCommand("copy");
}; };
handleClickOnCopyResult=(event)=>{ handleClickOnCopyResult = event => {
const input = this.urlResultField.current; const input = this.urlResultField.current;
input.focus(); input.focus();
input.select(); input.select();
document.execCommand("copy"); document.execCommand("copy");
}; };
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>Vote créé avec succès !</h2> <Col className="text-center offset-lg-3" lg="6">
<p className="mt-4 mb-1">Vous pouvez maintenant partager le lien du vote aux participants :</p> <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 "> <div className="input-group ">
<input type="text" className="form-control" ref={this.urlVoteField} <input
value={this.state.urlOfVote} readOnly onClick={this.handleClickOnField} /> type="text"
className="form-control"
ref={this.urlVoteField}
value={this.state.urlOfVote}
readOnly
onClick={this.handleClickOnField}
/>
<div className="input-group-append"> <div className="input-group-append">
<Button className="btn btn-outline-light" onClick={this.handleClickOnCopyVote} <Button
type="button"> className="btn btn-outline-light"
<FontAwesomeIcon icon={faCopy} className="mr-2"/>Copier</Button> onClick={this.handleClickOnCopyVote}
type="button"
>
<FontAwesomeIcon icon={faCopy} className="mr-2" />
Copier
</Button>
</div> </div>
</div> </div>
<p className="mt-4 mb-1">Voici le lien vers les résultats du vote en temps réel :</p> <p className="mt-4 mb-1">
Voici le lien vers les résultats du vote en temps réel :
</p>
<div className="input-group "> <div className="input-group ">
<input type="text" className="form-control" ref={this.urlResultField} <input
value={this.state.urlOfResult} readOnly onClick={this.handleClickOnField} /> type="text"
className="form-control"
ref={this.urlResultField}
value={this.state.urlOfResult}
readOnly
onClick={this.handleClickOnField}
/>
<div className="input-group-append"> <div className="input-group-append">
<Button className="btn btn-outline-light" onClick={this.handleClickOnCopyResult} <Button
type="button"> className="btn btn-outline-light"
<FontAwesomeIcon icon={faCopy} className="mr-2"/>Copier</Button> onClick={this.handleClickOnCopyResult}
type="button"
>
<FontAwesomeIcon icon={faCopy} className="mr-2" />
Copier
</Button>
</div> </div>
</div> </div>
</Col> </Col>
</Row> </Row>
<Row className="mt-4 mb-4" > <Row className="mt-4 mb-4">
<Col > <Col>
<div className=" bg-warning text-white p-2 "><p className="m-0 p-0 text-center">Conservez ces liens précieusement !</p> <div className=" bg-warning text-white p-2 ">
<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 <p className="m-0 p-0 text-center">
navigateur.</p></div> 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> </Col>
</Row> </Row>
<Row className="mt-4 mb-4" > <Row className="mt-4 mb-4">
<Col className="text-center"> <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> <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>
) );
} }
} }
export default UnknownView; export default UnknownView;

@ -1,59 +1,90 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { Container, Row, Col,Button, Input } from 'reactstrap'; import { Container, Row, Col, Button, Input } from "reactstrap";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faRocket } from '@fortawesome/free-solid-svg-icons'; import { faRocket } from "@fortawesome/free-solid-svg-icons";
import { Redirect } from 'react-router-dom'; import { Redirect } from "react-router-dom";
import logoLine from "../../logos/logo-line-white.svg"; import logoLine from "../../logos/logo-line-white.svg";
class Home extends Component { class Home extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
title:null, title: null,
redirect:false, redirect: false
}; };
this.focusInput= React.createRef(); this.focusInput = React.createRef();
} }
handleSubmit = (event) => { handleSubmit = event => {
event.preventDefault(); event.preventDefault();
this.setState({redirect: true}); this.setState({ redirect: true });
}; };
handleChangeTitle= (event) => { handleChangeTitle = event => {
this.setState({title: event.target.value}); this.setState({ title: event.target.value });
}; };
render(){ render() {
const redirect = this.state.redirect; const redirect = this.state.redirect;
if (redirect) { if (redirect) {
return <Redirect to={ '/create-election/?title='+encodeURIComponent(this.state.title)} />; return (
<Redirect
to={"/create-election/?title=" + encodeURIComponent(this.state.title)}
/>
);
} }
return( return (
<Container> <Container>
<form onSubmit={this.handleSubmit} autoComplete="off"> <form onSubmit={this.handleSubmit} autoComplete="off">
<Row> <Row>
<img src={logoLine} alt="logo" height="128" className="d-block ml-auto mr-auto mb-4"/> <img
src={logoLine}
alt="logo"
height="128"
className="d-block ml-auto mr-auto mb-4"
/>
</Row> </Row>
<Row> <Row>
<Col className="text-center"><h3>Simple et gratuit : organisez un vote à l'aide du Jugement Majoritaire.</h3></Col> <Col className="text-center">
<h3>
Simple et gratuit : organisez un vote à l'aide du Jugement
Majoritaire.
</h3>
</Col>
</Row> </Row>
<Row className="mt-2"> <Row className="mt-2">
<Col xs="12" md="9" xl="6" className="offset-xl-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" /> <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>
<Col xs="12" md="3" xl="2"> <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> <Button
type="submit"
className="btn btn-block btn-secondary mt-2"
>
<FontAwesomeIcon icon={faRocket} className="mr-2" />
Lancer
</Button>
</Col> </Col>
</Row> </Row>
<Row className="mt-4"> <Row className="mt-4">
<Col className="text-center"><p>Pas de publicité et pas de cookie publicitaire.</p></Col> <Col className="text-center">
<p>Pas de publicité et pas de cookie publicitaire.</p>
</Col>
</Row> </Row>
</form> </form>
</Container> </Container>
) );
}; }
} }
export default Home; export default Home;

@ -1,11 +1,19 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { Redirect } from 'react-router-dom'; import { Redirect } from "react-router-dom";
import { resolve } from 'url'; import { resolve } from "url";
import {Container, Row, Col, Collapse, Card, CardHeader, CardBody, Table} from "reactstrap"; import {
import { grades } from '../../Util'; Container,
Row,
Col,
Collapse,
Card,
CardHeader,
CardBody,
Table
} from "reactstrap";
import { grades } from "../../Util";
class Result extends Component { class Result extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -25,43 +33,63 @@ class Result extends Component {
}; };
} }
handleErrors = (response) => { handleErrors = response => {
if (!response.ok) { if (!response.ok) {
response.json().then( response => { response.json().then(response => {
this.setState(state => ({ this.setState(state => ({
redirectLost: '/unknown-election/' + encodeURIComponent(response)})); redirectLost: "/unknown-election/" + encodeURIComponent(response)
}) }));
});
throw Error(response); throw Error(response);
} }
return response; return response;
} };
resultsToState = (response) => { resultsToState = response => {
const candidates = response.map(c => ({ const candidates = response.map(c => ({
id: c.id, label: c.name, profile: c.profile, grade:c.grade, score: c.score id: c.id,
label: c.name,
profile: c.profile,
grade: c.grade,
score: c.score
})); }));
this.setState(state => ({candidates: candidates})); this.setState(state => ({ candidates: candidates }));
return response; return response;
} };
detailsToState = (response) => { detailsToState = response => {
const numGrades = response.num_grades; const numGrades = response.num_grades;
const colSizeGradeLg = Math.floor((12 - this.state.colSizeCandidateLg) / numGrades); const colSizeGradeLg = Math.floor(
const colSizeGradeMd = Math.floor((12 - this.state.colSizeCandidateMd) / numGrades); (12 - this.state.colSizeCandidateLg) / numGrades
const colSizeGradeXs = Math.floor((12 - this.state.colSizeCandidateXs) / numGrades); );
const colSizeGradeMd = Math.floor(
(12 - this.state.colSizeCandidateMd) / numGrades
);
const colSizeGradeXs = Math.floor(
(12 - this.state.colSizeCandidateXs) / numGrades
);
this.setState(state => ({ this.setState(state => ({
title: response.title, title: response.title,
numGrades: numGrades, numGrades: numGrades,
colSizeGradeLg: colSizeGradeLg, colSizeGradeLg: colSizeGradeLg,
colSizeGradeMd: colSizeGradeMd, colSizeGradeMd: colSizeGradeMd,
colSizeGradeXs: colSizeGradeXs, colSizeGradeXs: colSizeGradeXs,
colSizeCandidateLg: ((12 - colSizeGradeLg * numGrades) > 0) ? (12 - colSizeGradeLg * numGrades) : 12, colSizeCandidateLg:
colSizeCandidateMd: ((12 - colSizeGradeMd * numGrades) > 0) ? (12 - colSizeGradeMd * numGrades) : 12, 12 - colSizeGradeLg * numGrades > 0
colSizeCandidateXs: ((12 - colSizeGradeXs * numGrades) > 0) ? (12 - colSizeGradeXs * numGrades) : 12, ? 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) electionGrades: grades.slice(0, numGrades)
})); }));
return response; return response;
} };
componentDidMount() { componentDidMount() {
// FIXME we should better handling logs // FIXME we should better handling logs
@ -69,8 +97,10 @@ class Result extends Component {
const electionSlug = this.props.match.params.handle; const electionSlug = this.props.match.params.handle;
// get details of the election // get details of the election
const detailsEndpoint = resolve(process.env.REACT_APP_SERVER_URL, const detailsEndpoint = resolve(
'election/get/'.concat(electionSlug)); process.env.REACT_APP_SERVER_URL,
"election/get/".concat(electionSlug)
);
fetch(detailsEndpoint) fetch(detailsEndpoint)
.then(this.handleErrors) .then(this.handleErrors)
@ -79,52 +109,65 @@ class Result extends Component {
.catch(error => console.log(error)); .catch(error => console.log(error));
// get results of the election // get results of the election
const resultsEndpoint = resolve(process.env.REACT_APP_SERVER_URL, const resultsEndpoint = resolve(
'election/results/'.concat(electionSlug)); process.env.REACT_APP_SERVER_URL,
"election/results/".concat(electionSlug)
);
fetch(resultsEndpoint) fetch(resultsEndpoint)
.then(this.handleErrors) .then(this.handleErrors)
.then(response => response.json()) .then(response => response.json())
.then(this.resultsToState) .then(this.resultsToState)
.catch(error => console.log(error)); .catch(error => console.log(error));
} }
toggleGraphics = () => { toggleGraphics = () => {
this.setState(state => ({collapseGraphics: !state.collapseGraphics})); this.setState(state => ({ collapseGraphics: !state.collapseGraphics }));
}; };
toggleProfiles = () => { toggleProfiles = () => {
this.setState(state => ({collapseProfiles: !state.collapseProfiles})); this.setState(state => ({ collapseProfiles: !state.collapseProfiles }));
}; };
render() { render() {
const { redirectLost, candidates, electionGrades } = this.state;
const { redirectLost,
candidates,
electionGrades
} = this.state;
if (redirectLost) { if (redirectLost) {
return (<Redirect to={redirectLost}/>) return <Redirect to={redirectLost} />;
} }
return ( return (
<Container> <Container>
<Row> <Row>
<Col xs="12"><h1>{this.state.title}</h1></Col> <Col xs="12">
<h1>{this.state.title}</h1>
</Col>
</Row> </Row>
<Row className="mt-5"> <Row className="mt-5">
<Col><h1>Résultat du vote :</h1> <Col>
<ol>{candidates.map((candidate, i) => { <h1>Résultat du vote :</h1>
return (<li key={i} className="mt-2">{candidate.label}<span <ol>
className="badge badge-dark mr-2 mt-2">{candidate.score}%</span><span {candidates.map((candidate, i) => {
className="badge badge-light mr-2 mt-2" style={{ 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, backgroundColor: electionGrades[candidate.grade].color,
color: "#fff" color: "#fff"
}}>{grades[candidate.grade].label}</span></li>); }}
})}</ol> >
{grades[candidate.grade].label}
</span>
</li>
);
})}
</ol>
</Col> </Col>
</Row> </Row>
@ -132,20 +175,31 @@ class Result extends Component {
<Col> <Col>
<Card className="bg-light text-primary"> <Card className="bg-light text-primary">
<CardHeader className="pointer" onClick={this.toggleGraphics}> <CardHeader className="pointer" onClick={this.toggleGraphics}>
<h4 className={"m-0 panel-title " + (this.state.collapseGraphics ? "collapsed" : "")}>Graphique</h4> <h4
className={
"m-0 panel-title " +
(this.state.collapseGraphics ? "collapsed" : "")
}
>
Graphique
</h4>
</CardHeader> </CardHeader>
<Collapse isOpen={this.state.collapseGraphics}> <Collapse isOpen={this.state.collapseGraphics}>
<CardBody className="pt-5"> <CardBody className="pt-5">
<div> <div>
<div className="median" <div
style={{height: (candidates.length * 28) + 30}}/> className="median"
<table style={{width: "100%"}}><tbody> style={{ height: candidates.length * 28 + 30 }}
/>
<table style={{ width: "100%" }}>
<tbody>
{candidates.map((candidate, i) => { {candidates.map((candidate, i) => {
return (<tr key={i}> return (
<td style={{width: "30px"}}>{i + 1}</td> <tr key={i}>
<td style={{ width: "30px" }}>{i + 1}</td>
{/*candidate.label*/} {/*candidate.label*/}
<td> <td>
<table style={{width: "100%"}}> <table style={{ width: "100%" }}>
<tbody> <tbody>
<tr> <tr>
{candidate.profile.map((value, i) => { {candidate.profile.map((value, i) => {
@ -154,27 +208,41 @@ class Result extends Component {
if (i === 0) { if (i === 0) {
percent = "auto"; percent = "auto";
} }
return (<td key={i} style={{ return (
<td
key={i}
style={{
width: percent, width: percent,
backgroundColor: this.state.electionGrades[i].color backgroundColor: this.state
}}>&nbsp;</td>); .electionGrades[i].color
}else{ }}
return null >
&nbsp;
</td>
);
} else {
return null;
} }
})}
})}</tr> </tr>
</tbody> </tbody>
</table> </table>
</td> </td>
</tr>) </tr>
})}</tbody></table> );
})}
</tbody>
</table>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<small> <small>
{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>
);
})} })}
</small> </small>
</div> </div>
@ -182,11 +250,18 @@ class Result extends Component {
<small> <small>
{electionGrades.map((grade, i) => { {electionGrades.map((grade, i) => {
return ( 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: grade.color, backgroundColor: grade.color,
color: "#fff" color: "#fff"
}}>{grade.label}</span> }}
)})} >
{grade.label}
</span>
);
})}
</small> </small>
</div> </div>
</CardBody> </CardBody>
@ -198,8 +273,14 @@ class Result extends Component {
<Col> <Col>
<Card className="bg-light text-primary"> <Card className="bg-light text-primary">
<CardHeader className="pointer" onClick={this.toggleProfiles}> <CardHeader className="pointer" onClick={this.toggleProfiles}>
<h4 className={"m-0 panel-title " + (this.state.collapseProfiles ? "collapsed" : "")}>Profils <h4
de mérites</h4> className={
"m-0 panel-title " +
(this.state.collapseProfiles ? "collapsed" : "")
}
>
Profils de mérites
</h4>
</CardHeader> </CardHeader>
<Collapse isOpen={this.state.collapseProfiles}> <Collapse isOpen={this.state.collapseProfiles}>
<CardBody> <CardBody>
@ -209,34 +290,54 @@ class Result extends Component {
<tr> <tr>
<th>#</th> <th>#</th>
{electionGrades.map((grade, 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: grade.color, backgroundColor: grade.color,
color: "#fff" color: "#fff"
}}>{grade.label} </span></th>); }}
})}</tr> >
{grade.label}{" "}
</span>
</th>
);
})}
</tr>
</thead> </thead>
<tbody>{candidates.map((candidate, i) => { <tbody>
return (<tr key={i}> {candidates.map((candidate, i) => {
return (
<tr key={i}>
<td>{i + 1}</td> <td>{i + 1}</td>
{/*candidate.label*/} {/*candidate.label*/}
{candidate.profile.map((value, i) => { {candidate.profile.map((value, i) => {
return (<td key={i}>{value}%</td>); return <td key={i}>{value}%</td>;
})} })}
</tr>) </tr>
})}</tbody> );
})}
</tbody>
</Table> </Table>
</div> </div>
<small>{candidates.map((candidate, i) => { <small>
return (<span {candidates.map((candidate, i) => {
key={i}>{(i > 0) ? ", " : ""}<b>{i + 1}</b>: {candidate.label}</span>); return (
})}</small> <span key={i}>
{i > 0 ? ", " : ""}
<b>{i + 1}</b>: {candidate.label}
</span>
);
})}
</small>
</CardBody> </CardBody>
</Collapse> </Collapse>
</Card> </Card>
</Col> </Col>
</Row> </Row>
</Container> </Container>
) );
} }
} }

@ -1,34 +1,37 @@
import React, {Component} from "react"; import React, { Component } from "react";
import {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";
class UnknownElection extends Component { class UnknownElection extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
}
} }
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"><h2>Oups ! Ce vote n'existe pas ou n'est plus disponible.</h2> <Col className="text-center">
<p>N'hésitez pas à démarrer un nouveau vote</p></Col> <h2>Oups ! Ce vote n'existe pas ou n'est plus disponible.</h2>
<p>N'hésitez pas à démarrer un nouveau vote</p>
</Col>
</Row> </Row>
<Row className="mt-4" > <Row className="mt-4">
<Col className="text-center"> <Col className="text-center">
<Link to="/" className="btn btn-secondary">Revenir à l'accueil</Link> <Link to="/" className="btn btn-secondary">
Revenir à l'accueil
</Link>
</Col> </Col>
</Row> </Row>
</Container> </Container>
) );
} }
} }
export default UnknownElection; export default UnknownElection;

@ -1,33 +1,36 @@
import React, {Component} from "react"; import React, { Component } from "react";
import {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";
class UnknownView extends Component { class UnknownView extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
}
} }
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"><h2>Oups ! Cette page n'existe pas</h2></Col> <Col className="text-center">
<h2>Oups ! Cette page n'existe pas</h2>
</Col>
</Row> </Row>
<Row className="mt-4" > <Row className="mt-4">
<Col className="text-center"> <Col className="text-center">
<Link to="/" className="btn btn-secondary">Revenir à l'accueil</Link> <Link to="/" className="btn btn-secondary">
Revenir à l'accueil
</Link>
</Col> </Col>
</Row> </Row>
</Container> </Container>
) );
} }
} }
export default UnknownView; export default UnknownView;

@ -1,47 +1,45 @@
import React, {Component} from "react"; import React, { Component } from "react";
import { Redirect } from 'react-router-dom'; import { Redirect } from "react-router-dom";
import {Button, Col, Container, Row} from "reactstrap"; import { Button, Col, Container, Row } from "reactstrap";
import {toast, ToastContainer} from "react-toastify"; import { toast, ToastContainer } from "react-toastify";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {faCheck} from "@fortawesome/free-solid-svg-icons"; import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { resolve } from 'url'; import { resolve } from "url";
import { grades } from '../../Util'; import { grades } from "../../Util";
class Vote extends Component { class Vote extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
candidates:[], candidates: [],
title:null, title: null,
numGrades:0, numGrades: 0,
ratedCandidates:[], ratedCandidates: [],
colSizeCandidateLg:4, colSizeCandidateLg: 4,
colSizeCandidateMd:6, colSizeCandidateMd: 6,
colSizeCandidateXs:12, colSizeCandidateXs: 12,
colSizeGradeLg:1, colSizeGradeLg: 1,
colSizeGradeMd:1, colSizeGradeMd: 1,
colSizeGradeXs:1, colSizeGradeXs: 1,
redirectTo: null, redirectTo: null,
electionGrades: grades electionGrades: grades
}; };
} }
handleErrors = (response) => { handleErrors = response => {
if (!response.ok) { if (!response.ok) {
response.json().then( response => { response.json().then(response => {
console.log(response); console.log(response);
this.setState(state => ({ this.setState(state => ({
redirectTo: '/unknown-election/' + encodeURIComponent(response)})); redirectTo: "/unknown-election/" + encodeURIComponent(response)
}) }));
});
throw Error(response); throw Error(response);
} }
return response; return response;
} };
detailsToState = (response) => { detailsToState = response => {
const numGrades = response.num_grades; const numGrades = response.num_grades;
const candidates = response.candidates.map((c, i) => ({ const candidates = response.candidates.map((c, i) => ({
id: i, id: i,
@ -55,9 +53,15 @@ class Vote extends Component {
candidates[i] = candidates[j]; candidates[i] = candidates[j];
candidates[j] = temp; candidates[j] = temp;
} }
const colSizeGradeLg = Math.floor((12 - this.state.colSizeCandidateLg) / numGrades); const colSizeGradeLg = Math.floor(
const colSizeGradeMd = Math.floor((12 - this.state.colSizeCandidateMd) / numGrades); (12 - this.state.colSizeCandidateLg) / numGrades
const colSizeGradeXs = Math.floor((12 - this.state.colSizeCandidateXs) / numGrades); );
const colSizeGradeMd = Math.floor(
(12 - this.state.colSizeCandidateMd) / numGrades
);
const colSizeGradeXs = Math.floor(
(12 - this.state.colSizeCandidateXs) / numGrades
);
this.setState(state => ({ this.setState(state => ({
title: response.title, title: response.title,
@ -66,23 +70,31 @@ class Vote extends Component {
colSizeGradeLg: colSizeGradeLg, colSizeGradeLg: colSizeGradeLg,
colSizeGradeMd: colSizeGradeMd, colSizeGradeMd: colSizeGradeMd,
colSizeGradeXs: colSizeGradeXs, colSizeGradeXs: colSizeGradeXs,
colSizeCandidateLg: ((12 - colSizeGradeLg * numGrades) > 0) ? colSizeCandidateLg:
(12 - colSizeGradeLg * numGrades) : 12, 12 - colSizeGradeLg * numGrades > 0
colSizeCandidateMd: ((12 - colSizeGradeMd * numGrades) > 0) ? ? 12 - colSizeGradeLg * numGrades
(12 - colSizeGradeMd * numGrades) : 12, : 12,
colSizeCandidateXs: ((12 - colSizeGradeXs * numGrades) > 0) ? colSizeCandidateMd:
(12 - colSizeGradeXs * numGrades) : 12, 12 - colSizeGradeMd * numGrades > 0
? 12 - colSizeGradeMd * numGrades
: 12,
colSizeCandidateXs:
12 - colSizeGradeXs * numGrades > 0
? 12 - colSizeGradeXs * numGrades
: 12,
electionGrades: grades.slice(0, numGrades) electionGrades: grades.slice(0, numGrades)
})); }));
return response; return response;
} };
componentDidMount() { componentDidMount() {
// FIXME we should better handling logs // FIXME we should better handling logs
const electionSlug = this.props.match.params.handle; const electionSlug = this.props.match.params.handle;
const detailsEndpoint = resolve(process.env.REACT_APP_SERVER_URL, const detailsEndpoint = resolve(
'election/get/'.concat(electionSlug)); process.env.REACT_APP_SERVER_URL,
"election/get/".concat(electionSlug)
);
fetch(detailsEndpoint) fetch(detailsEndpoint)
.then(this.handleErrors) .then(this.handleErrors)
@ -91,17 +103,17 @@ class Vote extends Component {
.catch(error => console.log(error)); .catch(error => console.log(error));
} }
handleGradeClick = event => {
handleGradeClick = (event) => { let data = {
let data={ id: parseInt(event.currentTarget.getAttribute("data-id")),
id:parseInt(event.currentTarget.getAttribute("data-id")), value: parseInt(event.currentTarget.value)
value:parseInt(event.currentTarget.value)
}; };
//remove candidate //remove candidate
let ratedCandidates = this.state.ratedCandidates.filter(ratedCandidate => ratedCandidate.id !== data.id); let ratedCandidates = this.state.ratedCandidates.filter(
ratedCandidate => ratedCandidate.id !== data.id
);
ratedCandidates.push(data); ratedCandidates.push(data);
this.setState({ratedCandidates:ratedCandidates}); this.setState({ ratedCandidates: ratedCandidates });
}; };
handleSubmitWithoutAllRate = () => { handleSubmitWithoutAllRate = () => {
@ -110,104 +122,197 @@ class Vote extends Component {
}); });
}; };
handleSubmit = (event) => { handleSubmit = event => {
event.preventDefault(); event.preventDefault();
const { ratedCandidates } = this.state; const { ratedCandidates } = this.state;
const electionSlug = this.props.match.params.handle; const electionSlug = this.props.match.params.handle;
const endpoint = resolve(process.env.REACT_APP_SERVER_URL, const endpoint = resolve(
'election/vote/'); process.env.REACT_APP_SERVER_URL,
"election/vote/"
);
const gradesById = {}; const gradesById = {};
ratedCandidates.forEach(c => { gradesById[c.id] = c.value; }); ratedCandidates.forEach(c => {
gradesById[c.id] = c.value;
});
const gradesByCandidate = []; const gradesByCandidate = [];
Object.keys(gradesById) Object.keys(gradesById)
.sort() .sort()
.forEach(id => {gradesByCandidate.push(gradesById[id]);}); .forEach(id => {
gradesByCandidate.push(gradesById[id]);
});
fetch(endpoint, { fetch(endpoint, {
method: 'POST', method: "POST",
headers: {'Content-Type': 'application/json'}, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
election: electionSlug, election: electionSlug,
grades_by_candidate: gradesByCandidate, grades_by_candidate: gradesByCandidate
}) })
}) })
.then(this.handleErrors) .then(this.handleErrors)
.then(result => this.setState({redirectTo: '/vote-success/' + electionSlug})) .then(result =>
this.setState({ redirectTo: "/vote-success/" + electionSlug })
)
.catch(error => error); .catch(error => error);
}; };
render() {
render(){ const { redirectTo, candidates, electionGrades } = this.state;
const { redirectTo,
candidates,
electionGrades
} = this.state;
if (redirectTo) { if (redirectTo) {
return (<Redirect to={redirectTo}/>); return <Redirect to={redirectTo} />;
} }
return( return (
<Container> <Container>
<ToastContainer/> <ToastContainer />
<form onSubmit={this.handleSubmit} autoComplete="off" > <form onSubmit={this.handleSubmit} autoComplete="off">
<Row> <Row>
<Col ><h3>{ this.state.title }</h3></Col> <Col>
<h3>{this.state.title}</h3>
</Col>
</Row> </Row>
<Row className="cardVote d-none d-lg-flex" > <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> <Col
{ electionGrades.map((grade,j) => { xs={this.state.colSizeCandidateXs}
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; 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> </Row>
{ {candidates.map((candidate, i) => {
candidates.map((candidate,i) => { return (
return <Row key={i} className="cardVote"> <Row key={i} className="cardVote">
<Col xs={this.state.colSizeCandidateXs} md={this.state.colSizeCandidateMd} lg={this.state.colSizeCandidateLg} > <Col
<h5 className="m-0">{candidate.label}</h5><hr className="d-lg-none" /></Col> xs={this.state.colSizeCandidateXs}
{ this.state.electionGrades.map((grade,j) => { md={this.state.colSizeCandidateMd}
return (j<this.state.numGrades)?<Col lg={this.state.colSizeCandidateLg}
xs={this.state.colSizeGradeXs} md={this.state.colSizeGradeMd} lg={this.state.colSizeGradeLg} key={j} >
<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" className="text-lg-center"
> >
<label
<label htmlFor={"candidateGrade"+i+"-"+j} className="check" htmlFor={"candidateGrade" + i + "-" + j}
className="check"
> >
<small className="nowrap d-lg-none ml-2 bold badge" <small
className="nowrap d-lg-none ml-2 bold badge"
style={ style={
this.state.ratedCandidates.find(function(ratedCandidat){return JSON.stringify(ratedCandidat) === JSON.stringify({id:candidate.id,value:j})})? this.state.ratedCandidates.find(function(
{backgroundColor:grade.color,color:"#fff"}:{backgroundColor:'transparent',color:"#000"} ratedCandidat
} ) {
return (
>{grade.label}</small> JSON.stringify(ratedCandidat) ===
<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})})} /> 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
}) })
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "transparent",
color: "#000"
} }
</Row> }
>
{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> <Row>
<Col className="text-center" > <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>} {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> </Col>
</Row> </Row>
</form> </form>
</Container> </Container>
) );
} }
} }
export default Vote; export default Vote;

@ -1,13 +1,11 @@
import React, {Component} from "react"; import React, { Component } from "react";
import {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";
class UnknownView extends Component { class UnknownView extends Component {
render() {
render(){ return (
return(
<Container> <Container>
<Row> <Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4"> <Link to="/" className="d-block ml-auto mr-auto mb-4">
@ -18,17 +16,18 @@ class UnknownView extends Component {
<Col className="text-center offset-lg-3" lg="6"> <Col className="text-center offset-lg-3" lg="6">
<h2>Participation enregistrée avec succès !</h2> <h2>Participation enregistrée avec succès !</h2>
<p>Merci pour votre participation.</p> <p>Merci pour votre participation.</p>
</Col> </Col>
</Row> </Row>
<Row className="mt-4" > <Row className="mt-4">
<Col className="text-center"> <Col className="text-center">
<Link to="/" className="btn btn-secondary">Revenir à l'accueil</Link> <Link to="/" className="btn btn-secondary">
Revenir à l'accueil
</Link>
</Col> </Col>
</Row> </Row>
</Container> </Container>
) );
} }
} }
export default UnknownView; export default UnknownView;

@ -1,11 +1,10 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import './scss/config.scss'; import "./scss/config.scss";
import App from './App'; import App from "./App";
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from "./serviceWorker";
ReactDOM.render(<App />, document.getElementById("root"));
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls. // unregister() to register() below. Note this comes with some pitfalls.

@ -15,115 +15,112 @@ $theme-colors: (
"dark": $mv-dark-color, "dark": $mv-dark-color,
"danger": #990000, "danger": #990000,
"success": #009900, "success": #009900,
"info": #2B8299, "info": #2b8299,
"warning": #FF6E11, "warning": #ff6e11
) !default; ) !default;
.logo-text > h1{ .logo-text > h1 {
font-size:16px; font-size: 16px;
font-weight:bold; font-weight: bold;
margin:0; margin: 0;
line-height:1; line-height: 1;
} }
.bold{ .bold {
font-weight:bold; font-weight: bold;
} }
.logo-text > h1 > small{ .logo-text > h1 > small {
display:block; display: block;
letter-spacing: 0.09em; letter-spacing: 0.09em;
} }
html, body, #root, #root > div{ html,
height:100%; body,
#root,
#root > div {
height: 100%;
} }
main {
background-image: url("/background-mv.png");
main{ background-size: 100%;
background-image:url("/background-mv.png");
background-size:100%;
background-attachment: fixed; background-attachment: fixed;
background-repeat:no-repeat; background-repeat: no-repeat;
background-color:$mv-blue-color; background-color: $mv-blue-color;
min-height: calc(100% - 106px); min-height: calc(100% - 106px);
overflow: auto; overflow: auto;
padding-top:72px; padding-top: 72px;
padding-bottom:36px; padding-bottom: 36px;
} }
header{ header {
position:fixed; position: fixed;
z-index:10; z-index: 10;
width:100%; width: 100%;
} }
footer{ footer {
background-color:$body-bg; background-color: $body-bg;
color:$mv-light-color; color: $mv-light-color;
padding:25px; padding: 25px;
} }
footer a{ footer a {
color:$mv-light-color; color: $mv-light-color;
} }
footer a:hover{ footer a:hover {
color:#fff; color: #fff;
} }
hr{ hr {
border:none; border: none;
border-top:1px solid $mv-light-color; border-top: 1px solid $mv-light-color;
width:100%; width: 100%;
margin:auto; margin: auto;
} }
ul.sortable,
ul.sortable,li.sortable{ li.sortable {
padding:0; padding: 0;
margin:0 0 0 0; margin: 0 0 0 0;
list-style-type: none; list-style-type: none;
} }
li.sortable{ li.sortable {
margin:0 0 15px 0; margin: 0 0 15px 0;
} }
.pointer{ .pointer {
cursor: pointer; cursor: pointer;
} }
.modal {
.modal{ color: $mv-dark-color;
color:$mv-dark-color;
} }
/* card Vote */ /* card Vote */
.cardVote{ .cardVote {
background-color:$mv-light-color; background-color: $mv-light-color;
margin:1em 0; margin: 1em 0;
color:$mv-dark-color; color: $mv-dark-color;
border-radius:0.15em; border-radius: 0.15em;
padding:1em 0; padding: 1em 0;
} }
.cardVote .nowrap{ .cardVote .nowrap {
white-space: nowrap;; white-space: nowrap;
} }
.cardVote hr{ .cardVote hr {
border-top:1px solid $mv-dark-color; border-top: 1px solid $mv-dark-color;
margin:10px 0; margin: 10px 0;
} }
.cardVote.row:hover{ .cardVote.row:hover {
background-color:$mv-light-color-hover; background-color: $mv-light-color-hover;
} }
/* checkbox */ /* checkbox */
/* The radio */ /* The radio */
.radio { .radio {
display: block; display: block;
position: relative; position: relative;
padding-left: 30px; padding-left: 30px;
@ -133,7 +130,7 @@ li.sortable{
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none user-select: none;
} }
/* Hide the browser's default radio button */ /* Hide the browser's default radio button */
@ -145,20 +142,18 @@ li.sortable{
/* Create a custom radio button */ /* Create a custom radio button */
.checkround { .checkround {
position: absolute; position: absolute;
top: 6px; top: 6px;
left: 0; left: 0;
height: 20px; height: 20px;
width: 20px; width: 20px;
background-color: #fff ; background-color: #fff;
border-color:$mv-blue-color; border-color: $mv-blue-color;
border-style:solid; border-style: solid;
border-width:2px; border-width: 2px;
border-radius: 50%; border-radius: 50%;
} }
/* When the radio button is checked, add a blue background */ /* When the radio button is checked, add a blue background */
.radio input:checked ~ .checkround { .radio input:checked ~ .checkround {
background-color: #fff; background-color: #fff;
@ -183,9 +178,7 @@ li.sortable{
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 50%; border-radius: 50%;
background:$mv-blue-color; background: $mv-blue-color;
} }
/* The check */ /* The check */
@ -213,15 +206,15 @@ li.sortable{
/* Create a custom checkbox */ /* Create a custom checkbox */
.checkmark { .checkmark {
position: absolute; position: absolute;
margin-left: calc( 50% - 12px); margin-left: calc(50% - 12px);
top: 0; top: 0;
left: 0; left: 0;
height: 24px; height: 24px;
width: 24px; width: 24px;
background-color: #fff ; background-color: #fff;
border-color:$mv-blue-color; border-color: $mv-blue-color;
border-style:solid; border-style: solid;
border-width:2px; border-width: 2px;
} }
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
@ -234,7 +227,7 @@ li.sortable{
/* When the checkbox is checked, add a blue background */ /* When the checkbox is checked, add a blue background */
.check input:checked ~ .checkmark { .check input:checked ~ .checkmark {
background-color: #fff ; background-color: #fff;
} }
/* Create the checkmark/indicator (hidden when not checked) */ /* Create the checkmark/indicator (hidden when not checked) */
@ -255,64 +248,63 @@ li.sortable{
top: 1px; top: 1px;
width: 10px; width: 10px;
height: 15px; height: 15px;
border: solid ; border: solid;
border-color:#fff; border-color: #fff;
border-width: 0 3px 3px 0; border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg); -ms-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
cursor:pointer; cursor: pointer;
} }
.cust-btn{ .cust-btn {
margin-bottom: 10px; margin-bottom: 10px;
background-color: $mv-blue-color; background-color: $mv-blue-color;
border-width: 2px; border-width: 2px;
border-color: $mv-blue-color; border-color: $mv-blue-color;
color: #fff; color: #fff;
} }
.cust-btn:hover{ .cust-btn:hover {
border-color: $mv-blue-color; border-color: $mv-blue-color;
background-color: #fff; background-color: #fff;
color: $mv-blue-color; color: $mv-blue-color;
border-radius: 20px; border-radius: 20px;
transform-style: 2s; transform-style: 2s;
} }
/** collapse **/ /** collapse **/
.panel-title:after { .panel-title:after {
content:'+'; content: "+";
float:right; float: right;
font-size:28px; font-size: 28px;
font-weight:900; font-weight: 900;
} }
.panel-title.collapsed:after { .panel-title.collapsed:after {
content:'-'; content: "-";
} }
/** table profiles **/ /** table profiles **/
.profiles thead,.profiles tbody,.profiles tr,.profiles th, .profiles td,.profiles thead th{ .profiles thead,
border-color:$mv-blue-color; .profiles tbody,
color:$mv-blue-color; .profiles tr,
.profiles th,
.profiles td,
.profiles thead th {
border-color: $mv-blue-color;
color: $mv-blue-color;
} }
.median{ .median {
border-width:0 3px 0 0; border-width: 0 3px 0 0;
border-style:dashed; border-style: dashed;
border-color:$mv-blue-color; border-color: $mv-blue-color;
min-height:30px; min-height: 30px;
width: 1px; width: 1px;
position: absolute; position: absolute;
margin-left: 50%; margin-left: 50%;
margin-top: -15px; margin-top: -15px;
} }
/** react multi email **/ /** react multi email **/
.react-multi-email > span[data-placeholder] { .react-multi-email > span[data-placeholder] {
padding: 0.25em !important; padding: 0.25em !important;

@ -1,4 +1 @@
@import '~bootstrap/scss/bootstrap.scss'; @import "~bootstrap/scss/bootstrap.scss";

@ -1,19 +1,14 @@
// mieux voter vars // mieux voter vars
$mv-blue-color: #2A43A0; $mv-blue-color: #2a43a0;
$mv-red-color: #EE455B; $mv-red-color: #ee455b;
$mv-light-color: #efefff; $mv-light-color: #efefff;
$mv-light-color-hover:rgba(#efefff, .8); $mv-light-color-hover: rgba(#efefff, 0.8);
$mv-dark-color: #333; $mv-dark-color: #333;
// Override default variables before the import bootstrap // Override default variables before the import bootstrap
$body-bg: #000000; $body-bg: #000000;
$body-color: $mv-light-color; $body-color: $mv-light-color;
$theme-colors: ( $theme-colors: (
"primary": $mv-blue-color, "primary": $mv-blue-color,
"secondary": $mv-red-color, "secondary": $mv-red-color,
@ -21,10 +16,9 @@ $theme-colors: (
"dark": $mv-dark-color, "dark": $mv-dark-color,
"danger": #990000, "danger": #990000,
"success": #009900, "success": #009900,
"info": #2B8299, "info": #2b8299,
"warning": #FF6E11, "warning": #ff6e11
); );
@import '_bootstrap.scss'; @import "_bootstrap.scss";
@import 'app.scss'; @import "app.scss";

@ -11,9 +11,9 @@
// opt-in, read https://bit.ly/CRA-PWA // opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean( const isLocalhost = Boolean(
window.location.hostname === 'localhost' || window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
@ -21,7 +21,7 @@ const isLocalhost = Boolean(
); );
export function register(config) { export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
@ -31,7 +31,7 @@ export function register(config) {
return; return;
} }
window.addEventListener('load', () => { window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) { if (isLocalhost) {
@ -42,8 +42,8 @@ export function register(config) {
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + "This web app is being served cache-first by a service " +
'worker. To learn more, visit https://bit.ly/CRA-PWA' "worker. To learn more, visit https://bit.ly/CRA-PWA"
); );
}); });
} else { } else {
@ -64,14 +64,14 @@ function registerValidSW(swUrl, config) {
return; return;
} }
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched, // At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older // but the previous service worker will still serve the older
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
'New content is available and will be used when all ' + "New content is available and will be used when all " +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.' "tabs for this page are closed. See https://bit.ly/CRA-PWA."
); );
// Execute callback // Execute callback
@ -82,7 +82,7 @@ function registerValidSW(swUrl, config) {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log("Content is cached for offline use.");
// Execute callback // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
@ -94,7 +94,7 @@ function registerValidSW(swUrl, config) {
}; };
}) })
.catch(error => { .catch(error => {
console.error('Error during service worker registration:', error); console.error("Error during service worker registration:", error);
}); });
} }
@ -103,10 +103,10 @@ function checkValidServiceWorker(swUrl, config) {
fetch(swUrl) fetch(swUrl)
.then(response => { .then(response => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type'); const contentType = response.headers.get("content-type");
if ( if (
response.status === 404 || response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1) (contentType != null && contentType.indexOf("javascript") === -1)
) { ) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then(registration => {
@ -121,13 +121,13 @@ function checkValidServiceWorker(swUrl, config) {
}) })
.catch(() => { .catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.' "No internet connection found. App is running in offline mode."
); );
}); });
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then(registration => {
registration.unregister(); registration.unregister();
}); });

Loading…
Cancel
Save