pull/73/head
Pierre-Louis Guhur 3 years ago
parent 5344f5ede2
commit 720a9d990d

1
.gitignore vendored

@ -33,3 +33,4 @@ yarn-error.log*
# Local Netlify folder
.netlify
functions/next_*
.env

@ -8,6 +8,7 @@ import { toast, ToastContainer } from "react-toastify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { getDetails, castBallot, apiErrors } from "@services/api";
import Error from "@components/Error";
import { translateGrades } from "@services/grades";
import config from "../../../next-i18next.config.js";
@ -17,8 +18,14 @@ export async function getServerSideProps({ query: { pid, tid }, locale }) {
const [res, translations] = await Promise.all([
getDetails(
pid,
(res) => ({ ok: true, ...res }),
(err) => ({ ok: false, err })
(res) => {
console.log("DETAILS:", res);
return { ok: true, ...res };
},
(err) => {
console.log("ERR:", err);
return { ok: false, err: "Unknown error" };
}
),
serverSideTranslations(locale, [], config),
]);

@ -146,9 +146,12 @@ const getDetails = (pid, successCallback, failureCallback) => {
return fetch(detailsEndpoint.href)
.then((response) => {
if (!response.ok) {
console.log("NOK", response);
return Promise.reject(response.text());
}
return response.json();
const res = response.json();
console.log("OK", res);
return res;
})
.then(successCallback || ((res) => res))
.catch(failureCallback || ((err) => err));

@ -1,20 +0,0 @@
import React from "react";
import Routes from "./Routes";
import Header from "./components/layouts/Header";
import Footer from "./components/layouts/Footer";
import AppContextProvider from "./AppContext";
function App() {
return (
<AppContextProvider>
<div>
<Header />
<Routes />
<Footer />
</div>
</AppContextProvider>
);
}
export default App;

@ -1,90 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import { MemoryRouter } from "react-router-dom";
import Routes from "./Routes";
import App from "./App";
import Adapter from "enzyme-adapter-react-16";
import { mount, configure } from "enzyme";
import Home from "./components/views/Home";
import CreateElection from "./components/views/CreateElection";
import Result from "./components/views/Result";
import Vote from "./components/views/Vote";
import UnknownView from "./components/views/UnknownView";
configure({ adapter: new Adapter() });
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
describe("open good View component for each route", () => {
it("should show Home view component for `/`", () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/"]}>
<Routes />
</MemoryRouter>
);
expect(wrapper.find(Home)).toHaveLength(1);
expect(wrapper.find(UnknownView)).toHaveLength(0);
});
it("should show CreateElection view component for `/create-election`", () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/create-election"]}>
<Routes />
</MemoryRouter>
);
expect(wrapper.find(CreateElection)).toHaveLength(1);
expect(wrapper.find(UnknownView)).toHaveLength(0);
});
//this test is not good because window.location.search is empty even there is ?title= parameter in route
//Clement : I don't know how to achieve this test for now (maybe the component using window.location.search is not a good practice)
/*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"]}
>
<Routes />
</MemoryRouter>
);
expect(wrapper.find(CreateElection)).toHaveLength(1);
expect(wrapper.find('input[name="title"]').props().value).toBe(
"test with title"
);
expect(wrapper.find(UnknownView)).toHaveLength(0);
});*/
it("should show UnknownView view component for `/vote`", () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/vote"]}>
<Routes />
</MemoryRouter>
);
expect(wrapper.find(Vote)).toHaveLength(0);
expect(wrapper.find(UnknownView)).toHaveLength(1);
});
it("should show UnknownView view component for `/result`", () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/result"]}>
<Routes />
</MemoryRouter>
);
expect(wrapper.find(Result)).toHaveLength(0);
expect(wrapper.find(UnknownView)).toHaveLength(1);
});
it("should show UnknownView view component for `/aaabbbcccddd`", () => {
const wrapper = mount(
<MemoryRouter initialEntries={["/aaabbbcccddd"]} initialIndex={0}>
<Routes />
</MemoryRouter>
);
expect(wrapper.find(UnknownView)).toHaveLength(1);
});
});

@ -1,29 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { createContext, Suspense } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import Loader from "./components/loader";
export const AppContext = createContext();
const AppContextProvider = ({ children }) => {
const defaultState = {
urlServer: process.env.REACT_APP_SERVER_URL,
feedbackForm: process.env.REACT_APP_FEEDBACK_FORM,
routesServer: {
setElection: "election/",
getElection: "election/get/:slug/",
getResultsElection: "election/results/:slug",
voteElection: "election/vote/"
}
};
return (
<Suspense fallback={<Loader />}>
<Router>
<AppContext.Provider value={defaultState}>
{children}
</AppContext.Provider>
</Router>
</Suspense>
);
};
export default AppContextProvider;

@ -1,61 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import { Container, Row, Col } from "reactstrap";
import { Link } from "react-router-dom";
import logoLine from "./logos/logo-line-white.svg";
export const UNKNOWN_ELECTION_ERROR = "E1";
export const ONGOING_ELECTION_ERROR = "E2";
export const NO_VOTE_ERROR = "E3";
export const ELECTION_NOT_STARTED_ERROR = "E4";
export const ELECTION_FINISHED_ERROR = "E5";
export const INVITATION_ONLY_ERROR = "E6";
export const UNKNOWN_TOKEN_ERROR = "E7";
export const USED_TOKEN_ERROR = "E8";
export const WRONG_ELECTION_ERROR = "E9";
export const redirectError = () => {};
export const errorMessage = (error, t) => {
if (error.startsWith(UNKNOWN_ELECTION_ERROR)) {
return t("Oops... The election is unknown.");
} else if (error.startsWith(ONGOING_ELECTION_ERROR)) {
return t(
"The election is still going on. You can't access now to the results."
);
} else if (error.startsWith(NO_VOTE_ERROR)) {
return t("No votes have been recorded yet. Come back later.");
} else if (error.startsWith(ELECTION_NOT_STARTED_ERROR)) {
return t("The election has not started yet.");
} else if (error.startsWith(ELECTION_FINISHED_ERROR)) {
return t("The election is over. You can't vote anymore");
} else if (error.startsWith(INVITATION_ONLY_ERROR)) {
return t("You need a token to vote in this election");
} else if (error.startsWith(USED_TOKEN_ERROR)) {
return t("You seem to have already voted.");
} else if (error.startsWith(WRONG_ELECTION_ERROR)) {
return t("The parameters of the election are incorrect.");
}
};
export const Error = props => (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h4>{props.value}</h4>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<Link to="/" className="btn btn-secondary">
Back to home page
</Link>
</Col>
</Row>
</Container>
);

@ -1,48 +0,0 @@
import React from "react";
import { Switch, Route } from "react-router-dom";
import Home from "./components/views/Home";
import CreateElection from "./components/views/CreateElection";
import Vote from "./components/views/Vote";
import Result from "./components/views/Result";
import UnknownView from "./components/views/UnknownView";
import UnknownElection from "./components/views/UnknownElection";
import CreateSuccess from "./components/views/CreateSuccess";
import VoteSuccess from "./components/views/VoteSuccess";
import LegalNotices from "./components/views/LegalNotices";
import PrivacyPolicy from "./components/views/PrivacyPolicy";
import Faq from "./components/views/Faq";
function Routes() {
return (
<main className="d-flex flex-column justify-content-center">
<Switch>
<Route exact path="/" component={Home} />
<Route path="/create-election" component={CreateElection} />
<Route path="/vote/:slug" component={Vote} />
<Route path="/result/:slug" component={Result} />
<Route
path="/link/:slug"
component={props => (
<CreateSuccess invitationOnly={true} {...props} />
)}
/>
<Route
path="/links/:slug"
component={props => (
<CreateSuccess invitationOnly={false} {...props} />
)}
/>
<Route path="/vote-success/:slug" component={VoteSuccess} />
<Route path="/unknown-election/:slug" component={UnknownElection} />
<Route path="/legal-notices" component={LegalNotices} />
<Route path="/privacy-policy" component={PrivacyPolicy} />
<Route path="/faq" component={Faq} />
<Route component={UnknownView} />
</Switch>
</main>
);
}
export default Routes;

@ -1,37 +0,0 @@
import i18n from "./i18n.jsx";
const colors = [
"#015411",
"#019812",
"#6bca24",
"#ffb200",
"#ff5d00",
"#b20616",
"#6f0214"
];
const gradeNames = [
"Excellent",
"Very good",
"Good",
"Fair",
"Passable",
"Insufficient",
"To reject"
];
const gradeValues = [6, 5, 4, 3, 2, 1, 0];
export const grades = gradeNames.map((name, i) => ({
label: name,
color: colors[i],
value: gradeValues[i]
}));
export const i18nGrades = () => {
return gradeNames.map((name, i) => ({
label: i18n.t(name),
color: colors[i],
value: gradeValues[i]
}));
};

@ -1,57 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import { Button } from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const CopyField = props => {
const ref = React.createRef();
const handleClickOnField = event => {
event.target.focus();
event.target.select();
};
const handleClickOnButton = () => {
const input = ref.current;
input.focus();
input.select();
document.execCommand("copy");
};
const { t, value, iconCopy } = props;
return (
<div className="input-group ">
<input
type="text"
className="form-control"
ref={ref}
value={value}
readOnly
onClick={handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-secondary"
onClick={handleClickOnButton}
type="button"
>
<FontAwesomeIcon icon={iconCopy} className="mr-2" />
{t("Copy")}
</Button>
</div>
{/*<div className="input-group-append">
<a
className="btn btn-secondary"
href={value}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={iconOpen} className="mr-2" />
{t("Open")}
</a>
</div>*/}
</div>
);
};
export default CopyField;

@ -1,29 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFacebookSquare } from "@fortawesome/free-brands-svg-icons";
const Facebook = props => {
const handleClick = () => {
const url =
"https://www.facebook.com/sharer.php?u=" +
props.url +
"&t=" +
props.title;
window.open(
url,
"",
"menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=500,width=700"
);
};
return (
<button className={props.className} onClick={handleClick} type="button">
<FontAwesomeIcon icon={faFacebookSquare} className="mr-2" />
{props.text}
</button>
);
};
export default Facebook;
//i

@ -1,29 +0,0 @@
import React, { useContext } from "react";
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {faCommentAlt} from "@fortawesome/free-solid-svg-icons";
import { AppContext } from "../../AppContext"
const Gform = (props) => {
const context = useContext(AppContext);
console.log(context);
return (
<a
className={props.className}
href={context.feedbackForm}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={faCommentAlt} className="mr-2" />
Votre avis nous intéresse !
</a>
);
}
Gform.propTypes = {
className: PropTypes.string,
};
export default Gform;

@ -1,24 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import i18n from "../../i18n";
const Helloasso = props => {
const locale =
i18n.language.substring(0, 2).toLowerCase() === "fr" ? "fr" : "en";
const linkHelloAssoBanner =
locale === "fr"
? "https://www.helloasso.com/associations/mieux-voter/formulaires/1/widget"
: "https://www.helloasso.com/associations/mieux-voter/formulaires/1/widget/en";
return (
<a href={linkHelloAssoBanner} target="_blank" rel="noopener noreferrer">
<img
src={"/banner/" + locale + "/helloasso.png"}
alt="support us on helloasso"
style={{ width: props.width }}
/>
</a>
);
};
export default Helloasso;

@ -1,44 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import i18n from "../../i18n";
import { withTranslation } from "react-i18next";
import { faPaypal } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const Paypal = props => {
const { t } = props;
let localeStringShort = i18n.language? i18n.language.substring(0, 2): "en";
let localeStringComplete =
localeStringShort.toLowerCase() + "_" + localeStringShort.toUpperCase();
if (localeStringComplete === "en_EN") {
localeStringComplete = "en_US";
}
const pixelLink =
"https://www.paypal.com/" + localeStringComplete + "/i/scr/pixel.gif";
return (
<div className="d-inline-block m-auto">
<form
action="https://www.paypal.com/cgi-bin/webscr"
method="post"
target="_top"
>
<button
type="submit"
className={"btn " + props.btnColor}
title={t("PayPal - The safer, easier way to pay online!")}
>
{" "}
<FontAwesomeIcon icon={faPaypal} className="mr-2" />
{t("Support us !")}
</button>
<input type="hidden" name="cmd" value="_s-xclick" />
<input type="hidden" name="hosted_button_id" value="KB2Z7L9KARS7C" />
<img alt="" border="0" src={pixelLink} width="1" height="1" />
</form>
</div>
);
};
export default withTranslation()(Paypal);

@ -1,4 +0,0 @@
import * as React from "react";
import FlagIconFactory from "react-flag-icon-css";
export const FlagIcon = FlagIconFactory(React, { useCssModules: false });

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

@ -1,75 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
class HelpButton extends Component {
constructor(props) {
super(props);
this.state = {
tooltipOpen: false
};
}
showTooltip = () => {
this.setState({
tooltipOpen: true
});
};
hideTooltip = () => {
this.setState({
tooltipOpen: false
});
};
render() {
return (
<span className={this.props.className}>
<span>
{this.state.tooltipOpen ? (
<span
style={{
position: "absolute",
zIndex: 10,
fontSize: "12px",
color: "#000",
backgroundColor: "#fff",
display: "inline-block",
borderRadius: "0.25rem",
boxShadow: "-5px 0 5px rgba(0,0,0,0.5)",
maxWidth: "200px",
padding: "10px",
marginLeft: "-215px",
marginTop: "-25px"
}}
>
<span
style={{
position: "absolute",
width: 0,
height: 0,
borderTop: "10px solid transparent",
borderBottom: "10px solid transparent",
borderLeft: "10px solid #fff",
marginLeft: "190px",
marginTop: "15px"
}}
></span>
{this.props.children}
</span>
) : (
<span />
)}
</span>
<FontAwesomeIcon
icon={faQuestionCircle}
onMouseOver={this.showTooltip}
onMouseOut={this.hideTooltip}
/>
</span>
);
}
}
export default HelpButton;

