/** * This file provides a context and a reducer to manage an election */ import { createContext, useContext, useReducer, useEffect, Dispatch, SetStateAction, } from 'react'; import {useRouter} from 'next/router'; import LogRocket from 'logrocket'; import {CandidateItem, GradeItem} from './type'; import {gradeColors} from '@services/grades'; export interface ElectionContextInterface { name: string; description: string; candidates: Array; grades: Array; hideResults: boolean; forceClose: boolean; restricted: boolean; randomOrder: boolean; emails: Array; dateEnd: string; dateStart?: string; ref?: string; numVoters?: number; numVoted?: number; } export const defaultCandidate: CandidateItem = { name: '', image: '', description: '', active: false, }; const defaultElection: ElectionContextInterface = { name: '', description: '', candidates: [{...defaultCandidate}, {...defaultCandidate}], grades: [], randomOrder: true, hideResults: false, forceClose: false, restricted: false, dateEnd: null, emails: [], }; export enum ElectionTypes { SET = 'set', RESET = 'reset', CANDIDATE_PUSH = 'candidate-push', CANDIDATE_RM = 'candidate-rm', CANDIDATE_SET = 'candidate-set', GRADE_PUSH = 'grade-push', GRADE_RM = 'grade-rm', GRADE_SET = 'grade-set', } export type SetAction = { type: ElectionTypes.SET; field: string; value: any; }; export type ResetAction = { type: ElectionTypes.RESET; value: ElectionContextInterface; }; export type CandidatePushAction = { type: ElectionTypes.CANDIDATE_PUSH; value: string | CandidateItem; }; export type CandidateRmAction = { type: ElectionTypes.CANDIDATE_RM; position: number; }; export type CandidateSetAction = { type: ElectionTypes.CANDIDATE_SET; position: number; field: string; value: any; }; export type GradePushAction = { type: ElectionTypes.GRADE_PUSH; value: GradeItem; }; export type GradeRmAction = { type: ElectionTypes.GRADE_RM; position: number; }; export type GradeSetAction = { type: ElectionTypes.GRADE_SET; position: number; field: string; value: any; }; export type ElectionActionTypes = | SetAction | ResetAction | CandidateRmAction | CandidateSetAction | CandidatePushAction | GradeRmAction | GradeSetAction | GradePushAction; type DispatchType = Dispatch; const ElectionContext = createContext<[ElectionContextInterface, DispatchType]>( [defaultElection, () => {}] ); type ElectionProviderType = { children: React.ReactNode; initialValue?: ElectionContextInterface; }; export const ElectionProvider = ({children, initialValue}: ElectionProviderType) => { /** * Provide the election and the dispatch to all children components */ const [election, dispatch] = useReducer(electionReducer, initialValue || defaultElection); // At the initialization, set the name using GET param const router = useRouter(); useEffect(() => { if (!router.isReady) return; if (election.name === '' && router.query.name !== '') { dispatch({ type: ElectionTypes.SET, field: 'name', value: router.query.name, }); } if (election.ref != "") { LogRocket.identify(election.ref, { name: election.name, }); } }, [router.isReady]); return ( {children} ); } export function useElection() { /** * A simple hook to read the election */ return useContext(ElectionContext); } function electionReducer( election: ElectionContextInterface, action: ElectionActionTypes ): ElectionContextInterface { /** * Manage all types of action doable on an election */ switch (action.type) { case ElectionTypes.RESET: { return {...action.value}; } case ElectionTypes.SET: { // if (isCreated(election) && action.field === 'candidates') { // throw new Error("The election has already started"); // } return {...election, [action.field]: action.value}; } case ElectionTypes.CANDIDATE_PUSH: { // if (isCreated(election)) { // throw new Error("The election has already started"); // } if (typeof action.value === 'string' && action.value !== 'default') { throw new Error('Unexpected action'); } const candidate = action.value === 'default' ? {...defaultCandidate} : action.value; const candidates = [...election.candidates, candidate]; if (candidates.filter((c) => !c.active).length === 0) { return { ...election, candidates: [...candidates, {...defaultCandidate}], }; } else { return {...election, candidates}; } } case ElectionTypes.CANDIDATE_RM: { if (typeof action.position !== 'number') { throw new Error(`Unexpected candidate position ${action.position}`); } const candidates = [...election.candidates]; candidates.splice(action.position, 1); return {...election, candidates}; } case ElectionTypes.CANDIDATE_SET: { if (typeof action.position !== 'number') { throw new Error(`Unexpected candidate position ${action.value}`); } if (action.field === 'active') { throw new Error('You are not allowed the set the active flag'); } const candidates = [...election.candidates]; const candidate = candidates[action.position]; candidate[action.field] = action.value; candidate['active'] = true; if (!isCreated(election) && candidates.filter((c) => !c.active).length === 0) { return { ...election, candidates: [...candidates, {...defaultCandidate}], }; } return {...election, candidates}; } case ElectionTypes.GRADE_PUSH: { const grades = [...election.grades, action.value]; return {...election, grades}; } case ElectionTypes.GRADE_RM: { if (typeof action.position !== 'number') { throw new Error(`Unexpected grade position ${action.position}`); } const grades = [...election.grades]; grades.splice(action.position); return {...election, grades}; } case ElectionTypes.GRADE_SET: { if (typeof action.position !== 'number') { throw new Error(`Unexpected grade position ${action.position}`); } const grades = [...election.grades]; const grade = grades.find(grade => grade.value == action.position) grade[action.field] = action.value; return {...election, grades}; } } } export const isCreated = (election: ElectionContextInterface) => { return election.ref !== null && election.ref !== "" && election.ref !== undefined; }; export const isClosed = (election: ElectionContextInterface) => { if (election.dateEnd === null) { return election.forceClose; } const dateEnd = new Date(election.dateEnd); const now = new Date(); const isOver = +dateEnd < +now; return election.forceClose || isOver; }; export const canViewResults = (election: ElectionContextInterface) => { const dateEnd = new Date(election.dateEnd); const now = new Date(); const isOver = +dateEnd < +now; return election.forceClose || !election.hideResults || isOver; }; export const hasEnoughCandidates = (election: ElectionContextInterface) => { const numCandidates = election.candidates.filter( (c) => c.active && c.name != '' ).length; return numCandidates > 1; }; export const hasEnoughGrades = (election: ElectionContextInterface) => { const numGrades = election.grades.filter( (g) => g.active && g.name != '' ).length; return numGrades > 1 && numGrades <= gradeColors.length; }; export const checkName = (election: ElectionContextInterface) => { return election.name && election.name !== ''; }; export const canBeFinished = (election: ElectionContextInterface) => { return ( election.restricted || election.forceClose || election.dateEnd || !election.hideResults ); };