diff --git a/src/components/views/CreateElection.jsx b/src/components/views/CreateElection.jsx
new file mode 100644
index 0000000..b4940dc
--- /dev/null
+++ b/src/components/views/CreateElection.jsx
@@ -0,0 +1,896 @@
+/* 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 }) => (
+ {children}
+));
+
+const displayClockOptions = () =>
+ Array(24)
+ .fill(1)
+ .map((x, i) => (
+
+ ));
+
+const SortableCandidate = sortableElement(
+ ({ candidate, sortIndex, form, t }) => (
+
+
+
+
+
+
+ {sortIndex + 1}
+
+
+ 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"
+ />
+
+
+
+
+ {t("Delete?")}
+
+ {t("Are you sure to delete")}{" "}
+ {candidate.label !== "" ? (
+ "{candidate.label}"
+ ) : (
+
+ {t("the row")} {sortIndex + 1}
+
+ )}{" "}
+ ?
+
+ form.removeCandidate(sortIndex)}
+ >
+ Oui
+
+ Non
+
+
+
+
+
+ {t(
+ "Enter the name of your candidate or proposal here (250 characters max.)"
+ )}
+
+
+
+
+ )
+);
+
+const SortableCandidatesContainer = sortableContainer(({ items, form, t }) => {
+ return (
+
+ {items.map((candidate, index) => (
+
+ ))}
+
+ );
+});
+
+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_result: 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 ;
+
+ return (
+
+
+ {waiting ? : ""}
+
+
+ );
+ }
+}
+
+export default withTranslation()(withRouter(CreateElection));
\ No newline at end of file