@ -1,53 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
class ModalConfirm extends Component {
constructor(props) {
super(props);
this.state = {
modal: false
};
}
toggle = () => {
this.setState({
modal: !this.state.modal
});
};
getComponent = key => {
return this.props.children.filter(comp => {
return comp.key === key;
});
};
render() {
return (
<Modal
isOpen={this.state.modal}
toggle={this.toggle}
className={this.props.className + " modal-dialog-centered"}
>
<ModalHeader toggle={this.toggle}>
{this.getComponent("title")}
</ModalHeader>
<ModalBody>{this.getComponent("body")}</ModalBody>
<ModalFooter>
<Button
color="primary-outline"
className="text-primary border-primary"
onClick={this.toggle}
>
{this.getComponent("cancel")}
</Button>
<Button color="primary" onClick={this.toggle}>
{this.getComponent("confirm")}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default ModalConfirm;

@ -1,97 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import { withTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import Paypal from "../banner/Paypal";
import { useBbox } from "./useBbox";
import "./footer.css";
const Footer = props => {
const linkStyle = { whiteSpace: "nowrap" };
const { t } = props;
const [bboxLink1, link1] = useBbox();
const [bboxLink2, link2] = useBbox();
const [bboxLink3, link3] = useBbox();
const [bboxLink4, link4] = useBbox();
const [bboxLink5, link5] = useBbox();
const [bboxLink6, link6] = useBbox();
const [bboxLink7, link7] = useBbox();
return (
<footer className="text-center">
<div>
<ul className="tacky">
<li
ref={link1}
className={bboxLink1.top === bboxLink2.top ? "" : "no-tack"}
>
<Link to="/" style={linkStyle}>
{t("Homepage")}
</Link>
</li>
<li
ref={link2}
className={bboxLink2.top === bboxLink3.top ? "" : "no-tack"}
>
<Link to="/faq" style={linkStyle}>
{t("FAQ")}
</Link>
</li>
<li
ref={link3}
className={bboxLink3.top === bboxLink4.top ? "" : "no-tack"}
>
<a href="mailto:app@mieuxvoter.fr?subject=[HELP]" style={linkStyle}>
{t("Need help?")}
</a>
</li>
<li
ref={link4}
className={bboxLink4.top === bboxLink5.top ? "" : "no-tack"}
>
<a
href="https://mieuxvoter.fr/"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{t("Who are we?")}
</a>
</li>
<li
ref={link5}
className={bboxLink5.top === bboxLink6.top ? "" : "no-tack"}
>
<Link to="/privacy-policy" style={linkStyle}>
{t("Privacy policy")}
</Link>
</li>
<li
ref={link6}
className={bboxLink6.top === bboxLink7.top ? "" : "no-tack"}
>
<Link to="/legal-notices" style={linkStyle}>
{t("Legal notices")}
</Link>
</li>
<li ref={link7}>
{" "}
<a
href="https://github.com/MieuxVoter"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{t("Source code")}
</a>
</li>
</ul>
</div>
<div className="mt-3">
<Paypal btnColor="btn-primary" />
</div>
</footer>
);
};
export default withTranslation()(Footer);

@ -1,62 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Collapse, Navbar, NavbarToggler, Nav, NavItem } from "reactstrap";
import { Link } from "react-router-dom";
import { withTranslation } from "react-i18next";
import logo from "../../logos/logo-color.svg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faRocket } from "@fortawesome/free-solid-svg-icons";
import LanguageSelector from "./LanguageSelector";
class Header extends Component {
state = {
isOpen: false
};
toggle = () => {
this.setState({
isOpen: !this.state.isOpen
});
};
render() {
const { t } = this.props;
return (
<header>
<Navbar color="light" light expand="md">
<Link to="/" className="navbar-brand">
<div className="d-flex flex-row">
<div className="align-self-center">
<img src={logo} alt="logo" height="32" />
</div>
<div className="align-self-center ml-2">
<div className="logo-text">
<h1>
{t("Voting platform")}
<small>{t("Majority Judgment")}</small>
</h1>
</div>
</div>
</div>
</Link>
<NavbarToggler onClick={this.toggle} />
<Collapse isOpen={this.state.isOpen} navbar>
<Nav className="ml-auto" navbar>
<NavItem>
<Link className="text-primary nav-link" to="/create-election/">
<FontAwesomeIcon icon={faRocket} className="mr-2" />
{t("Start an election")}
</Link>
</NavItem>
<NavItem style={{ width: "150px" }}>
<LanguageSelector />
</NavItem>
</Nav>
</Collapse>
</Navbar>
</header>
);
}
}
export default withTranslation()(Header);

@ -1,30 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import ReactFlagsSelect from "react-flags-select";
import "react-flags-select/css/react-flags-select.css";
import i18n from "../../i18n";
const LanguageSelector = () => {
const selectHandler = e => {
let locale = e.toLowerCase();
if (locale === "gb") locale = "en";
i18n.changeLanguage(locale);
};
let locale = i18n.language? i18n.language.substring(0, 2).toUpperCase() : "EN";
if (locale === "EN") locale = "GB";
return (
<ReactFlagsSelect
onSelect={selectHandler}
countries={["GB", "FR", "ES", "DE", "RU"]}
showOptionLabel={false}
defaultCountry={locale}
selectedSize={15}
optionsSize={22}
showSelectedLabel={false}
/>
);
};
export default LanguageSelector;

@ -1,25 +0,0 @@
.tacky {
margin: 0;
padding: 0;
list-style-type: none;
}
.tacky li {
display: inline-block;
}
.tacky li:after {
content: "-";
margin: 0 5px;
}
.tacky li:last-of-type:after {
content: "";
margin: 0;
}
.tacky li.no-tack:after {
content: "";
margin: 0;
display: none;
}

@ -1,20 +0,0 @@
/* eslint react/prop-types: 0 */
import { useState } from 'react';
import { useRef } from 'react';
import { useEffect } from 'react';
export const useBbox = () => {
const ref = useRef();
const [bbox, setBbox] = useState({});
const set = () =>
setBbox(ref && ref.current ? ref.current.getBoundingClientRect() : {});
useEffect(() => {
set();
window.addEventListener('resize', set);
return () => window.removeEventListener('resize', set);
}, []);
return [bbox, ref];
};

@ -1,14 +0,0 @@
/* eslint react/prop-types: 0 */
import React from "react";
import logo from "./loader-pulse-2.gif";
import "./style.css";
const Loader = () => {
return (
<div className="loader bg-primary">
<img src={logo} alt="Loading..." />
</div>
);
};
export default Loader;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@ -1,18 +0,0 @@
.loader {
top:0;
left:0;
height:100%;
width:100%;
position:fixed;
z-index:15;
}
.loader > img {
width:150px;
height:150px;
position:absolute;
margin:-75px 0 0 -75px;
top:50%;
left:50%;
z-index:16;
}

@ -1,896 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Redirect, withRouter } from "react-router-dom";
import {
Collapse,
Container,
Row,
Col,
Input,
Label,
InputGroup,
InputGroupAddon,
Button,
Card,
CardBody
} from "reactstrap";
import { withTranslation } from "react-i18next";
import { ReactMultiEmail, isEmail } from "react-multi-email";
import "react-multi-email/style.css";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { resolve } from "url";
import queryString from "query-string";
import {
arrayMove,
sortableContainer,
sortableElement,
sortableHandle
} from "react-sortable-hoc";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPlus,
faTrashAlt,
faCheck,
faCogs,
faExclamationTriangle
} from "@fortawesome/free-solid-svg-icons";
import { i18nGrades } from "../../Util";
import { AppContext } from "../../AppContext";
import HelpButton from "../form/HelpButton";
import ButtonWithConfirm from "../form/ButtonWithConfirm";
import Loader from "../wait";
import i18n from "../../i18n";
// Error messages
const AT_LEAST_2_CANDIDATES_ERROR = "Please add at least 2 candidates.";
const NO_TITLE_ERROR = "Please add a title.";
const isValidDate = date => date instanceof Date && !isNaN(date);
const getOnlyValidDate = date => (isValidDate(date) ? date : new Date());
// Convert a Date object into YYYY-MM-DD
const dateToISO = date =>
getOnlyValidDate(date)
.toISOString()
.substring(0, 10);
// Retrieve the current hour, minute, sec, ms, time into a timestamp
const hours = date => getOnlyValidDate(date).getHours() * 3600 * 1000;
const minutes = date => getOnlyValidDate(date).getMinutes() * 60 * 1000;
const seconds = date => getOnlyValidDate(date).getSeconds() * 1000;
const ms = date => getOnlyValidDate(date).getMilliseconds();
const time = date =>
hours(getOnlyValidDate(date)) +
minutes(getOnlyValidDate(date)) +
seconds(getOnlyValidDate(date)) +
ms(getOnlyValidDate(date));
// Retrieve the time part from a timestamp and remove the day. Return a int.
const timeMinusDate = date => time(getOnlyValidDate(date));
// Retrieve the day and remove the time. Return a Date
const dateMinusTime = date =>
new Date(getOnlyValidDate(date).getTime() - time(getOnlyValidDate(date)));
const DragHandle = sortableHandle(({ children }) => (
<span className="input-group-text indexNumber">{children}</span>
));
const displayClockOptions = () =>
Array(24)
.fill(1)
.map((x, i) => (
<option value={i} key={i}>
{i}h00
</option>
));
const SortableCandidate = sortableElement(
({ candidate, sortIndex, form, t }) => (
<li className="sortable">
<Row key={"rowCandidate" + sortIndex}>
<Col>
<InputGroup>
<InputGroupAddon addonType="prepend">
<DragHandle>
<span>{sortIndex + 1}</span>
</DragHandle>
</InputGroupAddon>
<Input
type="text"
value={candidate.label}
onChange={event => form.editCandidateLabel(event, sortIndex)}
onKeyPress={event =>
form.handleKeypressOnCandidateLabel(event, sortIndex)
}
placeholder={t("Candidate/proposal name...")}
tabIndex={sortIndex + 1}
innerRef={ref => (form.candidateInputs[sortIndex] = ref)}
maxLength="250"
/>
<ButtonWithConfirm className="btn btn-primary input-group-append border-light">
<div key="button">
<FontAwesomeIcon icon={faTrashAlt} />
</div>
<div key="modal-title">{t("Delete?")}</div>
<div key="modal-body">
{t("Are you sure to delete")}{" "}
{candidate.label !== "" ? (
<b>&quot;{candidate.label}&quot;</b>
) : (
<span>
{t("the row")} {sortIndex + 1}
</span>
)}{" "}
?
</div>
<div
key="modal-confirm"
onClick={() => form.removeCandidate(sortIndex)}
>
Oui
</div>
<div key="modal-cancel">Non</div>
</ButtonWithConfirm>
</InputGroup>
</Col>
<Col xs="auto" className="align-self-center pl-0">
<HelpButton>
{t(
"Enter the name of your candidate or proposal here (250 characters max.)"
)}
</HelpButton>
</Col>
</Row>
</li>
)
);
const SortableCandidatesContainer = sortableContainer(({ items, form, t }) => {
return (
<ul className="sortable">
{items.map((candidate, index) => (
<SortableCandidate
key={`item-${index}`}
index={index}
sortIndex={index}
candidate={candidate}
form={form}
t={t}
/>
))}
</ul>
);
});
class CreateElection extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
// default value : start at the last hour
const now = new Date();
const start = new Date(
now.getTime() - minutes(now) - seconds(now) - ms(now)
);
const { title } = queryString.parse(this.props.location.search);
this.state = {
candidates: [{ label: "" }, { label: "" }],
title: title || "",
isVisibleTipsDragAndDropCandidate: true,
numGrades: 7,
waiting: false,
successCreate: false,
redirectTo: null,
isAdvancedOptionsOpen: false,
restrictResult: false,
isTimeLimited: false,
start,
// by default, the election ends in a week
finish: new Date(start.getTime() + 7 * 24 * 3600 * 1000),
electorEmails: []
};
this.candidateInputs = [];
this.focusInput = React.createRef();
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRestrictResultCheck = this.handleRestrictResultCheck.bind(this);
this.handleIsTimeLimited = this.handleIsTimeLimited.bind(this);
}
handleChangeTitle = event => {
this.setState({ title: event.target.value });
};
handleIsTimeLimited = event => {
this.setState({ isTimeLimited: event.target.value === "1" });
};
handleRestrictResultCheck = event => {
this.setState({ restrictResult: event.target.value === "1" });
};
addCandidate = event => {
let candidates = this.state.candidates;
if (candidates.length < 100) {
candidates.push({ label: "" });
this.setState({ candidates: candidates });
}
if (event.type === "keypress") {
setTimeout(() => {
this.candidateInputs[this.state.candidates.length - 1].focus();
}, 250);
}
};
removeCandidate = index => {
let candidates = this.state.candidates;
candidates.splice(index, 1);
if (candidates.length === 0) {
candidates = [{ label: "" }];
}
this.setState({ candidates: candidates });
};
editCandidateLabel = (event, index) => {
let candidates = this.state.candidates;
candidates[index].label = event.currentTarget.value;
candidates.map(candidate => {
return candidate.label;
});
this.setState({
candidates: candidates
});
};
handleKeypressOnCandidateLabel = (event, index) => {
if (event.key === "Enter") {
event.preventDefault();
if (index + 1 === this.state.candidates.length) {
this.addCandidate(event);
} else {
this.candidateInputs[index + 1].focus();
}
}
};
onCandidatesSortEnd = ({ oldIndex, newIndex }) => {
let candidates = this.state.candidates;
candidates = arrayMove(candidates, oldIndex, newIndex);
this.setState({ candidates: candidates });
};
handleChangeNumGrades = event => {
this.setState({ numGrades: event.target.value });
};
toggleAdvancedOptions = () => {
this.setState({ isAdvancedOptionsOpen: !this.state.isAdvancedOptionsOpen });
};
checkFields() {
const { candidates, title } = this.state;
if (!candidates) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
}
let numCandidates = 0;
candidates.forEach(c => {
if (c.label !== "") numCandidates += 1;
});
if (numCandidates < 2) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
}
if (!title || title === "") {
return { ok: false, msg: NO_TITLE_ERROR };
}
return { ok: true, msg: "OK" };
}
handleSubmit() {
const {
candidates,
title,
numGrades,
electorEmails
} = this.state;
let {
start,
finish,
} = this.state;
const endpoint = resolve(
this.context.urlServer,
this.context.routesServer.setElection
);
if(!this.state.isTimeLimited){
let now = new Date();
start = new Date(
now.getTime() - minutes(now) - seconds(now) - ms(now)
);
finish=new Date(start.getTime() + 10 * 365 * 24 * 3600 * 1000);
}
const { t } = this.props;
const locale =
i18n.language.substring(0, 2).toLowerCase() === "fr" ? "fr" : "en";
const check = this.checkFields();
if (!check.ok) {
toast.error(t(check.msg), {
position: toast.POSITION.TOP_CENTER
});
return;
}
this.setState({ waiting: true });
fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: title,
candidates: candidates.map(c => c.label).filter(c => c !== ""),
on_invitation_only: electorEmails.length > 0,
num_grades: numGrades,
elector_emails: electorEmails,
start_at: start.getTime() / 1000,
finish_at: finish.getTime() / 1000,
select_language: locale,
front_url: window.location.origin,
restrict_results: this.state.restrictResult
})
})
.then(response => response.json())
.then(result => {
if (result.id) {
const nextPage =
electorEmails && electorEmails.length
? `/link/${result.id}`
: `/links/${result.id}`;
this.setState(() => ({
redirectTo: nextPage,
successCreate: true,
waiting: false
}));
} else {
toast.error(t("Unknown error. Try again please."), {
position: toast.POSITION.TOP_CENTER
});
this.setState({ waiting: false });
}
})
.catch(error => error);
}
handleSendNotReady = msg => {
const { t } = this.props;
toast.error(t(msg), {
position: toast.POSITION.TOP_CENTER
});
};
render() {
const {
successCreate,
redirectTo,
waiting,
title,
start,
finish,
candidates,
numGrades,
isAdvancedOptionsOpen,
electorEmails
} = this.state;
const { t } = this.props;
const grades = i18nGrades();
const check = this.checkFields();
if (successCreate) return <Redirect to={redirectTo} />;
return (
<Container>
<ToastContainer />
{waiting ? <Loader /> : ""}
<form onSubmit={this.handleSubmit} autoComplete="off">
<Row>
<Col>
<h3>{t("Start an election")}</h3>
</Col>
</Row>
<hr />
<Row className="mt-4">
<Col xs="12">
<Label for="title">{t("Question of the election")}</Label>
</Col>
<Col>
<Input
placeholder={t("Write here the question of your election")}
tabIndex="1"
name="title"
id="title"
innerRef={this.focusInput}
autoFocus
value={title}
onChange={this.handleChangeTitle}
maxLength="250"
/>
</Col>
<Col xs="auto" className="align-self-center pl-0">
<HelpButton>
{t(
"Write here your question or introduce simple your election (250 characters max.)"
)}
<br />
<u>{t("For example:")}</u>{" "}
<em>
{t(
"For the role of my representative, I judge this candidate..."
)}
</em>
</HelpButton>
</Col>
</Row>
<Row className="mt-4">
<Col xs="12">
<Label for="title">{t("Candidates/Proposals")}</Label>
</Col>
<Col xs="12">
<SortableCandidatesContainer
items={candidates}
onSortEnd={this.onCandidatesSortEnd}
form={this}
t={t}
useDragHandle
/>
</Col>
</Row>
<Row className="justify-content-between">
<Col xs="12" sm="6" md="5" lg="4">
<Button
color="secondary"
className="btn-block mt-2"
tabIndex={candidates.length + 2}
type="button"
onClick={event => this.addCandidate(event)}
>
<FontAwesomeIcon icon={faPlus} className="mr-2" />
{t("Add a proposal")}
</Button>
</Col>
<Col
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"
onClick={this.toggleAdvancedOptions}
>
<FontAwesomeIcon icon={faCogs} className="mr-2" />
{t("Advanced options")}
</Button>
</Col>
</Row>
<Collapse isOpen={isAdvancedOptionsOpen}>
<Card>
<CardBody className="text-primary">
<Row>
<Col xs="12" md="3" lg="3">
<Label for="title">{t("Access to results")}</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio " htmlFor="restrict_result_false">
<span className="small text-dark">
{t("Immediately")}
</span>
<input
className="radio"
type="radio"
name="restrict_result"
id="restrict_result_false"
onClick={this.handleRestrictResultCheck}
defaultChecked={!this.state.restrictResult}
value="0"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio" htmlFor="restrict_result_true">
<span className="small">
<span className="text-dark">
{t("At the end of the election")}
</span>
<HelpButton className="ml-2">
{t(
"No one will be able to see the result until the end date is reached or until all participants have voted."
)}
</HelpButton>
</span>
<input
className="radio"
type="radio"
name="restrict_result"
id="restrict_result_true"
onClick={this.handleRestrictResultCheck}
defaultChecked={this.state.restrictResult}
value="1"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
</Row>
<hr className="mt-2 mb-2" />
<Row>
<Col xs="12" md="3" lg="3">
<Label for="title">{t("Voting time")}</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio " htmlFor="is_time_limited_false">
<span className="small text-dark">{t("Unlimited")}</span>
<input
className="radio"
type="radio"
name="time_limited"
id="is_time_limited_false"
onClick={this.handleIsTimeLimited}
defaultChecked={!this.state.isTimeLimited}
value="0"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio" htmlFor="is_time_limited_true">
<span className="small">
<span className="text-dark">
{t("Defined period")}
</span>
</span>
<input
className="radio"
type="radio"
name="time_limited"
id="is_time_limited_true"
onClick={this.handleIsTimeLimited}
defaultChecked={this.state.isTimeLimited}
value="1"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
</Row>
<div
className={(this.state.isTimeLimited ? "d-block " : "d-none")+" bg-light p-3"}
>
<Row >
<Col xs="12" md="3" lg="3">
<span className="label">- {t("Starting date")}</span>
</Col>
<Col xs="6" md="4" lg="3">
<input
className="form-control"
type="date"
value={dateToISO(start)}
onChange={e => {
this.setState({
start: new Date(
timeMinusDate(start) +
new Date(e.target.valueAsNumber).getTime()
)
});
}}
/>
</Col>
<Col xs="6" md="5" lg="3">
<select
className="form-control"
value={getOnlyValidDate(start).getHours()}
onChange={e =>
this.setState({
start: new Date(
dateMinusTime(start).getTime() +
e.target.value * 3600000
)
})
}
>
{displayClockOptions()}
</select>
</Col>
</Row>
<Row className="mt-2">
<Col xs="12" md="3" lg="3">
<span className="label">- {t("Ending date")}</span>
</Col>
<Col xs="6" md="4" lg="3">
<input
className="form-control"
type="date"
value={dateToISO(finish)}
min={dateToISO(start)}
onChange={e => {
this.setState({
finish: new Date(
timeMinusDate(finish) +
new Date(e.target.valueAsNumber).getTime()
)
});
}}
/>
</Col>
<Col xs="6" md="5" lg="3">
<select
className="form-control"
value={getOnlyValidDate(finish).getHours()}
onChange={e =>
this.setState({
finish: new Date(
dateMinusTime(finish).getTime() +
e.target.value * 3600000
)
})
}
>
{displayClockOptions()}
</select>
</Col>
</Row>
</div>
<hr className="mt-2 mb-2" />
<Row>
<Col xs="12" md="3" lg="3">
<span className="label">{t("Grades")}</span>
</Col>
<Col xs="10" sm="11" md="4" lg="3">
<select
className="form-control"
tabIndex={candidates.length + 3}
onChange={this.handleChangeNumGrades}
defaultValue="7"
>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</Col>
<Col xs="auto" className="align-self-center pl-0 ">
<HelpButton>
{t(
"You can select here the number of grades for your election"
)}
<br />
<u>{t("For example:")}</u>{" "}
<em>
{" "}
{t("5 = Excellent, Very good, Good, Fair, Passable")}
</em>
</HelpButton>
</Col>
<Col
xs="12"
md="9"
lg="9"
className="offset-xs-0 offset-md-3 offset-lg-3"
>
{grades.map((mention, i) => {
return (
<span
key={i}
className="badge badge-light mr-2 mt-2 "
style={{
backgroundColor: mention.color,
color: "#fff",
opacity: i < numGrades ? 1 : 0.3
}}
>
{mention.label}
</span>
);
})}
</Col>
</Row>
<hr className="mt-2 mb-2" />
<Row>
<Col xs="12" md="3" lg="3">
<span className="label">{t("Participants")}</span>
</Col>
<Col xs="12" md="9" lg="9">
<ReactMultiEmail
placeholder={t("Add here participants' emails")}
emails={electorEmails}
onChange={_emails => {
this.setState({ electorEmails: _emails });
}}
validateEmail={email => {
return isEmail(email); // return boolean
}}
getLabel={(email, index, removeEmail) => {
return (
<div data-tag key={index}>
{email}
<span
data-tag-handle
onClick={() => removeEmail(index)}
>
×
</span>
</div>
);
}}
/>
<div>
<small className="text-muted">
{t(
"If you list voters' emails, only them will be able to access the election"
)}
</small>
</div>
</Col>
</Row>
<hr className="mt-2 mb-2" />
</CardBody>
</Card>
</Collapse>
<Row className="justify-content-end mt-2">
<Col xs="12" md="3">
{check.ok ? (
<ButtonWithConfirm
className="btn btn-success float-right btn-block"
tabIndex={candidates.length + 4}
>
<div key="button">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Validate")}
</div>
<div key="modal-title" className="text-primary bold">
{t("Confirm your vote")}
</div>
<div key="modal-body">
<div className="mt-1 mb-1">
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Question of the election")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">{title}</div>
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Candidates/Proposals")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
<ul className="m-0 pl-4">
{candidates.map((candidate, i) => {
if (candidate.label !== "") {
return (
<li key={i} className="m-0">
{candidate.label}
</li>
);
} else {
return <li key={i} className="d-none" />;
}
})}
</ul>
</div>
<div className={(this.state.isTimeLimited ? "d-block " : "d-none")} >
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Dates")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
{t("The election will take place from")}{" "}
<b>
{start.toLocaleDateString()}, {t("at")}{" "}
{start.toLocaleTimeString()}
</b>{" "}
{t("to")}{" "}
<b>
{finish.toLocaleDateString()}, {t("at")}{" "}
{finish.toLocaleTimeString()}
</b>
</div>
</div>
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Grades")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
{grades.map((mention, i) => {
return i < numGrades ? (
<span
key={i}
className="badge badge-light mr-2 mt-2"
style={{
backgroundColor: mention.color,
color: "#fff"
}}
>
{mention.label}
</span>
) : (
<span key={i} />
);
})}
</div>
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Voters' list")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
{electorEmails.length > 0 ? (
electorEmails.join(", ")
) : (
<p>
{t("The form contains no address.")}
<br />
<em>
{t(
"The election will be opened to anyone with the link"
)}
</em>
</p>
)}
</div>
{this.state.restrictResult ? (
<div>
<div className="small bg-primary text-white p-3 mt-2 rounded">
<h6 className="m-0 p-0">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="mr-2"
/>
<u>{t("Results available at the close of the vote")}</u>
</h6>
<p className="m-2 p-0">
{electorEmails.length > 0 ? (
<span>
{t(
"The results page will not be accessible until all participants have voted."
)}
</span>
) : (
<span>
{t(
"The results page will not be accessible until the end date is reached."
)}{" "}
({finish.toLocaleDateString()} {t("at")}{" "}
{finish.toLocaleTimeString()})
</span>
)}
</p>
</div>
</div>
) : null}
</div>
</div>
<div key="modal-confirm" onClick={this.handleSubmit}>
{t("Start the election")}
</div>
<div key="modal-cancel">{t("Cancel")}</div>
</ButtonWithConfirm>
) : (
<Button
type="button"
className="btn btn-dark float-right btn-block"
onClick={this.handleSendWithoutCandidate}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Confirm")}
</Button>
)}
</Col>
</Row>
</form>
</Container>
);
}
}
export default withTranslation()(withRouter(CreateElection));

