@ -0,0 +1,35 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
# 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
|
@ -0,0 +1,28 @@
|
||||
# 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
|
@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
@ -0,0 +1,627 @@
|
||||
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;
|
@ -0,0 +1,82 @@
|
||||
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();
|
||||
}
|
||||
};
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@components/*": ["components/*"],
|
||||
"@styles/*": ["styles/*"],
|
||||
"@services/*": ["services/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "out"
|
||||
functions = "functions"
|
||||
|
||||
[dev]
|
||||
command = "npm run dev"
|
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: "fr",
|
||||
locales: ["en", "fr", "de", "es", "ru"],
|
||||
ns: ["resource", "common", "error"],
|
||||
defaultNS: "resource",
|
||||
fallbackNS: ["common", "error"],
|
||||
},
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
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",
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
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)
|
@ -0,0 +1,171 @@
|
||||
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;
|
@ -0,0 +1,29 @@
|
||||
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;
|
@ -0,0 +1,50 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
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;
|
@ -0,0 +1,79 @@
|
||||
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;
|
After Width: | Height: | Size: 165 B |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 525 B |
After Width: | Height: | Size: 521 B |
After Width: | Height: | Size: 375 B |
After Width: | Height: | Size: 297 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 240 B |
After Width: | Height: | Size: 765 B |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 512 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 406 B |
After Width: | Height: | Size: 386 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 32 KiB |
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1,100 @@
|
||||
{
|
||||
"title": "Plattform mit Mehrheitswahl",
|
||||
"Homepage": "Homepage",
|
||||
"Source code": "Quellcode",
|
||||
"Who are we?": "Wer wir sind?",
|
||||
"Privacy policy": "Datenschutzerklärung",
|
||||
"resource.legalNotices": "Rechtliche Hinweise",
|
||||
"FAQ": "FAQ",
|
||||
"resource.help": "Brauchen Sie Hilfe?",
|
||||
"BetterVote": " BetterVote",
|
||||
"Voting platform": "Wahlplattform",
|
||||
"Majority Judgment": " Mehrheitswahl ",
|
||||
"Start an election": "Eine Wahl beginnen",
|
||||
"resource.candidatePlaceholder": "Name des Kandidaten/Abstimmungsvorschlags",
|
||||
"Delete?": "Löschen?",
|
||||
"Are you sure to delete": "Sind Sie sich sicher, dass Sie dies löschen möchten?",
|
||||
"the row": "die Zeile",
|
||||
"Write here your question or introduce simple your election (250 characters max.)": "Schreiben Sie hier Ihre Frage oder erklären Sie kurz ihre Wahl (bis 250 Zeichen)",
|
||||
"Enter the name of your candidate or proposal here (250 characters max.)": "Geben Sie hier den Namen Ihres Kandidaten oder Antrags ein (max. 250 Zeichen)",
|
||||
"Please add at least 2 candidates.": "Bitte geben Sie mindestens zwei Kandidaten vor. ",
|
||||
"Question of the election": "Zur Wahl stehende Frage",
|
||||
"Write here the question of your election": "Schreiben Sie hier die zur Wahl stehenden Frage",
|
||||
"For example:": "Zum Beispiel",
|
||||
"For the role of my representative, I judge this candidate...": "Meine Einschätzung des Kandidaten als meinen Repräsentanten ist …",
|
||||
"Candidates/Proposals": "Kandidaten/Abstimmungsvorschlag ",
|
||||
"Add a proposal": "Weiteren hinzufügen",
|
||||
"Advanced options": "Weitere Optionen",
|
||||
"Starting date": "Anfangsdatum",
|
||||
"Ending date": "Enddatum",
|
||||
"Defined period" : "Definierte Periode",
|
||||
"Unlimited" : "Unbegrenzt",
|
||||
"Voting time" : "Abstimmungszeit",
|
||||
"Grades": "Note",
|
||||
"You can select here the number of grades for your election": " Sie können hier die Anzahl der Noten für Ihre Wahl auswählen ",
|
||||
"5 = Excellent, Very good, Good, Fair, Passable": "5 = hervorragend, sehr gut, gut, befriedigend, ausreichend",
|
||||
"Participants": "Teilnehmer",
|
||||
"Add here participants' emails": "Fügen Sie hier die Email Adressen der Teilnehmer hinzu.",
|
||||
"List voters' emails in case the election is not opened": "Falls die Wahl noch nicht sofort geöffnet werden soll, fügen Sie die Email Adressen der Teilnehmer hier zu.",
|
||||
"Validate": "Ok",
|
||||
"Submit my vote": "Ok",
|
||||
"Confirm your vote": "Bestätigen Sie Ihre Wahl",
|
||||
"The form contains no address.": "Keine Email Adresse wurde hinzugefügt.",
|
||||
"The election will be opened to anyone with the link": "Die Wahl ist offen afür jeden, der diesen Link hat.",
|
||||
"Start the election": "Mit der Wahl beginnen.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Confirm": "Ok",
|
||||
"Successful election creation!": "Die Wahl wurde erfolgreich erstellt!",
|
||||
"You can now share the election link to participants:": "Sie können nun den Teilnehmern den Link zukommen lassen.",
|
||||
"Copy": "Kopieren",
|
||||
"Here is the link for the results in real time:": " Hier ist der Link für die Ergebnisse in Echtzeit:",
|
||||
"Keep these links carefully": "Speichern Sie diesen Link an einem sicheren Ort.",
|
||||
"resource.participateBtn": "Machen Sie jetzt mit!",
|
||||
"t": "<0>Achtung</0> : Sie werden zu einem späteren Zeitpunkt keine Möglichkeit haben, diese Links abzurufen, auch wir haben keinen Zugriff darauf. Sie können aber beispielsweise diese Seite in Ihrem Browser als Lesezeichen speichern.",
|
||||
"Simple and free: organize an election with Majority Judgment.": "Einfach und kostenlos: Organisation von Mehrheitswahlen.",
|
||||
"Start": "Start",
|
||||
"No advertising or ad cookies": "Keine Werbung und auch keine Cookies zu Werbezwecken.",
|
||||
"Oops! This election does not exist or it is not available anymore.": "Ups! Diese Wahl existiert nicht oder ist nicht mehr verfügbar. ",
|
||||
"You can start another election.": "Sie können eine neue Umfrage starten.",
|
||||
"Go back to homepage": "Zurück zur Hompage",
|
||||
"You have to judge every candidate/proposal!": "Sie müssen jeden Kandidaten/Abstimmungsvorschlag bewerten!",
|
||||
"resource.voteSuccess": " Ihre Teilnahme wurde gespeichert!",
|
||||
"resource.thanks": " Vielen Dank für Ihre Teilnahme.",
|
||||
"Support us !": "Unterstützen Sie uns!",
|
||||
"PayPal - The safer, easier way to pay online!": "PayPal - Die sicherere und einfachere Art, online zu bezahlen!",
|
||||
"resource.numVotes": "Anzahl der Stimmen:",
|
||||
"Unknown error. Try again please.": "Unbekannter Fehler. Bitte versuchen Sie es erneut.",
|
||||
"Ending date:": "Enddatum:",
|
||||
"If you list voters' emails, only them will be able to access the election": "Wenn Sie die E-Mails der Wähler auflisten, haben nur diese Zugriff auf die Wahl",
|
||||
"Dates": "Datum",
|
||||
"The election will take place from": "Die Wahl findet von",
|
||||
"at": "um",
|
||||
"to": "bis",
|
||||
"Voters' list": "Wählerliste",
|
||||
"Voters received a link to vote by email. Each link can be used only once!": "Die Wähler erhielten per E-Mail einen Link zur Stimmabgabe. Jeder Link kann nur einmal verwendet werden!",
|
||||
"Results of the election:": "Ergebnisse der Wahl",
|
||||
"Graph": "Grafik",
|
||||
"Preference profile": "Präferenz-Profil",
|
||||
"Oops... The election is unknown.": "Hoppla... Die Wahl ist unbekannt.",
|
||||
"The election is still going on. You can't access now to the results.": "Die Wahl dauert noch an. Sie können jetzt nicht auf die Ergebnisse zugreifen.",
|
||||
"No votes have been recorded yet. Come back later.": "Es wurden noch keine Abstimmungen registriert. Kommen Sie später wieder.",
|
||||
"The election has not started yet.": "Die Wahlen haben noch nicht begonnen.",
|
||||
"The election is over. You can't vote anymore": "Die Wahl ist vorbei. Sie können nicht mehr wählen",
|
||||
"You need a token to vote in this election": "Sie brauchen eine Wertmarke, um an dieser Wahl teilzunehmen",
|
||||
"You seem to have already voted.": "Sie scheinen bereits abgestimmt zu haben.",
|
||||
"The parameters of the election are incorrect.": "Die Parameter der Wahl sind falsch.",
|
||||
"Access to results" : "Zugang zu den Ergebnissen",
|
||||
"Immediately": "Sofort",
|
||||
"At the end of the election": "Am Ende der Wahl",
|
||||
"Results available at the close of the vote": "Ergebnisse am Ende der Abstimmung verfügbar",
|
||||
"The results page will not be accessible until all participants have voted.":"Die Ergebnisseite wird nicht zugänglich sein, bis alle Teilnehmer abgestimmt haben.",
|
||||
"The results page will not be accessible until the end date is reached.": "Die Ergebnisseite wird nicht zugänglich sein, bis das Enddatum erreicht ist.",
|
||||
"No one will be able to see the result until the end date is reached or until all participants have voted.": "Niemand wird das Ergebnis sehen können, bis das Enddatum erreicht ist oder bis alle Teilnehmer abgestimmt haben.",
|
||||
"Send me this link" : "Senden Sie mir diesen Link",
|
||||
"Send me these links" : "Schicken Sie mir diesen Link",
|
||||
"Open" : "Öffnen Sie",
|
||||
"Voting address" : "Abstimmungs-URL",
|
||||
"Results address" : "Ergebnis-URL",
|
||||
"Share election on Facebook" : "Wahl auf Facebook teilen",
|
||||
"Share results on Facebook" : "Ergebnisse auf Facebook teilen"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"common.vote": "Vote!",
|
||||
"common.mieuxvoter": "Better Vote",
|
||||
"common.helpus": "Do you want to help us?",
|
||||
"common.valueProp": "Simple and free: organise a vote with Majority Judgment.",
|
||||
"common.candidates": "Candidates/Proposals",
|
||||
"common.backHomepage": "Back to home page"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"email.hello": "Hi, there! 🙂",
|
||||
"email.why": "This email was sent to you because your email was filled out to participate in the vote on the subject:",
|
||||
"email.linkVote": "The link for the vote is as follows:",
|
||||
"email.linkResult": "The link that will give you the results when they are available is as follows:",
|
||||
"email.happy": "We are happy to send you this email! You will be able to vote using majority judgment.",
|
||||
"email.copyLink": "If that doesn't work, copy and paste the following link into your browser:",
|
||||
"email.aboutjm": "If you require any further information, please visit our site.",
|
||||
"email.bye": "Good vote! 🤗"
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"error.e1": "Oops... The election is unknown",
|
||||
"error.e2": "The election is still going on. You can't access now to the results.",
|
||||
"error.e3": "No votes have been recorded yet. Come back later.",
|
||||
"error.e4": "The election has not started yet.",
|
||||
"error.e5": "The election is over. You can't vote anymore",
|
||||
"error.e6": "You need a token to vote in this election",
|
||||
"error.e7": "You seem to have already voted.",
|
||||
"error.e8": "The parameters of the election are incorrect.",
|
||||
"error.catch22": "Unknown error"
|
||||
}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1,107 @@
|
||||
{
|
||||
"title": "Application for Majority Judgment",
|
||||
"Homepage": "Homepage",
|
||||
"Source code": "Source code",
|
||||
"Who are we?": "Who are we?",
|
||||
"Privacy policy": "Privacy policy",
|
||||
"resource.legalNotices": "Legal notices",
|
||||
"FAQ": "FAQ",
|
||||
"resource.help": "Need help?",
|
||||
"BetterVote": "BetterVote",
|
||||
"Voting platform": "Voting platform",
|
||||
"Majority Judgment": "Majority Judgment",
|
||||
"Start an election": "Start a vote",
|
||||
"Candidate/proposal ...": "Candidate/proposal...",
|
||||
"Delete?": "Confirm deletion",
|
||||
"Are you sure to delete": "Are you sure you want to delete",
|
||||
"the row": "the row",
|
||||
"resource.candidatePlaceholder": "Candidates or proposal's name",
|
||||
"resource.writeQuestionHere": "Write here your question or describe your vote (max. 250 characters)",
|
||||
"Enter the name of your candidate or proposal here (250 characters max.)": "Enter your proposal or the name of your candidate (max. 250 characters)",
|
||||
"Please add at least 2 candidates.": "Please add at least 2 candidates.",
|
||||
"resource.questionLabel": "Question of the vote",
|
||||
"resource.writeQuestion": "Write here your question or describe your vote",
|
||||
"resource.eg": "For example:",
|
||||
"resource.exampleQuestion": "For the role of my representative, I think this candidate is...",
|
||||
"Add a proposal": "Add a candidate/proposal",
|
||||
"resource.advancedOptions": "Advanced options",
|
||||
"Starting date": "Start date",
|
||||
"Ending date": "End date",
|
||||
"Defined period": "Defined period",
|
||||
"Unlimited": "Unlimited",
|
||||
"Voting time": "Voting time",
|
||||
"Grades": "Mentions",
|
||||
"You can select here the number of grades for your election": "Select here the number of mentions of your vote",
|
||||
"5 = Excellent, Very good, Good, Fair, Passable": "5 = Excellent, Very good, Good, Fair, Poor",
|
||||
"Participants": "Participants",
|
||||
"Add here participants' emails": "Add here participants' emails",
|
||||
"List voters' emails in case the election is not opened": "List participants' emails in case the election is not open",
|
||||
"Validate": "Confirm",
|
||||
"Submit my vote": "Submit my vote",
|
||||
"Confirm your vote": "Confirm your vote",
|
||||
"The form contains no address.": "The form contains no email addresses.",
|
||||
"The election will be opened to anyone with the link": "The vote will be open to anyone with the link",
|
||||
"resource.startVote": "Start the vote",
|
||||
"Cancel": "Cancel",
|
||||
"Confirm": "Confirm",
|
||||
"Successful election creation!": "Vote successfully created!",
|
||||
"You can now share the election link to participants:": "You can now share the voting link to participants:",
|
||||
"Copy": "Copy",
|
||||
"Here is the link for the results in real time:": "Here is the link for real-time results:",
|
||||
"Keep these links carefully": "Keep these links carefully",
|
||||
"resource.participateBtn": "Participate now!",
|
||||
"resource.resultsBtn": "Go to results",
|
||||
"t": "<0>Warning</0>: you will have no possibility to recover these links, and we will not be able to share them with you. For safekeeping, you can bookmark them in your browser.",
|
||||
"resource.start": "Start",
|
||||
"resource.noAds": "No advertising or ad cookies",
|
||||
"Oops! This election does not exist or it is not available anymore.": "Oops! This vote does not exist or is no longer available.",
|
||||
"You can start another election.": "You can start another vote.",
|
||||
"Go back to homepage": "Go back to homepage",
|
||||
"You have to judge every candidate/proposal!": "Please assess every candidate/proposal.",
|
||||
"resource.voteSuccess": "Your participation was successfully recorded!",
|
||||
"resource.thanks": "Thank your for your participation.",
|
||||
"Ending date:": "Ending date:",
|
||||
"Excellent": "Excellent",
|
||||
"Very good": "Very good",
|
||||
"Good": "Good",
|
||||
"Fair": "Fair",
|
||||
"Passable": "Poor",
|
||||
"Insufficient": "Insufficient",
|
||||
"To reject": "To be rejected",
|
||||
"Dates": "Dates",
|
||||
"The election will take place from": "The vote will take place from",
|
||||
"at": "at",
|
||||
"to": "to",
|
||||
"Voters' list": "List of participants' emails",
|
||||
"Graph": "Graph",
|
||||
"Preference profile": "Detailed results",
|
||||
"Results of the election:": "Results of the vote:",
|
||||
"Unknown error. Try again please.": "Unknown error. Please try again.",
|
||||
"If you list voters' emails, only them will be able to access the election": "If you list participants' emails, only they will be able to access the election",
|
||||
"Voters received a link to vote by email. Each link can be used only once!": "Participants have received a link to vote by email. Each link can be used only once.",
|
||||
"Oops... The election is unknown.": "Oops... The vote is unknown.",
|
||||
"The election is still going on. You can't access now to the results.": "The vote is on-going. You cannot access the results at this time.",
|
||||
"No votes have been recorded yet. Come back later.": "No votes have been recorded yet. Please check in later.",
|
||||
"The election has not started yet.": "The vote has not started yet.",
|
||||
"The election is over. You can't vote anymore": "The vote is over. You can no longer participate",
|
||||
"You need a token to vote in this election": "You need a valid token to participate in this vote",
|
||||
"You seem to have already voted.": "You seem to have already voted.",
|
||||
"The parameters of the election are incorrect.": "The parameters of the vote are incorrect.",
|
||||
"Support us !": "Support us!",
|
||||
"PayPal - The safer, easier way to pay online!": "PayPal - The safer, easier way to pay online!",
|
||||
"resource.numVotes": "Number of votes:",
|
||||
"Access to results": "Results availability",
|
||||
"Immediately": "Immediately",
|
||||
"At the end of the election": "At the end of the vote",
|
||||
"Results available at the close of the vote": "Results available at the close of the vote",
|
||||
"The results page will not be accessible until all participants have voted.": "The results page will not be accessible until all participants have voted.",
|
||||
"The results page will not be accessible until the end date is reached.": "The results page will not be accessible until the end date is reached.",
|
||||
"No one will be able to see the result until the end date is reached or until all participants have voted.": "No one will be able to see the results until the end date is reached or until all participants have voted.",
|
||||
"Send me this link": "Send me this link",
|
||||
"Send me these links": "Send me these links",
|
||||
"Open": "Open",
|
||||
"Voting address": "Link to the vote",
|
||||
"Results address": "Link to the results",
|
||||
"Share election on Facebook": "Share vote on Facebook",
|
||||
"Share results on Facebook": "Share results on Facebook"
|
||||
}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1,108 @@
|
||||
{
|
||||
"title": "Plataforma de Juicio Mayoritario",
|
||||
"Homepage": "Página de inicio",
|
||||
"Source code": "Código fuente",
|
||||
"Who are we": "Quiénes somos",
|
||||
"Privacy policy": "Política de privacidad",
|
||||
"resource.legalNotices": "Avisos legales",
|
||||
"FAQ": "FAQ",
|
||||
"resource.help": "¿Necesitas ayuda?",
|
||||
"BetterVote": "VotarMejor",
|
||||
"Voting platform": "Plataforma de votación",
|
||||
"Majority Judgment": "Juicio Mayoritario",
|
||||
"Start an election": "Iniciar una elección",
|
||||
"resource.candidatePlaceholder": "Nombre del(la) candidato(a)/propuesta...",
|
||||
"Delete?": "Borrar?",
|
||||
"Are you sure to delete": "Estás seguro de querer borrar",
|
||||
"the row": "la fila",
|
||||
"Write here your question or introduce simple your election (250 characters max.)": "Escriba aquí su pregunta o introduzca simplemente su elección (250 caracteres máx.)",
|
||||
"Enter the name of your candidate or proposal here (250 characters max.)": "Escriba aquí el nombre de su candidato o propuesta (250 caracteres como máximo)",
|
||||
"Please add at least 2 candidates.": "Por favor, añada al menos dos canidatos(as).",
|
||||
"Question of the election": "Pregunta de su elección",
|
||||
"Write here the question of your election": "Escriba aquí la pregunta de su elección",
|
||||
"For example:": "Por ejemplo:",
|
||||
"For the role of my representative, I judge this candidate...": "Para ser mi representante, yo elijo a este(a) candidato(a)....",
|
||||
"Candidates/Proposals": "Candidatos(as)/Propuestas",
|
||||
"Add a proposal": "Añadir una propuesta",
|
||||
"Advanced options": "Opciones avanzadas",
|
||||
"Starting date": "Fecha de inicio",
|
||||
"Ending date": "Fecha de finalización",
|
||||
"Defined period" : "Período definido",
|
||||
"Unlimited" : "Ilimitado",
|
||||
"Voting time" : "Hora de la votación",
|
||||
"Grades": "Escala",
|
||||
"You can select here the number of grades for your election": "Puede seleccionar aquí el número de niveles de la escala para su elección",
|
||||
"5 = Excellent, Very good, Good, Fair, Passable": "5 == Excelente, Muy bien, Bien, Regular, Pasable",
|
||||
"Participants:": "Participantes",
|
||||
"Add here participants' emails": "Añadir aquí los correos electrónicos de los(as) participantes",
|
||||
"List voters' emails in case the election is not opened": "Enumere los correos electrónicos de los(as) votantes en caso de que la elección no se abra",
|
||||
"Validate": "Validar",
|
||||
"Submit my vote": "Validar",
|
||||
"Confirm your vote": "Confirme su voto",
|
||||
"The form contains no address.": "El formulario no contiene ningún correo electrónico",
|
||||
"The election will be opened to anyone with the link": "La elección se abrirá a cualquiera que tenga el enlace",
|
||||
"Start the election": "Iniciar la elección",
|
||||
"Cancel": "Cancelar",
|
||||
"Confirm": "Confirmar",
|
||||
"Successful election creation!": "La elección ha sido creada con éxito!",
|
||||
"You can now share the election link to participants:": "Ahora puede compartir el enlace de la elección con los(as) participantes",
|
||||
"Copy": "Copiar",
|
||||
"Here is the link for the results in real time:": "En este enlace puedes revisar los resultados en tiempo real",
|
||||
"Keep these links carefully": "Guarda cuidadosamente estos enlaces",
|
||||
"resource.participateBtn": "¡Participa ahora!",
|
||||
"t": "<0>Advertencia</0>: No tendrás otras opciones para recuperar los enlaces, y no podremos compartirlos contigo. Por ejemplo, puedes agregarlos a favoritos de tu buscador.",
|
||||
"Simple and free: organize an election with Majority Judgment.": "Simple y gratuito: organiza una elección con Juicio Mayoritario",
|
||||
"Start": "Comenzar",
|
||||
"No advertising or ad cookies": "No contiene publicidad ni cookies publicitarias",
|
||||
"Oops! This election does not exist or it is not available anymore.": "¡Uy! Esta elección no existe o ya no está disponible",
|
||||
"You can start another election.": "Puedes empezar otra elección",
|
||||
"Go back to homepage": "Vuelve a la página de inicio",
|
||||
"You have to judge every candidate/proposal!": "¡Tienes que evaluar a todos(as) los(as) candidatos(as)/propuestas",
|
||||
"resource.voteSuccess": "¡Su participación fue registrada con éxito!",
|
||||
"resource.thanks": "Muchas gracias por participar",
|
||||
"Ending date:": "Fecha de finalización:",
|
||||
"Excellent": "Excelente",
|
||||
"Very good": "Muy bien",
|
||||
"Good": "Bien",
|
||||
"Fair": "Regular",
|
||||
"Passable": "Pasable",
|
||||
"Insufficient": "Insuficiente",
|
||||
"To reject": "Rechazar",
|
||||
"Dates": "Fechas",
|
||||
"The election will take place from": "La elección tendrá lugar desde",
|
||||
"at": "a las",
|
||||
"to": "hasta",
|
||||
"Voters' list": "Lista de votantes",
|
||||
"Graph": "Gráfico",
|
||||
"Preference profile": "Perfil de preferencia",
|
||||
"Results of the election:": "Resultados de la elección",
|
||||
"PayPal - The safer, easier way to pay online!": "PayPal la forma más segura y fácil de pagar en linea!",
|
||||
"Support us !": "¡apórtanos!",
|
||||
"Who are we?": "¿Quiénes somos?",
|
||||
"Unknown error. Try again please.": "Error desconocido. Inténtelo de nuevo, por favor.",
|
||||
"If you list voters' emails, only them will be able to access the election": "Si enumera los correos electrónicos de los votantes, sólo ellos podrán acceder a la elección",
|
||||
"Voters received a link to vote by email. Each link can be used only once!": "Los votantes recibieron un enlace para votar por correo electrónico. ¡Cada enlace puede ser usado sólo una vez!",
|
||||
"resource.numVotes": "Número de votos:",
|
||||
"Oops... The election is unknown.": "Oops... La elección es desconocida",
|
||||
"The election is still going on. You can't access now to the results.": "La elección sigue en marcha. No puedes acceder ahora a los resultados.",
|
||||
"No votes have been recorded yet. Come back later.": "Aún no se han registrado votos. Vuelva más tarde.",
|
||||
"The election has not started yet.": "Las elecciones aún no han comenzado.",
|
||||
"The election is over. You can't vote anymore": "La elección ha terminado. Ya no puedes votar.",
|
||||
"You need a token to vote in this election": "Necesitas una ficha para votar en esta elección",
|
||||
"You seem to have already voted.": "Parece que ya has votado.",
|
||||
"The parameters of the election are incorrect.": "Los parámetros de la elección son incorrectos.",
|
||||
"Access to results" : "Acceso a los resultados",
|
||||
"Immediately": "Inmediatamente",
|
||||
"At the end of the election": "Al final de la elección",
|
||||
"Results available at the close of the vote": "Resultados disponibles al cierre de la votación",
|
||||
"The results page will not be accessible until all participants have voted.":"La página de resultados no será accesible hasta que todos los participantes hayan votado.",
|
||||
"The results page will not be accessible until the end date is reached.": "No se podrá acceder a la página de resultados hasta que se alcance la fecha de finalización.",
|
||||
"No one will be able to see the result until the end date is reached or until all participants have voted.": "Nadie podrá ver el resultado hasta que se alcance la fecha final o hasta que todos los participantes hayan votado.",
|
||||
"Send me this link" : "Envíame este enlace",
|
||||
"Send me these links" : "Envíame estos enlaces",
|
||||
"Open" : "Abrir",
|
||||
"Voting address" : "URL de la votación",
|
||||
"Results address" : "URL de los resultados",
|
||||
"Share election on Facebook" : "Compartir la elección en Facebook",
|
||||
"Share results on Facebook" : "Comparte los resultados en Facebook"
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"common.vote": "Votez !",
|
||||
"common.mieuxvoter": "Mieux Voter",
|
||||
"common.helpus": "Vous souhaitez nous soutenir ?",
|
||||
"common.candidates": "Candidats/Propositions",
|
||||
"common.valueProp": "Simple et gratuit : organisez un vote avec le Jugement Majoritaire",
|
||||
"common.backHomepage": "Retour à la page d'accueil"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"email.hello": "Bonjour ! 🙂",
|
||||
"email.why": "Vous avez été invité·e à participer à l'élection suivante : ",
|
||||
"email.linkVote": "Le lien pour voter est le suivant :",
|
||||
"email.linkResult": "A la fin de l'élection, vous pourrez accéder aux résultats en cliquant sur ce lien :",
|
||||
"email.happy": "Nous sommes très heureux de vous partager ce lien de vote ! Vous allez pouvoir voter avec le jugement majoritaire.",
|
||||
"email.copyLink": "Si le lien ne fonctionne pas, vous pouvez le copier et le coller dans la barre de navigation de votre navigateur.",
|
||||
"email.bye": "Bon vote ! 🤗",
|
||||
"email.aboutjm": "If you require any further information, please visit our site."
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"error.e1": "Impossible de retrouver le vote en question",
|
||||
"error.e2": "L'élection est encore en cours. Revenez plus tard.",
|
||||
"error.e3": "Aucun vote n'a encore été enregistré. Revenez plus tard.",
|
||||
"error.e4": "L'élection n'a pas encore démarrée.",
|
||||
"error.e5": "L'élection est terminée. Vous ne pouvez plus voter.",
|
||||
"error.e6": "Vous avez besoin d'un jeton pour participer à cette élection.",
|
||||
"error.e7": "Vous avez déjà voté pour cette élection.",
|
||||
"error.e8": "Les paramètres de l'élection sont inconnues.",
|
||||
"error.catch22": "Erreur inconnue."
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
{
|
||||
"Homepage": "Accueil",
|
||||
"Source code": "Code source",
|
||||
"Who are we": "Qui sommes-nous",
|
||||
"BetterVote": "MieuxVoter",
|
||||
"Voting platform": "Plateforme de vote",
|
||||
"Majority Judgment": "Jugement Majoritaire",
|
||||
"Start an election": "Lancer une élection",
|
||||
"resource.candidatePlaceholder": "Name du candidat/proposition",
|
||||
"Delete?": "Supprimer ?",
|
||||
"Are you sure to delete": "Êtes-vous sûr(e) de supprimer",
|
||||
"the row": "la ligne",
|
||||
"Write here your question or introduce simple your election (250 characters max.)": "Décrire ici votre question ou introduire simplement votre élection (250 caractères max.)",
|
||||
"Please add at least 2 candidates.": "Merci d'ajouter au moins 2 candidats.",
|
||||
"Question of the election": "Question de votre élection",
|
||||
"Write here the question of your election": "Ecrire ici la question de votre élection",
|
||||
"For example:": "Par exemple",
|
||||
"For the role of my representative, I judge this candidate...": "Pour être mon représentant, je juge ce candidat...",
|
||||
"Candidates/Proposals": "Candidats/Propositions",
|
||||
"Add a proposal": "Ajouter une proposition",
|
||||
"Advanced options": "Options avancées",
|
||||
"Starting date:": "Date de début :",
|
||||
"Ending date: ": "Date de fin : ",
|
||||
"Grades:": "Mentions",
|
||||
"You can select here the number of grades for your election": "You pouvez choisir ici le nombre de mentions de votre élection",
|
||||
"5 = Excellent, Very good, Good, Fair, Passable": "5 = Excellent, Très bien, Bien, Assez bien, Passable",
|
||||
"Participants:": "Participants :",
|
||||
"Add here participants' emails": "Ajouter ici les emails des participants",
|
||||
"List voters' emails in case the election is not opened": "Lister ici les emails des électeurs dans le cas où l'élection n'est pas ouverte.",
|
||||
"Validate": "Valider",
|
||||
"Confirm your vote": "Confirmer votre vote",
|
||||
"The form contains no address.": "Aucune adresse email n'a été ajoutée.",
|
||||
"The election will be opened to anyone with the link": "L'élection sera accessible à tous ceux qui disposent de ce lien",
|
||||
"Start the election": "Démarrer l'élection",
|
||||
"Cancel": "Annuler",
|
||||
"Confirm": "Valider",
|
||||
"Successful election creation!": "L'élection a été créée avec succès !",
|
||||
"You can now share the election link to participants:": "Vous pouvez maintenant partager ce lien à tous les participants",
|
||||
"Copy": "Copier",
|
||||
"Here is the link for the results in real time:": "Voici le lien pour afficher les résultats en temps réel :",
|
||||
"Keep these links carefully": "Gardez ces liens précieusement",
|
||||
"resource.participateBtn": "Participez maintenant !",
|
||||
"t": "<0>Attention</0> : vous n'aurez pas d'autres moyens pour récupérer ces liens par la suite, et nous ne serons pas capables de les partager avec vous. Vous pouvez, par exemple, ajouter ces liens à vos favoris dans votre navigateur.",
|
||||
"Simple and free: organize an election with Majority Judgment.": "Simple et grauit: organiser une élection avec le Jugement Majoritaire.",
|
||||
"Start": "Démarrer",
|
||||
"No advertising or ad cookies": "Pas de publictés, ni de cookies publicitaires",
|
||||
"Oops! This election does not exist or it is not available anymore.": "Oups ! L'élection n'existe pas ou n'est plus disponible.",
|
||||
"You can start another election.": "Vous pouvez démarrer une autre élection.",
|
||||
"Go back to homepage": "Revenir à la page d'accueil",
|
||||
"You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !",
|
||||
"resource.voteSuccess": "Votre participation a été enregistrée avec succès !",
|
||||
"resource.thanks": "Merci de votre participation."
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
{
|
||||
"title": "Application au Jugement Majoritaire",
|
||||
"Homepage": "Accueil",
|
||||
"Source code": "Code source",
|
||||
"Who are we?": "Qui sommes-nous ?",
|
||||
"Privacy policy": "Politique de confidentialité",
|
||||
"resource.legalNotices": "Mentions légales",
|
||||
"FAQ": "FAQ",
|
||||
"resource.help": "Besoin d'aide ?",
|
||||
"BetterVote": "MieuxVoter",
|
||||
"Voting platform": "Plateforme de vote",
|
||||
"Majority Judgment": "Jugement Majoritaire",
|
||||
"Start an election": "Lancer un vote",
|
||||
"resource.candidatePlaceholder": "Nom du candidat/proposition",
|
||||
"Delete?": "Supprimer ?",
|
||||
"Are you sure to delete": "Êtes-vous sûr(e) de supprimer",
|
||||
"the row": "la ligne",
|
||||
"resource.writeQuestionHere": "Décrire ici votre question ou introduire simplement votre vote (250 caractères max.)",
|
||||
"Enter the name of your candidate or proposal here (250 characters max.)": "Saisissez ici le nom de votre candidat ou de votre proposition (250 caractères max.)",
|
||||
"Please add at least 2 candidates.": "Merci d'ajouter au moins 2 candidats.",
|
||||
"resource.questionLabel": "Question de votre vote",
|
||||
"resource.writeQuestion": "Ecrire ici la question de votre vote",
|
||||
"resource.eg": "Par exemple",
|
||||
"resource.exampleQuestion": "Pour être mon représentant, je juge ce candidat...",
|
||||
"Add a proposal": "Ajouter une proposition",
|
||||
"resource.advancedOptions": "Options avancées",
|
||||
"Starting date": "Date de début",
|
||||
"Ending date": "Date de fin ",
|
||||
"Defined period": "Période définie",
|
||||
"Unlimited": "Illimitée",
|
||||
"Voting time": "Durée du vote",
|
||||
"Grades": "Mentions",
|
||||
"You can select here the number of grades for your election": "You pouvez choisir ici le nombre de mentions de votre vote",
|
||||
"5 = Excellent, Very good, Good, Fair, Passable": "5 = Excellent, Très bien, Bien, Assez bien, Passable",
|
||||
"Participants": "Participants",
|
||||
"Add here participants' emails": "Ajouter ici les emails des participants",
|
||||
"List voters' emails in case the election is not opened": "Lister ici les emails des électeurs dans le cas où le vote n'est pas ouverte.",
|
||||
"Validate": "Valider",
|
||||
"Submit my vote": "Enregistrer mon vote",
|
||||
"Confirm your vote": "Confirmer votre vote",
|
||||
"The form contains no address.": "Aucune adresse email n'a été ajoutée.",
|
||||
"The election will be opened to anyone with the link": "Le vote sera accessible à tous ceux qui disposent du lien",
|
||||
"resource.startVote": "Démarrer le vote",
|
||||
"Cancel": "Annuler",
|
||||
"Confirm": "Valider",
|
||||
"Successful election creation!": "Le vote a été créé avec succès !",
|
||||
"You can now share the election link to participants:": "Vous pouvez maintenant partager ce lien à tous les participants",
|
||||
"Copy": "Copier",
|
||||
"Here is the link for the results in real time:": "Voici le lien pour afficher les résultats en temps réel :",
|
||||
"Keep these links carefully": "Gardez ces liens précieusement",
|
||||
"resource.participateBtn": "Participez maintenant !",
|
||||
"resource.resultsBtn": "Résultats",
|
||||
"t": "<0>Attention</0> : vous n'aurez pas d'autres moyens pour récupérer ces liens par la suite, et nous ne serons pas capables de les partager avec vous. Vous pouvez, par exemple, ajouter ces liens à vos favoris dans votre navigateur.",
|
||||
"resource.start": "Démarrer",
|
||||
"resource.noAds": "Pas de publicités, ni de cookies publicitaires",
|
||||
"Oops! This election does not exist or it is not available anymore.": "Oups ! Le vote n'existe pas ou n'est plus disponible.",
|
||||
"You can start another election.": "Vous pouvez démarrer une autre vote.",
|
||||
"Go back to homepage": "Revenir à la page d'accueil",
|
||||
"You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !",
|
||||
"resource.voteSuccess": "Votre participation a été enregistrée avec succès !",
|
||||
"resource.thanks": "Merci de votre participation.",
|
||||
"Excellent": "Excellent",
|
||||
"Very good": "Très bien",
|
||||
"Good": "Bien",
|
||||
"Fair": "Assez bien",
|
||||
"Passable": "Passable",
|
||||
"Insufficient": "Insuffisant",
|
||||
"To reject": "A rejeter",
|
||||
"Dates": "Dates",
|
||||
"The election will take place from": "Le vote se déroulera du",
|
||||
"at": "à",
|
||||
"to": "au",
|
||||
"Voters' list": "Listes des électeurs",
|
||||
"Graph": "Graphique",
|
||||
"Preference profile": "Profil de mérites",
|
||||
"Results of the election:": "Résultats du vote",
|
||||
"Unknown error. Try again please.": "Erreur inconnue. Merci de ré-essayer plus tard.",
|
||||
"If you list voters' emails, only them will be able to access the election": "Si vous ajoutez des emails, seulement ceux-là seront capables d'accéder au vote",
|
||||
"Voters received a link to vote by email. Each link can be used only once!": "Les électeurs ont reçu un lien par courriel pour voter. Chaque lien ne peut être utilisé qu'une seule fois.",
|
||||
"Oops... The election is unknown.": "Oups... Le serveur ne retrouve pas le vote.",
|
||||
"The election is still going on. You can't access now to the results.": "le vote est encore en cours. Vous ne pouvez pas encore accéder aux résultats.",
|
||||
"No votes have been recorded yet. Come back later.": "Aucun vote n'a été enregistré. Merci de revenir plus tard.",
|
||||
"The election has not started yet.": "le vote n'a pas encore commencé.",
|
||||
"The election is over. You can't vote anymore": "le vote est terminée. Vous ne pouvez plus voter.",
|
||||
"You need a token to vote in this election": "Vous avez besoin d'un jeton pour participer à ce vote",
|
||||
"You seem to have already voted.": "Il semble que vous ayez déjà voté.",
|
||||
"The parameters of the election are incorrect.": "Les paramètres de vote sont incorrects.",
|
||||
"Support us !": "Soutenez-nous !",
|
||||
"PayPal - The safer, easier way to pay online!": "PayPal - Le moyen le plus sûr et le plus simple de payer en ligne !",
|
||||
"resource.numVotes": "Nombre de votes :",
|
||||
"Access to results": "Accès aux résultats",
|
||||
"Immediately": "Immédiatement",
|
||||
"At the end of the election": "A la clôture du vote",
|
||||
"Results available at the close of the vote": "Résultats disponibles à la clôture du vote",
|
||||
"The results page will not be accessible until all participants have voted.": "La page de résultats ne sera pas accessible tant que tous les participants n'auront pas voté.",
|
||||
"The results page will not be accessible until the end date is reached.": "La page de résultats ne sera pas accessible tant que la date de fin ne sera pas atteinte.",
|
||||
"No one will be able to see the result until the end date is reached or until all participants have voted.": "Personne ne pourra voir le résultat tant que la date de fin n'est pas atteinte ou que tous les participants n'ont pas voté.",
|
||||
"Send me this link": "Envoyez-moi ce lien",
|
||||
"Send me these links": "Envoyez-moi ces liens",
|
||||
"Open": "Ouvrir",
|
||||
"Voting address": "Adresse du vote",
|
||||
"Results address": "Adresse des résultats",
|
||||
"Share election on Facebook": "Partager le vote sur Facebook",
|
||||
"Share results on Facebook": "Partager ces résultats sur Facebook"
|
||||
}
|
@ -0,0 +1 @@
|
||||
{}
|
@ -0,0 +1 @@
|
||||
{}
|
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 403 B |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,191 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const paths = require('../config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild =
|
||||
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory('production');
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||
})
|
||||
.then(previousFileSizes => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// Merge with the public folder
|
||||
copyPublicFolder();
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
'\nSearch for the ' +
|
||||
chalk.underline(chalk.yellow('keywords')) +
|
||||
' to learn more about each warning.'
|
||||
);
|
||||
console.log(
|
||||
'To ignore, add ' +
|
||||
chalk.cyan('// eslint-disable-next-line') +
|
||||
' to the line before.\n'
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrl;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
useYarn
|
||||
);
|
||||
},
|
||||
err => {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
// We used to support resolving modules according to `NODE_PATH`.
|
||||
// This now has been deprecated in favor of jsconfig/tsconfig.json
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
if (err) {
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
messages = formatWebpackMessages({
|
||||
errors: [err.message],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true })
|
||||
);
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
|
||||
return resolve({
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml,
|
||||
});
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const clearConsole = require('react-dev-utils/clearConsole');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls,
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const paths = require('../config/paths');
|
||||
const configFactory = require('../config/webpack.config');
|
||||
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||
|
||||
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
return choosePort(HOST, DEFAULT_PORT);
|
||||
})
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
const config = configFactory('development');
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
const urls = prepareUrls(protocol, HOST, port);
|
||||
const devSocket = {
|
||||
warnings: warnings =>
|
||||
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
|
||||
errors: errors =>
|
||||
devServer.sockWrite(devServer.sockets, 'errors', errors),
|
||||
};
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler({
|
||||
appName,
|
||||
config,
|
||||
devSocket,
|
||||
urls,
|
||||
useYarn,
|
||||
useTypeScript,
|
||||
webpack,
|
||||
});
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.listen(port, HOST, err => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
// We used to support resolving modules according to `NODE_PATH`.
|
||||
// This now has been deprecated in favor of jsconfig/tsconfig.json
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Starting the development server...\n'));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(function(sig) {
|
||||
process.on(sig, function() {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PUBLIC_URL = '';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../config/env');
|
||||
|
||||
|
||||
const jest = require('jest');
|
||||
const execSync = require('child_process').execSync;
|
||||
let argv = process.argv.slice(2);
|
||||
|
||||
function isInGitRepository() {
|
||||
try {
|
||||
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isInMercurialRepository() {
|
||||
try {
|
||||
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch unless on CI or explicitly running all tests
|
||||
if (
|
||||
!process.env.CI &&
|
||||
argv.indexOf('--watchAll') === -1
|
||||
) {
|
||||
// https://github.com/facebook/create-react-app/issues/5210
|
||||
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||
}
|
||||
|
||||
|
||||
jest.run(argv);
|
@ -0,0 +1,228 @@
|
||||
const api = {
|
||||
urlServer:
|
||||
process.env.NEXT_PUBLIC_SERVER_URL || "https://demo.mieuxvoter.fr/api/",
|
||||
feedbackForm:
|
||||
process.env.NEXT_PUBLIC_FEEDBACK_FORM ||
|
||||
"https://docs.google.com/forms/d/e/1FAIpQLScuTsYeBXOSJAGSE_AFraFV7T2arEYua7UCM4NRBSCQQfRB6A/viewform",
|
||||
routesServer: {
|
||||
setElection: "election/",
|
||||
getElection: "election/get/:slug/",
|
||||
getResults: "election/results/:slug",
|
||||
voteElection: "election/vote/",
|
||||
},
|
||||
};
|
||||
|
||||
const sendInviteMail = (res) => {
|
||||
/**
|
||||
* Send an invitation mail using a micro-service with Netlify
|
||||
*/
|
||||
const { id, title, mails, tokens, locale } = res;
|
||||
|
||||
if (!mails || !mails.length) {
|
||||
throw new Error("No emails are provided.");
|
||||
}
|
||||
|
||||
if (mails.length !== tokens.length) {
|
||||
throw new Error("The number of emails differ from the number of tokens");
|
||||
}
|
||||
|
||||
const origin =
|
||||
typeof window !== "undefined" && window.location.origin
|
||||
? window.location.origin
|
||||
: "http://localhost";
|
||||
const urlVote = (pid, token) => new URL(`/vote/${pid}/${token}`, origin);
|
||||
const urlResult = (pid) => new URL(`/result/${pid}`, origin);
|
||||
|
||||
const recipientVariables = {};
|
||||
tokens.forEach((token, index) => {
|
||||
recipientVariables[mails[index]] = {
|
||||
urlVote: urlVote(id, token),
|
||||
urlResult: urlResult(id),
|
||||
};
|
||||
});
|
||||
|
||||
const req = fetch("/.netlify/functions/send-invite-email/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recipientVariables,
|
||||
title,
|
||||
locale,
|
||||
}),
|
||||
});
|
||||
|
||||
return req.then((any) => res);
|
||||
};
|
||||
|
||||
const createElection = (
|
||||
title,
|
||||
candidates,
|
||||
{
|
||||
/**
|
||||
* Create an election from its title, its candidates and a bunch of options
|
||||
*/
|
||||
mails,
|
||||
numGrades,
|
||||
start,
|
||||
finish,
|
||||
restrictResult,
|
||||
locale,
|
||||
},
|
||||
successCallback,
|
||||
failureCallback
|
||||
) => {
|
||||
const endpoint = new URL(api.routesServer.setElection, api.urlServer);
|
||||
|
||||
console.log(endpoint.href);
|
||||
const onInvitationOnly = mails && mails.length > 0;
|
||||
|
||||
fetch(endpoint.href, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
candidates,
|
||||
on_invitation_only: onInvitationOnly,
|
||||
num_grades: numGrades,
|
||||
elector_emails: mails || [],
|
||||
start_at: start,
|
||||
finish_at: finish,
|
||||
select_language: locale || "en",
|
||||
front_url: window.location.origin,
|
||||
restrict_results: restrictResult,
|
||||
send_mail: false,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((res) => {
|
||||
if (onInvitationOnly) {
|
||||
return sendInviteMail({ locale, mails: mails, ...res });
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then(successCallback)
|
||||
.catch(failureCallback || console.log);
|
||||
};
|
||||
|
||||
const getResults = (pid, successCallback, failureCallback) => {
|
||||
/**
|
||||
* Fetch results from external API
|
||||
*/
|
||||
|
||||
const endpoint = new URL(
|
||||
api.routesServer.getResults.replace(new RegExp(":slug", "g"), pid),
|
||||
api.urlServer
|
||||
);
|
||||
|
||||
return fetch(endpoint.href)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.text());
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(successCallback || ((res) => res))
|
||||
.catch(failureCallback || ((err) => err));
|
||||
};
|
||||
|
||||
const getDetails = (pid, successCallback, failureCallback) => {
|
||||
/**
|
||||
* Fetch data from external API
|
||||
*/
|
||||
|
||||
const detailsEndpoint = new URL(
|
||||
api.routesServer.getElection.replace(new RegExp(":slug", "g"), pid),
|
||||
api.urlServer
|
||||
);
|
||||
return fetch(detailsEndpoint.href)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return Promise.reject(response.text());
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(successCallback || ((res) => res))
|
||||
.catch(failureCallback || ((err) => err))
|
||||
.then((res) => res);
|
||||
};
|
||||
|
||||
const castBallot = (judgments, pid, token, callbackSuccess, callbackError) => {
|
||||
/**
|
||||
* Save a ballot on the remote database
|
||||
*/
|
||||
|
||||
const endpoint = new URL(api.routesServer.voteElection, api.urlServer);
|
||||
|
||||
const payload = {
|
||||
election: pid,
|
||||
grades_by_candidate: judgments,
|
||||
};
|
||||
if (token && token !== "") {
|
||||
payload["token"] = token;
|
||||
}
|
||||
|
||||
fetch(endpoint.href, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(callbackSuccess || ((res) => res))
|
||||
.catch(callbackError || console.log);
|
||||
};
|
||||
|
||||
export const UNKNOWN_ELECTION_ERROR = "E1:";
|
||||
export const ONGOING_ELECTION_ERROR = "E2:";
|
||||
export const NO_VOTE_ERROR = "E3:";
|
||||
export const ELECTION_NOT_STARTED_ERROR = "E4:";
|
||||
export const ELECTION_FINISHED_ERROR = "E5:";
|
||||
export const INVITATION_ONLY_ERROR = "E6:";
|
||||
export const UNKNOWN_TOKEN_ERROR = "E7:";
|
||||
export const USED_TOKEN_ERROR = "E8:";
|
||||
export const WRONG_ELECTION_ERROR = "E9:";
|
||||
|
||||
export const apiErrors = (error, t) => {
|
||||
if (error.includes(UNKNOWN_ELECTION_ERROR)) {
|
||||
return t("error.e1");
|
||||
}
|
||||
if (error.includes(ONGOING_ELECTION_ERROR)) {
|
||||
return t("error.e2");
|
||||
}
|
||||
if (error.includes(NO_VOTE_ERROR)) {
|
||||
return t("error.e3");
|
||||
}
|
||||
if (error.includes(ELECTION_NOT_STARTED_ERROR)) {
|
||||
return t("error.e4");
|
||||
}
|
||||
if (error.includes(ELECTION_FINISHED_ERROR)) {
|
||||
return t("error.e5");
|
||||
}
|
||||
if (error.includes(INVITATION_ONLY_ERROR)) {
|
||||
return t("error.e6");
|
||||
}
|
||||
if (error.includes(USED_TOKEN_ERROR)) {
|
||||
return t("error.e7");
|
||||
}
|
||||
if (error.includes(WRONG_ELECTION_ERROR)) {
|
||||
return t("error.e8");
|
||||
} else {
|
||||
return t("error.catch22");
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
api,
|
||||
getDetails,
|
||||
getResults,
|
||||
createElection,
|
||||
sendInviteMail,
|
||||
castBallot,
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import {createContext, useContext} from 'react';
|
||||
|
||||
const AppContext = createContext();
|
||||
|
||||
export function AppProvider({children}) {
|
||||
let state = {}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={state}>
|
||||
{ children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function useAppContext() {
|
||||
return useContext(AppContext);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
const colors = [
|
||||
"#015411",
|
||||
"#019812",
|
||||
"#6bca24",
|
||||
"#ffb200",
|
||||
"#ff5d00",
|
||||
"#b20616",
|
||||
"#6f0214"
|
||||
];
|
||||
|
||||
const gradeNames = [
|
||||
"Excellent",
|
||||
"Very good",
|
||||
"Good",
|
||||
"Fair",
|
||||
"Passable",
|
||||
"Insufficient",
|
||||
"To reject"
|
||||
];
|
||||
|
||||
const gradeValues = [6, 5, 4, 3, 2, 1, 0];
|
||||
|
||||
export const translateGrades = (t) => {
|
||||
return gradeNames.map((name, i) => ({
|
||||
label: t(name),
|
||||
color: colors[i],
|
||||
value: gradeValues[i]
|
||||
}));
|
||||
};
|