pull/81/head
jimmys-box 2 years ago
parent e3ad108349
commit 080f59b101

@ -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] + "<br>" + votes[0] + " votes").toString();
const texteBulle2 = (mentions[1] + "<br>" + votes[1] + " votes").toString();
const texteBulle3 = (mentions[2] + "<br>" + votes[2] + " votes").toString();
const texteBulle4 = (mentions[3] + "<br>" + votes[3] + " votes").toString();
const texteBulle5 = (mentions[4] + "<br>" + 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 (
// <div>
// {!loading ? (
// <React.Fragment>
<Plot
data={[
{
x: [0.7, 0.6, 0.5, 0.6, 0.7],
y: [0.3, 0.4, 0.5, 0.6, 0.5],
hovertemplate:
'<b>%{text}</b>' +
'<extra></extra>',
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.
}}
/>
// </React.Fragment>
// ) : (
// <LoadingScreen />
// )}
// </div>
)
}
export default Bulles;

@ -0,0 +1,41 @@
import * as React from "react";
import * as d3 from "d3";
function drawChart(svgRef: React.RefObject<SVGSVGElement>) {
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<SVGSVGElement>(null);
React.useEffect(() => {
drawChart(svg);
}, [svg]);
return (
<div id="chart">
<svg ref={svg} />
</div>
);
};
export default Chart;

@ -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 (
<div ref={chartArea}></div>
);
}
export default ChartWrapper;

@ -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 (
<div className="input-group ">
<input
type="text"
className="form-control"
ref={ref}
value={value}
readOnly
onClick={handleClickOnField}
/>
<div className="input-group-append">
{/*
<Button
href={value}
target="_blank"
rel="noreferrer"
className="btn btn-success"
type="button"
>
<FontAwesomeIcon icon={iconOpen} className="mr-2" />
{t("Go")}
</Button>
*/}
<Button
className="btn btn-secondary"
onClick={handleClickOnButton}
type="button"
>
<FontAwesomeIcon icon={iconCopy} className="mr-2" />
{t("Copy")}
</Button>
</div>
</div>
);
};
CopyField.defaultProps = {
iconCopy: faCopy,
iconOpen: faExternalLinkAlt,
};
export default CopyField;

@ -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")
})
}
}

@ -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 (
<Container>
<Row>
<Link href="/">
<a className="d-block ml-auto mr-auto mb-4">
<img src="/logos/logo-line-white.svg" alt="logo" height="128" />
</a>
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h4>{props.value}</h4>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-right mr-4">
<Link href="/">
<a className="btn btn-secondary">{t("common.backHomepage")}</a>
</Link>
</Col>
<Col className="text-left ml-4">
<a
href="mailto:app@mieuxvoter.fr?subject=[HELP]"
className="btn btn-success"
>
{t("resource.help")}
</a>
</Col>
</Row>
</Container>
);
};
export default Error;

@ -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 (
<Screen>
<Balls>
<div className="ball one"></div>
<div className="ball two"></div>
<div className="ball three"></div>
</Balls>
</Screen>
);
};
export default LoadingScreen;

@ -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 ? (
<StyledModalOverlay>
<StyledModal>
<StyledModalHeader>
<a href="#" onClick={handleCloseClick}>
x
</a>
</StyledModalHeader>
{title && <StyledModalTitle>{title}</StyledModalTitle>}
<StyledModalBody>{children}</StyledModalBody>
</StyledModal>
</StyledModalOverlay>
) : 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;

@ -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 (
<Fragment>
<Bulles donnees={resultats} />
<p style={{color: '#000000'}}>Le total des votes est de {totalVotes}.</p>
</Fragment>
);
}
export default SystemeVote;

@ -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 (
<button className={props.className} onClick={handleClick} type="button">
<FontAwesomeIcon icon={faFacebookSquare} className="mr-2" />
{props.text}
</button>
);
};
export default Facebook;
//i

@ -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 (
<a
className={props.className}
href={api.feedbackForm}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={faCommentAlt} className="mr-2" />
Votre avis nous intéresse !
</a>
);
}
Gform.propTypes = {
className: PropTypes.string,
};
export default Gform;

@ -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 (
<a href={linkHelloAssoBanner} target="_blank" rel="noopener noreferrer">
<img
src={"/banner/" + locale + "/helloasso.png"}
alt="support us on helloasso"
style={{ width: props.width }}
/>
</a>
);
};
export default Helloasso;

@ -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 (
<div className="d-inline-block m-auto">
<form
action="https://www.paypal.com/cgi-bin/webscr"
method="post"
target="_top"
>
<button
type="submit"
className="btn btn-primary"
title={t("PayPal - The safer, easier way to pay online!")}
>
{" "}
<FontAwesomeIcon icon={faPaypal} className="mr-2" />
{t("Support us !")}
</button>
<input type="hidden" name="cmd" value="_s-xclick" />
<input type="hidden" name="hosted_button_id" value="KB2Z7L9KARS7C" />
<img alt="" border="0" src={pixelLink} width="1" height="1" />
</form>
</div>
);
};
export default Paypal;

@ -0,0 +1,4 @@
import * as React from "react";
import FlagIconFactory from "react-flag-icon-css";
export const FlagIcon = FlagIconFactory(React, { useCssModules: false });

@ -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 (
<div className="ajout-avatar">
<div>
<div className="avatar-placeholer">
<img src={createObjectURL} />
</div>
</div>
<div className="avatar-text">
<h4>Photo <span> (facultatif)</span></h4>
<p>Importer une photo.<br />format : jpg, png, pdf</p>
<div className="btn-ajout-avatar">
<input type="file" name="myImage" id="myImage" onChange={uploadToClient} />
<label className="inputfile" for="myImage">Importer une photo</label>
</div>
</div>
</div>
);
}

@ -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 (
<Alert className="preventWarning">
<Alert.Heading>
<div>
<FontAwesomeIcon icon={faExclamationCircle} className="mr-2" />
<span>2 candidats minimum</span>
</div>
<FontAwesomeIcon onClick={() => setShow(false)} icon={faTimes} className="mr-2" />
</Alert.Heading>
</Alert>
);
}
return null;
}

