fix: ballot pages

pull/89/head
Pierre-Louis Guhur 1 year ago
parent 7b89b87269
commit d65056fb2d

@ -0,0 +1,48 @@
import {useTranslation} from 'next-i18next';
import Image from 'next/image';
import {Row, Col} from 'reactstrap';
import ballotBox from '../public/urne.svg';
import email from '../public/email.svg';
import respect from '../public/respect.svg';
const AdvantagesRow = () => {
const {t} = useTranslation('resource');
const resources = [
{
src: ballotBox,
alt: t('home.alt-icon-ballot-box'),
name: t('home.advantage-1-name'),
desc: t('home.advantage-1-desc'),
},
{
src: email,
alt: t('home.alt-icon-envelop'),
name: t('home.advantage-2-name'),
desc: t('home.advantage-2-desc'),
},
{
src: respect,
alt: t('home.alt-icon-respect'),
name: t('home.advantage-3-name'),
desc: t('home.advantage-3-desc'),
},
];
return (
<Row className="sectionTwoRowOne">
{resources.map((item, i) => (
<Col key={i} className="sectionTwoRowOneCol">
<Image
src={item.src}
alt={item.alt}
height="128"
className="d-block mx-auto"
/>
<h4>{item.name}</h4>
<p>{item.desc}</p>
</Col>
))}
</Row>
);
};
export default AdvantagesRow

