diff --git a/components/ballot/BallotDesktop.tsx b/components/ballot/BallotDesktop.tsx index 64b6f32..eb59472 100644 --- a/components/ballot/BallotDesktop.tsx +++ b/components/ballot/BallotDesktop.tsx @@ -6,8 +6,11 @@ import TitleBar from '@components/ballot/TitleBar'; import GradeInput from '@components/ballot/GradeInput'; import {CandidatePayload} from '@services/api'; import CandidateModal from '@components/CandidateModalGet'; +import {useTranslation} from 'next-i18next'; + +const BallotDesktop = ({hasVoted}) => { + const {t} = useTranslation(); -const BallotDesktop = () => { const [ballot, dispatch] = useBallot(); const numGrades = ballot.election.grades.length; const disabled = ballot.votes.length !== ballot.election.candidates.length; @@ -22,6 +25,9 @@ const BallotDesktop = () => { style={{maxWidth: '1050px'}} >

{ballot.election.name}

+ {hasVoted &&
+ {t("vote.already-voted")} +
} {ballot.election.candidates.map((candidate, candidateId) => { return (
{ ) } -const BallotMobile = () => { +const BallotMobile = ({hasVoted}) => { + const {t} = useTranslation(); + const [ballot, _] = useBallot(); const [offset, setOffset] = useState(0); @@ -36,6 +39,9 @@ const BallotMobile = () => { return (
+ {hasVoted &&
+ {t("vote.already-voted")} +
}
{ballot.election.candidates.map((candidate, candidateId) => { return ( diff --git a/pages/ballot/[pid]/[[...tid]].tsx b/pages/ballot/[pid]/[[...tid]].tsx index 3b2290f..675d064 100644 --- a/pages/ballot/[pid]/[[...tid]].tsx +++ b/pages/ballot/[pid]/[[...tid]].tsx @@ -1,15 +1,16 @@ -import { useEffect, useState } from 'react'; +import {useEffect, useState} from 'react'; import Head from 'next/head'; -import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useTranslation } from 'next-i18next'; -import { Container } from 'reactstrap'; -import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import {serverSideTranslations} from 'next-i18next/serverSideTranslations'; +import {useTranslation} from 'next-i18next'; +import {Container} from 'reactstrap'; +import {faCheck} from '@fortawesome/free-solid-svg-icons'; import BallotDesktop from '@components/ballot/BallotDesktop'; import Button from '@components/Button'; import BallotMobile from '@components/ballot/BallotMobile'; import Blur from '@components/Blur'; import { getElection, + getBallot, castBallot, ElectionPayload, BallotPayload, @@ -20,26 +21,27 @@ import { BallotTypes, BallotProvider, } from '@services/BallotContext'; -import { getUrl, RouteTypes } from '@services/routes'; -import { isEnded } from '@services/utils'; +import {getUrl, RouteTypes} from '@services/routes'; +import {isEnded} from '@services/utils'; import WaitingBallot from '@components/WaitingBallot'; import PatternedBackground from '@components/PatternedBackground'; 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) { - return { notFound: true }; + return {notFound: true}; } const electionRef = pid.replaceAll('-', ''); - const [election, translations] = await Promise.all([ + const [election, ballot, translations] = await Promise.all([ getElection(electionRef), + tid ? getBallot(tid) : new Response(null), serverSideTranslations(locale, ['resource']), ]); if ('msg' in election) { - return { notFound: true }; + return {notFound: true}; } if (isEnded(election.date_end)) { @@ -56,7 +58,7 @@ export async function getServerSideProps({ query: { pid, tid }, locale }) { !election.candidates || !Array.isArray(election.candidates) ) { - return { notFound: true }; + return {notFound: true}; } const description = JSON.parse(election.description); @@ -70,12 +72,13 @@ export async function getServerSideProps({ query: { pid, tid }, locale }) { ...translations, election, token: tid || null, + previousBallot: ballot, }, }; } const ButtonSubmit = () => { - const { t } = useTranslation(); + const {t} = useTranslation(); const [ballot, dispatch] = useBallot(); const disabled = ballot.votes.length !== ballot.election.candidates.length; @@ -100,9 +103,10 @@ interface VoteInterface { election: ElectionPayload; err: string; token?: string; + previousBallot: BallotPayload } -const VoteBallot = ({ election, token }: VoteInterface) => { - const { t } = useTranslation(); +const VoteBallot = ({election, token, previousBallot}: VoteInterface) => { + const {t} = useTranslation(); const [ballot, dispatch] = useBallot(); @@ -121,14 +125,6 @@ const VoteBallot = ({ election, token }: VoteInterface) => { return
"Loading..."
; } - if (voting) { - return ( - - - - ); - } - const handleSubmit = async (event) => { event.preventDefault(); setVoting(true); @@ -149,6 +145,14 @@ const VoteBallot = ({ election, token }: VoteInterface) => { } }; + if (voting) { + return ( + + + + ); + } + return (
{
- - + +
diff --git a/public/locales/en/resource.json b/public/locales/en/resource.json index 61c915a..7e22f42 100644 --- a/public/locales/en/resource.json +++ b/public/locales/en/resource.json @@ -185,6 +185,7 @@ "result.share": "Share results", "success.election-closed": "The vote has been created with success!", "success.election-updated": "The vote has been updated with success!", + "vote.already-voted": "You have already voted, but you can change your vote.", "vote.discover-mj": "Discover majority judgment", "vote.discover-mj-desc": "Developed by French researchers, majority judgment is a voting system that improves voter expressiveness and provides the best consensus.", "vote.form": "Your opinion is important to us", diff --git a/public/locales/fr/resource.json b/public/locales/fr/resource.json index 44fb0c8..4ca7cd9 100644 --- a/public/locales/fr/resource.json +++ b/public/locales/fr/resource.json @@ -186,6 +186,7 @@ "result.how-to-interpret": "Comment interpréter les résultats", "success.election-closed": "L'élection est désormais clôturée.", "success.election-updated": "L'élection a été mise à jour.", + "vote.already-voted": "Vous avez déjà voté, mais vous pouvez modifier votre vote.", "vote.discover-mj": "Découvrez le jugement majoritaire", "vote.discover-mj-desc": "Créé par des chercheurs français, le jugement majoritaire est un mode de scrutin qui améliore l'expressivité des électeurs et fournit le meilleur consensus.", "vote.go-to-results": "Voir les résultats", diff --git a/services/api.ts b/services/api.ts index b40a28d..7a1ecce 100644 --- a/services/api.ts +++ b/services/api.ts @@ -248,6 +248,40 @@ export const getElection = async ( } }; +/** + * Fetch a ballot from the API. Return null if the ballot does not exist. + */ +export const getBallot = async ( + token: string +): Promise => { + const path = api.routesServer.voteElection + const endpoint = new URL(path, URL_SERVER); + + if (!token) { + throw new Error('Missing token'); + } + + try { + const response = await fetch(endpoint.href, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.status != 200) { + console.log("STATUS", await response.text()) + return null; + } + + const payload = await response.json(); + return {...payload, status: response.status}; + + } catch (error) { + return {status: 400, msg: 'Unknown API error'}; + } +}; + export const castBallot = ( votes: Array, election: ElectionPayload,