fix: download csv

pull/89/head
Pierre-Louis Guhur 1 year ago
parent 9a41f72e70
commit 9e7e2c690f

@ -0,0 +1,125 @@
import React from 'react';
// import dynamic from 'next/dynamic'
// import {buildURI} from 'react-csv';
// /**
// * See https://github.com/react-csv/react-csv/issues/87
// */
export const isSafari = () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
export const isJsons = ((array) => Array.isArray(array) && array.every(
row => (typeof row === 'object' && !(row instanceof Array))
));
export const isArrays = ((array) => Array.isArray(array) && array.every(
row => Array.isArray(row)
));
export const jsonsHeaders = ((array) => Array.from(
array.map(json => Object.keys(json))
.reduce((a, b) => new Set([...a, ...b]), [])
));
export const jsons2arrays = (jsons, headers) => {
headers = headers || jsonsHeaders(jsons);
// allow headers to have custom labels, defaulting to having the header data key be the label
let headerLabels = headers;
let headerKeys = headers;
if (isJsons(headers)) {
headerLabels = headers.map((header) => header.label);
headerKeys = headers.map((header) => header.key);
}
const data = jsons.map((object) => headerKeys.map((header) => getHeaderValue(header, object)));
return [headerLabels, ...data];
};
export const getHeaderValue = (property, obj) => {
const foundValue = property
.replace(/\[([^\]]+)]/g, ".$1")
.split(".")
.reduce(function (o, p, i, arr) {
// if at any point the nested keys passed do not exist, splice the array so it doesnt keep reducing
const value = o[p];
if (value === undefined || value === null) {
arr.splice(1);
} else {
return value;
}
}, obj);
// if at any point the nested keys passed do not exist then looks for key `property` in object obj
return (foundValue === undefined) ? ((property in obj) ? obj[property] : '') : foundValue;
}
export const elementOrEmpty = (element) =>
(typeof element === 'undefined' || element === null) ? '' : element;
export const joiner = ((data, separator = ',', enclosingCharacter = '"') => {
return data
.filter(e => e)
.map(
row => row
.map((element) => elementOrEmpty(element))
.map(column => `${enclosingCharacter}${column}${enclosingCharacter}`)
.join(separator)
)
.join(`\n`);
});
export const arrays2csv = ((data, headers, separator, enclosingCharacter) =>
joiner(headers ? [headers, ...data] : data, separator, enclosingCharacter)
);
export const jsons2csv = ((data, headers, separator, enclosingCharacter) =>
joiner(jsons2arrays(data, headers), separator, enclosingCharacter)
);
export const string2csv = ((data, headers, separator) =>
(headers) ? `${headers.join(separator)}\n${data}` : data.replace(/"/g, '""')
);
export const toCSV = (data, headers, separator, enclosingCharacter) => {
if (isJsons(data)) return jsons2csv(data, headers, separator, enclosingCharacter);
if (isArrays(data)) return arrays2csv(data, headers, separator, enclosingCharacter);
if (typeof data === 'string') return string2csv(data, headers, separator);
throw new TypeError(`Data should be a "String", "Array of arrays" OR "Array of objects" `);
};
const CSVLink = ({filename, data, children}) => {
console.log("DATA", data);
const buildURI = ((data, uFEFF, headers, separator, enclosingCharacter) => {
console.log("DATA2", data);
const csv = toCSV(data, headers, separator, enclosingCharacter);
console.log("CSV", csv);
const type = isSafari() ? 'application/csv' : 'text/csv';
const blob = new Blob([uFEFF ? '\uFEFF' : '', csv], {type});
const dataURI = `data:${type};charset=utf-8,${uFEFF ? '\uFEFF' : ''}${csv}`;
const URL = window.URL || window.webkitURL;
return (typeof URL.createObjectURL === 'undefined')
? dataURI
: URL.createObjectURL(blob);
});
const isNodeEnvironment = typeof window === 'undefined';
const uFEFF = true;
const headers = undefined;
const separator = ",";
const enclosingCharacter = '"';
const href = isNodeEnvironment ? '' : buildURI(data, uFEFF, headers, separator, enclosingCharacter)
return (
<a
download={filename}
target="_blank"
href={href}
>
{children}
</a>
);
}
export default CSVLink;

@ -1,5 +1,4 @@
import Link from 'next/link';
import {Container, Row, Col} from 'reactstrap';
import {Container} from 'reactstrap';
import {useTranslation} from 'next-i18next';
import {CONTACT_MAIL} from '@services/constants';
import Button from '@components/Button'

@ -0,0 +1,206 @@
import {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {GradeResultInterface, MeritProfileInterface} from '@services/type';
import {getMajorityGrade} from '@services/majorityJudgment';
interface ParamsInterface {
numVotes: number;
outgaugeThreshold: number;
}
interface GradeBarInterface {
grade: GradeResultInterface;
size: number;
index: number;
params: ParamsInterface;
}
const GradeBar = ({index, grade, size, params}: GradeBarInterface) => {
const width = `${size * 100}%`
const textWidth = Math.floor(100 * size)
const left = `${(size * 100) / 2}%`;
const top = index % 2 ? "20px" : "-20px";
if (size < 0.001) {
return null;
}
return (
<div
className="h-100"
style={{flexBasis: width, backgroundColor: grade.color, minHeight: "20px"}}>
{
/* size < params.outgaugeThreshold ? (
<span
style={{
left: left,
top: top,
display: "relative",
backgroundColor: grade.color,
}}
>
{textWidth}%
</span>
) : (
<span>
{Math.floor(100 * size)}%
</span>
)
*/ }
</div>)
}
const DashedMedian = () => {
return <div className="position-relative d-flex justify-content-center"
style={{top: "60px", height: "50px"}}
>
<div
className="border h-100 border-1 border-dark border-opacity-75 border-dashed"
>
</div>
</div>
}
const MajorityGrade = ({grade, left}) => {
const spanRef = useRef<HTMLDivElement>();
const [width, setWidth] = useState(0)
useLayoutEffect(() => {
if (spanRef && spanRef.current) {
setWidth(spanRef.current.offsetWidth);
}
}, []);
useEffect(() => {
if (spanRef && spanRef.current) {
setWidth(spanRef.current.offsetWidth);
}
}, []);
return (
<>
<span
ref={spanRef}
style={{
color: 'white',
backgroundColor: grade.color,
left: `calc(${left * 100}% - ${width / 2}px)`,
top: "-20px",
}}
className="p-2 position-relative fw-bold rounded-1 text-center"
>
{grade.name}
</span>
<span style={{
position: "relative",
width: 0,
height: 0,
left: `calc(${left * 100}% - ${width + 6}px)`,
top: "20px",
borderLeftWidth: 6,
borderRightWidth: 6,
borderTopWidth: 12,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderTopColor: grade.color,
color: "transparent",
}}></span>
</>
);
}
interface MeritProfileBarInterface {
profile: MeritProfileInterface;
grades: Array<GradeResultInterface>;
}
const MeritProfileBar = ({profile, grades}: MeritProfileBarInterface) => {
const gradesByValue: {[key: number]: GradeResultInterface} = {}
grades.forEach(g => gradesByValue[g.value] = g)
const numVotes = Object.values(profile).reduce((a, b) => a + b, 0)
const values = grades.map(g => g.value).sort();
const normalized = values.map(value => value in profile ? profile[value] / numVotes : 0)
// low values means great grade
// find the majority grade
const majorityIdx = getMajorityGrade(profile)
const majorityGrade = grades[majorityIdx]
const proponentSizes = normalized.filter((_, i) => values[i] < majorityGrade.value)
const proponentWidth = proponentSizes.reduce((a, b) => a + b, 0)
const opponentSizes = normalized.filter((_, i) => values[i] > majorityGrade.value)
const opponentWidth = opponentSizes.reduce((a, b) => a + b, 0)
// is proponent higher than opposant?
const proponentMajority = proponentWidth > opponentWidth;
// for mobile phone, we outgauge earlier than on desktop
const innerWidth = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1000;
const params: ParamsInterface = {
outgaugeThreshold: (innerWidth <= 760) ? 0.05 : 0.03,
numVotes,
}
return (
<>
<DashedMedian />
<MajorityGrade
grade={majorityGrade}
left={proponentWidth + normalized[majorityIdx] / 2}
/>
<div className='d-flex'>
<div
className={`d-flex border border-${proponentMajority ? 2 : 1} border-success`}
style={{flexBasis: `${proponentWidth * 100}%`}}
>
{values.filter(v => v < majorityGrade.value).map(v => {
const index = values.indexOf(v);
const size = proponentWidth < 1e-3 ? 0 : normalized[index] / proponentWidth;
return (
<GradeBar index={index} params={params} grade={grades[v]} key={index} size={size} />
)
})}
</div>
<div className="border border-2 border-primary"
style={{flexBasis: `${normalized[majorityIdx] * 100}%`}}
>
{values.filter(v => v === majorityGrade.value).map(v => {
const index = values.indexOf(v);
return (
<GradeBar index={index} params={params} grade={grades[v]} key={index} size={1} />
)
})}
</div>
<div
className={`d-flex border border-${proponentMajority ? 1 : 2} border-danger`}
style={{flexBasis: `${opponentWidth * 100}%`}}
>
{values.filter(v => v > majorityGrade.value).map(v => {
const index = values.indexOf(v);
const size = opponentWidth < 1e-3 ? 0 : normalized[index] / opponentWidth;
return (
<GradeBar index={index} params={params} grade={grades[v]} key={index} size={size} />
)
})}
</div>
</div>
{ /* <div className='median dash'> </div> */}
</>
)
}
export default MeritProfileBar;

@ -11,7 +11,7 @@ import ErrorMessage from '@components/Error';
import AdminModalEmail from '@components/admin/AdminModalEmail';
import {BallotPayload, ErrorPayload} from '@services/api';
import {useAppContext} from '@services/context';
import {displayRef} from '@services/utils';
import {displayRef, isEnded} from '@services/utils';
import {RESULTS} from '@services/routes';
import Logo from './Logo';
import {FORM_FEEDBACK} from '@services/constants';
@ -29,11 +29,8 @@ export interface WaitingBallotInterface {
const ButtonResults = ({election}) => {
const {t} = useTranslation();
const dateEnd = new Date(election.date_end);
const now = new Date();
const isEnded = +dateEnd > +now;
if (!election.hideResults || isEnded) {
if (!election.hideResults || isEnded(election.date_end)) {
return (
<Link href={`${RESULTS}/${displayRef(election.ref)}`}>
<Button className="" icon={faArrowRight} position="right">

287
package-lock.json generated

@ -27,9 +27,10 @@
"eslint-config-next": "^13.0.0",
"framer-motion": "^7.6.4",
"i18next": "^22.0.6",
"next": "^13.0.0",
"next": "^13.0.5",
"next-i18next": "^12.1.0",
"react": "^18.2.0",
"react-csv": "^2.2.2",
"react-datepicker": "^4.8.0",
"react-dom": "^18.2.0",
"react-flags-select": "^2.2.3",
@ -749,9 +750,9 @@
}
},
"node_modules/@next/env": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.1.tgz",
"integrity": "sha512-gK60YoFae3s8qi5UgIzbvxOhsh5gKyEaiKH5+kLBUYXLlrPyWJR2xKBj2WqvHkO7wDX7/Hed3DAqjSpU4ijIvQ=="
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.5.tgz",
"integrity": "sha512-F3KLtiDrUslAZhTYTh8Zk5ZaavbYwLUn3NYPBnOjAXU8hWm0QVGVzKIOuURQ098ofRU4e9oglf3Sj9pFx5nI5w=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.0.1",
@ -762,9 +763,9 @@
}
},
"node_modules/@next/swc-android-arm-eabi": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.1.tgz",
"integrity": "sha512-M28QSbohZlNXNn//HY6lV2T3YaMzG58Jwr0YwOdVmOQv6i+7lu6xe3GqQu4kdqInqhLrBXnL+nabFuGTVSHtTg==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.5.tgz",
"integrity": "sha512-YO691dxHlviy6H0eghgwqn+5kU9J3iQnKERHTDSppqjjGDBl6ab4wz9XfI5AhljjkaTg3TknHoIEWFDoZ4Ve8g==",
"cpu": [
"arm"
],
@ -777,9 +778,9 @@
}
},
"node_modules/@next/swc-android-arm64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.1.tgz",
"integrity": "sha512-szmO/i6GoHcPXcbhUKhwBMETWHNXH3ITz9wfxwOOFBNKdDU8pjKsHL88lg28aOiQYZSU1sxu1v1p9KY5kJIZCg==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.5.tgz",
"integrity": "sha512-ugbwffkUmp8cd2afehDC8LtQeFUxElRUBBngfB5UYSWBx18HW4OgzkPFIY8jUBH16zifvGZWXbICXJWDHrOLtw==",
"cpu": [
"arm64"
],
@ -792,9 +793,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.1.tgz",
"integrity": "sha512-O1RxCaiDNOjGZmdAp6SQoHUITt9aVDQXoR3lZ/TloI/NKRAyAV4u0KUUofK+KaZeHOmVTnPUaQuCyZSc3i1x5Q==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.5.tgz",
"integrity": "sha512-mshlh8QOtOalfZbc17uNAftWgqHTKnrv6QUwBe+mpGz04eqsSUzVz1JGZEdIkmuDxOz00cK2NPoc+VHDXh99IQ==",
"cpu": [
"arm64"
],
@ -807,9 +808,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.1.tgz",
"integrity": "sha512-8E6BY/VO+QqQkthhoWgB8mJMw1NcN9Vhl2OwEwxv8jy2r3zjeU+WNRxz4y8RLbcY0R1h+vHlXuP0mLnuac84tQ==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.5.tgz",
"integrity": "sha512-SfigOKW4Z2UB3ruUPyvrlDIkcJq1hiw1wvYApWugD+tQsAkYZKEoz+/8emCmeYZ6Gwgi1WHV+z52Oj8u7bEHPg==",
"cpu": [
"x64"
],
@ -822,9 +823,9 @@
}
},
"node_modules/@next/swc-freebsd-x64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.1.tgz",
"integrity": "sha512-ocwoOxm2KVwF50RyoAT+2RQPLlkyoF7sAqzMUVgj+S6+DTkY3iwH+Zpo0XAk2pnqT9qguOrKnEpq9EIx//+K7Q==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.5.tgz",
"integrity": "sha512-0NJg8HZr4yG8ynmMGFXQf+Mahvq4ZgBmUwSlLXXymgxEQgH17erH/LoR69uITtW+KTsALgk9axEt5AAabM4ucg==",
"cpu": [
"x64"
],
@ -837,9 +838,9 @@
}
},
"node_modules/@next/swc-linux-arm-gnueabihf": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.1.tgz",
"integrity": "sha512-yO7e3zITfGol/N6lPQnmIRi0WyuILBMXrvH6EdmWzzqMDJFfTCII6l+B6gMO5WVDCTQUGQlQRNZ7sFqWR4I71g==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.5.tgz",
"integrity": "sha512-Cye+h3oDT3NDWjACMlRaolL8fokpKie34FlPj9nfoW7bYKmoMBY1d4IO/GgBF+5xEl7HkH0Ny/qex63vQ0pN+A==",
"cpu": [
"arm"
],
@ -852,9 +853,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.1.tgz",
"integrity": "sha512-OEs6WDPDI8RyM8SjOqTDMqMBfOlU97VnW6ZMXUvzUTyH0K9c7NF+cn7UMu+I4tKFN0uJ9WQs/6TYaFBGkgoVVA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.5.tgz",
"integrity": "sha512-5BfDS/VoRDR5QUGG9oedOCEZGmV2zxUVFYLUJVPMSMeIgqkjxWQBiG2BUHZI6/LGk9yvHmjx7BTvtBCLtRg6IQ==",
"cpu": [
"arm64"
],
@ -867,9 +868,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.1.tgz",
"integrity": "sha512-y5ypFK0Y3urZSFoQxbtDqvKsBx026sz+Fm+xHlPWlGHNZrbs3Q812iONjcZTo09QwRMk5X86iMWBRxV18xMhaw==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.5.tgz",
"integrity": "sha512-xenvqlXz+KxVKAB1YR723gnVNszpsCvKZkiFFaAYqDGJ502YuqU2fwLsaSm/ASRizNcBYeo9HPLTyc3r/9cdMQ==",
"cpu": [
"arm64"
],
@ -882,9 +883,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.1.tgz",
"integrity": "sha512-XDIHEE6SU8VCF+dUVntD6PDv6RK31N0forx9kucZBYirbe8vCZ+Yx8hYgvtIaGrTcWtGxibxmND0pIuHDq8H5g==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.5.tgz",
"integrity": "sha512-9Ahi1bbdXwhrWQmOyoTod23/hhK05da/FzodiNqd6drrMl1y7+RujoEcU8Dtw3H1mGWB+yuTlWo8B4Iba8hqiQ==",
"cpu": [
"x64"
],
@ -897,9 +898,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.1.tgz",
"integrity": "sha512-yxIOuuz5EOx0F1FDtsyzaLgnDym0Ysxv8CWeJyDTKKmt9BVyITg6q/cD+RP9bEkT1TQi+PYXIMATSz675Q82xw==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.5.tgz",
"integrity": "sha512-V+1mnh49qmS9fOZxVRbzjhBEz9IUGJ7AQ80JPWAYQM5LI4TxfdiF4APLPvJ52rOmNeTqnVz1bbKtVOso+7EZ4w==",
"cpu": [
"x64"
],
@ -912,9 +913,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.1.tgz",
"integrity": "sha512-+ucLe2qgQzP+FM94jD4ns6LDGyMFaX9k3lVHqu/tsQCy2giMymbport4y4p77mYcXEMlDaHMzlHgOQyHRniWFA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.5.tgz",
"integrity": "sha512-wRE9rkp7I+/3Jf2T9PFIJOKq3adMWYEFkPOA7XAkUfYbQHlDJm/U5cVCWUsKByyQq5RThwufI91sgd19MfxRxg==",
"cpu": [
"arm64"
],
@ -927,9 +928,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.1.tgz",
"integrity": "sha512-Krr/qGN7OB35oZuvMAZKoXDt2IapynIWLh5A5rz6AODb7f/ZJqyAuZSK12vOa2zKdobS36Qm4IlxxBqn9c00MA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.5.tgz",
"integrity": "sha512-Q1XQSLEhFuFhkKFdJIGt7cYQ4T3u6P5wrtUNreg5M+7P+fjSiC8+X+Vjcw+oebaacsdl0pWZlK+oACGafush1w==",
"cpu": [
"ia32"
],
@ -942,9 +943,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.1.tgz",
"integrity": "sha512-t/0G33t/6VGWZUGCOT7rG42qqvf/x+MrFp1CU+8CN6PrjSSL57R5bqkXfubV9t4eCEnUxVP+5Hn3MoEXEebtEw==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.5.tgz",
"integrity": "sha512-t5gRblrwwiNZP6cT7NkxlgxrFgHWtv9ei5vUraCLgBqzvIsa7X+PnarZUeQCXqz6Jg9JSGGT9j8lvzD97UqeJQ==",
"cpu": [
"x64"
],
@ -1049,9 +1050,9 @@
}
},
"node_modules/@swc/helpers": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz",
"integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==",
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
"dependencies": {
"tslib": "^2.4.0"
}
@ -3574,16 +3575,15 @@
"dev": true
},
"node_modules/next": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/next/-/next-13.0.1.tgz",
"integrity": "sha512-ErCNBPIeZMKFn6hX+ZBSlqZVgJIeitEqhGTuQUNmYXJ07/A71DZ7AJI8eyHYUdBb686LUpV1/oBdTq9RpzRVPg==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/next/-/next-13.0.5.tgz",
"integrity": "sha512-awpc3DkphyKydwCotcBnuKwh6hMqkT5xdiBK4OatJtOZurDPBYLP62jtM2be/4OunpmwIbsS0Eyv+ZGU97ciEg==",
"dependencies": {
"@next/env": "13.0.1",
"@swc/helpers": "0.4.11",
"@next/env": "13.0.5",
"@swc/helpers": "0.4.14",
"caniuse-lite": "^1.0.30001406",
"postcss": "8.4.14",
"styled-jsx": "5.1.0",
"use-sync-external-store": "1.2.0"
"styled-jsx": "5.1.0"
},
"bin": {
"next": "dist/bin/next"
@ -3592,19 +3592,19 @@
"node": ">=14.6.0"
},
"optionalDependencies": {
"@next/swc-android-arm-eabi": "13.0.1",
"@next/swc-android-arm64": "13.0.1",
"@next/swc-darwin-arm64": "13.0.1",
"@next/swc-darwin-x64": "13.0.1",
"@next/swc-freebsd-x64": "13.0.1",
"@next/swc-linux-arm-gnueabihf": "13.0.1",
"@next/swc-linux-arm64-gnu": "13.0.1",
"@next/swc-linux-arm64-musl": "13.0.1",
"@next/swc-linux-x64-gnu": "13.0.1",
"@next/swc-linux-x64-musl": "13.0.1",
"@next/swc-win32-arm64-msvc": "13.0.1",
"@next/swc-win32-ia32-msvc": "13.0.1",
"@next/swc-win32-x64-msvc": "13.0.1"
"@next/swc-android-arm-eabi": "13.0.5",
"@next/swc-android-arm64": "13.0.5",
"@next/swc-darwin-arm64": "13.0.5",
"@next/swc-darwin-x64": "13.0.5",
"@next/swc-freebsd-x64": "13.0.5",
"@next/swc-linux-arm-gnueabihf": "13.0.5",
"@next/swc-linux-arm64-gnu": "13.0.5",
"@next/swc-linux-arm64-musl": "13.0.5",
"@next/swc-linux-x64-gnu": "13.0.5",
"@next/swc-linux-x64-musl": "13.0.5",
"@next/swc-win32-arm64-msvc": "13.0.5",
"@next/swc-win32-ia32-msvc": "13.0.5",
"@next/swc-win32-x64-msvc": "13.0.5"
},
"peerDependencies": {
"fibers": ">= 3.1.0",
@ -4076,6 +4076,11 @@
"node": ">=0.10.0"
}
},
"node_modules/react-csv": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz",
"integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw=="
},
"node_modules/react-datepicker": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz",
@ -4774,14 +4779,6 @@
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"dev": true
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@ -5410,9 +5407,9 @@
}
},
"@next/env": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.1.tgz",
"integrity": "sha512-gK60YoFae3s8qi5UgIzbvxOhsh5gKyEaiKH5+kLBUYXLlrPyWJR2xKBj2WqvHkO7wDX7/Hed3DAqjSpU4ijIvQ=="
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.0.5.tgz",
"integrity": "sha512-F3KLtiDrUslAZhTYTh8Zk5ZaavbYwLUn3NYPBnOjAXU8hWm0QVGVzKIOuURQ098ofRU4e9oglf3Sj9pFx5nI5w=="
},
"@next/eslint-plugin-next": {
"version": "13.0.1",
@ -5423,81 +5420,81 @@
}
},
"@next/swc-android-arm-eabi": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.1.tgz",
"integrity": "sha512-M28QSbohZlNXNn//HY6lV2T3YaMzG58Jwr0YwOdVmOQv6i+7lu6xe3GqQu4kdqInqhLrBXnL+nabFuGTVSHtTg==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.5.tgz",
"integrity": "sha512-YO691dxHlviy6H0eghgwqn+5kU9J3iQnKERHTDSppqjjGDBl6ab4wz9XfI5AhljjkaTg3TknHoIEWFDoZ4Ve8g==",
"optional": true
},
"@next/swc-android-arm64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.1.tgz",
"integrity": "sha512-szmO/i6GoHcPXcbhUKhwBMETWHNXH3ITz9wfxwOOFBNKdDU8pjKsHL88lg28aOiQYZSU1sxu1v1p9KY5kJIZCg==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.0.5.tgz",
"integrity": "sha512-ugbwffkUmp8cd2afehDC8LtQeFUxElRUBBngfB5UYSWBx18HW4OgzkPFIY8jUBH16zifvGZWXbICXJWDHrOLtw==",
"optional": true
},
"@next/swc-darwin-arm64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.1.tgz",
"integrity": "sha512-O1RxCaiDNOjGZmdAp6SQoHUITt9aVDQXoR3lZ/TloI/NKRAyAV4u0KUUofK+KaZeHOmVTnPUaQuCyZSc3i1x5Q==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.5.tgz",
"integrity": "sha512-mshlh8QOtOalfZbc17uNAftWgqHTKnrv6QUwBe+mpGz04eqsSUzVz1JGZEdIkmuDxOz00cK2NPoc+VHDXh99IQ==",
"optional": true
},
"@next/swc-darwin-x64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.1.tgz",
"integrity": "sha512-8E6BY/VO+QqQkthhoWgB8mJMw1NcN9Vhl2OwEwxv8jy2r3zjeU+WNRxz4y8RLbcY0R1h+vHlXuP0mLnuac84tQ==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.5.tgz",
"integrity": "sha512-SfigOKW4Z2UB3ruUPyvrlDIkcJq1hiw1wvYApWugD+tQsAkYZKEoz+/8emCmeYZ6Gwgi1WHV+z52Oj8u7bEHPg==",
"optional": true
},
"@next/swc-freebsd-x64": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.1.tgz",
"integrity": "sha512-ocwoOxm2KVwF50RyoAT+2RQPLlkyoF7sAqzMUVgj+S6+DTkY3iwH+Zpo0XAk2pnqT9qguOrKnEpq9EIx//+K7Q==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.5.tgz",
"integrity": "sha512-0NJg8HZr4yG8ynmMGFXQf+Mahvq4ZgBmUwSlLXXymgxEQgH17erH/LoR69uITtW+KTsALgk9axEt5AAabM4ucg==",
"optional": true
},
"@next/swc-linux-arm-gnueabihf": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.1.tgz",
"integrity": "sha512-yO7e3zITfGol/N6lPQnmIRi0WyuILBMXrvH6EdmWzzqMDJFfTCII6l+B6gMO5WVDCTQUGQlQRNZ7sFqWR4I71g==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.5.tgz",
"integrity": "sha512-Cye+h3oDT3NDWjACMlRaolL8fokpKie34FlPj9nfoW7bYKmoMBY1d4IO/GgBF+5xEl7HkH0Ny/qex63vQ0pN+A==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.1.tgz",
"integrity": "sha512-OEs6WDPDI8RyM8SjOqTDMqMBfOlU97VnW6ZMXUvzUTyH0K9c7NF+cn7UMu+I4tKFN0uJ9WQs/6TYaFBGkgoVVA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.5.tgz",
"integrity": "sha512-5BfDS/VoRDR5QUGG9oedOCEZGmV2zxUVFYLUJVPMSMeIgqkjxWQBiG2BUHZI6/LGk9yvHmjx7BTvtBCLtRg6IQ==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.1.tgz",
"integrity": "sha512-y5ypFK0Y3urZSFoQxbtDqvKsBx026sz+Fm+xHlPWlGHNZrbs3Q812iONjcZTo09QwRMk5X86iMWBRxV18xMhaw==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.5.tgz",
"integrity": "sha512-xenvqlXz+KxVKAB1YR723gnVNszpsCvKZkiFFaAYqDGJ502YuqU2fwLsaSm/ASRizNcBYeo9HPLTyc3r/9cdMQ==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.1.tgz",
"integrity": "sha512-XDIHEE6SU8VCF+dUVntD6PDv6RK31N0forx9kucZBYirbe8vCZ+Yx8hYgvtIaGrTcWtGxibxmND0pIuHDq8H5g==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.5.tgz",
"integrity": "sha512-9Ahi1bbdXwhrWQmOyoTod23/hhK05da/FzodiNqd6drrMl1y7+RujoEcU8Dtw3H1mGWB+yuTlWo8B4Iba8hqiQ==",
"optional": true
},
"@next/swc-linux-x64-musl": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.1.tgz",
"integrity": "sha512-yxIOuuz5EOx0F1FDtsyzaLgnDym0Ysxv8CWeJyDTKKmt9BVyITg6q/cD+RP9bEkT1TQi+PYXIMATSz675Q82xw==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.5.tgz",
"integrity": "sha512-V+1mnh49qmS9fOZxVRbzjhBEz9IUGJ7AQ80JPWAYQM5LI4TxfdiF4APLPvJ52rOmNeTqnVz1bbKtVOso+7EZ4w==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.1.tgz",
"integrity": "sha512-+ucLe2qgQzP+FM94jD4ns6LDGyMFaX9k3lVHqu/tsQCy2giMymbport4y4p77mYcXEMlDaHMzlHgOQyHRniWFA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.5.tgz",
"integrity": "sha512-wRE9rkp7I+/3Jf2T9PFIJOKq3adMWYEFkPOA7XAkUfYbQHlDJm/U5cVCWUsKByyQq5RThwufI91sgd19MfxRxg==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.1.tgz",
"integrity": "sha512-Krr/qGN7OB35oZuvMAZKoXDt2IapynIWLh5A5rz6AODb7f/ZJqyAuZSK12vOa2zKdobS36Qm4IlxxBqn9c00MA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.5.tgz",
"integrity": "sha512-Q1XQSLEhFuFhkKFdJIGt7cYQ4T3u6P5wrtUNreg5M+7P+fjSiC8+X+Vjcw+oebaacsdl0pWZlK+oACGafush1w==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.1.tgz",
"integrity": "sha512-t/0G33t/6VGWZUGCOT7rG42qqvf/x+MrFp1CU+8CN6PrjSSL57R5bqkXfubV9t4eCEnUxVP+5Hn3MoEXEebtEw==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.5.tgz",
"integrity": "sha512-t5gRblrwwiNZP6cT7NkxlgxrFgHWtv9ei5vUraCLgBqzvIsa7X+PnarZUeQCXqz6Jg9JSGGT9j8lvzD97UqeJQ==",
"optional": true
},
"@nicolo-ribaudo/eslint-scope-5-internals": {
@ -5571,9 +5568,9 @@
}
},
"@swc/helpers": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz",
"integrity": "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==",
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
"requires": {
"tslib": "^2.4.0"
}
@ -7387,29 +7384,28 @@
"dev": true
},
"next": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/next/-/next-13.0.1.tgz",
"integrity": "sha512-ErCNBPIeZMKFn6hX+ZBSlqZVgJIeitEqhGTuQUNmYXJ07/A71DZ7AJI8eyHYUdBb686LUpV1/oBdTq9RpzRVPg==",
"requires": {
"@next/env": "13.0.1",
"@next/swc-android-arm-eabi": "13.0.1",
"@next/swc-android-arm64": "13.0.1",
"@next/swc-darwin-arm64": "13.0.1",
"@next/swc-darwin-x64": "13.0.1",
"@next/swc-freebsd-x64": "13.0.1",
"@next/swc-linux-arm-gnueabihf": "13.0.1",
"@next/swc-linux-arm64-gnu": "13.0.1",
"@next/swc-linux-arm64-musl": "13.0.1",
"@next/swc-linux-x64-gnu": "13.0.1",
"@next/swc-linux-x64-musl": "13.0.1",
"@next/swc-win32-arm64-msvc": "13.0.1",
"@next/swc-win32-ia32-msvc": "13.0.1",
"@next/swc-win32-x64-msvc": "13.0.1",
"@swc/helpers": "0.4.11",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/next/-/next-13.0.5.tgz",
"integrity": "sha512-awpc3DkphyKydwCotcBnuKwh6hMqkT5xdiBK4OatJtOZurDPBYLP62jtM2be/4OunpmwIbsS0Eyv+ZGU97ciEg==",
"requires": {
"@next/env": "13.0.5",
"@next/swc-android-arm-eabi": "13.0.5",
"@next/swc-android-arm64": "13.0.5",
"@next/swc-darwin-arm64": "13.0.5",
"@next/swc-darwin-x64": "13.0.5",
"@next/swc-freebsd-x64": "13.0.5",
"@next/swc-linux-arm-gnueabihf": "13.0.5",
"@next/swc-linux-arm64-gnu": "13.0.5",
"@next/swc-linux-arm64-musl": "13.0.5",
"@next/swc-linux-x64-gnu": "13.0.5",
"@next/swc-linux-x64-musl": "13.0.5",
"@next/swc-win32-arm64-msvc": "13.0.5",
"@next/swc-win32-ia32-msvc": "13.0.5",
"@next/swc-win32-x64-msvc": "13.0.5",
"@swc/helpers": "0.4.14",
"caniuse-lite": "^1.0.30001406",
"postcss": "8.4.14",
"styled-jsx": "5.1.0",
"use-sync-external-store": "1.2.0"
"styled-jsx": "5.1.0"
}
},
"next-i18next": {
@ -7703,6 +7699,11 @@
"loose-envify": "^1.1.0"
}
},
"react-csv": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz",
"integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw=="
},
"react-datepicker": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz",
@ -8162,12 +8163,6 @@
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"dev": true
},
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