@ -0,0 +1,165 @@
/**
* A modal to details a candidate
*/
import {ElectionPayload} from '@services/api';
import {
Button,
Col,
Container,
Row,
Modal,
ModalHeader,
ModalBody,
} from 'reactstrap';
interface CandidateModal {
isOpen: boolean;
toggle: Function;
election: ElectionPayload;
}
const CandidateModal = ({isOpen, toggle, election}) =>
(
< Modal
isOpen={isOpen}
toggle={toggle}
keyboard={true}
className="modalVote voteDesktop"
>
<div className="my-auto">
<ModalHeader className="modalVoteHeader">{election.name}</ModalHeader>
<ModalBody className="modalVoteBody">
<form onSubmit={handleSubmit} autoComplete="off">
{election.candidates.map((candidate, candidateId) => {
return (
<Row key={candidateId} className="cardVote">
<Col className="cardVoteLabel">
<h5 className="m-0">{candidate.name}</h5>
<h5 className="m-0">{candidate.description}</h5>
</Col>
<Col className="cardVoteGrades">
{election.grades.map((grade, gradeId) => {
console.assert(gradeId < numGrades);
const gradeValue = grade.value;
const color = getGradeColor(gradeId, numGrades);
return (
<Col
key={gradeId}
className="text-lg-center mx-2 voteCheck"
>
<label
htmlFor={
'candidateGrade' +
candidateId +
'-' +
gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none bold badge"
style={
judgments.find((judgment) => {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? {
backgroundColor: color,
color: '#fff',
}
: {
backgroundColor: 'transparent',
color: '#000',
}
}
>
{grade.name}
</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: color,
color: '#fff',
}
: {
backgroundColor: '#C3BFD8',
color: '#000',
}
}
>
<small
className="nowrap bold badge"
style={{
backgroundColor: 'transparent',
color: '#fff',
}}
>
{grade.name}
</small>
</span>
</label>
</Col>
);
})}
</Col>
</Row>
);
})}
<Row>
<Col className="text-center">
{judgments.length !== election.candidates.length ? (
<VoteButtonWithConfirm
action={handleSubmitWithoutAllRate}
/>
) : (
<Button type="submit" className="mt-5 btn btn-transparent">
<FontAwesomeIcon icon={faCheck} />
{t('Submit my vote')}
</Button>
)}
</Col>
</Row>
</form>
</ModalBody>
</div>
<Footer />
</Modal >

@ -0,0 +1,47 @@
import {useTranslation} from 'next-i18next';
import Image from 'next/image';
import {Row, Col, Button} from 'reactstrap';
import arrowRight from '../public/arrow-white.svg';
import vote from '../public/vote.svg';
const ExperienceRow = () => {
const {t} = useTranslation('resource');
return (
<Row className="sectionTwoRowTwo">
<Row className="sectionTwoHomeImage">
<Image src={vote} alt={t('home.alt-icon-ballot')} />
</Row>
<Row className="sectionTwoRowTwoCol">
<h3 className="col-md-8">{t('home.experience-name')}</h3>
</Row>
<Row className="sectionTwoRowTwoCol">
<Col className="sectionTwoRowTwoColText col-md-4">
<h5 className="">{t('home.experience-1-name')}</h5>
<p>{t('home.experience-1-desc')}</p>
</Col>
<Col className="sectionTwoRowTwoColText col-md-4 offset-md-1">
<h5 className="">{t('home.experience-2-name')}</h5>
<p>{t('home.experience-2-desc')}</p>
<p></p>
</Col>
</Row>
<Row className="sectionTwoRowThreeCol mt-5">
<Col>
<Button color="primary" className="p-4 fs-5">
{t('home.experience-call-to-action')}
<Image
src={arrowRight}
width={22}
height={22}
alt="icon arrow right"
/>
</Button>
</Col>
</Row>
</Row>
);
};
export default ExperienceRow

@ -1,247 +0,0 @@
import { createRef } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import {
getElection,
apiErrors,
ELECTION_NOT_STARTED_ERROR,
} from '@services/api';
import { Col, Container, Row, Button } from 'reactstrap';
import Link from 'next/link';
import {
faCopy,
faVoteYea,
faExclamationTriangle,
faExternalLinkAlt,
faPollH,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import CopyField from '@components/CopyField';
import Error from '@components/Error';
import Facebook from '@components/banner/Facebook';
import Twitter from '@components/banner/Twitter';
import config from '../../../next-i18next.config.js';
import { AnimatePresence, motion } from 'framer-motion';
export async function getServerSideProps({ query: { pid }, locale }) {
let [details, translations] = await Promise.all([
getElection(pid, console.log, console.log),
serverSideTranslations(locale, [], config),
]);
// if (details.includes(ELECTION_NOT_STARTED_ERROR)) {
// details = { title: "", on_invitation_only: true, restrict_results: true };
// } else {
// if (typeof details === "string" || details instanceof String) {
// return { props: { err: details, ...translations } };
// }
// if (!details.title) {
// return { props: { err: "Unknown error", ...translations } };
// }
// }
return {
props: {
invitationOnly: details.on_invitation_only,
restrictResults: details.restrict_results,
title: details.title,
pid: pid,
...translations,
},
};
}
const ConfirmElection = ({
title,
restrictResults,
invitationOnly,
pid,
err,
}) => {
const { t } = useTranslation();
if (err) {
return <Error msg={t(apiErrors(err))} />;
}
const origin =
typeof window !== 'undefined' && window.location.origin
? window.location.origin
: 'http://localhost';
const urlVote = new URL(`/vote/${pid}`, origin);
const urlResult = new URL(`/result/${pid}`, origin);
const electionLink = invitationOnly ? (
<>
<p className="mb-1">
{t(
'Voters received a link to vote by email. Each link can be used only once!'
)}
</p>
</>
) : (
<>
<CopyField
value={urlVote.href}
iconCopy={faCopy}
iconOpen={faExternalLinkAlt}
text={''}
/>
</>
);
const fb = invitationOnly ? null : (
<Facebook
className="btn btn-sm btn-outline-light m-2"
text={t('Share election on Facebook')}
url={urlVote}
title={'app.mieuxvoter.fr'}
/>
);
const tw = invitationOnly ? null : (
<Twitter
className="btn btn-sm btn-outline-light m-2"
text={t('Share election on Twitter')}
url={urlVote}
title={'app.mieuxvoter.fr'}
/>
);
const participate = invitationOnly ? null : (
<>
<Col className="col-lg-3 text-center">
<Link href={`/vote/${pid}`}>
<a target="_blank" rel="noreferrer" className="btn btn-success">
<FontAwesomeIcon icon={faVoteYea} />
{t('resource.participateBtn')}
</a>
</Link>
</Col>
</>
);
return (
<Container className="full-height-container">
<Head>
<title>{t('Successful election creation!')}</title>
<link rel="icon" href="/favicon.ico" />
<meta key="og:title" property="og:title" content={title} />
<meta
property="og:description"
key="og:description"
content={t('common.application')}
/>
</Head>
<motion.div
className="mx-auto row"
initial={{ scale: 1, paddingBottom: '200px' }}
animate={{ scale: 0.5, paddingBottom: '0px' }}
transition={{
type: 'spring',
damping: 100,
delay: 4,
}}
>
<motion.div
className="main-animation"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
type: 'spring',
damping: 20,
delay: 1,
}}
>
<img src="/urne-vide.svg" />
<motion.div
className="letter-animation"
initial={{ scale: 0, y: 50 }}
animate={{ scale: 1, y: 0 }}
transition={{
type: 'spring',
stiffness: 260,
damping: 20,
delay: 2,
}}
>
<img src="/urne-letter.svg" />
</motion.div>
<motion.div
className="star-animation"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: 'spring',
damping: 20,
delay: 3,
}}
>
<img className="urne-fronjt" src="/urne-star.svg" />
</motion.div>
<img className="urne-front" src="/urne-front.svg" />
</motion.div>
</motion.div>
<motion.div
className=""
initial={{ scale: 0, opacity: 0, y: 100 }}
animate={{ scale: 1, opacity: 1, y: -70 }}
transition={{
type: 'spring',
damping: 100,
delay: 4,
}}
>
<Row className="mt-5">
<Col className="text-center mx-auto success-title" lg="3">
<h2>{t('Successful election creation!')}</h2>
</Col>
</Row>
<Row className="mb-4">
<Col className=" mx-auto" lg="4">
<div className="p-4 pb-5">
<CopyField
value={urlVote}
iconCopy={faCopy}
iconOpen={faExternalLinkAlt}
text={t('Copier le lien du vote')}
/>
<CopyField
value={urlResult}
iconCopy={faCopy}
iconOpen={faExternalLinkAlt}
text={t('Copier le lien des résultats')}
/>
</div>
</Col>
</Row>
<Row className="mt-4 mb-4 justify-content-center">
{/* {participate}
<Col className="text-center col-lg-3">
<Link href={`/result/${pid}`}>
<a target="_blank" rel="noreferrer" className="btn btn-secondary">
<FontAwesomeIcon icon={faPollH} />
{t("resource.resultsBtn")}
</a>
</Link>
</Col> */}
<Button className="cursorPointer btn-validation mb-5">
{t('Administrez le vote')}
<img src="/arrow-white.svg" />
</Button>
</Row>
<Row className="mt-5">
<Col className="text-center offset-lg-3" lg="6">
{fb}
{tw}
{t('Partagez lélection')}
</Col>
</Row>
</motion.div>
</Container>
);
};
export default ConfirmElection;

