You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

896 lines
31 KiB

2 years ago
2 years ago
  1. /* eslint react/prop-types: 0 */
  2. import React, { Component } from "react";
  3. import { Redirect, withRouter } from "react-router-dom";
  4. import {
  5. Collapse,
  6. Container,
  7. Row,
  8. Col,
  9. Input,
  10. Label,
  11. InputGroup,
  12. InputGroupAddon,
  13. Button,
  14. Card,
  15. CardBody
  16. } from "reactstrap";
  17. import { withTranslation } from "react-i18next";
  18. import { ReactMultiEmail, isEmail } from "react-multi-email";
  19. import "react-multi-email/style.css";
  20. import { toast, ToastContainer } from "react-toastify";
  21. import "react-toastify/dist/ReactToastify.css";
  22. import { resolve } from "url";
  23. import queryString from "query-string";
  24. import {
  25. arrayMove,
  26. sortableContainer,
  27. sortableElement,
  28. sortableHandle
  29. } from "react-sortable-hoc";
  30. import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
  31. import {
  32. faPlus,
  33. faTrashAlt,
  34. faCheck,
  35. faCogs,
  36. faExclamationTriangle
  37. } from "@fortawesome/free-solid-svg-icons";
  38. import { i18nGrades } from "../../Util";
  39. import { AppContext } from "../../AppContext";
  40. import HelpButton from "../form/HelpButton";
  41. import ButtonWithConfirm from "../form/ButtonWithConfirm";
  42. import Loader from "../wait";
  43. import i18n from "../../i18n";
  44. // Error messages
  45. const AT_LEAST_2_CANDIDATES_ERROR = "Please add at least 2 candidates.";
  46. const NO_TITLE_ERROR = "Please add a title.";
  47. const isValidDate = date => date instanceof Date && !isNaN(date);
  48. const getOnlyValidDate = date => (isValidDate(date) ? date : new Date());
  49. // Convert a Date object into YYYY-MM-DD
  50. const dateToISO = date =>
  51. getOnlyValidDate(date)
  52. .toISOString()
  53. .substring(0, 10);
  54. // Retrieve the current hour, minute, sec, ms, time into a timestamp
  55. const hours = date => getOnlyValidDate(date).getHours() * 3600 * 1000;
  56. const minutes = date => getOnlyValidDate(date).getMinutes() * 60 * 1000;
  57. const seconds = date => getOnlyValidDate(date).getSeconds() * 1000;
  58. const ms = date => getOnlyValidDate(date).getMilliseconds();
  59. const time = date =>
  60. hours(getOnlyValidDate(date)) +
  61. minutes(getOnlyValidDate(date)) +
  62. seconds(getOnlyValidDate(date)) +
  63. ms(getOnlyValidDate(date));
  64. // Retrieve the time part from a timestamp and remove the day. Return a int.
  65. const timeMinusDate = date => time(getOnlyValidDate(date));
  66. // Retrieve the day and remove the time. Return a Date
  67. const dateMinusTime = date =>
  68. new Date(getOnlyValidDate(date).getTime() - time(getOnlyValidDate(date)));
  69. const DragHandle = sortableHandle(({ children }) => (
  70. <span className="input-group-text indexNumber">{children}</span>
  71. ));
  72. const displayClockOptions = () =>
  73. Array(24)
  74. .fill(1)
  75. .map((x, i) => (
  76. <option value={i} key={i}>
  77. {i}h00
  78. </option>
  79. ));
  80. const SortableCandidate = sortableElement(
  81. ({ candidate, sortIndex, form, t }) => (
  82. <li className="sortable">
  83. <Row key={"rowCandidate" + sortIndex}>
  84. <Col>
  85. <InputGroup>
  86. <InputGroupAddon addonType="prepend">
  87. <DragHandle>
  88. <span>{sortIndex + 1}</span>
  89. </DragHandle>
  90. </InputGroupAddon>
  91. <Input
  92. type="text"
  93. value={candidate.label}
  94. onChange={event => form.editCandidateLabel(event, sortIndex)}
  95. onKeyPress={event =>
  96. form.handleKeypressOnCandidateLabel(event, sortIndex)
  97. }
  98. placeholder={t("Candidate/proposal name...")}
  99. tabIndex={sortIndex + 1}
  100. innerRef={ref => (form.candidateInputs[sortIndex] = ref)}
  101. maxLength="250"
  102. />
  103. <ButtonWithConfirm className="btn btn-primary input-group-append border-light">
  104. <div key="button">
  105. <FontAwesomeIcon icon={faTrashAlt} />
  106. </div>
  107. <div key="modal-title">{t("Delete?")}</div>
  108. <div key="modal-body">
  109. {t("Are you sure to delete")}{" "}
  110. {candidate.label !== "" ? (
  111. <b>&quot;{candidate.label}&quot;</b>
  112. ) : (
  113. <span>
  114. {t("the row")} {sortIndex + 1}
  115. </span>
  116. )}{" "}
  117. ?
  118. </div>
  119. <div
  120. key="modal-confirm"
  121. onClick={() => form.removeCandidate(sortIndex)}
  122. >
  123. Oui
  124. </div>
  125. <div key="modal-cancel">Non</div>
  126. </ButtonWithConfirm>
  127. </InputGroup>
  128. </Col>
  129. <Col xs="auto" className="align-self-center pl-0">
  130. <HelpButton>
  131. {t(
  132. "Enter the name of your candidate or proposal here (250 characters max.)"
  133. )}
  134. </HelpButton>
  135. </Col>
  136. </Row>
  137. </li>
  138. )
  139. );
  140. const SortableCandidatesContainer = sortableContainer(({ items, form, t }) => {
  141. return (
  142. <ul className="sortable">
  143. {items.map((candidate, index) => (
  144. <SortableCandidate
  145. key={`item-${index}`}
  146. index={index}
  147. sortIndex={index}
  148. candidate={candidate}
  149. form={form}
  150. t={t}
  151. />
  152. ))}
  153. </ul>
  154. );
  155. });
  156. class CreateElection extends Component {
  157. static contextType = AppContext;
  158. constructor(props) {
  159. super(props);
  160. // default value : start at the last hour
  161. const now = new Date();
  162. const start = new Date(
  163. now.getTime() - minutes(now) - seconds(now) - ms(now)
  164. );
  165. const { title } = queryString.parse(this.props.location.search);
  166. this.state = {
  167. candidates: [{ label: "" }, { label: "" }],
  168. title: title || "",
  169. isVisibleTipsDragAndDropCandidate: true,
  170. numGrades: 7,
  171. waiting: false,
  172. successCreate: false,
  173. redirectTo: null,
  174. isAdvancedOptionsOpen: false,
  175. restrictResult: false,
  176. isTimeLimited: false,
  177. start,
  178. // by default, the election ends in a week
  179. finish: new Date(start.getTime() + 7 * 24 * 3600 * 1000),
  180. electorEmails: []
  181. };
  182. this.candidateInputs = [];
  183. this.focusInput = React.createRef();
  184. this.handleSubmit = this.handleSubmit.bind(this);
  185. this.handleRestrictResultCheck = this.handleRestrictResultCheck.bind(this);
  186. this.handleIsTimeLimited = this.handleIsTimeLimited.bind(this);
  187. }
  188. handleChangeTitle = event => {
  189. this.setState({ title: event.target.value });
  190. };
  191. handleIsTimeLimited = event => {
  192. this.setState({ isTimeLimited: event.target.value === "1" });
  193. };
  194. handleRestrictResultCheck = event => {
  195. this.setState({ restrictResult: event.target.value === "1" });
  196. };
  197. addCandidate = event => {
  198. let candidates = this.state.candidates;
  199. if (candidates.length < 100) {
  200. candidates.push({ label: "" });
  201. this.setState({ candidates: candidates });
  202. }
  203. if (event.type === "keypress") {
  204. setTimeout(() => {
  205. this.candidateInputs[this.state.candidates.length - 1].focus();
  206. }, 250);
  207. }
  208. };
  209. removeCandidate = index => {
  210. let candidates = this.state.candidates;
  211. candidates.splice(index, 1);
  212. if (candidates.length === 0) {
  213. candidates = [{ label: "" }];
  214. }
  215. this.setState({ candidates: candidates });
  216. };
  217. editCandidateLabel = (event, index) => {
  218. let candidates = this.state.candidates;
  219. candidates[index].label = event.currentTarget.value;
  220. candidates.map(candidate => {
  221. return candidate.label;
  222. });
  223. this.setState({
  224. candidates: candidates
  225. });
  226. };
  227. handleKeypressOnCandidateLabel = (event, index) => {
  228. if (event.key === "Enter") {
  229. event.preventDefault();
  230. if (index + 1 === this.state.candidates.length) {
  231. this.addCandidate(event);
  232. } else {
  233. this.candidateInputs[index + 1].focus();
  234. }
  235. }
  236. };
  237. onCandidatesSortEnd = ({ oldIndex, newIndex }) => {
  238. let candidates = this.state.candidates;
  239. candidates = arrayMove(candidates, oldIndex, newIndex);
  240. this.setState({ candidates: candidates });
  241. };
  242. handleChangeNumGrades = event => {
  243. this.setState({ numGrades: event.target.value });
  244. };
  245. toggleAdvancedOptions = () => {
  246. this.setState({ isAdvancedOptionsOpen: !this.state.isAdvancedOptionsOpen });
  247. };
  248. checkFields() {
  249. const { candidates, title } = this.state;
  250. if (!candidates) {
  251. return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
  252. }
  253. let numCandidates = 0;
  254. candidates.forEach(c => {
  255. if (c.label !== "") numCandidates += 1;
  256. });
  257. if (numCandidates < 2) {
  258. return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
  259. }
  260. if (!title || title === "") {
  261. return { ok: false, msg: NO_TITLE_ERROR };
  262. }
  263. return { ok: true, msg: "OK" };
  264. }
  265. handleSubmit() {
  266. const {
  267. candidates,
  268. title,
  269. numGrades,
  270. electorEmails
  271. } = this.state;
  272. let {
  273. start,
  274. finish,
  275. } = this.state;
  276. const endpoint = resolve(
  277. this.context.urlServer,
  278. this.context.routesServer.setElection
  279. );
  280. if(!this.state.isTimeLimited){
  281. let now = new Date();
  282. start = new Date(
  283. now.getTime() - minutes(now) - seconds(now) - ms(now)
  284. );
  285. finish=new Date(start.getTime() + 10 * 365 * 24 * 3600 * 1000);
  286. }
  287. const { t } = this.props;
  288. const locale =
  289. i18n.language.substring(0, 2).toLowerCase() === "fr" ? "fr" : "en";
  290. const check = this.checkFields();
  291. if (!check.ok) {
  292. toast.error(t(check.msg), {
  293. position: toast.POSITION.TOP_CENTER
  294. });
  295. return;
  296. }
  297. this.setState({ waiting: true });
  298. fetch(endpoint, {
  299. method: "POST",
  300. headers: {
  301. "Content-Type": "application/json"
  302. },
  303. body: JSON.stringify({
  304. title: title,
  305. candidates: candidates.map(c => c.label).filter(c => c !== ""),
  306. on_invitation_only: electorEmails.length > 0,
  307. num_grades: numGrades,
  308. elector_emails: electorEmails,
  309. start_at: start.getTime() / 1000,
  310. finish_at: finish.getTime() / 1000,
  311. select_language: locale,
  312. front_url: window.location.origin,
  313. restrict_results: this.state.restrictResult
  314. })
  315. })
  316. .then(response => response.json())
  317. .then(result => {
  318. if (result.id) {
  319. const nextPage =
  320. electorEmails && electorEmails.length
  321. ? `/link/${result.id}`
  322. : `/links/${result.id}`;
  323. this.setState(() => ({
  324. redirectTo: nextPage,
  325. successCreate: true,
  326. waiting: false
  327. }));
  328. } else {
  329. toast.error(t("Unknown error. Try again please."), {
  330. position: toast.POSITION.TOP_CENTER
  331. });
  332. this.setState({ waiting: false });
  333. }
  334. })
  335. .catch(error => error);
  336. }
  337. handleSendNotReady = msg => {
  338. const { t } = this.props;
  339. toast.error(t(msg), {
  340. position: toast.POSITION.TOP_CENTER
  341. });
  342. };
  343. render() {
  344. const {
  345. successCreate,
  346. redirectTo,
  347. waiting,
  348. title,
  349. start,
  350. finish,
  351. candidates,
  352. numGrades,
  353. isAdvancedOptionsOpen,
  354. electorEmails
  355. } = this.state;
  356. const { t } = this.props;
  357. const grades = i18nGrades();
  358. const check = this.checkFields();
  359. if (successCreate) return <Redirect to={redirectTo} />;
  360. return (
  361. <Container>
  362. <ToastContainer />
  363. {waiting ? <Loader /> : ""}
  364. <form onSubmit={this.handleSubmit} autoComplete="off">
  365. <Row>
  366. <Col>
  367. <h3>{t("Start an election")}</h3>
  368. </Col>
  369. </Row>
  370. <hr />
  371. <Row className="mt-4">
  372. <Col xs="12">
  373. <Label for="title">{t("Question of the election")}</Label>
  374. </Col>
  375. <Col>
  376. <Input
  377. placeholder={t("Write here the question of your election")}
  378. tabIndex="1"
  379. name="title"
  380. id="title"
  381. innerRef={this.focusInput}
  382. autoFocus
  383. value={title}
  384. onChange={this.handleChangeTitle}
  385. maxLength="250"
  386. />
  387. </Col>
  388. <Col xs="auto" className="align-self-center pl-0">
  389. <HelpButton>
  390. {t(
  391. "Write here your question or introduce simple your election (250 characters max.)"
  392. )}
  393. <br />
  394. <u>{t("For example:")}</u>{" "}
  395. <em>
  396. {t(
  397. "For the role of my representative, I judge this candidate..."
  398. )}
  399. </em>
  400. </HelpButton>
  401. </Col>
  402. </Row>
  403. <Row className="mt-4">
  404. <Col xs="12">
  405. <Label for="title">{t("Candidates/Proposals")}</Label>
  406. </Col>
  407. <Col xs="12">
  408. <SortableCandidatesContainer
  409. items={candidates}
  410. onSortEnd={this.onCandidatesSortEnd}
  411. form={this}
  412. t={t}
  413. useDragHandle
  414. />
  415. </Col>
  416. </Row>
  417. <Row className="justify-content-between">
  418. <Col xs="12" sm="6" md="5" lg="4">
  419. <Button
  420. color="secondary"
  421. className="btn-block mt-2"
  422. tabIndex={candidates.length + 2}
  423. type="button"
  424. onClick={event => this.addCandidate(event)}
  425. >
  426. <FontAwesomeIcon icon={faPlus} className="mr-2" />
  427. {t("Add a proposal")}
  428. </Button>
  429. </Col>
  430. <Col
  431. xs="12"
  432. sm="6"
  433. md="12"
  434. className="text-center text-sm-right text-md-left"
  435. >
  436. <Button
  437. color="link"
  438. className="text-white mt-3 mb-1"
  439. onClick={this.toggleAdvancedOptions}
  440. >
  441. <FontAwesomeIcon icon={faCogs} className="mr-2" />
  442. {t("Advanced options")}
  443. </Button>
  444. </Col>
  445. </Row>
  446. <Collapse isOpen={isAdvancedOptionsOpen}>
  447. <Card>
  448. <CardBody className="text-primary">
  449. <Row>
  450. <Col xs="12" md="3" lg="3">
  451. <Label for="title">{t("Access to results")}</Label>
  452. </Col>
  453. <Col xs="12" md="4" lg="3">
  454. <Label className="radio " htmlFor="restrict_result_false">
  455. <span className="small text-dark">
  456. {t("Immediately")}
  457. </span>
  458. <input
  459. className="radio"
  460. type="radio"
  461. name="restrict_result"
  462. id="restrict_result_false"
  463. onClick={this.handleRestrictResultCheck}
  464. defaultChecked={!this.state.restrictResult}
  465. value="0"
  466. />
  467. <span className="checkround checkround-gray" />
  468. </Label>
  469. </Col>
  470. <Col xs="12" md="4" lg="3">
  471. <Label className="radio" htmlFor="restrict_result_true">
  472. <span className="small">
  473. <span className="text-dark">
  474. {t("At the end of the election")}
  475. </span>
  476. <HelpButton className="ml-2">
  477. {t(
  478. "No one will be able to see the result until the end date is reached or until all participants have voted."
  479. )}
  480. </HelpButton>
  481. </span>
  482. <input
  483. className="radio"
  484. type="radio"
  485. name="restrict_result"
  486. id="restrict_result_true"
  487. onClick={this.handleRestrictResultCheck}
  488. defaultChecked={this.state.restrictResult}
  489. value="1"
  490. />
  491. <span className="checkround checkround-gray" />
  492. </Label>
  493. </Col>
  494. </Row>
  495. <hr className="mt-2 mb-2" />
  496. <Row>
  497. <Col xs="12" md="3" lg="3">
  498. <Label for="title">{t("Voting time")}</Label>
  499. </Col>
  500. <Col xs="12" md="4" lg="3">
  501. <Label className="radio " htmlFor="is_time_limited_false">
  502. <span className="small text-dark">{t("Unlimited")}</span>
  503. <input
  504. className="radio"
  505. type="radio"
  506. name="time_limited"
  507. id="is_time_limited_false"
  508. onClick={this.handleIsTimeLimited}
  509. defaultChecked={!this.state.isTimeLimited}
  510. value="0"
  511. />
  512. <span className="checkround checkround-gray" />
  513. </Label>
  514. </Col>
  515. <Col xs="12" md="4" lg="3">
  516. <Label className="radio" htmlFor="is_time_limited_true">
  517. <span className="small">
  518. <span className="text-dark">
  519. {t("Defined period")}
  520. </span>
  521. </span>
  522. <input
  523. className="radio"
  524. type="radio"
  525. name="time_limited"
  526. id="is_time_limited_true"
  527. onClick={this.handleIsTimeLimited}
  528. defaultChecked={this.state.isTimeLimited}
  529. value="1"
  530. />
  531. <span className="checkround checkround-gray" />
  532. </Label>
  533. </Col>
  534. </Row>
  535. <div
  536. className={(this.state.isTimeLimited ? "d-block " : "d-none")+" bg-light p-3"}
  537. >
  538. <Row >
  539. <Col xs="12" md="3" lg="3">
  540. <span className="label">- {t("Starting date")}</span>
  541. </Col>
  542. <Col xs="6" md="4" lg="3">
  543. <input
  544. className="form-control"
  545. type="date"
  546. value={dateToISO(start)}
  547. onChange={e => {
  548. this.setState({
  549. start: new Date(
  550. timeMinusDate(start) +
  551. new Date(e.target.valueAsNumber).getTime()
  552. )
  553. });
  554. }}
  555. />
  556. </Col>
  557. <Col xs="6" md="5" lg="3">
  558. <select
  559. className="form-control"
  560. value={getOnlyValidDate(start).getHours()}
  561. onChange={e =>
  562. this.setState({
  563. start: new Date(
  564. dateMinusTime(start).getTime() +
  565. e.target.value * 3600000
  566. )
  567. })
  568. }
  569. >
  570. {displayClockOptions()}
  571. </select>
  572. </Col>
  573. </Row>
  574. <Row className="mt-2">
  575. <Col xs="12" md="3" lg="3">
  576. <span className="label">- {t("Ending date")}</span>
  577. </Col>
  578. <Col xs="6" md="4" lg="3">
  579. <input
  580. className="form-control"
  581. type="date"
  582. value={dateToISO(finish)}
  583. min={dateToISO(start)}
  584. onChange={e => {
  585. this.setState({
  586. finish: new Date(
  587. timeMinusDate(finish) +
  588. new Date(e.target.valueAsNumber).getTime()
  589. )
  590. });
  591. }}
  592. />
  593. </Col>
  594. <Col xs="6" md="5" lg="3">
  595. <select
  596. className="form-control"
  597. value={getOnlyValidDate(finish).getHours()}
  598. onChange={e =>
  599. this.setState({
  600. finish: new Date(
  601. dateMinusTime(finish).getTime() +
  602. e.target.value * 3600000
  603. )
  604. })
  605. }
  606. >
  607. {displayClockOptions()}
  608. </select>
  609. </Col>
  610. </Row>
  611. </div>
  612. <hr className="mt-2 mb-2" />
  613. <Row>
  614. <Col xs="12" md="3" lg="3">
  615. <span className="label">{t("Grades")}</span>
  616. </Col>
  617. <Col xs="10" sm="11" md="4" lg="3">
  618. <select
  619. className="form-control"
  620. tabIndex={candidates.length + 3}
  621. onChange={this.handleChangeNumGrades}
  622. defaultValue="7"
  623. >
  624. <option value="5">5</option>
  625. <option value="6">6</option>
  626. <option value="7">7</option>
  627. </select>
  628. </Col>
  629. <Col xs="auto" className="align-self-center pl-0 ">
  630. <HelpButton>
  631. {t(
  632. "You can select here the number of grades for your election"
  633. )}
  634. <br />
  635. <u>{t("For example:")}</u>{" "}
  636. <em>
  637. {" "}
  638. {t("5 = Excellent, Very good, Good, Fair, Passable")}
  639. </em>
  640. </HelpButton>
  641. </Col>
  642. <Col
  643. xs="12"
  644. md="9"
  645. lg="9"
  646. className="offset-xs-0 offset-md-3 offset-lg-3"
  647. >
  648. {grades.map((mention, i) => {
  649. return (
  650. <span
  651. key={i}
  652. className="badge badge-light mr-2 mt-2 "
  653. style={{
  654. backgroundColor: mention.color,
  655. color: "#fff",
  656. opacity: i < numGrades ? 1 : 0.3
  657. }}
  658. >
  659. {mention.label}
  660. </span>
  661. );
  662. })}
  663. </Col>
  664. </Row>
  665. <hr className="mt-2 mb-2" />
  666. <Row>
  667. <Col xs="12" md="3" lg="3">
  668. <span className="label">{t("Participants")}</span>
  669. </Col>
  670. <Col xs="12" md="9" lg="9">
  671. <ReactMultiEmail
  672. placeholder={t("Add here participants' emails")}
  673. emails={electorEmails}
  674. onChange={_emails => {
  675. this.setState({ electorEmails: _emails });
  676. }}
  677. validateEmail={email => {
  678. return isEmail(email); // return boolean
  679. }}
  680. getLabel={(email, index, removeEmail) => {
  681. return (
  682. <div data-tag key={index}>
  683. {email}
  684. <span
  685. data-tag-handle
  686. onClick={() => removeEmail(index)}
  687. >
  688. ×
  689. </span>
  690. </div>
  691. );
  692. }}
  693. />
  694. <div>
  695. <small className="text-muted">
  696. {t(
  697. "If you list voters' emails, only them will be able to access the election"
  698. )}
  699. </small>
  700. </div>
  701. </Col>
  702. </Row>
  703. <hr className="mt-2 mb-2" />
  704. </CardBody>
  705. </Card>
  706. </Collapse>
  707. <Row className="justify-content-end mt-2">
  708. <Col xs="12" md="3">
  709. {check.ok ? (
  710. <ButtonWithConfirm
  711. className="btn btn-success float-right btn-block"
  712. tabIndex={candidates.length + 4}
  713. >
  714. <div key="button">
  715. <FontAwesomeIcon icon={faCheck} className="mr-2" />
  716. {t("Validate")}
  717. </div>
  718. <div key="modal-title" className="text-primary bold">
  719. {t("Confirm your vote")}
  720. </div>
  721. <div key="modal-body">
  722. <div className="mt-1 mb-1">
  723. <div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
  724. {t("Question of the election")}
  725. </div>
  726. <div className="p-2 pl-3 pr-3 bg-light mb-3">{title}</div>
  727. <div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
  728. {t("Candidates/Proposals")}
  729. </div>
  730. <div className="p-2 pl-3 pr-3 bg-light mb-3">
  731. <ul className="m-0 pl-4">
  732. {candidates.map((candidate, i) => {
  733. if (candidate.label !== "") {
  734. return (
  735. <li key={i} className="m-0">
  736. {candidate.label}
  737. </li>
  738. );
  739. } else {
  740. return <li key={i} className="d-none" />;
  741. }
  742. })}
  743. </ul>
  744. </div>
  745. <div className={(this.state.isTimeLimited ? "d-block " : "d-none")} >
  746. <div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
  747. {t("Dates")}
  748. </div>
  749. <div className="p-2 pl-3 pr-3 bg-light mb-3">
  750. {t("The election will take place from")}{" "}
  751. <b>
  752. {start.toLocaleDateString()}, {t("at")}{" "}
  753. {start.toLocaleTimeString()}
  754. </b>{" "}
  755. {t("to")}{" "}
  756. <b>
  757. {finish.toLocaleDateString()}, {t("at")}{" "}
  758. {finish.toLocaleTimeString()}
  759. </b>
  760. </div>
  761. </div>
  762. <div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
  763. {t("Grades")}
  764. </div>
  765. <div className="p-2 pl-3 pr-3 bg-light mb-3">
  766. {grades.map((mention, i) => {
  767. return i < numGrades ? (
  768. <span
  769. key={i}
  770. className="badge badge-light mr-2 mt-2"
  771. style={{
  772. backgroundColor: mention.color,
  773. color: "#fff"
  774. }}
  775. >
  776. {mention.label}
  777. </span>
  778. ) : (
  779. <span key={i} />
  780. );
  781. })}
  782. </div>
  783. <div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
  784. {t("Voters' list")}
  785. </div>
  786. <div className="p-2 pl-3 pr-3 bg-light mb-3">
  787. {electorEmails.length > 0 ? (
  788. electorEmails.join(", ")
  789. ) : (
  790. <p>
  791. {t("The form contains no address.")}
  792. <br />
  793. <em>
  794. {t(
  795. "The election will be opened to anyone with the link"
  796. )}
  797. </em>
  798. </p>
  799. )}
  800. </div>
  801. {this.state.restrictResult ? (
  802. <div>
  803. <div className="small bg-primary text-white p-3 mt-2 rounded">
  804. <h6 className="m-0 p-0">
  805. <FontAwesomeIcon
  806. icon={faExclamationTriangle}
  807. className="mr-2"
  808. />
  809. <u>{t("Results available at the close of the vote")}</u>
  810. </h6>
  811. <p className="m-2 p-0">
  812. {electorEmails.length > 0 ? (
  813. <span>
  814. {t(
  815. "The results page will not be accessible until all participants have voted."
  816. )}
  817. </span>
  818. ) : (
  819. <span>
  820. {t(
  821. "The results page will not be accessible until the end date is reached."
  822. )}{" "}
  823. ({finish.toLocaleDateString()} {t("at")}{" "}
  824. {finish.toLocaleTimeString()})
  825. </span>
  826. )}
  827. </p>
  828. </div>
  829. </div>
  830. ) : null}
  831. </div>
  832. </div>
  833. <div key="modal-confirm" onClick={this.handleSubmit}>
  834. {t("Start the election")}
  835. </div>
  836. <div key="modal-cancel">{t("Cancel")}</div>
  837. </ButtonWithConfirm>
  838. ) : (
  839. <Button
  840. type="button"
  841. className="btn btn-dark float-right btn-block"
  842. onClick={this.handleSendWithoutCandidate}
  843. >
  844. <FontAwesomeIcon icon={faCheck} className="mr-2" />
  845. {t("Confirm")}
  846. </Button>
  847. )}
  848. </Col>
  849. </Row>
  850. </form>
  851. </Container>
  852. );
  853. }
  854. }
  855. export default withTranslation()(withRouter(CreateElection));