@ -29,9 +29,10 @@
"eslint-config-next": "^13.0.0",
"framer-motion": "^7.6.4",
"i18next": "^22.0.6",
"next": "^13.0.0",
"next": "^13.0.5",
"next-i18next": "^12.1.0",
"react": "^18.2.0",
"react-csv": "^2.2.2",
"react-datepicker": "^4.8.0",
"react-dom": "^18.2.0",
"react-flags-select": "^2.2.3",

@ -1,11 +1,11 @@
import { Html, Head, Main, NextScript } from 'next/document';
import {Html, Head, Main, NextScript} from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin={true} />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"

@ -11,6 +11,7 @@ import Blur from '@components/Blur'
import {getElection, castBallot, ElectionPayload, BallotPayload, ErrorPayload} from '@services/api';
import {useBallot, BallotTypes, BallotProvider} from '@services/BallotContext';
import {ENDED_VOTE} from '@services/routes';
import {isEnded} from '@services/utils';
import WaitingBallot from '@components/WaitingBallot';
import PatternedBackground from '@components/PatternedBackground';
@ -21,7 +22,7 @@ export async function getServerSideProps({query: {pid, tid}, locale}) {
if (!pid) {
return {notFound: true}
}
const electionRef = pid.replace("-", "");
const electionRef = pid.replaceAll("-", "");
const [election, translations] = await Promise.all([
getElection(electionRef),
@ -32,11 +33,12 @@ export async function getServerSideProps({query: {pid, tid}, locale}) {
return {notFound: true}
}
const dateEnd = new Date(election.date_end)
if (dateEnd.getDate() > new Date().getDate()) {
if (isEnded(election.date_end)) {
return {
redirect: ENDED_VOTE,
permanent: false
redirect: {
destination: `${ENDED_VOTE}/${pid}/${tid || ""}`,
permanent: false
}
}
}

@ -0,0 +1,50 @@
import Link from 'next/link'
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
import {Container} from 'reactstrap';
import ErrorMessage from '@components/Error';
import {RESULTS} from '@services/routes'
import {displayRef} from '@services/utils'
import Blur from '@components/Blur'
import Button from '@components/Button';
export const getServerSideProps = async ({query: {pid, tid}, locale}) => {
return {
props: {
...(await serverSideTranslations(locale, ['resource'])),
token: tid,
electionRef: pid.replaceAll("-", "")
},
}
}
const End = ({electionRef, token}) => {
const {t} = useTranslation();
return (
<>
<Blur />
<div className="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<ErrorMessage msg={t('error.ended-election')} />
<Container className="full-height-container">
<Link
className="d-grid w-100 mt-5"
href={`${RESULTS}/${displayRef(electionRef)}/${token ? token : ""}`}>
<Button
color="secondary"
outline={true}
type="submit"
icon={faArrowRight}
position="right"
>
{t('vote.go-to-results')}</Button>
</Link>
</Container>
</div>
</>
);
};
export default End;

@ -1,18 +1,17 @@
import Link from 'next/link';
import { Container, Row, Col } from 'reactstrap';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import config from '../next-i18next.config.js';
import { GetStaticProps } from 'next';
import {Container, Row, Col} from 'reactstrap';
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import {GetStaticProps} from 'next';
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
export const getStaticProps: GetStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
...(await serverSideTranslations(locale, ['resource'])),
},
});
const FAQ = () => {
const { t } = useTranslation();
const {t} = useTranslation();
return (
<Container>
<Row>
@ -37,7 +36,7 @@ const FAQ = () => {
lélectorat (celui qui obtient la meilleure mention « majoritaire
»).
</p>
<div style={{ maxWidth: '445px' }}>
<div style={{maxWidth: '445px'}}>
<video width="100%" height="250" controls={true}>
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.mp4"

@ -1,18 +1,17 @@
import Link from 'next/link';
import { Container, Row, Col } from 'reactstrap';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import config from '../next-i18next.config.js';
import { GetStaticProps } from 'next';
import {Container, Row, Col} from 'reactstrap';
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import {GetStaticProps} from 'next';
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
export const getStaticProps: GetStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
...(await serverSideTranslations(locale, ['resource'])),
},
});
const LegalNotices = (props) => {
const { t } = useTranslation();
const {t} = useTranslation();
return (
<Container>
<Row>

@ -1,18 +1,17 @@
import { Col, Container, Row } from 'reactstrap';
import { useTranslation } from 'next-i18next';
import {Col, Container, Row} from 'reactstrap';
import {useTranslation} from 'next-i18next';
import Link from 'next/link';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import config from '../next-i18next.config.js';
import { GetStaticProps } from 'next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import {GetStaticProps} from 'next';
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
export const getStaticProps: GetStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
...(await serverSideTranslations(locale, ['resource'])),
},
});
const PrivacyPolicy = (props) => {
const { t } = useTranslation();
const {t} = useTranslation();
return (
<Container>
<Row>

@ -1,25 +0,0 @@
import dynamic from 'next/dynamic';
// import Plot from 'react-plotly.js';
import React, { Component } from 'react';
class BarChart extends Component {
render() {
return <div></div>;
}
}
export default BarChart;
// <Plot
// data={[
// {
// type: 'bar',
// x: ['Taubira', 'Hidalgo', 'Mélenchon'],
// y: [29, 150, 85]
// }
// ]}
// layout={{width: 1000, height: 500, title: 'Nombre de voix par candidat'}}
// config={{
// displayModeBar: false // this is the line that hides the bar.
// }}
// />

@ -23,10 +23,15 @@ import {
faChevronUp,
faGear,
} from '@fortawesome/free-solid-svg-icons';
// import dynamic from 'next/dynamic'
import ErrorMessage from '@components/Error';
import CSVLink from '@components/CSVLink';
import Logo from '@components/Logo';
import {getResults, getElection, apiErrors, ResultsPayload, CandidatePayload, GradePayload} from '@services/api';
import MeritProfile from '@components/MeritProfile';
import {getResults} from '@services/api';
import {GradeResultInterface, ResultInterface, MeritProfileInterface, CandidateResultInterface} from '@services/type';
import {getUrlAdmin} from '@services/routes';
import {displayRef} from '@services/utils';
import {getMajorityGrade} from '@services/majorityJudgment';
import avatarBlue from '../../../public/avatarBlue.svg'
import calendar from '../../../public/calendar.svg'
@ -35,50 +40,38 @@ import arrowLink from '../../../public/arrowL.svg'
import {getGradeColor} from '@services/grades';
interface GradeInterface extends GradePayload {
color: string;
}
interface CandidateInterface extends CandidatePayload {
majorityGrade: GradeInterface;
rank: number;
}
interface ElectionInterface {
name: string;
description: string;
ref: string;
dateStart: string;
dateEnd: string;
hideResults: boolean;
forceClose: boolean;
restricted: boolean;
grades: Array<GradeInterface>;
candidates: Array<CandidateInterface>;
}
// /**
// * See https://github.com/react-csv/react-csv/issues/87
// */
// const CSVDownload = dynamic(
// import('react-csv').then((m) => {
// const {
// CSVDownload
// } = m
// return CSVDownload
// }), {
// ssr: false,
// loading: () => <a>placeholder component...</a>
// })
interface ResultInterface extends ElectionInterface {
ranking: {[key: string]: number};
meritProfile: {[key: number]: Array<number>};
}
export async function getServerSideProps({query, locale}) {
const {pid, tid: token} = query;
const electionRef = pid.replace("-", "");
const electionRef = pid.replaceAll("-", "");
const [payload, translations] = await Promise.all([
getResults(electionRef),
serverSideTranslations(locale, ["resource"]),
]);
if (typeof payload === 'string' || payload instanceof String) {
return {props: {err: payload, ...translations}};
if ("msg" in payload) {
return {props: {err: payload.msg, ...translations}};
}
const numGrades = payload.grades.length;
const grades = payload.grades.map((g, i) => ({...g, color: getGradeColor(i, numGrades)}));
const gradesByValue: {[key: number]: GradeInterface} = {}
const gradesByValue: {[key: number]: GradeResultInterface} = {}
grades.forEach(g => gradesByValue[g.value] = g)
const result: ResultInterface = {
@ -93,13 +86,15 @@ export async function getServerSideProps({query, locale}) {
grades: grades,
candidates: payload.candidates.map(c => ({
...c,
rank: payload.ranking[c.id],
meritProfile: payload.merit_profile[c.id],
rank: payload.ranking[c.id] + 1,
majorityGrade: gradesByValue[getMajorityGrade(payload.merit_profile[c.id])]
})),
ranking: payload.ranking,
meritProfile: payload.merit_profile,
meritProfiles: payload.merit_profile,
}
console.log("GRADES", payload.grades, grades, result.grades)
return {
props: {
@ -112,11 +107,11 @@ export async function getServerSideProps({query, locale}) {
const getNumVotes = (result: ResultInterface) => {
const sum = (seq: Array<number>) =>
const sum = (seq: MeritProfileInterface) =>
Object.values(seq).reduce((a, b) => a + b, 0);
const anyCandidateId = result.candidates[0].id;
const numVotes = sum(result.meritProfile[anyCandidateId]);
Object.values(result.meritProfile).forEach(v => {
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")
}
@ -124,41 +119,54 @@ const getNumVotes = (result: ResultInterface) => {
return numVotes;
}
const WillClose = ({delay}) => {
const {t} = useTranslation();
if (delay < 365) {
return <div>{t('result.closed')}</div>
}
else if (delay < 0) {
return <div>{`${t('result.has-closed')} ${delay} ${t('common.days')}`}</div>
} else if (delay > 365) {
return <div>{t('result.opened')}</div>
} else {
return <div>{`${t('result.will-close')} ${delay} ${t('common.days')}`}</div>
}
}
interface ResultBanner {
result: ResultsPayload;
result: ResultInterface;
}
const ResultBanner = ({result}) => {
const {t} = useTranslation();
const dateEnd = new Date(result.date_end);
const dateEnd = new Date(result.dateEnd);
const now = new Date();
const closedSince = +dateEnd - (+now);
const numVotes = getNumVotes(result)
return (<div className="w-100 bg-white p-5 justify-content-between align-items-center">
return (<div className="w-100 bg-white p-5 d-flex justify-content-between align-items-center">
<div className="text-muted">
<div className="d-flex align-items-center">
<Image alt="Calendar" src={calendar} />
<p>{closedSince > 0 ? `${t('result.has-closed')} {closedSince}` : `${t('result.will-close')} {closedSince}`} {t('common.days')}</p>
<Image alt="Calendar" src={calendar} className="me-2" />
<WillClose delay={closedSince} />
</div>
<div className="d-flex align-items-center">
<Image src={avatarBlue} alt="Avatar" />
<p>{`${numVotes} ${t('common.participants')}`}</p>
<div className="d-flex align-items-center" >
<Image src={avatarBlue} alt="Avatar" className="me-2" />
<div>{numVotes} {numVotes > 1 ? t('common.participants') : t('common.participant')}</div>
</div>
</div>
<h3>{result.name}</h3>
<h4 className="text-black">{result.name}</h4>
<div className="text-muted">
<div className="d-flex align-items-center">
<Image alt="Download" src={arrowUpload} />
<p>{t('result.download')}</p>
<Image alt="Download" src={arrowUpload} className="me-2" />
<div>{t('result.download')}</div>
</div>
<div className="d-flex align-items-center">
<Image src={arrowLink} alt="Share" />
<p>{t('result.share')}</p>
<Image src={arrowLink} alt="Share" className="me-2" />
<div>{t('result.share')}</div>
</div>
</div>
</div >
@ -166,14 +174,28 @@ const ResultBanner = ({result}) => {
}
const BottomButtonsMobile = () => {
const BottomButtonsMobile = ({result}) => {
const {t} = useTranslation();
const values = result.grades.map(v => v.value).sort()
// const data = result.candidates.map(c => [c.name]);
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}
});
console.log(data)
return (
<div className="d-block d-md-none mt-5">
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary">
<Image alt="Download" src={arrowUpload} />
<p>{t('result.download')}</p>
</Button>
<div className="d-block d-md-none mt-5" role="button">
<CSVLink
filename={`results-${displayRef(result.ref)}.csv`}
data={data}>
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary">
<Image alt="Download" src={arrowUpload} />
<p>{t('result.download')}</p>
</Button>
</CSVLink>
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary">
<Image src={arrowLink} alt="Share" />
<p>{t('result.share')}</p>
@ -210,11 +232,11 @@ const TitleBanner = ({name, electionRef, token}: TitleBannerInterface) => {
}
interface ButtonGradeInterface {
grade: GradeInterface;
interface ButtonGradeResultInterface {
grade: GradeResultInterface;
}
const ButtonGrade = ({grade}: ButtonGradeInterface) => {
const ButtonGrade = ({grade}: ButtonGradeResultInterface) => {
const style = {
color: 'white',
@ -224,7 +246,7 @@ const ButtonGrade = ({grade}: ButtonGradeInterface) => {
return (
<div
style={style}
className="py-2 px-3 m-1 fw-bold rounded-1 d-flex justify-content-between gap-3"
className="p-2 fw-bold rounded-1 d-flex justify-content-between gap-3"
>
{grade.name}
</div>
@ -232,25 +254,78 @@ const ButtonGrade = ({grade}: ButtonGradeInterface) => {
};
interface CandidateRankedInterface {
candidate: CandidateInterface;
candidate: CandidateResultInterface;
}
const CandidateRanked = ({candidate}: CandidateRankedInterface) => {
const isFirst = candidate.rank == 1;
return <div>
<div className={isFirst ? "text-primary bg-white fs-5" : "text-white bg-secondary fs-6"}>
return <div className="m-3 d-flex flex-column justify-content-end align-items-center candidate_rank fw-bold">
<div className={isFirst ? "text-primary bg-white fs-5 badge" : "text-white bg-secondary fs-6 badge"}>
{candidate.rank}
</div>
<div className={`text-white ${isFirst ? "fs-4" : "fs-6"}`}>
<div className={`text-white my-2 ${isFirst ? "fs-4" : "fs-6"}`}>
{candidate.name}
</div>
<ButtonGrade grade={candidate.majorityGrade} />
</div>
}
interface CandidateCardInterface {
candidate: CandidateResultInterface;
grades: Array<GradeResultInterface>;
}
const CandidateCard = ({candidate, grades}: CandidateCardInterface) => {
const {t} = useTranslation();
const [collapse, setCollapse] = useState(true);
return (
<Card className="bg-light text-primary my-3">
<CardHeader
role="button"
className="p-3 d-flex justify-content-between"
onClick={() => setCollapse(s => !s)}
>
<div className=" align-items-center d-flex">
<span className="resultPositionCard me-2">{candidate.rank}</span>
<span className="candidateName">
{candidate.name}
</span>
</div>
<div className="d-flex align-items-center">
<ButtonGrade grade={candidate.majorityGrade} />
<FontAwesomeIcon
icon={collapse ? faChevronDown : faChevronUp}
className="ms-2 text-black-50"
size="xl"
/>
</div>
</CardHeader>
<Collapse isOpen={!collapse} >
<CardBody className="p-3 text-dark">
{t('result.merit-profile')}
<MeritProfile profile={candidate.meritProfile} grades={grades} />
<a
href="https://mieuxvoter.fr/le-jugement-majoritaire"
target="_blank"
rel="noopener noreferrer"
className="d-flex w-100 align-items-center justify-content-center mt-5 text-black-50 fs-5"
>
<div>{t('result.how-to-interpret')}</div>
<FontAwesomeIcon
icon={faChevronRight}
className="ms-3"
/>
</a>
</CardBody>
</Collapse>
</Card >
)
};
interface PodiumInterface {
candidates: Array<CandidateInterface>;
candidates: Array<CandidateResultInterface>;
}
@ -259,32 +334,35 @@ const Podium = ({candidates}: PodiumInterface) => {
// get best candidates
const numBest = Math.min(3, candidates.length);
const candidatesByRank = {}
candidates.forEach(c => candidatesByRank[c.rank] = c)
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 (<div>
<CandidateRanked candidate={candidates[0]} />
<CandidateRanked candidate={candidates[1]} />
return (<div className="d-md-flex my-5 justify-content-center d-none">
<CandidateRanked candidate={candidateByRank[1]} />
<CandidateRanked candidate={candidateByRank[2]} />
</div>)
}
return (<div>
<CandidateRanked candidate={candidates[1]} />
<CandidateRanked candidate={candidates[0]} />
<CandidateRanked candidate={candidates[2]} />
return (<div className="d-md-flex my-5 d-none justify-content-center">
<CandidateRanked candidate={candidateByRank[2]} />
<CandidateRanked candidate={candidateByRank[1]} />
<CandidateRanked candidate={candidateByRank[3]} />
</div>)
}
interface ErrorInterface {
message: string;
}
interface ResultPageInterface {
result?: ResultInterface;
token?: string;
err?: string;
err?: ErrorInterface;
}
@ -292,8 +370,9 @@ const ResultPage = ({result, token, err}: ResultPageInterface) => {
const {t} = useTranslation();
const router = useRouter();
if (err && err !== '') {
return <ErrorMessage msg={err} />;
if (err && err.message !== '') {
return <ErrorMessage msg={err.message} />;
}
if (!result) {
@ -305,11 +384,8 @@ const ResultPage = ({result, token, err}: ResultPageInterface) => {
throw Error("No candidates were loaded in this election")
}
// const collapsee = result.candidates[0].name;
// const [collapseProfiles, setCollapseProfiles] = useState(false);
// const [collapseGraphics, setCollapseGraphics] = useState(false);
const numVotes = getNumVotes(result)
const candidateByRank = {}
result.candidates.filter(c => c.rank < 4).forEach(c => candidateByRank[c.rank] = c)
return (
<Container className="resultContainer resultPage">
@ -324,143 +400,20 @@ const ResultPage = ({result, token, err}: ResultPageInterface) => {
<Podium candidates={result.candidates} />
<section className="sectionContentResult mb-5">
<Row className="mt-5 componentDesktop">
<Col>
<ol className="result px-0">
{result.candidates.map((candidate, i) => {
return (
<li key={i} className="mt-2">
<span className="resultPosition">{i + 1}</span>
<span className="my-3">{candidate.name}</span>
<span
className="badge badge-light"
style={{
backgroundColor: candidate.majorityGrade.color,
color: '#fff',
}}
>
{candidate.majorityGrade.name}
</span>
</li>
);
})}
</ol>
</Col>
</Row>
<Row className="mt-5">
<Col>
<h5>
<small>{t('Détails des résultats')}</small>
<h5 className="text-white">
{t('result.details')}
</h5>
{result.candidates.map((candidate, i) => {
{Object.keys(candidateByRank).sort().map((rank, i) => {
return (
<Card className="bg-light text-primary my-3">
<CardHeader
className="pointer"
>
{/*onClick={() => setCollapseGraphics(!collapseGraphics)}*/}
<h4
>
{/* className={'m-0 ' + (collapseGraphics ? 'collapsed' : '')}*/}
<span
key={i}
className="d-flex panel-title justify-content-between"
>
<div className="d-flex">
<span className="resultPositionCard ">{i + 1}</span>
<span className="candidateName">
{candidate.name}
</span>
</div>
<div>
<span
className="badge badge-light"
style={{
backgroundColor: candidate.majorityGrade.color,
color: '#fff',
}}
>
{candidate.majorityGrade.name}
</span>
<FontAwesomeIcon
icon={faChevronDown}
className="openIcon"
/>
<FontAwesomeIcon
icon={faChevronUp}
className="closeIcon"
/>
</div>
</span>
</h4>
</CardHeader>
<Collapse
>
{/*isOpen={collapseGraphics}*/}
<CardBody className="pt-5">
<Row className="column">
<Col>
{t('Preference profile')}
<div>
<div
className="median"
style={{height: '40px'}}
/>
<div style={{width: '100%'}}>
<div key={i}>
<div style={{width: '100%'}}>
{/* gradeIds
.slice(0)
.reverse()
.map((id, i) => {
const value = candidate.profile[id];
if (value > 0) {
let percent =
(value * 100) / numVotes + '%';
if (i === 0) {
percent = 'auto';
}
return (
<div
key={i}
style={{
width: percent,
backgroundColor: grades[i].color,
}}
>
&nbsp;
</div>
);
} else {
return null;
}
})*/}
</div>
</div>
</div>
</div>
</Col>
</Row>
<Row className="linkResult my-3">
<Link href="/" className="mx-auto">
{t('Comment interpréter les résultats')}
<FontAwesomeIcon
icon={faChevronRight}
className="closeIcon"
/>
</Link>
</Row>
</CardBody>
</Collapse>
</Card>
<CandidateCard candidate={candidateByRank[rank]} grades={result.grades} key={i} />
);
})}
</Col>
</Row>
<BottomButtonsMobile />
<BottomButtonsMobile result={result} />
</section>
</Container>
);

@ -15,13 +15,14 @@ import AdvantagesRow from '@components/Advantages';
import Logo from '@components/Logo';
import {BALLOT} from '@services/routes';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
import {displayRef} from '@services/utils';
export async function getServerSideProps({query: {pid, tid}, locale}) {
return {
props: {
...(await serverSideTranslations(locale, ['resource'])),
electionRef: pid.replace("-", ""),
electionRef: pid.replaceAll("-", ""),
token: tid || null,
},
}
@ -52,7 +53,7 @@ const GoToBallotConfirm = ({electionRef, token}) => {
</Row>
<Row>
<Link href={`${BALLOT}/${electionRef}/${token ? token : ""}`}>
<Link href={`${BALLOT}/${displayRef(electionRef)}/${token ? token : ""}`}>
<Button
color="secondary"
outline={true}

@ -39,6 +39,7 @@
"common.thumbnail": "Thumbnail",
"common.name": "Name",
"common.participants": "participants",
"common.participant": "participant",
"common.description": "Description",
"common.cancel": "Cancel",
"common.grades": "Grades",
@ -50,6 +51,7 @@
"common.the-params": "The parameters",
"common.welcome": "Welcome!",
"error.at-least-2-candidates": "At least two candidates are required.",
"error.ended-election": "The election has ended",
"error.catch22": "Unknown error...",
"error.help": "Ask for our help",
"error.no-title": "Please add a title to your election.",
@ -111,8 +113,13 @@
"admin.go-to-admin": "Manage the vote",
"result.download": "Download results",
"result.go-to-admin": "Manage the election",
"result.merit-profile": "Merit profile of the candidate",
"result.has-closed": "Closed since",
"result.opened": "The election is opened",
"result.closed": "The election is closed",
"result.result": "Results of the election",
"result.details": "Details of the results",
"result.how-to-interpret": "How to understand the results",
"result.will-close": "Will close in",
"result.share": "Share results",
"vote.discover-mj": "Discover majority judgment",

@ -39,6 +39,7 @@
"common.thumbnail": "Image miniature",
"common.name": "Nom",
"common.participants": "participants",
"common.participant": "participant",
"common.description": "Description",
"common.cancel": "Annuler",
"common.grades": "Mentions",
@ -52,6 +53,7 @@
"error.help": "Besoin d'aide ?",
"error.at-least-2-candidates": "Ajoutez au moins deux candidats.",
"error.no-title": "Ajoutez un titre à l'élection.",
"error.ended-election": "L'élection est terminée",
"error.catch22": "Erreur inconnue...",
"grades.very-good": "Très bien",
"grades.good": "Bien",
@ -111,10 +113,15 @@
"admin.go-to-admin": "Administrez le vote",
"result.download": "Télécharger les résultats",
"result.go-to-admin": "Administrer le vote",
"result.has-closed": "Closed since",
"result.has-closed": "Terminée depuis",
"result.closed": "L'élection est terminée",
"result.opened": "L'élection est en cours",
"result.result": "Résultat du vote",
"result.details": "Détails des résultats",
"result.merit-profile": "Profil de mérite du candidat",
"result.share": "Partager les résultats",
"result.will-close": "Will close in",
"result.will-close": "Se termine dans",
"result.how-to-interpret": "Comment interpréter les résultats",
"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",

@ -83,7 +83,7 @@ export const getResults = async (
pid: string,
successCallback = null,
failureCallback = null
): Promise<ResultsPayload | string> => {
): Promise<ResultsPayload | HTTPPayload> => {
/**
* Fetch results from external API
*/
@ -96,9 +96,11 @@ export const getResults = async (
try {
const response = await fetch(endpoint.href)
if (response.status != 200) {
return response.text();
const payload = await response.json();
return {status: response.status, msg: payload};
}
return response.json();
const payload = await response.json()
return {...payload, status: response.status};
} catch (error) {
return new Promise(() => "API errors")
}
@ -230,6 +232,11 @@ export interface ErrorPayload {
detail: Array<ErrorMessage>;
}
export interface HTTPPayload {
status: number;
msg: string;
}
export interface ElectionPayload {
name: string;
description: string;
@ -251,6 +258,7 @@ export interface ElectionCreatedPayload extends ElectionPayload {
export interface ResultsPayload extends ElectionPayload {
status: number;
ranking: {[key: string]: number};
merit_profile: {[key: number]: Array<number>};
}

@ -2,29 +2,31 @@
* A few useful function for dealing with majority judgment
*/
import {MeritProfileInterface} from "./type";
/**
* Return the index corresponding to the majority grade
*/
export const getMajorityGrade = (votes: Array<number>): number => {
const indices = votes.map((_, i) => i);
const numVotes = votes.reduce((a, b) => a + b, 0)
export const getMajorityGrade = (profile: MeritProfileInterface): number => {
const indices = Object.keys(profile);
const numVotes = Object.values(profile).reduce((a, b) => a + b, 0)
let majorityGrade = indices[0]
let accBefore = 0
let isBefore = true
for (const gradeId in votes) {
for (const value of indices) {
if (isBefore) {
accBefore += votes[gradeId]
accBefore += profile[value]
}
if (isBefore && accBefore > numVotes / 2) {
majorityGrade = indices[gradeId]
accBefore -= votes[gradeId]
majorityGrade = value
accBefore -= profile[value]
isBefore = false
}
}
return majorityGrade;
const value = indices.indexOf(majorityGrade);
return value
}

@ -6,7 +6,7 @@ import {getWindowUrl, displayRef} from './utils';
export const CREATE_ELECTION = '/admin/new';
export const BALLOT = '/ballot';
export const ENDED_VOTE = '/ballot/end';
export const ENDED_VOTE = '/end';
export const VOTE = '/vote';
export const RESULTS = '/result';

@ -23,3 +23,42 @@ export interface Vote {
gradeId: number;
}
export interface GradeResultInterface {
name: string;
description: string;
id: number;
value: number;
color: string;
}
export interface MeritProfileInterface {
[key: number]: number;
}
export interface CandidateResultInterface {
name: string;
description: string;
id: number;
image: string;
majorityGrade: GradeResultInterface;
meritProfile: MeritProfileInterface;
rank: number;
}
export interface ResultInterface {
name: string;
description: string;
ref: string;
dateStart: string;
dateEnd: string;
hideResults: boolean;
forceClose: boolean;
restricted: boolean;
grades: Array<GradeResultInterface>;
candidates: Array<CandidateResultInterface>;
ranking: {[key: string]: number};
meritProfiles: {[key: number]: MeritProfileInterface};
}

@ -28,3 +28,10 @@ export const displayRef = (ref: string): string => {
return `${ref.substring(0, 3)}-${ref.substring(3, 6)}-${ref.substring(6)}`
}
export const isEnded = (date: string): boolean => {
const dateEnd = new Date(date);
const now = new Date();
console.log(dateEnd, now)
return +dateEnd < +now;
}

@ -158,3 +158,10 @@ ol.result > li:nth-child(1) > .resultPosition {
flex-direction: column;
}
}
// EDIT
.candidate_rank > .badge {
gap: 10px;
}

@ -63,3 +63,25 @@ $font-sizes: (
@import '_resultVote.scss';
@import '_admin.scss';
@import '_switch.scss';
// Generate text-{color}-{level}
// $all-colors: map-merge-multiple($grays);
//
// $utilities: map-merge(
// $utilities,
// (
// "color": map-merge(
// map-get($utilities, "color"),
// (
// values: map-merge(
// map-get(map-get($utilities, "color"), "values"),
// (
// $all-colors
// ),
// ),
// ),
// ),
// )
// );
//

Loading…
Cancel
Save