diff --git a/pages/ballot/[pid]/[[...tid]].tsx b/pages/ballot/[pid]/[[...tid]].tsx
index 25a88ea..83e31ea 100644
--- a/pages/ballot/[pid]/[[...tid]].tsx
+++ b/pages/ballot/[pid]/[[...tid]].tsx
@@ -1,38 +1,51 @@
import {useState, useCallback, useEffect, MouseEvent} from 'react';
+import Image from 'next/image';
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 {Col, Row, Container} 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 {getElection, castBallot, apiErrors, ElectionPayload, CandidatePayload, GradePayload} from '@services/api';
+import Button from '@components/Button';
import useEmblaCarousel from 'embla-carousel-react';
import {DotButton} from '@components/admin/EmblaCarouselButtons';
-import VoteButtonWithConfirm from '@components/admin/VoteButtonWithConfirm';
import {getGradeColor} from '@services/grades';
+import {useBallot, BallotTypes, BallotProvider} from '@services/BallotContext';
+import {getLocaleShort} from '@services/utils';
+import {ENDED_VOTE} from '@services/routes';
+import defaultAvatar from '../../../public/avatarBlue.svg';
+
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
export async function getServerSideProps({query: {pid, tid}, locale}) {
+ if (!pid) {
+ return {notFound: true}
+ }
+
const [election, translations] = await Promise.all([
getElection(pid),
serverSideTranslations(locale, ['resource']),
]);
if (typeof election === 'string' || election instanceof String) {
- return {props: {err: election, ...translations}};
+ return {notFound: true}
+ }
+
+ const dateEnd = new Date(election.date_end)
+ if (dateEnd.getDate() > new Date().getDate()) {
+ return {
+ redirect: ENDED_VOTE,
+ permanent: false
+ }
}
if (!election || !election.candidates || !Array.isArray(election.candidates)) {
- return {props: {err: 'Unknown error', ...translations}};
+ console.log(election);
+ return {notFound: true}
}
const description = JSON.parse(election.description);
@@ -51,21 +64,140 @@ export async function getServerSideProps({query: {pid, tid}, locale}) {
};
}
+interface TitleBarInterface {
+ election: ElectionPayload;
+}
+const TitleBar = ({election}: TitleBarInterface) => {
+ const {t} = useTranslation();
+ const router = useRouter();
+ const locale = getLocaleShort(router);
+
+ return (
+
+
+
+
+
+ {` ${t("vote.open-until")} ${new Date(election.date_end).toLocaleDateString(locale, {dateStyle: "long"})}`}
+
+
+ )
+};
+
+interface CandidateCardInterface {
+ candidate: CandidatePayload;
+}
+const CandidateCard = ({candidate}: CandidateCardInterface) => {
+ const {t} = useTranslation();
+ return (
+
+
+ {candidate.name}
+
+ {t("vote.more-details")}
+
+
)
+}
+
+
+interface GradeInputInterface {
+ gradeId: number;
+ candidateId: number;
+}
+const GradeInput = ({gradeId, candidateId}: GradeInputInterface) => {
+ const [ballot, dispatch] = useBallot();
+ if (!ballot) {throw Error("Ensure the election is loaded")}
+
+ const grade = ballot.election.grades[gradeId];
+ const numGrades = ballot.election.grades.length;
+ const color = getGradeColor(gradeId, numGrades);
+
+ const handleClick = (event: MouseEvent) => {
+ dispatch({type: BallotTypes.VOTE, candidateId: candidateId, gradeId: gradeId})
+ };
+
+ const active = ballot.votes.some(b => b.gradeId === gradeId && b.candidateId === candidateId)
+
+ return (
+
+
+ {grade.name}
+
+
+ { /**/}
+ {grade.name}
+ { /**/}
+
+
+ )
+}
+
interface VoteInterface {
- election?: ElectionPayload;
+ election: ElectionPayload;
err: string;
token?: string;
}
-
-const VoteBallot = ({election, err, token}: VoteInterface) => {
+const VoteBallot = ({election, token}: VoteInterface) => {
const {t} = useTranslation();
- if (err || !election) {
- return ;
+ const router = useRouter();
+ const [ballot, dispatch] = useBallot();
+
+ useEffect(() => {
+ dispatch({
+ type: BallotTypes.ELECTION,
+ election: election,
+ });
+ }, []);
+
+ if (!ballot.election) {
+ return "Loading..."
;
}
- const numGrades = election.grades.length;
- const [judgments, setJudgments] = useState([]);
+ const numGrades = ballot.election.grades.length;
+ const disabled = ballot.votes.length !== ballot.election.candidates.length;
const colSizeCandidateLg = 4;
const colSizeCandidateMd = 6;
const colSizeCandidateXs = 12;
@@ -73,21 +205,6 @@ const VoteBallot = ({election, err, token}: VoteInterface) => {
const colSizeGradeMd = Math.floor((12 - colSizeCandidateMd) / numGrades);
const colSizeGradeXs = Math.floor((12 - colSizeCandidateXs) / numGrades);
- const router = useRouter();
-
- const handleGradeClick = (event: MouseEvent) => {
- 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!'));
};
@@ -95,49 +212,49 @@ const VoteBallot = ({election, err, token}: VoteInterface) => {
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 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`);
- });
+ // castBallot(gradesByCandidate, election.id.toString(), token, () => {
+ router.push(`/confirm/${election.id}`);
+ // });
};
- 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 [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 onSelect = useCallback(() => {
- if (!embla) return;
- setSelectedIndex(embla.selectedScrollSnap());
- setPrevBtnEnabled(embla.canScrollPrev());
- setNextBtnEnabled(embla.canScrollNext());
- }, [embla, setSelectedIndex]);
+ // const scrollPrev = useCallback(() => embla && embla.scrollPrev(), [embla]);
+ // const scrollNext = useCallback(() => embla && embla.scrollNext(), [embla]);
+ // const scrollTo = useCallback(
+ // (index) => embla && embla.scrollTo(index),
+ // [embla]
+ // );
- useEffect(() => {
- if (!embla) return;
- onSelect();
- setScrollSnaps(embla.scrollSnapList());
- embla.on('select', onSelect);
- }, [embla, setScrollSnaps, onSelect]);
+ // 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 (
-
+ <>
{election.name}
@@ -149,173 +266,50 @@ const VoteBallot = ({election, err, token}: VoteInterface) => {
/>
-
-
- VOTE OUVERT
-
-
-
{election.name}
+
+
-
-
- {judgments.length !== election.candidates.length ? (
-
- ) : (
-
- )}
-
-
-
+ {election.candidates.map((candidate, candidateId) => {
+ return (
+
+
+
+ {election.grades.map((_, gradeId) => {
+ console.assert(gradeId < numGrades);
+ return (
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+
+
+
+ >
);
};
-export default VoteBallot;
+
+const Ballot = (props) => {
+
+ return (
+
+
+ )
+}
+
+export default Ballot;
diff --git a/public/locales/en/resource.json b/public/locales/en/resource.json
index 4e4d236..8192375 100644
--- a/public/locales/en/resource.json
+++ b/public/locales/en/resource.json
@@ -47,7 +47,7 @@
"common.the-params": "The parameters",
"common.welcome": "Welcome!",
"error.at-least-2-candidates": "At least two candidates are required.",
- "error.catch22": "Erreur inconnue...",
+ "error.catch22": "Unknown error...",
"error.help": "Ask for our help",
"error.no-title": "Please add a title to your election.",
"grades.very-good": "Very good",
@@ -107,5 +107,8 @@
"admin.success-copy-admin": "Copy the admin link",
"admin.go-to-admin": "Manage the vote",
"vote.home-desc": "Participate in the vote and discover majority judgment",
- "vote.home-start": "I participate"
+ "vote.home-start": "I participate",
+ "vote.open-until": "Vote open until",
+ "vote.more-details": "More details...",
+ "vote.submit": "Cast my ballot"
}
diff --git a/public/locales/fr/resource.json b/public/locales/fr/resource.json
index fc88c20..648f533 100644
--- a/public/locales/fr/resource.json
+++ b/public/locales/fr/resource.json
@@ -107,5 +107,8 @@
"admin.success-copy-admin": "Copier le lien d'administration",
"admin.go-to-admin": "Administrez le vote",
"vote.home-desc": "Participez au vote et découvrez le jugement majoritaire.",
- "vote.home-start": "Je participe"
+ "vote.home-start": "Je participe",
+ "vote.open-until": "Vote ouvert jusqu'au",
+ "vote.more-details": "Cliquez ici pour en savoir plus",
+ "vote.submit": "Déposer mon bulletin de vote"
}
diff --git a/services/BallotContext.tsx b/services/BallotContext.tsx
new file mode 100644
index 0000000..a67a519
--- /dev/null
+++ b/services/BallotContext.tsx
@@ -0,0 +1,97 @@
+/**
+ * This file provides a context and a reducer to manage a ballot
+ */
+import {createContext, useContext, useReducer, Dispatch} from 'react';
+import {ElectionPayload} from './api';
+
+
+export interface Vote {
+ candidateId: number;
+ gradeId: number;
+}
+
+export interface BallotContextInterface {
+ election: ElectionPayload | null;
+ votes: Array;
+}
+
+const defaultBallot: BallotContextInterface = {
+ election: null,
+ votes: []
+};
+
+
+export enum BallotTypes {
+ ELECTION = 'ELECTION',
+ VOTE = 'VOTE',
+}
+
+export type ElectionAction = {
+ type: BallotTypes.ELECTION;
+ election: ElectionPayload;
+}
+
+export type VoteAction = {
+ type: BallotTypes.VOTE;
+ candidateId: number;
+ gradeId: number;
+}
+
+export type BallotActionTypes = ElectionAction | VoteAction;
+
+
+function reducer(ballot: BallotContextInterface, action: BallotActionTypes) {
+ /**
+ * Manage all types of action doable on an election
+ */
+ switch (action.type) {
+ case BallotTypes.ELECTION: {
+ return {...ballot, election: action.election}
+ }
+ case BallotTypes.VOTE: {
+ const votes = [...ballot.votes];
+ const voteForCandidate = votes.filter(v => v.candidateId === action.candidateId);
+ if (voteForCandidate.length > 1) {
+ throw Error("Inconsistent ballot")
+ }
+ if (voteForCandidate.length === 1) {
+ voteForCandidate[0].candidateId = action.candidateId;
+ voteForCandidate[0].gradeId = action.gradeId;
+ } else {
+ votes.push({
+ candidateId: action.candidateId,
+ gradeId: action.gradeId,
+ })
+ }
+ return {...ballot, votes};
+ }
+ default: {
+ return ballot
+ }
+ }
+}
+
+type DispatchType = Dispatch;
+const BallotContext = createContext<[BallotContextInterface, DispatchType]>([defaultBallot, () => {}]);
+// const BallotContext = createContext([defaultBallot, () => {}]);
+
+export function BallotProvider({children}) {
+ /**
+ * Provide the election and the dispatch to all children components
+ */
+ const [ballot, dispatch] = useReducer(reducer, defaultBallot);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useBallot() {
+ /**
+ * A simple hook to manage ballots
+ */
+ return useContext(BallotContext);
+}
+
diff --git a/services/ElectionContext.tsx b/services/ElectionContext.tsx
index 00e58ad..275ca3c 100644
--- a/services/ElectionContext.tsx
+++ b/services/ElectionContext.tsx
@@ -144,7 +144,7 @@ function electionReducer(election: ElectionContextInterface, action) {
}
case 'grade-set': {
if (typeof action.position !== 'number') {
- throw Error(`Unexpected grade position ${action.value}`);
+ throw Error(`Unexpected grade position ${action.position}`);
}
const grades = [...election.grades];
const grade = grades[action.position];
diff --git a/services/api.ts b/services/api.ts
index a673d72..1e25aac 100644
--- a/services/api.ts
+++ b/services/api.ts
@@ -107,9 +107,7 @@ export const getResults = (
export const getElection = async (
pid: string,
- successCallback = null,
- failureCallback = null
-): Promise => {
+): Promise => {
/**
* Fetch data from external API
*/
@@ -121,14 +119,13 @@ export const getElection = async (
const res = await fetch(detailsEndpoint.href);
if (!res.ok) {
- return failureCallback(res.text())
+ return res.text()
}
const payload: ElectionPayload = await res.json();
- if (successCallback) successCallback(payload);
return payload;
} catch (error) {
- return failureCallback && failureCallback(error);
+ return error;
}
};
diff --git a/services/routes.ts b/services/routes.ts
index 1b9e043..72b6659 100644
--- a/services/routes.ts
+++ b/services/routes.ts
@@ -6,6 +6,7 @@ import {getWindowUrl} from './utils';
export const CREATE_ELECTION = '/admin/new/';
export const BALLOT = '/ballot/';
+export const ENDED_VOTE = '/ballot/end';
export const VOTE = '/vote/';
export const RESULTS = '/result/';
diff --git a/styles/scss/config.scss b/styles/scss/config.scss
index 830fdf3..1451474 100644
--- a/styles/scss/config.scss
+++ b/styles/scss/config.scss
@@ -33,6 +33,23 @@ $theme-colors: (
// "very-good": #A0CF1C,
// "excellent": #3A9918,
// );
+$font-size-base: 1rem;
+
+$h1-font-size: $font-size-base * 2.5;
+$h2-font-size: $font-size-base * 2;
+$h3-font-size: $font-size-base * 1.75;
+$h4-font-size: $font-size-base * 1.5;
+$h5-font-size: $font-size-base * 1.25;
+
+$font-sizes: (
+ 1: $h1-font-size,
+ 2: $h2-font-size,
+ 3: $h3-font-size,
+ 4: $h4-font-size,
+ 5: $font-size-base,
+ 6: $font-size-base * 0.75
+);
+
@import "_bootstrap.scss";