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.
mvfront-react/pages/vote/[pid]/[[...tid]].jsx

603 lines
23 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useState, useCallback, useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useTranslation } from "next-i18next";
import { Button, Col, Container, Row, Modal, ModalHeader, ModalBody, } from "reactstrap";
import Link from "next/link";
import { toast, ToastContainer } from "react-toastify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { getDetails, castBallot, apiErrors } from "@services/api";
import Error from "@components/Error";
import { translateGrades } from "@services/grades";
import config from "../../../next-i18next.config.js";
import Footer from '@components/layouts/Footer'
import useEmblaCarousel from 'embla-carousel-react'
import { DotButton, PrevButton, NextButton } from "../../../components/form/EmblaCarouselButtons";
import VoteButtonWithConfirm from "../../../components/form/VoteButtonWithConfirm";
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
export async function getServerSideProps({ query: { pid, tid }, locale }) {
const [details, translations] = await Promise.all([
getDetails(pid),
serverSideTranslations(locale, [], config),
]);
if (typeof details === "string" || details instanceof String) {
return { props: { err: details, ...translations } };
}
if (!details.candidates || !Array.isArray(details.candidates)) {
return { props: { err: "Unknown error", ...translations } };
}
shuffle(details.candidates);
return {
props: {
...translations,
invitationOnly: details.on_invitation_only,
restrictResults: details.restrict_results,
candidates: details.candidates.map((name, i, infos) => ({ id: i, label: name, description: infos })),
title: details.title,
numGrades: details.num_grades,
pid: pid,
token: tid || null,
},
};
}
const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
const { t } = useTranslation();
if (err) {
return <Error value={apiErrors(err, t)}></Error>;
}
const [judgments, setJudgments] = useState([]);
const colSizeCandidateLg = 4;
const colSizeCandidateMd = 6;
const colSizeCandidateXs = 12;
const colSizeGradeLg = Math.floor((12 - colSizeCandidateLg) / numGrades);
const colSizeGradeMd = Math.floor((12 - colSizeCandidateMd) / numGrades);
const colSizeGradeXs = Math.floor((12 - colSizeCandidateXs) / numGrades);
const router = useRouter();
const allGrades = translateGrades(t);
const grades = allGrades.filter(
(grade) => grade.value >= allGrades.length - numGrades
);
const handleGradeClick = (event) => {
let data = {
id: parseInt(event.currentTarget.getAttribute("data-id")),
value: parseInt(event.currentTarget.value),
};
//remove candidate
const newJudgments = judgments.filter(
(judgment) => judgment.id !== data.id
);
newJudgments.push(data);
setJudgments(newJudgments);
};
const handleSubmitWithoutAllRate = () => {
alert(t("You have to judge every candidate/proposal!"));
};
const handleSubmit = (event) => {
event.preventDefault();
const gradesById = {};
judgments.forEach((c) => {
gradesById[c.id] = c.value;
});
const gradesByCandidate = [];
Object.keys(gradesById).forEach((id) => {
gradesByCandidate.push(gradesById[id]);
});
castBallot(gradesByCandidate, pid, token, () => {
router.push(`/vote/${pid}/confirm`);
});
};
const toggle = () => setVisibility(!visibled)
const [visibled, setVisibility] = useState(false);
const toggleMobile = () => setVisibilityMobile(!visibledMobile)
const [visibledMobile, setVisibilityMobile] = useState(false);
const toggleDesktop = () => setVisibilityDesktop(!visibledDesktop)
const [visibledDesktop, setVisibilityDesktop] = useState(false);
const [viewportRef, embla] = useEmblaCarousel({ skipSnaps: false });
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState([]);
const scrollPrev = useCallback(() => embla && embla.scrollPrev(), [embla]);
const scrollNext = useCallback(() => embla && embla.scrollNext(), [embla]);
const scrollTo = useCallback((index) => embla && embla.scrollTo(index), [
embla
]);
const onSelect = useCallback(() => {
if (!embla) return;
setSelectedIndex(embla.selectedScrollSnap());
setPrevBtnEnabled(embla.canScrollPrev());
setNextBtnEnabled(embla.canScrollNext());
}, [embla, setSelectedIndex]);
useEffect(() => {
if (!embla) return;
onSelect();
setScrollSnaps(embla.scrollSnapList());
embla.on("select", onSelect);
}, [embla, setScrollSnaps, onSelect]);
return (
<Container className="homePage">
<Head>
<title>{title}</title>
<title>{title}</title>
<meta key="og:title" property="og:title" content={title} />
<meta
property="og:description"
key="og:description"
content={t("common.application")}
/>
</Head>
<ToastContainer />
<Container className="homePage">
<section>
<div className="sectionOneHomeForm pb-5">
<Row className="sectionOneHomeRowOne">
<Col className="sectionOneHomeContent sectionOneVoteContent">
<Row>
<img
src="/logos/logo.svg"
alt="logo of Mieux Voter"
height="128"
className="d-block mb-5"
/>
</Row>
<Row>
<h2 className="mb-4 mt-5">{t("Bienvenue")}</h2>
</Row>
<Row>
<h4 className="mb-5">{t("Participez au vote et découvrez le vote par jugement majoritaire.")}</h4>
</Row>
<Row>
<Button
type="submit"
className="btn btn-block btn-secondary voteDesktop"
onClick={toggleDesktop}
>
{t("Je participe au vote")}
<img src="/arrow-white.svg" className="mr-2" />
</Button>
<Button
type="submit"
className="btn btn-block btn-secondary voteMobile"
onClick={toggleMobile}
>
{t("Je participe au vote")}
<img src="/arrow-white.svg" className="mr-2" />
</Button>
</Row>
<Row className="noAds my-0">
<p>{t("resource.noAds")}</p>
</Row>
<Row>
<Link href="/">
<Button className="btn-black mt-2 mb-5">
{t("En savoir plus sur Mieux voter")}
</Button>
</Link>
</Row>
</Col>
<Col></Col>
</Row>
<Row>
</Row>
</div>
</section>
<section className="sectionTwoHome">
<Row className="sectionTwoRowOne">
<Col className="sectionTwoRowOneCol">
<img
src="/urne.svg"
alt="icone d'urne"
height="128"
className="d-block mx-auto"
/>
<h4>Simple</h4>
<p>Créez un vote en moins dune minute</p>
</Col>
<Col className="sectionTwoRowOneCol">
<img
src="/email.svg"
alt="icone d'enveloppe"
height="128"
className="d-block mx-auto"
/>
<h4>Gratuit</h4>
<p>Envoyez des invitations par courriel sans limite d'envoi</p>
</Col>
<Col className="sectionTwoRowOneCol">
<img
src="/respect.svg"
alt="icone de mains qui se serrent"
height="128"
className="d-block mx-auto"
/>
<h4>Respect de votre vie privée</h4>
<p>Aucune donnée personnelle n'est enregistrée</p>
</Col>
</Row>
<Row className="sectionTwoRowTwo">
<Row className="sectionTwoHomeImage">
<img src="/vote.svg" />
</Row>
<Row className="sectionTwoRowTwoCol">
<h3 className="col-md-7">Une expérience de vote démocratique et intuitive</h3>
</Row>
<Row className="sectionTwoRowTwoCol">
<Col className="sectionTwoRowTwoColText col-md-4">
<h5 className="">Exprimez toute votre opinion</h5>
<p>Au jugement majoritaire, chaque candidat est évalué sur une grille de mention. Vous naurez plus besoin de faire un vote stratégique.</p>
</Col>
<Col className="sectionTwoRowTwoColText col-md-4 offset-md-1">
<h5 className="">Obtenez le meilleur consensus</h5>
<p>Le profil des mérites dresse un panorama précis de lopinion des électeurs. Le gagnant du vote est celui qui est la meilleure mention majoritaire.</p>
</Col>
</Row>
<Row className="sectionTwoRowThreeCol">
<Button
className="btn btn-block btn-secondary btn-sectionTwoHome"
>
Découvrez le jugement majoritaire
<img src="/arrow-white.svg" className="mr-2" />
</Button>
</Row>
</Row>
<Row className="sharing">
<p>Partagez lapplication Mieux voter</p>
<Link href="https://www.facebook.com/mieuxvoter.fr/"><img src="/facebook.svg" className="mr-2" /></Link>
<Link href="https://twitter.com/mieux_voter"><img src="/twitter.svg" className="mr-2" /></Link>
</Row>
</section>
<Footer />
</Container>
<Modal
isOpen={visibledDesktop}
toggle={toggleDesktop}
className="modalVote voteDesktop"
><div className="my-auto">
<ModalHeader className="modalVoteHeader">
{title}
</ModalHeader>
<ModalBody className="modalVoteBody">
<form onSubmit={handleSubmit} autoComplete="off">
{candidates.map((candidate, candidateId) => {
return (
<Row key={candidateId} className="cardVote">
<Col className="cardVoteLabel">
<h5 className="m-0">{candidate.label}</h5>
<h5 className="m-0">{candidate.infos}</h5>
</Col>
<Col className="cardVoteGrades">
{grades.map((grade, gradeId) => {
console.assert(gradeId < numGrades);
const gradeValue = grade.value;
return (
<Col
key={gradeId}
className="text-lg-center mx-2 voteCheck"
>
<label
htmlFor={
"candidateGrade" + candidateId + "-" + gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none ml-2 bold badge"
style={
judgments.find((judgment) => {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "transparent",
color: "#000",
}
}
>
{grade.label}
</small>
<input
type="radio"
name={"candidate" + candidateId}
id={"candidateGrade" + candidateId + "-" + gradeValue}
data-index={candidateId}
data-id={candidate.id}
value={grade.value}
onClick={handleGradeClick}
defaultChecked={judgments.find((element) => {
return (
JSON.stringify(element) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})}
/>
<span
className="checkmark candidateGrade "
style={
judgments.find(function (judgment) {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "#C3BFD8",
color: "#000",
}
}
>
<small
className="nowrap bold badge"
style={{ backgroundColor: "transparent", color: "#fff" }}
>
{grade.label}
</small>
</span>
</label>
</Col>
);
})}
</Col>
</Row>
);
})}
<Row>
<Col className="text-center">
{judgments.length !== candidates.length ? (
<VoteButtonWithConfirm className="btn btn-transparent my-3" action={handleSubmitWithoutAllRate} onClick={toggle} />
) : (
<Button type="submit" className="mt-5 btn btn-transparent">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Submit my vote")}
</Button>
)}
</Col>
</Row>
</form>
</ModalBody>
</div>
<Footer />
</Modal>
<Modal
isOpen={visibledMobile}
toggle={toggleMobile}
className="modalVote voteMobile"
><div className="my-auto">
<ModalHeader className="modalVoteHeader">
{title}
</ModalHeader>
<ModalBody className="modalVoteBody">
<form onSubmit={handleSubmit} autoComplete="off">
<div className="embla" ref={viewportRef}>
<div className="embla__container">
{candidates.map((candidate, candidateId) => {
return (
<div className="embla__slide">
<Row key={candidateId} className="cardVote">
<Col className="cardVoteLabel mb-3">
<h5 className="m-0">{candidate.label}</h5>
<h5 className="m-0">{candidate.id + 1}</h5>
</Col>
<Col className="cardVoteGrades">
{grades.map((grade, gradeId) => {
console.assert(gradeId < numGrades);
const gradeValue = grade.value;
return (
<Col
key={gradeId}
className="text-lg-center my-1 voteCheck"
>
<label
htmlFor={
"candidateGrade" + candidateId + "-" + gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none ml-2 bold badge"
style={
judgments.find((judgment) => {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "transparent",
color: "#000",
}
}
>
{grade.label}
</small>
<input
type="radio"
name={"candidate" + candidateId}
id={"candidateGrade" + candidateId + "-" + gradeValue}
data-index={candidateId}
data-id={candidate.id}
value={grade.value}
onClick={handleGradeClick}
defaultChecked={judgments.find((element) => {
return (
JSON.stringify(element) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})}
/>
<span
className="checkmark candidateGrade "
style={
judgments.find(function (judgment) {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? { backgroundColor: grade.color, color: "#fff" }
: {
backgroundColor: "#C3BFD8",
color: "#000",
}
}
>
<small
className="nowrap bold badge"
style={{ backgroundColor: "transparent", color: "#fff" }}
>
{grade.label}
</small>
</span>
</label>
</Col>
);
})}
</Col>
</Row>
<div className="d-flex embla__nav">
<div className="embla__btn embla__prev" onClick={scrollPrev}>
{candidate.id + 1}
</div>
<div className="embla__btn embla__next" onClick={scrollNext}>
Next
</div>
</div>
</div>
);
})}
</div>
<div className="embla__dots">
{scrollSnaps.map((_, index) => (
<DotButton
key={index}
selected={index === selectedIndex}
onClick={() => scrollTo(index)}
value={index + 1}
/>
))}
</div>
</div>
</form>
</ModalBody>
</div>
<Row className="btn-background mx-0">
<Col className="text-center">
{judgments.length !== candidates.length ? (
<VoteButtonWithConfirm className="btn btn-transparent my-3" action={handleSubmitWithoutAllRate} onClick={toggle} />
) : (
<Button type="submit" className="my-3 btn btn-transparent">
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Submit my vote")}
</Button>
)}
</Col>
</Row>
<Footer />
</Modal>
</Container>
);
};
export default VoteBallot;