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.

278 lines
8.9 KiB

1 year ago
1 year ago
  1. import { useState } from "react";
  2. import Head from "next/head";
  3. import { useRouter } from "next/router";
  4. import { serverSideTranslations } from "next-i18next/serverSideTranslations";
  5. import { useTranslation } from "next-i18next";
  6. import { Button, Col, Container, Row } from "reactstrap";
  7. import { toast, ToastContainer } from "react-toastify";
  8. import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
  9. import { faCheck } from "@fortawesome/free-solid-svg-icons";
  10. import { getDetails, castBallot, apiErrors } from "@services/api";
  11. import Error from "@components/Error";
  12. import { translateGrades } from "@services/grades";
  13. import config from "../../../next-i18next.config.js";
  14. const shuffle = (array) => array.sort(() => Math.random() - 0.5);
  15. export async function getServerSideProps({ query: { pid, tid }, locale }) {
  16. const [res, translations] = await Promise.all([
  17. getDetails(
  18. pid,
  19. (res) => {
  20. console.log("DETAILS:", res);
  21. return { ok: true, ...res };
  22. },
  23. (err) => {
  24. console.log("ERR:", err);
  25. return { ok: false, err: "Unknown error" };
  26. }
  27. ),
  28. serverSideTranslations(locale, [], config),
  29. ]);
  30. if (!res.ok) {
  31. return { props: { err: res.err, ...translations } };
  32. }
  33. console.log(res);
  34. shuffle(res.candidates);
  35. return {
  36. props: {
  37. ...translations,
  38. invitationOnly: res.on_invitation_only,
  39. restrictResults: res.restrict_results,
  40. candidates: res.candidates.map((name, i) => ({ id: i, label: name })),
  41. title: res.title,
  42. numGrades: res.num_grades,
  43. pid: pid,
  44. token: tid || null,
  45. },
  46. };
  47. }
  48. const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
  49. if (err) {
  50. return <Error value={err}></Error>;
  51. }
  52. const [judgments, setJudgments] = useState([]);
  53. const colSizeCandidateLg = 4;
  54. const colSizeCandidateMd = 6;
  55. const colSizeCandidateXs = 12;
  56. const colSizeGradeLg = Math.floor((12 - colSizeCandidateLg) / numGrades);
  57. const colSizeGradeMd = Math.floor((12 - colSizeCandidateMd) / numGrades);
  58. const colSizeGradeXs = Math.floor((12 - colSizeCandidateXs) / numGrades);
  59. const router = useRouter();
  60. const { t } = useTranslation();
  61. const allGrades = translateGrades(t);
  62. const grades = allGrades.filter(
  63. (grade) => grade.value >= allGrades.length - numGrades
  64. );
  65. const handleGradeClick = (event) => {
  66. let data = {
  67. id: parseInt(event.currentTarget.getAttribute("data-id")),
  68. value: parseInt(event.currentTarget.value),
  69. };
  70. //remove candidate
  71. const newJudgments = judgments.filter(
  72. (judgment) => judgment.id !== data.id
  73. );
  74. newJudgments.push(data);
  75. setJudgments(newJudgments);
  76. };
  77. const handleSubmitWithoutAllRate = () => {
  78. toast.error(t("You have to judge every candidate/proposal!"), {
  79. position: toast.POSITION.TOP_CENTER,
  80. });
  81. };
  82. const handleSubmit = (event) => {
  83. event.preventDefault();
  84. const gradesById = {};
  85. judgments.forEach((c) => {
  86. gradesById[c.id] = c.value;
  87. });
  88. const gradesByCandidate = [];
  89. Object.keys(gradesById).forEach((id) => {
  90. gradesByCandidate.push(gradesById[id]);
  91. });
  92. castBallot(gradesByCandidate, pid, token, () => {
  93. router.push(`/vote/${pid}/confirm`);
  94. });
  95. };
  96. return (
  97. <Container>
  98. <Head>
  99. <title>{title}</title>
  100. <title>{title}</title>
  101. <meta key="og:title" property="og:title" content={title} />
  102. <meta
  103. property="og:description"
  104. key="og:description"
  105. content={t("common.application")}
  106. />
  107. </Head>
  108. <ToastContainer />
  109. <form onSubmit={handleSubmit} autoComplete="off">
  110. <Row>
  111. <Col>
  112. <h3>{title}</h3>
  113. </Col>
  114. </Row>
  115. <Row className="cardVote d-none d-lg-flex">
  116. <Col
  117. xs={colSizeCandidateXs}
  118. md={colSizeCandidateMd}
  119. lg={colSizeCandidateLg}
  120. >
  121. <h5>&nbsp;</h5>
  122. </Col>
  123. {grades.map((grade, gradeId) => {
  124. return gradeId < numGrades ? (
  125. <Col
  126. xs={colSizeGradeXs}
  127. md={colSizeGradeMd}
  128. lg={colSizeGradeLg}
  129. key={gradeId}
  130. className="text-center p-0"
  131. style={{ lineHeight: 2 }}
  132. >
  133. <small
  134. className="nowrap bold badge"
  135. style={{ backgroundColor: grade.color, color: "#fff" }}
  136. >
  137. {grade.label}
  138. </small>
  139. </Col>
  140. ) : null;
  141. })}
  142. </Row>
  143. {candidates.map((candidate, candidateId) => {
  144. return (
  145. <Row key={candidateId} className="cardVote">
  146. <Col
  147. xs={colSizeCandidateXs}
  148. md={colSizeCandidateMd}
  149. lg={colSizeCandidateLg}
  150. >
  151. <h5 className="m-0">{candidate.label}</h5>
  152. <hr className="d-lg-none" />
  153. </Col>
  154. {grades.map((grade, gradeId) => {
  155. console.assert(gradeId < numGrades);
  156. const gradeValue = grade.value;
  157. return (
  158. <Col
  159. xs={colSizeGradeXs}
  160. md={colSizeGradeMd}
  161. lg={colSizeGradeLg}
  162. key={gradeId}
  163. className="text-lg-center"
  164. >
  165. <label
  166. htmlFor={
  167. "candidateGrade" + candidateId + "-" + gradeValue
  168. }
  169. className="check"
  170. >
  171. <small
  172. className="nowrap d-lg-none ml-2 bold badge"
  173. style={
  174. judgments.find((judgment) => {
  175. return (
  176. JSON.stringify(judgment) ===
  177. JSON.stringify({
  178. id: candidate.id,
  179. value: gradeValue,
  180. })
  181. );
  182. })
  183. ? { backgroundColor: grade.color, color: "#fff" }
  184. : {
  185. backgroundColor: "transparent",
  186. color: "#000",
  187. }
  188. }
  189. >
  190. {grade.label}
  191. </small>
  192. <input
  193. type="radio"
  194. name={"candidate" + candidateId}
  195. id={"candidateGrade" + candidateId + "-" + gradeValue}
  196. data-index={candidateId}
  197. data-id={candidate.id}
  198. value={grade.value}
  199. onClick={handleGradeClick}
  200. defaultChecked={judgments.find((element) => {
  201. return (
  202. JSON.stringify(element) ===
  203. JSON.stringify({
  204. id: candidate.id,
  205. value: gradeValue,
  206. })
  207. );
  208. })}
  209. />
  210. <span
  211. className="checkmark"
  212. style={
  213. judgments.find(function (judgment) {
  214. return (
  215. JSON.stringify(judgment) ===
  216. JSON.stringify({
  217. id: candidate.id,
  218. value: gradeValue,
  219. })
  220. );
  221. })
  222. ? { backgroundColor: grade.color, color: "#fff" }
  223. : {
  224. backgroundColor: "transparent",
  225. color: "#000",
  226. }
  227. }
  228. />
  229. </label>
  230. </Col>
  231. );
  232. })}
  233. </Row>
  234. );
  235. })}
  236. <Row>
  237. <Col className="text-center">
  238. {judgments.length !== candidates.length ? (
  239. <Button
  240. type="button"
  241. onClick={handleSubmitWithoutAllRate}
  242. className="btn btn-dark "
  243. >
  244. <FontAwesomeIcon icon={faCheck} className="mr-2" />
  245. {t("Submit my vote")}
  246. </Button>
  247. ) : (
  248. <Button type="submit" className="btn btn-success ">
  249. <FontAwesomeIcon icon={faCheck} className="mr-2" />
  250. {t("Submit my vote")}
  251. </Button>
  252. )}
  253. </Col>
  254. </Row>
  255. </form>
  256. </Container>
  257. );
  258. };
  259. export default VoteBallot;