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( "Write here your question or introduce simple your election (250 characters max.)" )}
  • ) ); const SortableCandidatesContainer = sortableContainer(({ items, form, t }) => { return ( ); }); 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, 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); } handleChangeTitle = event => { this.setState({ title: event.target.value }); }; 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, i) => { 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 !== "") 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, start, finish, electorEmails } = this.state; const endpoint = resolve( this.context.urlServer, this.context.routesServer.setElection ); 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(state => ({ 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 ? : ""}

    {t("Start an election")}


    {t( "Write here your question or introduce simple your election (250 characters max.)" )}
    {t("For example:")}{" "} {t( "For the role of my representative, I judge this candidate..." )}

    {t("Starting date")} { this.setState({ start: new Date( timeMinusDate(start) + new Date(e.target.valueAsNumber).getTime() ) }); }} />
    {t("Ending date")} { this.setState({ finish: new Date( timeMinusDate(finish) + new Date(getOnlyValidDate(e.target.valueAsNumber)).getTime() ) }); }} />
    {t("Grades")} {t( "You can select here the number of grades for your election" )}
    {t("For example:")}{" "} {" "} {t("5 = Excellent, Very good, Good, Fair, Passable")}
    {grades.map((mention, i) => { return ( {mention.label} ); })}

    {t("Participants")} { this.setState({ electorEmails: _emails }); }} validateEmail={email => { return isEmail(email); // return boolean }} getLabel={(email, index, removeEmail) => { return (
    {email} removeEmail(index)} > ×
    ); }} />
    {t( "If you list voters' emails, only them will be able to access the election" )}

    {check.ok ? (
    {t("Validate")}
    {t("Confirm your vote")}
    {t("Question of the election")}
    {title}
    {t("Candidates/Proposals")}
      {candidates.map((candidate, i) => { if (candidate.label !== "") { return (
    • {candidate.label}
    • ); } else { return
    • ; } })}
    {t("Dates")}

    {t("The election will take place from")}{" "} {start.toLocaleDateString()}, {t("at")}{" "} {start.toLocaleTimeString()} {" "} {t("to")}{" "} {finish.toLocaleDateString()}, {t("at")}{" "} {finish.toLocaleTimeString()}

    {t("Grades")}
    {grades.map((mention, i) => { return i < numGrades ? ( {mention.label} ) : ( ); })}
    {t("Voters' list")}
    {electorEmails.length > 0 ? ( electorEmails.join(", ") ) : (

    {t("The form contains no address.")}
    {t( "The election will be opened to anyone with the link" )}

    )}
    {this.state.restrictResult ? (
    {t("Accès aux résultats")}

    {electorEmails.length > 0 ? ( {t( "The results page will not be accessible until all participants have voted." )} ) : ( {t( "The results page will not be accessible until the end date is reached." )}{" "} ({finish.toLocaleDateString()} {t("at")}{" "} {finish.toLocaleTimeString()}) )}

    ) : null}
    {t("Start the election")}
    {t("Cancel")}
    ) : ( )}
    ); } } export default withTranslation()(withRouter(CreateElection));