@ -1,141 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import { Link } from "react-router-dom";
import { withTranslation } from "react-i18next";
import {
faCopy,
faVoteYea,
faExclamationTriangle,
faExternalLinkAlt
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppContext } from "../../AppContext";
import CopyField from "../CopyField";
import Facebook from "../banner/Facebook";
class CreateSuccess extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
const electionSlug = this.props.match.params.slug;
this.state = {
urlOfVote: window.location.origin + "/vote/" + electionSlug,
urlOfResult: window.location.origin + "/result/" + electionSlug
};
this.urlVoteField = React.createRef();
this.urlResultField = React.createRef();
}
handleClickOnCopyResult = () => {
const input = this.urlResultField.current;
input.focus();
input.select();
document.execCommand("copy");
};
render() {
const { t } = this.props;
const electionLink = this.props.invitationOnly ? (
<>
<p className="mb-1">
{t(
"Voters received a link to vote by email. Each link can be used only once!"
)}
</p>
</>
) : (
<>
<p className="mb-1">{t("Voting address")}</p>
<CopyField
value={this.state.urlOfVote}
iconCopy={faCopy}
iconOpen={faExternalLinkAlt}
t={t}
/>
</>
);
return (
<Container>
<Row className="mt-5">
<Col className="text-center offset-lg-3" lg="6">
<h2>{t("Successful election creation!")}</h2>
{this.props.invitationOnly ? null : (
<Facebook
className="btn btn-sm btn-outline-light m-2"
text={t("Share election on Facebook")}
url={this.state.urlOfVote}
title={"app.mieuxvoter.fr"}
/>
)}
</Col>
</Row>
<Row className="mt-5 mb-4">
<Col className="offset-lg-3" lg="6">
<h5 className="mb-3 text-center">
<FontAwesomeIcon icon={faExclamationTriangle} className="mr-2" />
{t("Keep these links carefully")}
</h5>
<div className="border rounded p-4 pb-5">
{electionLink}
<p className="mt-4 mb-1">{t("Results address")}</p>
<CopyField
value={this.state.urlOfResult}
iconCopy={faCopy}
iconOpen={faExternalLinkAlt}
t={t}
/>
</div>
{/*<div className="input-group ">
<input
type="text"
className="form-control"
value=""
placeholder="email@domaine.com"
/>
<div className="input-group-append">
<a
className="btn btn-success"
href=""
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={faPaperPlane} className="mr-2" />
{this.props.invitationOnly
? t("Send me this link")
: t("Send me these links")}
</a>
</div>
</div>*/}
{/*<div className="text-center">
<button
type="button"
className="btn btn-success m-2"
>
<FontAwesomeIcon icon={faPaperPlane} className="mr-2" />
{(this.props.invitationOnly?t("Send me this link by email"):t("Send me these links by email"))}
</button>
</div>*/}
</Col>
</Row>
{this.props.invitationOnly ? null : (
<Row className="mt-4 mb-4">
<Col className="text-center">
<Link
to={"/vote/" + this.props.match.params.slug}
className="btn btn-secondary"
>
<FontAwesomeIcon icon={faVoteYea} className="mr-2" />
{t("Participate now!")}
</Link>
</Col>
</Row>
)}
</Container>
);
}
}
export default withTranslation()(CreateSuccess);

