Browse Source

fix env

master
Pierre-Louis Guhur 1 year ago
parent
commit
720a9d990d
  1. 1
      .gitignore
  2. 11
      pages/vote/[pid]/[[...tid]].jsx
  3. 5
      services/api.js
  4. 0
      src/App.css
  5. 20
      src/App.jsx
  6. 90
      src/App.test.jsx
  7. 29
      src/AppContext.jsx
  8. 61
      src/Errors.js
  9. 48
      src/Routes.jsx
  10. 37
      src/Util.jsx
  11. 57
      src/components/CopyField.jsx
  12. 29
      src/components/banner/Facebook.jsx
  13. 29
      src/components/banner/Gform.jsx
  14. 24
      src/components/banner/Helloasso.jsx
  15. 44
      src/components/banner/Paypal.jsx
  16. 4
      src/components/flag.js
  17. 62
      src/components/form/ButtonWithConfirm.jsx
  18. 75
      src/components/form/HelpButton.jsx
  19. 53
      src/components/form/ModalConfirm.jsx
  20. 97
      src/components/layouts/Footer.jsx
  21. 62
      src/components/layouts/Header.jsx
  22. 30
      src/components/layouts/LanguageSelector.jsx
  23. 25
      src/components/layouts/footer.css
  24. 20
      src/components/layouts/useBbox.jsx
  25. 14
      src/components/loader/index.jsx
  26. BIN
      src/components/loader/loader-pulse-2-alpha.gif
  27. BIN
      src/components/loader/loader-pulse-2.gif
  28. 18
      src/components/loader/style.css
  29. 896
      src/components/views/CreateElection.jsx
  30. 141
      src/components/views/CreateSuccess.jsx
  31. 280
      src/components/views/Faq.jsx
  32. 95
      src/components/views/Home.jsx
  33. 77
      src/components/views/LegalNotices.jsx
  34. 113
      src/components/views/PrivacyPolicy.jsx
  35. 419
      src/components/views/Result.jsx
  36. 46
      src/components/views/UnknownElection.jsx
  37. 39
      src/components/views/UnknownView.jsx
  38. 355
      src/components/views/Vote.jsx
  39. 39
      src/components/views/VoteSuccess.jsx
  40. 8
      src/components/wait/index.jsx
  41. BIN
      src/components/wait/loader-pulse-2-alpha.gif
  42. BIN
      src/components/wait/loader-pulse-2.gif
  43. 35
      src/errorCode.js
  44. 41
      src/i18n.jsx
  45. 12
      src/index.jsx
  46. 21
      src/logos/logo-black.svg
  47. 21
      src/logos/logo-blue.svg
  48. 26
      src/logos/logo-color.svg
  49. 47
      src/logos/logo-line-black.svg
  50. 47
      src/logos/logo-line-blue.svg
  51. 47
      src/logos/logo-line-white.svg
  52. 21
      src/logos/logo-white.svg
  53. 357
      src/scss/_app.scss
  54. 1
      src/scss/_bootstrap.scss
  55. 24
      src/scss/config.scss
  56. 138
      src/serviceWorker.jsx

1
.gitignore

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

11
pages/vote/[pid]/[[...tid]].jsx

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

5
services/api.js

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

0
src/App.css

20
src/App.jsx

@ -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;

90
src/App.test.jsx

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

29
src/AppContext.jsx

@ -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;

61
src/Errors.js

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

48
src/Routes.jsx

@ -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;

37
src/Util.jsx

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

57
src/components/CopyField.jsx

@ -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;

29
src/components/banner/Facebook.jsx

@ -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

29
src/components/banner/Gform.jsx

@ -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;

24
src/components/banner/Helloasso.jsx

@ -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;

44
src/components/banner/Paypal.jsx

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

4
src/components/flag.js

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

62
src/components/form/ButtonWithConfirm.jsx

@ -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;

75
src/components/form/HelpButton.jsx

@ -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;

53
src/components/form/ModalConfirm.jsx

@ -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;

97
src/components/layouts/Footer.jsx

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

62
src/components/layouts/Header.jsx

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

30
src/components/layouts/LanguageSelector.jsx

@ -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;

25
src/components/layouts/footer.css

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

20
src/components/layouts/useBbox.jsx

@ -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];
};

14
src/components/loader/index.jsx

@ -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;

BIN
src/components/loader/loader-pulse-2-alpha.gif

Before

Width: 250  |  Height: 250  |  Size: 26 KiB

BIN
src/components/loader/loader-pulse-2.gif

Before

Width: 250  |  Height: 250  |  Size: 32 KiB

18
src/components/loader/style.css

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

896
src/components/views/CreateElection.jsx

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