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 (
{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,
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 ? : ""}
);
}
}
export default withTranslation()(withRouter(CreateElection));