fix: params field

pull/89/head
Pierre-Louis Guhur 1 year ago
parent 2883af2b4f
commit 8e9de4c944

@ -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 <Button {...props}>
<Row className='gx-2 align-items-end'>
@ -32,7 +39,4 @@ const ButtonWithIcon = ({icon, children, position, ...props}) => {
}
}
ButtonWithIcon.defaultProps = {
position: 'left'
}
export default ButtonWithIcon

@ -0,0 +1,68 @@
import {useTranslation} from "next-i18next";
import {faXmark} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {Row, Col} from "reactstrap"
import {useState} from "react";
import Button from "@components/Button"
const InputField = ({value, onDelete}) => {
return <Button
icon={faXmark}
className='bg-light text-primary border-0'
outline={true}
style={{boxShadow: "unset"}}
>
{value}
</Button >
}
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<HTMLInputElement>) => {
if (e.key === 'Enter') {
if (validator(state)) {
setState("");
onEdit([...inputs, state])
}
}
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setState(e.target.value);
}
return <Row className='gx-2 p-1 shadow list-input align-items-center'>
{inputs.map((item, i) =>
<Col className='col-auto'>
<InputField
key={i}
value={item}
onDelete={() => handleDelete(i)}
/>
</Col>
)
}
<Col className='col-auto'>
<input
type='text'
className='border-0 w-100'
placeholder={t('admin.private-placeholder')}
onKeyPress={handleKeyDown}
onChange={handleChange}
value={state}
/>
</Col>
</Row>
}
export default ListInput;

@ -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);
// }
// }
// );
// };

@ -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<HTMLInputElement>) => {
// 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 <Row
style={style}
onClick={toggle}
className='p-2 m-1 rounded-1'
>
<Col className={`${grade.active ? "" : "text-decoration-line-through"} col-auto fw-bold`}>
{grade.name}
</Col>
<Col onClick={handleActive} className='col-auto'>
<FontAwesomeIcon icon={grade.active ? faXmark : faRotateLeft} />
</Col>
</Row >
}
export default GradeField;

@ -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 <Row
onClick={toggle}
className='border border-2 border-primary text-black p-2 m-1 rounded-pill'
>
<Col className='col-auto'>
<FontAwesomeIcon icon={faPlus} />
</Col>
</Row >
}
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 (<Container className='bg-white container-fluid p-4 mt-1'>
@ -22,6 +60,8 @@ const Grades = () => {
<p className='text-muted'>{t('admin.grades-desc')}</p>
</Col>
<Col className='col-auto d-flex align-items-center'>
{grades.map((_, i) => <GradeField value={i} key={i} />)}
{ /* <AddField /> */}
</Col>
</Row>
</Container>)

@ -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 (
<Container className="params flex-grow-1 my-5 mt-5 flex-column d-flex justify-content-between">
<div className="d-flex flex-column">
@ -40,8 +20,14 @@ const ParamsField = ({onSubmit}) => {
<Grades />
<Private />
</div>
<div className="mb-5 d-flex justify-content-center">
<Button outline={true} color="secondary" onClick={onSubmit} disabled={disabled} icon={faArrowRight} position="right">
<div className="my-3 d-flex justify-content-center">
<Button
outline={true}
color="secondary"
onClick={onSubmit}
icon={faArrowRight}
position="right"
>
{t('admin.params-submit')}
</Button>
</div>

@ -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 (<Container className='bg-white container-fluid p-4 mt-1'>
<Row>
<Col className='col-auto me-auto'>
<h4 className='text-dark'>{t('common.grades')}</h4>
<p className='text-muted'>{t('admin.grades-desc')}</p>
<h4 className='text-dark'>{t('admin.private-title')}</h4>
<p className='text-muted'>{t('admin.private-desc')}</p>
</Col>
<Col className='col-auto d-flex align-items-center'>
<Switch toggle={toggle} state={election.restrictVote} />
</Col>
</Row>
{election.restrictVote ? <>
<ListInput onEdit={handleEmails} inputs={election.emails} validator={validateEmail} />
<Row className='text-bg-light bt-3 p-2 text-muted fw-bold'>
<Col className='col-auto'>
<FontAwesomeIcon icon={faCircleInfo} />
</Col>
<Col className='col-auto d-flex align-items-center'>
{t("admin.private-tip")}
</Col>
</Row></> : null}
</Container>)
}

@ -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 }) => {
>
<ModalHeader>{t("Attention vous navez pas votez pour tous les candidats")}</ModalHeader>
<ModalBody>
{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.")}
<Button
className="addButton warningVote my-4"
onClick={() => {action();}}>

@ -67,7 +67,11 @@
"admin.optional": "optional",
"admin.photo-import": "Import a picture",
"admin.photo-type": "Supported type:",
"admin.grades-desc": "You can choose to customize the name and the number of mentions. In case of doubt, keep the default mentions.",
"admin.grades-desc": "You can choose to customize the name and the number of mentions. If in doubt, leave the grades as is.",
"admin.ending-in": "In",
"admin.until": "Until"
"admin.until": "Until",
"admin.private-title": "Private vote",
"admin.private-desc": "Only participants who received an invite by email will be able to vote",
"admin.private-tip": "You can copy-paste a list of emails from a spreadsheet.",
"admin.private-placeholder": "Add here the emails of the participants."
}

@ -69,5 +69,10 @@
"admin.photo-type": "Format supporté :",
"admin.grades-desc": "Vous pouvez choisir de personaliser le nom et le nombre de mentions. En cas de doute, gardez les mentions par défaut.",
"admin.ending-in": "Dans",
"admin.until": "Jusqu'au"
"admin.until": "Jusqu'au",
"admin.private-title": "Vote privé",
"admin.private-desc": "Uniquement les personnes invités par mail pourront participé au vote",
"admin.private-tip": "Vous pouvez copier-coller une liste d'emails depuis un tableur.",
"admin.private-placeholder": "Ajoutez ici les emails des participants."
}

