@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|