import { useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import Link from 'next/link'; import { Container, Collapse, Card, CardHeader, CardBody } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowRight, faChevronDown, faChevronRight, faChevronUp, faGear, } from '@fortawesome/free-solid-svg-icons'; import ErrorMessage from '@components/Error'; import CSVLink from '@components/CSVLink'; import Logo from '@components/Logo'; import MeritProfile from '@components/MeritProfile'; import Button from '@components/Button'; import { getResults } from '@services/api'; import { GradeResultInterface, ResultInterface, MeritProfileInterface, CandidateResultInterface, } from '@services/type'; import { getUrl, RouteTypes } from '@services/routes'; import { displayRef, getLocaleShort } from '@services/utils'; import { getMajorityGrade } from '@services/majorityJudgment'; import avatarBlue from '../../../public/avatarBlue.svg'; import calendar from '../../../public/calendar.svg'; import arrowUpload from '../../../public/arrowUpload.svg'; import arrowLink from '../../../public/arrowL.svg'; import { getGradeColor } from '@services/grades'; import { useRouter } from 'next/router'; export async function getServerSideProps({ query, locale }) { const { pid, tid: token } = query; const electionRef = pid.replaceAll('-', ''); const [payload, translations] = await Promise.all([ getResults(electionRef), serverSideTranslations(locale, ['resource']), ]); if ('msg' in payload) { return { props: { err: payload.msg, electionRef, ...translations } }; } const numGrades = payload.grades.length; const grades = payload.grades.map((g, i) => ({ ...g, color: getGradeColor(i, numGrades), })); const gradesByValue: { [key: number]: GradeResultInterface } = {}; grades.forEach((g) => (gradesByValue[g.value] = g)); const result: ResultInterface = { name: payload.name, description: payload.description, ref: payload.ref, dateStart: payload.date_start, dateEnd: payload.date_end, hideResults: payload.hide_results, forceClose: payload.force_close, restricted: payload.restricted, grades: grades, candidates: payload.candidates.map((c) => { const profile = payload.merit_profile[c.id]; const values = grades.map((g) => g.value); values.forEach((v) => (profile[v] = profile[v] || 0)); const majValue = getMajorityGrade(profile); return { ...c, meritProfile: payload.merit_profile[c.id], rank: payload.ranking[c.id] + 1, majorityGrade: gradesByValue[majValue], }; }), ranking: payload.ranking, meritProfiles: payload.merit_profile, }; return { props: { result, token: token || '', ...translations, }, }; } const getNumVotes = (result: ResultInterface) => { const sum = (seq: MeritProfileInterface) => Object.values(seq).reduce((a, b) => a + b, 0); const anyCandidateId = result.candidates[0].id; const numVotes = sum(result.meritProfiles[anyCandidateId]); Object.values(result.meritProfiles).forEach((v) => { if (sum(v) !== numVotes) { throw Error( 'The election does not contain the same number of votes for each candidate' ); } }); return numVotes; }; const WillClose = ({ delay }) => { const { t } = useTranslation(); if (delay < 365) { return
{t('result.closed')}
; } else if (delay < 0) { return (
{`${t('result.has-closed')} ${delay} ${t('common.days')}`}
); } else if (delay > 365) { return
{t('result.opened')}
; } else { return (
{`${t('result.will-close')} ${delay} ${t('common.days')}`}
); } }; interface ResultBanner { result: ResultInterface; } const ResultBanner = ({ result }) => { const { t } = useTranslation(); const router = useRouter(); const dateEnd = new Date(result.dateEnd); const now = new Date(); const closedSince = +dateEnd - +now; const numVotes = getNumVotes(result); const url = getUrl(RouteTypes.RESULTS, router, result.ref); return ( <> { // MOBILE }

{result.name}

Calendar
Avatar
{numVotes}{' '} {numVotes > 1 ? t('common.participants') : t('common.participant')}
{ // DESKTOP }
Calendar
Avatar
{numVotes}{' '} {numVotes > 1 ? t('common.participants') : t('common.participant')}

{result.name}

Download
{t('result.download')}
Share
{t('result.share')}
); }; const Downloader = ({ result, children, ...rest }) => { const values = result.grades.map((v) => v.value).sort(); const data = result.candidates.map((c) => { const grades = {}; result.grades.forEach( (g) => (grades[g.name] = g.value in c.meritProfile ? c.meritProfile[g.value].toString() : '0') ); return { name: c.name, ...grades }; }); return ( {children} ); }; const BottomButtonsMobile = ({ result }) => { const { t } = useTranslation(); const router = useRouter(); const url = getUrl(RouteTypes.RESULTS, router, result.ref); return (
); }; interface TitleBannerInterface { name: string; electionRef: string; token?: string; } const TitleBanner = ({ name, electionRef, token }: TitleBannerInterface) => { const { t } = useTranslation(); const router = useRouter(); const locale = getLocaleShort(router); return ( <> { // MOBILE }
{name}
{token ? (
) : null}
{ // DESKTOP }
{t('result.result')}
{token ? (
) : null}
); }; interface ButtonGradeResultInterface { grade: GradeResultInterface; } const ButtonGrade = ({ grade }: ButtonGradeResultInterface) => { const style = { color: 'white', backgroundColor: grade.color, }; return (
{grade.name}
); }; interface CandidateRankedInterface { candidate: CandidateResultInterface; } const CandidateRanked = ({ candidate }: CandidateRankedInterface) => { const isFirst = candidate.rank == 1; return (
{candidate.rank}
{candidate.name}
); }; interface CandidateCardInterface { candidate: CandidateResultInterface; grades: Array; } const CandidateCard = ({ candidate, grades }: CandidateCardInterface) => { const { t } = useTranslation(); const [collapse, setCollapse] = useState(true); return ( setCollapse((s) => !s)} >
{candidate.rank} {candidate.name}
{t('result.merit-profile')}
{t('result.how-to-interpret')}
); }; interface PodiumInterface { candidates: Array; } const Podium = ({ candidates }: PodiumInterface) => { const { t } = useTranslation(); // get best candidates const numBest = Math.min(3, candidates.length); const candidateByRank = {}; candidates .filter((c) => c.rank < 4) .forEach((c) => (candidateByRank[c.rank] = c)); if (numBest < 2) { throw Error('Can not load enough candidates'); } if (numBest === 2) { return (
); } return (
); }; interface ErrorInterface { message: string; } interface ResultPageInterface { result?: ResultInterface; token?: string; err?: ErrorInterface; electionRef?: string; } const ResultPage = ({ result, token, err, electionRef, }: ResultPageInterface) => { const { t } = useTranslation(); const router = useRouter(); if (err && err.message.startsWith('No votes')) { const urlVote = getUrl(RouteTypes.VOTE, router, electionRef, token); return ( { <>

{t('result.no-votes')}

}
); } if (!result) { return {t('error.catch22')}; } if ( typeof result.candidates === 'undefined' || result.candidates.length === 0 ) { throw Error('No candidates were loaded in this election'); } const candidateByRank = {}; result.candidates .filter((c) => c.rank < 4) .forEach((c) => (candidateByRank[c.rank] = c)); return ( {result.name}
{t('result.details')}
{Object.keys(candidateByRank) .sort() .map((rank, i) => { return ( ); })}
); }; export default ResultPage;