@ -1,5 +1,6 @@
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 { useRouter } from 'next/router' ;
@ -15,167 +16,318 @@ import {
Table ,
Button ,
} from 'reactstrap' ;
import { getResults , getElection , apiErrors , ResultsPayload } from '@services/api' ;
import ErrorMessage from '@components/Error' ;
import config from '../../../next-i18next.config.js' ;
import Footer from '@components/layouts/Footer' ;
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
import {
faChevronDown ,
faChevronRight ,
faChevronUp ,
faGear ,
} from '@fortawesome/free-solid-svg-icons' ;
import ErrorMessage from '@components/Error' ;
import Logo from '@components/Logo' ;
import { getResults , getElection , apiErrors , ResultsPayload , CandidatePayload , GradePayload } from '@services/api' ;
import { getUrlAdmin } from '@services/routes' ;
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' ;
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 > ;
}
interface ResultInterface extends ElectionInterface {
ranking : { [ key : string ] : number } ;
meritProfile : { [ key : number ] : Array < number > } ;
}
export async function getServerSideProps ( { query , locale } ) {
const { pid , tid } = query ;
const { pid , tid : token } = query ;
const electionRef = pid . replace ( "-" , "" ) ;
const [ res , details , translations ] = await Promise . all ( [
getResults ( pid ) ,
getElection ( pid ) ,
serverSideTranslations ( locale , [ ] , config ) ,
const [ payload , translations ] = await Promise . all ( [
getResults ( electionRef ) ,
serverSideTranslations ( locale , [ "resource" ] ) ,
] ) ;
if ( typeof res === 'string' || res instanceof String ) {
return { props : { err : res.slice ( 1 , - 1 ) , . . . translations } } ;
if ( typeof payload === 'string' || payload instanceof String ) {
return { props : { err : payload , . . . translations } } ;
}
if ( typeof details === 'string' || details instanceof String ) {
return { props : { err : res.slice ( 1 , - 1 ) , . . . translations } } ;
}
const numGrades = payload . grades . length ;
const grades = payload . grades . map ( ( g , i ) = > ( { . . . g , color : getGradeColor ( i , numGrades ) } ) ) ;
const gradesByValue : { [ key : number ] : GradeInterface } = { }
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 = > ( {
. . . c ,
rank : payload.ranking [ c . id ] ,
majorityGrade : gradesByValue [ getMajorityGrade ( payload . merit_profile [ c . id ] ) ]
} ) ) ,
ranking : payload.ranking ,
meritProfile : payload.merit_profile ,
if ( ! details . candidates || ! Array . isArray ( details . candidates ) ) {
return { props : { err : 'Unknown error' , . . . translations } } ;
}
return {
props : {
title : details.name ,
numGrades : details.grades.length ,
finish : details.date_end ,
candidates : res ,
pid : pid ,
result ,
token : token || "" ,
. . . translations ,
} ,
} ;
}
interface ResultsInterface {
results : ResultsPayload ;
err : string ;
const getNumVotes = ( result : ResultInterface ) = > {
const sum = ( seq : Array < number > ) = >
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 = > {
if ( sum ( v ) !== numVotes ) {
throw Error ( "The election does not contain the same number of votes for each candidate" )
}
} )
return numVotes ;
}
const Results = ( { results , err } : ResultsInterface ) = > {
interface ResultBanner {
result : ResultsPayload ;
}
const ResultBanner = ( { result } ) = > {
const { t } = useTranslation ( ) ;
const newstart = new Date ( results . date_end ) . toLocaleDateString ( ) ;
const dateEnd = new Date ( result . date_end ) ;
const now = new Date ( ) ;
const closedSince = + dateEnd - ( + now ) ;
if ( err && err !== '' ) {
return < ErrorMessage msg = { t ( apiErrors ( err ) ) } / > ;
const numVotes = getNumVotes ( result )
return ( < div className = "w-100 bg-white p-5 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 >
< / div >
< div className = "d-flex align-items-center" >
< Image src = { avatarBlue } alt = "Avatar" / >
< p > { ` ${ numVotes } ${ t ( 'common.participants' ) } ` } < / p >
< / div >
< / div >
< h3 > { result . name } < / h3 >
< div className = "text-muted" >
< div className = "d-flex align-items-center" >
< Image alt = "Download" src = { arrowUpload } / >
< p > { t ( 'result.download' ) } < / p >
< / div >
< div className = "d-flex align-items-center" >
< Image src = { arrowLink } alt = "Share" / >
< p > { t ( 'result.share' ) } < / p >
< / div >
< / div >
< / d i v >
)
}
const BottomButtonsMobile = ( ) = > {
const { t } = useTranslation ( ) ;
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 >
< Button className = "cursorPointer btn-result btn-validation mb-5 btn btn-secondary" >
< Image src = { arrowLink } alt = "Share" / >
< p > { t ( 'result.share' ) } < / p >
< / Button >
< / div >
)
}
interface TitleBannerInterface {
name : string ;
electionRef : string ;
token? : string ;
}
const TitleBanner = ( { name , electionRef , token } : TitleBannerInterface ) = > {
const { t } = useTranslation ( ) ;
return (
< div className = "d-none d-md-flex p-3 justify-content-between text-white" >
< div className = "d-flex" >
< Logo title = { false } / >
< h5 > { name } < / h5 >
< / div >
{ token ?
< div className = "d-flex" >
< Link href = { getUrlAdmin ( electionRef , token ) } >
< Button icon = { faGear } position = "left" > { t ( 'result.go-to-admin' ) } < / Button >
< / Link >
< / div > : null
}
< / div >
)
}
interface ButtonGradeInterface {
grade : GradeInterface ;
}
const ButtonGrade = ( { grade } : ButtonGradeInterface ) = > {
const style = {
color : 'white' ,
backgroundColor : grade.color ,
} ;
return (
< div
style = { style }
className = "py-2 px-3 m-1 fw-bold rounded-1 d-flex justify-content-between gap-3"
>
{ grade . name }
< / div >
) ;
} ;
interface CandidateRankedInterface {
candidate : CandidateInterface ;
}
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" } >
{ candidate . rank }
< / div >
< div className = { ` text-white ${ isFirst ? "fs-4" : "fs-6" } ` } >
{ candidate . name }
< / div >
< ButtonGrade grade = { candidate . majorityGrade } / >
< / div >
}
interface PodiumInterface {
candidates : Array < CandidateInterface > ;
}
const Podium = ( { candidates } : PodiumInterface ) = > {
const { t } = useTranslation ( ) ;
// get best candidates
const numBest = Math . min ( 3 , candidates . length ) ;
const candidatesByRank = { }
candidates . forEach ( c = > candidatesByRank [ 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 ] } / >
< / div > )
}
return ( < div >
< CandidateRanked candidate = { candidates [ 1 ] } / >
< CandidateRanked candidate = { candidates [ 0 ] } / >
< CandidateRanked candidate = { candidates [ 2 ] } / >
< / div > )
}
interface ResultPageInterface {
result? : ResultInterface ;
token? : string ;
err? : string ;
}
const ResultPage = ( { result , token , err } : ResultPageInterface ) = > {
const { t } = useTranslation ( ) ;
const router = useRouter ( ) ;
const colSizeCandidateLg = 4 ;
const colSizeCandidateMd = 6 ;
const colSizeCandidateXs = 12 ;
const colSizeGradeLg = 1 ;
const colSizeGradeMd = 1 ;
const colSizeGradeXs = 1 ;
if ( err && err !== '' ) {
return < ErrorMessage msg = { err } / > ;
}
if ( ! result ) {
return < ErrorMessage msg = "error.catch22" / > ;
}
const origin =
typeof window !== 'undefined' && window . location . origin
? window . location . origin
: 'http://localhost' ;
const urlVote = new URL ( ` /vote/ ${ results . id } ` , origin ) ;
if ( typeof results . candidates === "undefined" || results . candidates . length === 0 ) {
if ( typeof result . candidates === "undefined" || result . candidates . length === 0 ) {
throw Error ( "No candidates were loaded in this election" )
}
// const collapsee = results.candidates[0].name;
// const collapsee = result .candidates[0].name;
// const [collapseProfiles, setCollapseProfiles] = useState(false);
const [ collapseGraphics , setCollapseGraphics ] = useState ( false ) ;
// const [collapseGraphics, setCollapseGraphics] = useState(false);
const sum = ( seq : Array < number > ) = >
Object . values ( seq ) . reduce ( ( a , b ) = > a + b , 0 ) ;
const anyCandidateName = results . candidates [ 0 ] . name ;
const numVotes = sum ( results . votes [ anyCandidateName ] ) ;
// check each vote contains the same number of votes
// TODO move it to a more appropriate location
Object . values ( results . votes ) . forEach ( v = > {
if ( sum ( v ) !== numVotes ) {
throw Error ( "The election does not contain the same numberof votes for each candidate" )
}
} )
const gradeIds = results . grades . map ( g = > g . value ) ;
const numVotes = getNumVotes ( result )
return (
< Container className = "resultContainer resultPage" >
< Head >
< title > { results . name } < / title >
< title > { result . name } < / title >
< link rel = "icon" href = "/favicon.ico" / >
< meta property = "og:title" content = { results . name } / >
< meta property = "og:title" content = { result . name } / >
< / Head >
< Row className = "sectionHeaderResult componentDesktop mx-0" >
< Col className = "col-md-3 sectionHeaderResultLeftCol" >
< Row >
< Col className = "sectionHeaderResultSideCol" >
< img src = "/calendar.svg" / >
< p > { newstart } < / p >
< / Col >
< / Row >
< Row >
< Col className = "sectionHeaderResultSideCol" >
< img src = "/avatarBlue.svg" / >
< p > { ' ' + numVotes } votants < / p >
< / Col >
< / Row >
< / Col >
< Col className = "sectionHeaderResultMiddleCol" >
< h3 > { results . name } < / h3 >
< / Col >
< Col className = "col-md-3 sectionHeaderResultRightCol" >
< Row >
< Col className = "sectionHeaderResultSideCol" >
< img src = "/arrowUpload.svg" / >
< p > T é l é charger les r é sultats < / p >
< / Col >
< / Row >
< Row >
< Col className = "sectionHeaderResultSideCol" >
< img src = "/arrowL.svg" / >
< p > Partagez les r é sultats < / p >
< / Col >
< / Row >
< / Col >
< / Row >
< Row className = "sectionHeaderResult componentMobile mx-0" >
< Col className = "px-0" >
< h3 > { results . name } < / h3 >
< / Col >
< Row >
< Col className = "sectionHeaderResultSideCol" >
< img src = "/calendar.svg" / >
< p > { newstart } < / p >
< / Col >
< Col className = "sectionHeaderResultSideCol" >
< img src = "/avatarBlue.svg" / >
< p > { ' ' + numVotes } votants < / p >
< / Col >
< / Row >
< / Row >
< TitleBanner electionRef = { result . ref } token = { token } name = { result . name } / >
< ResultBanner result = { result } / >
< Podium candidates = { result . candidates } / >
< section className = "sectionContentResult mb-5" >
< Row className = "mt-5 componentDesktop" >
< Col >
< ol className = "result px-0" >
{ / * { r e s u l t s . c a n d i d a t e s . m a p ( ( c a n d i d a t e , i ) = > {
const gradeValue = candidate . grade + offsetGrade ;
{ result . candidates . map ( ( candidate , i ) = > {
return (
< li key = { i } className = "mt-2" >
< span className = "resultPosition" > { i + 1 } < / span >
@ -183,38 +335,34 @@ const Results = ({results, err}: ResultsInterface) => {
< span
className = "badge badge-light"
style = { {
backgroundColor : grades.slice ( 0 ) . reverse ( ) [
candidate . grade
] . color ,
backgroundColor : candidate.majorityGrade.color ,
color : '#fff' ,
} }
>
{ allGrades. slice ( 0 ) . reverse ( ) [ gradeValue ] . label }
{ candidate. majorityGrade . name }
< / span >
< / li >
) ;
} ) }
* / }
< / ol >
< / Col >
< / Row >
< Row className = "mt-5" >
< Col >
{/ * < h 5 >
<h5 >
< small > { t ( 'Détails des résultats' ) } < / small >
< / h5 >
{ candidates . map ( ( candidate , i ) = > {
const gradeValue = candidate . grade + offsetGrade ;
{ result . candidates . map ( ( candidate , i ) = > {
return (
< Card className = "bg-light text-primary my-3" >
< CardHeader
className = "pointer"
onClick = { ( ) = > setCollapseGraphics ( ! collapseGraphics ) }
>
{ /*onClick={() => setCollapseGraphics(!collapseGraphics)}*/ }
< h4
className = { 'm-0 ' + ( collapseGraphics ? 'collapsed' : '' ) }
>
{ /* className={'m-0 ' + (collapseGraphics ? 'collapsed' : '')}*/ }
< span
key = { i }
className = "d-flex panel-title justify-content-between"
@ -229,13 +377,11 @@ const Results = ({results, err}: ResultsInterface) => {
< span
className = "badge badge-light"
style = { {
backgroundColor : grades.slice ( 0 ) . reverse ( ) [
candidate . grade
] . color ,
backgroundColor : candidate.majorityGrade.color ,
color : '#fff' ,
} }
>
{ allGrades. slice ( 0 ) . reverse ( ) [ gradeValue ] . label }
{ candidate. majorityGrade . name }
< / span >
< FontAwesomeIcon
icon = { faChevronDown }
@ -249,7 +395,9 @@ const Results = ({results, err}: ResultsInterface) => {
< / span >
< / h4 >
< / CardHeader >
< Collapse isOpen = { collapseGraphics } >
< Collapse
>
{ /*isOpen={collapseGraphics}*/ }
< CardBody className = "pt-5" >
< Row className = "column" >
< Col >
@ -264,7 +412,7 @@ const Results = ({results, err}: ResultsInterface) => {
< div key = { i } >
< div style = { { width : '100%' } } >
{ gradeId s
{ / * g r a d e I d s
. slice ( 0 )
. reverse ( )
. map ( ( id , i ) = > {
@ -289,15 +437,12 @@ const Results = ({results, err}: ResultsInterface) => {
} else {
return null ;
}
} ) }
} ) * / }
< / div >
< / div >
< / div >
< / div >
< / Col >
< Col >
< p > Graph bulles < / p >
< / Col >
< / Row >
< Row className = "linkResult my-3" >
< Link href = "/" className = "mx-auto" >
@ -313,27 +458,12 @@ const Results = ({results, err}: ResultsInterface) => {
< / Card >
) ;
} ) }
* / }
< / Col >
< / Row >
< div className = "componentMobile mt-5" >
< Row >
< Button className = "cursorPointer btn-result btn-validation mb-5 btn btn-secondary" >
< img src = "/arrowUpload.svg" / >
< p > T é l é charger les r é sultats < / p >
< / Button >
< / Row >
< Row >
< Button className = "cursorPointer btn-result btn-validation mb-5 btn btn-secondary" >
< img src = "/arrowL.svg" / >
< p > Partagez les r é sultats < / p >
< / Button >
< / Row >
< / div >
< BottomButtonsMobile / >
< / section >
< Footer / >
< / Container >
) ;
} ;
export default Result s ;
export default ResultPage ;