switch to Nextjs

pull/73/head
Pierre-Louis Guhur 3 years ago
parent 2fba1f7405
commit 867042e791

22
.gitignore vendored

@ -1,7 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.tern-port
.#*
*.env.local
# dependencies # dependencies
/node_modules /node_modules
@ -11,17 +8,28 @@
# testing # testing
/coverage /coverage
# next.js
/.next/
/out/
# production # production
/build /build
# misc # misc
/.idea
.DS_Store .DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
npm-debug.log* # Local Netlify folder
yarn-debug.log* .netlify
yarn-error.log* functions/next_*

@ -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

@ -1,34 +1,61 @@
[![Continuous Integration](https://circleci.com/gh/MieuxVoter/mvfront-react.svg?style=svg)](https://circleci.com/gh/MieuxVoter/mvfront-react) # Front-end election web application using NextJs
[![Continuous Deployment](https://api.netlify.com/api/v1/badges/021c39c6-1018-4e3f-98e2-f808b4ea8f6d/deploy-status)](https://app.netlify.com/sites/epic-nightingale-99f910/deploys)
# Voting application in React
A demo is available at [our website](http://demo.mieuxvoter.fr/). [![Netlify Status](https://api.netlify.com/api/v1/badges/e5e19870-2d67-4082-973e-593f58c56f87/deploy-status)](https://app.netlify.com/sites/mv-front-react/deploys)
## Installation
1. Copy `.env` file into `.env.local` and set there environment variables. :ballot_box: This project is going to be the default front-end for our [election application](https://app.mieuxvoter.fr).
2. Install [yarn](https://classic.yarnpkg.com/en/docs/install/#debian-stable).
3. Install dependencies:
```bash
$ cd mvfront-react
$ yarn install
```
## Translation :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.
We are welcoming translations of the application in any language. :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).
To add a new language, copy a [language folder](./public/locale/i18n/en/) into a new folder with your language as a name.
Then, replace values in the JSON files.
To compile them, launch: `$ yarn translate`. :world_map: The front-end stores its own translations. See below how you can edit them easily.
## Starting
In development, you might want to copy `.env` into `.env.local` and set the environment variables. Then launch `$ yarn start` ## :paintbrush: Customize your own application
For production, see our [CI/CD configuration](https://github.com/MieuxVoter/continuous-integration). The separation between the front-end and the back-end makes it easy to customize your own application. Just install
## Testing
Launch `$ yarn test`
## :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-nextjs&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,57 @@
/* eslint react/prop-types: 0 */
import React from "react";
import { Button } from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const CopyField = props => {
const ref = React.createRef();
const handleClickOnField = event => {
event.target.focus();
event.target.select();
};
const handleClickOnButton = () => {
const input = ref.current;
input.focus();
input.select();
document.execCommand("copy");
};
const { t, value, iconCopy } = props;
return (
<div className="input-group ">
<input
type="text"
className="form-control"
ref={ref}
value={value}
readOnly
onClick={handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-secondary"
onClick={handleClickOnButton}
type="button"
>
<FontAwesomeIcon icon={iconCopy} className="mr-2" />
{t("Copy")}
</Button>
</div>
{/*<div className="input-group-append">
<a
className="btn btn-secondary"
href={value}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={iconOpen} className="mr-2" />
{t("Open")}
</a>
</div>*/}
</div>
);
};
export default CopyField;

@ -0,0 +1,35 @@
import Link from 'next/link'
import {Container, Row, Col} from "reactstrap";
import {useTranslation} from "next-i18next";
const Error = props => {
const {t} = useTranslation();
return (
<Container>
<Row>
<Link href="/">
<a className="d-block ml-auto mr-auto mb-4">
<img src="/logos/logo-line-white.svg" alt="logo" height="128" />
</a>
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h4>{props.value}</h4>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<Link href="/">
<a className="btn btn-secondary">
{ t("common.backHomepage") }
</a>
</Link>
</Col>
</Row>
</Container>
);
}
export default Error

@ -0,0 +1,29 @@
/* eslint react/prop-types: 0 */
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFacebookSquare } from "@fortawesome/free-brands-svg-icons";
const Facebook = props => {
const handleClick = () => {
const url =
"https://www.facebook.com/sharer.php?u=" +
props.url +
"&t=" +
props.title;
window.open(
url,
"",
"menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=500,width=700"
);
};
return (
<button className={props.className} onClick={handleClick} type="button">
<FontAwesomeIcon icon={faFacebookSquare} className="mr-2" />
{props.text}
</button>
);
};
export default Facebook;
//i

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCommentAlt} from "@fortawesome/free-solid-svg-icons";
import {api} from "@services/api"
const Gform = (props) => {
return (
<a
className={props.className}
href={api.feedbackForm}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={faCommentAlt} className="mr-2" />
Votre avis nous intéresse !
</a>
);
}
Gform.propTypes = {
className: PropTypes.string,
};
export default Gform;

@ -0,0 +1,24 @@
/* eslint react/prop-types: 0 */
import React from "react";
import i18n from "../../i18n";
const Helloasso = props => {
const locale =
i18n.language.substring(0, 2).toLowerCase() === "fr" ? "fr" : "en";
const linkHelloAssoBanner =
locale === "fr"
? "https://www.helloasso.com/associations/mieux-voter/formulaires/1/widget"
: "https://www.helloasso.com/associations/mieux-voter/formulaires/1/widget/en";
return (
<a href={linkHelloAssoBanner} target="_blank" rel="noopener noreferrer">
<img
src={"/banner/" + locale + "/helloasso.png"}
alt="support us on helloasso"
style={{ width: props.width }}
/>
</a>
);
};
export default Helloasso;

@ -0,0 +1,44 @@
import {useTranslation} from "next-i18next";
import {useRouter} from "next/router"
import {faPaypal} from "@fortawesome/free-brands-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
const Paypal = () => {
const {t} = useTranslation();
// FIXME generate a xx_XX string for locale version
const {locale} = useRouter();
let localeShort = locale.substring(0, 2);
let localeComplete =
localeShort.toLowerCase() + "_" + localeShort.toUpperCase();
if (localeComplete === "en_EN") {
localeComplete = "en_US";
}
const pixelLink =
`https://www.paypal.com/${localeComplete}/i/scr/pixel.gif`;
return (
<div className="d-inline-block m-auto">
<form
action="https://www.paypal.com/cgi-bin/webscr"
method="post"
target="_top"
>
<button
type="submit"
className="btn btn-primary"
title={t("PayPal - The safer, easier way to pay online!")}
>
{" "}
<FontAwesomeIcon icon={faPaypal} className="mr-2" />
{t("Support us !")}
</button>
<input type="hidden" name="cmd" value="_s-xclick" />
<input type="hidden" name="hosted_button_id" value="KB2Z7L9KARS7C" />
<img alt="" border="0" src={pixelLink} width="1" height="1" />
</form>
</div>
);
};
export default Paypal;

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

@ -0,0 +1,57 @@
import {useState} from "react";
import {
faTrashAlt,
} from "@fortawesome/free-solid-svg-icons";
import {Button, Modal, ModalHeader, ModalBody, ModalFooter} from "reactstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {useTranslation} from "next-i18next";
const ButtonWithConfirm = ({className, label, onDelete}) => {
const [visibled, setVisibility] = useState(false);
const {t} = useTranslation();
const toggle = () => setVisibility(!visibled)
return (
<div className="input-group-append">
<button
type="button"
className={className}
onClick={toggle}
>
<FontAwesomeIcon icon={faTrashAlt} />
</button>
<Modal
isOpen={visibled}
toggle={toggle}
className="modal-dialog-centered"
>
<ModalHeader toggle={toggle}>{t("Delete?")}</ModalHeader>
<ModalBody>
{t("Are you sure to delete")}{" "}
{label && label !== "" ? (
<b>&quot;{label}&quot;</b>
) : (
<>{t("the row")}</>
)}{" "}
?
</ModalBody>
<ModalFooter>
<Button
color="primary-outline"
className="text-primary border-primary"
onClick={toggle}>
{t("No")}
</Button>
<Button color="primary"
onClick={() => {toggle(); onDelete();}}
>
{t("Yes")}
</Button>
</ModalFooter>
</Modal>
</div >
);
}
export default ButtonWithConfirm;

@ -0,0 +1,55 @@
import {useState} from 'react'
import ButtonWithConfirm from "./ButtonWithConfirm";
import {
Row,
Col,
Input,
InputGroup,
InputGroupAddon,
} from "reactstrap";
import {useTranslation} from "react-i18next";
import {
sortableHandle
} from "react-sortable-hoc";
import HelpButton from "@components/form/HelpButton";
const DragHandle = sortableHandle(({children}) => (
<span className="input-group-text indexNumber">{children}</span>
));
const CandidateField = ({label, candIndex, onDelete, ...inputProps}) => {
const {t} = useTranslation();
return (
<Row>
<Col>
<InputGroup>
<InputGroupAddon addonType="prepend">
<DragHandle>
<span>{candIndex + 1}</span>
</DragHandle>
</InputGroupAddon>
<Input
type="text"
value={label}
{...inputProps}
placeholder={t("resource.candidatePlaceholder")}
tabIndex={candIndex + 1}
maxLength="250"
autoFocus
/>
<ButtonWithConfirm className="btn btn-primary border-light" label={label} onDelete={onDelete}>
</ButtonWithConfirm>
</InputGroup>
</Col>
<Col xs="auto" className="align-self-center pl-0">
<HelpButton>
{t(
"Enter the name of your candidate or proposal here (250 characters max.)"
)}
</HelpButton>
</Col>
</Row>
);
}
export default CandidateField

@ -0,0 +1,128 @@
import {useState, useEffect, createRef} from 'react'
import {useTranslation} from "react-i18next";
import {
Button,
Card,
CardBody
} from "reactstrap";
import {
faPlus,
} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
sortableContainer,
sortableElement,
sortableHandle
} from "react-sortable-hoc";
import arrayMove from "array-move"
import CandidateField from './CandidateField'
// const SortableItem = sortableElement(({className, ...childProps}) => <li className={className}><CandidateField {...childProps} /></li>);
//
// const SortableContainer = sortableContainer(({children}) => {
// return <ul className="sortable">{children}</ul>;
// });
const SortableItem = ({className, ...childProps}) => <li className={className}><CandidateField {...childProps} /></li>;
const SortableContainer = ({children}) => {
return <ul className="sortable">{children}</ul>;
};
const CandidatesField = ({onChange}) => {
const {t} = useTranslation();
const [candidates, setCandidates] = useState([])
const addCandidate = () => {
if (candidates.length < 1000) {
candidates.push({label: "", fieldRef: createRef()});
setCandidates([...candidates]);
onChange(candidates)
} else {
console.error("Too many candidates")
}
};
useEffect(() => {
addCandidate();
addCandidate();
}, [])
const removeCandidate = index => {
if (candidates.length === 1) {
const newCandidates = []
newCandidates.push({label: "", fieldRef: createRef()});
newCandidates.push({label: "", fieldRef: createRef()});
setCandidates(newCandidates);
onChange(newCandidates)
}
else {
const newCandidates = candidates.filter((c, i) => i != index)
setCandidates(newCandidates);
onChange(newCandidates);
}
};
const editCandidate = (index, label) => {
candidates[index].label = label
setCandidates([...candidates]);
onChange(candidates);
};
const handleKeyPress = (e, index) => {
if (e.key === "Enter") {
e.preventDefault();
if (index + 1 === candidates.length) {
addCandidate();
}
else {
candidates[index + 1].fieldRef.current.focus();
}
}
}
const onSortEnd = ({oldIndex, newIndex}) => {
setCandidates(arrayMove(candidates, oldIndex, newIndex));
};
return (
<>
<SortableContainer onSortEnd={onSortEnd}>
{candidates.map((candidate, index) => {
const className = "sortable"
return (
<SortableItem
className={className}
key={`item-${index}`}
index={index}
candIndex={index}
label={candidate.label}
onDelete={() => removeCandidate(index)}
onChange={(e) => editCandidate(index, e.target.value)}
onKeyPress={(e) => handleKeyPress(e, index)}
innerRef={candidate.fieldRef}
/>
)
})}
</SortableContainer>
<Button
color="secondary"
className="btn-block mt-2"
tabIndex={candidates.length + 2}
type="button"
onClick={addCandidate}
>
<FontAwesomeIcon icon={faPlus} className="mr-2" />
{t("Add a proposal")}
</Button>
</>
);
}
export default CandidatesField

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

@ -0,0 +1,75 @@
/* eslint react/prop-types: 0 */
import React, { Component } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
class HelpButton extends Component {
constructor(props) {
super(props);
this.state = {
tooltipOpen: false
};
}
showTooltip = () => {
this.setState({
tooltipOpen: true
});
};
hideTooltip = () => {
this.setState({
tooltipOpen: false
});
};
render() {
return (
<span className={this.props.className}>
<span>
{this.state.tooltipOpen ? (
<span
style={{
position: "absolute",
zIndex: 10,
fontSize: "12px",
color: "#000",
backgroundColor: "#fff",
display: "inline-block",
borderRadius: "0.25rem",
boxShadow: "-5px 0 5px rgba(0,0,0,0.5)",
maxWidth: "200px",
padding: "10px",
marginLeft: "-215px",
marginTop: "-25px"
}}
>
<span
style={{
position: "absolute",
width: 0,
height: 0,
borderTop: "10px solid transparent",
borderBottom: "10px solid transparent",
borderLeft: "10px solid #fff",
marginLeft: "190px",
marginTop: "15px"
}}
></span>
{this.props.children}
</span>
) : (
<span />
)}
</span>
<FontAwesomeIcon
icon={faQuestionCircle}
onMouseOver={this.showTooltip}
onMouseOut={this.hideTooltip}
/>
</span>
);
}
}
export default HelpButton;

@ -0,0 +1,94 @@
import Link from "next/link";
import {useTranslation} from "next-i18next";
import Paypal from "../banner/Paypal";
import {useBbox} from "./useBbox";
const Footer = () => {
const linkStyle = {whiteSpace: "nowrap"};
const {t} = useTranslation();
const [bboxLink1, link1] = useBbox();
const [bboxLink2, link2] = useBbox();
const [bboxLink3, link3] = useBbox();
const [bboxLink4, link4] = useBbox();
const [bboxLink5, link5] = useBbox();
const [bboxLink6, link6] = useBbox();
const [bboxLink7, link7] = useBbox();
return (
<footer className="text-center">
<div>
<ul className="tacky">
<li
ref={link1}
className={bboxLink1.top === bboxLink2.top ? "" : "no-tack"}
>
<Link href="/" style={linkStyle}>
{t("Homepage")}
</Link>
</li>
<li
ref={link2}
className={bboxLink2.top === bboxLink3.top ? "" : "no-tack"}
>
<Link href="/faq" style={linkStyle}>
{t("FAQ")}
</Link>
</li>
<li
ref={link3}
className={bboxLink3.top === bboxLink4.top ? "" : "no-tack"}
>
<a href="mailto:app@mieuxvoter.fr?subject=[HELP]" style={linkStyle}>
{t("Need help?")}
</a>
</li>
<li
ref={link4}
className={bboxLink4.top === bboxLink5.top ? "" : "no-tack"}
>
<a
href="https://mieuxvoter.fr/"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{t("Who are we?")}
</a>
</li>
<li
ref={link5}
className={bboxLink5.top === bboxLink6.top ? "" : "no-tack"}
>
<Link href="/privacy-policy" style={linkStyle}>
{t("Privacy policy")}
</Link>
</li>
<li
ref={link6}
className={bboxLink6.top === bboxLink7.top ? "" : "no-tack"}
>
<Link href="/legal-notices" style={linkStyle}>
{t("resource.legalNotices")}
</Link>
</li>
<li ref={link7}>
{" "}
<a
href="https://github.com/MieuxVoter"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{t("Source code")}
</a>
</li>
</ul>
</div>
<div className="mt-3">
<Paypal btnColor="btn-primary" />
</div>
</footer>
);
};
export default Footer;

@ -0,0 +1,61 @@
/* eslint react/prop-types: 0 */
import {useState} from "react";
import {Collapse, Navbar, NavbarToggler, Nav, NavItem} from "reactstrap";
import Link from "next/link";
import Head from "next/head";
import {useTranslation} from 'next-i18next'
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faRocket} from "@fortawesome/free-solid-svg-icons";
import LanguageSelector from "./LanguageSelector";
const Header = () => {
const [isOpen, setOpen] = useState(false)
const toggle = () => setOpen(!isOpen);
const {t} = useTranslation()
return (
<>
<Head><title>{t("title")}</title></Head>
<header>
<Navbar color="light" light expand="md">
<Link href="/">
<a className="navbar-brand">
<div className="d-flex flex-row">
<div className="align-self-center">
<img src="/logos/logo-color.svg" alt="logo" height="32" />
</div>
<div className="align-self-center ml-2">
<div className="logo-text">
<h1>
{t("Voting platform")}
<small>{t("Majority Judgment")}</small>
</h1>
</div>
</div>
</div>
</a>
</Link>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="ml-auto" navbar>
<NavItem>
<Link href="/new/">
<a className="text-primary nav-link"> <FontAwesomeIcon icon={faRocket} className="mr-2" />
{t("Start an election")}
</a>
</Link>
</NavItem>
<NavItem style={{width: "80px"}}>
<LanguageSelector />
</NavItem>
</Nav>
</Collapse>
</Navbar>
</header>
</>
);
}
export default Header;

@ -0,0 +1,32 @@
import {useRouter} from 'next/router'
import ReactFlagsSelect from 'react-flags-select';
const LanguageSelector = () => {
const router = useRouter();
let localeShort = router.locale.substring(0, 2).toUpperCase();
if (localeShort === "EN") localeShort = "GB";
const selectHandler = e => {
let locale = e.toLowerCase();
if (locale === "gb") locale = "en";
router.push("", "", {locale})
};
return (
<ReactFlagsSelect
onSelect={selectHandler}
countries={
// ["GB", "FR", "ES", "DE", "RU"]
["GB", "FR"]
}
showOptionLabel={false}
selected={localeShort}
selectedSize={15}
optionsSize={22}
showSelectedLabel={false}
showSecondaryOptionLabel={false}
/>
);
};
export default LanguageSelector;

@ -0,0 +1,20 @@
/* eslint react/prop-types: 0 */
import { useState } from 'react';
import { useRef } from 'react';
import { useEffect } from 'react';
export const useBbox = () => {
const ref = useRef();
const [bbox, setBbox] = useState({});
const set = () =>
setBbox(ref && ref.current ? ref.current.getBoundingClientRect() : {});
useEffect(() => {
set();
window.addEventListener('resize', set);
return () => window.removeEventListener('resize', set);
}, []);
return [bbox, ref];
};

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

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

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

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

@ -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"],
defaultNS: "resource",
fallbackNS: ["common"],
},
}

@ -0,0 +1,45 @@
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",
};

17331
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,165 +1,44 @@
{ {
"name": "front-mieux-voter", "name": "next-netlify-starter",
"version": "0.1.0", "version": "0.3.0",
"private": true, "private": true,
"dependencies": {
"@babel/core": "7.4.3",
"@fortawesome/fontawesome-free": "^5.9.0",
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "^0.1.4",
"@svgr/webpack": "4.1.0",
"@typescript-eslint/eslint-plugin": "2.29.0",
"@typescript-eslint/parser": "2.29.0",
"axios": "^0.19.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^24.8.0",
"babel-loader": "8.0.5",
"babel-plugin-named-asset-import": "^0.3.2",
"babel-preset-react-app": "^9.0.0",
"bootstrap": "^4.3.1",
"camelcase": "^5.2.0",
"case-sensitive-paths-webpack-plugin": "2.2.0",
"css-loader": "2.1.1",
"dotenv": "6.2.0",
"dotenv-expand": "4.2.0",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.13.1",
"eslint": "^6.8.0",
"eslint-config-react-app": "^5.2.1",
"eslint-loader": "4.0.2",
"eslint-plugin-flowtype": "4.7.0",
"eslint-plugin-import": "2.20.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "^3.0.0",
"file-loader": "3.0.1",
"fs-extra": "7.0.1",
"global": "^4.4.0",
"html-webpack-plugin": "4.0.0-beta.5",
"i18next": "^19.4.1",
"i18next-browser-languagedetector": "^4.0.2",
"i18next-xhr-backend": "^3.2.2",
"identity-obj-proxy": "3.0.0",
"is-wsl": "^1.1.0",
"jest": "24.7.1",
"jest-environment-jsdom-fourteen": "0.1.0",
"jest-resolve": "24.7.1",
"jest-watch-typeahead": "0.3.0",
"mini-css-extract-plugin": "0.5.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"pnp-webpack-plugin": "1.2.1",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "7.0.1",
"postcss-preset-env": "6.6.0",
"postcss-safe-parser": "4.0.1",
"prop-types": "^15.7.2",
"query-string": "^6.12.0",
"querystringify": "^2.0.0",
"react": "^16.8.6",
"react-app-polyfill": "^1.0.1",
"react-dev-utils": "^9.0.1",
"react-dom": "^16.8.6",
"react-flag-icon-css": "^1.0.25",
"react-flags-select": "^1.1.12",
"react-i18next": "^11.3.4",
"react-loader-spinner": "^3.1.14",
"react-multi-email": "^0.5.3",
"react-router-dom": "^5.0.0",
"react-sortable-hoc": "^1.9.1",
"react-toastify": "^5.2.1",
"reactstrap": "^8.0.0",
"resolve": "1.10.0",
"sass-loader": "7.1.0",
"semver": "6.0.0",
"style-loader": "0.23.1",
"terser-webpack-plugin": "1.2.3",
"ts-pnp": "1.1.2",
"url-loader": "1.1.2",
"webpack": "4.29.6",
"webpack-dev-server": "3.2.1",
"webpack-manifest-plugin": "2.0.4",
"workbox-webpack-plugin": "4.2.0"
},
"scripts": { "scripts": {
"start": "node scripts/start.js", "dev": "next dev",
"build": "node scripts/build.js", "build": "next build",
"test": "node scripts/test.js", "start": "next start",
"translate": "i18next-scanner --config i18n.config.js 'src/**/*.{js,jsx}'" "export": "next export"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}, },
"jshintConfig": { "dependencies": {
"esversion": 6 "@fortawesome/fontawesome-free": "^5.15.3",
}, "@fortawesome/fontawesome-svg-core": "^1.2.35",
"jest": { "@fortawesome/free-brands-svg-icons": "^5.15.3",
"collectCoverageFrom": [ "@fortawesome/free-solid-svg-icons": "^5.15.3",
"src/**/*.{js,jsx,ts,tsx}", "@fortawesome/react-fontawesome": "^0.1.14",
"!src/**/*.d.ts" "@svgr/webpack": "^5.5.0",
], "array-move": "^3.0.1",
"setupFiles": [ "bootstrap": "^4.6.0",
"react-app-polyfill/jsdom" "bootstrap-scss": "^4.6.0",
], "domexception": "^2.0.1",
"setupFilesAfterEnv": [], "dotenv": "^8.2.0",
"testMatch": [ "form-data": "^4.0.0",
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "handlebars": "^4.7.7",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}" "i18next": "^20.2.1",
], "i18next-chained-backend": "^2.1.0",
"testEnvironment": "jest-environment-jsdom-fourteen", "i18next-fs-backend": "^1.1.1",
"transform": { "i18next-http-backend": "^1.2.1",
"^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest", "i18next-localstorage-backend": "^3.1.2",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js", "mailgun.js": "^3.3.0",
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js" "next": "^10.0.9",
}, "next-i18next": "^8.1.3",
"transformIgnorePatterns": [ "query-string": "^7.0.0",
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", "react": "^17.0.2",
"^.+\\.module\\.(css|sass|scss)$" "react-dom": "^17.0.1",
], "react-flags-select": "^2.1.2",
"modulePaths": [], "react-i18next": "^11.8.12",
"moduleNameMapper": { "react-multi-email": "^0.5.3",
"^react-native$": "react-native-web", "react-sortable-hoc": "^2.0.0",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy" "react-toastify": "^7.0.3",
}, "reactstrap": "^8.9.0",
"moduleFileExtensions": [ "sass": "^1.32.8"
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
},
"babel": {
"presets": [
"react-app"
]
},
"devDependencies": {
"i18next-scanner": "^2.11.0",
"prettier": "1.19.1"
} }
} }

@ -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,262 @@
import Link from "next/link";
import { Container, Row, Col } from "reactstrap";
import { useTranslation } from "next-i18next";
import Paypal from "@components/banner/Paypal";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import config from "../next-i18next.config.js";
export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const FAQ = (props) => {
const { t } = useTranslation();
return (
<Container>
<Row>
<Link href="/" className="d-block ml-auto mr-auto mb-4">
<img src="/logos/logo-line-white.svg" alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h1>{t("FAQ")}</h1>
</Col>
</Row>
<Row className="mt-4">
<Col>
<h4 className="bold mt-5">Quest-ce que le Jugement Majoritaire ?</h4>
<p>
Un principe simple et intuitif, qui change tout : lélecteur vote en
donnant son avis sur toutes les candidatures présentées, leur
attribuant la mention de son choix (par exemple. Très bien, Bien,
Assez bien, Passable, Insuffisant, À Rejeter). La candidature
retenue est celle jugée la plus méritante par la majorité de
lélectorat (celui qui obtient la meilleure mention « majoritaire
»).
</p>
<div style={{ maxWidth: "445px" }}>
<video width="100%" height="250" controls="controls">
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.mp4"
type="video/mp4"
/>
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.webm"
type="video/webm"
/>
<source
src="/video/Le_Jugement_Majoritaire_en_1_minute.3gpp"
type="video/3gpp"
/>
</video>
</div>
<h4 className="bold mt-5">D vient le Jugement Majoritaire ?</h4>
<p>
Le jugement majoritaire est un mode de scrutin inventé par deux
chercheurs Français du Centre National de la Recherche Scientifique
(CNRS) en 2011, <u>Michel Balinski</u> et <u>Rida Laraki</u>.
</p>
<h4 className="bold mt-5">
Quels sont les avantages du Jugement Majoritaire ?
</h4>
<p>
Une mesure précise de lopinion des participants au vote, à même
déclairer la décision collective. En demandant aux électeurs leur
opinion sur chaque option soumise au vote, on bénéficie de beaucoup
plus dinformations que dans le cadre du scrutin uninominal qui,
résumant lopinion des électeurs à un choix, ignore lessentiel de
linformation quant à ce quils pensent. En agrégeant un grand
nombre dinformations, le Jugement Majoritaire ne produit pas «
juste » un gagnant qui obtiendrait la majorité des voix. Il mesure
précisément le crédit porté à chacune des options et permet
daffiner autant que de pacifier la prise de décision.
</p>
<h4 className="bold mt-5">
Quand et comment utiliser le Jugement Majoritaire ?
</h4>
<p>
Le Jugement majoritaire sapplique à tout type de votation
collective, quil sagisse délire un candidat, de retenir une ou
plusieurs idées lors dun atelier collaboratif, de choisir entre
plusieurs projets, de classer les vins, etc. Il peut être utilisé à
toutes les échelles (locale, nationale, internationale) et dans tous
les milieux (écoles, entreprises, associations, coopératives,
collectivités publiques).
</p>
<h4 className="bold mt-5">Qui peut utiliser cette application ?</h4>
<p>
Cette application de Jugement Majoritaire est ouverte à toute
personne désireuse de prendre une décision collective, entre amis,
entre collègues, entre membres dun groupe. Elle est libre daccès
et gratuite. Notre ambition est de vous proposer la meilleure
expérience de prise de décision collective et démocratique.
</p>
<h4 className="bold mt-5">
Comment organiser une élection avec plusieurs milliers de votants ?
</h4>
<p>
Cette application ne convient pas pour les votes à plus de 1000
votants. Si cest votre cas, nous vous invitons à nous contacter par
email à ladresse{" "}
<a href="mailto:contact@mieuxvoter.fr" className="text-light">
contact@mieuxvoter.fr
</a>
. Dans le cas dun vote sur invitation nous vous suggérons de ne pas
dépasser 200 participants (le temps de création du vote peut prendre
quelques minutes).
</p>
<h4 className="bold mt-5">
Je rencontre un problème, comment obtenir de laide ?
</h4>
<p>
Si vous rencontrez un problème en utilisant notre application,
prenez contact avec nous par email à ladresse «
<a
href="mailto:app@mieuxvoter.fr?subject=[HELP]"
className="text-light"
>
app@mieuxvoter.fr
</a>
», et prenez soin de bien décrire le problème rencontré dans votre
message. Ajoutez éventuellement dans votre description le lien de
votre vote.
</p>
<h4 className="bold mt-5">
Y-a til une limite de votants appliquée pour les votes sur
invitation ?
</h4>
<p>
Le nombre maximum de votants pour un vote sur invitation est de 1000
personnes. Si toutefois votre besoin est supérieur à cette limite,
nous vous invitons à nous envoyer un email à ladresse «
<a href="mailto:contact@mieuxvoter.fr" className="text-light">
contact@mieuxvoter.fr
</a>
».
</p>
<h4 className="bold mt-5">
Combien de temps le lien vers la page de résultat reste-t-il actif ?
</h4>
<p>
Les liens fournis lors de la création de votre vote nont pas de
date dexpiration. Conservez-les précieusement afin de pouvoir
consulter les résultat dans le futur.
</p>
<h4 className="bold mt-5">
Comment puis-je massurer quune même personne ne vote pas deux
fois?
</h4>
<p>
Dans le cas dun vote sur invitation, seules les personnes dont le
courriel a été ajouté à la création du vote reçoivent une invitation
et peuvent donc voter. Chacune des invitations dispose dun lien
unique auquel est associé un jeton à usage unique. Ce jeton est
détruit aussitôt que la participation au vote de linvité est
enregistrée. Il garantit donc à lorganisateur que chaque
participant na pu voter quune seule fois.
</p>
<p>
Dans le cas dun vote public, toute personne peut participer à
lélection sil dispose du lien de lélection. Il ny a dans ce cas
aucune limite de soumission dun vote. Une même personne peut donc
voter plusieurs fois.
</p>
<h4 className="bold mt-5">
Lorsque jorganise une élection, puis-je connaître le nombre et
lidentité des votants?
</h4>
<p>
Le nombre de votants est indiqué sur la page de résultats de votre
élection. Lidentité des votants est quant à elle effacée, afin de
respecter les conditions dun vote démocratique lanonymat
garantit la sincérité des électeurs.
</p>
<h4 className="bold mt-5">Puis-je modifier mon vote ?</h4>
<p>
Une fois votre vote enregistré, vous ne pouvez plus le modifier. En
effet, votre vote étant anonymisé, ce qui nous empêche de faire le
lien entre vous et votre vote.
</p>
<h4 className="bold mt-5">
Comment puis-je récupérer un lien si je lai perdu ?
</h4>
<p>
Vous ne pouvez pas récupérer un lien pour voter après quil vous
soit communiquer. Gardez le précieusement. Cependant si vous avez le
lien pour voter, nous pouvons vous transmettre le lien des
résultats.
</p>
<h4 className="bold mt-5">
Comment interpréter les résultats dun vote au Jugement Majoritaire
?
</h4>
<p>
Les candidats ou propositions sont triées de la mention majoritaire
la plus favorable à la plus défavorable. En cas dégalité, on
calcule alors pour chaque candidat à départager: le pourcentage
délecteurs attribuant strictement plus que la mention majoritaire
commune et le pourcentage délecteurs attribuant strictement moins
que la mention majoritaire commune. La plus grande des 4 valeurs
détermine le résultat.
</p>
<h4 className="bold mt-5">Quelle sécurité pour mes données ?</h4>
<p>
Afin de garantir la sécurité de vos données, leur transmission est
chiffrée et vos votes sont anonymisés.
</p>
<h4 className="bold mt-5">
Que faites-vous des données collectées ?
</h4>
<p>
Lapplication app.mieuxvoter.fr a pour seul et unique but de faire
découvrir le vote au Jugement Majoritaire. Elle na pas de but
politique, ni commercial. Mieux Voter attache la plus grande
importance au strict respect de la vie privée, et utilise ces
données uniquement de manière responsable et confidentielle, dans
une finalité précise.
</p>
<h4 className="bold mt-5">Qui est Mieux Voter ?</h4>
<p>
« Mieux Voter » est une association loi 1901 qui promeut
lutilisation du Jugement Majoritaire, nouvelle théorie du choix
social, comme un outil pour améliorer les décisions collectives et
les exercices de démocratie participative à lusage de tous.
</p>
<h4 className="bold mt-5">
Comment nous aider à faire connaître le Jugement Majoritaire ?
</h4>
<p>
Vous avez apprécié votre expérience de vote démocratique au Jugement
Majoritaire ? <br />
Nous en sommes ravis ! Vous pouvez nous aider en faisant un don à
lassociation ici :
</p>
<Paypal btnColor="btn-success" className="mt-1" />
</Col>
</Row>
</Container>
);
};
export default FAQ;

@ -0,0 +1,71 @@
import { useState } from "react";
import Head from "next/head";
import Link from "next/link";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useTranslation } from "next-i18next";
import { Container, Row, Col, Button, Input } from "reactstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faRocket } from "@fortawesome/free-solid-svg-icons";
import config from "../next-i18next.config.js";
export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const Home = () => {
const [title, setTitle] = useState(null);
const { t } = useTranslation();
return (
<Container>
<form autoComplete="off">
<Row>
<img
src="logos/logo-line-white.svg"
alt="logo of Mieux Voter"
height="128"
className="d-block ml-auto mr-auto mb-4"
/>
</Row>
<Row>
<Col className="text-center">
<h3>{t("common.valueProp")}</h3>
</Col>
</Row>
<Row className="mt-2">
<Col xs="12" md="9" xl="6" className="offset-xl-2">
<Input
placeholder={t("resource.writeQuestion")}
autoFocus
required
className="mt-2"
name="title"
value={title ? title : ""}
onChange={(e) => setTitle(e.target.value)}
maxLength="250"
/>
</Col>
<Col xs="12" md="3" xl="2">
<Link href={{ pathname: "/new/", query: { title: title } }}>
<Button
type="submit"
className="btn btn-block btn-secondary mt-2"
>
<FontAwesomeIcon icon={faRocket} className="mr-2" />
{t("resource.start")}
</Button>
</Link>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<p>{t("resource.noAds")}</p>
</Col>
</Row>
</form>
</Container>
);
};
export default Home;