@ -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 (
<div className="input-group-append cancelButton">
<button
type="button"
className={className}
onClick={toggle}
>
<div className="annuler"><img src="/arrow-dark-left.svg" /><p>Annuler</p></div>
</button>
<Modal
isOpen={visibled}
toggle={toggle}
className="modal-dialog-centered"
>
<ModalHeader toggle={toggle}>{t("Delete?")}</ModalHeader>
<ModalBody>
{t("Are you sure to delete")}{" "}
{label && label !== "" ? (
<b>&quot;{label}&quot;</b>
) : (
<>{t("the row")}</>
)}{" "}
?
</ModalBody>
<ModalFooter>
<Button
color="primary-outline"
className="text-primary border-primary"
onClick={toggle}>
{t("No")}
</Button>
<Button color="primary"
onClick={() => {toggle(); onDelete();}}
>
{t("Yes")}
</Button>
</ModalFooter>
</Modal>
</div >
);
}
export default ButtonWithConfirm;

@ -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 }) => (
<span className="input-group-text indexNumber">{children}</span>
));
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 (
<Row className="rowNoMargin">
<div className="candidateButton">
<div className="avatarThumb">
<img src={createObjectURL} alt="" />
<span className="ml-2">Ajouter un candidat</span>
</div>
<FontAwesomeIcon onClick={toggle} icon={faPlus} className="mr-2 cursorPointer" />
</div>
<Modal
isOpen={visibled}
toggle={toggle}
className="modal-dialog-centered"
>
<ModalHeader className='closeModalAddCandidate' toggle={toggle}>
</ModalHeader>
<ModalBody>
<Col className="addCandidateCard">
<InputGroup className="addCandidateForm">
<InputGroupAddon addonType="prepend" className="addCandidateHeader">
<DragHandle>
<h6>Ajouter un participant</h6>
<p>Ajoutez une photo, le nom et une description au candidat.</p>
<div className="ajout-avatar">
<div>
<div className="avatar-placeholer">
<img src={createObjectURL} />
</div>
</div>
<div className="avatar-text">
<h4>Photo <span> (facultatif)</span></h4>
<p>Importer une photo.<br />format : jpg, png, pdf</p>
<div className="btn-ajout-avatar">
<input type="file" name="myImage" id="myImage" onChange={uploadToClient} />
<label className="inputfile" for="myImage">Importer une photo</label>
</div>
</div>
</div>
<img src="/avatar.svg" />
</DragHandle>
</InputGroupAddon>
<Label className="addCandidateText">Nom et prenom</Label>
<Input
type="text"
value={label}
{...inputProps}
placeholder={t("resource.candidatePlaceholder")}
tabIndex={candIndex + 1}
maxLength="250"
autoFocus
className="addCandidateText"
/>
<Label className="sss">Description (Facultatif)</Label>
<Input
type="text"
value={description}
{...inputProps}
placeholder="Texte"
tabIndex={candIndex + 1}
maxLength="250"
autoFocus
className="sss"
/>
<Row className="removeAddButtons">
<ButtonWithConfirm className="removeButton" label={label} onDelete={onDelete, toggle}></ButtonWithConfirm>
<Button className="addButton" label={label} onClick={test}>
<FontAwesomeIcon icon={faPlus} />
<span>Ajouter</span>
</Button>
</Row>
</InputGroup>
</Col>
</ModalBody></Modal>
{/* <Col xs="auto" className="align-self-center pl-0">
<HelpButton>
{t(
"Enter the name of your candidate or proposal here (250 characters max.)"
)}
</HelpButton>
</Col> */}
</Row>
);
}
export default CandidateField

@ -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}) => <li className={className}><CandidateField {...childProps} /></li>);
//
// const SortableContainer = sortableContainer(({children}) => {
// return <ul className="sortable">{children}</ul>;
// });
const SortableItem = ({className, ...childProps}) => <li className={className}><CandidateField {...childProps} /></li>;
const SortableContainer = ({children}) => {
return <ul className="sortable">{children}</ul>;
};
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 (
<div className="sectionAjouterCandidat">
<div className="ajouterCandidat">
<h4>Saisissez ici le nom de vos candidats.</h4>
<AlertDismissibleExample />
<SortableContainer onSortEnd={onSortEnd}>
{candidates.map((candidate, index) => {
const className = "sortable"
return (
<SortableItem
className={className}
key={`item-${index}`}
index={index}
candIndex={index}
label={candidate.label}
description={candidate.description}
onDelete={() => removeCandidate(index)}
onChange={(e) => editCandidate(index, e.target.value)}
onKeyPress={(e) => handleKeyPress(e, index)}
onAdd={addCandidate}
innerRef={candidate.fieldRef}
/>
)
})}
</SortableContainer>
</div>
</div>
);
}
export default CandidatesField

