From 8e9de4c944c0e93d65a1c13c79d00dc21ce9ed17 Mon Sep 17 00:00:00 2001 From: Pierre-Louis Guhur Date: Sat, 5 Nov 2022 18:04:43 +0100 Subject: [PATCH] fix: params field --- components/Button.tsx | 12 ++-- components/ListInput.tsx | 68 ++++++++++++++++++++ components/admin/ElectionContext.tsx | 74 +++++++--------------- components/admin/GradeField.tsx | 56 ++++++++++++++++ components/admin/Grades.tsx | 42 +++++++++++- components/admin/ParamsField.tsx | 32 +++------- components/admin/Private.tsx | 49 ++++++++++++-- components/admin/VoteButtonWithConfirm.tsx | 23 +++---- public/locales/en/resource.json | 8 ++- public/locales/fr/resource.json | 7 +- services/constants.ts | 20 +++++- styles/scss/_app.scss | 11 +++- styles/scss/config.scss | 14 +++- 13 files changed, 313 insertions(+), 103 deletions(-) create mode 100644 components/ListInput.tsx create mode 100644 components/admin/GradeField.tsx diff --git a/components/Button.tsx b/components/Button.tsx index 5735264..7f92108 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -1,7 +1,14 @@ +import {IconProp} from "@fortawesome/fontawesome-svg-core"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {Row, Col, Button} from "reactstrap"; -const ButtonWithIcon = ({icon, children, position, ...props}) => { +interface ButtonProps { + children?: React.ReactNode; + icon?: IconProp; + position?: 'left' | 'right'; + [props: string]: any; +} +const ButtonWithIcon = ({icon, children, position = 'left', ...props}: ButtonProps) => { if (icon && position === 'left') { return +} + + +const ListInput = ({onEdit, inputs, validator}) => { + + const [state, setState] = useState(""); + + const {t} = useTranslation(); + + const handleDelete = (position: number) => { + onEdit({...inputs}.splice(position)) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + if (validator(state)) { + setState(""); + onEdit([...inputs, state]) + } + } + } + + const handleChange = (e: React.ChangeEvent) => { + setState(e.target.value); + } + + return + {inputs.map((item, i) => + + handleDelete(i)} + /> + + ) + } + + + + +} + +export default ListInput; diff --git a/components/admin/ElectionContext.tsx b/components/admin/ElectionContext.tsx index 15f6ddf..2960620 100644 --- a/components/admin/ElectionContext.tsx +++ b/components/admin/ElectionContext.tsx @@ -99,6 +99,28 @@ function electionReducer(election, action) { candidate['active'] = true; return {...election, candidates} } + case 'grade-push': { + const grade = action.value === 'default' ? {...defaultCandidate} : action.value; + const grades = [...election.grades, grade]; + return {...election, grades} + } + case 'grade-rm': { + if (typeof action.position !== "number") { + throw Error(`Unexpected grade position ${action.position}`) + } + const grades = [...election.grades]; + grades.splice(action.position) + return {...election, grades} + } + case 'grade-set': { + if (typeof action.position !== "number") { + throw Error(`Unexpected grade position ${action.value}`) + } + const grades = [...election.grades]; + const grade = grades[action.position] + grade[action.field] = action.value; + return {...election, grades} + } default: { throw Error(`Unknown action: ${action.type}`); } @@ -115,7 +137,7 @@ const initialElection = { title: "", description: "", candidates: [{...defaultCandidate}, {...defaultCandidate}], - grades: DEFAULT_GRADES, + grades: [], isTimeLimited: false, isRandomOrder: false, restrictResult: true, @@ -124,53 +146,3 @@ const initialElection = { endVote: null, emails: [], }; - - -// const checkFields = () => { -// const numCandidates = candidates ? candidates.filter(c => c.label !== '') : 0; -// if (numCandidates < 2) { -// return {ok: false, msg: 'error.at-least-2-candidates'}; -// } -// -// if (!title || title === "") { -// return {ok: false, msg: 'error.no-title'}; -// } -// -// return {ok: true, msg: "OK"}; -// }; -// -// const handleSubmit = () => { -// const check = checkFields(); -// if (!check.ok) { -// toast.error(t(check.msg), { -// position: toast.POSITION.TOP_CENTER, -// }); -// return; -// } -// -// setWaiting(true); -// -// createElection( -// title, -// candidates.map((c) => c.label).filter((c) => c !== ""), -// { -// mails: emails, -// numGrades, -// start: start.getTime() / 1000, -// finish: finish.getTime() / 1000, -// restrictResult: restrictResult, -// restrictVote: restrictVote, -// locale: router.locale.substring(0, 2).toLowerCase(), -// }, -// (result) => { -// if (result.id) { -// router.push(`/new/confirm/${result.id}`); -// } else { -// toast.error(t("Unknown error. Try again please."), { -// position: toast.POSITION.TOP_CENTER, -// }); -// setWaiting(false); -// } -// } -// ); -// }; diff --git a/components/admin/GradeField.tsx b/components/admin/GradeField.tsx new file mode 100644 index 0000000..f6eaa31 --- /dev/null +++ b/components/admin/GradeField.tsx @@ -0,0 +1,56 @@ +import {useState} from 'react' +import {Row, Col} from 'reactstrap'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import { + faPlus, faPen, faXmark, faCheck, faRotateLeft +} from "@fortawesome/free-solid-svg-icons"; +import {useElection, useElectionDispatch} from './ElectionContext'; + +const GradeField = ({value}) => { + const [modal, setModal] = useState(false); + const toggle = () => setModal(m => !m) + + const election = useElection(); + const grade = election.grades[value]; + const dispatch = useElectionDispatch(); + + // const handleChange = (e: React.ChangeEvent) => { + // dispatch({ + // type: 'grade-set', + // position: value, + // field: 'name', + // value: e.target.value, + // }) + // } + + const handleActive = () => { + dispatch({ + type: 'grade-set', + position: value, + field: 'active', + value: !grade.active, + }) + } + + const style = { + color: grade.active ? "white" : "#8F88BA", + backgroundColor: grade.active ? grade.color : "#F2F0FF", + } + + return + + {grade.name} + + + + + +} + + + +export default GradeField; diff --git a/components/admin/Grades.tsx b/components/admin/Grades.tsx index c8d3722..ef073fa 100644 --- a/components/admin/Grades.tsx +++ b/components/admin/Grades.tsx @@ -1,10 +1,34 @@ /** * A field to update the grades */ -import {useState} from 'react' +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 GradeField from './GradeField'; + +const AddField = () => { + const {t} = useTranslation(); + + const [modal, setModal] = useState(false); + const toggle = () => setModal(m => !m) + + const dispatch = useElectionDispatch(); + + return + + + + +} const Grades = () => { const {t} = useTranslation(); @@ -12,7 +36,21 @@ const Grades = () => { defaultEndDate.setUTCDate(defaultEndDate.getUTCDate() + 15) const [endDate, setEndDate] = useState(defaultEndDate); + useEffect(() => { + dispatch({ + type: "set", + field: "grades", + value: DEFAULT_GRADES.map((g, i) => ({ + name: t(g), + active: true, + color: GRADE_COLORS[i] + })) + }) + console.log('foo') + }, []) + const election = useElection(); + const grades = election.grades; const dispatch = useElectionDispatch(); return ( @@ -22,6 +60,8 @@ const Grades = () => {

{t('admin.grades-desc')}

+ {grades.map((_, i) => )} + { /* */}
) diff --git a/components/admin/ParamsField.tsx b/components/admin/ParamsField.tsx index c1ef709..ab04537 100644 --- a/components/admin/ParamsField.tsx +++ b/components/admin/ParamsField.tsx @@ -1,10 +1,7 @@ -import {useState, useEffect} from 'react' import {useTranslation} from "next-i18next"; -import {Container, Row, Col} from 'reactstrap'; +import {Container} from 'reactstrap'; import {faArrowRight} from "@fortawesome/free-solid-svg-icons"; -import {MAX_NUM_CANDIDATES} from '@services/constants'; import Button from '@components/Button' -import {useElection, useElectionDispatch} from './ElectionContext'; import Grades from './Grades' import LimitDate from './LimitDate' import AccessResults from './AccessResults' @@ -14,23 +11,6 @@ import Private from './Private' const ParamsField = ({onSubmit}) => { const {t} = useTranslation(); - const election = useElection(); - const dispatch = useElectionDispatch(); - const candidates = election.candidates; - const [error, setError] = useState(null) - const disabled = candidates.filter(c => c.name !== "").length < 2; - - // What to do when we change the candidates - useEffect(() => { - // Initialize the list with at least two candidates - if (candidates.length < 2) { - dispatch({'type': 'candidate-push', 'value': "default"}) - } - if (candidates.length > MAX_NUM_CANDIDATES) { - setError('error.too-many-candidates') - } - }, [candidates]) - return (
@@ -40,8 +20,14 @@ const ParamsField = ({onSubmit}) => {
-
-
diff --git a/components/admin/Private.tsx b/components/admin/Private.tsx index 6d3e515..e7df323 100644 --- a/components/admin/Private.tsx +++ b/components/admin/Private.tsx @@ -4,26 +4,65 @@ 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'; +const validateEmail = (email) => { + // https://stackoverflow.com/a/46181/4986615 + return String(email) + .toLowerCase() + .match( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + ); +}; + const Private = () => { const {t} = useTranslation(); - const defaultEndDate = new Date(); - defaultEndDate.setUTCDate(defaultEndDate.getUTCDate() + 15) - const [endDate, setEndDate] = useState(defaultEndDate); + + const [emails, setEmails] = useState([]); const election = useElection(); const dispatch = useElectionDispatch(); + const toggle = () => { + dispatch({ + 'type': 'set', + 'field': 'restrictVote', + 'value': !election.restrictVote, + }) + } + + const handleEmails = (emails) => { + dispatch({ + 'type': 'set', + 'field': 'emails', + 'value': emails, + }) + } + return ( -

{t('common.grades')}

-

{t('admin.grades-desc')}

+

{t('admin.private-title')}

+

{t('admin.private-desc')}

+
+ {election.restrictVote ? <> + + + + + + + {t("admin.private-tip")} + + : null}
) } diff --git a/components/admin/VoteButtonWithConfirm.tsx b/components/admin/VoteButtonWithConfirm.tsx index f3f4d7f..1196f46 100644 --- a/components/admin/VoteButtonWithConfirm.tsx +++ b/components/admin/VoteButtonWithConfirm.tsx @@ -1,15 +1,12 @@ -import { useState } from "react"; -import { - faTrashAlt, - faCheck -} from "@fortawesome/free-solid-svg-icons"; -import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useTranslation } from "next-i18next"; +import {useState} from "react"; +import {faCheck} from "@fortawesome/free-solid-svg-icons"; +import {Button, Modal, ModalHeader, ModalBody, ModalFooter} from "reactstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {useTranslation} from "next-i18next"; -const VoteButtonWithConfirm = ({ action }) => { +const VoteButtonWithConfirm = ({action}) => { const [visibled, setVisibility] = useState(false); - const { t } = useTranslation(); + const {t} = useTranslation(); const toggle = () => setVisibility(!visibled) @@ -32,9 +29,9 @@ const VoteButtonWithConfirm = ({ action }) => { > {t("Attention vous n’avez pas votez pour tous les candidats")} - {t("Si vous validez votre vote, les candidats sans vote auront la mention la plus basse du scrutin.")} - - + {t("Si vous validez votre vote, les candidats sans vote auront la mention la plus basse du scrutin.")} + +