feat: convert to typescript

pull/89/head
Pierre-Louis Guhur 1 year ago
parent daafcc9036
commit 43b0720eb7

@ -5,7 +5,10 @@ import DatePicker from 'react-datepicker'
const CustomDatePicker = ({icon, date, setDate}) => {
return (<DatePicker selected={startDate} onChange={(date) => setStartDate(date)} />);
return (<DatePicker
selected={date}
onChange={(date) => setDate(date)}
/>);
// const ExampleCustomInput = forwardRef(({value, onClick}, ref) => (
// <Button onClick={onClick} ref={ref}>
// <Row className='gx-2 align-items-end'>

@ -44,8 +44,8 @@ const AccessResults = () => {
const LimitDate = () => {
const {t} = useTranslation();
const defaultEndDate = new Date();
defaultEndDate.setUTCDate(endDate.getUTCDate() + 15)
const [endDate, setStartDate] = useState(defaultEndDate);
defaultEndDate.setUTCDate(defaultEndDate.getUTCDate() + 15)
const [endDate, setEndDate] = useState(defaultEndDate);
const election = useElection();
const dispatch = useElectionDispatch();
@ -58,7 +58,7 @@ const LimitDate = () => {
dispatch({
'type': 'set',
'field': 'endVote',
'value': hasDate() ? null : endVote,
'value': hasDate() ? null : endDate,
})
}
@ -72,7 +72,7 @@ const LimitDate = () => {
}
</Col>
<Col className='col-auto d-flex align-items-center'>
<Switch toggle={toggle} state={endVote} />
<Switch toggle={toggle} state={endDate} />
<DatePicker />
</Col>
</Row>

@ -1,7 +1,7 @@
import Link from "next/link";
import {useTranslation} from "next-i18next";
import {Button, Row, Col} from "reactstrap";
import Logo from '@components/Logo.jsx';
import Logo from '@components/Logo';
import LanguageSelector from "@components/layouts/LanguageSelector";
const Footer = () => {

5
next-env.d.ts vendored

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

19
package-lock.json generated

@ -15,6 +15,8 @@
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@types/node": "^18.11.9",
"@types/react": "^18.0.24",
"bootstrap": "^5.2.2",
"bootstrap-scss": "^5.2.2",
"clipboard": "^2.0.10",
@ -34,7 +36,8 @@
"react-i18next": "^12.0.0",
"react-toastify": "^9.1.0",
"reactstrap": "^9.1.4",
"sass": "^1.32.13"
"sass": "^1.32.13",
"typescript": "^4.8.4"
},
"devDependencies": {
"eslint": "^8.11.0",
@ -944,6 +947,11 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@ -4289,7 +4297,6 @@
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -5100,6 +5107,11 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
},
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@ -7396,8 +7408,7 @@
"typescript": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"peer": true
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
},
"unbox-primitive": {
"version": "1.0.2",

@ -17,6 +17,8 @@
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@types/node": "^18.11.9",
"@types/react": "^18.0.24",
"bootstrap": "^5.2.2",
"bootstrap-scss": "^5.2.2",
"clipboard": "^2.0.10",
@ -36,7 +38,8 @@
"react-i18next": "^12.0.0",
"react-toastify": "^9.1.0",
"reactstrap": "^9.1.4",
"sass": "^1.32.13"
"sass": "^1.32.13",
"typescript": "^4.8.4"
},
"devDependencies": {
"eslint": "^8.11.0",

@ -4,7 +4,7 @@ import Image from "next/image";
import {serverSideTranslations} from "next-i18next/serverSideTranslations";
import {useTranslation} from "next-i18next";
import {Container, Row, Col, Button, Input} from "reactstrap";
import Logo from '@components/Logo.jsx';
import Logo from '@components/Logo';
import {CREATE_ELECTION} from '@services/routes';
import ballotBox from '../public/urne.svg'
import email from '../public/email.svg'

@ -1,601 +0,0 @@
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 Footer from '@components/layouts/Footer'
// import useEmblaCarousel from 'embla-carousel-react'
import {DotButton, PrevButton, NextButton} from "@components/admin/EmblaCarouselButtons";
import VoteButtonWithConfirm from "@components/admin/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, ['resource']),
]);
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;

@ -0,0 +1,601 @@
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 Footer from '@components/layouts/Footer'
// import useEmblaCarousel from 'embla-carousel-react'
import {DotButton, PrevButton, NextButton} from "@components/admin/EmblaCarouselButtons";
import VoteButtonWithConfirm from "@components/admin/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, ['resource']),
]);
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;

@ -0,0 +1,36 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@components/*": ["components/*"],
"@styles/*": ["styles/*"],
"@services/*": ["services/*"]
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
Loading…
Cancel
Save