From c8ae7147e6b35dc9d8d8833fb420cb5049f60c2f Mon Sep 17 00:00:00 2001 From: Pierre-Louis Guhur Date: Sat, 29 Oct 2022 17:17:05 +0200 Subject: [PATCH] wip: refactor --- components/Alert.jsx | 30 + components/Error.jsx | 19 +- components/Logo.jsx | 2 +- components/Modal.jsx | 52 +- components/admin/ElectionContext.jsx | 63 + components/form/AlertButton.jsx | 24 - components/form/CandidateField.jsx | 350 +- components/form/CandidatesField.jsx | 113 +- components/layouts/Footer.jsx | 76 +- components/layouts/Header.jsx | 65 +- components/layouts/HeaderMobile.jsx | 33 +- components/layouts/LanguageSelector.jsx | 12 +- next-i18next.config.js | 44 +- next.config.js | 2 +- package-lock.json | 19030 +++++++--------------- package.json | 57 +- pages/_app.jsx | 28 +- pages/{new => admin}/confirm/[pid].jsx | 0 pages/{new/index.js => admin/new.js} | 217 +- pages/{new => admin}/settings/index.js | 72 +- pages/index.jsx | 67 +- pages/legal-notices.jsx | 12 +- pages/result/[pid]/[[...tid]].jsx | 62 +- public/locales/de/common.json | 1 - public/locales/de/resource.json | 100 - public/locales/en/common.json | 8 - public/locales/en/emailInvite.json | 10 - public/locales/en/error.json | 11 - public/locales/en/locale.json | 1 - public/locales/en/resource.json | 59 +- public/locales/es/common.json | 1 - public/locales/es/locale.json | 1 - public/locales/es/resource.json | 108 - public/locales/fr/common.json | 8 - public/locales/fr/emailInvite.json | 10 - public/locales/fr/error.json | 11 - public/locales/fr/locale.json | 53 - public/locales/fr/resource.json | 54 +- public/locales/ru/common.json | 1 - public/locales/ru/locale.json | 1 - public/locales/ru/resource.json | 107 - services/constants.js | 7 + services/date.js | 37 + services/routes.js | 5 + styles/scss/_app.scss | 70 - styles/scss/_bootstrap.scss | 3 +- styles/scss/_buttons.scss | 5 + styles/scss/_footer.scss | 59 +- styles/scss/_header.scss | 17 - styles/scss/_homePage.scss | 19 - 50 files changed, 6445 insertions(+), 14752 deletions(-) create mode 100644 components/Alert.jsx create mode 100644 components/admin/ElectionContext.jsx delete mode 100644 components/form/AlertButton.jsx rename pages/{new => admin}/confirm/[pid].jsx (100%) rename pages/{new/index.js => admin/new.js} (83%) rename pages/{new => admin}/settings/index.js (92%) delete mode 100644 public/locales/de/common.json delete mode 100644 public/locales/de/resource.json delete mode 100644 public/locales/en/common.json delete mode 100644 public/locales/en/emailInvite.json delete mode 100644 public/locales/en/error.json delete mode 100644 public/locales/en/locale.json delete mode 100644 public/locales/es/common.json delete mode 100644 public/locales/es/locale.json delete mode 100644 public/locales/es/resource.json delete mode 100644 public/locales/fr/common.json delete mode 100644 public/locales/fr/emailInvite.json delete mode 100644 public/locales/fr/error.json delete mode 100644 public/locales/fr/locale.json delete mode 100644 public/locales/ru/common.json delete mode 100644 public/locales/ru/locale.json delete mode 100644 public/locales/ru/resource.json create mode 100644 services/constants.js create mode 100644 services/date.js create mode 100644 services/routes.js diff --git a/components/Alert.jsx b/components/Alert.jsx new file mode 100644 index 0000000..fa80190 --- /dev/null +++ b/components/Alert.jsx @@ -0,0 +1,30 @@ +import {UncontrolledAlert} from 'reactstrap'; +import {useTranslation} from "next-i18next"; + +const AlertDismissible = ({msg, color}) => { + const {t} = useTranslation(); + + if (msg) { + return ( + +

{t(msg)}

+

+ + {t("error.help")} + +

+
+ ); + } + return null; +} + + +AlertDismissible.defaultProps = { + color: 'danger' +}; + +export default AlertDismissible; diff --git a/components/Error.jsx b/components/Error.jsx index 9f30dfd..71387ef 100644 --- a/components/Error.jsx +++ b/components/Error.jsx @@ -1,9 +1,10 @@ import Link from "next/link"; -import { Container, Row, Col } from "reactstrap"; -import { useTranslation } from "next-i18next"; +import {Container, Row, Col} from "reactstrap"; +import {useTranslation} from "next-i18next"; +import {CONTACT_MAIL} from '@services/constants'; -const Error = (props) => { - const { t } = useTranslation(); +const Error = ({msg}) => { + const {t} = useTranslation(); return ( @@ -15,25 +16,25 @@ const Error = (props) => { -

{props.value}

+

{t(msg)}

- {t("common.backHomepage")} + {t("common.back-homepage")} - {t("resource.help")} + {t("error.help")} -
+ ); }; diff --git a/components/Logo.jsx b/components/Logo.jsx index 8324c6d..060f2df 100644 --- a/components/Logo.jsx +++ b/components/Logo.jsx @@ -16,7 +16,7 @@ const Logo = props => { return ( {t('logo-alt')} diff --git a/components/Modal.jsx b/components/Modal.jsx index f4b6f31..931d66d 100644 --- a/components/Modal.jsx +++ b/components/Modal.jsx @@ -1,7 +1,6 @@ -import React, {useEffect, useRef, useState} from "react"; -import ReactDOM from "react-dom"; -import styled from "styled-components"; - +// TODO use bootstrap modal +// https://getbootstrap.com/docs/5.0/components/modal/ +// const Modal = ({show, onClose, children, title}) => { const handleCloseClick = (e) => { @@ -10,17 +9,17 @@ const Modal = ({show, onClose, children, title}) => { }; const modalContent = show ? ( - - - +
+
+
x - - {title && {title}} - {children} - - +
+ {title &&
{title}
} +
{children}
+
+
) : null; @@ -30,34 +29,5 @@ const Modal = ({show, onClose, children, title}) => { }; -const StyledModalBody = styled.div` - padding-top: 10px; - `; - -const StyledModalHeader = styled.div` - display: flex; - justify-content: flex-end; - font-size: 25px; - `; - -const StyledModal = styled.div` - background: white; - width: 500px; - height: 600px; - border-radius: 15px; - padding: 15px; - `; -const StyledModalTitle = styled.div -const StyledModalOverlay = styled.div` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - background-color: rgba(0, 0, 0, 0.5); - `; export default Modal; diff --git a/components/admin/ElectionContext.jsx b/components/admin/ElectionContext.jsx new file mode 100644 index 0000000..e214751 --- /dev/null +++ b/components/admin/ElectionContext.jsx @@ -0,0 +1,63 @@ +import {createContext, useContext, useReducer} from 'react'; + +const TasksContext = createContext(null); +const TasksDispatchContext = createContext(null); + +export function TasksProvider({children}) { + const [tasks, dispatch] = useReducer( + tasksReducer, + initialTasks + ); + + return ( + + + {children} + + + ); +} + +export function useTasks() { + return useContext(TasksContext); +} + +export function useTasksDispatch() { + return useContext(TasksDispatchContext); +} + +function tasksReducer(tasks, action) { + switch (action.type) { + case 'added': { + return [...tasks, { + id: action.id, + text: action.text, + done: false + }]; + } + case 'changed': { + return tasks.map(t => { + if (t.id === action.task.id) { + return action.task; + } else { + return t; + } + }); + } + case 'deleted': { + return tasks.filter(t => t.id !== action.id); + } + default: { + throw Error('Unknown action: ' + action.type); + } + } +} + +const initialTasks = [ + {id: 0, text: 'Philosopher’s Path', done: true}, + {id: 1, text: 'Visit the temple', done: false}, + {id: 2, text: 'Drink matcha', done: false} +]; + diff --git a/components/form/AlertButton.jsx b/components/form/AlertButton.jsx deleted file mode 100644 index 10c1e0a..0000000 --- a/components/form/AlertButton.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useState } from 'react' -import { Alert, Button } from 'react-bootstrap'; -import { faTimes, faExclamationCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -export default function AlertDismissibleExample() { - const [show, setShow] = useState(true); - - if (show) { - return ( - - -
- - 2 candidats minimum -
- setShow(false)} icon={faTimes} className="mr-2" /> -
- -
- ); - } - return null; -} - diff --git a/components/form/CandidateField.jsx b/components/form/CandidateField.jsx index beb215a..28228b0 100644 --- a/components/form/CandidateField.jsx +++ b/components/form/CandidateField.jsx @@ -1,186 +1,170 @@ -import {useState, useEffect} from 'react' -import ButtonWithConfirm from "./ButtonWithConfirm"; -import TrashButton from "./TrashButton"; -import { - Row, - Col, - Label, - Input, - InputGroup, - InputGroupAddon, - Button, Modal, ModalHeader, ModalBody, Form -} from "reactstrap"; -import {useTranslation} from "react-i18next"; -import { - sortableHandle -} from "react-sortable-hoc"; -import HelpButton from "@components/form/HelpButton"; -import AddPicture from "@components/form/AddPicture"; -import { - faPlus, faCogs, faCheck, faTrash -} from "@fortawesome/free-solid-svg-icons"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -const DragHandle = sortableHandle(({children}) => ( - {children} -)); - -const CandidateField = ({avatar, label, description, candIndex, onDelete, onAdd, ...inputProps}) => { - const {t} = useTranslation(); - const [visibled, setVisibility] = useState(false); - const toggle = () => setVisibility(!visibled) - - - - const [selected, setSelectedState] = useState(false); - const [className, setClassName] = useState("none"); - const [trashIcon, setTrashIcon] = useState("none"); - const [plusIcon, setPlusIcon] = useState("none"); - - const addCandidate = () => { - if (label != "") { - toggle(); - onAdd(); - setSelectedState(!selected); - } - else {} - } - if (label != "") { - const type = "button"; - } - else { - const type = "submit"; - } - - useEffect(() => { - setClassName("candidateButton " + (selected ? "candidateAdded" : "")) - }, [selected]); - useEffect(() => { - setPlusIcon("mr-2 cursorPointer " + (selected ? "trashIcon" : "")) - }, [selected]); - useEffect(() => { - setTrashIcon("trashIcon " + (selected ? "displayTrash" : "")) - }, [selected]); - - const addFunction = () => { - addCandidate(); - setSelectedState(!selected); - } - const removeCandidate = () => { - onDelete(); - toggle(); - - } - - - - const [image, setImage] = useState(null); - const [createObjectURL, setCreateObjectURL] = useState(null); - - const uploadToClient = (event) => { - if (event.target.files && event.target.files[0]) { - const i = event.target.files[0]; - setImage(i); - setCreateObjectURL(URL.createObjectURL(i)); - } - }; - - - - - - - - - - return ( - -
-
- - -
- - -
-
- - - - - - - - - -
- - -
Ajouter un participant
-

Ajoutez une photo, le nom et une description au candidat.

-
-
-
- -
-
-
-

Photo (facultatif)

- -

Importer une photo.
format : jpg, png, pdf

-
- - -
-
-
- -
-
- - - - - - - - - - - -
-
- -
- {/* - - {t( - "Enter the name of your candidate or proposal here (250 characters max.)" - )} - - */} -
- ); +// import {useState, useEffect} from 'react' +// import ButtonWithConfirm from "./ButtonWithConfirm"; +// import TrashButton from "./TrashButton"; +// import { +// Row, +// Col, +// Label, +// Input, +// InputGroup, +// InputGroupAddon, +// Button, Modal, ModalHeader, ModalBody, Form +// } from "reactstrap"; +// import {useTranslation} from "react-i18next"; +// import HelpButton from "@components/form/HelpButton"; +// import AddPicture from "@components/form/AddPicture"; +// import { +// faPlus, faCogs, faCheck, faTrash +// } from "@fortawesome/free-solid-svg-icons"; +// import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +// const DragHandle = sortableHandle(({children}) => ( +// {children} +// )); + +// const CandidateField = ({avatar, label, description, candIndex, onDelete, onAdd, ...inputProps}) => { +const CandidateField = (props) => { + // const {t} = useTranslation(); + // const [visibled, setVisibility] = useState(false); + // const toggle = () => setVisibility(!visibled) + + // const [selected, setSelectedState] = useState(false); + // const [className, setClassName] = useState("none"); + // const [trashIcon, setTrashIcon] = useState("none"); + // const [plusIcon, setPlusIcon] = useState("none"); + + // const addCandidate = () => { + // if (label != "") { + // toggle(); + // onAdd(); + // setSelectedState(!selected); + // } + // else {} + // } + // const type = label != "" ? "button" : "submit"; + + // useEffect(() => { + // setClassName("candidateButton " + (selected ? "candidateAdded" : "")) + // }, [selected]); + // useEffect(() => { + // setPlusIcon("mr-2 cursorPointer " + (selected ? "trashIcon" : "")) + // }, [selected]); + // useEffect(() => { + // setTrashIcon("trashIcon " + (selected ? "displayTrash" : "")) + // }, [selected]); + + // const addFunction = () => { + // addCandidate(); + // setSelectedState(!selected); + // } + // const removeCandidate = () => { + // onDelete(); + // toggle(); + + // } + + //const [image, setImage] = useState(null); + // const [createObjectURL, setCreateObjectURL] = useState(null); + + // const uploadToClient = (event) => { + // if (event.target.files && event.target.files[0]) { + // const i = event.target.files[0]; + // setImage(i); + // setCreateObjectURL(URL.createObjectURL(i)); + // } + // }; + + return (

FOO

); + // return ( + // + //
+ //
+ // + // + //
+ // + // + //
+ //
+ // + // + // + // + // + // + // + // + // + //
+ // + // { // + // } + //
Ajouter un participant
+ //

Ajoutez une photo, le nom et une description au candidat.

+ //
+ //
+ //
+ // + //
+ //
+ //
+ //

Photo (facultatif)

+ // + //

Importer une photo.
format : jpg, png, pdf

+ //
+ // + // + //
+ //
+ //
+ // + // { //
+ // } + // + // + // + // + // + // + // + // + // + // + // + // + //
+ //
+ // + //
+ // {/* + // + // {t( + // "Enter the name of your candidate or proposal here (250 characters max.)" + // )} + // + // */} + //
+ // ); } export default CandidateField diff --git a/components/form/CandidatesField.jsx b/components/form/CandidatesField.jsx index 65707dc..76085cd 100644 --- a/components/form/CandidatesField.jsx +++ b/components/form/CandidatesField.jsx @@ -1,102 +1,116 @@ import {useState, useEffect, createRef} from 'react' import {useTranslation} from "react-i18next"; -import { - Button, - Card, - CardBody -} from "reactstrap"; -import { - faPlus, -} from "@fortawesome/free-solid-svg-icons"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import { - sortableContainer, - sortableElement, - sortableHandle -} from "react-sortable-hoc"; -import arrayMove from "array-move" +// import {DndContext, useDroppable} from '@dnd-kit/core'; import CandidateField from './CandidateField' -import AlertDismissibleExample from './AlertButton' +import Alert from '@components/Alert' +import {MAX_NUM_CANDIDATES} from '@services/constants'; + + +// export function CandidateList(props) { +// const {isOver, setNodeRef} = useDroppable({ +// id: props.id, +// }); +// const style = { +// opacity: isOver ? 1 : 0.5, +// }; +// +// return ( +//
+// {props.children} +//
+// ); +// } + + // const SortableItem = sortableElement(({className, ...childProps}) =>
  • ); // // const SortableContainer = sortableContainer(({children}) => { // return
      {children}
    ; // }); -const SortableItem = ({className, ...childProps}) =>
  • ; -const SortableContainer = ({children}) => { - return
      {children}
    ; -}; +// const arrayMove = (arr, fromIndex, toIndex) => { +// // https://stackoverflow.com/a/6470794/4986615 +// const element = arr[fromIndex]; +// arr.splice(fromIndex, 1); +// arr.splice(toIndex, 0, element); +// return arr +// } const CandidatesField = ({onChange}) => { const {t} = useTranslation(); - const [candidates, setCandidates] = useState([]) + const createCandidate = () => ({label: "", description: "", fieldRef: createRef()}) + + // Initialize the list with at least two candidates + const [candidates, setCandidates] = useState([createCandidate(), createCandidate()]) + const [error, setError] = useState(null) const addCandidate = () => { - if (candidates.length < 1000) { - candidates.push({label: "", description: "", fieldRef: createRef()}); - setCandidates([...candidates]); - onChange(candidates) + if (candidates.length < MAX_NUM_CANDIDATES) { + setCandidates( + c => { + c.push(createCandidate()); + return c + } + ); } else { - console.error("Too many candidates") + setError('error.too-many-candidates') } }; - useEffect(() => { - addCandidate(); - }, []) + // What to do when we change the candidates + useEffect(() => { + onChange(); + }, [candidates]) const removeCandidate = index => { if (candidates.length === 1) { - const newCandidates = [] - newCandidates.push({label: "", fieldRef: createRef()}); - setCandidates(newCandidates); - onChange(newCandidates) + setCandidates([createCandidate()]); } else { - const newCandidates = candidates.filter((c, i) => i != index) - setCandidates(newCandidates); - onChange(newCandidates); + setCandidates(oldCandidates => + oldCandidates.filter((_, i) => i != index) + ); } }; const editCandidate = (index, label) => { - candidates[index].label = label - setCandidates([...candidates]); - onChange(candidates); + setCandidates( + oldCandidates => { + oldCandidates[index].label = label; + return oldCandidates; + } + ) }; const handleKeyPress = (e, index) => { - if (e.key === "Enter") { + if (e.key !== "Enter") { e.preventDefault(); if (index + 1 === candidates.length) { addCandidate(); } - else { - - }candidates[index + 1].fieldRef.current.focus(); + candidates[index + 1].fieldRef.current.focus(); } - + } const onSortEnd = ({oldIndex, newIndex}) => { - setCandidates(arrayMove(candidates, oldIndex, newIndex)); + setCandidates(c => arrayMove(c, oldIndex, newIndex)); }; + return (

    Saisissez ici le nom de vos candidats.

    - - + {candidates.map((candidate, index) => { const className = "sortable" return ( - { /> ) })} -
    -
    + ); } diff --git a/components/layouts/Footer.jsx b/components/layouts/Footer.jsx index 877cfb4..864ac7e 100644 --- a/components/layouts/Footer.jsx +++ b/components/layouts/Footer.jsx @@ -10,24 +10,78 @@ const Footer = () => { const linkStyle = {whiteSpace: "nowrap"}; const {t} = useTranslation(); - const [bboxLink1, link1] = useBbox(); - const [bboxLink2, link2] = useBbox(); - const [bboxLink3, link3] = useBbox(); - const [bboxLink4, link4] = useBbox(); - const [bboxLink5, link5] = useBbox(); + // const [bboxLink1, link1] = useBbox(); + // const [bboxLink2, link2] = useBbox(); + // const [bboxLink3, link3] = useBbox(); + // const [bboxLink4, link4] = useBbox(); + // const [bboxLink5, link5] = useBbox(); //{t("menu.majority-judgment")} + ) + }, + { + component: ( + + {t("menu.whoarewe")} + + ) + }, + { + component: ( + + {t("menu.faq")} + + ) + }, + { + component: ( + + {t("menu.news")} + + ) + }, + { + component: ( + + Nous contacter + + ) + }, + { + component: ( +
    + ) + } + ] + return (