diff --git a/components/PatternedBackground.tsx b/components/PatternedBackground.tsx new file mode 100644 index 0000000..abd978a --- /dev/null +++ b/components/PatternedBackground.tsx @@ -0,0 +1,6 @@ + + +export default ({children}) => { + + return

{children}

+} diff --git a/components/WaitingBallot.tsx b/components/WaitingBallot.tsx new file mode 100644 index 0000000..990c3b0 --- /dev/null +++ b/components/WaitingBallot.tsx @@ -0,0 +1,5 @@ + +export default ({onSubmit}) => { + + return

FOO

+} diff --git a/components/admin/AccessResults.tsx b/components/admin/AccessResults.tsx index d1e1069..c00dbc4 100644 --- a/components/admin/AccessResults.tsx +++ b/components/admin/AccessResults.tsx @@ -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')}

- + - {election.restrictResult ? ( + {election.hideResults ? ( {t('admin.access-results-desc')} diff --git a/components/admin/CandidateField.tsx b/components/admin/CandidateField.tsx index 67bc68c..7b21e49 100644 --- a/components/admin/CandidateField.tsx +++ b/components/admin/CandidateField.tsx @@ -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')} /> diff --git a/components/admin/CandidateModalDel.tsx b/components/admin/CandidateModalDel.tsx index e49afcd..db20a3d 100644 --- a/components/admin/CandidateModalDel.tsx +++ b/components/admin/CandidateModalDel.tsx @@ -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 ( diff --git a/components/admin/CandidateModalSet.tsx b/components/admin/CandidateModalSet.tsx index 250ef00..8f7a9ea 100644 --- a/components/admin/CandidateModalSet.tsx +++ b/components/admin/CandidateModalSet.tsx @@ -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" />
diff --git a/components/admin/CandidatesField.tsx b/components/admin/CandidatesField.tsx index 2287b88..eb413e3 100644 --- a/components/admin/CandidatesField.tsx +++ b/components/admin/CandidatesField.tsx @@ -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'); diff --git a/components/admin/ConfirmField.tsx b/components/admin/ConfirmField.tsx index 7568208..5f6e3c6 100644 --- a/components/admin/ConfirmField.tsx +++ b/components/admin/ConfirmField.tsx @@ -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 ( @@ -37,13 +37,13 @@ const TitleField = () => {
{t('admin.confirm-question')}
-

{election.title}

+

{election.name}

); }; const CandidatesField = () => { - const { t } = useTranslation(); + const {t} = useTranslation(); const election = useElection(); return ( @@ -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 ( diff --git a/components/admin/GradeField.tsx b/components/admin/GradeField.tsx index 9c8e74f..3099f84 100644 --- a/components/admin/GradeField.tsx +++ b/components/admin/GradeField.tsx @@ -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); diff --git a/components/admin/Grades.tsx b/components/admin/Grades.tsx index 5adaa5c..bc5b04a 100644 --- a/components/admin/Grades.tsx +++ b/components/admin/Grades.tsx @@ -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); diff --git a/components/admin/LimitDate.tsx b/components/admin/LimitDate.tsx index 57ad318..43f99a7 100644 --- a/components/admin/LimitDate.tsx +++ b/components/admin/LimitDate.tsx @@ -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); diff --git a/components/admin/ParamsField.tsx b/components/admin/ParamsField.tsx index f3adcf9..871ca67 100644 --- a/components/admin/ParamsField.tsx +++ b/components/admin/ParamsField.tsx @@ -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 ( @@ -28,6 +33,7 @@ const ParamsField = ({ onSubmit }) => { color="secondary" className="bg-blue" onClick={onSubmit} + disabled={checkDisability} icon={faArrowRight} position="right" > diff --git a/components/admin/Private.tsx b/components/admin/Private.tsx index ac8d4af..948950f 100644 --- a/components/admin/Private.tsx +++ b/components/admin/Private.tsx @@ -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')}

- + - {election.restrictVote ? ( + {election.restricted ? ( <> { ) : null} - {election.restrictVote ? ( + {election.restricted ? ( {t('admin.access-results-desc')} diff --git a/components/admin/Title.tsx b/components/admin/Title.tsx index 94be512..2fcbeba 100644 --- a/components/admin/Title.tsx +++ b/components/admin/Title.tsx @@ -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(); diff --git a/components/layouts/LanguageSelector.tsx b/components/layouts/LanguageSelector.tsx index 777d14c..e5678b7 100644 --- a/components/layouts/LanguageSelector.tsx +++ b/components/layouts/LanguageSelector.tsx @@ -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 ( { ['GB', 'FR'] } selected={localeShort} - customLabels={{ GB: 'English', FR: 'Francais' }} + customLabels={{GB: 'English', FR: 'Francais'}} {...props} className="menu-flags" /> diff --git a/pages/admin/new.tsx b/pages/admin/new.tsx index 8eafc30..6d20eb5 100644 --- a/pages/admin/new.tsx +++ b/pages/admin/new.tsx @@ -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 + ; + } else { throw Error(`Unknown step ${step}`); } diff --git a/pages/index.tsx b/pages/index.tsx index f6bdeab..c9318cb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -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 (
@@ -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)} />

250

- +