@ -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 (
<div className="input-group-append">
<button
type="button"
className={className}
onClick={toggle}
tabIndex={tabIndex}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Validate")}
</button>
<Modal
isOpen={visibled}
toggle={toggle}
className="modal-dialog-centered"
>
<ModalHeader toggle={toggle}>
{t("Confirm your vote")}
</ModalHeader>
<ModalBody>
<div className="mt-1 mb-1">
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Question of the election")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">{title}</div>
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Candidates/Proposals")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
<ul className="m-0 pl-4">
{candidates.map((candidate, i) => {
if (candidate.label !== "") {
return (
<li key={i} className="m-0">
{candidate.label}
</li>
);
} else {
return <li key={i} className="d-none" />;
}
})}
</ul>
</div>
<div className={(isTimeLimited ? "d-block " : "d-none")} >
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Dates")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
{t("The election will take place from")}{" "}
<b>
{start.toLocaleDateString()}, {t("at")}{" "}
{start.toLocaleTimeString()}
</b>{" "}
{t("to")}{" "}
<b>
{finish.toLocaleDateString()}, {t("at")}{" "}
{finish.toLocaleTimeString()}
</b>
</div>
</div>
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Grades")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
{grades.map((mention, i) => {
return i < grades.length ? (
<span
key={i}
className="badge badge-light mr-2 mt-2"
style={{
backgroundColor: mention.color,
color: "#fff"
}}
>
{mention.label}
</span>
) : (
<span key={i} />
);
})}
</div>
<div className="text-white bg-primary p-2 pl-3 pr-3 rounded">
{t("Voters' list")}
</div>
<div className="p-2 pl-3 pr-3 bg-light mb-3">
{emails.length > 0 ? (
emails.join(", ")
) : (
<p>
{t("The form contains no address.")}
<br />
<em>
{t(
"The election will be opened to anyone with the link"
)}
</em>
</p>
)}
</div>
{restrictResult ? (
<div>
<div className="small bg-primary text-white p-3 mt-2 rounded">
<h6 className="m-0 p-0">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="mr-2"
/>
<u>{t("Results available at the close of the vote")}</u>
</h6>
<p className="m-2 p-0">
<span>
{t(
"The results page will not be accessible until the end date is reached."
)}{" "}
({finish.toLocaleDateString()} {t("at")}{" "}
{finish.toLocaleTimeString()})
</span>
</p>
</div>
</div>
) : (
<div>
<div className="small bg-primary text-white p-3 mt-2 rounded">
<h6 className="m-0 p-0">
{t("Results available at any time")}
</h6>
</div>
</div>
)}
</div>
</ModalBody>
<ModalFooter>
<Button
color="primary-outline"
className="text-primary border-primary"
onClick={toggle}>
{t("Cancel")}
</Button>
<Button color="primary"
onClick={() => {toggle(); confirmCallback();}}
>
{t("Start the election")}
</Button>
</ModalFooter>
</Modal>
</div >
)
}
export default ConfirmModal

@ -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 (
<span className={this.props.className}>
<span>
{this.state.tooltipOpen ? (
<span
style={{
position: "absolute",
zIndex: 10,
fontSize: "12px",
color: "#000",
backgroundColor: "#fff",
display: "inline-block",
borderRadius: "0.25rem",
boxShadow: "-5px 0 5px rgba(0,0,0,0.5)",
maxWidth: "200px",
padding: "10px",
marginLeft: "-215px",
marginTop: "-25px"
}}
>
<span
style={{
position: "absolute",
width: 0,
height: 0,
borderTop: "10px solid transparent",
borderBottom: "10px solid transparent",
borderLeft: "10px solid #fff",
marginLeft: "190px",
marginTop: "15px"
}}
></span>
{this.props.children}
</span>
) : (
<span />
)}
</span>
<FontAwesomeIcon
icon={faQuestionCircle}
onMouseOver={this.showTooltip}
onMouseOut={this.hideTooltip}
/>
</span>
);
}
}
export default HelpButton;

@ -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 (
<footer className="text-center">
<div>
<Row className="tacky">
<Col className="col-md-10 col-sm-10">
<Row className="footerRow">
<Col className="col-md-1 footerLogo">
<img src="/logos/logo-footer.svg" alt="logo of Mieux Voter" />
</Col>
<Col ref={link1}
className={bboxLink1.top === bboxLink2.top ? "" : "no-tack"}
>
<Link href="/" style={linkStyle}>
Le jugement majoritaire
</Link>
</Col>
<Col ref={link2}
className={bboxLink2.top === bboxLink3.top ? "" : "no-tack"}
>
<Link
href="https://mieuxvoter.fr/"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{t("Who are we?")}
</Link>
</Col>
<Col ref={link3}
className={bboxLink3.top === bboxLink4.top ? "" : "no-tack"}
>
<Link href="/faq" style={linkStyle}>
{t("FAQ")}
</Link>
</Col>
<Col ref={link4}
className={bboxLink4.top === bboxLink5.top ? "" : "no-tack"}
>
<Link href="/" style={linkStyle}>
Actualités
</Link>
</Col>
<Col ref={link5}>
<a href="mailto:app@mieuxvoter.fr?subject=[HELP]" style={linkStyle}>
Nous contacter
</a>
</Col>
<Col><LanguageSelector /></Col>
</Row>
</Col>
<Col className="footerButton">
<Col className="col-xl-10 col-md-12 offset-xl-2">
<Button className="btn-primary btn-footer">
<a href="/">
Soutenez-nous
</a>
</Button>
</Col>
</Col>
</Row>
</div>
</footer>
);
};
export default Footer;

@ -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 (
<header className="mobile-header">
<Navbar light className="nav-mobile" expand="lg">
<div className="navbar-header">
<Button onClick={toggle} className="navbar-toggle">
<img src="/open-menu-icon.svg" alt="" height="50" />
</Button>
</div>
<Collapse isOpen={isOpen} navbar>
<Nav className="ml-auto navbar-nav-scroll" navbar>
<div className="d-flex flex-row justify-content-between nav-logo">
<Link href="/">
<a className="navbar-brand navbar-brand-mobile">
<img src="/logos/logo.svg" alt="logo" height="80" />
</a>
</Link>
<Button onClick={toggle} className="navbar-toggle navbar-close-button">
<img height="20" src="/close-menu-icon.svg" alt="logo" />
</Button>
</div>
<div>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
Le jugement majoritaire
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
Qui sommes-nous ?
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
Foire aux questions
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
On parle de nous
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
Nous contactez
</a>
</Link>
</NavItem>
<NavItem>
<LanguageSelector style={{ width: "80px" }} />
</NavItem>
</div>
<NavItem className="navbar-credits-container">
<Button className="btn-primary btn-nav">
<a href="/">
Soutenez-nous
</a>
</Button>
<hr />
<div className="navbar-credits sharing sharing-mobile">
<p>Partagez lapplication Mieux voter</p>
<Link href="https://www.facebook.com/mieuxvoter.fr/"><img src="/facebook.svg" className="mr-2" /></Link>
<Link href="https://twitter.com/mieux_voter"><img src="/twitter.svg" className="mr-2" /></Link>
</div>
<div className="d-flex">
<Link href="https://jimmys-box.com/">
<a onClick={toggle} className="navbar-jimmy-link">
développé parJIMMY
</a>
</Link>
</div>
</NavItem>
</Nav>
</Collapse>
</Navbar>
</header>
);
};
export default Header;