@ -0,0 +1,81 @@
import Link from "next/link";
import { Container, Row, Col } from "reactstrap";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import config from "../next-i18next.config.js";
export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const LegalNotices = (props) => {
const { t } = useTranslation();
return (
<Container>
<Row>
<Link href="/" className="d-block ml-auto mr-auto mb-4">
<img src="/logos/logo-line-white.svg" alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h1>{t("resource.legalNotices")}</h1>
</Col>
</Row>
<Row className="mt-4">
<Col>
<h3 className="bold">Editeur</h3>
<p>
Cette Application est éditée par lassociation loi 1901{" "}
<a
href="https://mieuxvoter.fr/"
target="_blank"
rel="noopener noreferrer"
className="text-white"
>
Mieux Voter
</a>
, dont le siège social est situé au 59 rue Saint-André des Arts, à
Paris (75006).
</p>
<p>
Adresse email :{" "}
<a href="mailto:app@mieuxvoter.fr" className="text-light">
app@mieuxvoter.fr
</a>
</p>
<p>
<b>Directeur de la publication</b>
<br />
Pierre-Louis Guhur
</p>
<h3 className="mt-2 bold">Hébergement</h3>
<ul>
<li>Base de données : Institut Systèmes Complexes, Paris ;</li>
<li>
Réseau de diffusion de contenu (CDN) : Netlify, 2325 3rd Street,
Suite 215, San Francisco, California 94107.
</li>
</ul>
<h3 className="mt-2 bold">&OElig;uvres graphiques</h3>
<p>
Les illustrations et graphismes sur cette application sont
l&oelig;uvre de lassociation Mieux Voter.
</p>
</Col>
</Row>
<Row className="mt-4">
<Col className="text-center">
<Link href="/" className="btn btn-secondary">
{t("common.backHomepage")}
</Link>
</Col>
</Row>
</Container>
);
};
export default LegalNotices;

