@ -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;
|
Before Width: | Height: | Size: 26 KiB |
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,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,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,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
|
||||
}}
|
||||
>
|
||||
|
||||
</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'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> </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;
|
Before Width: | Height: | Size: 26 KiB |
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();
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 4.7 KiB |
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();
|
||||
});
|
||||
}
|
||||
}
|