@ -1,35 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"commonjs": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
functions/next_*
|
||||
.env
|
@ -1,28 +0,0 @@
|
||||
# Usage: $ make
|
||||
|
||||
NPM := $(shell eval command -v npm)
|
||||
APT := $(shell eval command -v apt)
|
||||
|
||||
|
||||
.PHONY: help
|
||||
|
||||
help: ## Usage: make <concept>, eg: make install
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
install: ## Install required javascript dependencies
|
||||
ifndef NPM
|
||||
ifdef APT
|
||||
@echo "Installing NPM debian package…"
|
||||
sudo apt install -y npm
|
||||
endif
|
||||
endif
|
||||
npm install
|
||||
|
||||
|
||||
demo: ## Run locally at http://localhost:3000
|
||||
npm run dev
|
||||
|
||||
|
||||
love: ## Fund development of Majority Judgment
|
||||
firefox https://www.paypal.com/donate/?hosted_button_id=QD6U4D323WV4S
|
@ -1,62 +0,0 @@
|
||||
# Front-end election web application using NextJs
|
||||
|
||||
[![aGPLV3](https://img.shields.io/github/license/MieuxVoter/mv-front-react)](./LICENSE.md)
|
||||
[![Netlify Status](https://api.netlify.com/api/v1/badges/021c39c6-1018-4e3f-98e2-f808b4ea8f6d/deploy-status)](https://app.netlify.com/sites/epic-nightingale-99f910/deploys)
|
||||
[![Join the Discord chat at https://discord.gg/rAAQG9S](https://img.shields.io/discord/705322981102190593.svg)](https://discord.gg/rAAQG9S)
|
||||
|
||||
|
||||
:ballot_box: This project is going to be the default front-end for our [election application](https://app.mieuxvoter.fr).
|
||||
|
||||
:computer: It is connected to our [back-end](https://github.com/MieuxVoter/mv-api-server-apiplatform). The back-end is used for storing the votes and computing the majority judgment ranking. You can use our back-end free of charge, but you can also start your own instance of the back-end using our Dockerfiles.
|
||||
|
||||
:incoming_envelope: The front-end is responsable for sending the invitation mails. You can find the mail templates [on the functions folder](./functions/send-invite-email).
|
||||
|
||||
:world_map: The front-end stores its own translations. See below how you can edit them easily.
|
||||
|
||||
|
||||
## :paintbrush: Customize your own application
|
||||
|
||||
The separation between the front-end and the back-end makes it easy to customize your own application. Just install
|
||||
|
||||
|
||||
|
||||
## :gear: Install options
|
||||
|
||||
**Option one:** One-click deploy
|
||||
|
||||
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/MieuxVoter/mv-front-react&utm_source=github)
|
||||
|
||||
|
||||
**Option two:** Manual clone
|
||||
|
||||
1. Clone this repo: `git clone https://github.com/MieuxVoter/mv-front-nextjs.git`
|
||||
2. Navigate to the directory and install dependencies: `npm install` or `make`
|
||||
3. Start a local server: `npm run dev` and open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
4. Make your changes
|
||||
5. Deploy your project.
|
||||
|
||||
We advise for deploying the project to [Netlify](https://netlify.com), because we wrote the mail functions for the framework. Netlify parameters are written in `netlify.toml`.
|
||||
|
||||
If you decide to deploy your project in another way, please fill a pull-request to guide futur users!
|
||||
|
||||
## :incoming_envelope: Support for mail
|
||||
|
||||
To add support for mail sending, you need to connect the application with a mailing service. For now, we only support [Mailgun](mailgun.com), which offer very competitive prices. You can fill an issue if you require another mailing service.
|
||||
|
||||
To connect your application with Mailgun, you need to add the environment variables to your project:
|
||||
|
||||
- `MAILGUN_API_KEY`,
|
||||
- `MAILGUN_DOMAIN`,
|
||||
- `MAILGUN_URL`,
|
||||
- `FROM_EMAIL_ADDRESS`,
|
||||
- `CONTACT_TO_EMAIL_ADDRESS`.
|
||||
|
||||
You can add the environment variables on an `.env` file or directly on [Netlify](https://docs.netlify.com/configure-builds/environment-variables/).
|
||||
|
||||
|
||||
## :world_map: I18N at heart
|
||||
|
||||
You can directly modified the translation files in the folder `public/locales`.
|
||||
|
||||
In case you want to add support for another language, you need as well to add it on `net-i18next.config.js` and on the `LanguageSelector` component.
|
||||
|
@ -1,627 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Collapse,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Table,
|
||||
} from "reactstrap";
|
||||
import { getResults, getDetails, apiErrors } from "@services/api";
|
||||
import { grades } from "@services/grades";
|
||||
import { translateGrades } from "@services/grades";
|
||||
import Facebook from "@components/banner/Facebook";
|
||||
import Error from "@components/Error";
|
||||
import config from "../../../next-i18next.config.js";
|
||||
|
||||
export async function getServerSideProps({ query, locale }) {
|
||||
const { pid, tid } = query;
|
||||
|
||||
const [res, details, translations] = await Promise.all([
|
||||
getResults(pid),
|
||||
getDetails(pid),
|
||||
serverSideTranslations(locale, [], config),
|
||||
]);
|
||||
|
||||
if (typeof res === "string" || res instanceof String) {
|
||||
return { props: { err: res.slice(1, -1), ...translations } };
|
||||
}
|
||||
|
||||
if (typeof details === "string" || details instanceof String) {
|
||||
return { props: { err: res.slice(1, -1), ...translations } };
|
||||
}
|
||||
|
||||
if (!details.candidates || !Array.isArray(details.candidates)) {
|
||||
return { props: { err: "Unknown error", ...translations } };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
title: details.title,
|
||||
numGrades: details.num_grades,
|
||||
candidates: res,
|
||||
pid: pid,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const Result = ({ candidates, numGrades, title, pid, err }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (err && err !== "") {
|
||||
return <Error value={apiErrors(err, t)} />;
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const allGrades = translateGrades(t);
|
||||
const grades = allGrades.filter(
|
||||
(grade) => grade.value >= allGrades.length - numGrades
|
||||
);
|
||||
const offsetGrade = grades.length - numGrades;
|
||||
|
||||
const colSizeCandidateLg = 4;
|
||||
const colSizeCandidateMd = 6;
|
||||
const colSizeCandidateXs = 12;
|
||||
const colSizeGradeLg = 1;
|
||||
const colSizeGradeMd = 1;
|
||||
const colSizeGradeXs = 1;
|
||||
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
: "http://localhost";
|
||||
console.log("origin", origin);
|
||||
const urlVote = new URL(`/vote/${pid}`, origin);
|
||||
|
||||
|
||||
const [collapseProfiles, setCollapseProfiles] = useState(false);
|
||||
const [collapseGraphics, setCollapseGraphics] = useState(false);
|
||||
|
||||
const sum = (seq) => Object.values(seq).reduce((a, b) => a + b, 0);
|
||||
const numVotes =
|
||||
candidates && candidates.length > 0 ? sum(candidates[0].profile) : 1;
|
||||
const gradeIds =
|
||||
candidates && candidates.length > 0
|
||||
? Object.keys(candidates[0].profile)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta property="og:title" content={title} />
|
||||
</Head>
|
||||
{/* <Row>
|
||||
<Col xs="12">
|
||||
<h3>{title}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-5">
|
||||
<Col>
|
||||
<ol className="result">
|
||||
{candidates.map((candidate, i) => {
|
||||
const gradeValue = candidate.grade + offsetGrade;
|
||||
return (
|
||||
<li key={i} className="mt-2">
|
||||
<br />
|
||||
<span className="mt-2 ml-2">{candidate.name}</span><br />
|
||||
<span
|
||||
className="badge badge-light ml-2 mt-2"
|
||||
style={{
|
||||
backgroundColor: grades.slice(0).reverse()[
|
||||
candidate.grade
|
||||
].color,
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{allGrades.slice(0).reverse()[gradeValue].label}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<h5>
|
||||
<small>
|
||||
{t("resource.numVotes")}
|
||||
{" " + numVotes}
|
||||
</small>
|
||||
</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{candidates.map((candidate, i) => {
|
||||
const gradeValue = candidate.grade + offsetGrade;
|
||||
return (
|
||||
<Row key={i}>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader
|
||||
className="pointer"
|
||||
onClick={() => setCollapseGraphics(!collapseGraphics)}
|
||||
>
|
||||
<h4
|
||||
className={
|
||||
"m-0 panel-title " + (collapseGraphics ? "collapsed" : "")
|
||||
}
|
||||
>
|
||||
|
||||
<span style={{color: "#2400FD"}}>{candidate.name}</span>
|
||||
<span
|
||||
className="badge badge-light ml-2 mt-2"
|
||||
style={{
|
||||
backgroundColor: grades.slice(0).reverse()[
|
||||
candidate.grade
|
||||
].color,
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{allGrades.slice(0).reverse()[gradeValue].label}
|
||||
</span>
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<Collapse isOpen={collapseGraphics}>
|
||||
<CardBody className="pt-5">
|
||||
<div>
|
||||
<div
|
||||
className="median"
|
||||
style={{ height: candidates.length * 28 + 30 }}
|
||||
/>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td style={{ width: "30px" }}>{i + 1}</td>
|
||||
{candidate.label}
|
||||
<td>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
{gradeIds
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((id, i) => {
|
||||
const value = candidate.profile[id];
|
||||
if (value > 0) {
|
||||
let percent =
|
||||
(value * 100) / numVotes + "%";
|
||||
if (i === 0) {
|
||||
percent = "auto";
|
||||
}
|
||||
return (
|
||||
<td
|
||||
key={i}
|
||||
style={{
|
||||
width: percent,
|
||||
backgroundColor:
|
||||
grades[i].color,
|
||||
}}
|
||||
>
|
||||
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<small>
|
||||
|
||||
|
||||
<span>
|
||||
|
||||
{candidate.name}
|
||||
</span>
|
||||
|
||||
|
||||
</small>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<small>
|
||||
{grades.map((grade, i) => {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="badge badge-light mr-2 mt-2"
|
||||
style={{
|
||||
backgroundColor: grade.color,
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{grade.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Row className="mt-5">
|
||||
<Col>
|
||||
<Card className="bg-light text-primary">
|
||||
<CardHeader
|
||||
className="pointer"
|
||||
onClick={() => setCollapseGraphics(!collapseGraphics)}
|
||||
>
|
||||
<h4
|
||||
className={
|
||||
"m-0 panel-title " + (collapseGraphics ? "collapsed" : "")
|
||||
}
|
||||
>
|
||||
{t("Graph")}
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<Collapse isOpen={collapseGraphics}>
|
||||
<CardBody className="pt-5">
|
||||
<div>
|
||||
<div
|
||||
className="median"
|
||||
style={{ height: candidates.length * 28 + 30 }}
|
||||
/>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td style={{ width: "30px" }}>{i + 1}</td>
|
||||
{candidate.label}
|
||||
<td>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
{gradeIds
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((id, i) => {
|
||||
const value = candidate.profile[id];
|
||||
if (value > 0) {
|
||||
let percent =
|
||||
(value * 100) / numVotes + "%";
|
||||
if (i === 0) {
|
||||
percent = "auto";
|
||||
}
|
||||
return (
|
||||
<td
|
||||
key={i}
|
||||
style={{
|
||||
width: percent,
|
||||
backgroundColor:
|
||||
grades[i].color,
|
||||
}}
|
||||
>
|
||||
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<small>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
{i > 0 ? ", " : ""}
|
||||
<b>{i + 1}</b>: {candidate.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<small>
|
||||
{grades.map((grade, i) => {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="badge badge-light mr-2 mt-2"
|
||||
style={{
|
||||
backgroundColor: grade.color,
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{grade.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Row className="mt-5">
|
||||
<Col>
|
||||
<Card className="bg-light text-primary">
|
||||
<CardHeader
|
||||
className="pointer"
|
||||
onClick={() => setCollapseGraphics(!collapseGraphics)}
|
||||
>
|
||||
<h4
|
||||
className={
|
||||
"m-0 panel-title " + (collapseGraphics ? "collapsed" : "")
|
||||
}
|
||||
>
|
||||
{t("Graph")}
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<Collapse isOpen={collapseGraphics}>
|
||||
<CardBody className="pt-5">
|
||||
<div>
|
||||
<div
|
||||
className="median"
|
||||
style={{ height: candidates.length * 28 + 30 }}
|
||||
/>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td style={{ width: "30px" }}>{i + 1}</td>
|
||||
{candidate.label}
|
||||
<td>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
{gradeIds
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((id, i) => {
|
||||
const value = candidate.profile[id];
|
||||
if (value > 0) {
|
||||
let percent =
|
||||
(value * 100) / numVotes + "%";
|
||||
if (i === 0) {
|
||||
percent = "auto";
|
||||
}
|
||||
return (
|
||||
<td
|
||||
key={i}
|
||||
style={{
|
||||
width: percent,
|
||||
backgroundColor:
|
||||
grades[i].color,
|
||||
}}
|
||||
>
|
||||
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<small>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
{i > 0 ? ", " : ""}
|
||||
<b>{i + 1}</b>: {candidate.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<small>
|
||||
{grades.map((grade, i) => {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="badge badge-light mr-2 mt-2"
|
||||
style={{
|
||||
backgroundColor: grade.color,
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{grade.label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<Col>
|
||||
<Card className="bg-light text-primary">
|
||||
<CardHeader
|
||||
className="pointer"
|
||||
onClick={() => setCollapseProfiles(!collapseProfiles)}
|
||||
>
|
||||
<h4
|
||||
className={
|
||||
"m-0 panel-title " + (collapseProfiles ? "collapsed" : "")
|
||||
}
|
||||
>
|
||||
{t("Preference profile")}
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<Collapse isOpen={collapseProfiles}>
|
||||
<CardBody>
|
||||
<div className="table-responsive">
|
||||
<Table className="profiles">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
{grades.map((grade, i) => {
|
||||
return (
|
||||
<th key={i}>
|
||||
<span
|
||||
className="badge badge-light"
|
||||
style={{
|
||||
backgroundColor: grade.color,
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
{grade.label}{" "}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>{i + 1}</td>
|
||||
{gradeIds
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((id, i) => {
|
||||
const value = candidate.profile[id];
|
||||
const percent = (
|
||||
(value / numVotes) *
|
||||
100
|
||||
).toFixed(1);
|
||||
return <td key={i}>{percent} %</td>;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
<small>
|
||||
{candidates.map((candidate, i) => {
|
||||
return (
|
||||
<span key={i}>
|
||||
{i > 0 ? ", " : ""}
|
||||
<b>{i + 1}</b>: {candidate.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</small>
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs="12" className="text-center pt-2 pb-5">
|
||||
<Facebook
|
||||
className="btn btn-outline-light m-2"
|
||||
text={t("Share results on Facebook")}
|
||||
url={urlVote}
|
||||
title={title}
|
||||
/>
|
||||
</Col>
|
||||
</Row>*/}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default Result;
|
@ -1,166 +0,0 @@
|
||||
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;
|
@ -1,41 +0,0 @@
|
||||
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;
|
@ -1,21 +0,0 @@
|
||||
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;
|
@ -1,69 +0,0 @@
|
||||
/* 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;
|
@ -1,38 +0,0 @@
|
||||
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")
|
||||
})
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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;
|
@ -1,75 +0,0 @@
|
||||
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;
|
@ -1,62 +0,0 @@
|
||||
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;
|
@ -1,37 +0,0 @@
|
||||
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;
|
@ -1,29 +0,0 @@
|
||||
/* 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
|
@ -1,25 +0,0 @@
|
||||
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;
|
@ -1,24 +0,0 @@
|
||||
/* 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;
|
@ -1,44 +0,0 @@
|
||||
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;
|
@ -1,4 +0,0 @@
|
||||
import * as React from "react";
|
||||
import FlagIconFactory from "react-flag-icon-css";
|
||||
|
||||
export const FlagIcon = FlagIconFactory(React, { useCssModules: false });
|
@ -1,57 +0,0 @@
|
||||
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}
|
||||
>
|
||||
<img src="/arrow-dark-left.svg" /><p>Annuler</p>
|
||||
</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>"{label}"</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;
|
@ -1,72 +0,0 @@
|
||||
import {useState} from 'react'
|
||||
import ButtonWithConfirm from "./ButtonWithConfirm";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Label,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
} from "reactstrap";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {
|
||||
sortableHandle
|
||||
} from "react-sortable-hoc";
|
||||
import HelpButton from "@components/form/HelpButton";
|
||||
|
||||
const DragHandle = sortableHandle(({children}) => (
|
||||
<span className="input-group-text indexNumber">{children}</span>
|
||||
));
|
||||
const CandidateField = ({label, description, candIndex, onDelete, ...inputProps}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<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>
|
||||
<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="addCandidateText">Description (Facultatif)</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={description}
|
||||
{...inputProps}
|
||||
placeholder={t("resource.candidatePlaceholder")}
|
||||
tabIndex={candIndex + 1}
|
||||
maxLength="250"
|
||||
autoFocus
|
||||
className="addCandidateText"
|
||||
/>
|
||||
<ButtonWithConfirm className="" label={label} onDelete={onDelete}>
|
||||
|
||||
</ButtonWithConfirm>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<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
|
@ -1,133 +0,0 @@
|
||||
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'
|
||||
|
||||
// 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 nm de vos candidats.</h4>
|
||||
<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)}
|
||||
innerRef={candidate.fieldRef}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContainer>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
className="btnValidateCandidate"
|
||||
tabIndex={candidates.length + 2}
|
||||
type="button"
|
||||
onClick={addCandidate}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-2" />
|
||||
{t("Add a proposal")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default CandidatesField
|
||||
|
@ -1,164 +0,0 @@
|
||||
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
|
@ -1,75 +0,0 @@
|
||||
/* 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;
|
@ -1,83 +0,0 @@
|
||||
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;
|
@ -1,61 +0,0 @@
|
||||
/* 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;
|
@ -1,33 +0,0 @@
|
||||
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;
|
@ -1,47 +0,0 @@
|
||||
/* 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>
|
||||
);
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
/* 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 >
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
|
||||
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 />;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/* 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];
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
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;
|
@ -1,18 +0,0 @@
|
||||
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' } }
|
||||
/>
|
||||
)
|
@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
import Loader from "../loader";
|
||||
|
||||
const Wait = () => {
|
||||
return <Loader />;
|
||||
};
|
||||
|
||||
export default Wait;
|
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 32 KiB |
@ -1,14 +0,0 @@
|
||||
'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';
|
||||
},
|
||||
};
|
@ -1,40 +0,0 @@
|
||||
'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};`;
|
||||
},
|
||||
};
|
@ -1,84 +0,0 @@
|
||||
'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();
|
@ -1,90 +0,0 @@
|
||||
'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;
|
@ -1,35 +0,0 @@
|
||||
'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
|
||||
);
|
||||
};
|
@ -1,628 +0,0 @@
|
||||
'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,
|
||||
};
|
||||
};
|
@ -1,218 +0,0 @@
|
||||
<!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}}
|
||||
|
||||
<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}}
|
||||
|
||||
<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}}
|
||||
|
||||
<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>
|
@ -1,19 +0,0 @@
|
||||
{{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}}
|
@ -1,203 +0,0 @@
|
||||
<!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.
|
||||
|
||||
<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 :
|
||||
|
||||
<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>
|
@ -1,17 +0,0 @@
|
||||
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
|
@ -1,218 +0,0 @@
|
||||
<!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}}
|
||||
|
||||
<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}}
|
||||
|
||||
<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}}
|
||||
|
||||
<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>
|
@ -1,19 +0,0 @@
|
||||
{{#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}}
|
@ -1,150 +0,0 @@
|
||||
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;
|
@ -1,82 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const chalk = require("chalk");
|
||||
|
||||
module.exports = {
|
||||
input: [
|
||||
"app/**/*.{js,jsx}",
|
||||
// Use ! to filter out files or directories
|
||||
"!app/**/*.spec.{js,jsx}",
|
||||
"!app/i18n/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
output: ".",
|
||||
options: {
|
||||
debug: true,
|
||||
func: {
|
||||
list: ["i18next.t", "i18n.t", "t"],
|
||||
extensions: [".js", ".jsx"]
|
||||
},
|
||||
trans: {
|
||||
component: "Trans",
|
||||
i18nKey: "i18nKey",
|
||||
defaultsKey: "defaults",
|
||||
extensions: [".js", ".jsx"],
|
||||
fallbackKey: function(ns, value) {
|
||||
return value;
|
||||
},
|
||||
acorn: {
|
||||
ecmaVersion: 10, // defaults to 10
|
||||
sourceType: "module" // defaults to 'module'
|
||||
// Check out https://github.com/acornjs/acorn/tree/master/acorn#interface for additional options
|
||||
}
|
||||
},
|
||||
lngs: ["en", "fr", "es", "de", "ru"],
|
||||
ns: ["resource", "common"],
|
||||
defaultLng: "en",
|
||||
defaultNs: "resource",
|
||||
defaultValue: "__STRING_NOT_TRANSLATED__",
|
||||
resource: {
|
||||
loadPath: "./public/locale/i18n/{{lng}}/{{ns}}.json",
|
||||
savePath: "./public/locale/i18n/{{lng}}/{{ns}}.json",
|
||||
jsonIndent: 2,
|
||||
lineEnding: "\n"
|
||||
},
|
||||
nsSeparator: false, // namespace separator
|
||||
keySeparator: false, // key separator
|
||||
interpolation: {
|
||||
prefix: "{{",
|
||||
suffix: "}}"
|
||||
}
|
||||
},
|
||||
transform: function customTransform(file, enc, done) {
|
||||
"use strict";
|
||||
const parser = this.parser;
|
||||
const content = fs.readFileSync(file.path, enc);
|
||||
let count = 0;
|
||||
|
||||
parser.parseFuncFromString(
|
||||
content,
|
||||
{ list: ["i18next._", "i18next.__"] },
|
||||
(key, options) => {
|
||||
parser.set(
|
||||
key,
|
||||
Object.assign({}, options, {
|
||||
nsSeparator: false,
|
||||
keySeparator: false
|
||||
})
|
||||
);
|
||||
++count;
|
||||
}
|
||||
);
|
||||
|
||||
if (count > 0) {
|
||||
/* console.log(
|
||||
`i18next-scanner: count=${chalk.cyan(count)}, file=${chalk.yellow(
|
||||
JSON.stringify(file.relative)
|
||||
)}`
|
||||
);*/
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@components/*": ["components/*"],
|
||||
"@styles/*": ["styles/*"],
|
||||
"@services/*": ["services/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "out"
|
||||
functions = "functions"
|
||||
|
||||
[dev]
|
||||
command = "npm run dev"
|
@ -1,9 +0,0 @@
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: "fr",
|
||||
locales: ["en", "fr", "de", "es", "ru"],
|
||||
ns: ["resource", "common", "error"],
|
||||
defaultNS: "resource",
|
||||
fallbackNS: ["common", "error"],
|
||||
},
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
const { i18n } = require("./next-i18next.config");
|
||||
|
||||
module.exports = {
|
||||
|
||||
i18n,
|
||||
// See https://github.com/netlify/netlify-plugin-nextjs/issues/223
|
||||
unstableNetlifyFunctionsSupport: {
|
||||
"pages/index.jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/faq.jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/legal-notices.jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/new/confirm/[pid].jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/new.jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/result/[pid]/[[...tid]].jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/vote/[pid]/[[...tid]].jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/vote/[pid]/confirm.jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
"pages/privacy-policy.jsx": {
|
||||
includeDirs: ["public"],
|
||||
},
|
||||
},
|
||||
pageExtensions: ["mdx", "jsx", "js", "ts", "tsx"],
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ["@svgr/webpack"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
target: "experimental-serverless-trace",
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "mv-front-react",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"export": "next export"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@weknow/react-bubble-chart-d3": "^1.0.12",
|
||||
"array-move": "^3.0.1",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-scss": "^4.6.0",
|
||||
"d3": "^7.3.0",
|
||||
"d3-require": "^1.2.4",
|
||||
"domexception": "^2.0.1",
|
||||
"dotenv": "^8.6.0",
|
||||
"form-data": "^4.0.0",
|
||||
"gsap": "^3.9.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"handlebars-i18next": "^1.0.1",
|
||||
"highcharts": "^9.3.2",
|
||||
"highcharts-react-official": "^3.1.0",
|
||||
"i18next": "^20.2.2",
|
||||
"i18next-chained-backend": "^2.1.0",
|
||||
"i18next-fs-backend": "^1.1.1",
|
||||
"i18next-http-backend": "^1.2.4",
|
||||
"i18next-localstorage-backend": "^3.1.2",
|
||||
"i18next-text": "^0.5.6",
|
||||
"mailgun.js": "^3.3.2",
|
||||
"next": "^10.2.0",
|
||||
"next-i18next": "^8.2.0",
|
||||
"plotly.js": "^2.8.3",
|
||||
"plotly.js-dist": "^2.8.3",
|
||||
"query-string": "^7.0.0",
|
||||
"ramda": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.1.0",
|
||||
"react-bubble-chart": "^0.4.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-flags-select": "^2.1.2",
|
||||
"react-google-charts": "^3.0.15",
|
||||
"react-i18next": "^11.8.15",
|
||||
"react-modal": "^3.14.4",
|
||||
"react-multi-email": "^0.5.3",
|
||||
"react-plotly.js": "^2.5.1",
|
||||
"react-responsive": "^9.0.0-beta.6",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-toastify": "^7.0.4",
|
||||
"reactstrap": "^8.9.0",
|
||||
"sass": "^1.32.13",
|
||||
"styled-components": "^5.3.3"
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import Head from 'next/head'
|
||||
import '@styles/globals.css'
|
||||
import '@styles/footer.css'
|
||||
import '@styles/loader.css'
|
||||
import "@styles/scss/config.scss";
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||
import {appWithTranslation} from 'next-i18next'
|
||||
import {AppProvider} from '@services/context.js'
|
||||
import Header from '@components/layouts/Header'
|
||||
import Footer from '@components/layouts/Footer'
|
||||
|
||||
|
||||
function Application({Component, pageProps}) {
|
||||
const origin = typeof window !== 'undefined' && window.location.origin ? window.location.origin : 'http://localhost';
|
||||
return (<AppProvider>
|
||||
<Head>
|
||||
<link rel="icon" key="favicon" href="/favicon.ico" />
|
||||
<meta property="og:url" content={origin} key="og:url" />
|
||||
<meta property="og:type" content="website" key="og:type" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://app.mieuxvoter.fr/app-mieux-voter.png"
|
||||
key="og:image"
|
||||
/>
|
||||
</Head>
|
||||
<Header />
|
||||
<main className="d-flex flex-column justify-content-center">
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
<Footer />
|
||||
</AppProvider>);
|
||||
}
|
||||
|
||||
export default appWithTranslation(Application)
|
@ -1,171 +0,0 @@
|
||||
import { createRef } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import {
|
||||
getDetails,
|
||||
apiErrors,
|
||||
ELECTION_NOT_STARTED_ERROR,
|
||||
} from "@services/api";
|
||||
import { Col, Container, Row } from "reactstrap";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
faCopy,
|
||||
faVoteYea,
|
||||
faExclamationTriangle,
|
||||
faExternalLinkAlt,
|
||||
faPollH,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import CopyField from "@components/CopyField";
|
||||
import Error from "@components/Error";
|
||||
import Facebook from "@components/banner/Facebook";
|
||||
import config from "../../../next-i18next.config.js";
|
||||
|
||||
export async function getServerSideProps({ query: { pid }, locale }) {
|
||||
let [details, translations] = await Promise.all([
|
||||
getDetails(pid),
|
||||
serverSideTranslations(locale, [], config),
|
||||
]);
|
||||
|
||||
// if (details.includes(ELECTION_NOT_STARTED_ERROR)) {
|
||||
// details = { title: "", on_invitation_only: true, restrict_results: true };
|
||||
// } else {
|
||||
// if (typeof details === "string" || details instanceof String) {
|
||||
// return { props: { err: details, ...translations } };
|
||||
// }
|
||||
|
||||
// if (!details.title) {
|
||||
// return { props: { err: "Unknown error", ...translations } };
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
props: {
|
||||
invitationOnly: details.on_invitation_only,
|
||||
restrictResults: details.restrict_results,
|
||||
title: details.title,
|
||||
pid: pid,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ConfirmElection = ({
|
||||
title,
|
||||
restrictResults,
|
||||
invitationOnly,
|
||||
pid,
|
||||
err,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (err) {
|
||||
return <Error value={apiErrors(err, t)} />;
|
||||
}
|
||||
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
: "http://localhost";
|
||||
const urlVote = new URL(`/vote/${pid}`, origin);
|
||||
const urlResult = new URL(`/result/${pid}`, origin);
|
||||
|
||||
const electionLink = invitationOnly ? (
|
||||
<>
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
"Voters received a link to vote by email. Each link can be used only once!"
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-1">{t("Voting address")}</p>
|
||||
<CopyField
|
||||
value={urlVote.href}
|
||||
iconCopy={faCopy}
|
||||
iconOpen={faExternalLinkAlt}
|
||||
t={t}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const fb = invitationOnly ? null : (
|
||||
<Facebook
|
||||
className="btn btn-sm btn-outline-light m-2"
|
||||
text={t("Share election on Facebook")}
|
||||
url={urlVote}
|
||||
title={"app.mieuxvoter.fr"}
|
||||
/>
|
||||
);
|
||||
|
||||
const participate = invitationOnly ? null : (
|
||||
<>
|
||||
<Col className="col-lg-3 text-center mr-10">
|
||||
<Link href={`/vote/${pid}`}>
|
||||
<a target="_blank" rel="noreferrer" className="btn btn-success">
|
||||
<FontAwesomeIcon icon={faVoteYea} className="mr-2" />
|
||||
{t("resource.participateBtn")}
|
||||
</a>
|
||||
</Link>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Head>
|
||||
<title>{t("Successful election creation!")}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta key="og:title" property="og:title" content={title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
key="og:description"
|
||||
content={t("common.application")}
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<Row className="mt-5">
|
||||
<Col className="text-center offset-lg-3" lg="6">
|
||||
<h2>{t("Successful election creation!")}</h2>
|
||||
{fb}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="mt-5 mb-4">
|
||||
<Col className="offset-lg-3" lg="6">
|
||||
<h3 className="mb-3 text-center">{title}</h3>
|
||||
<h5 className="mb-3 text-center">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="mr-2" />
|
||||
{t("Keep these links carefully")}
|
||||
</h5>
|
||||
<div className="border rounded p-4 pb-5">
|
||||
{electionLink}
|
||||
|
||||
<p className="mt-4 mb-1">{t("Results address")}</p>
|
||||
<CopyField
|
||||
value={urlResult}
|
||||
iconCopy={faCopy}
|
||||
iconOpen={faExternalLinkAlt}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="mt-4 mb-4 justify-content-md-center">
|
||||
{participate}
|
||||
<Col className="text-center col-lg-3">
|
||||
<Link href={`/result/${pid}`}>
|
||||
<a target="_blank" rel="noreferrer" className="btn btn-secondary">
|
||||
<FontAwesomeIcon icon={faPollH} className="mr-2" />
|
||||
{t("resource.resultsBtn")}
|
||||
</a>
|
||||
</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmElection;
|
@ -1,29 +0,0 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import plotly from 'plotly.js/dist/plotly';
|
||||
import createPlotComponent from 'react-plotly.js/factory';
|
||||
import React, { Component } from 'react';
|
||||
//import Plot from 'react-plotly.js';
|
||||
|
||||
class BarChart extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Plot
|
||||
data={[
|
||||
{
|
||||
type: 'bar',
|
||||
x: ['Taubira', 'Hidalgo', 'Mélenchon'],
|
||||
y: [29,150,85]
|
||||
}
|
||||
]}
|
||||
layout={ { width: 1000, height: 500, title: 'Nombre de voix par candidat' } }
|
||||
config={{
|
||||
displayModeBar: false // this is the line that hides the bar.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export default BarChart;
|
@ -1,50 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import dynamic from 'next/dynamic';
|
||||
import HeaderResult from '../../../components/layouts/result/HeaderResult'
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Collapse,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
} from "reactstrap";
|
||||
|
||||
const DynamicPlot = dynamic(import('../../../components/plot'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
//import ChartWrapper from "../../../components/ChartWrapper";
|
||||
import SystemeVote from '../../../components/SystemeVote';
|
||||
import LoadingScreen from '../../../components/LoadingScreen'
|
||||
export default function Result() {
|
||||
const [collapseGraphics, setCollapseGraphics] = useState(false);
|
||||
// const [loading, setLoading] = React.useState(true);
|
||||
// React.useEffect(() =>{
|
||||
// setTimeout(() => setLoading(false), 3000);
|
||||
// })
|
||||
return (
|
||||
<div>
|
||||
<HeaderResult />
|
||||
<section className="resultPage">
|
||||
<h4>Détails des résultats</h4>
|
||||
<Card className="resultCard">
|
||||
<CardHeader className="pointer" onClick={() => setCollapseGraphics(!collapseGraphics)}>
|
||||
<h4 className={"m-0 panel-title " + (collapseGraphics ? "collapsed" : "")}>
|
||||
Taubira
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<Collapse isOpen={collapseGraphics}>
|
||||
<CardBody className="pt-5">
|
||||
|
||||
<SystemeVote />
|
||||
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,271 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Button, Col, Container, Row } from "reactstrap";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getDetails, castBallot, apiErrors } from "@services/api";
|
||||
import Error from "@components/Error";
|
||||
import { translateGrades } from "@services/grades";
|
||||
import config from "../../../next-i18next.config.js";
|
||||
|
||||
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
|
||||
|
||||
export async function getServerSideProps({ query: { pid, tid }, locale }) {
|
||||
const [details, translations] = await Promise.all([
|
||||
getDetails(pid),
|
||||
serverSideTranslations(locale, [], config),
|
||||
]);
|
||||
|
||||
if (typeof details === "string" || details instanceof String) {
|
||||
return { props: { err: details, ...translations } };
|
||||
}
|
||||
|
||||
if (!details.candidates || !Array.isArray(details.candidates)) {
|
||||
return { props: { err: "Unknown error", ...translations } };
|
||||
}
|
||||
|
||||
shuffle(details.candidates);
|
||||
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
invitationOnly: details.on_invitation_only,
|
||||
restrictResults: details.restrict_results,
|
||||
candidates: details.candidates.map((name, i) => ({ id: i, label: name })),
|
||||
title: details.title,
|
||||
numGrades: details.num_grades,
|
||||
pid: pid,
|
||||
token: tid || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (err) {
|
||||
return <Error value={apiErrors(err, t)}></Error>;
|
||||
}
|
||||
|
||||
const [judgments, setJudgments] = useState([]);
|
||||
const colSizeCandidateLg = 4;
|
||||
const colSizeCandidateMd = 6;
|
||||
const colSizeCandidateXs = 12;
|
||||
const colSizeGradeLg = Math.floor((12 - colSizeCandidateLg) / numGrades);
|
||||
const colSizeGradeMd = Math.floor((12 - colSizeCandidateMd) / numGrades);
|
||||
const colSizeGradeXs = Math.floor((12 - colSizeCandidateXs) / numGrades);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const allGrades = translateGrades(t);
|
||||
const grades = allGrades.filter(
|
||||
(grade) => grade.value >= allGrades.length - numGrades
|
||||
);
|
||||
|
||||
const handleGradeClick = (event) => {
|
||||
let data = {
|
||||
id: parseInt(event.currentTarget.getAttribute("data-id")),
|
||||
value: parseInt(event.currentTarget.value),
|
||||
};
|
||||
//remove candidate
|
||||
const newJudgments = judgments.filter(
|
||||
(judgment) => judgment.id !== data.id
|
||||
);
|
||||
newJudgments.push(data);
|
||||
setJudgments(newJudgments);
|
||||
};
|
||||
|
||||
const handleSubmitWithoutAllRate = () => {
|
||||
toast.error(t("You have to judge every candidate/proposal!"), {
|
||||
position: toast.POSITION.TOP_CENTER,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const gradesById = {};
|
||||
judgments.forEach((c) => {
|
||||
gradesById[c.id] = c.value;
|
||||
});
|
||||
const gradesByCandidate = [];
|
||||
Object.keys(gradesById).forEach((id) => {
|
||||
gradesByCandidate.push(gradesById[id]);
|
||||
});
|
||||
|
||||
castBallot(gradesByCandidate, pid, token, () => {
|
||||
router.push(`/vote/${pid}/confirm`);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
|
||||
<title>{title}</title>
|
||||
<meta key="og:title" property="og:title" content={title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
key="og:description"
|
||||
content={t("common.application")}
|
||||
/>
|
||||
</Head>
|
||||
<ToastContainer />
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<Row>
|
||||
<Col>
|
||||
<h3>{title}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="cardVote d-none d-lg-flex">
|
||||
<Col
|
||||
xs={colSizeCandidateXs}
|
||||
md={colSizeCandidateMd}
|
||||
lg={colSizeCandidateLg}
|
||||
>
|
||||
<h5> </h5>
|
||||
</Col>
|
||||
{grades.map((grade, gradeId) => {
|
||||
return gradeId < numGrades ? (
|
||||
<Col
|
||||
xs={colSizeGradeXs}
|
||||
md={colSizeGradeMd}
|
||||
lg={colSizeGradeLg}
|
||||
key={gradeId}
|
||||
className="text-center p-0"
|
||||
style={{ lineHeight: 2 }}
|
||||
>
|
||||
<small
|
||||
className="nowrap bold badge"
|
||||
style={{ backgroundColor: grade.color, color: "#fff" }}
|
||||
>
|
||||
{grade.label}
|
||||
</small>
|
||||
</Col>
|
||||
) : null;
|
||||
})}
|
||||
</Row>
|
||||
|
||||
{candidates.map((candidate, candidateId) => {
|
||||
return (
|
||||
<Row key={candidateId} className="cardVote">
|
||||
<Col
|
||||
xs={colSizeCandidateXs}
|
||||
md={colSizeCandidateMd}
|
||||
lg={colSizeCandidateLg}
|
||||
>
|
||||
<h5 className="m-0">{candidate.label}</h5>
|
||||
<hr className="d-lg-none" />
|
||||
</Col>
|
||||
{grades.map((grade, gradeId) => {
|
||||
console.assert(gradeId < numGrades);
|
||||
const gradeValue = grade.value;
|
||||
return (
|
||||
<Col
|
||||
xs={colSizeGradeXs}
|
||||
md={colSizeGradeMd}
|
||||
lg={colSizeGradeLg}
|
||||
key={gradeId}
|
||||
className="text-lg-center"
|
||||
>
|
||||
<label
|
||||
htmlFor={
|
||||
"candidateGrade" + candidateId + "-" + gradeValue
|
||||
}
|
||||
className="check"
|
||||
>
|
||||
<small
|
||||
className="nowrap d-lg-none ml-2 bold badge"
|
||||
style={
|
||||
judgments.find((judgment) => {
|
||||
return (
|
||||
JSON.stringify(judgment) ===
|
||||
JSON.stringify({
|
||||
id: candidate.id,
|
||||
value: gradeValue,
|
||||
})
|
||||
);
|
||||
})
|
||||
? { backgroundColor: grade.color, color: "#fff" }
|
||||
: {
|
||||
backgroundColor: "transparent",
|
||||
color: "#000",
|
||||
}
|
||||
}
|
||||
>
|
||||
{grade.label}
|
||||
</small>
|
||||
<input
|
||||
type="radio"
|
||||
name={"candidate" + candidateId}
|
||||
id={"candidateGrade" + candidateId + "-" + gradeValue}
|
||||
data-index={candidateId}
|
||||
data-id={candidate.id}
|
||||
value={grade.value}
|
||||
onClick={handleGradeClick}
|
||||
defaultChecked={judgments.find((element) => {
|
||||
return (
|
||||
JSON.stringify(element) ===
|
||||
JSON.stringify({
|
||||
id: candidate.id,
|
||||
value: gradeValue,
|
||||
})
|
||||
);
|
||||
})}
|
||||
/>
|
||||
<span
|
||||
className="checkmark"
|
||||
style={
|
||||
judgments.find(function (judgment) {
|
||||
return (
|
||||
JSON.stringify(judgment) ===
|
||||
JSON.stringify({
|
||||
id: candidate.id,
|
||||
value: gradeValue,
|
||||
})
|
||||
);
|
||||
})
|
||||
? { backgroundColor: grade.color, color: "#fff" }
|
||||
: {
|
||||
backgroundColor: "transparent",
|
||||
color: "#000",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
<Row>
|
||||
<Col className="text-center">
|
||||
{judgments.length !== candidates.length ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmitWithoutAllRate}
|
||||
className="btn btn-dark "
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{t("Submit my vote")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="submit" className="btn btn-success ">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
{t("Submit my vote")}
|
||||
</Button>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</form>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default VoteBallot;
|
@ -1,79 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import { Col, Container, Row } from "reactstrap";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Paypal from "@components/banner/Paypal";
|
||||
import Gform from "@components/banner/Gform";
|
||||
import Error from "@components/Error";
|
||||
import { getDetails, apiErrors } from "@services/api";
|
||||
import config from "../../../next-i18next.config.js";
|
||||
|
||||
export async function getServerSideProps({ query: { pid }, locale }) {
|
||||
const [details, translations] = await Promise.all([
|
||||
getDetails(pid),
|
||||
serverSideTranslations(locale, [], config),
|
||||
]);
|
||||
|
||||
if (typeof details === "string" || details instanceof String) {
|
||||
return { props: { err: res.slice(1, -1), ...translations } };
|
||||
}
|
||||
|
||||
if (!details.candidates || !Array.isArray(details.candidates)) {
|
||||
return { props: { err: "Unknown error", ...translations } };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
invitationOnly: details.on_invitation_only,
|
||||
restrictResults: details.restrict_results,
|
||||
candidates: details.candidates.map((name, i) => ({ id: i, label: name })),
|
||||
title: details.title,
|
||||
numGrades: details.num_grades,
|
||||
pid: pid,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const VoteSuccess = ({ title, invitationOnly, pid, err }) => {
|
||||
const { t } = useTranslation();
|
||||
if (err && err !== "") {
|
||||
return <Error value={apiErrors(err, t)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Head>
|
||||
<title>{t("resource.voteSuccess")}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta key="og:title" property="og:title" content={title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
key="og:description"
|
||||
content={t("common.application")}
|
||||
/>
|
||||
</Head>
|
||||
<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 offset-lg-3" lg="6">
|
||||
<h2>{t("resource.voteSuccess")}</h2>
|
||||
<p>{t("resource.thanks")}</p>
|
||||
<div className="mt-3">
|
||||
<Gform className="btn btn-secondary" />
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Paypal btnColor="btn-success" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default VoteSuccess;
|
Before Width: | Height: | Size: 165 B |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 525 B |
Before Width: | Height: | Size: 521 B |
Before Width: | Height: | Size: 375 B |
Before Width: | Height: | Size: 297 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 765 B |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 386 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 26 KiB |