@ -0,0 +1,151 @@
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 } from "@services/api";
import { Col, Container, Row } from "reactstrap";
import Link from "next/link";
import {
faCopy,
faVoteYea,
faExclamationTriangle,
faExternalLinkAlt,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import CopyField from "@components/CopyField";
import Facebook from "@components/banner/Facebook";
import config from "../../../next-i18next.config.js";
export async function getServerSideProps({ query: { pid }, locale }) {
const [res, translations] = await Promise.all([
getDetails(
pid,
(res) => ({ ok: true, ...res }),
(err) => ({ ok: false, err })
),
serverSideTranslations(locale, [], config),
]);
if (!res.ok) {
return { props: { err: res.err, ...translations } };
}
return {
props: {
invitationOnly: res.on_invitation_only,
restrictResults: res.restrict_results,
title: res.title,
pid: pid,
...translations,
},
};
}
const ConfirmElection = ({
title,
restrictResults,
invitationOnly,
pid,
err,
}) => {
if (err) {
return <Error value={err}></Error>;
}
const { t } = useTranslation();
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 : (
<Row className="mt-4 mb-4">
<Col className="text-center">
<Link href={`/vote/${pid}`}>
<a className="btn btn-secondary">
<FontAwesomeIcon icon={faVoteYea} className="mr-2" />
{t("Participate now!")}
</a>
</Link>
</Col>
</Row>
);
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>
{participate}
</Container>
);
};
export default ConfirmElection;

