parent
5344f5ede2
commit
720a9d990d
@ -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,896 +0,0 @@
|
||||
/* eslint react/prop-types: 0 */
|
||||
import React, { Component } from "react";
|
||||
import { Redirect, withRouter } from "react-router-dom";
|
||||
import {
|
||||
Collapse,
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Input,
|
||||
Label,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
Button,
|
||||
Card,
|
||||
CardBody
|
||||
} from "reactstrap";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import { ReactMultiEmail, isEmail } from "react-multi-email";
|
||||
import "react-multi-email/style.css";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { resolve } from "url";
|
||||
import queryString from "query-string";
|
||||
import {
|
||||
arrayMove,
|
||||
sortableContainer,
|
||||
sortableElement,
|
||||
sortableHandle
|
||||
} from "react-sortable-hoc";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faPlus,
|
||||
faTrashAlt,
|
||||
faCheck,
|
||||
faCogs,
|
||||
faExclamationTriangle
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { i18nGrades } from "../../Util";
|
||||
import { AppContext } from "../../AppContext";
|
||||
import HelpButton from "../form/HelpButton";
|
||||
import ButtonWithConfirm from "../form/ButtonWithConfirm";
|
||||
import Loader from "../wait";
|
||||
import i18n from "../../i18n";
|
||||
|
||||
// Error messages
|
||||
const AT_LEAST_2_CANDIDATES_ERROR = "Please add at least 2 candidates.";
|
||||
const NO_TITLE_ERROR = "Please add a title.";
|
||||
|
||||
const isValidDate = date => date instanceof Date && !isNaN(date);
|
||||
const getOnlyValidDate = date => (isValidDate(date) ? date : new Date());
|
||||
|
||||
// Convert a Date object into YYYY-MM-DD
|
||||
const dateToISO = date =>
|
||||
getOnlyValidDate(date)
|
||||
.toISOString()
|
||||
.substring(0, 10);
|
||||
|
||||
// Retrieve the current hour, minute, sec, ms, time into a timestamp
|
||||
const hours = date => getOnlyValidDate(date).getHours() * 3600 * 1000;
|
||||
const minutes = date => getOnlyValidDate(date).getMinutes() * 60 * 1000;
|
||||
const seconds = date => getOnlyValidDate(date).getSeconds() * 1000;
|
||||
const ms = date => getOnlyValidDate(date).getMilliseconds();
|
||||
const time = date =>
|
||||
hours(getOnlyValidDate(date)) +
|
||||
minutes(getOnlyValidDate(date)) +
|
||||
seconds(getOnlyValidDate(date)) +
|
||||
ms(getOnlyValidDate(date));
|
||||
|
||||
// Retrieve the time part from a timestamp and remove the day. Return a int.
|
||||
const timeMinusDate = date => time(getOnlyValidDate(date));
|
||||
|
||||
// Retrieve the day and remove the time. Return a Date
|
||||
const dateMinusTime = date =>
|
||||
new Date(getOnlyValidDate(date).getTime() - time(getOnlyValidDate(date)));
|
||||
|
||||
const DragHandle = sortableHandle(({ children }) => (
|
||||
<span className="input-group-text indexNumber">{children}</span>
|
||||
));
|
||||
|
||||
const displayClockOptions = () =>
|
||||
Array(24)
|
||||
.fill(1)
|
||||
.map((x, i) => (
|
||||
<option value={i} key={i}>
|
||||
{i}h00
|
||||
</option>
|
||||
));
|
||||
|
||||
const SortableCandidate = sortableElement(
|
||||
({ candidate, sortIndex, form, t }) => (
|
||||
<li className="sortable">
|
||||
<Row key={"rowCandidate" + sortIndex}>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<DragHandle>
|
||||
<span>{sortIndex + 1}</span>
|
||||
</DragHandle>
|
||||
</InputGroupAddon>
|
||||
<Input
|
||||
type="text"
|
||||
value={candidate.label}
|
||||
onChange={event => form.editCandidateLabel(event, sortIndex)}
|
||||
onKeyPress={event =>
|
||||
form.handleKeypressOnCandidateLabel(event, sortIndex)
|
||||
}
|
||||
placeholder={t("Candidate/proposal name...")}
|
||||
tabIndex={sortIndex + 1}
|
||||
innerRef={ref => (form.candidateInputs[sortIndex] = ref)}
|
||||
maxLength="250"
|
||||
/>
|
||||
<ButtonWithConfirm className="btn btn-primary input-group-append border-light">
|
||||
<div key="button">
|
||||
<FontAwesomeIcon icon={faTrashAlt} />
|
||||
</div>
|
||||
<div key="modal-title">{t("Delete?")}</div>
|
||||
<div key="modal-body">
|
||||
{t("Are you sure to delete")}{" "}
|
||||
{candidate.label !== "" ? (
|
||||
<b>"{candidate.label}"</b>
|
||||
) : (
|
||||
<span>
|
||||
{t("the row")} {sortIndex + 1}
|
||||
</span>
|
||||
)}{" "}
|
||||
?
|
||||
</div>
|
||||
<div
|
||||
key="modal-confirm"
|
||||
onClick={() => form.removeCandidate(sortIndex)}
|
||||
>
|
||||
Oui
|
||||
</div>
|
||||
<div key="modal-cancel">Non</div>
|
||||
</ButtonWithConfirm>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs="auto" className="align-self-center pl-0">
|
||||
<HelpButton>
|
||||
{t(
|
||||
"Enter the name of your candidate or proposal here (250 characters max.)"
|
||||
)}
|
||||
</HelpButton>
|
||||
</Col>
|
||||
</Row>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
|
||||
const SortableCandidatesContainer = sortableContainer(({ items, form, t }) => {
|
||||
return (
|
||||
<ul className="sortable">
|
||||
{items.map((candidate, index) => (
|
||||
<SortableCandidate
|
||||
key={`item-${index}`}
|
||||
index={index}
|
||||
sortIndex={index}
|
||||
candidate={candidate}
|
||||
form={form}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
class CreateElection extends Component {
|
||||
static contextType = AppContext;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// default value : start at the last hour
|
||||
const now = new Date();
|
||||
const start = new Date(
|
||||
now.getTime() - minutes(now) - seconds(now) - ms(now)
|
||||
);
|
||||
const { title } = queryString.parse(this.props.location.search);
|
||||
|
||||
this.state = {
|
||||
candidates: [{ label: "" }, { label: "" }],
|
||||
title: title || "",
|
||||
isVisibleTipsDragAndDropCandidate: true,
|
||||
numGrades: 7,
|
||||
waiting: false,
|
||||
successCreate: false,
|
||||
redirectTo: null,
|
||||
isAdvancedOptionsOpen: false,
|
||||
restrictResult: false,
|
||||
isTimeLimited: false,
|
||||
start,
|
||||
// by default, the election ends in a week
|
||||
finish: new Date(start.getTime() + 7 * 24 * 3600 * 1000),
|
||||
electorEmails: []
|
||||
};
|
||||
this.candidateInputs = [];
|
||||
this.focusInput = React.createRef();
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleRestrictResultCheck = this.handleRestrictResultCheck.bind(this);
|
||||
this.handleIsTimeLimited = this.handleIsTimeLimited.bind(this);
|
||||
}
|
||||
|
||||
handleChangeTitle = event => {
|
||||
this.setState({ title: event.target.value });
|
||||
};
|
||||
|
||||
handleIsTimeLimited = event => {
|
||||
this.setState({ isTimeLimited: event.target.value === "1" });
|
||||
};
|
||||
|
||||
handleRestrictResultCheck = event => {
|
||||
this.setState({ restrictResult: event.target.value === "1" });
|
||||
};
|
||||
|
||||
addCandidate = event => {
|
||||
let candidates = this.state.candidates;
|
||||
if (candidates.length < 100) {
|
||||
candidates.push({ label: "" });
|
||||
this.setState({ candidates: candidates });
|
||||
}
|
||||
if (event.type === "keypress") {
|
||||
setTimeout(() => {
|
||||
this.candidateInputs[this.state.candidates.length - 1].focus();
|
||||
}, 250);
|
||||
}
|
||||
};
|
||||
|
||||
removeCandidate = index => {
|
||||
let candidates = this.state.candidates;
|
||||
candidates.splice(index, 1);
|
||||
if (candidates.length === 0) {
|
||||
candidates = [{ label: "" }];
|
||||
}
|
||||
this.setState({ candidates: candidates });
|
||||
};
|
||||
|
||||
editCandidateLabel = (event, index) => {
|
||||
let candidates = this.state.candidates;
|
||||
candidates[index].label = event.currentTarget.value;
|
||||
candidates.map(candidate => {
|
||||
return candidate.label;
|
||||
});
|
||||
this.setState({
|
||||
candidates: candidates
|
||||
});
|
||||
};
|
||||
|
||||
handleKeypressOnCandidateLabel = (event, index) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (index + 1 === this.state.candidates.length) {
|
||||
this.addCandidate(event);
|
||||
} else {
|
||||
this.candidateInputs[index + 1].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onCandidatesSortEnd = ({ oldIndex, newIndex }) => {
|
||||
let candidates = this.state.candidates;
|
||||
candidates = arrayMove(candidates, oldIndex, newIndex);
|
||||
this.setState({ candidates: candidates });
|
||||
};
|
||||
|
||||
handleChangeNumGrades = event => {
|
||||
this.setState({ numGrades: event.target.value });
|
||||
};
|
||||
|
||||
toggleAdvancedOptions = () => {
|
||||
this.setState({ isAdvancedOptionsOpen: !this.state.isAdvancedOptionsOpen });
|
||||
};
|
||||
|
||||
checkFields() {
|
||||
const { candidates, title } = this.state;
|
||||
if (!candidates) {
|
||||
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
|
||||
}
|
||||
|
||||
let numCandidates = 0;
|
||||
candidates.forEach(c => {
|
||||
if (c.label !== "") numCandidates += 1;
|
||||
});
|
||||
if (numCandidates < 2) {
|
||||
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
|
||||
}
|
||||
|
||||
if (!title || title === "") {
|
||||
return { ok: false, msg: NO_TITLE_ERROR };
|
||||
}
|
||||
|
||||
return { ok: true, msg: "OK" };
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const {
|
||||
candidates,
|
||||
title,
|
||||
numGrades,
|
||||
electorEmails
|
||||
} = this.state;
|
||||
|
||||
let {
|
||||
start,
|
||||
finish,
|
||||
} = this.state;
|
||||
|
||||
const endpoint = resolve(
|
||||
this.context.urlServer,
|
||||
this.context.routesServer.setElection
|
||||
);
|
||||
|
||||
if(!this.state.isTimeLimited){
|
||||
let now = new Date();
|
||||
start = new Date(
|
||||
now.getTime() - minutes(now) - seconds(now) - ms(now)
|
||||
);
|
||||
finish=new Date(start.getTime() + 10 * 365 * 24 * 3600 * 1000);
|
||||
}
|
||||
|
||||
const { t } = this.props;
|
||||
const locale =
|
||||
i18n.language.substring(0, 2).toLowerCase() === "fr" ? "fr" : "en";
|
||||
|
||||
const check = this.checkFields();
|
||||
if (!check.ok) {
|
||||
toast.error(t(check.msg), {
|
||||
position: toast.POSITION.TOP_CENTER
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ waiting: true });
|
||||
|
||||
fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
candidates: candidates.map(c => c.label).filter(c => c !== ""),
|
||||
on_invitation_only: electorEmails.length > 0,
|
||||
num_grades: numGrades,
|
||||
elector_emails: electorEmails,
|
||||
start_at: start.getTime() / 1000,
|
||||
finish_at: finish.getTime() / 1000,
|
||||
select_language: locale,
|
||||
front_url: window.location.origin,
|
||||
restrict_results: this.state.restrictResult
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.id) {
|
||||
const nextPage =
|
||||
electorEmails && electorEmails.length
|
||||
? `/link/${result.id}`
|
||||
: `/links/${result.id}`;
|
||||
this.setState(() => ({
|
||||
redirectTo: nextPage,
|
||||
successCreate: true,
|
||||
waiting: false
|
||||
}));
|
||||
} else {
|
||||
toast.error(t("Unknown error. Try again please."), {
|
||||
position: toast.POSITION.TOP_CENTER
|
||||
});
|
||||
this.setState({ waiting: false });
|
||||
}
|
||||
})
|
||||
.catch(error => error);
< |