@ -1,280 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import logoLine from "../../logos/logo-line-white.svg";
import { Link } from "react-router-dom";
import { AppContext } from "../../AppContext";
import { withTranslation } from "react-i18next";
import Paypal from "../banner/Paypal";
class Faq extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {};
}
render() {
const { t } = this.props;
return (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h1>{t("FAQ")}</h1>
</Col>
</Row>
<Row className="mt-4">
<Col>
<h4 className="bold mt-5">
Quest-ce que le Jugement Majoritaire ?
</h4>
<p>
Un principe simple et intuitif, qui change tout : lélecteur vote
en donnant son avis sur toutes les candidatures présentées, leur
attribuant la mention de son choix (par exemple. Très bien, Bien,
Assez bien, Passable, Insuffisant, À Rejeter). La candidature
retenue est celle jugée la plus méritante par la majorité de
lélectorat (celui qui obtient la meilleure mention « majoritaire
»).
</p>
<div style={{ maxWidth: "445px" }}>
<video width="100%" height="250" controls="controls">
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.mp4"
type="video/mp4"
/>
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.webm"
type="video/webm"
/>
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.3gpp"
type="video/3gpp"
/>
</video>
</div>
<h4 className="bold mt-5">D vient le Jugement Majoritaire ?</h4>
<p>
Le jugement majoritaire est un mode de scrutin inventé par deux
chercheurs Français du Centre National de la Recherche
Scientifique (CNRS) en 2011, <u>Michel Balinski</u> et{" "}
<u>Rida Laraki</u>.
</p>
<h4 className="bold mt-5">
Quels sont les avantages du Jugement Majoritaire ?
</h4>
<p>
Une mesure précise de lopinion des participants au vote, à même
déclairer la décision collective. En demandant aux électeurs leur
opinion sur chaque option soumise au vote, on bénéficie de
beaucoup plus dinformations que dans le cadre du scrutin
uninominal qui, résumant lopinion des électeurs à un choix,
ignore lessentiel de linformation quant à ce quils pensent. En
agrégeant un grand nombre dinformations, le Jugement Majoritaire
ne produit pas « juste » un gagnant qui obtiendrait la majorité
des voix. Il mesure précisément le crédit porté à chacune des
options et permet daffiner autant que de pacifier la prise de
décision.
</p>
<h4 className="bold mt-5">
Quand et comment utiliser le Jugement Majoritaire ?
</h4>
<p>
Le Jugement majoritaire sapplique à tout type de votation
collective, quil sagisse délire un candidat, de retenir une ou
plusieurs idées lors dun atelier collaboratif, de choisir entre
plusieurs projets, de classer les vins, etc. Il peut être utilisé
à toutes les échelles (locale, nationale, internationale) et dans
tous les milieux (écoles, entreprises, associations, coopératives,
collectivités publiques).
</p>
<h4 className="bold mt-5">Qui peut utiliser cette application ?</h4>
<p>
Cette application de Jugement Majoritaire est ouverte à toute
personne désireuse de prendre une décision collective, entre amis,
entre collègues, entre membres dun groupe. Elle est libre daccès
et gratuite. Notre ambition est de vous proposer la meilleure
expérience de prise de décision collective et démocratique.
</p>
<h4 className="bold mt-5">
Comment organiser une élection avec plusieurs milliers de votants
?
</h4>
<p>
Cette application ne convient pas pour les votes à plus de 1000
votants. Si cest votre cas, nous vous invitons à nous contacter
par email à ladresse{" "}
<a href="mailto:contact@mieuxvoter.fr" className="text-light">
contact@mieuxvoter.fr
</a>
. Dans le cas dun vote sur invitation nous vous suggérons de ne
pas dépasser 200 participants (le temps de création du vote peut
prendre quelques minutes).
</p>
<h4 className="bold mt-5">
Je rencontre un problème, comment obtenir de laide ?
</h4>
<p>
Si vous rencontrez un problème en utilisant notre application,
prenez contact avec nous par email à ladresse «
<a
href="mailto:app@mieuxvoter.fr?subject=[HELP]"
className="text-light"
>
app@mieuxvoter.fr
</a>
», et prenez soin de bien décrire le problème rencontré dans votre
message. Ajoutez éventuellement dans votre description le lien de
votre vote.
</p>
<h4 className="bold mt-5">
Y-a til une limite de votants appliquée pour les votes sur
invitation ?
</h4>
<p>
Le nombre maximum de votants pour un vote sur invitation est de
1000 personnes. Si toutefois votre besoin est supérieur à cette
limite, nous vous invitons à nous envoyer un email à ladresse «
<a href="mailto:contact@mieuxvoter.fr" className="text-light">
contact@mieuxvoter.fr
</a>
».
</p>
<h4 className="bold mt-5">
Combien de temps le lien vers la page de résultat reste-t-il actif
?
</h4>
<p>
Les liens fournis lors de la création de votre vote nont pas de
date dexpiration. Conservez-les précieusement afin de pouvoir
consulter les résultat dans le futur.
</p>
<h4 className="bold mt-5">
Comment puis-je massurer quune même personne ne vote pas deux
fois?
</h4>
<p>
Dans le cas dun vote sur invitation, seules les personnes dont le
courriel a été ajouté à la création du vote reçoivent une
invitation et peuvent donc voter. Chacune des invitations dispose
dun lien unique auquel est associé un jeton à usage unique. Ce
jeton est détruit aussitôt que la participation au vote de
linvité est enregistrée. Il garantit donc à lorganisateur que
chaque participant na pu voter quune seule fois.
</p>
<p>
Dans le cas dun vote public, toute personne peut participer à
lélection sil dispose du lien de lélection. Il ny a dans ce
cas aucune limite de soumission dun vote. Une même personne peut
donc voter plusieurs fois.
</p>
<h4 className="bold mt-5">
Lorsque jorganise une élection, puis-je connaître le nombre et
lidentité des votants?
</h4>
<p>
Le nombre de votants est indiqué sur la page de résultats de votre
élection. Lidentité des votants est quant à elle effacée, afin de
respecter les conditions dun vote démocratique lanonymat
garantit la sincérité des électeurs.
</p>
<h4 className="bold mt-5">Puis-je modifier mon vote ?</h4>
<p>
Une fois votre vote enregistré, vous ne pouvez plus le modifier.
En effet, votre vote étant anonymisé, ce qui nous empêche de faire
le lien entre vous et votre vote.
</p>
<h4 className="bold mt-5">
Comment puis-je récupérer un lien si je lai perdu ?
</h4>
<p>
Vous ne pouvez pas récupérer un lien pour voter après quil vous
soit communiquer. Gardez le précieusement. Cependant si vous avez
le lien pour voter, nous pouvons vous transmettre le lien des
résultats.
</p>
<h4 className="bold mt-5">
Comment interpréter les résultats dun vote au Jugement
Majoritaire ?
</h4>
<p>
Les candidats ou propositions sont triées de la mention
majoritaire la plus favorable à la plus défavorable. En cas
dégalité, on calcule alors pour chaque candidat à départager: le
pourcentage délecteurs attribuant strictement plus que la mention
majoritaire commune et le pourcentage délecteurs attribuant
strictement moins que la mention majoritaire commune. La plus
grande des 4 valeurs détermine le résultat.
</p>
<h4 className="bold mt-5">Quelle sécurité pour mes données ?</h4>
<p>
Afin de garantir la sécurité de vos données, leur transmission est
chiffrée et vos votes sont anonymisés.
</p>
<h4 className="bold mt-5">
Que faites-vous des données collectées ?
</h4>
<p>
Lapplication app.mieuxvoter.fr a pour seul et unique but de faire
découvrir le vote au Jugement Majoritaire. Elle na pas de but
politique, ni commercial. Mieux Voter attache la plus grande
importance au strict respect de la vie privée, et utilise ces
données uniquement de manière responsable et confidentielle, dans
une finalité précise.
</p>
<h4 className="bold mt-5">Qui est Mieux Voter ?</h4>
<p>
« Mieux Voter » est une association loi 1901 qui promeut
lutilisation du Jugement Majoritaire, nouvelle théorie du choix
social, comme un outil pour améliorer les décisions collectives et
les exercices de démocratie participative à lusage de tous.
</p>
<h4 className="bold mt-5">
Comment nous aider à faire connaître le Jugement Majoritaire ?
</h4>
<p>
Vous avez apprécié votre expérience de vote démocratique au
Jugement Majoritaire ? <br />
Nous en sommes ravis ! Vous pouvez nous aider en faisant un don à
lassociation ici :
</p>
<Paypal btnColor="btn-success" className="mt-1" />
</Col>
</Row>
{/* <Row className="mt-4">
<Col className="text-center">
<Link to="/" className="btn btn-secondary">
{t("Go back to homepage")}
</Link>
</Col>
</Row> */}
</Container>
);
}
}
export default withTranslation()(Faq);

@ -1,95 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { withTranslation } from "react-i18next";
import { Container, Row, Col, Button, Input } from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faRocket } from "@fortawesome/free-solid-svg-icons";
import { Redirect } from "react-router-dom";
import logoLine from "../../logos/logo-line-white.svg";
class Home extends Component {
constructor(props) {
super(props);
this.state = {
title: null,
redirect: false
};
this.focusInput = React.createRef();
}
handleSubmit = event => {
event.preventDefault();
this.setState({ redirect: true });
};
handleChangeTitle = event => {
//console.log(this.context.routesServer.setElection);
this.setState({ title: event.target.value });
};
render() {
const { t } = this.props;
const redirect = this.state.redirect;
if (redirect) {
return (
<Redirect
to={"/create-election/?title=" + encodeURIComponent(this.state.title)}
/>
);
}
return (
<Container>
<form onSubmit={this.handleSubmit} autoComplete="off">
<Row>
<img
src={logoLine}
alt="logo"
height="128"
className="d-block ml-auto mr-auto mb-4"
/>
</Row>
<Row>
<Col className="text-center">
<h3>
{t(
"Simple and free: organize an election with Majority Judgment."
)}
</h3>
</Col>
</Row>
<Row className="mt-2">
<Col xs="12" md="9" xl="6" className="offset-xl-2">
<Input
placeholder={t("Write here the question of your election")}
innerRef={this.focusInput}
autoFocus
required
className="mt-2"
name="title"
value={this.state.title ? this.state.title : ""}
onChange={this.handleChangeTitle}
maxLength="250"
/>
</Col>
<Col xs="12" md="3" xl="2">
<Button
type="submit"
className="btn btn-block btn-secondary mt-2"
>
<FontAwesomeIcon icon={faRocket} className="mr-2" />
{t("Start")}
</Button>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<p>{t("No advertising or ad cookies")}</p>
</Col>
</Row>
</form>
</Container>
);
}
}
export default withTranslation()(Home);

@ -1,77 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import logoLine from "../../logos/logo-line-white.svg";
import { Link } from "react-router-dom";
import { AppContext } from "../../AppContext";
import { withTranslation } from "react-i18next";
class LegalNotices extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {};
}
render() {
const { t } = this.props;
return (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h1>{t("Legal notices")}</h1>
</Col>
</Row>
<Row className="mt-4">
<Col>
<h3 className="bold">Editeur</h3>
<p>Cette Application est éditée par lassociation loi 1901
{" "}<a
href="https://mieuxvoter.fr/"
target="_blank"
rel="noopener noreferrer"
className="text-white"
>
Mieux Voter
</a>, dont le siège social est situé au 59 rue Saint-André des Arts, à Paris (75006).</p>
<p>
Adresse email :
<a href="mailto:contact@mieuxvoter.fr" className="text-light">
contact@mieuxvoter.fr
</a>
</p>
<p>
<b>Directeur de la publication</b>
<br />Chloé Ridel
</p>
<h3 className="mt-2 bold">Hébergement</h3>
<p>
<ul>
<li>Base de données : Institut Systèmes Complexes, Paris ;</li>
<li>Fichiers statiques : Netlify, 2325 3rd Street, Suite 215, San Francisco, California 94107.</li>
</ul>
</p>
<h3 className="mt-2 bold">&OElig;uvres graphiques</h3>
<p>
Les illustrations et graphismes sur cette application sont l&oelig;uvre de lassociation Mieux Voter.
</p>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<Link to="/" className="btn btn-secondary">
{t("Go back to homepage")}
</Link>
</Col>
</Row>
</Container>
);
}
}
export default withTranslation()(LegalNotices);

