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 ('message' in payload) { return {props: {err: payload, electionRef, ...translations}}; } const numGrades = payload.grades.length; const grades = payload.grades.map((g, i) => ({ ...g, color: getGradeColor(g.value, 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 new Error( 'The election does not contain the same number of votes for each candidate' ); } }); return numVotes; }; const WillClose = ({delay, forceClose}) => { const {t} = useTranslation(); if (delay < 365 || forceClose) { 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 new Error('Can not load enough candidates'); } if (numBest === 2) { return (
); } return (
); }; interface ErrorInterface { message: string; details?: 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 (err && err.details.startsWith('The election is not closed')) { const urlVote = getUrl(RouteTypes.VOTE, router, electionRef, token); return ( { <>

{t('result.hide-results')}

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