@ -0,0 +1,557 @@
import { useState, useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import {
Collapse,
Container,
Row,
Col,
Input,
Label,
InputGroup,
InputGroupAddon,
Button,
Card,
CardBody,
} from "reactstrap";
import { ReactMultiEmail, isEmail } from "react-multi-email";
import "react-multi-email/style.css";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import queryString from "query-string";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPlus,
faTrashAlt,
faCheck,
faCogs,
faExclamationTriangle,
} from "@fortawesome/free-solid-svg-icons";
import { useAppContext } from "@services/context";
import { createElection } from "@services/api";
import { translateGrades } from "@services/grades";
import HelpButton from "@components/form/HelpButton";
import Loader from "@components/wait";
import CandidatesField from "@components/form/CandidatesField";
import ConfirmModal from "@components/form/ConfirmModal";
import config from "../../next-i18next.config.js";
// Error messages
const AT_LEAST_2_CANDIDATES_ERROR = "Please add at least 2 candidates.";
const NO_TITLE_ERROR = "Please add a title.";
const isValidDate = (date) => date instanceof Date && !isNaN(date);
const getOnlyValidDate = (date) => (isValidDate(date) ? date : new Date());
// Convert a Date object into YYYY-MM-DD
const dateToISO = (date) =>
getOnlyValidDate(date).toISOString().substring(0, 10);
// Retrieve the current hour, minute, sec, ms, time into a timestamp
const hours = (date) => getOnlyValidDate(date).getHours() * 3600 * 1000;
const minutes = (date) => getOnlyValidDate(date).getMinutes() * 60 * 1000;
const seconds = (date) => getOnlyValidDate(date).getSeconds() * 1000;
const ms = (date) => getOnlyValidDate(date).getMilliseconds();
const time = (date) =>
hours(getOnlyValidDate(date)) +
minutes(getOnlyValidDate(date)) +
seconds(getOnlyValidDate(date)) +
ms(getOnlyValidDate(date));
// Retrieve the time part from a timestamp and remove the day. Return a int.
const timeMinusDate = (date) => time(getOnlyValidDate(date));
// Retrieve the day and remove the time. Return a Date
const dateMinusTime = (date) =>
new Date(getOnlyValidDate(date).getTime() - time(getOnlyValidDate(date)));
const displayClockOptions = () =>
Array(24)
.fill(1)
.map((x, i) => (
<option value={i} key={i}>
{i}h00
</option>
));
export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const CreateElection = (props) => {
const { t } = useTranslation();
// default value : start at the last hour
const now = new Date();
const [title, setTitle] = useState("");
const [candidates, setCandidates] = useState([{ label: "" }, { label: "" }]);
const [numGrades, setNumGrades] = useState(5);
const [waiting, setWaiting] = useState(false);
const [isAdvancedOptionsOpen, setAdvancedOptionsOpen] = useState(false);
const [isTimeLimited, setTimeLimited] = useState(false);
const [restrictResult, setRestrictResult] = useState(false);
const [start, setStart] = useState(
new Date(now.getTime() - minutes(now) - seconds(now) - ms(now))
);
const [finish, setFinish] = useState(
new Date(start.getTime() + 7 * 24 * 3600 * 1000)
);
const [emails, setEmails] = useState([]);
// set the title on loading
const router = useRouter();
useEffect(() => {
if (!router.isReady) return;
const { title: urlTitle } = router.query;
setTitle(urlTitle || "");
}, [router.isReady]);
const handleIsTimeLimited = (event) => {
setTimeLimited(event.target.value === "1");
};
const handleRestrictResultCheck = (event) => {
setRestrictResult(event.target.value === "1");
};
const toggleAdvancedOptions = () => {
setAdvancedOptionsOpen(!isAdvancedOptionsOpen);
};
const addCandidate = () => {
if (candidates.length < 1000) {
candidates.push({ label: "" });
setCandidates(candidates);
}
};
const checkFields = () => {
if (!candidates) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
}
let numCandidates = 0;
candidates.forEach((c) => {
if (c.label !== "") numCandidates += 1;
});
if (numCandidates < 2) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
}
if (!title || title === "") {
return { ok: false, msg: NO_TITLE_ERROR };
}
return { ok: true, msg: "OK" };
};
const handleSubmit = () => {
const check = checkFields();
if (!check.ok) {
toast.error(t(check.msg), {
position: toast.POSITION.TOP_CENTER,
});
return;
}
setWaiting(true);
createElection(
title,
candidates.map((c) => c.label).filter((c) => c !== ""),
{
emails,
numGrades,
start: start.getTime() / 1000,
finish: finish.getTime() / 1000,
restrictResult: restrictResult,
locale: router.locale.substring(0, 2).toLowerCase(),
},
(result) => {
if (result.id) {
router.push(`/new/confirm/${result.id}`);
} else {
toast.error(t("Unknown error. Try again please."), {
position: toast.POSITION.TOP_CENTER,
});
setWaiting(false);
}
}
);
};
const handleSendNotReady = (msg) => {
toast.error(t(msg), {
position: toast.POSITION.TOP_CENTER,
});
};
const check = checkFields();
const grades = translateGrades(t);
return (
<Container>
<Head>
<meta
key="og:title"
property="og:title"
content={t("common.application")}
/>
<meta
property="og:description"
key="og:description"
content={t("resource.valueProp")}
/>
</Head>
<ToastContainer />
{waiting ? <Loader /> : ""}
<form onSubmit={handleSubmit} autoComplete="off">
<Row>
<Col>
<h3>{t("resource.startVote")}</h3>
</Col>
</Row>
<hr />
<Row className="mt-4">
<Col xs="12">
<Label for="title">{t("resource.questionLabel")}</Label>
</Col>
<Col>
<Input
placeholder={t("resource.writeQuestionHere")}
tabIndex="1"
name="title"
id="title"
autoFocus
value={title}
onChange={(e) => setTitle(e.target.value)}
maxLength="250"
/>
</Col>
<Col xs="auto" className="align-self-center pl-0">
<HelpButton>
<u>{t("resource.eg")}</u> <em>{t("resource.exampleQuestion")}</em>
</HelpButton>
</Col>
</Row>
<Row className="mt-4">
<Col xs="12">
<Label for="title">{t("common.candidates")}</Label>
</Col>
<Col xs="12">
<CandidatesField onChange={setCandidates} />
</Col>
<Col
xs="12"
sm="6"
md="12"
className="text-center text-sm-right text-md-left"
>
<Button
color="link"
className="text-white mt-3 mb-1"
onClick={toggleAdvancedOptions}
>
<FontAwesomeIcon icon={faCogs} className="mr-2" />
{t("resource.advancedOptions")}
</Button>
</Col>
</Row>
<Collapse isOpen={isAdvancedOptionsOpen}>
<Card>
<CardBody className="text-primary">
<Row>
<Col xs="12" md="3" lg="3">
<Label for="title">{t("Access to results")}</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio " htmlFor="restrict_result_false">
<span className="small text-dark">{t("Immediately")}</span>
<input
className="radio"
type="radio"
name="restrict_result"
id="restrict_result_false"
onClick={handleRestrictResultCheck}
defaultChecked={!restrictResult}
value="0"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio" htmlFor="restrict_result_true">
<span className="small">
<span className="text-dark">
{t("At the end of the election")}
</span>
<HelpButton className="ml-2">
{t(
"No one will be able to see the result until the end date is reached or until all participants have voted."
)}
</HelpButton>
</span>
<input
className="radio"
type="radio"
name="restrict_result"
id="restrict_result_true"
onClick={handleRestrictResultCheck}
defaultChecked={restrictResult}
value="1"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
</Row>
<hr className="mt-2 mb-2" />
<Row>
<Col xs="12" md="3" lg="3">
<Label for="title">{t("Voting time")}</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio " htmlFor="is_time_limited_false">
<span className="small text-dark">{t("Unlimited")}</span>
<input
className="radio"
type="radio"
name="time_limited"
id="is_time_limited_false"
onClick={handleIsTimeLimited}
defaultChecked={!isTimeLimited}
value="0"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
<Col xs="12" md="4" lg="3">
<Label className="radio" htmlFor="is_time_limited_true">
<span className="small">
<span className="text-dark">{t("Defined period")}</span>
</span>
<input
className="radio"
type="radio"
name="time_limited"
id="is_time_limited_true"
onClick={handleIsTimeLimited}
defaultChecked={isTimeLimited}
value="1"
/>
<span className="checkround checkround-gray" />
</Label>
</Col>
</Row>
<div
className={
(isTimeLimited ? "d-block " : "d-none") + " bg-light p-3"
}
>
<Row>
<Col xs="12" md="3" lg="3">
<span className="label">- {t("Starting date")}</span>
</Col>
<Col xs="6" md="4" lg="3">
<input
className="form-control"
type="date"
value={dateToISO(start)}
onChange={(e) => {
setStart(
new Date(
timeMinusDate(start) +
new Date(e.target.valueAsNumber).getTime()
)
);
}}
/>
</Col>
<Col xs="6" md="5" lg="3">
<select
className="form-control"
value={getOnlyValidDate(start).getHours()}
onChange={(e) =>
setStart(
new Date(
dateMinusTime(start).getTime() +
e.target.value * 3600000
)
)
}
>
{displayClockOptions()}
</select>
</Col>
</Row>
<Row className="mt-2">
<Col xs="12" md="3" lg="3">
<span className="label">- {t("Ending date")}</span>
</Col>
<Col xs="6" md="4" lg="3">
<input
className="form-control"
type="date"
value={dateToISO(finish)}
min={dateToISO(start)}
onChange={(e) => {
setFinish(
new Date(
timeMinusDate(finish) +
new Date(e.target.valueAsNumber).getTime()
)
);
}}
/>
</Col>
<Col xs="6" md="5" lg="3">
<select
className="form-control"
value={getOnlyValidDate(finish).getHours()}
onChange={(e) =>
setFinish(
new Date(
dateMinusTime(finish).getTime() +
e.target.value * 3600000
)
)
}
>
{displayClockOptions()}
</select>
</Col>
</Row>
</div>
<hr className="mt-2 mb-2" />
<Row>
<Col xs="12" md="3" lg="3">
<span className="label">{t("Grades")}</span>
</Col>
<Col xs="10" sm="11" md="4" lg="3">
<select
className="form-control"
tabIndex={candidates.length + 3}
onChange={(e) => setNumGrades(e.target.value)}
defaultValue="5"
>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</Col>
<Col xs="auto" className="align-self-center pl-0 ">
<HelpButton>
{t(
"You can select here the number of grades for your election"
)}
<br />
<u>{t("For example:")}</u>{" "}
<em>
{" "}
{t("5 = Excellent, Very good, Good, Fair, Passable")}
</em>
</HelpButton>
</Col>
<Col
xs="12"
md="9"
lg="9"
className="offset-xs-0 offset-md-3 offset-lg-3"
>
{grades.map((mention, i) => {
return (
<span
key={i}
className="badge badge-light mr-2 mt-2 "
style={{
backgroundColor: mention.color,
color: "#fff",
opacity: i < numGrades ? 1 : 0.3,
}}
>
{mention.label}
</span>
);
})}
</Col>
</Row>
<hr className="mt-2 mb-2" />
<Row>
<Col xs="12" md="3" lg="3">
<span className="label">{t("Participants")}</span>
</Col>
<Col xs="12" md="9" lg="9">
<ReactMultiEmail
placeholder={t("Add here participants' emails")}
emails={emails}
onChange={setEmails}
validateEmail={(email) => {
return isEmail(email); // return boolean
}}
getLabel={(email, index, removeEmail) => {
return (
<div data-tag key={index}>
{email}
<span
data-tag-handle
onClick={() => removeEmail(index)}
>
×
</span>
</div>
);
}}
/>
<div>
<small className="text-muted">
{t(
"If you list voters' emails, only them will be able to access the election"
)}
</small>
</div>
</Col>
</Row>
<hr className="mt-2 mb-2" />
</CardBody>
</Card>
</Collapse>
<Row className="justify-content-end mt-2">
<Col xs="12" md="3">
{check.ok ? (
<ConfirmModal
title={title}
candidates={candidates}
isTimeLimited={isTimeLimited}
start={start}
finish={finish}
emails={emails}
restrictResult={restrictResult}
grades={grades.slice(0, numGrades)}
className={"btn btn-success float-right btn-block"}
tabIndex={candidates.length + 1}
confirmCallback={handleSubmit}
/>
) : (
<Button
type="button"
className="btn btn-dark float-right btn-block"
onClick={handleSendNotReady}
>
<FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Confirm")}
</Button>
)}
</Col>
</Row>
</form>
</Container>
);
};
export default CreateElection;