@ -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 (
<>
<Head><title>{t("title")}</title></Head>
<header>
<Navbar color="light" light expand="md">
<Link href="/">
<a className="navbar-brand">
<div className="d-flex flex-row">
<div className="align-self-center">
<img src="/logos/logo-color.svg" alt="logo" height="32" />
</div>
<div className="align-self-center ml-2">
<div className="logo-text">
<h1>
{t("Voting platform")}
<small>{t("Majority Judgment")}</small>
</h1>
</div>
</div>
</div>
</a>
</Link>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="ml-auto" navbar>
<NavItem>
<Link href="/new/">
<a className="text-primary nav-link"> <FontAwesomeIcon icon={faRocket} className="mr-2" />
{t("Start an election")}
</a>
</Link>
</NavItem>
<NavItem style={{width: "80px"}}>
<LanguageSelector />
</NavItem>
</Nav>
</Collapse>
</Navbar>
</header>
</>
);
}
export default Header;

@ -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 (
<ReactFlagsSelect
onSelect={selectHandler}
countries={
// ["GB", "FR", "ES", "DE", "RU"]
["GB", "FR"]
}
showOptionLabel={false}
selected={localeShort}
selectedSize={15}
optionsSize={22}
showSelectedLabel={false}
showSecondaryOptionLabel={false}
className="menu-flags"
/>
);
};
export default LanguageSelector;

@ -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 (
<Container className="sectionHeaderResult">
<Row>
<Col className="col-md-3">
<Row>
<Col className="sectionHeaderResultSideCol">
<img src="/calendar.svg" />
<p>Clos il y a 2 jours</p></Col>
</Row>
<Row>
<Col className="sectionHeaderResultSideCol"><img src="/avatarBlue.svg" />
<p>14 votants</p></Col>
</Row>
</Col>
<Col className="sectionHeaderResultMiddleCol col-md-6">
<h3>Quel est le meilleur candidat pour les éléctions présidentielle ?</h3>
</Col>
<Col className="col-md-3">
<Row>
<Col className="sectionHeaderResultSideCol"><img src="/arrowUpload.svg" /><p>Télécharger les résultats</p></Col>
</Row>
<Row>
<Col className="sectionHeaderResultSideCol"><img src="/arrowL.svg" /><p>Partagez les résultats</p></Col>
</Row>
</Col>
</Row>
</Container>
);
}

@ -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 (
<Container className="sectionHeaderResult">
<Row className="sectionHeaderResultMiddleCol">
<h3>Quel est le meilleur candidat pour les éléctions présidentielle ?</h3>
</Row>
<Row>
<Col className="sectionHeaderResultSideCol">
<img src="/calendar.svg" />
<p>Clos il y a 2 jours</p>
</Col>
<Col className="sectionHeaderResultSideCol">
<img src="/avatarBlue.svg" />
<p>14 votants</p>
</Col>
</Row>
</Container >
);
}

@ -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 <HeaderMobileResult />;
else return <HeaderDesktopResult />;
}

@ -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];
};

@ -0,0 +1,14 @@
import React from "react";
import Image from 'next/image'
const Loader = () => {
return (
<div className="loader bg-primary">
<img src="/loader-pulse-2.gif" alt="Loading..." />
</div>
);
};
export default Loader;

@ -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 () => (
<Plot
data={[
{
type: 'bar',
x: ['Taubira', 'Hidalgo', 'Mélenchon'],
y: [29,150,85]
}
]}
layout={ { width: 1000, height: 500, title: 'Nombre de voix par candidat' } }
/>
)

@ -0,0 +1,8 @@
import React from "react";
import Loader from "../loader";
const Wait = () => {
return <Loader />;
};
export default Wait;

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@ -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 were 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, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// 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;

@ -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';
},
};

