fix: refactor services

pull/89/head
Pierre-Louis Guhur 1 year ago
parent a04dc68269
commit b8ae38b522

@ -0,0 +1,6 @@
export default ({children}) => {
return <p>{children}</p>
}

@ -0,0 +1,5 @@
export default ({onSubmit}) => {
return <p>FOO</p>
}

@ -1,10 +1,10 @@
import { useTranslation } from 'next-i18next';
import { useElection, useElectionDispatch } from './ElectionContext';
import { Container, Row, Col } from 'reactstrap';
import {useTranslation} from 'next-i18next';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
import {Container, Row, Col} from 'reactstrap';
import Switch from '@components/Switch';
const AccessResults = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
@ -13,7 +13,7 @@ const AccessResults = () => {
dispatch({
type: 'set',
field: 'restrictResult',
value: !election.restrictResult,
value: !election.hideResults,
});
};
@ -27,10 +27,10 @@ const AccessResults = () => {
{t('admin.access-results-desc')}
</p>
</div>
<Switch toggle={toggle} state={election.restrictResult} />
<Switch toggle={toggle} state={election.hideResults} />
</div>
</Container>
{election.restrictResult ? (
{election.hideResults ? (
<Container className="text-white d-md-none p-3">
{t('admin.access-results-desc')}
</Container>

@ -1,13 +1,13 @@
/**
* This is the candidate field used during election creation
*/
import { useState } from 'react';
import {useState} from 'react';
import Image from 'next/image';
import { useTranslation } from 'next-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlus, faTrashCan } from '@fortawesome/free-solid-svg-icons';
import { Row, Col } from 'reactstrap';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useTranslation} from 'next-i18next';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faPlus, faTrashCan} from '@fortawesome/free-solid-svg-icons';
import {Row, Col} from 'reactstrap';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
import whiteAvatar from '../../public/avatar.svg';
import CandidateModalSet from './CandidateModalSet';
import CandidateModalDel from './CandidateModalDel';
@ -25,7 +25,7 @@ const CandidateField = ({
defaultAvatar = whiteAvatar,
...props
}: CandidateProps) => {
const { t } = useTranslation();
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
@ -37,7 +37,7 @@ const CandidateField = ({
const [modalSet, setModalSet] = useState(false);
const addCandidate = () => {
dispatch({ type: 'candidate-push', value: 'default' });
dispatch({type: 'candidate-push', value: 'default'});
};
const toggleSet = () => setModalSet((m) => !m);
@ -59,9 +59,8 @@ const CandidateField = ({
src={image}
width={24}
height={24}
className={`${
image == defaultAvatar ? 'default-avatar' : ''
} bg-primary`}
className={`${image == defaultAvatar ? 'default-avatar' : ''
} bg-primary`}
alt={t('common.thumbnail')}
/>
</Col>

@ -1,28 +1,28 @@
import { Row, Col, Label, Input, Modal, ModalBody, Form } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {Row, Col, Label, Input, Modal, ModalBody, Form} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faTrashCan,
faTrashAlt,
faArrowLeft,
} from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'next-i18next';
import {useTranslation} from 'next-i18next';
import Image from 'next/image';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
import Button from '@components/Button';
import { upload } from '@services/imgpush';
import { IMGPUSH_URL } from '@services/constants';
import {upload} from '@services/imgpush';
import {IMGPUSH_URL} from '@services/constants';
import defaultAvatar from '../../public/default-avatar.svg';
import { useEffect } from 'react';
import {useEffect} from 'react';
const CandidateModal = ({ isOpen, position, toggle }) => {
const { t } = useTranslation();
const CandidateModal = ({isOpen, position, toggle}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidate = election.candidates[position];
const removeCandidate = () => {
dispatch({ type: 'candidate-rm', position: position });
dispatch({type: 'candidate-rm', position: position});
};
return (

@ -1,16 +1,16 @@
import { useState, useEffect, useRef } from 'react';
import { Row, Col, Label, Input, Modal, ModalBody, Form } from 'reactstrap';
import { faPlus, faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'next-i18next';
import {useState, useEffect, useRef} from 'react';
import {Row, Col, Label, Input, Modal, ModalBody, Form} from 'reactstrap';
import {faPlus, faArrowLeft} from '@fortawesome/free-solid-svg-icons';
import {useTranslation} from 'next-i18next';
import Image from 'next/image';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
import Button from '@components/Button';
import { upload } from '@services/imgpush';
import { IMGPUSH_URL } from '@services/constants';
import {upload} from '@services/imgpush';
import {IMGPUSH_URL} from '@services/constants';
import defaultAvatar from '../../public/default-avatar.svg';
const CandidateModal = ({ isOpen, position, toggle }) => {
const { t } = useTranslation();
const CandidateModal = ({isOpen, position, toggle}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
const candidate = election.candidates[position];
@ -19,7 +19,7 @@ const CandidateModal = ({ isOpen, position, toggle }) => {
const handleFile = async (event) => {
const payload = await upload(event.target.files[0]);
setState((s) => ({ ...s, image: `${IMGPUSH_URL}/${payload['filename']}` }));
setState((s) => ({...s, image: `${IMGPUSH_URL}/${payload['filename']}`}));
};
// to manage the hidden ugly file input
@ -27,7 +27,6 @@ const CandidateModal = ({ isOpen, position, toggle }) => {
useEffect(() => {
setState(election.candidates[position]);
console.log('effect election', election);
}, [election]);
const save = () => {
@ -53,11 +52,11 @@ const CandidateModal = ({ isOpen, position, toggle }) => {
};
const handleName = (e) => {
setState((s) => ({ ...s, name: e.target.value }));
setState((s) => ({...s, name: e.target.value}));
};
const handleDescription = (e) => {
setState((s) => ({ ...s, description: e.target.value }));
setState((s) => ({...s, description: e.target.value}));
};
return (
@ -138,7 +137,7 @@ const CandidateModal = ({ isOpen, position, toggle }) => {
placeholder={t('admin.candidate-desc-placeholder')}
onChange={handleDescription}
value={state.description}
// maxLength="250"
// maxLength="250"
/>
</div>
<div className="mt-5 gap-2 d-grid mb-3 d-md-flex">

@ -1,15 +1,15 @@
import { useState, useEffect, createRef } from 'react';
import { useTranslation } from 'next-i18next';
import { Container } from 'reactstrap';
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
import { MAX_NUM_CANDIDATES } from '@services/constants';
import {useState, useEffect, createRef} from 'react';
import {useTranslation} from 'next-i18next';
import {Container} from 'reactstrap';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
import {MAX_NUM_CANDIDATES} from '@services/constants';
import Alert from '@components/Alert';
import Button from '@components/Button';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
import CandidateField from './CandidateField';
const CandidatesField = ({ onSubmit }) => {
const { t } = useTranslation();
const CandidatesField = ({onSubmit}) => {
const {t} = useTranslation();
const election = useElection();
const dispatch = useElectionDispatch();
@ -21,7 +21,7 @@ const CandidatesField = ({ onSubmit }) => {
useEffect(() => {
// Initialize the list with at least two candidates
if (candidates.length < 2) {
dispatch({ type: 'candidate-push', value: 'default' });
dispatch({type: 'candidate-push', value: 'default'});
}
if (candidates.length > MAX_NUM_CANDIDATES) {
setError('error.too-many-candidates');

@ -1,4 +1,4 @@
import { useTranslation } from 'next-i18next';
import {useTranslation} from 'next-i18next';
import Footer from '@components/layouts/Footer';
import TrashButton from './TrashButton';
import {
@ -19,8 +19,8 @@ import {
Label,
Container,
} from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useElection } from './ElectionContext';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {useElection} from '../../services/ElectionContext';
import CandidateField from './CandidateField';
import AccessResults from './AccessResults';
import LimitDate from './LimitDate';
@ -28,7 +28,7 @@ import Grades from './Grades';
import Private from './Private';
const TitleField = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const election = useElection();
return (
<Container className="bg-white p-4">
@ -37,13 +37,13 @@ const TitleField = () => {
<h5 className="text-dark">{t('admin.confirm-question')}</h5>
</Col>
</Row>
<h4 className="text-primary">{election.title}</h4>
<h4 className="text-primary">{election.name}</h4>
</Container>
);
};
const CandidatesField = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const election = useElection();
return (
<Container className="bg-white p-4 mt-3 mt-md-0">
@ -66,8 +66,8 @@ const CandidatesField = () => {
);
};
const ConfirmField = ({ onSubmit, goToCandidates, goToParams }) => {
const { t } = useTranslation();
const ConfirmField = ({onSubmit, goToCandidates, goToParams}) => {
const {t} = useTranslation();
const election = useElection();
return (

@ -1,6 +1,6 @@
import { useState } from 'react';
import { Row, Col } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {useState} from 'react';
import {Row, Col} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faPlus,
faPen,
@ -8,9 +8,9 @@ import {
faCheck,
faRotateLeft,
} from '@fortawesome/free-solid-svg-icons';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
const GradeField = ({ value }) => {
const GradeField = ({value}) => {
const [modal, setModal] = useState(false);
const toggle = () => setModal((m) => !m);

@ -1,22 +1,22 @@
/**
* A field to update the grades
*/
import { useState, useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import { Container, Row, Col } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {useState, useEffect} from 'react';
import {useTranslation} from 'next-i18next';
import {Container, Row, Col} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faPlus,
faPen,
faXmark,
faCheck,
} from '@fortawesome/free-solid-svg-icons';
import { DEFAULT_GRADES, GRADE_COLORS } from '@services/constants';
import { useElection, useElectionDispatch } from './ElectionContext';
import {DEFAULT_GRADES, GRADE_COLORS} from '@services/constants';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
import GradeField from './GradeField';
const AddField = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const [modal, setModal] = useState(false);
const toggle = () => setModal((m) => !m);
@ -36,7 +36,7 @@ const AddField = () => {
};
const Grades = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const defaultEndDate = new Date();
defaultEndDate.setUTCDate(defaultEndDate.getUTCDate() + 15);
const [endDate, setEndDate] = useState(defaultEndDate);

@ -1,12 +1,12 @@
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Container, Row, Col } from 'reactstrap';
import {useState} from 'react';
import {useTranslation} from 'next-i18next';
import {Container, Row, Col} from 'reactstrap';
import DatePicker from '@components/DatePicker';
import Switch from '@components/Switch';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
const LimitDate = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const defaultEndDate = new Date();
defaultEndDate.setUTCDate(defaultEndDate.getUTCDate() + 15);
const [endDate, setEndDate] = useState(defaultEndDate);

@ -1,14 +1,19 @@
import { useTranslation } from 'next-i18next';
import { Container } from 'reactstrap';
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
import {useTranslation} from 'next-i18next';
import {Container} from 'reactstrap';
import {faArrowRight} from '@fortawesome/free-solid-svg-icons';
import Button from '@components/Button';
import Grades from './Grades';
import LimitDate from './LimitDate';
import AccessResults from './AccessResults';
import Private from './Private';
import {useElection} from '@services/ElectionContext';
const ParamsField = ({ onSubmit }) => {
const { t } = useTranslation();
const ParamsField = ({onSubmit}) => {
const {t} = useTranslation();
const election = useElection();
const checkDisability = election.restricted && (typeof election.emails === "undefined" || election.emails.length === 0);
console.log(election.restricted, typeof election.emails === "undefined", election.emails.length === 0)
return (
<Container className="params d-flex flex-column flex-grow-1 my-5">
@ -28,6 +33,7 @@ const ParamsField = ({ onSubmit }) => {
color="secondary"
className="bg-blue"
onClick={onSubmit}
disabled={checkDisability}
icon={faArrowRight}
position="right"
>

@ -1,14 +1,14 @@
/**
* A field to update the grades
*/
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { Container, Row, Col } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleInfo } from '@fortawesome/free-solid-svg-icons';
import {useState} from 'react';
import {useTranslation} from 'next-i18next';
import {Container, Row, Col} from 'reactstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCircleInfo} from '@fortawesome/free-solid-svg-icons';
import Switch from '@components/Switch';
import ListInput from '@components/ListInput';
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
const validateEmail = (email) => {
// https://stackoverflow.com/a/46181/4986615
@ -20,7 +20,7 @@ const validateEmail = (email) => {
};
const Private = () => {
const { t } = useTranslation();
const {t} = useTranslation();
const [emails, setEmails] = useState([]);
@ -30,8 +30,8 @@ const Private = () => {
const toggle = () => {
dispatch({
type: 'set',
field: 'restrictVote',
value: !election.restrictVote,
field: 'restricted',
value: !election.restricted,
});
};
@ -53,9 +53,9 @@ const Private = () => {
{t('admin.private-desc')}
</p>
</div>
<Switch toggle={toggle} state={election.restrictVote} />
<Switch toggle={toggle} state={election.restricted} />
</div>
{election.restrictVote ? (
{election.restricted ? (
<>
<ListInput
onEdit={handleEmails}
@ -69,7 +69,7 @@ const Private = () => {
</>
) : null}
</Container>
{election.restrictVote ? (
{election.restricted ? (
<Container className="text-white d-md-none p-3">
{t('admin.access-results-desc')}
</Container>

@ -1,7 +1,7 @@
/**
* This component manages the title of the election
*/
import { useElection, useElectionDispatch } from './ElectionContext';
import {useElection, useElectionDispatch} from '../../services/ElectionContext';
const TitleField = () => {
const election = useElection();

@ -1,15 +1,16 @@
import { useRouter } from 'next/router';
import {useRouter} from 'next/router';
import ReactFlagsSelect from 'react-flags-select';
import {getLocaleShort} from '@services/utils';
const LanguageSelector = (props) => {
const router = useRouter();
let localeShort = router.locale.substring(0, 2).toUpperCase();
if (localeShort === 'EN') localeShort = 'GB';
const localeShort = getLocaleShort();
const selectHandler = (e) => {
let locale = e.toLowerCase();
if (locale === 'gb') locale = 'en';
router.push('', '', { locale });
router.push('', '', {locale});
};
return (
<ReactFlagsSelect
@ -19,7 +20,7 @@ const LanguageSelector = (props) => {
['GB', 'FR']
}
selected={localeShort}
customLabels={{ GB: 'English', FR: 'Francais' }}
customLabels={{GB: 'English', FR: 'Francais'}}
{...props}
className="menu-flags"
/>

@ -1,16 +1,22 @@
import { useState } from 'react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import {useState} from 'react';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import CandidatesField from '@components/admin/CandidatesField';
import ParamsField from '@components/admin/ParamsField';
import ConfirmField from '@components/admin/ConfirmField';
import WaitingBallot from '@components/WaitingBallot';
import PatternedBackground from '@components/PatternedBackground';
import {
ElectionProvider,
useElection,
} from '@components/admin/ElectionContext';
import { ProgressSteps, creationSteps } from '@components/CreationSteps';
import { GetStaticProps } from 'next';
} from '@services/ElectionContext';
import {ProgressSteps, creationSteps} from '@components/CreationSteps';
import {GetStaticProps} from 'next';
import {createElection, ElectionPayload} from '@services/api';
import {getUrlVote, getUrlResult} from '@services/routes';
import {GradeItem, CandidateItem} from '@services/type';
import {sendInviteMails} from '@services/mail';
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
export const getStaticProps: GetStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, ['resource'])),
},
@ -22,12 +28,43 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => ({
const CreateElectionForm = () => {
// load the election
const election = useElection();
const [wait, setWait] = useState(false);
const handleSubmit = () => {
if (stepId < creationSteps.length - 1) {
setStepId((i) => i + 1);
} else {
// TODO
setWait(true);
createElection(
election.name,
election.candidates.map((c: CandidateItem) => ({name: c.name, description: c.description, image: c.image})),
election.grades.map((g: GradeItem, i: number) => ({name: g.name, value: i})),
election.description,
election.emails.length,
election.hideResults,
election.forceClose,
election.restricted,
(payload: ElectionPayload) => {
const id = payload.id;
const tokens = payload.tokens;
if (typeof election.emails !== 'undefined' && election.emails.length > 0) {
if (typeof payload.tokens === 'undefined' || payload.tokens.length === election.emails.length) {
throw Error('Can not send invite emails');
}
const urlVotes = election.tokens.map((token: string) => getUrlVote(id.toString(), token));
const urlResult = getUrlResult(id.toString());
sendInviteMails(
election.emails,
tokens,
election.name,
urlVotes,
urlResult,
);
}
}
)
}
};
@ -48,6 +85,10 @@ const CreateElectionForm = () => {
goToParams={() => setStepId(1)}
/>
);
} else if (step == 'waiting') {
return <PatternedBackground>
<WaitingBallot />;
</PatternedBackground>
} else {
throw Error(`Unknown step ${step}`);
}

@ -1,12 +1,12 @@
import { useState } from 'react';
import {useState} from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import { Container, Row, Col, Button, Input } from 'reactstrap';
import {GetStaticProps} from 'next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import {useTranslation} from 'next-i18next';
import {Container, Row, Col, Button, Input} from 'reactstrap';
import Logo from '@components/Logo';
import { CREATE_ELECTION } from '@services/routes';
import {CREATE_ELECTION} from '@services/routes';
import ballotBox from '../public/urne.svg';
import email from '../public/email.svg';
import respect from '../public/respect.svg';
@ -15,15 +15,15 @@ import twitter from '../public/twitter.svg';
import facebook from '../public/facebook.svg';
import arrowRight from '../public/arrow-white.svg';
export const getStaticProps: GetStaticProps = async ({ locale }) => ({
export const getStaticProps: GetStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, ['resource'])),
},
});
const StartForm = () => {
const { t } = useTranslation('resource');
const [title, setTitle] = useState(null);
const {t} = useTranslation('resource');
const [name, setName] = useState(null);
return (
<form className="sectionOneHomeForm" autoComplete="off">
@ -42,15 +42,15 @@ const StartForm = () => {
autoFocus
required
className="mt-2 mb-0 sectionOneHomeInput"
name="title"
value={title ? title : ''}
onChange={(e) => setTitle(e.target.value)}
name="name"
value={name ? name : ''}
onChange={(e) => setName(e.target.value)}
/>
<p className="pt-0 mt-0 maxLength">250</p>
</Row>
<Row>
<Link href={{ pathname: CREATE_ELECTION, query: { title: title } }}>
<Link href={{pathname: CREATE_ELECTION, query: {name: name}}}>
<Button color="secondary" outline={true} type="submit">
<Row className="justify-content-md-center p-2">
<Col className="col-auto">{t('home.start')}</Col>
@ -78,24 +78,24 @@ const StartForm = () => {
};
const AdvantagesRow = () => {
const { t } = useTranslation('resource');
const {t} = useTranslation('resource');
const resources = [
{
src: ballotBox,
alt: t('home.alt-icon-ballot-box'),
title: t('home.advantage-1-title'),
name: t('home.advantage-1-name'),
desc: t('home.advantage-1-desc'),
},
{
src: email,
alt: t('home.alt-icon-envelop'),
title: t('home.advantage-2-title'),
name: t('home.advantage-2-name'),
desc: t('home.advantage-2-desc'),
},
{
src: respect,
alt: t('home.alt-icon-respect'),
title: t('home.advantage-3-title'),
name: t('home.advantage-3-name'),
desc: t('home.advantage-3-desc'),
},
];
@ -109,7 +109,7 @@ const AdvantagesRow = () => {
height="128"
className="d-block mx-auto"
/>
<h4>{item.title}</h4>
<h4>{item.name}</h4>
<p>{item.desc}</p>
</Col>
))}
@ -118,22 +118,22 @@ const AdvantagesRow = () => {
};
const ExperienceRow = () => {
const { t } = useTranslation('resource');
const {t} = useTranslation('resource');
return (
<Row className="sectionTwoRowTwo">
<Row className="sectionTwoHomeImage">
<Image src={vote} alt={t('home.alt-icon-ballot')} />
</Row>
<Row className="sectionTwoRowTwoCol">
<h3 className="col-md-8">{t('home.experience-title')}</h3>
<h3 className="col-md-8">{t('home.experience-name')}</h3>
</Row>
<Row className="sectionTwoRowTwoCol">
<Col className="sectionTwoRowTwoColText col-md-4">
<h5 className="">{t('home.experience-1-title')}</h5>
<h5 className="">{t('home.experience-1-name')}</h5>
<p>{t('home.experience-1-desc')}</p>
</Col>
<Col className="sectionTwoRowTwoColText col-md-4 offset-md-1">
<h5 className="">{t('home.experience-2-title')}</h5>
<h5 className="">{t('home.experience-2-name')}</h5>
<p>{t('home.experience-2-desc')}</p>
<p></p>
</Col>
@ -156,7 +156,7 @@ const ExperienceRow = () => {
};
const ShareRow = () => {
const { t } = useTranslation('resource');
const {t} = useTranslation('resource');
return (
<Row className="sharing justify-content-md-center">
<Col className="col-auto">{t('home.share')}</Col>
@ -183,7 +183,7 @@ const ShareRow = () => {
};
const Home = () => {
const { t } = useTranslation('resource');
const {t} = useTranslation('resource');
return (
<Container fluid={true} className="p-0">
<section>

@ -1,29 +1,66 @@
/**
* This file provides a context and a reducer to manage an election
*/
import { createContext, useContext, useReducer, useEffect } from 'react';
import { useRouter } from 'next/router';
import {createContext, useContext, useReducer, useEffect, Dispatch, SetStateAction} from 'react';
import {useRouter} from 'next/router';
import {CandidateItem, GradeItem} from './type';
interface ElectionContextInterface {
name: string;
description: string;
candidates: Array<CandidateItem>;
grades: Array<GradeItem>;
isRandomOrder: boolean;
hideResults: boolean;
forceClose: boolean;
restricted: boolean;
endVote: string;
emails: Array<string>;
}
const defaultCandidate: CandidateItem = {
name: '',
image: '',
description: '',
active: false,
};
const defaultElection: ElectionContextInterface = {
name: '',
description: '',
candidates: [{...defaultCandidate}, {...defaultCandidate}],
grades: [],
isRandomOrder: false,
hideResults: true,
forceClose: false,
restricted: false,
endVote: null,
emails: [],
};
type DispatchType = Dispatch<SetStateAction<ElectionContextInterface>>;
// Store data about an election
const ElectionContext = createContext(null);
const ElectionContext = createContext<ElectionContextInterface>(defaultElection);
// Store the dispatch function that can modify an election
// const ElectionDispatchContext = createContext<DispatchType | null>(null);
const ElectionDispatchContext = createContext(null);
export function ElectionProvider({ children }) {
export function ElectionProvider({children}) {
/**
* Provide the election and the dispatch to all children components
*/
const [election, dispatch] = useReducer(electionReducer, defaultElection);
// At the initialization, set the title using GET param
// At the initialization, set the name using GET param
const router = useRouter();
useEffect(() => {
if (!router.isReady) return;
dispatch({
type: 'set',
field: 'title',
value: router.query.title || '',
field: 'name',
value: router.query.name || '',
});
}, [router.isReady]);
@ -50,13 +87,13 @@ export function useElectionDispatch() {
return useContext(ElectionDispatchContext);
}
function electionReducer(election: Election, action) {
function electionReducer(election: ElectionContextInterface, action) {
/**
* Manage all types of action doable on an election
*/
switch (action.type) {
case 'set': {
return { ...election, [action.field]: action.value };
return {...election, [action.field]: action.value};
}
case 'commit': {
throw Error('Not implemented yet');
@ -66,9 +103,9 @@ function electionReducer(election: Election, action) {
}
case 'candidate-push': {
const candidate =
action.value === 'default' ? { ...defaultCandidate } : action.value;
action.value === 'default' ? {...defaultCandidate} : action.value;
const candidates = [...election.candidates, candidate];
return { ...election, candidates };
return {...election, candidates};
}
case 'candidate-rm': {
if (typeof action.position !== 'number') {
@ -76,7 +113,7 @@ function electionReducer(election: Election, action) {
}
const candidates = [...election.candidates];
candidates.splice(action.position);
return { ...election, candidates };
return {...election, candidates};
}
case 'candidate-set': {
if (typeof action.position !== 'number') {
@ -89,13 +126,13 @@ function electionReducer(election: Election, action) {
const candidate = candidates[action.position];
candidate[action.field] = action.value;
candidate['active'] = true;
return { ...election, candidates };
return {...election, candidates};
}
case 'grade-push': {
const grade =
action.value === 'default' ? { ...defaultCandidate } : action.value;
action.value === 'default' ? {...defaultCandidate} : action.value;
const grades = [...election.grades, grade];
return { ...election, grades };
return {...election, grades};
}
case 'grade-rm': {
if (typeof action.position !== 'number') {
@ -103,7 +140,7 @@ function electionReducer(election: Election, action) {
}
const grades = [...election.grades];
grades.splice(action.position);
return { ...election, grades };
return {...election, grades};
}
case 'grade-set': {
if (typeof action.position !== 'number') {
@ -112,7 +149,7 @@ function electionReducer(election: Election, action) {
const grades = [...election.grades];
const grade = grades[action.position];
grade[action.field] = action.value;
return { ...election, grades };
return {...election, grades};
}
default: {
throw Error(`Unknown action: ${action.type}`);
@ -120,43 +157,3 @@ function electionReducer(election: Election, action) {
}
}
interface Candidate {
name: string;
description: string;
active: boolean;
}
interface Grade {
name: string;
active: boolean;
}
interface Election {
title: string;
description: string;
candidates: Array<Candidate>;
grades: Array<Grade>;
isRandomOrder: boolean;
restrictResult: boolean;
restrictVote: boolean;
endVote: string;
emails: Array<string>;
}
const defaultCandidate: Candidate = {
name: '',
description: '',
active: false,
};
const defaultElection: Election = {
title: '',
description: '',
candidates: [{ ...defaultCandidate }, { ...defaultCandidate }],
grades: [],
isRandomOrder: false,
restrictResult: true,
restrictVote: false,
endVote: null,
emails: [],
};

@ -1,117 +1,69 @@
const api = {
import {Candidate, Grade} from './type';
export const api = {
urlServer:
process.env.NEXT_PUBLIC_SERVER_URL || 'https://demo.mieuxvoter.fr/api/',
process.env.NEXT_PUBLIC_SERVER_URL || 'https://apiv2.mieuxvoter.fr/',
feedbackForm:
process.env.NEXT_PUBLIC_FEEDBACK_FORM ||
'https://docs.google.com/forms/d/e/1FAIpQLScuTsYeBXOSJAGSE_AFraFV7T2arEYua7UCM4NRBSCQQfRB6A/viewform',
routesServer: {
setElection: 'election/',
getElection: 'election/get/:slug/',
getResults: 'election/results/:slug',
voteElection: 'election/vote/',
setElection: 'elections',
getElection: 'elections/:slug',
getResults: 'results/:slug',
voteElection: 'votes',
},
};
const sendInviteMail = (res) => {
/**
* Send an invitation mail using a micro-service with Netlify
*/
const { id, title, mails, tokens, locale } = res;
if (!mails || !mails.length) {
throw new Error('No emails are provided.');
}
if (mails.length !== tokens.length) {
throw new Error('The number of emails differ from the number of tokens');
}
const origin =
typeof window !== 'undefined' && window.location.origin
? window.location.origin
: 'http://localhost';
const urlVote = (pid, token) => new URL(`/vote/${pid}/${token}`, origin);
const urlResult = (pid) => new URL(`/result/${pid}`, origin);
const recipientVariables = {};
tokens.forEach((token, index) => {
recipientVariables[mails[index]] = {
urlVote: urlVote(id, token),
urlResult: urlResult(id),
};
});
const req = fetch('/.netlify/functions/send-invite-email/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipientVariables,
title,
locale,
}),
});
return req.then((any) => res);
};
const createElection = (
title: string,
candidates: Array<string>,
description?: string,
mails?: Array<string>,
numGrades?: number,
finish?: string,
restrictResult?: boolean,
locale?: string,
export const createElection = async (
name: string,
candidates: Array<Candidate>,
grades: Array<Grade>,
description: string = "",
numVoters: number = 0,
hideResults: boolean = true,
forceClose: boolean = false,
restricted: boolean = false,
successCallback = null,
failureCallback = null
failureCallback = console.log
) => {
/**
* Create an election from its title, its candidates and a bunch of options
*/
const endpoint = new URL(api.routesServer.setElection, api.urlServer);
console.log(endpoint.href);
const onInvitationOnly = mails && mails.length > 0;
if (!restricted && numVoters > 0) {
throw Error("Set the election as not private!");
}
fetch(endpoint.href, {
const req = await fetch(endpoint.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
candidates,
on_invitation_only: onInvitationOnly,
num_grades: numGrades,
elector_emails: mails || [],
// start_at: start,
finish_at: finish,
select_language: locale || 'en',
front_url: window.location.origin,
restrict_results: restrictResult,
send_mail: false,
name,
description,
candidates: candidates,
grades: grades,
hide_results: hideResults,
force_close: forceClose,
private: restricted,
}),
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.json();
})
.then((res) => {
if (onInvitationOnly) {
return sendInviteMail({ locale, mails: mails, ...res });
}
return res;
})
.then(successCallback)
.catch(failureCallback || console.log);
if (!req.ok) {
if (successCallback) {
const payload = await req.json();
successCallback(payload);
}
} else if (failureCallback) {
failureCallback(req.statusText)
}
};
const getResults = (
export const getResults = (
pid: string,
successCallback = null,
failureCallback = null
@ -136,7 +88,8 @@ const getResults = (
.catch(failureCallback || ((err) => err));
};
const getDetails = (
export const getElectionDetails = (
pid: string,
successCallback = null,
failureCallback = null
@ -161,7 +114,8 @@ const getDetails = (
.then((res) => res);
};
const castBallot = (
export const castBallot = (
judgments: Array<number>,
pid: string,
token: string,
@ -184,7 +138,7 @@ const castBallot = (
fetch(endpoint.href, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
})
.then(callbackSuccess || ((res) => res))
@ -222,11 +176,38 @@ export const apiErrors = (error: string): string => {
}
};
export {
api,
getDetails,
getResults,
createElection,
sendInviteMail,
castBallot,
};
export interface GradePayload {
name: string;
description: string;
id: number;
value: number;
}
export interface CandidatePayload {
name: string;
description: string;
id: number;
image: string;
}
export interface ElectionPayload {
name: string;
description: string;
ref: string;
date_create: string;
date_modified: string;
num_voters: number;
date_start: string;
date_end: string;
hide_results: boolean;
force_close: boolean;
private: boolean;
id: number;
grades: Array<GradePayload>;
candidates: Array<CandidatePayload>;
tokens: Array<string>;
admin: string;
}

@ -1,4 +1,4 @@
const colors = [
export const gradeColors = [
"#F2F0FF",
"#C23D13",
"#C27C13",

@ -12,5 +12,6 @@ export const upload = async (photo) => {
{method: "POST", body: formData}
)
.then(ans => {return ans.json();})
.catch(console.log)
}

@ -0,0 +1,47 @@
import {getLocaleShort} from './utils';
export const sendInviteMails = async (
mails: Array<string>,
tokens: Array<string>,
name: string,
urlVote: string | URL,
urlResult: string | URL,
) => {
/**
* Send an invitation mail using a micro-service with Netlify
*/
if (!mails || !mails.length) {
throw new Error('No emails are provided.');
}
if (mails.length !== tokens.length) {
throw new Error('The number of emails differ from the number of tokens');
}
const recipientVariables = {};
tokens.forEach((token, index) => {
recipientVariables[mails[index]] = {
urlVote: urlVote,
urlResult: urlResult,
};
});
const locale = getLocaleShort();
const req = await fetch('/.netlify/functions/send-invite-email/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipientVariables,
name,
locale,
}),
});
return req;
};

@ -1,5 +1,17 @@
/**
* This file provides the paths to the pages
*/
import {getWindowUrl} from './utils';
export const CREATE_ELECTION = '/admin/new/';
export const getUrlVote = (electionId: string, token: string): URL => {
const origin = getWindowUrl();
return new URL(`/vote/${electionId}/${token}`, origin);
}
export const getUrlResult = (electionId: string): URL => {
const origin = getWindowUrl();
return new URL(`/result/${electionId}`, origin);
}

@ -0,0 +1,19 @@
export interface Candidate {
name: string,
image?: string,
description?: string
}
export interface CandidateItem extends Candidate {
active: boolean;
}
export interface Grade {
name: string,
value: number,
description?: string
}
export interface GradeItem extends Grade {
active: boolean;
}

@ -0,0 +1,38 @@
/**
* This file contains several utils functions
*/
import {useRouter} from 'next/router';
export const getLocaleShort = (): string => {
const router = useRouter();
if (!router.locale) {
return router.defaultLocale.substring(0, 2).toUpperCase();
}
if (router.locale.startsWith("en")) {
return "GB";
}
return router.locale.substring(0, 2).toUpperCase();
}
export const getWindowUrl = (): string => {
return typeof window !== 'undefined' && window.location.origin
? window.location.origin
: 'http://localhost';
}
export const getUrlVote = (electionId: string, token: string): URL => {
const origin = getWindowUrl();
return new URL(`/vote/${electionId}/${token}`, origin);
}
export const getUrlResult = (electionId: string): URL => {
const origin = getWindowUrl();
return new URL(`/result/${electionId}`, origin);
}
Loading…
Cancel
Save