@ -0,0 +1,321 @@
import {useState, useCallback, useEffect, MouseEvent} 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,
} from 'reactstrap';
// import {toast, ToastContainer} from "react-toastify";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCalendarDays, faCheck} from '@fortawesome/free-solid-svg-icons';
import {getElection, castBallot, apiErrors, ElectionPayload} from '@services/api';
import ErrorMessage from '@components/Error';
import useEmblaCarousel from 'embla-carousel-react';
import {DotButton} from '@components/admin/EmblaCarouselButtons';
import VoteButtonWithConfirm from '@components/admin/VoteButtonWithConfirm';
import {getGradeColor} from '@services/grades';
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
export async function getServerSideProps({query: {pid, tid}, locale}) {
const [election, translations] = await Promise.all([
getElection(pid),
serverSideTranslations(locale, ['resource']),
]);
if (typeof election === 'string' || election instanceof String) {
return {props: {err: election, ...translations}};
}
if (!election || !election.candidates || !Array.isArray(election.candidates)) {
return {props: {err: 'Unknown error', ...translations}};
}
const description = JSON.parse(election.description);
if (description.randomOrder) {
shuffle(election.candidates);
}
return {
props: {
...translations,
election,
pid: pid,
token: tid || null,
},
};
}
interface VoteInterface {
election?: ElectionPayload;
err: string;
token?: string;
}
const VoteBallot = ({election, err, token}: VoteInterface) => {
const {t} = useTranslation();
if (err || !election) {
return <ErrorMessage msg={t(apiErrors(err))} />;
}
const numGrades = election.grades.length;
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 handleGradeClick = (event: MouseEvent<HTMLInputElement>) => {
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, election.id.toString(), token, () => {
router.push(`/vote/${election.id}/confirm`);
});
};
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>{election.name}</title>
<meta key="og:title" property="og:title" content={election.name} />
<meta
property="og:description"
key="og:description"
content={t('common.application')}
/>
</Head>
<div className="w-100 bg-light text-center">
<FontAwesomeIcon icon={faCalendarDays} />
VOTE OUVERT
</div>
<div className="d-flex justify-content-center align-items-center">
<h1>{election.name}</h1>
<form onSubmit={handleSubmit} autoComplete="off">
<div className="embla" ref={viewportRef}>
<div className="embla__container">
{election.candidates.map((candidate, candidateId) => {
return (
<div className="embla__slide">
<Row key={candidateId} className="cardVote">
<Col className="cardVoteLabel mb-3">
<h5 className="m-0">{candidate.name}</h5>
<h5 className="m-0">{candidate.id + 1}</h5>
</Col>
<Col className="cardVoteGrades">
{election.grades.map((grade, gradeId) => {
console.assert(gradeId < numGrades);
const gradeValue = grade.value;
const color = getGradeColor(gradeId, numGrades);
return (
<Col
key={gradeId}
className="text-lg-center my-1 voteCheck"
>
<label
htmlFor={
'candidateGrade' +
candidateId +
'-' +
gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none bold badge"
style={
judgments.find((judgment) => {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? {
backgroundColor: color,
color: '#fff',
}
: {
backgroundColor: 'transparent',
color: '#000',
}
}
>
{grade.name}
</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: color,
color: '#fff',
}
: {
backgroundColor: '#C3BFD8',
color: '#000',
}
}
>
<small
className="nowrap bold badge"
style={{
backgroundColor: 'transparent',
color: '#fff',
}}
>
{grade.name}
</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>
</div>
<Row className="btn-background mx-0">
<Col className="text-center">
{judgments.length !== election.candidates.length ? (
<VoteButtonWithConfirm action={handleSubmitWithoutAllRate} />
) : (
<Button type="submit" className="my-3 btn btn-transparent">
<FontAwesomeIcon icon={faCheck} />
{t('Submit my vote')}
</Button>
)}
</Col>
</Row>
</Container >
);
};
export default VoteBallot;

@ -0,0 +1,192 @@
import Head from 'next/head';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight } from '@fortawesome/free-solid-svg-icons';
import { Col, Container, Row, Button } from 'reactstrap';
import Link from 'next/link';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
// import PaypalNoLogo from "@components/banner/PaypalNoLogo";
import Gform from '@components/banner/Gform';
import Error from '@components/Error';
import { getElection, apiErrors } from '@services/api';
import config from '../../../next-i18next.config.js';
import { motion } from 'framer-motion';
export async function getServerSideProps({ query: { pid }, locale }) {
const [details, translations] = await Promise.all([
getElection(pid),
serverSideTranslations(locale, [], config),
]);
if (typeof details === 'string' || details instanceof String) {
return { props: { err: details.slice(1, -1), ...translations } };
}
if (!details.candidates || !Array.isArray(details.candidates)) {
return { props: { err: 'Unknown error', ...translations } };
}
return {
props: {
...translations,
invitationOnly: details.on_invitation_only,
restrictResults: details.restrict_results,
candidates: details.candidates.map((name, i) => ({ id: i, label: name })),
title: details.title,
numGrades: details.num_grades,
pid: pid,
},
};
}
const VoteSuccess = ({ title, invitationOnly, pid, err }) => {
const { t } = useTranslation();
if (err && err !== '') {
return <Error msg={t(apiErrors(err))} />;
}
return (
<Container className="full-height-container">
<Head>
<title>{t('resource.voteSuccess')}</title>
<link rel="icon" href="/favicon.ico" />
<meta key="og:title" property="og:title" content={title} />
<meta
property="og:description"
key="og:description"
content={t('common.application')}
/>
</Head>
<motion.div
className="mx-auto"
initial={{ scale: 1, paddingBottom: '200px' }}
animate={{ scale: 0.5, paddingBottom: '0px' }}
transition={{
type: 'spring',
damping: 100,
delay: 3,
}}
>
<Row>
<motion.div
className="main-animation"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
type: 'spring',
damping: 20,
delay: 1,
}}
>
<motion.div
className="vote-animation"
initial={{ scale: 0, y: 50 }}
animate={{ scale: 1, y: 0 }}
transition={{
type: 'spring',
stiffness: 260,
damping: 20,
delay: 1,
}}
>
<img src="/vote.svg" />
</motion.div>
<motion.div
className="star-animation"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: 'spring',
damping: 20,
delay: 2,
}}
>
<img src="/vote-star.svg" />
</motion.div>
</motion.div>
</Row>
</motion.div>
<motion.div
className=""
initial={{ scale: 0, opacity: 0, y: 100 }}
animate={{ scale: 1, opacity: 1, y: -70 }}
transition={{
type: 'spring',
damping: 100,
delay: 4,
}}
>
<Row className="mt-4 px-3 confirmRowOne">
<Col className="text-center">
<h2 className="confirmH2">{t('resource.voteSuccess')}</h2>
<Button className="voteDesktop mx-auto mt-4 mb-5">
{t('Voir les résultats')}
<img src="/arrow-white.svg" />
</Button>
<Button className="voteMobile mx-auto mt-4 mb-5">
{t('Voir les résultats')}
<img src="/arrow-white.svg" />
</Button>
</Col>
</Row>
<Row className="confirmRowTwo justify-content-center mb-5 px-4">
<Col className="confirmLeft">
<h2 className="confirmH2 mb-4">
{t('Découvrez le jugement majoritaire')}
</h2>
<p>
{t(
'créé par des chercheurs français, le jugement majoritaire est un mode de scrutin qui améliore lexpressivité des électeurs et fournit le meilleur consensus.'
)}
</p>
<Link href="/">
<div>
{t('En savoir plus')}
<FontAwesomeIcon icon={faChevronRight} />
</div>
</Link>
</Col>
<Col className="confirmRight">
<Row className="align-items-center">
<Col xs="8" className="pr-0">
<h2 className="confirmH2">{t('Soutenez Mieux Voter')}</h2>
</Col>
<Col xs="4" className="text-right">
<img src="/logo-red-blue.svg" alt="logo of Mieux Voter" />
</Col>
</Row>
<p className="pt-4">
{t(
'Mieux Voter est une association transpartisane et sans but lucratif. En adhérant à lassociation, vous contribuez à financer son fonctionnement et ses activités. '
)}
</p>
</Col>
</Row>
<Row>
<Col className="text-center col-md-3 mx-auto my-5 thanksVote">
<h4>{t('resource.thanks')}</h4>
<p>
{t('Aidez nous à améliorer lapplication en cliquant ci-dessous')}
</p>
<Gform className="btn btn-secondary mt-3 mx-auto" />
</Col>
</Row>
<div className="mx-auto my-5">
<Row className="justify-content-center">
<Link href="https://www.facebook.com/mieuxvoter.fr/">
<img src="/facebook.svg" />
</Link>
<p className="m-0">
{t('Faites découvrir lapplication a vos amis')}
</p>
</Row>
</div>
</motion.div>
</Container>
);
};
export default VoteSuccess;

@ -7,11 +7,9 @@ import {useTranslation} from 'next-i18next';
import {Container, Row, Col, Button, Input} from 'reactstrap';
import Logo from '@components/Logo';
import Share from '@components/Share';
import AdvantagesRow from '@components/Advantages'
import ExperienceRow from '@components/Experience'
import {CREATE_ELECTION} from '@services/routes';
import ballotBox from '../public/urne.svg';
import email from '../public/email.svg';
import respect from '../public/respect.svg';
import vote from '../public/vote.svg';
import arrowRight from '../public/arrow-white.svg';
export const getStaticProps: GetStaticProps = async ({locale}) => ({
@ -76,83 +74,6 @@ const StartForm = () => {
);
};
const AdvantagesRow = () => {
const {t} = useTranslation('resource');
const resources = [
{
src: ballotBox,
alt: t('home.alt-icon-ballot-box'),
name: t('home.advantage-1-name'),
desc: t('home.advantage-1-desc'),
},
{
src: email,
alt: t('home.alt-icon-envelop'),
name: t('home.advantage-2-name'),
desc: t('home.advantage-2-desc'),
},
{
src: respect,
alt: t('home.alt-icon-respect'),
name: t('home.advantage-3-name'),
desc: t('home.advantage-3-desc'),
},
];
return (
<Row className="sectionTwoRowOne">
{resources.map((item, i) => (
<Col key={i} className="sectionTwoRowOneCol">
<Image
src={item.src}
alt={item.alt}
height="128"
className="d-block mx-auto"
/>
<h4>{item.name}</h4>
<p>{item.desc}</p>
</Col>
))}
</Row>
);
};
const ExperienceRow = () => {
const {t} = useTranslation('resource');
return (
<Row className="sectionTwoRowTwo">
<Row className="sectionTwoHomeImage">
<Image src={vote} alt={t('home.alt-icon-ballot')} />
</Row>
<Row className="sectionTwoRowTwoCol">
<h3 className="col-md-8">{t('home.experience-name')}</h3>
</Row>
<Row className="sectionTwoRowTwoCol">
<Col className="sectionTwoRowTwoColText col-md-4">
<h5 className="">{t('home.experience-1-name')}</h5>
<p>{t('home.experience-1-desc')}</p>
</Col>
<Col className="sectionTwoRowTwoColText col-md-4 offset-md-1">
<h5 className="">{t('home.experience-2-name')}</h5>
<p>{t('home.experience-2-desc')}</p>
<p></p>
</Col>
</Row>
<Row className="sectionTwoRowThreeCol mt-5">
<Col>
<Button color="primary" className="p-4 fs-5">
{t('home.experience-call-to-action')}
<Image
src={arrowRight}
width={22}
height={22}
alt="icon arrow right"
/>
</Button>
</Col>
</Row>
</Row>
);
};
const Home = () => {
const {t} = useTranslation('resource');

@ -1,627 +1,97 @@
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 {getElection, castBallot, apiErrors, ElectionPayload} from '@services/api';
import ErrorMessage from '@components/Error';
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';
import {getGradeColor} from '@services/grades';
import ShareRow from '@components/Share';
import Button from '@components/Button';
import ExperienceRow from '@components/Experience';
import AdvantagesRow from '@components/Advantages';
import Logo from '@components/Logo';
import {BALLOT} from '@services/routes';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
export async function getServerSideProps({query: {pid, tid}, locale}) {
const [details, translations] = await Promise.all([
getElection(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,
name: name,
description: infos,
})),
title: details.title,
numGrades: details.num_grades,
pid: pid,
...(await serverSideTranslations(locale, ['resource'])),
electionId: pid,
token: tid || null,
},
};
}
}
interface VoteInterface {
election: ElectionPayload;
err: string;
electionId: string;
token?: string;
}
const VoteBallot = ({election, err, token}: VoteInterface) => {
const {t} = useTranslation();
if (err) {
return <ErrorMessage msg={t(apiErrors(err))} />;
}
const numGrades = election.grades.length;
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 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]);
});
const GoToBallotConfirm = ({electionId, token}) => {
castBallot(gradesByCandidate, election.id.toString(), token, () => {
router.push(`/vote/${election.id}/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]);
const {t} = useTranslation();
return (
<Container className="homePage">
<Head>
<title>{election.name}</title>
<meta key="og:title" property="og:title" content={election.name} />
<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" />
</Button>
<Button
type="submit"
className="btn btn-block btn-secondary voteMobile"
onClick={toggleMobile}
>
{t('Je participe au vote')}
<img src="/arrow-white.svg" />
</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>
<div className="sectionOneHomeForm">
<Row className="sectionOneHomeRowOne">
<Col className="sectionOneHomeContent">
<Logo height="128" />
<Row>
<h2 className="mb-4 mt-5">{t('common.welcome')}</h2>
</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" />
</Button>
</Row>
<Row>
<h4 className="mb-5">
{t('vote.home-desc')}
</h4>
</Row>
<Row className="sharing">
<p>Partagez lapplication Mieux voter</p>
<Link href="https://www.facebook.com/mieuxvoter.fr/">
<img src="/facebook.svg" />
<Row>
<Link href={{pathname: BALLOT, query: {electionId, token}}}>
<Button
color="secondary"
outline={true}
type="submit"
icon={faArrowRight}
position="right"
>
{t('vote.home-start')}</Button>
</Link>
<Link href="https://twitter.com/mieux_voter">
<img src="/twitter.svg" />
</Row>
<Row className="noAds my-0">
<p>{t('resource.noAds')}</p>
</Row>
<Row>
<Link href="https://mieuxvoter.fr/le-jugement-majoritaire">
<Button className="btn-black mt-2 mb-5">
{t('common.about-mj')}
</Button>
</Link>
</Row>
</section>
<Footer />
</Container>
<Modal
isOpen={visibledDesktop}
toggle={toggleDesktop}
className="modalVote voteDesktop"
>
<div className="my-auto">
<ModalHeader className="modalVoteHeader">{election.name}</ModalHeader>
<ModalBody className="modalVoteBody">
<form onSubmit={handleSubmit} autoComplete="off">
{election.candidates.map((candidate, candidateId) => {
return (
<Row key={candidateId} className="cardVote">
<Col className="cardVoteLabel">
<h5 className="m-0">{candidate.name}</h5>
<h5 className="m-0">{candidate.description}</h5>
</Col>
<Col className="cardVoteGrades">
{election.grades.map((grade, gradeId) => {
console.assert(gradeId < numGrades);
const gradeValue = grade.value;
const color = getGradeColor(gradeId, numGrades);
return (
<Col
key={gradeId}
className="text-lg-center mx-2 voteCheck"
>
<label
htmlFor={
'candidateGrade' +
candidateId +
'-' +
gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none bold badge"
style={
judgments.find((judgment) => {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? {
backgroundColor: color,
color: '#fff',
}
: {
backgroundColor: 'transparent',
color: '#000',
}
}
>
{grade.name}
</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: color,
color: '#fff',
}
: {
backgroundColor: '#C3BFD8',
color: '#000',
}
}
>
<small
className="nowrap bold badge"
style={{
backgroundColor: 'transparent',
color: '#fff',
}}
>
{grade.name}
</small>
</span>
</label>
</Col>
);
})}
</Col>
</Row>
);
})}
</Col>
</Row >
</div >
)
}
<Row>
<Col className="text-center">
{judgments.length !== election.candidates.length ? (
<VoteButtonWithConfirm
action={handleSubmitWithoutAllRate}
/>
) : (
<Button type="submit" className="mt-5 btn btn-transparent">
<FontAwesomeIcon icon={faCheck} />
{t('Submit my vote')}
</Button>
)}
</Col>
</Row>
</form>
</ModalBody>
</div>
<Footer />
</Modal>
const Vote = ({electionId, token}: VoteInterface) => {
<Modal
isOpen={visibledMobile}
toggle={toggleMobile}
className="modalVote voteMobile"
>
<div className="my-auto">
<ModalHeader className="modalVoteHeader">{election.name}</ModalHeader>
<ModalBody className="modalVoteBody">
<form onSubmit={handleSubmit} autoComplete="off">
<div className="embla" ref={viewportRef}>
<div className="embla__container">
{election.candidates.map((candidate, candidateId) => {
return (
<div className="embla__slide">
<Row key={candidateId} className="cardVote">
<Col className="cardVoteLabel mb-3">
<h5 className="m-0">{candidate.name}</h5>
<h5 className="m-0">{candidate.id + 1}</h5>
</Col>
<Col className="cardVoteGrades">
{election.grades.map((grade, gradeId) => {
console.assert(gradeId < numGrades);
const gradeValue = grade.value;
const color = getGradeColor(gradeId, numGrades);
return (
<Col
key={gradeId}
className="text-lg-center my-1 voteCheck"
>
<label
htmlFor={
'candidateGrade' +
candidateId +
'-' +
gradeValue
}
className="check"
>
<small
className="nowrap d-lg-none bold badge"
style={
judgments.find((judgment) => {
return (
JSON.stringify(judgment) ===
JSON.stringify({
id: candidate.id,
value: gradeValue,
})
);
})
? {
backgroundColor: color,
color: '#fff',
}
: {
backgroundColor: 'transparent',
color: '#000',
}
}
>
{grade.name}
</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: color,
color: '#fff',
}
: {
backgroundColor: '#C3BFD8',
color: '#000',
}
}
>
<small
className="nowrap bold badge"
style={{
backgroundColor: 'transparent',
color: '#fff',
}}
>
{grade.name}
</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 !== election.candidates.length ? (
<VoteButtonWithConfirm action={handleSubmitWithoutAllRate} />
) : (
<Button type="submit" className="my-3 btn btn-transparent">
<FontAwesomeIcon icon={faCheck} />
{t('Submit my vote')}
</Button>
)}
</Col>
</Row>
<Footer />
</Modal>
</Container>
return (
<>
<section>
<GoToBallotConfirm electionId={electionId} token={token} />
</section>
<section className="sectionTwoHome">
<AdvantagesRow />
<ExperienceRow />
<ShareRow />
</section>
</>
);
};
export default VoteBallot;
export default Vote;

@ -26,6 +26,7 @@
"menu.faq": "FAQ",
"menu.news": "News",
"menu.contact-us": "Contact us",
"common.about-mj": "Read more about Better Vote",
"common.error": "Oh no... An error has occured...",
"common.better-vote": "Better Vote",
"common.share": "Share the application Better Vote",
@ -44,8 +45,10 @@
"common.vote": "Vote",
"common.the-vote": "The vote",
"common.the-params": "The parameters",
"error.help": "Ask for our help",
"common.welcome": "Welcome!",
"error.at-least-2-candidates": "At least two candidates are required.",
"error.catch22": "Erreur inconnue...",
"error.help": "Ask for our help",
"error.no-title": "Please add a title to your election.",
"grades.very-good": "Very good",
"grades.good": "Good",
@ -102,5 +105,7 @@
"admin.success-copy-vote": "Copy the voting link",
"admin.success-copy-result": "Copy the result link",
"admin.success-copy-admin": "Copy the admin link",
"admin.go-to-admin": "Manage the vote"
"admin.go-to-admin": "Manage the vote",
"vote.home-desc": "Participate in the vote and discover majority judgment",
"vote.home-start": "I participate"
}

@ -26,6 +26,7 @@
"menu.faq": "FAQ",
"menu.news": "Actualités",
"menu.contact-us": "Nous contacter",
"common.about-mj": "En savoir plus sur Mieux voter",
"common.error": "Oh non ! Une erreur s'est produite...",
"common.better-vote": "Mieux Voter",
"common.share": "Partagez l'application Mieux voter",
@ -44,9 +45,11 @@
"common.the-vote": "Le vote",
"common.the-params": "Les paramètres",
"common.vote": "Voter",
"common.welcome": "Bienvenue !",
"error.help": "Besoin d'aide ?",
"error.at-least-2-candidates": "Ajoutez au moins deux candidats.",
"error.no-title": "Ajoutez un titre à l'élection.",
"error.catch22": "Erreur inconnue...",
"grades.very-good": "Très bien",
"grades.good": "Bien",
"grades.passable": "Passable",
@ -102,5 +105,7 @@
"admin.success-copy-vote": "Copier le lien du vote",
"admin.success-copy-result": "Copier le lien des résultats",
"admin.success-copy-admin": "Copier le lien d'administration",
"admin.go-to-admin": "Administrez le vote"
"admin.go-to-admin": "Administrez le vote",
"vote.home-desc": "Participez au vote et découvrez le jugement majoritaire.",
"vote.home-start": "Je participe"
}

@ -61,6 +61,7 @@ export const createElection = async (
if (successCallback) {
const payload = await req.json();
successCallback(payload);
return payload;
}
} else if (failureCallback) {
try {
@ -104,29 +105,31 @@ export const getResults = (
};
export const getElection = (
export const getElection = async (
pid: string,
successCallback = null,
failureCallback = null
) => {
): Promise<ElectionPayload> => {
/**
* Fetch data from external API
*/
const detailsEndpoint = new URL(
api.routesServer.getElection.replace(new RegExp(':slug', 'g'), pid),
api.urlServer
);
return fetch(detailsEndpoint.href)
.then((response) => {
if (!response.ok) {
return Promise.reject(response.text());
}
return response.json();
})
.then(successCallback || ((res) => res))
.catch(failureCallback || ((err) => err))
.then((res) => res);
try {
const res = await fetch(detailsEndpoint.href);
if (!res.ok) {
return failureCallback(res.text())
}
const payload: ElectionPayload = await res.json();
if (successCallback) successCallback(payload);
return payload;
} catch (error) {
return failureCallback && failureCallback(error);
}
};

@ -5,17 +5,20 @@ import {getWindowUrl} from './utils';
export const CREATE_ELECTION = '/admin/new/';
export const BALLOT = '/ballot/';
export const VOTE = '/vote/';
export const RESULTS = '/result/';
export const getUrlVote = (electionId: string | number, token?: string): URL => {
const origin = getWindowUrl();
if (token)
return new URL(`/vote/${electionId}/${token}`, origin);
return new URL(`/vote/${electionId}`, origin);
return new URL(`/${VOTE}/${electionId}/${token}`, origin);
return new URL(`/${VOTE}/${electionId}`, origin);
}
export const getUrlResults = (electionId: string | number): URL => {
const origin = getWindowUrl();
return new URL(`/result/${electionId}`, origin);
return new URL(`/${RESULTS}/${electionId}`, origin);
}
export const getUrlAdmin = (electionId: string | number, adminToken: string): URL => {

Loading…
Cancel
Save