@ -1,113 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import logoLine from "../../logos/logo-line-white.svg";
import { Link } from "react-router-dom";
import { AppContext } from "../../AppContext";
import { withTranslation } from "react-i18next";
class PrivacyPolicy extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {};
}
render() {
const { t } = this.props;
return (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h1>{t("Privacy policy")}</h1>
</Col>
</Row>
<Row className="mt-4">
<Col>
<p className="text-center">
Dernière mise à jour de notre politique de confidentialité
effectuée le 27 avril 2020.
</p>
<h4 className="bold mt-5">Introduction</h4>
<p>
Dans le cadre de la mise à disposition de son application web de
vote au jugement majoritaire, accessible sur Internet à ladresse
app.mieuxvoter.fr, ci-après lApplication, lassociation loi 1901
« Mieux Voter » , dont le siège social est situé au 59 rue saint
andré des arts, à Paris (75006), ci-après lAssociation, est
amenée à collecter et à traiter des informations dont certaines
sont qualifiées de « Données personnelles » . Mieux Voter attache
la plus grande importance au respect de la vie privée, et utilise
ces données uniquement de manière responsable et confidentielle et
dans une finalité précise.
</p>
<h4 className="bold mt-5">Notre politique de confidentialité</h4>
<p>
La présente politique de confidentialité détaille les conditions
dutilisation et de traitement par lAssociation des Données
personnelles (ci-après définies) collectées via lApplication.
LAssociation sengage à respecter les dispositions de la loi
n°78-17 du 6 janvier 1978 relative à linformatique, aux fichiers
et aux libertés modifiée et au Règlement (UE) 2016/679 du
Parlement européen et du Conseil du 27 avril 2016 dit « RGPD » et
prendre toute précaution nécessaire pour préserver la sécurité des
Données personnelles confiées.
</p>
<h4 className="bold mt-5">Responsable de traitement</h4>
<p>
En qualité de responsable de traitement, lAssociation peut
traiter les Données personnelles.
</p>
<h4 className="bold mt-5">
Données personnelles traitées et finalités de traitement
</h4>
<p>
LAssociation recueille sur lApplication les Données personnelles
dans une finalité précise. Ces données sont nécessaires à la
fourniture de notre service. Dans le cadre de la fourniture de ce
service, lAssociation traite uniquement les données personnelles
suivantes (définies comme les « Données personnelles »)
strictement nécessaires à la fourniture du service :
</p>
<ul>
<li> Les emails des personnes invitées à un vote</li>
</ul>
<p>
{" "}
La finalité de traitement de ces données personnelles est de
permettre à lAssociation de fournir le service. Ces données sont
traitées au moment de la création du vote pour envoyer les
invitations et détruites aussitôt les invitations envoyées. Elles
ne sont jamais stockées sur nos serveurs.
</p>
<h4 className="bold mt-5">Sécurité des Données personnelles</h4>
<p>
LAssociation sengage, au titre de son obligation de moyens, à
prendre toutes les précautions utiles et met en œuvre des mesures
techniques et organisationnelles appropriées en la matière pour
garantir un niveau de sécurité adapté et pour protéger les Données
personnelles contre les altérations, destructions et accès non
autorisés.
</p>
</Col>
</Row>
{/*<Row className="mt-4">
<Col className="text-center">
<Link to="/" className="btn btn-secondary">
{t("Go back to homepage")}
</Link>
</Col>
</Row>*/}
</Container>
);
}
}
export default withTranslation()(PrivacyPolicy);

@ -1,419 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { withTranslation } from "react-i18next";
import { resolve } from "url";
import {
Container,
Row,
Col,
Collapse,
Card,
CardHeader,
CardBody,
Table
} from "reactstrap";
import { i18nGrades } from "../../Util";
import { AppContext } from "../../AppContext";
import { errorMessage, Error } from "../../Errors";
import Facebook from "../banner/Facebook";
class Result extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {
candidates: [],
title: null,
numGrades: 0,
colSizeCandidateLg: 4,
colSizeCandidateMd: 6,
colSizeCandidateXs: 12,
colSizeGradeLg: 1,
colSizeGradeMd: 1,
colSizeGradeXs: 1,
collapseGraphics: false,
collapseProfiles: false,
electionGrades: i18nGrades(),
errorMessage: ""
};
}
handleErrors = response => {
if (!response.ok) {
response.json().then(response => {
this.setState(() => ({
errorMessage: errorMessage(response, this.props.t)
}));
});
throw Error(response);
}
return response;
};
resultsToState = response => {
const candidates = response.map(c => ({
id: c.id,
name: c.name,
profile: c.profile,
grade: c.grade
}));
this.setState(() => ({ candidates: candidates }));
return response;
};
detailsToState = response => {
const numGrades = response.num_grades;
const colSizeGradeLg = Math.floor(
(12 - this.state.colSizeCandidateLg) / numGrades
);
const colSizeGradeMd = Math.floor(
(12 - this.state.colSizeCandidateMd) / numGrades
);
const colSizeGradeXs = Math.floor(
(12 - this.state.colSizeCandidateXs) / numGrades
);
this.setState(() => ({
title: response.title,
numGrades: numGrades,
colSizeGradeLg: colSizeGradeLg,
colSizeGradeMd: colSizeGradeMd,
colSizeGradeXs: colSizeGradeXs,
colSizeCandidateLg:
12 - colSizeGradeLg * numGrades > 0
? 12 - colSizeGradeLg * numGrades
: 12,
colSizeCandidateMd:
12 - colSizeGradeMd * numGrades > 0
? 12 - colSizeGradeMd * numGrades
: 12,
colSizeCandidateXs:
12 - colSizeGradeXs * numGrades > 0
? 12 - colSizeGradeXs * numGrades
: 12,
electionGrades: i18nGrades().slice(0, numGrades)
}));
return response;
};
componentDidMount() {
// get details of the election
const electionSlug = this.props.match.params.slug;
if (electionSlug === "dev") {
const dataTest = [
{
name: "BB",
id: 1,
score: 1.0,
profile: [1, 1, 0, 0, 0, 0, 0],
grade: 1
},
{
name: "CC",
id: 2,
score: 1.0,
profile: [0, 0, 2, 0, 0, 0, 0],
grade: 2
},
{
name: "AA",
id: 0,
score: 1.0,
profile: [1, 1, 0, 0, 0, 0, 0],
grade: 1
}
];
this.setState({ candidates: dataTest });
} else {
const detailsEndpoint = resolve(
this.context.urlServer,
this.context.routesServer.getElection.replace(
new RegExp(":slug", "g"),
electionSlug
)
);
fetch(detailsEndpoint)
.then(this.handleErrors)
.then(response => response.json())
.then(this.detailsToState)
.catch(error => console.log(error));
// get results of the election
const resultsEndpoint = resolve(
this.context.urlServer,
this.context.routesServer.getResultsElection.replace(
new RegExp(":slug", "g"),
electionSlug
)
);
fetch(resultsEndpoint)
.then(this.handleErrors)
.then(response => response.json())
.then(this.resultsToState)
.catch(error => console.log(error));
}
}
toggleGraphics = () => {
this.setState(state => ({ collapseGraphics: !state.collapseGraphics }));
};
toggleProfiles = () => {
this.setState(state => ({ collapseProfiles: !state.collapseProfiles }));
};
render() {
const { errorMessage, candidates, electionGrades } = this.state;
const { t } = this.props;
const i18nGradesObject = i18nGrades();
const offsetGrade = i18nGradesObject.length - this.state.numGrades;
if (errorMessage && errorMessage !== "") {
return <Error value={errorMessage} />;
}
const sum = seq => Object.values(seq).reduce((a, b) => a + b, 0);
const numVotes =
candidates && candidates.length > 0 ? sum(candidates[0].profile) : 1;
const gradeIds =
candidates && candidates.length > 0
? Object.keys(candidates[0].profile)
: [];
return (
<Container>
<Row>
<Col xs="12">
<h3>{this.state.title}</h3>
</Col>
</Row>
<Row className="mt-5">
<Col>
<ol className="result">
{candidates.map((candidate, i) => {
const gradeValue = candidate.grade + offsetGrade;
return (
<li key={i} className="mt-2">
<span className="mt-2 ml-2">{candidate.name}</span>
<span
className="badge badge-light ml-2 mt-2"
style={{
backgroundColor: electionGrades.slice(0).reverse()[
candidate.grade
].color,
color: "#fff"
}}
>
{i18nGradesObject.slice(0).reverse()[gradeValue].label}
</span>
{/* <span className="badge badge-dark mt-2 ml-2">
{(100 * candidate.score).toFixed(1)}%
</span> */}
</li>
);
})}
</ol>
<h5>
<small>
{t("Number of votes:")}
{" " + numVotes}
</small>
</h5>
</Col>
</Row>
<Row className="mt-5">
<Col>
<Card className="bg-light text-primary">
<CardHeader className="pointer" onClick={this.toggleGraphics}>
<h4
className={
"m-0 panel-title " +
(this.state.collapseGraphics ? "collapsed" : "")
}
>
{t("Graph")}
</h4>
</CardHeader>
<Collapse isOpen={this.state.collapseGraphics}>
<CardBody className="pt-5">
<div>
<div
className="median"
style={{ height: candidates.length * 28 + 30 }}
/>
<table style={{ width: "100%" }}>
<tbody>
{candidates.map((candidate, i) => {
return (
<tr key={i}>
<td style={{ width: "30px" }}>{i + 1}</td>
{/*candidate.label*/}
<td>
<table style={{ width: "100%" }}>
<tbody>
<tr>
{gradeIds
.slice(0)
.reverse()
.map((id, i) => {
const value = candidate.profile[id];
if (value > 0) {
let percent =
(value * 100) / numVotes + "%";
if (i === 0) {
percent = "auto";
}
return (
<td
key={i}
style={{
width: percent,
backgroundColor: this.state
.electionGrades[i].color
}}
>
&nbsp;
</td>
);
} else {
return null;
}
})}
</tr>
</tbody>
</table>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="mt-4">
<small>
{candidates.map((candidate, i) => {
return (
<span key={i}>
{i > 0 ? ", " : ""}
<b>{i + 1}</b>: {candidate.name}
</span>
);
})}
</small>
</div>
<div className="mt-2">
<small>
{electionGrades.map((grade, i) => {
return (
<span
key={i}
className="badge badge-light mr-2 mt-2"
style={{
backgroundColor: grade.color,
color: "#fff"
}}
>
{grade.label}
</span>
);
})}
</small>
</div>
</CardBody>
</Collapse>
</Card>
</Col>
</Row>
<Row className="mt-3">
<Col>
<Card className="bg-light text-primary">
<CardHeader className="pointer" onClick={this.toggleProfiles}>
<h4
className={
"m-0 panel-title " +
(this.state.collapseProfiles ? "collapsed" : "")
}
>
{t("Preference profile")}
</h4>
</CardHeader>
<Collapse isOpen={this.state.collapseProfiles}>
<CardBody>
<div className="table-responsive">
<Table className="profiles">
<thead>
<tr>
<th>#</th>
{electionGrades.map((grade, i) => {
return (
<th key={i}>
<span
className="badge badge-light"
style={{
backgroundColor: grade.color,
color: "#fff"
}}
>
{grade.label}{" "}
</span>
</th>
);
})}
</tr>
</thead>
<tbody>
{candidates.map((candidate, i) => {
return (
<tr key={i}>
<td>{i + 1}</td>
{gradeIds
.slice(0)
.reverse()
.map((id, i) => {
const value = candidate.profile[id];
const percent = (
(value / numVotes) *
100
).toFixed(1);
return <td key={i}>{percent} %</td>;
})}
</tr>
);
})}
</tbody>
</Table>
</div>
<small>
{candidates.map((candidate, i) => {
return (
<span key={i}>
{i > 0 ? ", " : ""}
<b>{i + 1}</b>: {candidate.name}
</span>
);
})}
</small>
</CardBody>
</Collapse>
</Card>
</Col>
</Row>
<Row>
<Col xs="12" className="text-center pt-2 pb-5">
<Facebook
className="btn btn-outline-light m-2"
text={t("Share results on Facebook")}
url={window.location.origin + "/result/" + this.props.match.params.slug}
title={encodeURI(this.state.title)}
/>
</Col>
</Row>
</Container>
);
}
}
export default withTranslation()(Result);

@ -1,46 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import { withTranslation } from "react-i18next";
import logoLine from "../../logos/logo-line-white.svg";
import { Link } from "react-router-dom";
import { AppContext } from "../../AppContext";
class UnknownElection extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {};
}
render() {
const { t } = this.props;
return (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h2>
{t(
"Oops! This election does not exist or it is not available anymore."
)}
</h2>
<p>{t("You can start another election.")}</p>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<Link to="/" className="btn btn-secondary">
{t("Go back to homepage")}
</Link>
</Col>
</Row>
</Container>
);
}
}
export default withTranslation()(UnknownElection);

@ -1,39 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import logoLine from "../../logos/logo-line-white.svg";
import { Link } from "react-router-dom";
import { AppContext } from "../../AppContext";
class UnknownView extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h2>Ooops ! this page doesn&#39;t exist !</h2>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<Link to="/" className="btn btn-secondary">
Go back to homepage
</Link>
</Col>
</Row>
</Container>
);
}
}
export default UnknownView;

