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}
{numVotes}{' '}
{numVotes > 1
? t('common.participants')
: t('common.participant')}
{
// DESKTOP
}
{numVotes}{' '}
{numVotes > 1
? t('common.participants')
: t('common.participant')}
{result.name}
>
);
};
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;