fix: emails

pull/89/head
Pierre-Louis Guhur 1 year ago
parent 7055245ecd
commit c8040922f9

@ -93,7 +93,7 @@ const CandidatesField = ({ onSubmit }) => {
<> <>
<Container onClick={toggleModalTitle} className="candidate mt-5"> <Container onClick={toggleModalTitle} className="candidate mt-5">
<h4 className="mb-4">{t('admin.confirm-question')}</h4> <h4 className="mb-4">{t('admin.confirm-question')}</h4>
<div className="d-flex justify-content-between border border-dashed border-2 border-light border-opacity-25 px-4 py-3 mx-2"> <div className="d-flex justify-content-between border border-dashed border-2 border-light border-opacity-25 px-4 py-3 mx-2 mx-md-0">
<h5 className="m-0 text-white">{election.name}</h5> <h5 className="m-0 text-white">{election.name}</h5>
<FontAwesomeIcon icon={faPen} /> <FontAwesomeIcon icon={faPen} />
</div> </div>

@ -64,7 +64,6 @@ const Grades = () => {
}); });
} }
}, []); }, []);
console.log('GRADES', grades);
const handleDragEnd = (event) => { const handleDragEnd = (event) => {
/** /**

@ -81,7 +81,7 @@
<tr> <tr>
<th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;"> <th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;">
<a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer"> <a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer">
<img alt="Logo" src="https://mieuxvoter.fr/img/logo.svg" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0"> <img alt="Logo" src="https://app.mieuxvoter.fr/logos/logo.png" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0">
</a> </a>
</th> </th>
</tr> </tr>
@ -135,7 +135,7 @@
<!-- BLOCK THANKS --> <!-- BLOCK THANKS -->
<tr> <tr>
<th scope="col" style="background-color: #ffffff; padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" > <th scope="col" style="background-color: #ffffff; padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">{{#i18n 'email.bye'}}{{/i18n}},<br>{{#i18n 'common.better-vote'}}{{/i18n}}</p> <p style="margin: 0; text-align: left;">{{#i18n 'email.bye'}}{{/i18n}},<br>{{#i18n 'resource:common.better-vote'}}{{/i18n}}</p>
</th> </th>
</tr> </tr>
</table> </table>
@ -155,7 +155,7 @@
</p> </p>
<p style="margin: 0;"> <strong> <p style="margin: 0;"> <strong>
<a href="https://www.paypal.com/donate/?hosted_button_id=QD6U4D323WV4S" target="_blank" style="color: #111111;" rel="noopener noreferrer"> <a href="https://www.paypal.com/donate/?hosted_button_id=QD6U4D323WV4S" target="_blank" style="color: #111111;" rel="noopener noreferrer">
{{#i18n 'common.support-us'}}{{/i18n}} {{#i18n 'resource:common.support-us'}}{{/i18n}}
</a></strong> </a></strong>
</p> </p>
</th> </th>
@ -179,7 +179,7 @@
<!-- ADDRESS --> <!-- ADDRESS -->
<tr> <tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" > <th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">{{#i18n common.better-vote }}{{/i18n}} - <a "mailto:{{ from_email_address }}">{{ from_email_address }}</a></p> <p style="margin: 0;">{{#i18n resource:common.better-vote }}{{/i18n}} - <a "mailto:{{ from_email_address }}">{{ from_email_address }}</a></p>
</th> </th>
</tr> </tr>
</table> </table>

@ -12,4 +12,4 @@
{{#i18n 'email.bye'}}{{/i18n}} {{#i18n 'email.bye'}}{{/i18n}}
{{#i18n 'common.better-vote'}}{{/i18n}} {{#i18n 'resource:common.better-vote'}}{{/i18n}}

@ -81,7 +81,7 @@
<tr> <tr>
<th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;"> <th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;">
<a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer"> <a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer">
<img alt="Logo" src="https://mieuxvoter.fr/img/logo.svg" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0"> <img alt="Logo" src="https://app.mieuxvoter.fr/logos/logo.png" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0">
</a> </a>
</th> </th>
</tr> </tr>

@ -16,4 +16,4 @@
{{#i18n 'email.bye'}}{{/i18n}} {{#i18n 'email.bye'}}{{/i18n}}
{{#i18n 'common.better-vote'}}{{/i18n}} {{#i18n 'resource:common.better-vote'}}{{/i18n}}

@ -1,14 +1,25 @@
import {useEffect, useState} from 'react'; import { useEffect, useState } from 'react';
import {useRouter} from 'next/router'; import { useRouter } from 'next/router';
import Link from 'next/link' import Link from 'next/link';
import {useTranslation} from 'next-i18next'; import { useTranslation } from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import {Container, Row, Col} from 'reactstrap'; import { Container, Row, Col } from 'reactstrap';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons'; import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
import {getElection, updateElection} from '@services/api'; import { getElection, updateElection } from '@services/api';
import {ElectionContextInterface, ElectionProvider, ElectionTypes, useElection, isClosed, canViewResults, checkName, hasEnoughGrades, hasEnoughCandidates, canBeFinished} from '@services/ElectionContext'; import {
import {CandidateItem, GradeItem} from '@services/type'; ElectionContextInterface,
import {gradeColors} from '@services/grades'; ElectionProvider,
ElectionTypes,
useElection,
isClosed,
canViewResults,
checkName,
hasEnoughGrades,
hasEnoughCandidates,
canBeFinished,
} from '@services/ElectionContext';
import { CandidateItem, GradeItem } from '@services/type';
import { gradeColors } from '@services/grades';
import TitleField from '@components/admin/Title'; import TitleField from '@components/admin/Title';
import Button from '@components/Button'; import Button from '@components/Button';
import AccessResults from '@components/admin/AccessResults'; import AccessResults from '@components/admin/AccessResults';
@ -18,33 +29,35 @@ import Grades from '@components/admin/Grades';
import Order from '@components/admin/Order'; import Order from '@components/admin/Order';
import Private from '@components/admin/Private'; import Private from '@components/admin/Private';
import Blur from '@components/Blur'; import Blur from '@components/Blur';
import {getUrlResults, getUrlVote, RESULTS, VOTE} from '@services/routes'; import { getUrlResults, getUrlVote, RESULTS, VOTE } from '@services/routes';
import {sendInviteMails} from '@services/mail'; import { sendInviteMails } from '@services/mail';
import {AppTypes, useAppContext} from '@services/context'; import { AppTypes, useAppContext } from '@services/context';
export async function getServerSideProps({ query, locale }) {
export async function getServerSideProps({query, locale}) { const { pid, tid: token } = query;
const {pid, tid: token} = query; const electionRef = pid.replaceAll('-', '');
const electionRef = pid.replaceAll("-", "");
const [payload, translations] = await Promise.all([ const [payload, translations] = await Promise.all([
getElection(electionRef), getElection(electionRef),
serverSideTranslations(locale, ["resource"]), serverSideTranslations(locale, ['resource']),
]); ]);
if ("msg" in payload) { if ('msg' in payload) {
return {props: {err: payload.msg, ...translations}}; return { props: { err: payload.msg, ...translations } };
} }
const grades = payload.grades.map((g, i) => ({...g, active: true})); const grades = payload.grades.map((g, i) => ({ ...g, active: true }));
const candidates: Array<CandidateItem> = payload.candidates.map(c => ({...c, active: true})) const candidates: Array<CandidateItem> = payload.candidates.map((c) => ({
const description = JSON.parse(payload.description) ...c,
const randomOrder = description["randomOrder"] active: true,
}));
const description = JSON.parse(payload.description);
const randomOrder = description['randomOrder'];
const context: ElectionContextInterface = { const context: ElectionContextInterface = {
name: payload.name, name: payload.name,
description: description["description"], description: description['description'],
ref: payload.ref, ref: payload.ref,
dateStart: payload.date_start, dateStart: payload.date_start,
dateEnd: payload.date_end, dateEnd: payload.date_end,
@ -54,45 +67,51 @@ export async function getServerSideProps({query, locale}) {
randomOrder, randomOrder,
emails: [], emails: [],
grades, grades,
candidates candidates,
} };
return { return {
props: { props: {
context, context,
token: token || "", token: token || '',
...translations, ...translations,
}, },
}; };
} }
const Spinner = () => { const Spinner = () => {
return ( return (
<div className="spinner-border text-light" role="status"> <div className="spinner-border text-light" role="status">
<span className="visually-hidden">Loading...</span> <span className="visually-hidden">Loading...</span>
</div> </div>
) );
} };
const HeaderRubbon = () => { const HeaderRubbon = () => {
const {t} = useTranslation(); const { t } = useTranslation();
const [election, dispatch] = useElection(); const [election, dispatch] = useElection();
const [_, dispatchApp] = useAppContext(); const [_, dispatchApp] = useAppContext();
const router = useRouter(); const router = useRouter();
const [waiting, setWaiting] = useState(false); const [waiting, setWaiting] = useState(false);
const handleClosing = async () => { const handleClosing = async () => {
setWaiting(true) setWaiting(true);
dispatch({ dispatch({
type: ElectionTypes.SET, type: ElectionTypes.SET,
field: "forceClose", field: 'forceClose',
value: true value: true,
}) });
const candidates = election.candidates.filter(c => c.active).map((c: CandidateItem) => ({name: c.name, description: c.description, image: c.image})) const candidates = election.candidates
const grades = election.grades.filter(c => c.active).map((g: GradeItem, i: number) => ({name: g.name, value: i})) .filter((c) => c.active)
setWaiting(true) .map((c: CandidateItem) => ({
name: c.name,
description: c.description,
image: c.image,
}));
const grades = election.grades
.filter((c) => c.active)
.map((g: GradeItem, i: number) => ({ name: g.name, value: i }));
setWaiting(true);
const response = await updateElection( const response = await updateElection(
election.ref, election.ref,
election.name, election.name,
@ -103,130 +122,141 @@ const HeaderRubbon = () => {
election.hideResults, election.hideResults,
true, true,
election.restricted, election.restricted,
election.randomOrder, election.randomOrder
); );
if (response.status === 200 && "ref" in response) { if (response.status === 200 && 'ref' in response) {
if (election.restricted && election.emails.length > 0) { if (election.restricted && election.emails.length > 0) {
if (election.emails.length !== response.invites.length) { if (election.emails.length !== response.invites.length) {
throw Error("Unexpected number of invites!") throw Error('Unexpected number of invites!');
} }
const urlVotes = response.invites.map((token: string) => getUrlVote(response.ref, token)); const urlVotes = response.invites.map((token: string) =>
getUrlVote(response.ref, token)
);
const urlResult = getUrlResults(response.ref); const urlResult = getUrlResults(response.ref);
await sendInviteMails( await sendInviteMails(
election.emails, election.emails,
election.name, election.name,
urlVotes, urlVotes,
urlResult, urlResult,
router, router
); );
} }
setWaiting(false) setWaiting(false);
dispatchApp({ dispatchApp({
type: AppTypes.TOAST_ADD, type: AppTypes.TOAST_ADD,
status: "success", status: 'success',
message: t("success.election-closed") message: t('success.election-closed'),
}) });
} }
} };
return <div className="w-100 p-4 bg-primary text-white d-flex justify-content-between align-items-center">
<h5>{t('admin.admin-title')}</h5>
<div className="d-flex">
{election.restricted ? null :
<Link href={`${VOTE}/${election.ref}`}>
<Button icon={faArrowRight}
color="primary"
className="me-3"
style={{border: "2px solid rgba(255, 255, 255, 0.4)"}}
position="right">
{t('admin.go-to-vote')}
</Button>
</Link>
}
{canViewResults(election) ? return (
<Link href={`${RESULTS}/${election.ref}`}> <div className="w-100 p-4 bg-primary text-white d-flex justify-content-between align-items-center">
<Button icon={faArrowRight} <h5>{t('admin.admin-title')}</h5>
color="primary"
className="me-3" <div className="d-flex">
style={{border: "2px solid rgba(255, 255, 255, 0.4)"}} {election.restricted ? null : (
position="right"> <Link href={`${VOTE}/${election.ref}`}>
{t('admin.go-to-result')} <Button
icon={faArrowRight}
color="primary"
className="me-3"
style={{ border: '2px solid rgba(255, 255, 255, 0.4)' }}
position="right"
>
{t('admin.go-to-vote')}
</Button>
</Link>
)}
{canViewResults(election) ? (
<Link href={`${RESULTS}/${election.ref}`}>
<Button
icon={faArrowRight}
color="primary"
className="me-3"
style={{ border: '2px solid rgba(255, 255, 255, 0.4)' }}
position="right"
>
{t('admin.go-to-result')}
</Button>
</Link>
) : null}
{isClosed(election) ? null : (
<Button
className="me-3 btn_closing"
style={{ border: '2px solid rgba(255, 255, 255, 0.4)' }}
onClick={handleClosing}
position="right"
>
{waiting ? <Spinner /> : t('admin.close-election')}
</Button> </Button>
</Link> )}
: null} </div>
{isClosed(election) ? null :
<Button
className="me-3 btn_closing"
style={{border: "2px solid rgba(255, 255, 255, 0.4)"}}
onClick={handleClosing}
position="right">
{waiting ?
<Spinner />
:
t('admin.close-election')
}
</Button>
}
</div> </div>
);
};
</div > const CreateElection = ({ context, token }) => {
} const { t } = useTranslation();
const CreateElection = ({context, token}) => {
const {t} = useTranslation();
const [election, dispatch] = useElection(); const [election, dispatch] = useElection();
const [_, dispatchApp] = useAppContext(); const [_, dispatchApp] = useAppContext();
const router = useRouter(); const router = useRouter();
const [waiting, setWaiting] = useState(false); const [waiting, setWaiting] = useState(false);
useEffect(() => { useEffect(() => {
dispatch({type: ElectionTypes.RESET, value: context}) dispatch({ type: ElectionTypes.RESET, value: context });
}, []) }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!checkName(election)) { if (!checkName(election)) {
dispatchApp({ dispatchApp({
type: AppTypes.TOAST_ADD, type: AppTypes.TOAST_ADD,
status: "error", status: 'error',
message: t("error.uncorrect-name") message: t('error.uncorrect-name'),
}) });
return return;
} }
if (!hasEnoughGrades(election)) { if (!hasEnoughGrades(election)) {
dispatchApp({ dispatchApp({
type: AppTypes.TOAST_ADD, type: AppTypes.TOAST_ADD,
status: "error", status: 'error',
message: t("error.not-enough-grades") message: t('error.not-enough-grades'),
}) });
return return;
} }
if (!hasEnoughCandidates(election)) { if (!hasEnoughCandidates(election)) {
dispatchApp({ dispatchApp({
type: AppTypes.TOAST_ADD, type: AppTypes.TOAST_ADD,
status: "error", status: 'error',
message: t("error.not-enough-candidates") message: t('error.not-enough-candidates'),
}) });
return return;
} }
if (!canBeFinished(election)) { if (!canBeFinished(election)) {
dispatchApp({ dispatchApp({
type: AppTypes.TOAST_ADD, type: AppTypes.TOAST_ADD,
status: "error", status: 'error',
message: t("error.cant-be-finished") message: t('error.cant-be-finished'),
}) });
return return;
} }
const candidates = election.candidates.filter(c => c.active).map((c: CandidateItem) => ({name: c.name, description: c.description, image: c.image})) const candidates = election.candidates
const grades = election.grades.filter(c => c.active).map((g: GradeItem, i: number) => ({name: g.name, value: i})) .filter((c) => c.active)
setWaiting(true) .map((c: CandidateItem) => ({
name: c.name,
description: c.description,
image: c.image,
}));
const grades = election.grades
.filter((c) => c.active)
.map((g: GradeItem, i: number) => ({ name: g.name, value: i }));
setWaiting(true);
const response = await updateElection( const response = await updateElection(
election.ref, election.ref,
@ -238,43 +268,51 @@ const CreateElection = ({context, token}) => {
election.hideResults, election.hideResults,
true, true,
election.restricted, election.restricted,
election.randomOrder, election.randomOrder
); );
if (response.status === 200 && "ref" in response) { if (response.status === 200 && 'ref' in response) {
if (election.restricted && election.emails.length > 0) { if (election.restricted && election.emails.length > 0) {
if (election.emails.length !== response.invites.length) { if (election.emails.length !== response.invites.length) {
throw Error("Unexpected number of invites!") throw Error('Unexpected number of invites!');
} }
const urlVotes = response.invites.map((token: string) => getUrlVote(response.ref, token)); const urlVotes = response.invites.map((token: string) =>
getUrlVote(response.ref, token)
);
const urlResult = getUrlResults(response.ref); const urlResult = getUrlResults(response.ref);
await sendInviteMails( await sendInviteMails(
election.emails, election.emails,
election.name, election.name,
urlVotes, urlVotes,
urlResult, urlResult,
router, router
); );
} }
setWaiting(false) setWaiting(false);
dispatchApp({ dispatchApp({
type: AppTypes.TOAST_ADD, type: AppTypes.TOAST_ADD,
status: "success", status: 'success',
message: t("success.election-updated") message: t('success.election-updated'),
}) });
} }
} };
const numCandidates = election.candidates.filter(c => c.active && c.name != "").length; const numCandidates = election.candidates.filter(
const numGrades = election.grades.filter(g => g.active && g.name != "").length; (c) => c.active && c.name != ''
const disabled = ( ).length;
!election.name || election.name == "" || const numGrades = election.grades.filter(
(g) => g.active && g.name != ''
).length;
const disabled =
!election.name ||
election.name == '' ||
numCandidates < 2 || numCandidates < 2 ||
numGrades < 2 || numGrades > gradeColors.length numGrades < 2 ||
) numGrades > gradeColors.length;
return ( return (
<><HeaderRubbon /> <>
<HeaderRubbon />
<Container <Container
fluid="xl" fluid="xl"
className="my-5 flex-column d-flex justify-content-center" className="my-5 flex-column d-flex justify-content-center"
@ -319,12 +357,11 @@ const CreateElection = ({context, token}) => {
); );
}; };
const CreateElectionProviding = ({ children, context, token }) => (
const CreateElectionProviding = ({children, context, token}) => (
<ElectionProvider> <ElectionProvider>
<Blur /> <Blur />
<CreateElection context={context} token={token} /> <CreateElection context={context} token={token} />
</ElectionProvider> </ElectionProvider>
) );
export default CreateElectionProviding; export default CreateElectionProviding;

@ -1,50 +1,63 @@
import {useEffect, useState} from 'react'; import { useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import {useTranslation} from 'next-i18next'; import { useTranslation } from 'next-i18next';
import {Container} from 'reactstrap'; import { Container } from 'reactstrap';
import {faCheck} from '@fortawesome/free-solid-svg-icons'; import { faCheck } from '@fortawesome/free-solid-svg-icons';
import BallotDesktop from '@components/ballot/BallotDesktop' import BallotDesktop from '@components/ballot/BallotDesktop';
import Button from '@components/Button'; import Button from '@components/Button';
import BallotMobile from '@components/ballot/BallotMobile' import BallotMobile from '@components/ballot/BallotMobile';
import Blur from '@components/Blur' import Blur from '@components/Blur';
import {getElection, castBallot, ElectionPayload, BallotPayload, ErrorPayload} from '@services/api'; import {
import {useBallot, BallotTypes, BallotProvider} from '@services/BallotContext'; getElection,
import {ENDED_VOTE} from '@services/routes'; castBallot,
import {isEnded} from '@services/utils'; ElectionPayload,
BallotPayload,
ErrorPayload,
} from '@services/api';
import {
useBallot,
BallotTypes,
BallotProvider,
} from '@services/BallotContext';
import { ENDED_VOTE } from '@services/routes';
import { isEnded } from '@services/utils';
import WaitingBallot from '@components/WaitingBallot'; import WaitingBallot from '@components/WaitingBallot';
import PatternedBackground from '@components/PatternedBackground'; import PatternedBackground from '@components/PatternedBackground';
const shuffle = (array) => array.sort(() => Math.random() - 0.5); const shuffle = (array) => array.sort(() => Math.random() - 0.5);
export async function getServerSideProps({query: {pid, tid}, locale}) { export async function getServerSideProps({ query: { pid, tid }, locale }) {
if (!pid) { if (!pid) {
return {notFound: true} return { notFound: true };
} }
const electionRef = pid.replaceAll("-", ""); const electionRef = pid.replaceAll('-', '');
const [election, translations] = await Promise.all([ const [election, translations] = await Promise.all([
getElection(electionRef), getElection(electionRef),
serverSideTranslations(locale, ['resource']), serverSideTranslations(locale, ['resource']),
]); ]);
if ("msg" in election) { if ('msg' in election) {
return {notFound: true} return { notFound: true };
} }
if (isEnded(election.date_end)) { if (isEnded(election.date_end)) {
return { return {
redirect: { redirect: {
destination: `${ENDED_VOTE}/${pid}/${tid || ""}`, destination: `${ENDED_VOTE}/${pid}/${tid || ''}`,
permanent: false permanent: false,
} },
} };
} }
if (!election || !election.candidates || !Array.isArray(election.candidates)) { if (
!election ||
!election.candidates ||
!Array.isArray(election.candidates)
) {
console.log(election); console.log(election);
return {notFound: true} return { notFound: true };
} }
const description = JSON.parse(election.description); const description = JSON.parse(election.description);
@ -62,36 +75,35 @@ export async function getServerSideProps({query: {pid, tid}, locale}) {
}; };
} }
const ButtonSubmit = () => { const ButtonSubmit = () => {
const {t} = useTranslation(); const { t } = useTranslation();
const [ballot, dispatch] = useBallot(); const [ballot, dispatch] = useBallot();
const disabled = ballot.votes.length !== ballot.election.candidates.length; const disabled = ballot.votes.length !== ballot.election.candidates.length;
return (<Container className="my-5 d-md-flex d-grid justify-content-md-center"> return (
<Button <Container className="my-5 d-md-flex d-grid justify-content-md-center">
outline={true} <Button
color="secondary" outline={true}
className="bg-blue" color="secondary"
role="submit" className="bg-blue"
disabled={disabled} role="submit"
icon={faCheck} disabled={disabled}
position="left" icon={faCheck}
> position="left"
{t('vote.submit')} >
</Button> {t('vote.submit')}
</Container> </Button>
) </Container>
} );
};
interface VoteInterface { interface VoteInterface {
election: ElectionPayload; election: ElectionPayload;
err: string; err: string;
token?: string; token?: string;
} }
const VoteBallot = ({election, token}: VoteInterface) => { const VoteBallot = ({ election, token }: VoteInterface) => {
const {t} = useTranslation(); const { t } = useTranslation();
const [ballot, dispatch] = useBallot(); const [ballot, dispatch] = useBallot();
@ -111,9 +123,11 @@ const VoteBallot = ({election, token}: VoteInterface) => {
} }
if (voting) { if (voting) {
return <PatternedBackground> return (
<WaitingBallot ballot={payload} error={error} /> <PatternedBackground>
</PatternedBackground> <WaitingBallot ballot={payload} error={error} />
</PatternedBackground>
);
} }
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
@ -121,27 +135,27 @@ const VoteBallot = ({election, token}: VoteInterface) => {
setVoting(true); setVoting(true);
try { try {
const res = await castBallot( const res = await castBallot(ballot.votes, ballot.election, token);
ballot.votes,
ballot.election,
token)
if (res.status !== 200) { if (res.status !== 200) {
console.error(res); console.error(res);
const msg = await res.json(); const msg = await res.json();
setError(msg) setError(msg);
} } else {
else {
const msg = await res.json(); const msg = await res.json();
setPayload(msg) setPayload(msg);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setError(err.message) setError(err.message);
} }
}; };
return ( return (
<form className="w-100 h-100" onSubmit={handleSubmit} autoComplete="off"> <form
className="w-100 flex-fill d-flex align-items-center"
onSubmit={handleSubmit}
autoComplete="off"
>
<Head> <Head>
<title>{election.name}</title> <title>{election.name}</title>
@ -159,16 +173,16 @@ const VoteBallot = ({election, token}: VoteInterface) => {
<BallotMobile /> <BallotMobile />
<ButtonSubmit /> <ButtonSubmit />
</div> </div>
</form > </form>
); );
}; };
const Ballot = (props) => { const Ballot = (props) => {
return (
return (<BallotProvider> <BallotProvider>
<VoteBallot {...props} /> <VoteBallot {...props} />
</BallotProvider>
</BallotProvider>) );
} };
export default Ballot; export default Ballot;

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

@ -1,8 +1,7 @@
import {Candidate, Grade, Vote} from './type'; import { Candidate, Grade, Vote } from './type';
export const api = { export const api = {
urlServer: urlServer: process.env.NEXT_PUBLIC_SERVER_URL || 'https://api.mieuxvoter.fr/',
process.env.NEXT_PUBLIC_SERVER_URL || 'https://api.mieuxvoter.fr/',
feedbackForm: feedbackForm:
process.env.NEXT_PUBLIC_FEEDBACK_FORM || process.env.NEXT_PUBLIC_FEEDBACK_FORM ||
'https://docs.google.com/forms/d/e/1FAIpQLScuTsYeBXOSJAGSE_AFraFV7T2arEYua7UCM4NRBSCQQfRB6A/viewform', 'https://docs.google.com/forms/d/e/1FAIpQLScuTsYeBXOSJAGSE_AFraFV7T2arEYua7UCM4NRBSCQQfRB6A/viewform',
@ -14,7 +13,6 @@ export const api = {
}, },
}; };
export interface GradePayload { export interface GradePayload {
name: string; name: string;
description: string; description: string;
@ -22,7 +20,6 @@ export interface GradePayload {
value: number; value: number;
} }
export interface CandidatePayload { export interface CandidatePayload {
name: string; name: string;
description: string; description: string;
@ -30,7 +27,6 @@ export interface CandidatePayload {
image: string; image: string;
} }
export interface ErrorMessage { export interface ErrorMessage {
loc: Array<string>; loc: Array<string>;
msg: string; msg: string;
@ -72,14 +68,12 @@ export interface ElectionUpdatedPayload extends ElectionPayload {
status?: number; status?: number;
} }
export interface ResultsPayload extends ElectionPayload { export interface ResultsPayload extends ElectionPayload {
status: number; status: number;
ranking: {[key: string]: number}; ranking: { [key: string]: number };
merit_profile: {[key: number]: Array<number>}; merit_profile: { [key: number]: Array<number> };
} }
export interface VotePayload { export interface VotePayload {
id: string; id: string;
candidate: CandidatePayload; candidate: CandidatePayload;
@ -111,7 +105,7 @@ export const createElection = async (
const endpoint = new URL(api.routesServer.setElection, api.urlServer); const endpoint = new URL(api.routesServer.setElection, api.urlServer);
if (!restricted && numVoters > 0) { if (!restricted && numVoters > 0) {
throw Error("Set the election as not restricted!"); throw Error('Set the election as not restricted!');
} }
try { try {
@ -124,7 +118,7 @@ export const createElection = async (
name, name,
description: JSON.stringify({ description: JSON.stringify({
description: description, description: description,
randomOrder: randomOrder randomOrder: randomOrder,
}), }),
candidates, candidates,
grades, grades,
@ -133,7 +127,7 @@ export const createElection = async (
force_close: forceClose, force_close: forceClose,
restricted, restricted,
}), }),
}) });
if (req.ok && req.status === 200) { if (req.ok && req.status === 200) {
if (successCallback) { if (successCallback) {
const payload = await req.json(); const payload = await req.json();
@ -143,19 +137,16 @@ export const createElection = async (
} else if (failureCallback) { } else if (failureCallback) {
try { try {
const payload = await req.json(); const payload = await req.json();
failureCallback(payload) failureCallback(payload);
} catch (e) { } catch (e) {
failureCallback(req.statusText) failureCallback(req.statusText);
} }
} }
} } catch (e) {
catch (e) {
return failureCallback && failureCallback(e); return failureCallback && failureCallback(e);
} }
}; };
export const updateElection = async ( export const updateElection = async (
ref: string, ref: string,
name: string, name: string,
@ -166,17 +157,13 @@ export const updateElection = async (
hideResults: boolean, hideResults: boolean,
forceClose: boolean, forceClose: boolean,
restricted: boolean, restricted: boolean,
randomOrder: boolean, randomOrder: boolean
): Promise<ElectionUpdatedPayload | HTTPPayload> => { ): Promise<ElectionUpdatedPayload | HTTPPayload> => {
/** /**
* Create an election from its title, its candidates and a bunch of options * Create an election from its title, its candidates and a bunch of options
*/ */
const endpoint = new URL(api.routesServer.setElection, api.urlServer); const endpoint = new URL(api.routesServer.setElection, api.urlServer);
if (!restricted && numVoters > 0) {
throw Error("Set the election as not restricted!");
}
try { try {
const req = await fetch(endpoint.href, { const req = await fetch(endpoint.href, {
method: 'POST', method: 'POST',
@ -188,7 +175,7 @@ export const updateElection = async (
name, name,
description: JSON.stringify({ description: JSON.stringify({
description: description, description: description,
randomOrder: randomOrder randomOrder: randomOrder,
}), }),
candidates, candidates,
grades, grades,
@ -197,23 +184,22 @@ export const updateElection = async (
force_close: forceClose, force_close: forceClose,
restricted, restricted,
}), }),
}) });
if (!req.ok || req.status !== 200) { if (!req.ok || req.status !== 200) {
const payload = await req.json(); const payload = await req.json();
return {status: req.status, msg: payload}; return { status: req.status, msg: payload };
} }
const payload = await req.json(); const payload = await req.json();
return {status: 200, ...payload} return { status: 200, ...payload };
} } catch (e) {
catch (e) { console.error(e);
console.error(e) return { status: 400, msg: 'Unknown API error' };
return {status: 400, msg: "Unknown API error"}
} }
}; };
export const getResults = async (
export const getResults = async (pid: string): Promise<ResultsPayload | HTTPPayload> => { pid: string
): Promise<ResultsPayload | HTTPPayload> => {
/** /**
* Fetch results from external API * Fetch results from external API
*/ */
@ -224,25 +210,29 @@ export const getResults = async (pid: string): Promise<ResultsPayload | HTTPPayl
); );
try { try {
const response = await fetch(endpoint.href) const response = await fetch(endpoint.href);
if (response.status != 200) { if (response.status != 200) {
const payload = await response.json(); const payload = await response.json();
return {status: response.status, msg: payload}; return { status: response.status, msg: payload };
} }
const payload = await response.json() const payload = await response.json();
return {...payload, status: response.status}; return { ...payload, status: response.status };
} catch (error) { } catch (error) {
console.error(error) console.error(error);
return {status: 400, msg: "Unknown API error"} return { status: 400, msg: 'Unknown API error' };
} }
}; };
export const getElection = async (
export const getElection = async (pid: string): Promise<ElectionPayload | HTTPPayload> => { pid: string
): Promise<ElectionPayload | HTTPPayload> => {
/** /**
* Fetch data from external API * Fetch data from external API
*/ */
const path = api.routesServer.getElection.replace(new RegExp(':slug', 'g'), pid); const path = api.routesServer.getElection.replace(
new RegExp(':slug', 'g'),
pid
);
const endpoint = new URL(path, api.urlServer); const endpoint = new URL(path, api.urlServer);
try { try {
@ -250,20 +240,19 @@ export const getElection = async (pid: string): Promise<ElectionPayload | HTTPPa
if (response.status != 200) { if (response.status != 200) {
const payload = await response.json(); const payload = await response.json();
return {status: response.status, msg: payload}; return { status: response.status, msg: payload };
} }
const payload = await response.json() const payload = await response.json();
return {...payload, status: response.status}; return { ...payload, status: response.status };
} catch (error) { } catch (error) {
return {status: 400, msg: "Unknown API error"} return { status: 400, msg: 'Unknown API error' };
} }
}; };
export const castBallot = ( export const castBallot = (
votes: Array<Vote>, votes: Array<Vote>,
election: ElectionPayload, election: ElectionPayload,
token?: string, token?: string
) => { ) => {
/** /**
* Save a ballot on the remote database * Save a ballot on the remote database
@ -273,31 +262,30 @@ export const castBallot = (
const payload = { const payload = {
election_ref: election.ref, election_ref: election.ref,
votes: votes.map(v => ({ votes: votes.map((v) => ({
"candidate_id": election.candidates[v.candidateId].id, candidate_id: election.candidates[v.candidateId].id,
"grade_id": election.grades[v.gradeId].id grade_id: election.grades[v.gradeId].id,
})) })),
}; };
if (!election.restricted) { if (!election.restricted) {
return fetch(endpoint.href, { return fetch(endpoint.href, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}) });
} } else {
else {
if (!token) { if (!token) {
throw Error("Missing token") throw Error('Missing token');
} }
return fetch(endpoint.href, { return fetch(endpoint.href, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
"Authorization": `Bearer ${token}` Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}) });
} }
}; };
@ -331,4 +319,3 @@ export const apiErrors = (error: string): string => {
return 'error.catch22'; return 'error.catch22';
} }
}; };

Loading…
Cancel
Save