diff --git a/components/Bulles.jsx b/components/Bulles.jsx new file mode 100644 index 0000000..5e5d23d --- /dev/null +++ b/components/Bulles.jsx @@ -0,0 +1,166 @@ +import React from 'react'; +import plotly from 'plotly.js/dist/plotly'; +import createPlotComponent from 'react-plotly.js/factory'; +import LoadingScreen from "./LoadingScreen"; + +function Bulles (props) { + +// récupération des résultats de l'élection et stockage en tableau +const votesBrut = (Object.values(props))[0]; + +// déclaration et initialisation des mentions et couleurs +const mentionsBrut = ['Passable', 'Assez bien', 'Bien', 'Très bien', 'Excellent']; +const couleursBrut = ['#BB9C42', '#AABA44', '#DCDF44', '#B3D849', '#61AD45']; + +//----------- Traitement des données -----------// + +// fonction d'inversement des éléments de tableau +function inverse(obj){ + var retobj = {}; + for(var key in obj){ + retobj[obj[key]] = key; + } + return retobj; + } + +// fonction de réduction d'amplitude permettant de conserver une représentation ordinale du nombre de votes sans décalage visuel trop important +/* +Pattern de calcul : + +Soient Ai, Bi, Ci, Di, Ei les nombres de votes initiaux fournis dans le tableau classé par ordre mélioratif de mention (de Passable à Excellent). Il vient : +A = 1 +B = <{[1 + (Bi/Ai)] / 40} * A> +C = <{[1 + (Ci/Bi)] / 40} * B> +D = <{[1 + (Di/Ci)] / 40} * C> +E = <{[1 + (Ei/Di)] / 40} * D> +*/ +function redAmpli(tab) { + var nvTab = []; + nvTab[0] = 100; + + for(i = 1; i < tab.length; i++) { + nvTab[i] = ( (1 + ((tab[i]/tab[(i-1)]) / 40 ) ) * nvTab[(i-1)]); + } + return nvTab; +} + + +// déclaration de l'objet votes-mention et votes-couleur +var votesMentionNonOrdonnes = {}; +var votesCouleurNonOrdonnes = {}; + +// initialisation votes-mention ordonnés croissants +for (var i = 0; i < mentionsBrut.length; i++) { + votesMentionNonOrdonnes[votesBrut[i]] = mentionsBrut[i]; + votesCouleurNonOrdonnes[votesBrut[i]] = couleursBrut[i]; +} + +// déclaration des mentions-votes par ordre croissant +var votesMentionOrdonnes = inverse(votesMentionNonOrdonnes); +var votesCouleurOrdonnes = inverse(votesCouleurNonOrdonnes); + +// vérification du nombre de votes classés par ordre croissant et passés initialement en propriétés au composant +console.log("Les données transmises au composant concernant le nombre de votes par mention sont : "); +console.log(votesBrut); + +// vérification des mentions destinées à être associées aux votes et ordonnées initialement par ordre mélioratif +console.log("Les mentions des votes sont classées initialement par ordre mélioratif de la façon suivante :"); +console.log(mentionsBrut); + +// vérification du nombre de votes classés par ordre croissant +console.log("Les mentions-votes classées par ordre croissant de votes sont : "); +console.log(votesMentionOrdonnes); + +// séparation des mentions et des votes +const mentions = Object.keys(votesMentionOrdonnes); +const votes = Object.values(votesMentionOrdonnes); +const couleurs = Object.keys(votesCouleurOrdonnes); + +// vérification des mentions et des votes prêts à être traités pour la représentation graphique +console.log('La liste des mentions issue du classement par ordre croissant de votes est :'); +console.log(mentions); +console.log('La liste du nombre de votes correspondant, classée par ordre croissant, est :'); +console.log(votes); + +// déclaration et initialisation des rayons de bulle pour la représentation graphique +var rayons = []; +rayons = redAmpli(votes) + +// vérification des rayons +console.log('La liste des rayons à représenter graphiquement est la suivante :'); +console.log(rayons); + +// déclaration et initialisation des textes des bulles +const texteBulle1 = (mentions[0] + "
" + votes[0] + " votes").toString(); +const texteBulle2 = (mentions[1] + "
" + votes[1] + " votes").toString(); +const texteBulle3 = (mentions[2] + "
" + votes[2] + " votes").toString(); +const texteBulle4 = (mentions[3] + "
" + votes[3] + " votes").toString(); +const texteBulle5 = (mentions[4] + "
" + votes[4] + " votes").toString(); + +// déclaration et initialisation d'une instance de graphique en bulles +// const Plot = createPlotComponent(plotly); +const Plot = require('react-plotly.js').default; + +//---------------------------------------------// + + + +//----------- Affichage des données -----------// +const [loading, setLoading] = React.useState(true); + React.useEffect(() =>{ + setTimeout(() => setLoading(false), 3000); + }) +return ( + + //
+ // {!loading ? ( + // + %{text}' + + '', + text: [texteBulle1, texteBulle2, texteBulle3, texteBulle4, texteBulle5], + showlegend: false, + mode: 'markers', + marker: { + color: [couleurs[0], couleurs[1], couleurs[2], couleurs[3], couleurs[4]], + size: rayons + } + } + ]} + layout={ { + width: 600, + height: 600, + title: 'Nombre de voix par candidat', + xaxis: { + showgrid: false, + showticklabels: false, + showline: false, + zeroline: false, + range: [0, 1] + }, + yaxis: { + showgrid: false, + showticklabels: false, + showline: false, + zeroline: false, + range: [0, 1] + } + } } + config={{ + displayModeBar: false // this is the line that hides the bar. + }} +/> +// +// ) : ( +// +// )} +//
+) +} + +export default Bulles; \ No newline at end of file diff --git a/components/Chart.tsx b/components/Chart.tsx new file mode 100644 index 0000000..c093c58 --- /dev/null +++ b/components/Chart.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; +import * as d3 from "d3"; + +function drawChart(svgRef: React.RefObject) { + const data = [12, 5, 6, 6, 9, 10]; + const h = 120; + const w = 250; + const svg = d3.select(svgRef.current); + + svg + .attr("width", w) + .attr("height", h) + .style("margin-top", 50) + .style("margin-left", 50); + + svg + .selectAll("rect") + .data(data) + .enter() + .append("rect") + .attr("x", (d, i) => i * 40) + .attr("y", (d, i) => h - 10 * d) + .attr("width", 20) + .attr("height", (d, i) => d * 10) + .attr("fill", "steelblue"); +} + +const Chart: React.FunctionComponent = () => { + const svg = React.useRef(null); + React.useEffect(() => { + drawChart(svg); + }, [svg]); + + return ( +
+ +
+ ); +}; + +export default Chart; diff --git a/components/ChartWrapper.js b/components/ChartWrapper.js new file mode 100644 index 0000000..de849cd --- /dev/null +++ b/components/ChartWrapper.js @@ -0,0 +1,21 @@ +import React, { useRef, useState, useEffect } from 'react'; +import D3Chart from './D3Chart'; + +const ChartWrapper = () => { + + const chartArea = useRef(null); + const [chart, setChart] = useState(null); + + useEffect(() => { + if (!chart) { + setChart(new D3Chart(chartArea.current)); + } + }, [chart]); + + return ( +
+ ); + +} + +export default ChartWrapper; \ No newline at end of file diff --git a/components/CopyField.jsx b/components/CopyField.jsx new file mode 100644 index 0000000..bc37856 --- /dev/null +++ b/components/CopyField.jsx @@ -0,0 +1,69 @@ +/* eslint react/prop-types: 0 */ +import React from "react"; +import { Button } from "reactstrap"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCopy, + faVoteYea, + faExclamationTriangle, + faExternalLinkAlt, +} from "@fortawesome/free-solid-svg-icons"; + +const CopyField = (props) => { + const ref = React.createRef(); + const handleClickOnField = (event) => { + event.target.focus(); + event.target.select(); + }; + const handleClickOnButton = () => { + const input = ref.current; + input.focus(); + input.select(); + document.execCommand("copy"); + }; + + const { t, value, iconCopy, iconOpen } = props; + + return ( +
+ + +
+ {/* + + */} + +
+
+ ); +}; + +CopyField.defaultProps = { + iconCopy: faCopy, + iconOpen: faExternalLinkAlt, +}; + +export default CopyField; diff --git a/components/D3Chart.js b/components/D3Chart.js new file mode 100644 index 0000000..dd62855 --- /dev/null +++ b/components/D3Chart.js @@ -0,0 +1,38 @@ +import * as d3 from 'd3'; + +const url = "https://udemy-react-d3.firebaseio.com/tallest_men.json"; + +const WIDTH = 800; +const HEIGHT = 500; + +export default class D3Chart { + constructor(element) { + const svg = d3.select(element) + .append("svg") + .attr("width", 800) + .attr("height", 500) + + d3.json(url).then(data => { + const max = d3.max(data, d => d.height) + const y = d3.scaleLinear() + .domain([0, max]) + .range([0, HEIGHT]) + + const x = d3.scaleBand() + .domain(data.map(d => d.name)) + .range([0, WIDTH]) + .padding(0.4) + + const rects = svg.selectAll("rect") + .data(data) + + rects.enter() + .append("rect") + .attr("x", d => x(d.name)) + .attr("y", d => HEIGHT - y(d.height)) + .attr("width", x.bandwidth) + .attr("height", d => y(d.height)) + .attr("fill", "grey") + }) + } +} \ No newline at end of file diff --git a/components/Error.jsx b/components/Error.jsx new file mode 100644 index 0000000..dc855db --- /dev/null +++ b/components/Error.jsx @@ -0,0 +1,40 @@ +import Link from "next/link"; +import { Container, Row, Col } from "reactstrap"; +import { useTranslation } from "next-i18next"; + +const Error = (props) => { + const { t } = useTranslation(); + return ( + + + + + logo + + + + + +

{props.value}

+ +
+ + + + {t("common.backHomepage")} + + + + + {t("resource.help")} + + + +
+ ); +}; + +export default Error; diff --git a/components/LoadingScreen.js b/components/LoadingScreen.js new file mode 100644 index 0000000..c148442 --- /dev/null +++ b/components/LoadingScreen.js @@ -0,0 +1,75 @@ +import React from "react" +import styled from "styled-components" + +const Screen = styled.div` + position: relative; + + opacity: 0; + animation: fade 0.4s ease-in forwards; + background: black; + + @keyframes fade { + 0% { + opacity: 0.4; + } + 50% { + opacity: 0.8; + } + 100% { + opacity: 1; + } + } +`; + +const Balls = styled.div` + display: flex; + + + .ball { + height: 20px; + width: 20px; + border-radius: 50%; + background: red; + margin: 0 6px 0 0; + animation: oscillate 0.7s ease-in forwards infinite; + } + + .one { + animation-delay: 0.5s; + } + .two { + animation-delay: 1s; + } + .three { + animation-delay: 2s; + } + + @keyframes oscillate { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(20px); + } + 100% { + transform: translateY(0); + } + } +`; + + + +const LoadingScreen = () => { + return ( + + + +
+
+
+
+
+ ); +}; + +export default LoadingScreen; diff --git a/components/Modal.jsx b/components/Modal.jsx new file mode 100644 index 0000000..c502450 --- /dev/null +++ b/components/Modal.jsx @@ -0,0 +1,62 @@ +import React, { useEffect, useRef, useState } from "react"; +import ReactDOM from "react-dom"; +import styled from "styled-components"; + +const Modal = ({ show, onClose, children, title }) => { + + const handleCloseClick = (e) => { + e.preventDefault(); + onClose(); + }; + + const modalContent = show ? ( + + + + + x + + + {title && {title}} + {children} + + + ) : null; + + + return ( + modalContent + ); + + }; + + 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 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; \ No newline at end of file diff --git a/components/SystemeVote.jsx b/components/SystemeVote.jsx new file mode 100644 index 0000000..62df611 --- /dev/null +++ b/components/SystemeVote.jsx @@ -0,0 +1,37 @@ +import React, {Fragment} from 'react'; +import Head from 'next/head'; +import dynamic from 'next/dynamic'; + + +const Bulles = dynamic(import('./Bulles'), { + ssr: false +}) + +const nbVotesPassables = 15; +const nbVotesAssezBien = 200; +const nbVotesBien = 389; +const nbVotesTresBien = 12; +const nbVotesExcellent = 2; + +const resultats = [nbVotesPassables, nbVotesAssezBien, nbVotesBien, nbVotesTresBien, nbVotesExcellent]; + +var totalVotes = 0; + +for(var i = 0; i < resultats.length; i++) { + totalVotes += resultats[i]; +} + +function SystemeVote() { + + + return ( + + + +

Le total des votes est de {totalVotes}.

+ +
+ ); +} + +export default SystemeVote; \ No newline at end of file diff --git a/components/banner/Facebook.jsx b/components/banner/Facebook.jsx new file mode 100644 index 0000000..e710a23 --- /dev/null +++ b/components/banner/Facebook.jsx @@ -0,0 +1,29 @@ +/* eslint react/prop-types: 0 */ +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFacebookSquare } from "@fortawesome/free-brands-svg-icons"; + +const Facebook = props => { + const handleClick = () => { + const url = + "https://www.facebook.com/sharer.php?u=" + + props.url + + "&t=" + + props.title; + window.open( + url, + "", + "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=500,width=700" + ); + }; + return ( + + ); +}; + +export default Facebook; + +//i diff --git a/components/banner/Gform.jsx b/components/banner/Gform.jsx new file mode 100644 index 0000000..cd20281 --- /dev/null +++ b/components/banner/Gform.jsx @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faCommentAlt} from "@fortawesome/free-solid-svg-icons"; +import {api} from "@services/api" + + +const Gform = (props) => { + return ( + + + Votre avis nous intéresse ! + + ); +} + +Gform.propTypes = { + className: PropTypes.string, +}; + +export default Gform; diff --git a/components/banner/Helloasso.jsx b/components/banner/Helloasso.jsx new file mode 100644 index 0000000..eeaa595 --- /dev/null +++ b/components/banner/Helloasso.jsx @@ -0,0 +1,24 @@ +/* eslint react/prop-types: 0 */ +import React from "react"; +import i18n from "../../i18n"; + +const Helloasso = props => { + const locale = + i18n.language.substring(0, 2).toLowerCase() === "fr" ? "fr" : "en"; + const linkHelloAssoBanner = + locale === "fr" + ? "https://www.helloasso.com/associations/mieux-voter/formulaires/1/widget" + : "https://www.helloasso.com/associations/mieux-voter/formulaires/1/widget/en"; + + return ( + + support us on helloasso + + ); +}; + +export default Helloasso; diff --git a/components/banner/Paypal.jsx b/components/banner/Paypal.jsx new file mode 100644 index 0000000..fdec7dc --- /dev/null +++ b/components/banner/Paypal.jsx @@ -0,0 +1,44 @@ +import {useTranslation} from "next-i18next"; +import {useRouter} from "next/router" +import {faPaypal} from "@fortawesome/free-brands-svg-icons"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; + +const Paypal = () => { + const {t} = useTranslation(); + + // FIXME generate a xx_XX string for locale version + const {locale} = useRouter(); + let localeShort = locale.substring(0, 2); + let localeComplete = + localeShort.toLowerCase() + "_" + localeShort.toUpperCase(); + if (localeComplete === "en_EN") { + localeComplete = "en_US"; + } + const pixelLink = + `https://www.paypal.com/${localeComplete}/i/scr/pixel.gif`; + + return ( +
+
+ + + + +
+
+ ); +}; + +export default Paypal; diff --git a/components/flag.js b/components/flag.js new file mode 100644 index 0000000..df68eaf --- /dev/null +++ b/components/flag.js @@ -0,0 +1,4 @@ +import * as React from "react"; +import FlagIconFactory from "react-flag-icon-css"; + +export const FlagIcon = FlagIconFactory(React, { useCssModules: false }); diff --git a/components/form/AddPicture.jsx b/components/form/AddPicture.jsx new file mode 100644 index 0000000..b858e18 --- /dev/null +++ b/components/form/AddPicture.jsx @@ -0,0 +1,37 @@ +import { useState } from "react"; + + + +export default function AddPicture(props) { + 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 ( +
+
+
+ +
+
+
+

Photo (facultatif)

+ +

Importer une photo.
format : jpg, png, pdf

+
+ + +
+
+
+ + ); +} \ No newline at end of file diff --git a/components/form/AlertButton.jsx b/components/form/AlertButton.jsx new file mode 100644 index 0000000..10c1e0a --- /dev/null +++ b/components/form/AlertButton.jsx @@ -0,0 +1,24 @@ +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/ButtonWithConfirm.jsx b/components/form/ButtonWithConfirm.jsx new file mode 100644 index 0000000..4dc46db --- /dev/null +++ b/components/form/ButtonWithConfirm.jsx @@ -0,0 +1,57 @@ +import {useState} from "react"; +import { + faTrashAlt, +} 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 ButtonWithConfirm = ({className, label, onDelete}) => { + const [visibled, setVisibility] = useState(false); + const {t} = useTranslation(); + + const toggle = () => setVisibility(!visibled) + + return ( +
+ + + {t("Delete?")} + + {t("Are you sure to delete")}{" "} + {label && label !== "" ? ( + "{label}" + ) : ( + <>{t("the row")} + )}{" "} + ? + + + + + + +
+ ); +} + +export default ButtonWithConfirm; diff --git a/components/form/CandidateField.jsx b/components/form/CandidateField.jsx new file mode 100644 index 0000000..08b3c78 --- /dev/null +++ b/components/form/CandidateField.jsx @@ -0,0 +1,143 @@ +import { useState } from 'react' +import ButtonWithConfirm from "./ButtonWithConfirm"; +import { + Row, + Col, + Label, + Input, + InputGroup, + InputGroupAddon, + Button, Modal, ModalHeader, ModalBody, ModalFooter +} 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 +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +const DragHandle = sortableHandle(({ children }) => ( + {children} +)); + +const CandidateField = ({ label, description, candIndex, onDelete, onAdd, ...inputProps }) => { + const { t } = useTranslation(); + const [visibled, setVisibility] = useState(false); + const toggle = () => setVisibility(!visibled) + const test = () => { + toggle(); + onAdd(); + } + 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 candidat + +
+ +
+ + + + + + + + + + + + + + + + + + + + +
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 new file mode 100644 index 0000000..1fdefea --- /dev/null +++ b/components/form/CandidatesField.jsx @@ -0,0 +1,124 @@ +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 CandidateField from './CandidateField' +import AlertDismissibleExample from './AlertButton' +// const SortableItem = sortableElement(({className, ...childProps}) =>
  • ); +// +// const SortableContainer = sortableContainer(({children}) => { +// return ; +// }); + +const SortableItem = ({className, ...childProps}) =>
  • ; + +const SortableContainer = ({children}) => { + return ; +}; + + +const CandidatesField = ({onChange}) => { + const {t} = useTranslation(); + const [candidates, setCandidates] = useState([]) + + const addCandidate = () => { + if (candidates.length < 1000) { + candidates.push({label: "", description: "", fieldRef: createRef()}); + setCandidates([...candidates]); + onChange(candidates) + } else { + console.error("Too many candidates") + } + }; + + useEffect(() => { + addCandidate(); + addCandidate(); + }, []) + + + const removeCandidate = index => { + if (candidates.length === 1) { + const newCandidates = [] + newCandidates.push({label: "", description: "", fieldRef: createRef()}); + newCandidates.push({label: "", description: "", fieldRef: createRef()}); + setCandidates(newCandidates); + onChange(newCandidates) + } + else { + const newCandidates = candidates.filter((c, i) => i != index) + setCandidates(newCandidates); + onChange(newCandidates); + } + }; + + const editCandidate = (index, label, description) => { + candidates[index].label = label + candidates[index].description = description + setCandidates([...candidates]); + onChange(candidates); + }; + + const handleKeyPress = (e, index) => { + if (e.key === "Enter") { + e.preventDefault(); + if (index + 1 === candidates.length) { + addCandidate(); + } + else { + candidates[index + 1].fieldRef.current.focus(); + } + } + } + + const onSortEnd = ({oldIndex, newIndex}) => { + setCandidates(arrayMove(candidates, oldIndex, newIndex)); + }; + + return ( +
    +
    +

    Saisissez ici le nom de vos candidats.

    + + + {candidates.map((candidate, index) => { + const className = "sortable" + return ( + removeCandidate(index)} + onChange={(e) => editCandidate(index, e.target.value)} + onKeyPress={(e) => handleKeyPress(e, index)} + onAdd={addCandidate} + innerRef={candidate.fieldRef} + /> + ) + })} + +
    +
    + ); + +} + + +export default CandidatesField + diff --git a/components/form/ConfirmModal.jsx b/components/form/ConfirmModal.jsx new file mode 100644 index 0000000..fcc23fd --- /dev/null +++ b/components/form/ConfirmModal.jsx @@ -0,0 +1,164 @@ +import {useTranslation} from "next-i18next"; +import {useState} from "react"; +import { + faExclamationTriangle, + faCheck, +} from "@fortawesome/free-solid-svg-icons"; +import {Button, Modal, ModalHeader, ModalBody, ModalFooter} from "reactstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; + +const ConfirmModal = ({tabIndex, title, candidates, grades, isTimeLimited, start, finish, emails, restrictResult, className, confirmCallback}) => { + const [visibled, setVisibility] = useState(false); + const {t} = useTranslation(); + const toggle = () => setVisibility(!visibled) + + return ( +
    + + + + {t("Confirm your vote")} + + +
    +
    + {t("Question of the election")} +
    +
    {title}
    +
    + {t("Candidates/Proposals")} +
    +
    +
      + {candidates.map((candidate, i) => { + if (candidate.label !== "") { + return ( +
    • + {candidate.label} +
    • + ); + } else { + return
    • ; + } + })} +
    +
    +
    +
    + {t("Dates")} +
    +
    + {t("The election will take place from")}{" "} + + {start.toLocaleDateString()}, {t("at")}{" "} + {start.toLocaleTimeString()} + {" "} + {t("to")}{" "} + + {finish.toLocaleDateString()}, {t("at")}{" "} + {finish.toLocaleTimeString()} + +
    +
    +
    + {t("Grades")} +
    +
    + {grades.map((mention, i) => { + return i < grades.length ? ( + + {mention.label} + + ) : ( + + ); + })} +
    +
    + {t("Voters' list")} +
    +
    + {emails.length > 0 ? ( + emails.join(", ") + ) : ( +

    + {t("The form contains no address.")} +
    + + {t( + "The election will be opened to anyone with the link" + )} + +

    + )} +
    + {restrictResult ? ( +
    +
    +
    + + {t("Results available at the close of the vote")} +
    +

    + + {t( + "The results page will not be accessible until the end date is reached." + )}{" "} + ({finish.toLocaleDateString()} {t("at")}{" "} + {finish.toLocaleTimeString()}) + +

    +
    +
    + ) : ( +
    +
    +
    + {t("Results available at any time")} +
    +
    +
    + )} +
    +
    + + + + +
    +
    + ) +} + +export default ConfirmModal diff --git a/components/form/HelpButton.jsx b/components/form/HelpButton.jsx new file mode 100644 index 0000000..1d87b53 --- /dev/null +++ b/components/form/HelpButton.jsx @@ -0,0 +1,75 @@ +/* eslint react/prop-types: 0 */ +import React, { Component } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; + +class HelpButton extends Component { + constructor(props) { + super(props); + + this.state = { + tooltipOpen: false + }; + } + + showTooltip = () => { + this.setState({ + tooltipOpen: true + }); + }; + + hideTooltip = () => { + this.setState({ + tooltipOpen: false + }); + }; + + render() { + return ( + + + {this.state.tooltipOpen ? ( + + + {this.props.children} + + ) : ( + + )} + + + + ); + } +} +export default HelpButton; diff --git a/components/layouts/Footer.jsx b/components/layouts/Footer.jsx new file mode 100644 index 0000000..86c789f --- /dev/null +++ b/components/layouts/Footer.jsx @@ -0,0 +1,83 @@ +import Link from "next/link"; +import { useTranslation } from "next-i18next"; +import Paypal from "../banner/Paypal"; +import { useBbox } from "./useBbox"; +import { Button, Row, Col } from "reactstrap"; +import LanguageSelector from "./LanguageSelector"; + +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(); + + return ( + + ); +}; +export default Footer; diff --git a/components/layouts/Header.jsx b/components/layouts/Header.jsx new file mode 100644 index 0000000..270208d --- /dev/null +++ b/components/layouts/Header.jsx @@ -0,0 +1,125 @@ +/* eslint react/prop-types: 0 */ +import { useState } from "react"; +import { + Collapse, + Navbar, + NavbarToggler, + Nav, + NavItem, + Button, +} from "reactstrap"; +import Link from "next/link"; +import { useTranslation } from "next-i18next"; +import LanguageSelector from "./LanguageSelector"; +import Accordion from "react-bootstrap/Accordion"; + +const Header = () => { + const [isOpen, setOpen] = useState(false); + + const toggle = () => setOpen(!isOpen); + + const { t } = useTranslation("common"); + + return ( +
    + +
    + + +
    + + + + +
    +
    + ); +}; + +export default Header; diff --git a/components/layouts/HeaderMobile.jsx b/components/layouts/HeaderMobile.jsx new file mode 100644 index 0000000..f738561 --- /dev/null +++ b/components/layouts/HeaderMobile.jsx @@ -0,0 +1,61 @@ +/* eslint react/prop-types: 0 */ +import {useState} from "react"; +import {Collapse, Navbar, NavbarToggler, Nav, NavItem} from "reactstrap"; +import Link from "next/link"; +import Head from "next/head"; +import {useTranslation} from 'next-i18next' +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faRocket} from "@fortawesome/free-solid-svg-icons"; +import LanguageSelector from "./LanguageSelector"; + + +const Header = () => { + const [isOpen, setOpen] = useState(false) + + const toggle = () => setOpen(!isOpen); + + const {t} = useTranslation() + return ( + <> + {t("title")} +
    + + + +
    +
    + logo +
    +
    +
    +

    + {t("Voting platform")} + {t("Majority Judgment")} +

    +
    +
    +
    +
    + + + + + +
    +
    + + ); +} + +export default Header; diff --git a/components/layouts/LanguageSelector.jsx b/components/layouts/LanguageSelector.jsx new file mode 100644 index 0000000..e850fc7 --- /dev/null +++ b/components/layouts/LanguageSelector.jsx @@ -0,0 +1,33 @@ +import {useRouter} from 'next/router' +import ReactFlagsSelect from 'react-flags-select'; + +const LanguageSelector = () => { + + const router = useRouter(); + let localeShort = router.locale.substring(0, 2).toUpperCase(); + if (localeShort === "EN") localeShort = "GB"; + + const selectHandler = e => { + let locale = e.toLowerCase(); + if (locale === "gb") locale = "en"; + router.push("", "", {locale}) + }; + return ( + + ); +}; + +export default LanguageSelector; diff --git a/components/layouts/result/HeaderDesktopResult.jsx b/components/layouts/result/HeaderDesktopResult.jsx new file mode 100644 index 0000000..2d4e457 --- /dev/null +++ b/components/layouts/result/HeaderDesktopResult.jsx @@ -0,0 +1,47 @@ +/* eslint react/prop-types: 0 */ +import { useState } from "react"; +import { Container, Row, Col, Nav, NavItem } from "reactstrap"; +import Link from "next/link"; +import Head from "next/head"; +import { useTranslation } from 'next-i18next' + + + +export default function HeaderResultResult() { + + ; + return ( + + + + + + + + +

    Clos il y a 2 jours

    +
    + + +

    14 votants

    +
    + + + +

    Quel est le meilleur candidat pour les éléctions présidentielle ?

    + + + + +

    Télécharger les résultats

    +
    + +

    Partagez les résultats

    +
    + +
    + +
    + ); +} + diff --git a/components/layouts/result/HeaderMobileResult.jsx b/components/layouts/result/HeaderMobileResult.jsx new file mode 100644 index 0000000..772d826 --- /dev/null +++ b/components/layouts/result/HeaderMobileResult.jsx @@ -0,0 +1,38 @@ +/* eslint react/prop-types: 0 */ +import { useState } from "react"; +import { Container, Row, Col, Nav, NavItem } from "reactstrap"; +import Link from "next/link"; +import Head from "next/head"; +import { useTranslation } from 'next-i18next' + + +export default function HeaderMobileResult() { + + ; + return ( + + + + +

    Quel est le meilleur candidat pour les éléctions présidentielle ?

    +
    + + + +

    Clos il y a 2 jours

    + + + + +

    14 votants

    + +
    + + + + + +
    + ); +} + diff --git a/components/layouts/result/HeaderResult.jsx b/components/layouts/result/HeaderResult.jsx new file mode 100644 index 0000000..121f03f --- /dev/null +++ b/components/layouts/result/HeaderResult.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import HeaderDesktopResult from './HeaderDesktopResult'; +import HeaderMobileResult from './HeaderMobileResult'; +import { useMediaQuery } from "react-responsive"; + +export default function HeaderResult() { + + const isMobile = useMediaQuery({ query: "(max-width: 800px)" }); + + if (isMobile) return ; + + else return ; +} \ No newline at end of file diff --git a/components/layouts/useBbox.jsx b/components/layouts/useBbox.jsx new file mode 100644 index 0000000..42e11f8 --- /dev/null +++ b/components/layouts/useBbox.jsx @@ -0,0 +1,20 @@ +/* eslint react/prop-types: 0 */ +import { useState } from 'react'; +import { useRef } from 'react'; +import { useEffect } from 'react'; + +export const useBbox = () => { + const ref = useRef(); + const [bbox, setBbox] = useState({}); + + const set = () => + setBbox(ref && ref.current ? ref.current.getBoundingClientRect() : {}); + + useEffect(() => { + set(); + window.addEventListener('resize', set); + return () => window.removeEventListener('resize', set); + }, []); + + return [bbox, ref]; +}; \ No newline at end of file diff --git a/components/loader/index.jsx b/components/loader/index.jsx new file mode 100644 index 0000000..5de0aba --- /dev/null +++ b/components/loader/index.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import Image from 'next/image' + +const Loader = () => { + return ( +
    + Loading... + + +
    + ); +}; + +export default Loader; diff --git a/components/plot.js b/components/plot.js new file mode 100644 index 0000000..8b326e2 --- /dev/null +++ b/components/plot.js @@ -0,0 +1,18 @@ +import React from 'react'; +import plotly from 'plotly.js/dist/plotly'; +import createPlotComponent from 'react-plotly.js/factory'; + +// const Plot = createPlotComponent(plotly); +const Plot = require('react-plotly.js').default; +export default () => ( + +) diff --git a/components/wait/index.jsx b/components/wait/index.jsx new file mode 100644 index 0000000..9765a71 --- /dev/null +++ b/components/wait/index.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import Loader from "../loader"; + +const Wait = () => { + return ; +}; + +export default Wait; diff --git a/components/wait/loader-pulse-2-alpha.gif b/components/wait/loader-pulse-2-alpha.gif new file mode 100644 index 0000000..a2fc0a2 Binary files /dev/null and b/components/wait/loader-pulse-2-alpha.gif differ diff --git a/components/wait/loader-pulse-2.gif b/components/wait/loader-pulse-2.gif new file mode 100644 index 0000000..e9a4c48 Binary files /dev/null and b/components/wait/loader-pulse-2.gif differ diff --git a/config/env.js b/config/env.js new file mode 100644 index 0000000..b0344c5 --- /dev/null +++ b/config/env.js @@ -0,0 +1,93 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./paths')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +var dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + `${paths.dotenv}.${NODE_ENV}`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + NODE_ENV !== 'test' && `${paths.dotenv}.local`, + paths.dotenv, +].filter(Boolean); + +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. Variable expansion is supported in .env files. +// https://github.com/motdotla/dotenv +// https://github.com/motdotla/dotenv-expand +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv-expand')( + require('dotenv').config({ + path: dotenvFile, + }) + ); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebook/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. +const REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + const raw = Object.keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. + // For example, . + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + PUBLIC_URL: publicUrl, + } + ); + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +} + +module.exports = getClientEnvironment; diff --git a/config/jest/cssTransform.js b/config/jest/cssTransform.js new file mode 100644 index 0000000..8f65114 --- /dev/null +++ b/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/config/jest/fileTransform.js b/config/jest/fileTransform.js new file mode 100644 index 0000000..74dc1aa --- /dev/null +++ b/config/jest/fileTransform.js @@ -0,0 +1,40 @@ +'use strict'; + +const path = require('path'); +const camelcase = require('camelcase'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + // Based on how SVGR generates a component name: + // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 + const pascalCaseFileName = camelcase(path.parse(filename).name, { + pascalCase: true, + }); + const componentName = `Svg${pascalCaseFileName}`; + return `const React = require('react'); + module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: React.forwardRef(function ${componentName}(props, ref) { + return { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }; + }), + };`; + } + + return `module.exports = ${assetFilename};`; + }, +}; diff --git a/config/modules.js b/config/modules.js new file mode 100644 index 0000000..4646eb0 --- /dev/null +++ b/config/modules.js @@ -0,0 +1,84 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); + +/** + * Get the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + // We need to explicitly check for null and undefined (and not a falsy value) because + // TypeScript treats an empty string as `.`. + if (baseUrl == null) { + // If there's no baseUrl set we respect NODE_PATH + // Note that NODE_PATH is deprecated and will be removed + // in the next major release of create-react-app. + + const nodePath = process.env.NODE_PATH || ''; + return nodePath.split(path.delimiter).filter(Boolean); + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + config = require(paths.appTsConfig); + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/config/paths.js b/config/paths.js new file mode 100644 index 0000000..f23c121 --- /dev/null +++ b/config/paths.js @@ -0,0 +1,90 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const url = require('url'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebook/create-react-app/issues/637 +const appDirectory = fs.realpathSync(process.cwd()); +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +const envPublicUrl = process.env.PUBLIC_URL; + +function ensureSlash(inputPath, needsSlash) { + const hasSlash = inputPath.endsWith('/'); + if (hasSlash && !needsSlash) { + return inputPath.substr(0, inputPath.length - 1); + } else if (!hasSlash && needsSlash) { + return `${inputPath}/`; + } else { + return inputPath; + } +} + +const getPublicUrl = appPackageJson => + envPublicUrl || require(appPackageJson).homepage; + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// Webpack needs to know it to put the right