@ -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};`;
},
};

@ -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();

@ -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 <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
function getServedPath(appPackageJson) {
const publicUrl = getPublicUrl(appPackageJson);
const servedUrl =
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
return ensureSlash(servedUrl, true);
}
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
publicUrl: getPublicUrl(resolveApp('package.json')),
servedPath: getServedPath(resolveApp('package.json')),
};
module.exports.moduleFileExtensions = moduleFileExtensions;

@ -0,0 +1,35 @@
'use strict';
const { resolveModuleName } = require('ts-pnp');
exports.resolveModuleName = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveModuleName
);
};
exports.resolveTypeReferenceDirective = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveTypeReferenceDirective
);
};

@ -0,0 +1,628 @@
'use strict';
const fs = require('fs');
const isWsl = require('is-wsl');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const postcssNormalize = require('postcss-normalize');
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// In development, we always serve from the root. This makes config easier.
const publicPath = isEnvProduction
? paths.servedPath
: isEnvDevelopment && '/';
// Some apps do not use client-side routing with pushState.
// For these, "homepage" can be set to "." to enable relative asset paths.
const shouldUseRelativeAssetPaths = publicPath === './';
// `publicUrl` is just like `publicPath`, but we will provide it to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
const publicUrl = isEnvProduction
? publicPath.slice(0, -1)
: isEnvDevelopment && '';
// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: shouldUseRelativeAssetPaths ? { publicPath: '../../' } : {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push({
loader: require.resolve(preProcessor),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
});
}
return loaders;
};
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
publicPath: publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// we want terser to parse ecma 8 code. However, we don't want it
// to apply any minfication steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending futher investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
// Disabled on WSL (Windows Subsystem for Linux) due to an issue with Terser
// https://github.com/webpack-contrib/terser-webpack-plugin/issues/21
parallel: !isWsl,
// Enable file caching
cache: true,
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
runtimeChunk: true,
},
resolve: {
// This allows you to set a fallback for where Webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
// First, run the linter.
// It's important to do this before Babel processes the JS.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
enforce: 'pre',
use: [
{
options: {
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
},
loader: require.resolve('eslint-loader'),
},
],
include: paths.appSrc,
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+ref![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
cacheCompression: isEnvProduction,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
cacheCompression: isEnvProduction,
// If an error happens in a package, it's possible to be
// because it was compiled. Thus, we don't want the browser
// debugger to show the original code. Instead, the code
// being evaluated would be much more helpful.
sourceMaps: false,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime~.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In production, it will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
// In development, this will be an empty string.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (currently CSS only):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for Webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate a manifest file which contains a mapping of all asset filenames
// to their corresponding output file so that tools can pick it up without
// having to parse `index.html`.
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: publicPath,
generate: (seed, files) => {
const manifestFiles = files.reduce(function(manifest, file) {
manifest[file.name] = file.path;
return manifest;
}, seed);
return {
files: manifestFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how Webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the Webpack build.
isEnvProduction &&
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/],
importWorkboxFrom: 'cdn',
navigateFallback: publicUrl + '/index.html',
navigateFallbackBlacklist: [
// Exclude URLs starting with /_, as they're likely an API call
new RegExp('^/_'),
// Exclude URLs containing a dot, as they're likely a resource in
// public/ and not a SPA route
new RegExp('/[^/]+\\.[^/]+$'),
],
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: isEnvDevelopment,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [
'**',
'!**/__tests__/**',
'!**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
watch: paths.appSrc,
silent: true,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};

@ -0,0 +1,104 @@
'use strict';
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
const ignoredFiles = require('react-dev-utils/ignoredFiles');
const paths = require('./paths');
const fs = require('fs');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const host = process.env.HOST || '0.0.0.0';
module.exports = function(proxy, allowedHost) {
return {
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
// https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a
// compromise. Since our WDS configuration only serves files in the `public`
// folder we won't consider accessing them a vulnerability. However, if you
// use the `proxy` feature, it gets more dangerous because it can expose
// remote code execution vulnerabilities in backends like Django and Rails.
// So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable.
disableHostCheck:
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files wont automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through Webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the Webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// It is important to tell WebpackDevServer to use the same "root" path
// as we specified in the config. In development, we always serve from /.
publicPath: '/',
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebook/create-react-app/issues/293
// src/node_modules is not ignored to support absolute imports
// https://github.com/facebook/create-react-app/issues/1065
watchOptions: {
ignored: ignoredFiles(paths.appSrc),
},
// Enable HTTPS if the HTTPS environment variable is set to 'true'
https: protocol === 'https',
host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
},
public: allowedHost,
proxy,
before(app, server) {
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware());
},
};
};

@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style type="text/css">
/* FONTS */
@media screen {
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v11/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v11/qdgUG4U09HnJwhYI-uK18wLUuEpTyoUstqEm5AMlJo4.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v11/RYyZNoeFgb0l7W3Vu1aSWOvvDin1pK8aKteLpeZ5c0A.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(https://fonts.gstatic.com/s/lato/v11/HkF_qI1x_noxlxhrhMQYELO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
}
/* CLIENT-SPECIFIC STYLES */
body, table, th, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none;}
/* RESET STYLES */
table { border-collapse: collapse !important; padding: 0 !important;}
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* MOBILE STYLES */
@media screen and (max-width:600px){
h1 {
font-size: 32px !important;
line-height: 32px !important;
}
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] { margin: 0 !important; }
</style>
</head>
<body style="background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;">
<!-- HIDDEN PREHEADER TEXT -->
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Lato', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
{{#i18n 'email.happy' }}We are happy to send you this email! You will be able to vote using majority judgment.{{/i18n}}
</div>
<table border="0" style="margin: 0px auto 0px auto; width: 100%;" aria-describedby="Email">
<!-- LOGO -->
<tr>
<th scope="col" style="background-color:#efefff ;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="Logo picture">
<tr>
<th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;">
<a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer">
<img alt="Logo" src="https://mieuxvoter.fr/wp-content/uploads/2019/10/mieuxvoter_logo.png" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0">
</a>
</th>
</tr>
</table>
</th>
</tr>
<!-- TITLE -->
<tr>
<th scope="col" style="background-color: #efefff; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="email title">
<tr>
<th scope="col" style="vertical-align: top; background-color: #ffffff; padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">{{#i18n 'email.hello'}}Hi, there! 🙂{{/i18n}}</h1>
</th>
</tr>
</table>
</th>
</tr>
<!-- BLOCKS -->
<tr>
<th scope="col" style="background-color: #2a43a0; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="email body">
<!-- BLOCK SUBTITLE-->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.happy'}}We are happy to send you this email! You will be able to vote using majority judgment.{{/i18n}}
</p>
</th>
</tr>
<!-- BLOCK EXPLANATION-->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.why'}}This email was sent to you because your email address was entered to participate in the vote on the subject:{{/i18n}}
&nbsp;
<strong>{{title}}</strong>
</p>
</th>
</tr>
<!-- BULLETPROOF BUTTON BLUE-->
<tr>
<th scope="col" style="background-color: #ffffff;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%;" aria-describedby="Blue bulletproof button">
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 60px 30px;">
<table border="0" style="margin: 0px auto 0px auto; border-collapse: collapse;" aria-describedby="invitation url">
<tr>
<th scope="col" style="border-radius: 3px; background-color: #2a43a0;">
<a href="%recipient.urlVote%" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #2a43a0; display: inline-block;">
{{#i18n 'common.vote' }}Vote!{{/i18n}}</a></th>
</tr>
</table>
</th>
</tr>
</table>
</th>
</tr>
<!-- BLOCK DOES NOT WORK -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.copyLink' }}If that doesn't work, copy and paste the following link into your browser:{{/i18n}}
&nbsp;
<a target="_blank" style="color: #2a43a0;">%recipient.urlVote%</a>
</p>
</th>
</tr>
<!-- BLOCK TEXT RESULT -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.linkResult' }}The results will be available with the following link when the vote is finished:{{/i18n}}
&nbsp;
<a target="_blank" style="color: #2a43a0;">%recipient.urlResult%</a>
</p>
</th>
</tr>
<!-- BLOCK THANKS -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">{{#i18n 'email.bye'}}Good vote{{/i18n}},<br>{{#i18n 'common.mieuxvoter'}}Mieux Voter{{/i18n}}</p>
</th>
</tr>
</table>
</th>
</tr>
<!-- SUPPORT CALLOUT -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 30px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="support callout">
<!-- HEADLINE -->
<tr>
<th scope="col" style="background-color: #7d8ecf; padding: 30px 30px 30px 30px; border-radius: 4px 4px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;"><strong>
<a href="https://mieuxvoter.fr/index.php/decouvrir/" target="_blank" style="color: #FFFFFF;" rel="noopener noreferrer">
{{#i18n 'email.aboutjm'}}Need any further information?{{/i18n}}
</a></strong>
</p>
<p style="margin: 0;"> <strong>
<a href="https://mieuxvoter.fr/index.php/decouvrir/" target="_blank" style="color: #111111;" rel="noopener noreferrer">
{{#i18n 'common.helpus'}}Do you want to help us?{{/i18n}}
</a></strong>
</p>
</th>
</tr>
</table>
</th>
</tr>
<!-- FOOTER -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="footer informations">
<!-- EXPLAIN WHY -->
</br>
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">
{{#i18n email.why }}You received this email because someone invited you to vote.{{/i18n}}
</p>
</th>
</tr>
<!-- ADDRESS -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">{{#i18n mieuxvoter }}Mieux Voter{{/i18n}} - <a "mailto:app@mieuxvoter.fr">app@mieuxvoter.fr</a></p>
</th>
</tr>
</table>
</th>
</tr>
</table>
</body>
</html>

@ -0,0 +1,19 @@
{{i18n 'email.hello'}}Hi there! 🙂{{i18n}}
{{i18n 'email.happy'}}We are happy to send you this email! You will be able to vote using majority judgment.{{i18n}}
{{i18n 'email.why'}}This email was sent to you because your email was filled out to participate in the vote on the subject:{{i18n}}
{{ title }}
{{i18n 'email.linkVote' }}The link for the vote is as follows:{{i18n}}
%recipient.urlVote%
{{i18n 'email.linkResult' }}The link that will give you the results when they are available is as follows:{{i18n}}
%recipient.urlResult%
{{i18n 'email.bye'}}Good vote{{i18n}}
{{i18n 'common.mieuxvoter'}}Mieux Voter{{i18n}}

@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style type="text/css">
/* FONTS */
@media screen {
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v11/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v11/qdgUG4U09HnJwhYI-uK18wLUuEpTyoUstqEm5AMlJo4.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v11/RYyZNoeFgb0l7W3Vu1aSWOvvDin1pK8aKteLpeZ5c0A.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(https://fonts.gstatic.com/s/lato/v11/HkF_qI1x_noxlxhrhMQYELO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
}
/* CLIENT-SPECIFIC STYLES */
body, table, th, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none;}
/* RESET STYLES */
table { border-collapse: collapse !important; padding: 0 !important;}
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* MOBILE STYLES */
@media screen and (max-width:600px){
h1 {
font-size: 32px !important;
line-height: 32px !important;
}
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] { margin: 0 !important; }
</style>
</head>
<body style="background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;">
<!-- HIDDEN PREHEADER TEXT -->
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Lato', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">Nous sommes très heureux de vous partager ce lien de vote ! Vous allez pouvoir voter avec le jugement majoritaire.</div>
<table border="0" style="margin: 0px auto 0px auto; width: 100%;" aria-describedby="Email">
<!-- LOGO -->
<tr>
<th scope="col" style="background-color:#efefff ;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="Logo picture">
<tr>
<th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;">
<a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer">
<img alt="Logo" src="https://mieuxvoter.fr/wp-content/uploads/2019/10/mieuxvoter_logo.png" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0">
</a>
</th>
</tr>
</table>
</th>
</tr>
<!-- TITLE -->
<tr>
<th scope="col" style="background-color: #efefff; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="email title">
<tr>
<th scope="col" style="vertical-align: top; background-color: #ffffff; padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">Bonjour ! 🙂</h1>
</th>
</tr>
</table>
</th>
</tr>
<!-- BLOCKS -->
<tr>
<th scope="col" style="background-color: #2a43a0; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="email body">
<!-- BLOCK SUBTITLE-->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">Nous sommes très heureux de vous partager ce lien de vote ! Vous allez pouvoir voter avec le jugement majoritaire.</p>
</th>
</tr>
<!-- BLOCK EXPLANATION-->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">Vous avez été invité·e à participer à l'élection suivante : </p>
</th>
</tr>
<!-- BULLETPROOF BUTTON BLUE-->
<tr>
<th scope="col" style="background-color: #ffffff;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%;" aria-describedby="Blue bulletproof button">
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 60px 30px;">
<table border="0" style="margin: 0px auto 0px auto; border-collapse: collapse;" aria-describedby="invitation url">
<tr>
<th scope="col" style="border-radius: 3px; background-color: #2a43a0;">
<a href="%recipient.urlVote%" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #2a43a0; display: inline-block;">Voter !</a></th>
</tr>
</table>
</th>
</tr>
</table>
</th>
</tr>
<!-- BLOCK DOES NOT WORK -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
Si le lien ne fonctionne pas, vous pouvez le copier et le coller dans la barre de navigation de votre navigateur.
&nbsp;
<a target="_blank" style="color: #2a43a0;">%recipient.urlVote%</a>
</p>
</th>
</tr>
<!-- BLOCK TEXT RESULT -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
A la fin de l'élection, vous pourrez accéder aux résultats en cliquant sur ce lien :
&nbsp;
<a target="_blank" style="color: #2a43a0;">%recipient.urlResult%</a>
</p>
</th>
</tr>
<!-- BLOCK THANKS -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">Bon vote,<br>Mieux Voter</p>
</th>
</tr>
</table>
</th>
</tr>
<!-- SUPPORT CALLOUT -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 30px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="support callout">
<!-- HEADLINE -->
<tr>
<th scope="col" style="background-color: #7d8ecf; padding: 30px 30px 30px 30px; border-radius: 4px 4px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;"><strong>
<a href="https://mieuxvoter.fr/index.php/decouvrir/" target="_blank" style="color: #FFFFFF;" rel="noopener noreferrer">Besoin de plus d'information</a></strong>
</p>
<p style="margin: 0;"> <strong>
<a href="https://mieuxvoter.fr/index.php/decouvrir/" target="_blank" style="color: #111111;" rel="noopener noreferrer">Vous souhaitez nous aider ?</a></strong>
</p>
</th>
</tr>
</table>
</th>
</tr>
<!-- FOOTER -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="footer informations">
<!-- EXPLAIN WHY -->
</br>
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">Vous avez été invité·e à participer à l'élection suivante</p>
</th>
</tr>
<!-- ADDRESS -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">Mieux Voter - <a "mailto:app@mieuxvoter.fr">app@mieuxvoter.fr</a></p>
</th>
</tr>
</table>
</th>
</tr>
</table>
</body>
</html>

@ -0,0 +1,17 @@
Bonjour ! 🙂
Vous avez été invité·e à participer à l'élection suivante :
{{ title }}
Le lien pour voter est le suivant :
%recipient.urlVote%
A la fin de l'élection, vous pourrez accéder aux résultats en cliquant sur ce lien :
%recipient.urlResult%
Bon vote ! 🤗
Mieux Voter

@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style type="text/css">
/* FONTS */
@media screen {
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v11/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v11/qdgUG4U09HnJwhYI-uK18wLUuEpTyoUstqEm5AMlJo4.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v11/RYyZNoeFgb0l7W3Vu1aSWOvvDin1pK8aKteLpeZ5c0A.woff) format('woff');
}
@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(https://fonts.gstatic.com/s/lato/v11/HkF_qI1x_noxlxhrhMQYELO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
}
/* CLIENT-SPECIFIC STYLES */
body, table, th, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none;}
/* RESET STYLES */
table { border-collapse: collapse !important; padding: 0 !important;}
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* MOBILE STYLES */
@media screen and (max-width:600px){
h1 {
font-size: 32px !important;
line-height: 32px !important;
}
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] { margin: 0 !important; }
</style>
</head>
<body style="background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;">
<!-- HIDDEN PREHEADER TEXT -->
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Lato', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
{{#i18n 'email.happy' }}We are happy to send you this email! You will be able to vote using majority judgment.{{/i18n}}
</div>
<table border="0" style="margin: 0px auto 0px auto; width: 100%;" aria-describedby="Email">
<!-- LOGO -->
<tr>
<th scope="col" style="background-color:#efefff ;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="Logo picture">
<tr>
<th scope="col" style="vertical-align: top; padding: 40px 10px 40px 10px;">
<a href="https://mieuxvoter.fr/" target="_blank" rel="noopener noreferrer">
<img alt="Logo" src="https://mieuxvoter.fr/wp-content/uploads/2019/10/mieuxvoter_logo.png" width="40" height="40" style="display: block; margin: 0px auto 0px auto; width: 50%; max-width: 250px; min-width: 40px; height: auto; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0">
</a>
</th>
</tr>
</table>
</th>
</tr>
<!-- TITLE -->
<tr>
<th scope="col" style="background-color: #efefff; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="email title">
<tr>
<th scope="col" style="vertical-align: top; background-color: #ffffff; padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">{{#i18n 'email.hello'}}Hi, there! 🙂{{/i18n}}</h1>
</th>
</tr>
</table>
</th>
</tr>
<!-- BLOCKS -->
<tr>
<th scope="col" style="background-color: #2a43a0; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="email body">
<!-- BLOCK SUBTITLE-->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.happy'}}We are happy to send you this email! You will be able to vote using majority judgment.{{/i18n}}
</p>
</th>
</tr>
<!-- BLOCK EXPLANATION-->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.why'}}This email was sent to you because your email address was entered to participate in the vote on the subject:{{/i18n}}
&nbsp;
<strong>{{title}}</strong>
</p>
</th>
</tr>
<!-- BULLETPROOF BUTTON BLUE-->
<tr>
<th scope="col" style="background-color: #ffffff;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%;" aria-describedby="Blue bulletproof button">
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 60px 30px;">
<table border="0" style="margin: 0px auto 0px auto; border-collapse: collapse;" aria-describedby="invitation url">
<tr>
<th scope="col" style="border-radius: 3px; background-color: #2a43a0;">
<a href="%recipient.urlVote%" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #2a43a0; display: inline-block;">
{{#i18n 'common.vote' }}Vote!{{/i18n}}</a></th>
</tr>
</table>
</th>
</tr>
</table>
</th>
</tr>
<!-- BLOCK DOES NOT WORK -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.copyLink' }}If that doesn't work, copy and paste the following link into your browser:{{/i18n}}
&nbsp;
<a target="_blank" style="color: #2a43a0;">%recipient.urlVote%</a>
</p>
</th>
</tr>
<!-- BLOCK TEXT RESULT -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 20px 30px 20px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">
{{#i18n 'email.linkResult' }}The results will be available with the following link when the vote is finished:{{/i18n}}
&nbsp;
<a target="_blank" style="color: #2a43a0;">%recipient.urlResult%</a>
</p>
</th>
</tr>
<!-- BLOCK THANKS -->
<tr>
<th scope="col" style="background-color: #ffffff; padding: 0px 30px 40px 30px; border-radius: 0px 0px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0; text-align: left;">{{#i18n 'email.bye'}}Good vote{{/i18n}},<br>{{#i18n 'common.mieuxvoter'}}Mieux Voter{{/i18n}}</p>
</th>
</tr>
</table>
</th>
</tr>
<!-- SUPPORT CALLOUT -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 30px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="support callout">
<!-- HEADLINE -->
<tr>
<th scope="col" style="background-color: #7d8ecf; padding: 30px 30px 30px 30px; border-radius: 4px 4px 4px 4px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
<p style="margin: 0;"><strong>
<a href="https://mieuxvoter.fr/index.php/decouvrir/" target="_blank" style="color: #FFFFFF;" rel="noopener noreferrer">
{{#i18n 'email.aboutjm'}}Need any further information?{{/i18n}}
</a></strong>
</p>
<p style="margin: 0;"> <strong>
<a href="https://mieuxvoter.fr/index.php/decouvrir/" target="_blank" style="color: #111111;" rel="noopener noreferrer">
{{#i18n 'common.helpus'}}Do you want to help us?{{/i18n}}
</a></strong>
</p>
</th>
</tr>
</table>
</th>
</tr>
<!-- FOOTER -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 10px 0px 10px;">
<table border="0" style="margin: 0px auto 0px auto; width: 100%; max-width: 600px;" aria-describedby="footer informations">
<!-- EXPLAIN WHY -->
</br>
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">
{{#i18n email.why }}You received this email because someone invited you to vote.{{/i18n}}
</p>
</th>
</tr>
<!-- ADDRESS -->
<tr>
<th scope="col" style="background-color: #f4f4f4; padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;" >
<p style="margin: 0;">{{#i18n mieuxvoter }}Mieux Voter{{/i18n}} - <a "mailto:app@mieuxvoter.fr">app@mieuxvoter.fr</a></p>
</th>
</tr>
</table>
</th>
</tr>
</table>
</body>
</html>

@ -0,0 +1,19 @@
{{#i18n 'email.hello'}}Hi there! 🙂{{/i18n}}
{{#i18n 'email.happy'}}We are happy to send you this email! You will be able to vote using majority judgment.{{/i18n}}
{{#i18n 'email.why'}}This email was sent to you because your email was filled out to participate in the vote on the subject:{{/i18n}}
{{ title }}
{{#i18n 'email.linkVote' }}The link for the vote is as follows:{{/i18n}}
%recipient.urlVote%
{{#i18n 'email.linkResult' }}The link that will give you the results when they are available is as follows:{{/i18n}}
%recipient.urlResult%
{{#i18n 'email.bye'}}Good vote{{/i18n}}
{{#i18n 'common.mieuxvoter'}}Mieux Voter{{/i18n}}

@ -0,0 +1,150 @@
const fs = require("fs");
const Mailgun = require("mailgun.js");
const formData = require("form-data");
const dotenv = require("dotenv");
const i18next = require("i18next");
const Backend = require("i18next-chained-backend");
const FSBackend = require("i18next-fs-backend");
const HttpApi = require("i18next-http-backend");
const Handlebars = require("handlebars");
dotenv.config();
const {
MAILGUN_API_KEY,
MAILGUN_DOMAIN,
MAILGUN_URL,
FROM_EMAIL_ADDRESS,
CONTACT_TO_EMAIL_ADDRESS,
} = process.env;
const mailgun = new Mailgun(formData);
const mg = mailgun.client({
username: "api",
key: MAILGUN_API_KEY,
url: "https://api.eu.mailgun.net",
});
const success = {
statusCode: 200,
body: "Your message was sent successfully! We'll be in touch.",
};
const err = {
statusCode: 422,
body: "Can't send message",
};
// setup i18n
// i18next.use(Backend).init({
// lng: "fr",
// ns: ["emailInvite", "common"],
// defaultNS: "emailInvite",
// fallbackNS: "common",
// debug: false,
// fallbackLng: ["fr"],
// backend: {
// backends: [FSBackend, HttpApi],
// backendOptions: [{ loadPath: "/public/locales/{{lng}}/{{ns}}.json" }, {}],
// },
// });
// setup the template engine
// See https://github.com/UUDigitalHumanitieslab/handlebars-i18next
// function extend(target, ...sources) {
// sources.forEach((source) => {
// if (source)
// for (let key in source) {
// target[key] = source[key];
// }
// });
// return target;
// }
// Handlebars.registerHelper("i18n", function (key, { hash, data, fn }) {
// let parsed = {};
// const jsonKeys = [
// "lngs",
// "fallbackLng",
// "ns",
// "postProcess",
// "interpolation",
// ];
// jsonKeys.forEach((key) => {
// if (hash[key]) {
// parsed[key] = JSON.parse(hash[key]);
// delete hash[key];
// }
// });
// let options = extend({}, data.root.i18next, hash, parsed, {
// returnObjects: false,
// });
// let replace = (options.replace = extend({}, this, options.replace, hash));
// delete replace.i18next; // may creep in if this === data.root
// if (fn) options.defaultValue = fn(replace);
// return new Handlebars.SafeString(i18next.t(key, options));
// });
// const txtStr = fs.readFileSync(__dirname + "/invite.txt").toString();
const txtStr = {
en: fs.readFileSync(__dirname + "/invite-en.txt").toString(),
fr: fs.readFileSync(__dirname + "/invite-fr.txt").toString(),
};
const txtTemplate = {
en: Handlebars.compile(txtStr.en),
fr: Handlebars.compile(txtStr.fr),
};
const htmlStr = {
en: fs.readFileSync(__dirname + "/invite-en.html").toString(),
fr: fs.readFileSync(__dirname + "/invite-fr.html").toString(),
};
const htmlTemplate = {
en: Handlebars.compile(htmlStr.en),
fr: Handlebars.compile(htmlStr.fr),
};
const test = Handlebars.compile("test");
const sendMail = async (event) => {
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
body: "Method Not Allowed",
headers: { Allow: "POST" },
};
}
const data = JSON.parse(event.body);
if (!data.recipientVariables || !data.title || !data.locale) {
return {
statusCode: 422,
body: "Recipient variables and title are required.",
};
}
// i18next.changeLanguage(data.locale);
const templateData = {
title: data.title,
};
const mailgunData = {
// from: `${i18next.t("Mieux Voter")} <mailgun@mg.app.mieuxvoter.fr>`,
from: '"Mieux Voter" <postmaster@mg.app.mieuxvoter.fr>',
to: Object.keys(data.recipientVariables),
text: txtTemplate.fr(templateData),
html: htmlTemplate.fr(templateData),
subject: data.title,
"h:Reply-To": "app@mieuxvoter.fr",
"recipient-variables": JSON.stringify(data.recipientVariables),
};
const res = mg.messages
.create("mg.app.mieuxvoter.fr", mailgunData)
.then((msg) => {
return success;
}) // logs response data
.catch((err) => {
console.log(err);
return success;
}); // logs any error
return res;
};
exports.handler = sendMail;
Loading…
Cancel
Save