@ -0,0 +1,101 @@
import { Col, Container, Row } from "reactstrap";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import config from "../next-i18next.config.js";
export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const PrivacyPolicy = (props) => {
const { t } = useTranslation();
return (
<Container>
<Row>
<Link href="/" className="d-block ml-auto mr-auto mb-4">
<img src="/logos/logo-line-white.svg" alt="logo" height="128" />
</Link>
</Row>
<Row className="mt-4">
<Col className="text-center">
<h1>{t("Privacy policy")}</h1>
</Col>
</Row>
<Row className="mt-4">
<Col>
<p className="text-center">
Dernière mise à jour de notre politique de confidentialité effectuée
le 27 avril 2020.
</p>
<h4 className="bold mt-5">Introduction</h4>
<p>
Dans le cadre de la mise à disposition de son application web de
vote au jugement majoritaire, accessible sur Internet à ladresse
app.mieuxvoter.fr, ci-après lApplication, lassociation loi 1901 «
Mieux Voter » , dont le siège social est situé au 59 rue saint andré
des arts, à Paris (75006), ci-après lAssociation, est amenée à
collecter et à traiter des informations dont certaines sont
qualifiées de « Données personnelles » . Mieux Voter attache la plus
grande importance au respect de la vie privée, et utilise ces
données uniquement de manière responsable et confidentielle et dans
une finalité précise.
</p>
<h4 className="bold mt-5">Notre politique de confidentialité</h4>
<p>
La présente politique de confidentialité détaille les conditions
dutilisation et de traitement par lAssociation des Données
personnelles (ci-après définies) collectées via lApplication.
LAssociation sengage à respecter les dispositions de la loi
n°78-17 du 6 janvier 1978 relative à linformatique, aux fichiers et
aux libertés modifiée et au Règlement (UE) 2016/679 du Parlement
européen et du Conseil du 27 avril 2016 dit « RGPD » et prendre
toute précaution nécessaire pour préserver la sécurité des Données
personnelles confiées.
</p>
<h4 className="bold mt-5">Responsable de traitement</h4>
<p>
En qualité de responsable de traitement, lAssociation peut traiter
les Données personnelles.
</p>
<h4 className="bold mt-5">
Données personnelles traitées et finalités de traitement
</h4>
<p>
LAssociation recueille sur lApplication les Données personnelles
dans une finalité précise. Ces données sont nécessaires à la
fourniture de notre service. Dans le cadre de la fourniture de ce
service, lAssociation traite uniquement les données personnelles
suivantes (définies comme les « Données personnelles ») strictement
nécessaires à la fourniture du service :
</p>
<ul>
<li> Les emails des personnes invitées à un vote</li>
</ul>
<p>
{" "}
La finalité de traitement de ces données personnelles est de
permettre à lAssociation de fournir le service. Ces données sont
traitées au moment de la création du vote pour envoyer les
invitations et détruites aussitôt les invitations envoyées. Elles ne
sont jamais stockées sur nos serveurs.
</p>
<h4 className="bold mt-5">Sécurité des Données personnelles</h4>
<p>
LAssociation sengage, au titre de son obligation de moyens, à
prendre toutes les précautions utiles et met en œuvre des mesures
techniques et organisationnelles appropriées en la matière pour
garantir un niveau de sécurité adapté et pour protéger les Données
personnelles contre les altérations, destructions et accès non
autorisés.
</p>
</Col>
</Row>
</Container>
);
};
export default PrivacyPolicy;

