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 (
+
+
+
+
+ {/*
+
+
+ {t("Go")}
+
+ */}
+
+
+ {t("Copy")}
+
+
+
+ );
+};
+
+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 (
+
+
+
+
+
+
+
+
+
+
+ {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 (
+
+
+ {props.text}
+
+ );
+};
+
+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 (
+
+
+
+ );
+};
+
+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
+
+
+ Importer une photo
+
+
+
+
+ );
+}
\ 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 (
+
+
+ Annuler
+
+
+ {t("Delete?")}
+
+ {t("Are you sure to delete")}{" "}
+ {label && label !== "" ? (
+ "{label}"
+ ) : (
+ <>{t("the row")}>
+ )}{" "}
+ ?
+
+
+
+ {t("No")}
+
+ {toggle(); onDelete();}}
+ >
+ {t("Yes")}
+
+
+
+
+ );
+}
+
+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
+
+
+ Importer une photo
+
+
+
+
+
+
+ Nom et prenom
+
+ Description (Facultatif)
+
+
+
+
+
+ Ajouter
+
+
+
+
+
+ {/*
+
+ {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("Validate")}
+
+
+
+ {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")}
+
+
+
+ )}
+
+
+
+
+ {t("Cancel")}
+
+ {toggle(); confirmCallback();}}
+ >
+ {t("Start the election")}
+
+
+
+
+ )
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+
+ Le jugement majoritaire
+
+
+
+
+ {t("Who are we?")}
+
+
+
+
+ {t("FAQ")}
+
+
+
+
+ Actualités
+
+
+
+
+ Nous contacter
+
+
+
+
+
+
+
+
+
+ Soutenez-nous
+
+
+
+
+
+
+
+
+ );
+};
+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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Soutenez-nous
+
+
+
+
+
+
+
Partagez l’application Mieux voter
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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")}
+
+ >
+ );
+}
+
+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 (
+
+
+
+
+
+ );
+};
+
+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