@ -1,355 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Redirect } from "react-router-dom";
import { withTranslation } from "react-i18next";
import { Button, Col, Container, Row } from "reactstrap";
import { toast, ToastContainer } from "react-toastify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { resolve } from "url";
import { i18nGrades } from "../../Util";
import { AppContext } from "../../AppContext";
import { errorMessage } from "../../Errors";
const shuffle = array => array.sort(() => Math.random() - 0.5);
class Vote extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {
candidates: [],
title: null,
numGrades: 0,
ratedCandidates: [],
colSizeCandidateLg: 4,
colSizeCandidateMd: 6,
colSizeCandidateXs: 12,
colSizeGradeLg: 1,
colSizeGradeMd: 1,
colSizeGradeXs: 1,
redirectTo: null,
electionGrades: i18nGrades(),
errorMsg: ""
};
}
handleErrors = response => {
if (!response.ok) {
response.json().then(response => {
console.log(response);
const { t } = this.props;
this.setState(() => ({
errorMsg: errorMessage(response, t)
}));
});
throw Error(response);
}
return response;
};
detailsToState = response => {
const numGrades = response.num_grades;
const candidates = response.candidates.map((c, i) => ({
id: i,
label: c
}));
shuffle(candidates);
const colSizeGradeLg = Math.floor(
(12 - this.state.colSizeCandidateLg) / numGrades
);
const colSizeGradeMd = Math.floor(
(12 - this.state.colSizeCandidateMd) / numGrades
);
const colSizeGradeXs = Math.floor(
(12 - this.state.colSizeCandidateXs) / numGrades
);
this.setState(() => ({
title: response.title,
candidates: candidates,
numGrades: numGrades,
colSizeGradeLg: colSizeGradeLg,
colSizeGradeMd: colSizeGradeMd,
colSizeGradeXs: colSizeGradeXs,
colSizeCandidateLg:
12 - colSizeGradeLg * numGrades > 0
? 12 - colSizeGradeLg * numGrades
: 12,
colSizeCandidateMd:
12 - colSizeGradeMd * numGrades > 0
? 12 - colSizeGradeMd * numGrades
: 12,
colSizeCandidateXs:
12 - colSizeGradeXs * numGrades > 0
? 12 - colSizeGradeXs * numGrades
: 12
}));
return response;
};
componentDidMount() {
// FIXME we should better handling logs
const electionSlug = this.props.match.params.slug;
const detailsEndpoint = resolve(
this.context.urlServer,
this.context.routesServer.getElection.replace(
new RegExp(":slug", "g"),
electionSlug
)
);
fetch(detailsEndpoint)
.then(this.handleErrors)
.then(response => response.json())
.then(this.detailsToState)
.catch(error => console.log(error));
}
handleGradeClick = event => {
let data = {
id: parseInt(event.currentTarget.getAttribute("data-id")),
value: parseInt(event.currentTarget.value)
};
//remove candidate
let ratedCandidates = this.state.ratedCandidates.filter(
ratedCandidate => ratedCandidate.id !== data.id
);
ratedCandidates.push(data);
this.setState({ ratedCandidates });
};
handleSubmitWithoutAllRate = () => {
const { t } = this.props;
toast.error(t("You have to judge every candidate/proposal!"), {
position: toast.POSITION.TOP_CENTER
});
};
handleSubmit = event => {
event.preventDefault();
const { ratedCandidates } = this.state;
const electionSlug = this.props.match.params.slug;
const token = this.props.location.search.substr(7);
const endpoint = resolve(
this.context.urlServer,
this.context.routesServer.voteElection
);
const gradesById = {};
ratedCandidates.forEach(c => {
gradesById[c.id] = c.value;
});
const gradesByCandidate = [];
Object.keys(gradesById).forEach(id => {
gradesByCandidate.push(gradesById[id]);
});
const payload = {
election: electionSlug,
grades_by_candidate: gradesByCandidate
};
if (token !== "") {
payload["token"] = token;
}
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then(this.handleErrors)
.then(() =>
this.setState({ redirectTo: "/vote-success/" + electionSlug })
)
.catch(error => error);
};
render() {
const { t } = this.props;
const { candidates, errorMsg, redirectTo } = this.state;
const grades = i18nGrades();
const offsetGrade = grades.length - this.state.numGrades;
const electionGrades = grades.slice(0, this.state.numGrades);
if (redirectTo) {
return <Redirect to={redirectTo} />;
}
if (errorMsg !== "") {
return (
<Container>
<Row>
<Col>
<h3>{errorMsg}</h3>
</Col>
</Row>
</Container>
);
}
return (
<Container>
<ToastContainer />
<form onSubmit={this.handleSubmit} autoComplete="off">
<Row>
<Col>
<h3>{this.state.title}</h3>
</Col>
</Row>
<Row className="cardVote d-none d-lg-flex">
<Col
xs={this.state.colSizeCandidateXs}
md={this.state.colSizeCandidateMd}
lg={this.state.colSizeCandidateLg}
>
<h5>&nbsp;</h5>
</Col>
{electionGrades.map((grade, gradeId) => {
return gradeId < this.state.numGrades ? (
<Col
xs={this.state.colSizeGradeXs}
md={this.state.colSizeGradeMd}
lg={this.state.colSizeGradeLg}
key={gradeId}
className="text-center p-0"
style={{ lineHeight: 2 }}
>
<small
className="nowrap bold badge"
style={{ backgroundColor: grade.color, color: "#fff" }}
>
{grade.label}
</small>
</Col>
) : null;
})}
</Row>
{candidates.map((candidate, candidateId) => {
return (
<Row key={candidateId} className="cardVote">
<Col
xs={this.state.colSizeCandidateXs}
md={this.state.colSizeCandidateMd}
lg={this.state.colSizeCandidateLg}
>
<h5 className="m-0">{candidate.label}</h5>
<hr className="d-lg-none" />
</Col>
{electionGrades.map((grade, gradeId) => {
console.assert(gradeId < this.state.numGrades);
const gradeValue = grade.value - offsetGrade;
return (
<Col
xs={this.state.colSizeGradeXs}
md={this.state.colSizeGradeMd}
lg={this.state.colSizeGradeLg}
key={gradeId}
className="text-lg-center"
>
<label
htmlFor={
"candidateGrade" + candidateId + "-" + gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none ml-2 bold badge"
style={
this.state.ratedCandidates.find(function(
ratedCandidat
) {
return (
JSON.stringify(ratedCandidat) ===
JSON.stringify({
id: candidate.id,
value: gradeValue
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "transparent",
color: "#000"
}
}
>
{grade.label}
</small>
<input
type="radio"
name={"candidate" + candidateId}
id={"candidateGrade" + candidateId + "-" + gradeValue}
data-index={candidateId}
data-id={candidate.id}
value={grade.value - offsetGrade}
onClick={this.handleGradeClick}
defaultChecked={this.state.ratedCandidates.find(
function(element) {
return (
JSON.stringify(element) ===
JSON.stringify({
id: candidate.id,
value: gradeValue
})
);
}
)}
/>
<span
className="checkmark"
style={
this.state.ratedCandidates.find(function(
ratedCandidat
) {
return (
JSON.stringify(ratedCandidat) ===
JSON.stringify({
id: candidate.id,
value: gradeValue
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "transparent",
color: "#000"
}
}
/>
</label>
</Col>
);
})}
</Row>
);
})}
<Row>
<Col className="text-center">
{this.state.ratedCandidates.length !==
this.state.candidates.length ? (
<Button
type="button"
onClick={this.handleSubmitWithoutAllRate}
className="btn btn-dark "
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Submit my vote")}
</Button>
) : (
<Button type="submit" className="btn btn-success ">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Submit my vote")}
</Button>
)}
</Col>
</Row>
</form>
</Container>
);
}
}
export default withTranslation()(Vote);

@ -1,39 +0,0 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { Col, Container, Row } from "reactstrap";
import { withTranslation } from "react-i18next";
import logoLine from "../../logos/logo-line-white.svg";
import { Link } from "react-router-dom";
import { AppContext } from "../../AppContext";
import Paypal from "../banner/Paypal";
import Gform from "../banner/Gform";
class VoteSuccess extends Component {
static contextType = AppContext;
render() {
const { t } = this.props;
return (
<Container>
<Row>
<Link to="/" className="d-block ml-auto mr-auto mb-4">
<img src={logoLine} alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center offset-lg-3" lg="6">
<h2>{t("Your participation was recorded with success!")}</h2>
<p>{t("Thanks for your participation.")}</p>
<div className="mt-3">
<Gform className="btn btn-secondary"/>
</div>
<div className="mt-5">
<Paypal btnColor="btn-success" />
</div>
</Col>
</Row>
</Container>
);
}
}
export default withTranslation()(VoteSuccess);

@ -1,8 +0,0 @@
import React from "react";
import Loader from "../loader";
const Wait = () => {
return <Loader />;
};
export default Wait;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

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

@ -1,41 +0,0 @@
import i18n from "i18next";
import XHR from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
i18n
.use(XHR)
.use(LanguageDetector)
.use(initReactI18next) // bind react-i18next to the instance
.init({
fallbackLng: "en",
debug: true,
saveMissing: true, // send not translated keys to endpoint
defaultValue: "__STRING_NOT_TRANSLATED__",
react: { useSuspense: false },
keySeparator: ">",
nsSeparator: "|",
backend: {
loadPath: "/locale/i18n/{{lng}}/resource.json"
// path to post missing resources
},
interpolation: {
escapeValue: false // not needed for react!!
}
// react i18next special options (optional)
// override if needed - omit if ok with defaults
/*
react: {
bindI18n: 'languageChanged',
bindI18nStore: '',
transEmptyNodeValue: '',
transSupportBasicHtmlNodes: true,
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],
useSuspense: true,
}
*/
});
export default i18n;

@ -1,12 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import "./scss/config.scss";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(<App />, document.getElementById("root"));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<path class="st0" d="M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200s200-89.54,200-200S310.46,0,200,0z M249.67,210.48
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35c0.5,1.54,1.06,3.06,1.64,4.71
c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13
C258.54,185.81,254.12,198.15,249.67,210.48z M263.37,154.73c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01
c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32
c-0.96,3.36-2.37,4.44-5.83,4.32c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45
c-4.73-16.28-9.75-32.48-14.68-48.71c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98
c0.03,3.35-0.68,4.69-4.34,4.55c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47
c-0.01-3.38,1.15-4.16,4.29-4.11c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67c0.86-2.71,2.07-3.64,4.9-3.61
c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64C263.28,112.41,263.29,133.57,263.37,154.73z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#2A43A0;}
</style>
<path class="st0" d="M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200s200-89.54,200-200S310.46,0,200,0z M249.67,210.48
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35c0.5,1.54,1.06,3.06,1.64,4.71
c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13
C258.54,185.81,254.12,198.15,249.67,210.48z M263.37,154.73c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01
c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32
c-0.96,3.36-2.37,4.44-5.83,4.32c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45
c-4.73-16.28-9.75-32.48-14.68-48.71c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98
c0.03,3.35-0.68,4.69-4.34,4.55c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47
c-0.01-3.38,1.15-4.16,4.29-4.11c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67c0.86-2.71,2.07-3.64,4.9-3.61
c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64C263.28,112.41,263.29,133.57,263.37,154.73z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#2A43A0;}
.st1{fill:#EE455B;}
.st2{fill:#FDFDFE;}
</style>
<circle id="BACKGROUND" class="st0" cx="200" cy="200" r="200"/>
<path id="V" class="st1" d="M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13c-4.67,13.01-9.09,25.35-13.54,37.68
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6z"/>
<path id="M" class="st2" d="M200.14,153.15c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67
c0.86-2.71,2.07-3.64,4.9-3.61c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59
c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32c-0.96,3.36-2.37,4.44-5.83,4.32
c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98c0.03,3.35-0.68,4.69-4.34,4.55
c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47c-0.01-3.38,1.15-4.16,4.29-4.11
c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
C198.48,149.05,199.11,150.5,200.14,153.15z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<path class="st0" d="M200,8c25.92,0,51.07,5.08,74.73,15.09c22.86,9.67,43.4,23.51,61.03,41.15c17.64,17.64,31.48,38.17,41.15,61.03
C386.92,148.93,392,174.08,392,200s-5.08,51.07-15.09,74.73c-9.67,22.86-23.51,43.4-41.15,61.03
c-17.64,17.64-38.17,31.48-61.03,41.15C251.07,386.92,225.92,392,200,392s-51.07-5.08-74.73-15.09
c-22.86-9.67-43.4-23.51-61.03-41.15c-17.64-17.64-31.48-38.17-41.15-61.03C13.08,251.07,8,225.92,8,200s5.08-51.07,15.08-74.73
c9.67-22.86,23.51-43.4,41.15-61.03c17.64-17.64,38.17-31.48,61.03-41.15C148.93,13.08,174.08,8,200,8 M157.57,79.72
c-5.34,0-10.77-0.04-16.61-0.13c-0.13,0-0.25,0-0.37,0c-1.39,0-5.63,0-8.83,3.21c-3.23,3.24-3.21,7.53-3.21,8.93
c0.11,32.12,0.12,65.57,0,99.42c0,1.37-0.02,5.55,3.15,8.73c3.12,3.13,7.15,3.13,8.47,3.13c0.21,0,0.42,0,0.63-0.01
c4.16-0.11,7.78-0.17,11.09-0.17c3.53,0,6.76,0.06,9.86,0.19c0.29,0.01,0.57,0.02,0.84,0.02c3.59,0,6.49-1.08,8.61-3.22
c3.27-3.29,3.23-7.73,3.22-9.4c-0.02-2.5-0.04-5.01-0.05-7.53c0.22,0.74,0.43,1.48,0.65,2.22c1.97,6.79,6.32,10.23,12.91,10.23
c0.26,0,0.54-0.01,0.82-0.02c3.58-0.14,7.29-0.22,11.03-0.22c3.68,0,7.5,0.07,11.35,0.21c0.27,0.01,0.54,0.01,0.8,0.01
c1.34,0,2.79-0.15,4.24-0.56c-7.07,21.09-14.23,42.25-21.28,63.04c-3.54-10.78-7.15-21.8-10.62-32.69
c-2.45-7.67-7.68-8.82-10.58-8.82c-1.62,0-3.32,0.35-5.2,1.08c-6.4,2.48-13.03,4.76-19.71,6.77c-3.66,1.1-6.22,3.07-7.61,5.86
c-2.1,4.2-0.59,8.28,0.05,10.02c9.7,26.29,18.41,50.2,26.63,73.09c2.17,6.04,6.69,9.24,13.07,9.24c0.16,0,0.31,0,0.47-0.01
c4.28-0.09,8.89-0.18,13.54-0.18c4.19,0,7.93,0.07,11.45,0.21c0.32,0.01,0.64,0.02,0.94,0.02c10.55,0,13.4-8.01,14.33-10.64
c8.58-24.11,17.45-48.58,26.02-72.25c3.18-8.77,6.35-17.54,9.52-26.31c3.04-8.42,6.07-16.85,9.17-25.5c1.44-4.01,2.9-8.07,4.38-12.2
c0.88-2.45,0.51-5.18-0.99-7.31c-1.05-1.49-2.55-2.55-4.25-3.06c1.05-0.49,1.98-1.15,2.81-1.98c3.08-3.09,3.07-7.13,3.06-8.46
c-0.08-23.52-0.08-44.26,0.01-63.42c0.01-1.34,0.03-5.41-3.11-8.55c-3.12-3.13-7.15-3.13-8.47-3.13l-0.24,0
c-5.84,0.07-11.56,0.1-17,0.1c-5.74,0-11.38-0.04-16.79-0.11l-0.27,0c-4.32,0-9.97,1.6-12.37,9.2c-2.55,8.06-5.3,16.25-7.97,24.17
c-0.94,2.79-1.88,5.58-2.81,8.38c-0.78,2.35-1.57,4.7-2.36,7.07c-4.33-12.9-8.75-26.12-12.99-39.17c-2.08-6.41-6.43-9.66-12.93-9.66
c-0.12,0-0.25,0-0.38,0C168.53,79.67,163.11,79.72,157.57,79.72 M230.91,164.86c0.33,0.18,0.68,0.35,1.03,0.49
c-0.46,0.14-0.91,0.31-1.33,0.49C230.71,165.51,230.81,165.19,230.91,164.86 M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200
s200-89.54,200-200S310.46,0,200,0L200,0z M157.57,87.72c5.42,0,10.84-0.04,16.26-0.14c0.08,0,0.16,0,0.24,0
c3.08,0,4.35,1.14,5.32,4.13c6.06,18.66,12.35,37.24,18.59,55.84c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28
c3.63-10.88,7.36-21.73,10.82-32.67c0.84-2.66,2.02-3.61,4.74-3.61c0.05,0,0.11,0,0.16,0c5.63,0.08,11.26,0.11,16.9,0.11
c5.7,0,11.4-0.04,17.09-0.1c0.05,0,0.1,0,0.15,0c2.66,0,3.59,0.82,3.58,3.65c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.45-0.61,3.48-3.15,3.48c-0.06,0-0.13,0-0.2,0c-3.95-0.08-7.91-0.13-11.86-0.13c-3.71,0-7.42,0.04-11.13,0.14
c-0.09,0-0.17,0-0.26,0c-2.86,0-3.26-1.36-3.22-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91
c-4.76,15.41-9.49,30.83-13.94,46.32c-0.91,3.19-2.23,4.33-5.32,4.33c-0.16,0-0.33,0-0.51-0.01c-3.87-0.14-7.76-0.22-11.64-0.22
c-3.79,0-7.57,0.07-11.35,0.22c-0.17,0.01-0.33,0.01-0.49,0.01c-3.2,0-4.33-1.38-5.22-4.47c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,17.99-0.08,35.99,0.07,53.98c0.03,3.18-0.62,4.56-3.82,4.56
c-0.17,0-0.34,0-0.52-0.01c-3.39-0.14-6.79-0.19-10.18-0.19c-3.77,0-7.54,0.07-11.3,0.17c-0.14,0-0.28,0.01-0.42,0.01
c-2.65,0-3.63-0.79-3.62-3.83c0.11-33.16,0.11-66.32,0-99.47c-0.01-3.29,1.09-4.11,4.04-4.11c0.08,0,0.17,0,0.25,0
C146.42,87.67,152,87.72,157.57,87.72L157.57,87.72z M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.26-3.8,2.8-5.65,6.8-5.65c0.26,0,0.52,0.01,0.8,0.02c3.39,0.18,6.79,0.24,10.24,0.24c4.95,0,10-0.11,15.28-0.11
c-4.67,13.01-9.09,25.35-13.54,37.68c-11.87,32.86-23.84,65.69-35.55,98.6c-1.33,3.74-2.98,5.32-6.79,5.32c-0.2,0-0.41,0-0.62-0.01
c-3.92-0.16-7.85-0.22-11.78-0.22c-4.57,0-9.14,0.08-13.71,0.18c-0.1,0-0.2,0-0.3,0c-2.92,0-4.48-0.97-5.54-3.94
c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45c6.85-2.06,13.63-4.38,20.29-6.97
c0.93-0.36,1.68-0.54,2.31-0.54c1.53,0,2.27,1.09,2.96,3.25c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6L194.36,281.6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#2943A0;}
</style>
<path class="st0" d="M200,8c25.92,0,51.07,5.08,74.73,15.09c22.86,9.67,43.4,23.51,61.03,41.15c17.64,17.64,31.48,38.17,41.15,61.03
C386.92,148.93,392,174.08,392,200s-5.08,51.07-15.09,74.73c-9.67,22.86-23.51,43.4-41.15,61.03
c-17.64,17.64-38.17,31.48-61.03,41.15C251.07,386.92,225.92,392,200,392s-51.07-5.08-74.73-15.09
c-22.86-9.67-43.4-23.51-61.03-41.15c-17.64-17.64-31.48-38.17-41.15-61.03C13.08,251.07,8,225.92,8,200s5.08-51.07,15.08-74.73
c9.67-22.86,23.51-43.4,41.15-61.03c17.64-17.64,38.17-31.48,61.03-41.15C148.93,13.08,174.08,8,200,8 M157.57,79.72
c-5.34,0-10.77-0.04-16.61-0.13c-0.13,0-0.25,0-0.37,0c-1.39,0-5.63,0-8.83,3.21c-3.23,3.24-3.21,7.53-3.21,8.93
c0.11,32.12,0.12,65.57,0,99.42c0,1.37-0.02,5.55,3.15,8.73c3.12,3.13,7.15,3.13,8.47,3.13c0.21,0,0.42,0,0.63-0.01
c4.16-0.11,7.78-0.17,11.09-0.17c3.53,0,6.76,0.06,9.86,0.19c0.29,0.01,0.57,0.02,0.84,0.02c3.59,0,6.49-1.08,8.61-3.22
c3.27-3.29,3.23-7.73,3.22-9.4c-0.02-2.5-0.04-5.01-0.05-7.53c0.22,0.74,0.43,1.48,0.65,2.22c1.97,6.79,6.32,10.23,12.91,10.23
c0.26,0,0.54-0.01,0.82-0.02c3.58-0.14,7.29-0.22,11.03-0.22c3.68,0,7.5,0.07,11.35,0.21c0.27,0.01,0.54,0.01,0.8,0.01
c1.34,0,2.79-0.15,4.24-0.56c-7.07,21.09-14.23,42.25-21.28,63.04c-3.54-10.78-7.15-21.8-10.62-32.69
c-2.45-7.67-7.68-8.82-10.58-8.82c-1.62,0-3.32,0.35-5.2,1.08c-6.4,2.48-13.03,4.76-19.71,6.77c-3.66,1.1-6.22,3.07-7.61,5.86
c-2.1,4.2-0.59,8.28,0.05,10.02c9.7,26.29,18.41,50.2,26.63,73.09c2.17,6.04,6.69,9.24,13.07,9.24c0.16,0,0.31,0,0.47-0.01
c4.28-0.09,8.89-0.18,13.54-0.18c4.19,0,7.93,0.07,11.45,0.21c0.32,0.01,0.64,0.02,0.94,0.02c10.55,0,13.4-8.01,14.33-10.64
c8.58-24.11,17.45-48.58,26.02-72.25c3.18-8.77,6.35-17.54,9.52-26.31c3.04-8.42,6.07-16.85,9.17-25.5c1.44-4.01,2.9-8.07,4.38-12.2
c0.88-2.45,0.51-5.18-0.99-7.31c-1.05-1.49-2.55-2.55-4.25-3.06c1.05-0.49,1.98-1.15,2.81-1.98c3.08-3.09,3.07-7.13,3.06-8.46
c-0.08-23.52-0.08-44.26,0.01-63.42c0.01-1.34,0.03-5.41-3.11-8.55c-3.12-3.13-7.15-3.13-8.47-3.13l-0.24,0
c-5.84,0.07-11.56,0.1-17,0.1c-5.74,0-11.38-0.04-16.79-0.11l-0.27,0c-4.32,0-9.97,1.6-12.37,9.2c-2.55,8.06-5.3,16.25-7.97,24.17
c-0.94,2.79-1.88,5.58-2.81,8.38c-0.78,2.35-1.57,4.7-2.36,7.07c-4.33-12.9-8.75-26.12-12.99-39.17c-2.08-6.41-6.43-9.66-12.93-9.66
c-0.12,0-0.25,0-0.38,0C168.53,79.67,163.11,79.72,157.57,79.72 M230.91,164.86c0.33,0.18,0.68,0.35,1.03,0.49
c-0.46,0.14-0.91,0.31-1.33,0.49C230.71,165.51,230.81,165.19,230.91,164.86 M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200
s200-89.54,200-200S310.46,0,200,0L200,0z M157.57,87.72c5.42,0,10.84-0.04,16.26-0.14c0.08,0,0.16,0,0.24,0
c3.08,0,4.35,1.14,5.32,4.13c6.06,18.66,12.35,37.24,18.59,55.84c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28
c3.63-10.88,7.36-21.73,10.82-32.67c0.84-2.66,2.02-3.61,4.74-3.61c0.05,0,0.11,0,0.16,0c5.63,0.08,11.26,0.11,16.9,0.11
c5.7,0,11.4-0.04,17.09-0.1c0.05,0,0.1,0,0.15,0c2.66,0,3.59,0.82,3.58,3.65c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.45-0.61,3.48-3.15,3.48c-0.06,0-0.13,0-0.2,0c-3.95-0.08-7.91-0.13-11.86-0.13c-3.71,0-7.42,0.04-11.13,0.14
c-0.09,0-0.17,0-0.26,0c-2.86,0-3.26-1.36-3.22-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91
c-4.76,15.41-9.49,30.83-13.94,46.32c-0.91,3.19-2.23,4.33-5.32,4.33c-0.16,0-0.33,0-0.51-0.01c-3.87-0.14-7.76-0.22-11.64-0.22
c-3.79,0-7.57,0.07-11.35,0.22c-0.17,0.01-0.33,0.01-0.49,0.01c-3.2,0-4.33-1.38-5.22-4.47c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,17.99-0.08,35.99,0.07,53.98c0.03,3.18-0.62,4.56-3.82,4.56
c-0.17,0-0.34,0-0.52-0.01c-3.39-0.14-6.79-0.19-10.18-0.19c-3.77,0-7.54,0.07-11.3,0.17c-0.14,0-0.28,0.01-0.42,0.01
c-2.65,0-3.63-0.79-3.62-3.83c0.11-33.16,0.11-66.32,0-99.47c-0.01-3.29,1.09-4.11,4.04-4.11c0.08,0,0.17,0,0.25,0
C146.42,87.67,152,87.72,157.57,87.72L157.57,87.72z M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.26-3.8,2.8-5.65,6.8-5.65c0.26,0,0.52,0.01,0.8,0.02c3.39,0.18,6.79,0.24,10.24,0.24c4.95,0,10-0.11,15.28-0.11
c-4.67,13.01-9.09,25.35-13.54,37.68c-11.87,32.86-23.84,65.69-35.55,98.6c-1.33,3.74-2.98,5.32-6.79,5.32c-0.2,0-0.41,0-0.62-0.01
c-3.92-0.16-7.85-0.22-11.78-0.22c-4.57,0-9.14,0.08-13.71,0.18c-0.1,0-0.2,0-0.3,0c-2.92,0-4.48-0.97-5.54-3.94
c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45c6.85-2.06,13.63-4.38,20.29-6.97
c0.93-0.36,1.68-0.54,2.31-0.54c1.53,0,2.27,1.09,2.96,3.25c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6L194.36,281.6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#ffffff;}
</style>
<path class="st0" d="M200,8c25.92,0,51.07,5.08,74.73,15.09c22.86,9.67,43.4,23.51,61.03,41.15c17.64,17.64,31.48,38.17,41.15,61.03
C386.92,148.93,392,174.08,392,200s-5.08,51.07-15.09,74.73c-9.67,22.86-23.51,43.4-41.15,61.03
c-17.64,17.64-38.17,31.48-61.03,41.15C251.07,386.92,225.92,392,200,392s-51.07-5.08-74.73-15.09
c-22.86-9.67-43.4-23.51-61.03-41.15c-17.64-17.64-31.48-38.17-41.15-61.03C13.08,251.07,8,225.92,8,200s5.08-51.07,15.08-74.73
c9.67-22.86,23.51-43.4,41.15-61.03c17.64-17.64,38.17-31.48,61.03-41.15C148.93,13.08,174.08,8,200,8 M157.57,79.72
c-5.34,0-10.77-0.04-16.61-0.13c-0.13,0-0.25,0-0.37,0c-1.39,0-5.63,0-8.83,3.21c-3.23,3.24-3.21,7.53-3.21,8.93
c0.11,32.12,0.12,65.57,0,99.42c0,1.37-0.02,5.55,3.15,8.73c3.12,3.13,7.15,3.13,8.47,3.13c0.21,0,0.42,0,0.63-0.01
c4.16-0.11,7.78-0.17,11.09-0.17c3.53,0,6.76,0.06,9.86,0.19c0.29,0.01,0.57,0.02,0.84,0.02c3.59,0,6.49-1.08,8.61-3.22
c3.27-3.29,3.23-7.73,3.22-9.4c-0.02-2.5-0.04-5.01-0.05-7.53c0.22,0.74,0.43,1.48,0.65,2.22c1.97,6.79,6.32,10.23,12.91,10.23
c0.26,0,0.54-0.01,0.82-0.02c3.58-0.14,7.29-0.22,11.03-0.22c3.68,0,7.5,0.07,11.35,0.21c0.27,0.01,0.54,0.01,0.8,0.01
c1.34,0,2.79-0.15,4.24-0.56c-7.07,21.09-14.23,42.25-21.28,63.04c-3.54-10.78-7.15-21.8-10.62-32.69
c-2.45-7.67-7.68-8.82-10.58-8.82c-1.62,0-3.32,0.35-5.2,1.08c-6.4,2.48-13.03,4.76-19.71,6.77c-3.66,1.1-6.22,3.07-7.61,5.86
c-2.1,4.2-0.59,8.28,0.05,10.02c9.7,26.29,18.41,50.2,26.63,73.09c2.17,6.04,6.69,9.24,13.07,9.24c0.16,0,0.31,0,0.47-0.01
c4.28-0.09,8.89-0.18,13.54-0.18c4.19,0,7.93,0.07,11.45,0.21c0.32,0.01,0.64,0.02,0.94,0.02c10.55,0,13.4-8.01,14.33-10.64
c8.58-24.11,17.45-48.58,26.02-72.25c3.18-8.77,6.35-17.54,9.52-26.31c3.04-8.42,6.07-16.85,9.17-25.5c1.44-4.01,2.9-8.07,4.38-12.2
c0.88-2.45,0.51-5.18-0.99-7.31c-1.05-1.49-2.55-2.55-4.25-3.06c1.05-0.49,1.98-1.15,2.81-1.98c3.08-3.09,3.07-7.13,3.06-8.46
c-0.08-23.52-0.08-44.26,0.01-63.42c0.01-1.34,0.03-5.41-3.11-8.55c-3.12-3.13-7.15-3.13-8.47-3.13l-0.24,0
c-5.84,0.07-11.56,0.1-17,0.1c-5.74,0-11.38-0.04-16.79-0.11l-0.27,0c-4.32,0-9.97,1.6-12.37,9.2c-2.55,8.06-5.3,16.25-7.97,24.17
c-0.94,2.79-1.88,5.58-2.81,8.38c-0.78,2.35-1.57,4.7-2.36,7.07c-4.33-12.9-8.75-26.12-12.99-39.17c-2.08-6.41-6.43-9.66-12.93-9.66
c-0.12,0-0.25,0-0.38,0C168.53,79.67,163.11,79.72,157.57,79.72 M230.91,164.86c0.33,0.18,0.68,0.35,1.03,0.49
c-0.46,0.14-0.91,0.31-1.33,0.49C230.71,165.51,230.81,165.19,230.91,164.86 M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200
s200-89.54,200-200S310.46,0,200,0L200,0z M157.57,87.72c5.42,0,10.84-0.04,16.26-0.14c0.08,0,0.16,0,0.24,0
c3.08,0,4.35,1.14,5.32,4.13c6.06,18.66,12.35,37.24,18.59,55.84c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28
c3.63-10.88,7.36-21.73,10.82-32.67c0.84-2.66,2.02-3.61,4.74-3.61c0.05,0,0.11,0,0.16,0c5.63,0.08,11.26,0.11,16.9,0.11
c5.7,0,11.4-0.04,17.09-0.1c0.05,0,0.1,0,0.15,0c2.66,0,3.59,0.82,3.58,3.65c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.45-0.61,3.48-3.15,3.48c-0.06,0-0.13,0-0.2,0c-3.95-0.08-7.91-0.13-11.86-0.13c-3.71,0-7.42,0.04-11.13,0.14
c-0.09,0-0.17,0-0.26,0c-2.86,0-3.26-1.36-3.22-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91
c-4.76,15.41-9.49,30.83-13.94,46.32c-0.91,3.19-2.23,4.33-5.32,4.33c-0.16,0-0.33,0-0.51-0.01c-3.87-0.14-7.76-0.22-11.64-0.22
c-3.79,0-7.57,0.07-11.35,0.22c-0.17,0.01-0.33,0.01-0.49,0.01c-3.2,0-4.33-1.38-5.22-4.47c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,17.99-0.08,35.99,0.07,53.98c0.03,3.18-0.62,4.56-3.82,4.56
c-0.17,0-0.34,0-0.52-0.01c-3.39-0.14-6.79-0.19-10.18-0.19c-3.77,0-7.54,0.07-11.3,0.17c-0.14,0-0.28,0.01-0.42,0.01
c-2.65,0-3.63-0.79-3.62-3.83c0.11-33.16,0.11-66.32,0-99.47c-0.01-3.29,1.09-4.11,4.04-4.11c0.08,0,0.17,0,0.25,0
C146.42,87.67,152,87.72,157.57,87.72L157.57,87.72z M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.26-3.8,2.8-5.65,6.8-5.65c0.26,0,0.52,0.01,0.8,0.02c3.39,0.18,6.79,0.24,10.24,0.24c4.95,0,10-0.11,15.28-0.11
c-4.67,13.01-9.09,25.35-13.54,37.68c-11.87,32.86-23.84,65.69-35.55,98.6c-1.33,3.74-2.98,5.32-6.79,5.32c-0.2,0-0.41,0-0.62-0.01
c-3.92-0.16-7.85-0.22-11.78-0.22c-4.57,0-9.14,0.08-13.71,0.18c-0.1,0-0.2,0-0.3,0c-2.92,0-4.48-0.97-5.54-3.94
c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45c6.85-2.06,13.63-4.38,20.29-6.97
c0.93-0.36,1.68-0.54,2.31-0.54c1.53,0,2.27,1.09,2.96,3.25c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6L194.36,281.6z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200s200-89.54,200-200S310.46,0,200,0z M249.67,210.48
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35c0.5,1.54,1.06,3.06,1.64,4.71
c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13
C258.54,185.81,254.12,198.15,249.67,210.48z M263.37,154.73c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01
c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32
c-0.96,3.36-2.37,4.44-5.83,4.32c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45
c-4.73-16.28-9.75-32.48-14.68-48.71c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98
c0.03,3.35-0.68,4.69-4.34,4.55c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47
c-0.01-3.38,1.15-4.16,4.29-4.11c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67c0.86-2.71,2.07-3.64,4.9-3.61
c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64C263.28,112.41,263.29,133.57,263.37,154.73z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@ -1,357 +0,0 @@
// mieux voter vars
$mv-blue-color: #009900 !default;
$mv-red-color: #000099 !default;
$mv-light-color: #efefff !default;
$mv-dark-color: #333 !default;
// Override default variables before the import bootstrap
$body-bg: #000000 !default;
$body-color: $mv-light-color !default;
$theme-colors: (
"primary": $mv-blue-color,
"secondary": $mv-red-color,
"light": $mv-light-color,
"dark": $mv-dark-color,
"danger": #990000,
"success": #009900,
"info": #2b8299,
"warning": #ff6e11
) !default;
.logo-text > h1 {
font-size: 16px;
font-weight: bold;
margin: 0;
line-height: 1;
}
.bold {
font-weight: bold;
}
.logo-text > h1 > small {
display: block;
letter-spacing: 0.09em;
}
html,
body,
#root,
#root > div {
height: 100%;
}
main {
background-image: url("/background-mv.png");
background-size: 100%;
background-attachment: fixed;
background-repeat: no-repeat;
background-color: $mv-blue-color;
min-height: calc(100% - 128px);
overflow: auto;
padding-top: 72px;
padding-bottom: 100px;
}
header {
position: fixed;
z-index: 10;
width: 100%;
}
footer {
background-color: $body-bg;
color: $mv-light-color;
padding: 25px;
}
footer a {
color: $mv-light-color;
}
footer a:hover {
color: #fff;
}
hr {
border: none;
border-top: 1px solid $mv-light-color;
width: 100%;
margin: auto;
}
ul.sortable,
li.sortable {
padding: 0;
margin: 0 0 0 0;
list-style-type: none;
}
li.sortable {
margin: 0 0 15px 0;
}
.pointer {
cursor: pointer;
}
.modal {
color: $mv-dark-color;
}
/* card Vote */
.cardVote {
background-color: $mv-light-color;
margin: 1em 0;
color: $mv-dark-color;
border-radius: 0.15em;
padding: 1em 0;
}
.cardVote .nowrap {
white-space: nowrap;
}
.cardVote hr {
border-top: 1px solid $mv-dark-color;
margin: 10px 0;
}
.cardVote.row:hover {
background-color: $mv-light-color-hover;
}
/* checkbox */
/* The radio */
.radio {
display: block;
position: relative;
padding-left: 30px;
margin-bottom: 12px;
cursor: pointer;
font-size: 20px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Hide the browser's default radio button */
.radio input {
position: absolute;
opacity: 0;
cursor: pointer;
}
/* Create a custom radio button */
.checkround {
position: absolute;
top: 6px;
left: 0;
height: 20px;
width: 20px;
background-color: #fff;
border-color: $mv-blue-color;
border-style: solid;
border-width: 2px;
border-radius: 50%;
}
.checkround.checkround-gray {
border-color: $gray-600;
}
/* When the radio button is checked, add a blue background */
.radio input:checked ~ .checkround {
background-color: #fff;
}
/* Create the indicator (the dot/circle - hidden when not checked) */
.checkround:after {
content: "";
position: absolute;
display: none;
}
/* Show the indicator (dot/circle) when checked */
.radio input:checked ~ .checkround:after {
display: block;
}
/* Style the indicator (dot/circle) */
.radio .checkround:after {
left: 2px;
top: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: $mv-blue-color;
}
/*.radio .checkround.checkround-gray:after {
background: $gray-600;
}*/
/* The check */
.check {
display: block;
position: relative;
padding-left: 25px;
margin-bottom: 12px;
padding-right: 15px;
cursor: pointer;
font-size: 18px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Hide the browser's default checkbox */
.check input {
position: absolute;
opacity: 0;
cursor: pointer;
}
/* Create a custom checkbox */
.checkmark {
position: absolute;
margin-left: calc(50% - 12px);
top: 0;
left: 0;
height: 24px;
width: 24px;
background-color: #fff;
border-color: $mv-blue-color;
border-style: solid;
border-width: 2px;
}
@include media-breakpoint-down(md) {
.checkmark {
position: absolute;
margin-left: 0;
top: 3px;
}
}
/* When the checkbox is checked, add a blue background */
.check input:checked ~ .checkmark {
background-color: #fff;
}
/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
.check input:checked ~ .checkmark:after {
display: block;
}
/* Style the checkmark/indicator */
.check .checkmark:after {
left: 5px;
top: 1px;
width: 10px;
height: 15px;
border: solid;
border-color: #fff;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
cursor: pointer;
}
.cust-btn {
margin-bottom: 10px;
background-color: $mv-blue-color;
border-width: 2px;
border-color: $mv-blue-color;
color: #fff;
}
.cust-btn:hover {
border-color: $mv-blue-color;
background-color: #fff;
color: $mv-blue-color;
border-radius: 20px;
transform-style: 2s;
}
/** collapse **/
.panel-title:after {
content: "+";
float: right;
font-size: 28px;
font-weight: 900;
}
.panel-title.collapsed:after {
content: "-";
}
/** table profiles **/
.profiles thead,
.profiles tbody,
.profiles tr,
.profiles th,
.profiles td,
.profiles thead th {
border-color: $mv-blue-color;
color: $mv-blue-color;
}
.median {
border-width: 0 3px 0 0;
border-style: dashed;
border-color: #000;
min-height: 30px;
width: 1px;
position: absolute;
left: 50%;
margin-top: -15px;
margin-left: 13px;
}
/** react multi email **/
.react-multi-email > span[data-placeholder] {
padding: 0.25em !important;
}
/** flag selector **/
.flag-select > button {
height: 35px;
}
.flag-select__options {
width: 65px;
text-align: center;
background-color: $mv-light-color !important;
}
.flag-select__options .flag-select__option {
padding: 0;
margin: 0;
}
.flag-select__options .flag-select__option__icon {
top: 0;
}
/** result **/
ol.result > li{
font-size:1rem;
font-weight:normal;
}
ol.result > li:nth-child(1){
font-size:1.75rem;
font-weight:bold;
}
ol.result > li:nth-child(2){
font-size:1.5rem;
}
ol.result > li:nth-child(3){
font-size:1.25rem;
}

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

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

@ -1,138 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
// eslint-disable-next-line no-undef
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
// eslint-disable-next-line no-undef
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://bit.ly/CRA-PWA"
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
"New content is available and will be used when all " +
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(contentType != null && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
"No internet connection found. App is running in offline mode."
);
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
Loading…
Cancel
Save