@ -4,5 +4,23 @@
export const MAX_NUM_CANDIDATES = process.env.MAX_NUM_CANDIDATES || 1000;
export const CONTACT_MAIL = process.env.CONTACT_MAIL || "app@mieuxvoter.fr";
export const DEFAULT_GRADES = process.env.DEFAULT_GRADES || ['grades.very-good', 'grades.good', 'grades.passable', 'grades.inadequate', 'grades.mediocre'];
export const DEFAULT_GRADES = process.env.DEFAULT_GRADES ? process.env.DEFAULT_GRADES.split(",") : ['grades.very-good', 'grades.good', 'grades.passable', 'grades.inadequate', 'grades.mediocre'];
export const IMGPUSH_URL = process.env.IMGPUSH_URL || 'https://imgpush.mieuxvoter.fr';
export const GRADE_CLASSES = [
"to-reject",
"insufficient",
"passable",
"fair",
"good",
"very-good",
"excellent"
];
export const GRADE_COLORS = [
"#3A9918",
"#A0CF1C",
"#D3D715",
"#C2B113",
"#C27C13",
"#C23D13",
"#F2F0FF",
];

@ -376,10 +376,14 @@ ol.result > li {
}
.text-muted {
color: inherit;
color: #8F88BA!important;
opacity: 0.5;
}
.text-bg-light {
background: #F2F0FF!important;
}
.cursor-pointer {
cursor: pointer;
}
@ -390,3 +394,8 @@ ol.result > li {
.noshadow {
box-shadow: unset !important;
}
.list-input input[type="text"]:focus {
outline: none;
}

@ -24,6 +24,19 @@ $theme-colors: (
"warning": #ff6e11,
);
// $grade-colors: (
// "to-reject": #F2F0FF,
// "insufficient": #C23D13,
// "passable": #C27C13,
// "fair": #C2B113,
// "good": #D3D715,
// "very-good": #A0CF1C,
// "excellent": #3A9918,
// );
@import "_bootstrap.scss";
@import "app.scss";
@import "_button.scss";
@ -34,7 +47,6 @@ $theme-colors: (
@import "_modal.scss";
@import "_homePage.scss";
@import "_vote.scss";
// @import "_newVote.scss";
@import "_resultVote.scss";
@import "_admin.scss";
@import "_switch.scss";

Loading…
Cancel
Save