@ -0,0 +1,340 @@
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,
(res) => {
ok: true, res;
},
(err) => ({ ok: false, err })
),
getDetails(
pid,
(res) => {
ok: true, res;
},
(err) => ({ ok: false, err })
),
serverSideTranslations(locale, [], config),
]);
if (!res.ok) {
return { props: { err: res.err, ...translations } };
}
if (!details.ok) {
return { props: { err: details.err, ...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">
<span className="mt-2 ml-2">{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>
</li>
);
})}
</ol>
<h5>
<small>
{t("Number of votes:")}
{" " + numVotes}
</small>
</h5>
</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,
}}
>
&nbsp;
</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,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 { 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 [res, translations] = await Promise.all([
getDetails(
pid,
(res) => ({ ok: true, ...res }),
(err) => ({ ok: false, err })
),
serverSideTranslations(locale, [], config),
]);
if (!res.ok) {
return { props: { err: res.err, ...translations } };
}
console.log(res);
shuffle(res.candidates);
return {
props: {
...translations,
invitationOnly: res.on_invitation_only,
restrictResults: res.restrict_results,
candidates: res.candidates.map((name, i) => ({ id: i, label: name })),
title: res.title,
numGrades: res.num_grades,
pid: pid,
token: tid || null,
},
};
}
const VoteBallot = ({ candidates, title, numGrades, pid, err, token }) => {
if (err) {
return <Error value={err}></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 { t } = useTranslation();
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>&nbsp;</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,74 @@
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 { getDetails } from "@services/api";
import config from "../../../next-i18next.config.js";
export async function getServerSideProps({ query: { pid }, locale }) {
const [res, translations] = await Promise.all([
getDetails(
pid,
(res) => ({ ok: true, ...res }),
(err) => ({ ok: false, err })
),
serverSideTranslations(locale, [], config),
]);
if (!res.ok) {
return { props: { err: res.err, ...translations } };
}
return {
props: {
...translations,
invitationOnly: res.on_invitation_only,
restrictResults: res.restrict_results,
candidates: res.candidates.map((name, i) => ({ id: i, label: name })),
title: res.title,
numGrades: res.num_grades,
pid: pid,
},
};
}
const VoteSuccess = ({ title, invitationOnly, pid }) => {
const { t } = useTranslation();
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;

@ -1 +0,0 @@
/* /index.html 200

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

@ -1,41 +0,0 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

@ -1,119 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link
rel="apple-touch-icon"
sizes="57x57"
href="%PUBLIC_URL%/favicon/apple-icon-57x57.png"
/>
<link
rel="apple-touch-icon"
sizes="60x60"
href="%PUBLIC_URL%/favicon/apple-icon-60x60.png"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="%PUBLIC_URL%/favicon/apple-icon-72x72.png"
/>
<link
rel="apple-touch-icon"
sizes="76x76"
href="%PUBLIC_URL%/favicon/apple-icon-76x76.png"
/>
<link
rel="apple-touch-icon"
sizes="114x114"
href="%PUBLIC_URL%/favicon/apple-icon-114x114.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="%PUBLIC_URL%/favicon/apple-icon-120x120.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="%PUBLIC_URL%/favicon/apple-icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="%PUBLIC_URL%/favicon/apple-icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/favicon/apple-icon-180x180.png"
/>
<link
rel="icon"
type="image/png"
sizes="192x192"
href="%PUBLIC_URL%/favicon/android-icon-192x192.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="96x96"
href="%PUBLIC_URL%/favicon/favicon-96x96.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon/favicon-16x16.png"
/>
<meta name="msapplication-TileColor" content="#ffffff" />
<meta
name="msapplication-TileImage"
content="%PUBLIC_URL%/favicon/ms-icon-144x144.png"
/>
<meta name="theme-color" content="#ffffff" />
<title>Application Jugement Majoritaire</title>
<meta property="og:url" content="https://app.mieuxvoter.fr" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Application Jugement Majoritaire" />
<meta
property="og:description"
content="Simple et gratuit : organisez un vote avec le Jugement Majoritaire."
/>
<meta
property="og:image"
content="https://app.mieuxvoter.fr/app-mieux-voter.png"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@ -1,16 +1,17 @@
{ {
"title": "Plattform mit Mehrheitswahl",
"Homepage": "Homepage", "Homepage": "Homepage",
"Source code": "Quellcode", "Source code": "Quellcode",
"Who are we?": "Wer wir sind?", "Who are we?": "Wer wir sind?",
"Privacy policy": "Datenschutzerklärung", "Privacy policy": "Datenschutzerklärung",
"Legal notices": "Rechtliche Hinweise", "resource.legalNotices": "Rechtliche Hinweise",
"FAQ": "FAQ", "FAQ": "FAQ",
"Need help?": "Brauchen Sie Hilfe?", "Need help?": "Brauchen Sie Hilfe?",
"BetterVote": " BetterVote", "BetterVote": " BetterVote",
"Voting platform": "Wahlplattform", "Voting platform": "Wahlplattform",
"Majority Judgment": " Mehrheitswahl ", "Majority Judgment": " Mehrheitswahl ",
"Start an election": "Eine Wahl beginnen", "Start an election": "Eine Wahl beginnen",
"Candidate/proposal name...": "Name des Kandidaten/Abstimmungsvorschlags", "resource.candidatePlaceholder": "Name des Kandidaten/Abstimmungsvorschlags",
"Delete?": "Löschen?", "Delete?": "Löschen?",
"Are you sure to delete": "Sind Sie sich sicher, dass Sie dies löschen möchten?", "Are you sure to delete": "Sind Sie sich sicher, dass Sie dies löschen möchten?",
"the row": "die Zeile", "the row": "die Zeile",
@ -57,8 +58,8 @@
"You can start another election.": "Sie können eine neue Umfrage starten.", "You can start another election.": "Sie können eine neue Umfrage starten.",
"Go back to homepage": "Zurück zur Hompage", "Go back to homepage": "Zurück zur Hompage",
"You have to judge every candidate/proposal!": "Sie müssen jeden Kandidaten/Abstimmungsvorschlag bewerten!", "You have to judge every candidate/proposal!": "Sie müssen jeden Kandidaten/Abstimmungsvorschlag bewerten!",
"Your participation was recorded with success!": " Ihre Teilnahme wurde gespeichert!", "resource.voteSuccess": " Ihre Teilnahme wurde gespeichert!",
"Thanks for your participation.": " Vielen Dank für Ihre Teilnahme.", "resource.thanks": " Vielen Dank für Ihre Teilnahme.",
"Support us !": "Unterstützen Sie uns!", "Support us !": "Unterstützen Sie uns!",
"PayPal - The safer, easier way to pay online!": "PayPal - Die sicherere und einfachere Art, online zu bezahlen!", "PayPal - The safer, easier way to pay online!": "PayPal - Die sicherere und einfachere Art, online zu bezahlen!",
"Number of votes:": "Anzahl der Stimmen:", "Number of votes:": "Anzahl der Stimmen:",

@ -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! 🤗"
}

@ -1,9 +1,10 @@
{ {
"title": "Application for Majority Judgment",
"Homepage": "Homepage", "Homepage": "Homepage",
"Source code": "Source code", "Source code": "Source code",
"Who are we?": "Who are we?", "Who are we?": "Who are we?",
"Privacy policy": "Privacy policy", "Privacy policy": "Privacy policy",
"Legal notices": "Legal notices", "resource.legalNotices": "Legal notices",
"FAQ": "FAQ", "FAQ": "FAQ",
"Need help?": "Need help?", "Need help?": "Need help?",
"BetterVote": "BetterVote", "BetterVote": "BetterVote",
@ -14,16 +15,16 @@
"Delete?": "Confirm deletion", "Delete?": "Confirm deletion",
"Are you sure to delete": "Are you sure you want to delete", "Are you sure to delete": "Are you sure you want to delete",
"the row": "the row", "the row": "the row",
"Write here your question or introduce simple your election (250 characters max.)": "Write here your question or describe your vote (max. 250 characters)", "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)", "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.", "Please add at least 2 candidates.": "Please add at least 2 candidates.",
"Question of the election": "Question of the vote", "resource.questionLabel": "Question of the vote",
"Write here the question of your election": "Write here your question or describe your vote", "resource.writeQuestion": "Write here your question or describe your vote",
"For example:": "For example:", "resource.eg": "For example:",
"For the role of my representative, I judge this candidate...": "For the role of my representative, I think this candidate is...", "resource.exampleQuestion": "For the role of my representative, I think this candidate is...",
"Candidates/Proposals": "Candidates/Proposals",
"Add a proposal": "Add a candidate/proposal", "Add a proposal": "Add a candidate/proposal",
"Advanced options": "Advanced options", "resource.advancedOptions": "Advanced options",
"Starting date": "Start date", "Starting date": "Start date",
"Ending date": "End date", "Ending date": "End date",
"Defined period": "Defined period", "Defined period": "Defined period",
@ -40,7 +41,7 @@
"Confirm your vote": "Confirm your vote", "Confirm your vote": "Confirm your vote",
"The form contains no address.": "The form contains no email addresses.", "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", "The election will be opened to anyone with the link": "The vote will be open to anyone with the link",
"Start the election": "Start the vote", "resource.startVote": "Start the vote",
"Cancel": "Cancel", "Cancel": "Cancel",
"Confirm": "Confirm", "Confirm": "Confirm",
"Successful election creation!": "Vote successfully created!", "Successful election creation!": "Vote successfully created!",
@ -50,15 +51,14 @@
"Keep these links carefully": "Keep these links carefully", "Keep these links carefully": "Keep these links carefully",
"Participate now!": "Participate now!", "Participate now!": "Participate now!",
"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.", "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.",
"Simple and free: organize an election with Majority Judgment.": "Simple and free: organise a vote with Majority Judgment.", "resource.start": "Start",
"Start": "Start", "resource.noAds": "No advertising or ad cookies",
"No advertising or ad cookies": "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.", "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.", "You can start another election.": "You can start another vote.",
"Go back to homepage": "Go back to homepage", "Go back to homepage": "Go back to homepage",
"You have to judge every candidate/proposal!": "Please assess every candidate/proposal.", "You have to judge every candidate/proposal!": "Please assess every candidate/proposal.",
"Your participation was recorded with success!": "Your participation was successfully recorded!", "resource.voteSuccess": "Your participation was successfully recorded!",
"Thanks for your participation.": "Thank your for your participation.", "resource.thanks": "Thank your for your participation.",
"Ending date:": "Ending date:", "Ending date:": "Ending date:",
"Excellent": "Excellent", "Excellent": "Excellent",
"Very good": "Very good", "Very good": "Very good",

@ -1,16 +1,17 @@
{ {
"title": "Plataforma de Juicio Mayoritario",
"Homepage": "Página de inicio", "Homepage": "Página de inicio",
"Source code": "Código fuente", "Source code": "Código fuente",
"Who are we": "Quiénes somos", "Who are we": "Quiénes somos",
"Privacy policy": "Política de privacidad", "Privacy policy": "Política de privacidad",
"Legal notices": "Avisos legales", "resource.legalNotices": "Avisos legales",
"FAQ": "FAQ", "FAQ": "FAQ",
"Need help?": "¿Necesitas ayuda?", "Need help?": "¿Necesitas ayuda?",
"BetterVote": "VotarMejor", "BetterVote": "VotarMejor",
"Voting platform": "Plataforma de votación", "Voting platform": "Plataforma de votación",
"Majority Judgment": "Juicio Mayoritario", "Majority Judgment": "Juicio Mayoritario",
"Start an election": "Iniciar una elección", "Start an election": "Iniciar una elección",
"Candidate/proposal name...": "Nombre del(la) candidato(a)/propuesta...", "resource.candidatePlaceholder": "Nombre del(la) candidato(a)/propuesta...",
"Delete?": "Borrar?", "Delete?": "Borrar?",
"Are you sure to delete": "Estás seguro de querer borrar", "Are you sure to delete": "Estás seguro de querer borrar",
"the row": "la fila", "the row": "la fila",
@ -57,8 +58,8 @@
"You can start another election.": "Puedes empezar otra elección", "You can start another election.": "Puedes empezar otra elección",
"Go back to homepage": "Vuelve a la página de inicio", "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", "You have to judge every candidate/proposal!": "¡Tienes que evaluar a todos(as) los(as) candidatos(as)/propuestas",
"Your participation was recorded with success!": "¡Su participación fue registrada con éxito!", "resource.voteSuccess": "¡Su participación fue registrada con éxito!",
"Thanks for your participation.": "Muchas gracias por participar", "resource.thanks": "Muchas gracias por participar",
"Ending date:": "Fecha de finalización:", "Ending date:": "Fecha de finalización:",
"Excellent": "Excelente", "Excellent": "Excelente",
"Very good": "Muy bien", "Very good": "Muy bien",

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

@ -6,7 +6,7 @@
"Voting platform": "Plateforme de vote", "Voting platform": "Plateforme de vote",
"Majority Judgment": "Jugement Majoritaire", "Majority Judgment": "Jugement Majoritaire",
"Start an election": "Lancer une élection", "Start an election": "Lancer une élection",
"Candidate/proposal name...": "Name du candidat/proposition", "resource.candidatePlaceholder": "Name du candidat/proposition",
"Delete?": "Supprimer ?", "Delete?": "Supprimer ?",
"Are you sure to delete": "Êtes-vous sûr(e) de supprimer", "Are you sure to delete": "Êtes-vous sûr(e) de supprimer",
"the row": "la ligne", "the row": "la ligne",
@ -48,6 +48,6 @@
"You can start another election.": "Vous pouvez démarrer une autre élection.", "You can start another election.": "Vous pouvez démarrer une autre élection.",
"Go back to homepage": "Revenir à la page d'accueil", "Go back to homepage": "Revenir à la page d'accueil",
"You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !", "You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !",
"Your participation was recorded with success!": "Votre participation a été enregistrée avec succès !", "resource.voteSuccess": "Votre participation a été enregistrée avec succès !",
"Thanks for your participation.": "Merci de votre participation." "resource.thanks": "Merci de votre participation."
} }

@ -1,29 +1,29 @@
{ {
"title": "Application au Jugement Majoritaire",
"Homepage": "Accueil", "Homepage": "Accueil",
"Source code": "Code source", "Source code": "Code source",
"Who are we?": "Qui sommes-nous ?", "Who are we?": "Qui sommes-nous ?",
"Privacy policy": "Politique de confidentialité", "Privacy policy": "Politique de confidentialité",
"Legal notices": "Mentions légales", "resource.legalNotices": "Mentions légales",
"FAQ": "FAQ", "FAQ": "FAQ",
"Need help?": "Besoin d'aide ?", "Need help?": "Besoin d'aide ?",
"BetterVote": "MieuxVoter", "BetterVote": "MieuxVoter",
"Voting platform": "Plateforme de vote", "Voting platform": "Plateforme de vote",
"Majority Judgment": "Jugement Majoritaire", "Majority Judgment": "Jugement Majoritaire",
"Start an election": "Lancer un vote", "Start an election": "Lancer un vote",
"Candidate/proposal name...": "Nom du candidat/proposition", "resource.candidatePlaceholder": "Nom du candidat/proposition",
"Delete?": "Supprimer ?", "Delete?": "Supprimer ?",
"Are you sure to delete": "Êtes-vous sûr(e) de supprimer", "Are you sure to delete": "Êtes-vous sûr(e) de supprimer",
"the row": "la ligne", "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 vote (250 caractères max.)", "resource.writeQuestion": "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.)", "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.", "Please add at least 2 candidates.": "Merci d'ajouter au moins 2 candidats.",
"Question of the election": "Question de votre vote", "resource.questionLabel": "Question de votre vote",
"Write here the question of your election": "Ecrire ici la question de votre vote", "resource.writeQuestion": "Ecrire ici la question de votre vote",
"For example:": "Par exemple", "resource.eg": "Par exemple",
"For the role of my representative, I judge this candidate...": "Pour être mon représentant, je juge ce candidat...", "resource.exampleQuestion": "Pour être mon représentant, je juge ce candidat...",
"Candidates/Proposals": "Candidats/Propositions",
"Add a proposal": "Ajouter une proposition", "Add a proposal": "Ajouter une proposition",
"Advanced options": "Options avancées", "resource.advancedOptions": "Options avancées",
"Starting date": "Date de début", "Starting date": "Date de début",
"Ending date": "Date de fin ", "Ending date": "Date de fin ",
"Defined period" : "Période définie", "Defined period" : "Période définie",
@ -40,7 +40,7 @@
"Confirm your vote": "Confirmer votre vote", "Confirm your vote": "Confirmer votre vote",
"The form contains no address.": "Aucune adresse email n'a été ajoutée.", "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", "The election will be opened to anyone with the link": "Le vote sera accessible à tous ceux qui disposent du lien",
"Start the election": "Démarrer le vote", "resource.startVote": "Démarrer le vote",
"Cancel": "Annuler", "Cancel": "Annuler",
"Confirm": "Valider", "Confirm": "Valider",
"Successful election creation!": "Le vote a été créé avec succès !", "Successful election creation!": "Le vote a été créé avec succès !",
@ -50,15 +50,14 @@
"Keep these links carefully": "Gardez ces liens précieusement", "Keep these links carefully": "Gardez ces liens précieusement",
"Participate now!": "Participez maintenant !", "Participate now!": "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.", "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 gratuit : organisez un vote avec le Jugement Majoritaire", "resource.start": "Démarrer",
"Start": "Démarrer", "resource.noAds": "Pas de publicités, ni de cookies publicitaires",
"No advertising or ad cookies": "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.", "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.", "You can start another election.": "Vous pouvez démarrer une autre vote.",
"Go back to homepage": "Revenir à la page d'accueil", "Go back to homepage": "Revenir à la page d'accueil",
"You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !", "You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !",
"Your participation was recorded with success!": "Votre participation a été enregistrée avec succès !", "resource.voteSuccess": "Votre participation a été enregistrée avec succès !",
"Thanks for your participation.": "Merci de votre participation.", "resource.thanks": "Merci de votre participation.",
"Excellent": "Excellent", "Excellent": "Excellent",
"Very good": "Très bien", "Very good": "Très bien",
"Good": "Bien", "Good": "Bien",

@ -1,16 +1,17 @@
{ {
"title": "Решение Большинства",
"Homepage": "Главная страница", "Homepage": "Главная страница",
"Source code": "Исходный код", "Source code": "Исходный код",
"Who are we?": "Кто мы?", "Who are we?": "Кто мы?",
"Privacy policy": "Политика конфиденциальности", "Privacy policy": "Политика конфиденциальности",
"Legal notices": "Официальные уведомления", "resource.legalNotices": "Официальные уведомления",
"FAQ": "Часто задаваемые вопросы", "FAQ": "Часто задаваемые вопросы",
"Need help?": "Нужна помощь?", "Need help?": "Нужна помощь?",
"BetterVote": "BetterVote", "BetterVote": "BetterVote",
"Voting platform": "Платформа голосования", "Voting platform": "Платформа голосования",
"Majority Judgment": "Решение Большинства", "Majority Judgment": "Решение Большинства",
"Start an election": "Создать голосование", "Start an election": "Создать голосование",
"Candidate/proposal name...": "Имя кандидата/предлжения...", "resource.candidatePlaceholder": "Имя кандидата/предлжения...",
"Delete?": "Удалить?", "Delete?": "Удалить?",
"Are you sure to delete": "Вы уверены в удалении", "Are you sure to delete": "Вы уверены в удалении",
"the row": "ряд", "the row": "ряд",
@ -57,8 +58,8 @@
"You can start another election.": "Вы можете создать другое голосование.", "You can start another election.": "Вы можете создать другое голосование.",
"Go back to homepage": "Вернуться на главную", "Go back to homepage": "Вернуться на главную",
"You have to judge every candidate/proposal!": "Вам нужно проголосовать за каждого кандидата/предложение!", "You have to judge every candidate/proposal!": "Вам нужно проголосовать за каждого кандидата/предложение!",
"Your participation was recorded with success!": "Ваш голос был учтен!", "resource.voteSuccess": "Ваш голос был учтен!",
"Thanks for your participation.": "Спасибо за ваше участие.", "resource.thanks": "Спасибо за ваше участие.",
"Ending date:": "Дата окончания:", "Ending date:": "Дата окончания:",
"Excellent": "Отлично", "Excellent": "Отлично",
"Very good": "Очень хорошо", "Very good": "Очень хорошо",

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<path class="st0" d="M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200s200-89.54,200-200S310.46,0,200,0z M249.67,210.48
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35c0.5,1.54,1.06,3.06,1.64,4.71
c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13
C258.54,185.81,254.12,198.15,249.67,210.48z M263.37,154.73c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01
c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32
c-0.96,3.36-2.37,4.44-5.83,4.32c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45
c-4.73-16.28-9.75-32.48-14.68-48.71c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98
c0.03,3.35-0.68,4.69-4.34,4.55c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47
c-0.01-3.38,1.15-4.16,4.29-4.11c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67c0.86-2.71,2.07-3.64,4.9-3.61
c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64C263.28,112.41,263.29,133.57,263.37,154.73z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#2A43A0;}
</style>
<path class="st0" d="M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200s200-89.54,200-200S310.46,0,200,0z M249.67,210.48
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35c0.5,1.54,1.06,3.06,1.64,4.71
c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13
C258.54,185.81,254.12,198.15,249.67,210.48z M263.37,154.73c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01
c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32
c-0.96,3.36-2.37,4.44-5.83,4.32c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45
c-4.73-16.28-9.75-32.48-14.68-48.71c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98
c0.03,3.35-0.68,4.69-4.34,4.55c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47
c-0.01-3.38,1.15-4.16,4.29-4.11c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67c0.86-2.71,2.07-3.64,4.9-3.61
c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64C263.28,112.41,263.29,133.57,263.37,154.73z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#2A43A0;}
.st1{fill:#EE455B;}
.st2{fill:#FDFDFE;}
</style>
<circle id="BACKGROUND" class="st0" cx="200" cy="200" r="200"/>
<path id="V" class="st1" d="M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13c-4.67,13.01-9.09,25.35-13.54,37.68
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6z"/>
<path id="M" class="st2" d="M200.14,153.15c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67
c0.86-2.71,2.07-3.64,4.9-3.61c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59
c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32c-0.96,3.36-2.37,4.44-5.83,4.32
c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98c0.03,3.35-0.68,4.69-4.34,4.55
c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47c-0.01-3.38,1.15-4.16,4.29-4.11
c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
C198.48,149.05,199.11,150.5,200.14,153.15z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000000;}
</style>
<path class="st0" d="M200,8c25.92,0,51.07,5.08,74.73,15.09c22.86,9.67,43.4,23.51,61.03,41.15c17.64,17.64,31.48,38.17,41.15,61.03
C386.92,148.93,392,174.08,392,200s-5.08,51.07-15.09,74.73c-9.67,22.86-23.51,43.4-41.15,61.03
c-17.64,17.64-38.17,31.48-61.03,41.15C251.07,386.92,225.92,392,200,392s-51.07-5.08-74.73-15.09
c-22.86-9.67-43.4-23.51-61.03-41.15c-17.64-17.64-31.48-38.17-41.15-61.03C13.08,251.07,8,225.92,8,200s5.08-51.07,15.08-74.73
c9.67-22.86,23.51-43.4,41.15-61.03c17.64-17.64,38.17-31.48,61.03-41.15C148.93,13.08,174.08,8,200,8 M157.57,79.72
c-5.34,0-10.77-0.04-16.61-0.13c-0.13,0-0.25,0-0.37,0c-1.39,0-5.63,0-8.83,3.21c-3.23,3.24-3.21,7.53-3.21,8.93
c0.11,32.12,0.12,65.57,0,99.42c0,1.37-0.02,5.55,3.15,8.73c3.12,3.13,7.15,3.13,8.47,3.13c0.21,0,0.42,0,0.63-0.01
c4.16-0.11,7.78-0.17,11.09-0.17c3.53,0,6.76,0.06,9.86,0.19c0.29,0.01,0.57,0.02,0.84,0.02c3.59,0,6.49-1.08,8.61-3.22
c3.27-3.29,3.23-7.73,3.22-9.4c-0.02-2.5-0.04-5.01-0.05-7.53c0.22,0.74,0.43,1.48,0.65,2.22c1.97,6.79,6.32,10.23,12.91,10.23
c0.26,0,0.54-0.01,0.82-0.02c3.58-0.14,7.29-0.22,11.03-0.22c3.68,0,7.5,0.07,11.35,0.21c0.27,0.01,0.54,0.01,0.8,0.01
c1.34,0,2.79-0.15,4.24-0.56c-7.07,21.09-14.23,42.25-21.28,63.04c-3.54-10.78-7.15-21.8-10.62-32.69
c-2.45-7.67-7.68-8.82-10.58-8.82c-1.62,0-3.32,0.35-5.2,1.08c-6.4,2.48-13.03,4.76-19.71,6.77c-3.66,1.1-6.22,3.07-7.61,5.86
c-2.1,4.2-0.59,8.28,0.05,10.02c9.7,26.29,18.41,50.2,26.63,73.09c2.17,6.04,6.69,9.24,13.07,9.24c0.16,0,0.31,0,0.47-0.01
c4.28-0.09,8.89-0.18,13.54-0.18c4.19,0,7.93,0.07,11.45,0.21c0.32,0.01,0.64,0.02,0.94,0.02c10.55,0,13.4-8.01,14.33-10.64
c8.58-24.11,17.45-48.58,26.02-72.25c3.18-8.77,6.35-17.54,9.52-26.31c3.04-8.42,6.07-16.85,9.17-25.5c1.44-4.01,2.9-8.07,4.38-12.2
c0.88-2.45,0.51-5.18-0.99-7.31c-1.05-1.49-2.55-2.55-4.25-3.06c1.05-0.49,1.98-1.15,2.81-1.98c3.08-3.09,3.07-7.13,3.06-8.46
c-0.08-23.52-0.08-44.26,0.01-63.42c0.01-1.34,0.03-5.41-3.11-8.55c-3.12-3.13-7.15-3.13-8.47-3.13l-0.24,0
c-5.84,0.07-11.56,0.1-17,0.1c-5.74,0-11.38-0.04-16.79-0.11l-0.27,0c-4.32,0-9.97,1.6-12.37,9.2c-2.55,8.06-5.3,16.25-7.97,24.17
c-0.94,2.79-1.88,5.58-2.81,8.38c-0.78,2.35-1.57,4.7-2.36,7.07c-4.33-12.9-8.75-26.12-12.99-39.17c-2.08-6.41-6.43-9.66-12.93-9.66
c-0.12,0-0.25,0-0.38,0C168.53,79.67,163.11,79.72,157.57,79.72 M230.91,164.86c0.33,0.18,0.68,0.35,1.03,0.49
c-0.46,0.14-0.91,0.31-1.33,0.49C230.71,165.51,230.81,165.19,230.91,164.86 M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200
s200-89.54,200-200S310.46,0,200,0L200,0z M157.57,87.72c5.42,0,10.84-0.04,16.26-0.14c0.08,0,0.16,0,0.24,0
c3.08,0,4.35,1.14,5.32,4.13c6.06,18.66,12.35,37.24,18.59,55.84c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28
c3.63-10.88,7.36-21.73,10.82-32.67c0.84-2.66,2.02-3.61,4.74-3.61c0.05,0,0.11,0,0.16,0c5.63,0.08,11.26,0.11,16.9,0.11
c5.7,0,11.4-0.04,17.09-0.1c0.05,0,0.1,0,0.15,0c2.66,0,3.59,0.82,3.58,3.65c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.45-0.61,3.48-3.15,3.48c-0.06,0-0.13,0-0.2,0c-3.95-0.08-7.91-0.13-11.86-0.13c-3.71,0-7.42,0.04-11.13,0.14
c-0.09,0-0.17,0-0.26,0c-2.86,0-3.26-1.36-3.22-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91
c-4.76,15.41-9.49,30.83-13.94,46.32c-0.91,3.19-2.23,4.33-5.32,4.33c-0.16,0-0.33,0-0.51-0.01c-3.87-0.14-7.76-0.22-11.64-0.22
c-3.79,0-7.57,0.07-11.35,0.22c-0.17,0.01-0.33,0.01-0.49,0.01c-3.2,0-4.33-1.38-5.22-4.47c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,17.99-0.08,35.99,0.07,53.98c0.03,3.18-0.62,4.56-3.82,4.56
c-0.17,0-0.34,0-0.52-0.01c-3.39-0.14-6.79-0.19-10.18-0.19c-3.77,0-7.54,0.07-11.3,0.17c-0.14,0-0.28,0.01-0.42,0.01
c-2.65,0-3.63-0.79-3.62-3.83c0.11-33.16,0.11-66.32,0-99.47c-0.01-3.29,1.09-4.11,4.04-4.11c0.08,0,0.17,0,0.25,0
C146.42,87.67,152,87.72,157.57,87.72L157.57,87.72z M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.26-3.8,2.8-5.65,6.8-5.65c0.26,0,0.52,0.01,0.8,0.02c3.39,0.18,6.79,0.24,10.24,0.24c4.95,0,10-0.11,15.28-0.11
c-4.67,13.01-9.09,25.35-13.54,37.68c-11.87,32.86-23.84,65.69-35.55,98.6c-1.33,3.74-2.98,5.32-6.79,5.32c-0.2,0-0.41,0-0.62-0.01
c-3.92-0.16-7.85-0.22-11.78-0.22c-4.57,0-9.14,0.08-13.71,0.18c-0.1,0-0.2,0-0.3,0c-2.92,0-4.48-0.97-5.54-3.94
c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45c6.85-2.06,13.63-4.38,20.29-6.97
c0.93-0.36,1.68-0.54,2.31-0.54c1.53,0,2.27,1.09,2.96,3.25c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6L194.36,281.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#2943A0;}
</style>
<path class="st0" d="M200,8c25.92,0,51.07,5.08,74.73,15.09c22.86,9.67,43.4,23.51,61.03,41.15c17.64,17.64,31.48,38.17,41.15,61.03
C386.92,148.93,392,174.08,392,200s-5.08,51.07-15.09,74.73c-9.67,22.86-23.51,43.4-41.15,61.03
c-17.64,17.64-38.17,31.48-61.03,41.15C251.07,386.92,225.92,392,200,392s-51.07-5.08-74.73-15.09
c-22.86-9.67-43.4-23.51-61.03-41.15c-17.64-17.64-31.48-38.17-41.15-61.03C13.08,251.07,8,225.92,8,200s5.08-51.07,15.08-74.73
c9.67-22.86,23.51-43.4,41.15-61.03c17.64-17.64,38.17-31.48,61.03-41.15C148.93,13.08,174.08,8,200,8 M157.57,79.72
c-5.34,0-10.77-0.04-16.61-0.13c-0.13,0-0.25,0-0.37,0c-1.39,0-5.63,0-8.83,3.21c-3.23,3.24-3.21,7.53-3.21,8.93
c0.11,32.12,0.12,65.57,0,99.42c0,1.37-0.02,5.55,3.15,8.73c3.12,3.13,7.15,3.13,8.47,3.13c0.21,0,0.42,0,0.63-0.01
c4.16-0.11,7.78-0.17,11.09-0.17c3.53,0,6.76,0.06,9.86,0.19c0.29,0.01,0.57,0.02,0.84,0.02c3.59,0,6.49-1.08,8.61-3.22
c3.27-3.29,3.23-7.73,3.22-9.4c-0.02-2.5-0.04-5.01-0.05-7.53c0.22,0.74,0.43,1.48,0.65,2.22c1.97,6.79,6.32,10.23,12.91,10.23
c0.26,0,0.54-0.01,0.82-0.02c3.58-0.14,7.29-0.22,11.03-0.22c3.68,0,7.5,0.07,11.35,0.21c0.27,0.01,0.54,0.01,0.8,0.01
c1.34,0,2.79-0.15,4.24-0.56c-7.07,21.09-14.23,42.25-21.28,63.04c-3.54-10.78-7.15-21.8-10.62-32.69
c-2.45-7.67-7.68-8.82-10.58-8.82c-1.62,0-3.32,0.35-5.2,1.08c-6.4,2.48-13.03,4.76-19.71,6.77c-3.66,1.1-6.22,3.07-7.61,5.86
c-2.1,4.2-0.59,8.28,0.05,10.02c9.7,26.29,18.41,50.2,26.63,73.09c2.17,6.04,6.69,9.24,13.07,9.24c0.16,0,0.31,0,0.47-0.01
c4.28-0.09,8.89-0.18,13.54-0.18c4.19,0,7.93,0.07,11.45,0.21c0.32,0.01,0.64,0.02,0.94,0.02c10.55,0,13.4-8.01,14.33-10.64
c8.58-24.11,17.45-48.58,26.02-72.25c3.18-8.77,6.35-17.54,9.52-26.31c3.04-8.42,6.07-16.85,9.17-25.5c1.44-4.01,2.9-8.07,4.38-12.2
c0.88-2.45,0.51-5.18-0.99-7.31c-1.05-1.49-2.55-2.55-4.25-3.06c1.05-0.49,1.98-1.15,2.81-1.98c3.08-3.09,3.07-7.13,3.06-8.46
c-0.08-23.52-0.08-44.26,0.01-63.42c0.01-1.34,0.03-5.41-3.11-8.55c-3.12-3.13-7.15-3.13-8.47-3.13l-0.24,0
c-5.84,0.07-11.56,0.1-17,0.1c-5.74,0-11.38-0.04-16.79-0.11l-0.27,0c-4.32,0-9.97,1.6-12.37,9.2c-2.55,8.06-5.3,16.25-7.97,24.17
c-0.94,2.79-1.88,5.58-2.81,8.38c-0.78,2.35-1.57,4.7-2.36,7.07c-4.33-12.9-8.75-26.12-12.99-39.17c-2.08-6.41-6.43-9.66-12.93-9.66
c-0.12,0-0.25,0-0.38,0C168.53,79.67,163.11,79.72,157.57,79.72 M230.91,164.86c0.33,0.18,0.68,0.35,1.03,0.49
c-0.46,0.14-0.91,0.31-1.33,0.49C230.71,165.51,230.81,165.19,230.91,164.86 M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200
s200-89.54,200-200S310.46,0,200,0L200,0z M157.57,87.72c5.42,0,10.84-0.04,16.26-0.14c0.08,0,0.16,0,0.24,0
c3.08,0,4.35,1.14,5.32,4.13c6.06,18.66,12.35,37.24,18.59,55.84c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28
c3.63-10.88,7.36-21.73,10.82-32.67c0.84-2.66,2.02-3.61,4.74-3.61c0.05,0,0.11,0,0.16,0c5.63,0.08,11.26,0.11,16.9,0.11
c5.7,0,11.4-0.04,17.09-0.1c0.05,0,0.1,0,0.15,0c2.66,0,3.59,0.82,3.58,3.65c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.45-0.61,3.48-3.15,3.48c-0.06,0-0.13,0-0.2,0c-3.95-0.08-7.91-0.13-11.86-0.13c-3.71,0-7.42,0.04-11.13,0.14
c-0.09,0-0.17,0-0.26,0c-2.86,0-3.26-1.36-3.22-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91
c-4.76,15.41-9.49,30.83-13.94,46.32c-0.91,3.19-2.23,4.33-5.32,4.33c-0.16,0-0.33,0-0.51-0.01c-3.87-0.14-7.76-0.22-11.64-0.22
c-3.79,0-7.57,0.07-11.35,0.22c-0.17,0.01-0.33,0.01-0.49,0.01c-3.2,0-4.33-1.38-5.22-4.47c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,17.99-0.08,35.99,0.07,53.98c0.03,3.18-0.62,4.56-3.82,4.56
c-0.17,0-0.34,0-0.52-0.01c-3.39-0.14-6.79-0.19-10.18-0.19c-3.77,0-7.54,0.07-11.3,0.17c-0.14,0-0.28,0.01-0.42,0.01
c-2.65,0-3.63-0.79-3.62-3.83c0.11-33.16,0.11-66.32,0-99.47c-0.01-3.29,1.09-4.11,4.04-4.11c0.08,0,0.17,0,0.25,0
C146.42,87.67,152,87.72,157.57,87.72L157.57,87.72z M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.26-3.8,2.8-5.65,6.8-5.65c0.26,0,0.52,0.01,0.8,0.02c3.39,0.18,6.79,0.24,10.24,0.24c4.95,0,10-0.11,15.28-0.11
c-4.67,13.01-9.09,25.35-13.54,37.68c-11.87,32.86-23.84,65.69-35.55,98.6c-1.33,3.74-2.98,5.32-6.79,5.32c-0.2,0-0.41,0-0.62-0.01
c-3.92-0.16-7.85-0.22-11.78-0.22c-4.57,0-9.14,0.08-13.71,0.18c-0.1,0-0.2,0-0.3,0c-2.92,0-4.48-0.97-5.54-3.94
c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45c6.85-2.06,13.63-4.38,20.29-6.97
c0.93-0.36,1.68-0.54,2.31-0.54c1.53,0,2.27,1.09,2.96,3.25c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6L194.36,281.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#ffffff;}
</style>
<path class="st0" d="M200,8c25.92,0,51.07,5.08,74.73,15.09c22.86,9.67,43.4,23.51,61.03,41.15c17.64,17.64,31.48,38.17,41.15,61.03
C386.92,148.93,392,174.08,392,200s-5.08,51.07-15.09,74.73c-9.67,22.86-23.51,43.4-41.15,61.03
c-17.64,17.64-38.17,31.48-61.03,41.15C251.07,386.92,225.92,392,200,392s-51.07-5.08-74.73-15.09
c-22.86-9.67-43.4-23.51-61.03-41.15c-17.64-17.64-31.48-38.17-41.15-61.03C13.08,251.07,8,225.92,8,200s5.08-51.07,15.08-74.73
c9.67-22.86,23.51-43.4,41.15-61.03c17.64-17.64,38.17-31.48,61.03-41.15C148.93,13.08,174.08,8,200,8 M157.57,79.72
c-5.34,0-10.77-0.04-16.61-0.13c-0.13,0-0.25,0-0.37,0c-1.39,0-5.63,0-8.83,3.21c-3.23,3.24-3.21,7.53-3.21,8.93
c0.11,32.12,0.12,65.57,0,99.42c0,1.37-0.02,5.55,3.15,8.73c3.12,3.13,7.15,3.13,8.47,3.13c0.21,0,0.42,0,0.63-0.01
c4.16-0.11,7.78-0.17,11.09-0.17c3.53,0,6.76,0.06,9.86,0.19c0.29,0.01,0.57,0.02,0.84,0.02c3.59,0,6.49-1.08,8.61-3.22
c3.27-3.29,3.23-7.73,3.22-9.4c-0.02-2.5-0.04-5.01-0.05-7.53c0.22,0.74,0.43,1.48,0.65,2.22c1.97,6.79,6.32,10.23,12.91,10.23
c0.26,0,0.54-0.01,0.82-0.02c3.58-0.14,7.29-0.22,11.03-0.22c3.68,0,7.5,0.07,11.35,0.21c0.27,0.01,0.54,0.01,0.8,0.01
c1.34,0,2.79-0.15,4.24-0.56c-7.07,21.09-14.23,42.25-21.28,63.04c-3.54-10.78-7.15-21.8-10.62-32.69
c-2.45-7.67-7.68-8.82-10.58-8.82c-1.62,0-3.32,0.35-5.2,1.08c-6.4,2.48-13.03,4.76-19.71,6.77c-3.66,1.1-6.22,3.07-7.61,5.86
c-2.1,4.2-0.59,8.28,0.05,10.02c9.7,26.29,18.41,50.2,26.63,73.09c2.17,6.04,6.69,9.24,13.07,9.24c0.16,0,0.31,0,0.47-0.01
c4.28-0.09,8.89-0.18,13.54-0.18c4.19,0,7.93,0.07,11.45,0.21c0.32,0.01,0.64,0.02,0.94,0.02c10.55,0,13.4-8.01,14.33-10.64
c8.58-24.11,17.45-48.58,26.02-72.25c3.18-8.77,6.35-17.54,9.52-26.31c3.04-8.42,6.07-16.85,9.17-25.5c1.44-4.01,2.9-8.07,4.38-12.2
c0.88-2.45,0.51-5.18-0.99-7.31c-1.05-1.49-2.55-2.55-4.25-3.06c1.05-0.49,1.98-1.15,2.81-1.98c3.08-3.09,3.07-7.13,3.06-8.46
c-0.08-23.52-0.08-44.26,0.01-63.42c0.01-1.34,0.03-5.41-3.11-8.55c-3.12-3.13-7.15-3.13-8.47-3.13l-0.24,0
c-5.84,0.07-11.56,0.1-17,0.1c-5.74,0-11.38-0.04-16.79-0.11l-0.27,0c-4.32,0-9.97,1.6-12.37,9.2c-2.55,8.06-5.3,16.25-7.97,24.17
c-0.94,2.79-1.88,5.58-2.81,8.38c-0.78,2.35-1.57,4.7-2.36,7.07c-4.33-12.9-8.75-26.12-12.99-39.17c-2.08-6.41-6.43-9.66-12.93-9.66
c-0.12,0-0.25,0-0.38,0C168.53,79.67,163.11,79.72,157.57,79.72 M230.91,164.86c0.33,0.18,0.68,0.35,1.03,0.49
c-0.46,0.14-0.91,0.31-1.33,0.49C230.71,165.51,230.81,165.19,230.91,164.86 M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200
s200-89.54,200-200S310.46,0,200,0L200,0z M157.57,87.72c5.42,0,10.84-0.04,16.26-0.14c0.08,0,0.16,0,0.24,0
c3.08,0,4.35,1.14,5.32,4.13c6.06,18.66,12.35,37.24,18.59,55.84c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28
c3.63-10.88,7.36-21.73,10.82-32.67c0.84-2.66,2.02-3.61,4.74-3.61c0.05,0,0.11,0,0.16,0c5.63,0.08,11.26,0.11,16.9,0.11
c5.7,0,11.4-0.04,17.09-0.1c0.05,0,0.1,0,0.15,0c2.66,0,3.59,0.82,3.58,3.65c-0.1,21.16-0.09,42.32-0.01,63.48
c0.01,2.45-0.61,3.48-3.15,3.48c-0.06,0-0.13,0-0.2,0c-3.95-0.08-7.91-0.13-11.86-0.13c-3.71,0-7.42,0.04-11.13,0.14
c-0.09,0-0.17,0-0.26,0c-2.86,0-3.26-1.36-3.22-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91
c-4.76,15.41-9.49,30.83-13.94,46.32c-0.91,3.19-2.23,4.33-5.32,4.33c-0.16,0-0.33,0-0.51-0.01c-3.87-0.14-7.76-0.22-11.64-0.22
c-3.79,0-7.57,0.07-11.35,0.22c-0.17,0.01-0.33,0.01-0.49,0.01c-3.2,0-4.33-1.38-5.22-4.47c-4.73-16.28-9.75-32.48-14.68-48.71
c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,17.99-0.08,35.99,0.07,53.98c0.03,3.18-0.62,4.56-3.82,4.56
c-0.17,0-0.34,0-0.52-0.01c-3.39-0.14-6.79-0.19-10.18-0.19c-3.77,0-7.54,0.07-11.3,0.17c-0.14,0-0.28,0.01-0.42,0.01
c-2.65,0-3.63-0.79-3.62-3.83c0.11-33.16,0.11-66.32,0-99.47c-0.01-3.29,1.09-4.11,4.04-4.11c0.08,0,0.17,0,0.25,0
C146.42,87.67,152,87.72,157.57,87.72L157.57,87.72z M194.36,281.6c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37
c1.26-3.8,2.8-5.65,6.8-5.65c0.26,0,0.52,0.01,0.8,0.02c3.39,0.18,6.79,0.24,10.24,0.24c4.95,0,10-0.11,15.28-0.11
c-4.67,13.01-9.09,25.35-13.54,37.68c-11.87,32.86-23.84,65.69-35.55,98.6c-1.33,3.74-2.98,5.32-6.79,5.32c-0.2,0-0.41,0-0.62-0.01
c-3.92-0.16-7.85-0.22-11.78-0.22c-4.57,0-9.14,0.08-13.71,0.18c-0.1,0-0.2,0-0.3,0c-2.92,0-4.48-0.97-5.54-3.94
c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45c6.85-2.06,13.63-4.38,20.29-6.97
c0.93-0.36,1.68-0.54,2.31-0.54c1.53,0,2.27,1.09,2.96,3.25c5.26,16.48,10.7,32.91,16.08,49.35
C193.22,278.43,193.78,279.95,194.36,281.6L194.36,281.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="LOGO" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M200,0C89.54,0,0,89.54,0,200s89.54,200,200,200s200-89.54,200-200S310.46,0,200,0z M249.67,210.48
c-11.87,32.86-23.84,65.69-35.55,98.6c-1.4,3.94-3.16,5.48-7.41,5.31c-8.48-0.35-16.99-0.22-25.48-0.04
c-3.12,0.07-4.74-0.87-5.85-3.94c-8.77-24.43-17.67-48.81-26.65-73.16c-1.07-2.91-1.05-4.42,2.36-5.45
c6.85-2.06,13.63-4.38,20.29-6.97c3.22-1.25,4.29-0.33,5.26,2.7c5.26,16.48,10.7,32.91,16.08,49.35c0.5,1.54,1.06,3.06,1.64,4.71
c1.99-1.26,2-3.28,2.56-4.93c11.12-32.77,22.25-65.54,33.17-98.37c1.34-4.04,3-5.88,7.6-5.63c8.26,0.45,16.57,0.13,25.52,0.13
C258.54,185.81,254.12,198.15,249.67,210.48z M263.37,154.73c0.01,2.52-0.65,3.54-3.34,3.48c-7.66-0.15-15.33-0.19-22.99,0.01
c-3.09,0.08-3.52-1.28-3.48-3.87c0.12-7.45,0.04-14.9,0.04-22.59c-2.19,1.2-1.94,3.31-2.43,4.91c-4.76,15.41-9.49,30.83-13.94,46.32
c-0.96,3.36-2.37,4.44-5.83,4.32c-7.65-0.28-15.33-0.3-22.99,0.01c-3.58,0.14-4.78-1.21-5.72-4.45
c-4.73-16.28-9.75-32.48-14.68-48.71c-0.23-0.76-0.55-1.49-1.67-2.2c0,1.51,0,3.03,0,4.54c0,18-0.08,35.99,0.07,53.98
c0.03,3.35-0.68,4.69-4.34,4.55c-7.15-0.29-14.33-0.22-21.49-0.02c-2.96,0.08-4.04-0.63-4.03-3.83c0.11-33.16,0.11-66.31,0-99.47
c-0.01-3.38,1.15-4.16,4.29-4.11c10.99,0.16,22,0.19,32.99-0.01c3.26-0.06,4.56,1.07,5.56,4.13c6.06,18.66,12.35,37.24,18.59,55.84
c0.5,1.5,1.14,2.96,2.17,5.6c3.53-10.55,6.67-19.91,9.8-29.28c3.63-10.88,7.36-21.73,10.82-32.67c0.86-2.71,2.07-3.64,4.9-3.61
c11.33,0.15,22.66,0.14,33.99,0.01c2.78-0.03,3.75,0.77,3.73,3.64C263.28,112.41,263.29,133.57,263.37,154.73z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -1,46 +0,0 @@
{
"short_name": "MVFRONT",
"name": "App Mieux Voter (Front App for MVAPI)",
"icons": [
{
"src": "\/favicon\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/favicon\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/favicon\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/favicon\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/favicon\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/favicon\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#2943a0"
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save