wip: refactor

pull/89/head
Pierre-Louis Guhur 2 years ago
parent 56836e5ea1
commit c8ae7147e6

@ -0,0 +1,30 @@
import {UncontrolledAlert} from 'reactstrap';
import {useTranslation} from "next-i18next";
const AlertDismissible = ({msg, color}) => {
const {t} = useTranslation();
if (msg) {
return (
<UncontrolledAlert color={color}>
<h4 className={`${color}-heading`}>{t(msg)}</h4>
<p>
<a
href={`mailto:${CONTACT_MAIL}?subject=[HELP]`}
className="btn btn-success m-auto"
>
{t("error.help")}
</a>
</p>
</UncontrolledAlert >
);
}
return null;
}
AlertDismissible.defaultProps = {
color: 'danger'
};
export default AlertDismissible;

@ -1,9 +1,10 @@
import Link from "next/link";
import { Container, Row, Col } from "reactstrap";
import { useTranslation } from "next-i18next";
import {Container, Row, Col} from "reactstrap";
import {useTranslation} from "next-i18next";
import {CONTACT_MAIL} from '@services/constants';
const Error = (props) => {
const { t } = useTranslation();
const Error = ({msg}) => {
const {t} = useTranslation();
return (
<Container className="full-height-container">
<Row>
@ -15,25 +16,25 @@ const Error = (props) => {
</Row>
<Row className="mt-4">
<Col className="text-center">
<h4>{props.value}</h4>
<h4>{t(msg)}</h4>
</Col>
</Row>
<Row className="mt-4">
<Col className="my-3" sm="6">
<Link href="/">
<a className="btn btn-secondary m-auto">{t("common.backHomepage")}</a>
<a className="btn btn-secondary m-auto">{t("common.back-homepage")}</a>
</Link>
</Col>
<Col className="my-3" sm="6">
<a
href="mailto:app@mieuxvoter.fr?subject=[HELP]"
href={`mailto:${CONTACT_MAIL}?subject=[HELP]`}
className="btn btn-success m-auto"
>
{t("resource.help")}
{t("error.help")}
</a>
</Col>
</Row>
</Container>
</Container >
);
};

@ -16,7 +16,7 @@ const Logo = props => {
return (
<Image
src={src}
alt={t('logo-alt')}
alt={t('logo.alt')}
className="d-block"
{...props}
/>

@ -1,7 +1,6 @@
import React, {useEffect, useRef, useState} from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
// TODO use bootstrap modal
// https://getbootstrap.com/docs/5.0/components/modal/
//
const Modal = ({show, onClose, children, title}) => {
const handleCloseClick = (e) => {
@ -10,17 +9,17 @@ const Modal = ({show, onClose, children, title}) => {
};
const modalContent = show ? (
<StyledModalOverlay>
<StyledModal>
<StyledModalHeader>
<div className='vh-100 modal overlay'>
<div className='modal body'>
<div className='modal header'>
<a href="#" onClick={handleCloseClick}>
x
</a>
</StyledModalHeader>
{title && <StyledModalTitle>{title}</StyledModalTitle>}
<StyledModalBody>{children}</StyledModalBody>
</StyledModal>
</StyledModalOverlay>
</div>
{title && <div>{title}</div>}
<div className='pt-5'>{children}</div>
</div>
</div>
) : null;
@ -30,34 +29,5 @@ const Modal = ({show, onClose, children, title}) => {
};
const StyledModalBody = styled.div`
padding-top: 10px;
`;
const StyledModalHeader = styled.div`
display: flex;
justify-content: flex-end;
font-size: 25px;
`;
const StyledModal = styled.div`
background: white;
width: 500px;
height: 600px;
border-radius: 15px;
padding: 15px;
`;
const StyledModalTitle = styled.div
const StyledModalOverlay = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
`;
export default Modal;

@ -0,0 +1,63 @@
import {createContext, useContext, useReducer} from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({children}) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider
value={dispatch}
>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const initialTasks = [
{id: 0, text: 'Philosophers Path', done: true},
{id: 1, text: 'Visit the temple', done: false},
{id: 2, text: 'Drink matcha', done: false}
];

@ -1,24 +0,0 @@
import { useState } from 'react'
import { Alert, Button } from 'react-bootstrap';
import { faTimes, faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function AlertDismissibleExample() {
const [show, setShow] = useState(true);
if (show) {
return (
<Alert className="preventWarning">
<Alert.Heading>
<div>
<FontAwesomeIcon icon={faExclamationCircle} className="mr-2" />
<span>2 candidats minimum</span>
</div>
<FontAwesomeIcon onClick={() => setShow(false)} icon={faTimes} className="mr-2" />
</Alert.Heading>
</Alert>
);
}
return null;
}

@ -1,186 +1,170 @@
import {useState, useEffect} from 'react'
import ButtonWithConfirm from "./ButtonWithConfirm";
import TrashButton from "./TrashButton";
import {
Row,
Col,
Label,
Input,
InputGroup,
InputGroupAddon,
Button, Modal, ModalHeader, ModalBody, Form
} from "reactstrap";
import {useTranslation} from "react-i18next";
import {
sortableHandle
} from "react-sortable-hoc";
import HelpButton from "@components/form/HelpButton";
import AddPicture from "@components/form/AddPicture";
import {
faPlus, faCogs, faCheck, faTrash
} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
const DragHandle = sortableHandle(({children}) => (
<span className="input-group-text indexNumber">{children}</span>
));
const CandidateField = ({avatar, label, description, candIndex, onDelete, onAdd, ...inputProps}) => {
const {t} = useTranslation();
const [visibled, setVisibility] = useState(false);
const toggle = () => setVisibility(!visibled)
const [selected, setSelectedState] = useState(false);
const [className, setClassName] = useState("none");
const [trashIcon, setTrashIcon] = useState("none");
const [plusIcon, setPlusIcon] = useState("none");
const addCandidate = () => {
if (label != "") {
toggle();
onAdd();
setSelectedState(!selected);
}
else {}
}
if (label != "") {
const type = "button";
}
else {
const type = "submit";
}
useEffect(() => {
setClassName("candidateButton " + (selected ? "candidateAdded" : ""))
}, [selected]);
useEffect(() => {
setPlusIcon("mr-2 cursorPointer " + (selected ? "trashIcon" : ""))
}, [selected]);
useEffect(() => {
setTrashIcon("trashIcon " + (selected ? "displayTrash" : ""))
}, [selected]);
const addFunction = () => {
addCandidate();
setSelectedState(!selected);
}
const removeCandidate = () => {
onDelete();
toggle();
}
const [image, setImage] = useState(null);
const [createObjectURL, setCreateObjectURL] = useState(null);
const uploadToClient = (event) => {
if (event.target.files && event.target.files[0]) {
const i = event.target.files[0];
setImage(i);
setCreateObjectURL(URL.createObjectURL(i));
}
};
return (
<Row className="rowNoMargin">
<div className={className}>
<div className="avatarThumb">
<img src={createObjectURL} alt="" />
<input placeholder="Ajouter un candidat" className="candidate-placeholder ml-2" value={label} />
</div>
<FontAwesomeIcon onClick={toggle} icon={faPlus} className={plusIcon} />
<div className={trashIcon}><TrashButton label={label} onDelete={onDelete} /></div>
</div>
<Modal
isOpen={visibled}
toggle={toggle}
className="modal-dialog-centered"
>
<ModalHeader className='closeModalAddCandidate' toggle={toggle}>
</ModalHeader>
<ModalBody>
<Col className="addCandidateCard">
<InputGroup className="addCandidateForm">
<Form>
<InputGroupAddon addonType="prepend" className="addCandidateHeader">
<DragHandle>
<h6>Ajouter un participant</h6>
<p>Ajoutez une photo, le nom et une description au candidat.</p>
<div className="ajout-avatar">
<div>
<div className="avatar-placeholer">
<img src={createObjectURL} />
</div>
</div>
<div className="avatar-text">
<h4>Photo <span> (facultatif)</span></h4>
<p>Importer une photo.<br />format : jpg, png, pdf</p>
<div className="btn-ajout-avatar">
<input type="file" name="myImage" id="myImage" value={avatar} onChange={uploadToClient} />
<label className="inputfile" htmlFor="myImage">Importer une photo</label>
</div>
</div>
</div>
<img src="/avatar.svg" />
</DragHandle>
</InputGroupAddon>
<Label className="addCandidateText">Nom et prenom</Label>
<Input
type="text"
value={label}
{...inputProps}
placeholder={t("resource.candidatePlaceholder")}
tabIndex={candIndex + 1}
maxLength="250"
autoFocus
className="addCandidateText"
required
/>
<Label>Description (Facultatif)</Label>
<Input
type="text"
defaultValue={description}
maxLength="250"
/>
<Row className="removeAddButtons">
<ButtonWithConfirm className="removeButton" label={label} onDelete={removeCandidate} />
<Button type={type} className="addButton" label={label} onClick={addCandidate}>
<FontAwesomeIcon icon={faPlus} />
<span>Ajouter</span>
</Button>
</Row>
</Form>
</InputGroup>
</Col>
</ModalBody></Modal>
{/* <Col xs="auto" className="align-self-center pl-0">
<HelpButton>
{t(
"Enter the name of your candidate or proposal here (250 characters max.)"
)}
</HelpButton>
</Col> */}
</Row>
);
// import {useState, useEffect} from 'react'
// import ButtonWithConfirm from "./ButtonWithConfirm";
// import TrashButton from "./TrashButton";
// import {
// Row,
// Col,
// Label,
// Input,
// InputGroup,
// InputGroupAddon,
// Button, Modal, ModalHeader, ModalBody, Form
// } from "reactstrap";
// import {useTranslation} from "react-i18next";
// import HelpButton from "@components/form/HelpButton";
// import AddPicture from "@components/form/AddPicture";
// import {
// faPlus, faCogs, faCheck, faTrash
// } from "@fortawesome/free-solid-svg-icons";
// import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
// const DragHandle = sortableHandle(({children}) => (
// <span className="input-group-text indexNumber">{children}</span>
// ));
// const CandidateField = ({avatar, label, description, candIndex, onDelete, onAdd, ...inputProps}) => {
const CandidateField = (props) => {
// const {t} = useTranslation();
// const [visibled, setVisibility] = useState(false);
// const toggle = () => setVisibility(!visibled)
// const [selected, setSelectedState] = useState(false);
// const [className, setClassName] = useState("none");
// const [trashIcon, setTrashIcon] = useState("none");
// const [plusIcon, setPlusIcon] = useState("none");
// const addCandidate = () => {
// if (label != "") {
// toggle();
// onAdd();
// setSelectedState(!selected);
// }
// else {}
// }
// const type = label != "" ? "button" : "submit";
// useEffect(() => {
// setClassName("candidateButton " + (selected ? "candidateAdded" : ""))
// }, [selected]);
// useEffect(() => {
// setPlusIcon("mr-2 cursorPointer " + (selected ? "trashIcon" : ""))
// }, [selected]);
// useEffect(() => {
// setTrashIcon("trashIcon " + (selected ? "displayTrash" : ""))
// }, [selected]);
// const addFunction = () => {
// addCandidate();
// setSelectedState(!selected);
// }
// const removeCandidate = () => {
// onDelete();
// toggle();
// }
//const [image, setImage] = useState(null);
// const [createObjectURL, setCreateObjectURL] = useState(null);
// const uploadToClient = (event) => {
// if (event.target.files && event.target.files[0]) {
// const i = event.target.files[0];
// setImage(i);
// setCreateObjectURL(URL.createObjectURL(i));
// }
// };
return (<p>FOO</p>);
// return (
// <Row className="rowNoMargin">
// <div className={className}>
// <div className="avatarThumb">
// <img src={createObjectURL} alt="" />
// <input placeholder="Ajouter un candidat" className="candidate-placeholder ml-2" value={label} />
// </div>
//
// <FontAwesomeIcon onClick={toggle} icon={faPlus} className={plusIcon} />
// <div className={trashIcon}><TrashButton label={label} onDelete={onDelete} /></div>
// </div>
//
// <Modal
// isOpen={visibled}
// toggle={toggle}
// className="modal-dialog-centered"
// >
//
// <ModalHeader className='closeModalAddCandidate' toggle={toggle}>
//
// </ModalHeader>
// <ModalBody>
// <Col className="addCandidateCard">
// <InputGroup className="addCandidateForm">
// <Form>
// <InputGroupAddon addonType="prepend" className="addCandidateHeader">
// { //<DragHandle>
// }
// <h6>Ajouter un participant</h6>
// <p>Ajoutez une photo, le nom et une description au candidat.</p>
// <div className="ajout-avatar">
// <div>
// <div className="avatar-placeholer">
// <img src={createObjectURL} />
// </div>
// </div>
// <div className="avatar-text">
// <h4>Photo <span> (facultatif)</span></h4>
//
// <p>Importer une photo.<br />format : jpg, png, pdf</p>
// <div className="btn-ajout-avatar">
// <input type="file" name="myImage" id="myImage" value={avatar} onChange={uploadToClient} />
// <label className="inputfile" htmlFor="myImage">Importer une photo</label>
// </div>
// </div>
// </div>
// <img src="/avatar.svg" />
// { //</InputGroupAddon></DragHandle>
// }
// </InputGroupAddon>
// <Label className="addCandidateText">Nom et prenom</Label>
// <Input
// type="text"
// value={label}
// {...inputProps}
// placeholder={t("resource.candidatePlaceholder")}
// tabIndex={candIndex + 1}
// maxLength="250"
// autoFocus
// className="addCandidateText"
// required
// />
// <Label>Description (Facultatif)</Label>
// <Input
// type="text"
// defaultValue={description}
// maxLength="250"
// />
// <Row className="removeAddButtons">
//
// <ButtonWithConfirm className="removeButton" label={label} onDelete={removeCandidate} />
//
// <Button type={type} className="addButton" label={label} onClick={addCandidate}>
// <FontAwesomeIcon icon={faPlus} />
// <span>Ajouter</span>
// </Button>
//
// </Row>
// </Form>
// </InputGroup>
// </Col>
// </ModalBody></Modal>
// {/* <Col xs="auto" className="align-self-center pl-0">
// <HelpButton>
// {t(
// "Enter the name of your candidate or proposal here (250 characters max.)"
// )}
// </HelpButton>
// </Col> */}
// </Row >
// );
}
export default CandidateField

@ -1,102 +1,116 @@
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 {DndContext, useDroppable} from '@dnd-kit/core';
import CandidateField from './CandidateField'
import AlertDismissibleExample from './AlertButton'
import Alert from '@components/Alert'
import {MAX_NUM_CANDIDATES} from '@services/constants';
// export function CandidateList(props) {
// const {isOver, setNodeRef} = useDroppable({
// id: props.id,
// });
// const style = {
// opacity: isOver ? 1 : 0.5,
// };
//
// return (
// <div ref={setNodeRef} style={style}>
// {props.children}
// </div>
// );
// }
// 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 arrayMove = (arr, fromIndex, toIndex) => {
// // https://stackoverflow.com/a/6470794/4986615
// const element = arr[fromIndex];
// arr.splice(fromIndex, 1);
// arr.splice(toIndex, 0, element);
// return arr
// }
const CandidatesField = ({onChange}) => {
const {t} = useTranslation();
const [candidates, setCandidates] = useState([])
const createCandidate = () => ({label: "", description: "", fieldRef: createRef()})
// Initialize the list with at least two candidates
const [candidates, setCandidates] = useState([createCandidate(), createCandidate()])
const [error, setError] = useState(null)
const addCandidate = () => {
if (candidates.length < 1000) {
candidates.push({label: "", description: "", fieldRef: createRef()});
setCandidates([...candidates]);
onChange(candidates)
if (candidates.length < MAX_NUM_CANDIDATES) {
setCandidates(
c => {
c.push(createCandidate());
return c
}
);
} else {
console.error("Too many candidates")
setError('error.too-many-candidates')
}
};
useEffect(() => {
addCandidate();
}, [])
// What to do when we change the candidates
useEffect(() => {
onChange();
}, [candidates])
const removeCandidate = index => {
if (candidates.length === 1) {
const newCandidates = []
newCandidates.push({label: "", fieldRef: createRef()});
setCandidates(newCandidates);
onChange(newCandidates)
setCandidates([createCandidate()]);
}
else {
const newCandidates = candidates.filter((c, i) => i != index)
setCandidates(newCandidates);
onChange(newCandidates);
setCandidates(oldCandidates =>
oldCandidates.filter((_, i) => i != index)
);
}
};
const editCandidate = (index, label) => {
candidates[index].label = label
setCandidates([...candidates]);
onChange(candidates);
setCandidates(
oldCandidates => {
oldCandidates[index].label = label;
return oldCandidates;
}
)
};
const handleKeyPress = (e, index) => {
if (e.key === "Enter") {
if (e.key !== "Enter") {
e.preventDefault();
if (index + 1 === candidates.length) {
addCandidate();
}
else {
}candidates[index + 1].fieldRef.current.focus();
candidates[index + 1].fieldRef.current.focus();
}
}
const onSortEnd = ({oldIndex, newIndex}) => {
setCandidates(arrayMove(candidates, oldIndex, newIndex));
setCandidates(c => arrayMove(c, oldIndex, newIndex));
};
return (
<div className="sectionAjouterCandidat">
<div className="ajouterCandidat">
<h4>Saisissez ici le nom de vos candidats.</h4>
<AlertDismissibleExample />
<SortableContainer onSortEnd={onSortEnd}>
<Alert msg={error} />
{candidates.map((candidate, index) => {
const className = "sortable"
return (
<SortableItem
<CandidateField
className={className}
key={`item-${index}`}
index={index}
@ -111,9 +125,8 @@ const CandidatesField = ({onChange}) => {
/>
)
})}
</SortableContainer>
</div>
</div>
</div >
);
}

@ -10,24 +10,78 @@ 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 [bboxLink1, link1] = useBbox();
// const [bboxLink2, link2] = useBbox();
// const [bboxLink3, link3] = useBbox();
// const [bboxLink4, link4] = useBbox();
// const [bboxLink5, link5] = useBbox();
//<Col className="col-
const menu = [
{
component: (
<Logo title={false} />
)
},
{
component: (
<Link href="/" style={linkStyle}>{t("menu.majority-judgment")}</Link>
)
},
{
component: (
<Link
href="https://mieuxvoter.fr/"
target="_blank"
rel="noopener noreferrer"
style={linkStyle}
>
{t("menu.whoarewe")}
</Link>
)
},
{
component: (
<Link href="/faq" style={linkStyle}>
{t("menu.faq")}
</Link>
)
},
{
component: (
<Link href="/" style={linkStyle}>
{t("menu.news")}
</Link>
)
},
{
component: (
<a href="mailto:app@mieuxvoter.fr?subject=[HELP]" style={linkStyle}>
Nous contacter
</a>
)
},
{
component: (
<div><LanguageSelector /></div>
)
}
]
return (
<footer>
<Row>
<Col className="col-auto me-auto">
<Button className="btn-info">
<a href="/">
Soutenez-nous
</a>
</Button>
<Row>
{menu.map((item, i) =>
<Col key={i} className="col-auto d-flex align-items-center">
{item.component}
</Col>
)}
</Row>
</Col>
<Col className="col-auto ms-auto">
<Col className="col-auto">
<Button className="btn-info">
<a href="/">
Soutenez-nous

@ -1,29 +1,27 @@
/* eslint react/prop-types: 0 */
import { useState } from "react";
import {useState} from "react";
import {
Collapse,
Navbar,
NavbarToggler,
Nav,
NavItem,
Button,
} from "reactstrap";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import {useTranslation} from "next-i18next";
import LanguageSelector from "./LanguageSelector";
import Accordion from "react-bootstrap/Accordion";
const Header = () => {
const [isOpen, setOpen] = useState(false);
const toggle = () => setOpen(!isOpen);
const { t } = useTranslation("common");
const {t} = useTranslation("resource");
return (
<header className="mobile-header">
<Navbar light className="nav-mobile" expand="lg">
<div className="navbar-header">
<div className="navbar-header">
<Button onClick={toggle} className="navbar-toggle pt-0 mt-0">
<img src="/open-menu-icon.svg" alt="" height="50" />
@ -33,10 +31,8 @@ const Header = () => {
<Collapse isOpen={isOpen} navbar>
<Nav className="ml-auto navbar-nav-scroll" navbar>
<div className="d-flex flex-row justify-content-between nav-logo">
<Link href="/">
<a className="navbar-brand navbar-brand-mobile">
<img src="/logos/logo.svg" alt="logo" height="80" />
</a>
<Link href="/" className="navbar-brand navbar-brand-mobile">
<img src="/logos/logo.svg" alt="logo" height="80" />
</Link>
<Button onClick={toggle} className="navbar-toggle navbar-close-button">
@ -46,73 +42,56 @@ const Header = () => {
<div>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
<Link href="/" onClick={toggle} className="navbar-my-link nav-link">
Le jugement majoritaire
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
<Link href="/" onClick={toggle} className="navbar-my-link nav-link">
Qui sommes-nous ?
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
<Link href="/" onClick={toggle} className="navbar-my-link nav-link">
Foire aux questions
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
<Link href="/" onClick={toggle} className="navbar-my-link nav-link">
On parle de nous
</a>
</Link>
</NavItem>
<NavItem>
<Link href="/">
<a onClick={toggle} className="navbar-my-link nav-link">
<Link href="/" onClick={toggle} className="navbar-my-link nav-link">
Nous contactez
</a>
</Link>
</NavItem>
<NavItem>
<LanguageSelector style={{ width: "80px" }} />
</NavItem>
<NavItem>
<LanguageSelector style={{width: "80px"}} />
</NavItem>
</div>
<NavItem className="navbar-credits-container">
<Button className="btn-primary btn-nav">
<a href="/">
Soutenez-nous
</a>
</a>
</Button>
<hr />
<div className="navbar-credits sharing sharing-mobile">
<p>Partagez lapplication Mieux voter</p>
<Link href="https://www.facebook.com/mieuxvoter.fr/"><img src="/facebook.svg" className="mr-2" /></Link>
<Link href="https://twitter.com/mieux_voter"><img src="/twitter.svg" className="mr-2" /></Link>
</div>
<div className="d-flex">
<Link href="https://jimmys-box.com/">
<a onClick={toggle} className="navbar-jimmy-link">
développé parJIMMY
</a>
</Link>
<p>Partagez lapplication Mieux voter</p>
<Link href="https://www.facebook.com/mieuxvoter.fr/"><img src="/facebook.svg" className="mr-2" /></Link>
<Link href="https://twitter.com/mieux_voter"><img src="/twitter.svg" className="mr-2" /></Link>
</div>
</NavItem>
</Nav>

@ -20,31 +20,28 @@ const Header = () => {
<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>
<Link href="/" 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>
</a>
</div>
</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 href="/new/" className="text-primary nav-link"> <FontAwesomeIcon icon={faRocket} className="mr-2" />
{t("Start an election")}
</Link>
</NavItem>
<NavItem style={{width: "80px"}}>

@ -19,16 +19,12 @@ const LanguageSelector = () => {
// ["GB", "FR", "ES", "DE", "RU"]
["GB", "FR"]
}
showOptionLabel={true}
selected={localeShort}
selectedSize={15}
optionsSize={22}
showSelectedLabel={true}
showSecondaryOptionLabel={false}
customLabels={{ "GB": "Lang: EN", "FR": "Lang: FR" }}
fullWidth={false}
customLabels={{"GB": "English", "FR": "Francais"}}
className="menu-flags"
selectedSize={14}
selectedSize={14}
/>
);
};

@ -1,10 +1,36 @@
module.exports = {
i18n: {
defaultLocale: "fr",
locales: ["en", "fr"],
},
react: {
useSuspense: false,
wait: true
}
};
// https://www.i18next.com/overview/configuration-options#logging
debug: process.env.NODE_ENV === 'development',
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
ns: ["resource"],
defaultNS: "resource",
defaultValue: "__STRING_NOT_TRANSLATED__",
/** To avoid issues when deploying to some paas (vercel...) */
localePath: typeof window === 'undefined' ?
require('path').resolve('./public/locales') : '/locales',
reloadOnPrerender: process.env.NODE_ENV === 'development',
/**
* @link https://github.com/i18next/next-i18next#6-advanced-configuration
*/
// saveMissing: false,
// strictMode: true,
// serializeConfig: false,
// react: { useSuspense: false }
}
// const path = require('path')
// module.exports = {
// i18n: {
// defaultLocale: "fr",
// locales: ["en", "fr"],
// },
// localePath: path.resolve('./public/locales'),
// // react: {
// // useSuspense: false,
// // wait: true
// // }
// };

@ -1,4 +1,4 @@
const {i18n} = require("./next-i18next.config");
const {i18n} = require('./next-i18next.config.js')
module.exports = {

19030
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -10,62 +10,33 @@
"export": "next export"
},
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@fortawesome/fontawesome-free": "^5.15.3",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@fortawesome/react-fontawesome": "^0.1.19",
"@svgr/webpack": "^6.2.1",
"@weknow/react-bubble-chart-d3": "^1.0.12",
"array-move": "^3.0.1",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-scss": "^4.6.0",
"caniuse-lite": "^1.0.30001423",
"bootstrap": "^5.2.2",
"bootstrap-scss": "^5.2.2",
"clipboard": "^2.0.10",
"d3": "^7.3.0",
"d3-require": "^1.2.4",
"domexception": "^2.0.1",
"dotenv": "^8.6.0",
"embla-carousel-react": "^6.2.0",
"form-data": "^4.0.0",
"framer-motion": "^6.2.8",
"gsap": "^3.9.1",
"handlebars": "^4.7.7",
"highcharts": "^9.3.2",
"eslint-config-next": "^13.0.0",
"highcharts-react-official": "^3.1.0",
"i18next": "^20.2.2",
"i18next-chained-backend": "^2.1.0",
"i18next-fs-backend": "^1.1.1",
"i18next-http-backend": "^1.2.4",
"i18next-localstorage-backend": "^3.1.2",
"i18next-text": "^0.5.6",
"i18next": "^22.0.3",
"mailgun.js": "^3.3.2",
"next": "^12.1.0",
"next-i18next": "^8.2.0",
"query-string": "^7.0.0",
"ramda": "^0.27.2",
"react": "^17.0.2",
"react-bootstrap": "^2.1.0",
"react-bubble-chart": "^0.4.0",
"react-datepicker": "^4.7.0",
"react-dom": "^17.0.2",
"react-flags-select": "^2.1.2",
"react-google-charts": "^3.0.15",
"react-i18next": "^11.8.15",
"react-modal": "^3.14.4",
"react-plotly.js": "^2.5.1",
"react-responsive": "^9.0.0-beta.6",
"react-simple-timestamp-to-date": "^1.0.3",
"react-sortable-hoc": "^2.0.0",
"react-toastify": "^7.0.4",
"reactstrap": "^8.9.0",
"sass": "^1.32.13",
"styled-components": "^5.3.3"
"next": "^13.0.0",
"next-i18next": "^12.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-flags-select": "^2.2.3",
"react-i18next": "^12.0.0",
"reactstrap": "^9.1.4",
"sass": "^1.32.13"
},
"devDependencies": {
"eslint": "^8.11.0",
"eslint-config-next": "12.1.0",
"eslint-plugin-react": "^7.29.3"
}
}

@ -3,6 +3,9 @@ import '@styles/globals.css'
import '@styles/loader.css'
import "@styles/scss/config.scss";
import '@fortawesome/fontawesome-svg-core/styles.css'
// import nextI18NextConfig from '../next-i18next.config.js'
import {appWithTranslation} from 'next-i18next'
import {AppProvider} from '@services/context.js'
import Header from '@components/layouts/Header'
@ -11,23 +14,22 @@ 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>
return (<AppProvider><Head>
<link rel="icon" key="favicon" href="/favicon.ico" />
<meta property="og:type" content="website" key="og:type" />
<meta property="og:url" content={origin} key="og:url" />
<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>
</main></AppProvider>);
</AppProvider>);
}
export default appWithTranslation(Application)

@ -1,137 +1,87 @@
import { useState, useEffect } from "react";
import {useReducer, 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 {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,
Modal, ModalHeader, ModalBody, ModalFooter, CustomInput
Modal, ModalHeader, ModalBody
} 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 {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faPlus,
faTrashAlt,
faCheck,
faCogs,
faExclamationTriangle,
faArrowLeft,
faExclamationCircle,
faChevronRight
} 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 {createElection} from "@services/api";
import {translateGrades} from "@services/grades";
import {extractTime, extractDay} from "@services/date";
import {DEFAULT_NUM_GRADES} from '@services/constants';
import Loader from "@components/wait";
import CandidatesField from "@components/form/CandidatesField";
import ConfirmModal from "@components/form/ConfirmModal";
import config from "../../next-i18next.config.js";
import Footer from '@components/layouts/Footer'
import DatePicker from "react-datepicker";
// 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 }) => ({
// import DatePicker from "react-datepicker";
//
export const getStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
...(await serverSideTranslations(locale, ['resource'])),
},
});
const CreateElection = (props) => {
const { t } = useTranslation();
/**
* Manage the election data structure
*/
const electionReducer = (election, action) => {
election[action.type] = action.value;
}
const CreateElectionForm = (props) => {
const {t} = useTranslation();
// default value : start at the last hour
const now = new Date();
const [title, setTitle] = useState("");
const [candidates, setCandidates] = useState([{ label: "" }, { description: "" }]);
const [numGrades, setNumGrades] = useState(6);
const [election, dispatch] = useReducer(electionReducer, {
title: "",
description: "",
candidates: [],
grades: DEFAULT_NUM_GRADES,
isTimeLimited: false,
isRandomOrder: false,
restrictResult: false,
restrictVote: false,
startVote: null,
endVote: null,
emails: [],
});
const [waiting, setWaiting] = useState(false);
const [isGradesOpen, setGradesOpen] = useState(false);
const [isAddCandidateMOpen, setAddCandidateMOpen] = useState(false);
const [isTimeLimited, setTimeLimited] = useState(false);
const [restrictResult, setRestrictResult] = useState(false);
const [restrictVote, setRestrictVote] = 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
// At the initialization, set the title using GET param
const router = useRouter();
useEffect(() => {
if (!router.isReady) return;
const { title: urlTitle } = router.query;
setTitle(urlTitle || "");
dispatch({'type': title, 'value': router.query.title || ""})
}, [router.isReady]);
const handleIsTimeLimited = (event) => {
setTimeLimited(event.target.value === "1");
};
/**
* Handle change to an input
*/
const handleEvent = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
dispatch({'type': e.target.name, 'value': value});
const handleRestrictResultCheck = (event) => {
setRestrictResult(event.target.value === "1");
};
const handleRestrictVote = (event) => {
setRestrictVote(event.target.value === "1");
};
}
const toggleMails = () => setVisibilityMails(!visibledMails)
const toggleGrades = () => setVisibilityGrades(!visibledGrades)
const toggle = () => setVisibility(!visibled)
@ -142,14 +92,14 @@ const CreateElection = (props) => {
const addCandidate = () => {
if (candidates.length < 1000) {
candidates.push({ label: "" });
candidates.push({label: ""});
setCandidates(candidates);
}
};
const checkFields = () => {
if (!candidates) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
return {ok: false, msg: 'error.at-least-2-candidates'};
}
let numCandidates = 0;
@ -157,14 +107,14 @@ const CreateElection = (props) => {
if (c.label !== "") numCandidates += 1;
});
if (numCandidates < 2) {
return { ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR };
return {ok: false, msg: AT_LEAST_2_CANDIDATES_ERROR};
}
if (!title || title === "") {
return { ok: false, msg: NO_TITLE_ERROR };
return {ok: false, msg: 'error.no-title'};
}
return { ok: true, msg: "OK" };
return {ok: true, msg: "OK"};
};
const handleSubmit = () => {
@ -204,7 +154,6 @@ const CreateElection = (props) => {
};
const [visibled, setVisibility] = useState(false);
const [visibledGrades, setVisibilityGrades] = useState(false);
const [visibledMails, setVisibilityMails] = useState(false);
const handleSendNotReady = (msg) => {
toast.error(t(msg), {
@ -243,6 +192,7 @@ const CreateElection = (props) => {
};
const [startDate, setStartDate] = useState(new Date());
return (
<Container className="addCandidatePage">
<Head>
@ -257,7 +207,8 @@ const CreateElection = (props) => {
content={t("resource.valueProp")}
/>
</Head>
<ToastContainer />
{ //<ToastContainer />
}
{waiting ? <Loader /> : ""}
<form className="form" onSubmit={handleSubmit} autoComplete="off">
<div className={displayNone}>
@ -391,8 +342,8 @@ const CreateElection = (props) => {
(isTimeLimited ? "d-block " : "d-none")
}
>
<DatePicker selected={startDate} onChange=
{(date) => setStartDate(date)} />
{ // <DatePicker selected={startDate} onChange= {(date) => setStartDate(date)} />
}
{/* <Row className="displayNone">
<Col xs="12" md="3" lg="3">
<span className="label">- {t("Starting date")}</span>
@ -628,7 +579,7 @@ const CreateElection = (props) => {
<Row>
<p className="mr-2 my-auto">{t("À ")}</p>
<ReactMultiEmail
{ /* <ReactMultiEmail
placeholder={t("Add here participants' emails")}
emails={emails}
onChange={setEmails}
@ -652,6 +603,7 @@ const CreateElection = (props) => {
}}
/>
*/}
</Row>
<Row>
<Button
@ -673,7 +625,7 @@ const CreateElection = (props) => {
<Col xs="12">
<Label>{t("Participants")}</Label>
<p>{t("If you list voters' emails, only them will be able to access the election")}</p>
<ReactMultiEmail
{ /* <ReactMultiEmail
placeholder={t("Add here participants' emails")}
emails={emails}
onChange={setEmails}
@ -693,7 +645,7 @@ const CreateElection = (props) => {
</div>
);
}}
/>
/>*/}
<div className="mt-2 mailMutedText">
<small className="text-muted">
<FontAwesomeIcon icon={faExclamationCircle} className="mr-2" />
@ -743,4 +695,47 @@ const CreateElection = (props) => {
);
};
export default CreateElection;
export default CreateElectionForm;
//
// const handleIsTimeLimited = (event) => {
// setTimeLimited(event.target.value === "1");
// };
// const handleRestrictResultCheck = (event) => {
// setRestrictResult(event.target.value === "1");
// };
// const handleRestrictVote = (event) => {
// setRestrictVote(event.target.value === "1");
// };
//
// 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";
//
//
//
// // 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>
// ));
//
// switch (action.type) {
// case 'title': {
// election.title = action.value;
// return election;
// }
// case 'time': {
// election.endTime = action.value;
// return election;
// }
//
// }

@ -3,42 +3,42 @@ 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,
Modal, ModalHeader, ModalBody, ModalFooter, CustomInput
} 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,
faArrowLeft,
faExclamationCircle
} 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 {
// Collapse,
// Container,
// Row,
// Col,
// Input,
// Label,
// InputGroup,
// InputGroupAddon,
// Button,
// Card,
// CardBody,
// Modal, ModalHeader, ModalBody, ModalFooter, CustomInput
// } 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,
// faArrowLeft,
// faExclamationCircle
// } 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";

@ -4,9 +4,9 @@ import Image from "next/image";
import {serverSideTranslations} from "next-i18next/serverSideTranslations";
import {useTranslation} from "next-i18next";
import {Container, Row, Col, Button, Input} from "reactstrap";
import config from "../next-i18next.config.js";
import Footer from '@components/layouts/Footer';
import Logo from '@components/Logo.jsx';
import {CREATE_ELECTION} from '@services/routes';
import ballotBox from '../public/urne.svg'
import email from '../public/email.svg'
import respect from '../public/respect.svg'
@ -15,11 +15,14 @@ import twitter from '../public/twitter.svg'
import facebook from '../public/facebook.svg'
import arrowRight from '../public/arrow-white.svg'
export const getStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
...(await serverSideTranslations(locale, ['resource'])),
},
});
const StartForm = () => {
const {t} = useTranslation('resource');
const [title, setTitle] = useState(null);
@ -32,14 +35,14 @@ const StartForm = () => {
<Logo height="128" />
</Row>
<Row>
<h4>{t("motto")}</h4>
<h4>{t("home.motto")}</h4>
</Row>
<Row>
<h2>{t("slogan")}</h2>
<h2>{t("home.slogan")}</h2>
</Row>
<Row className="justify-content-end">
<Input
placeholder={t("writeQuestion")}
placeholder={t("home.writeQuestion")}
autoFocus
required
className="mt-2 mb-0 sectionOneHomeInput"
@ -53,19 +56,27 @@ const StartForm = () => {
</Row>
<Row>
<Link href={{pathname: "/new/", query: {title: title}}}>
<Link href={{pathname: CREATE_ELECTION, query: {title: title}}}>
<Button type="submit">
<Row className="justify-content-md-center p-2">
<Col className='col-auto'>{t("start")}
</Col><Col className='col-auto d-flex'>
<Image src={arrowRight} width={22} height={22} className="align-self-center" />
<Col className='col-auto'>
{t("home.start")}
</Col>
<Col className='col-auto d-flex'>
<Image
src={arrowRight}
width={22}
height={22}
alt={t("home.start")}
className="align-self-center"
/>
</Col>
</Row>
</Button>
</Link>
</Row>
<Row className="noAds">
<p>{t("noAds")}</p>
<p>{t("home.noAds")}</p>
</Row>
</Col>
<Col></Col>
@ -75,26 +86,27 @@ const StartForm = () => {
}
const AdvantagesRow = () => {
const {t} = useTranslation('resource');
const resources = [
{
"src": ballotBox,
"alt": t("alt-icon-ballot-box"),
"title": t('advantage-1-title'),
"desc": t('advantage-1-desc'),
"alt": t("home.alt-icon-ballot-box"),
"title": t('home.advantage-1-title'),
"desc": t('home.advantage-1-desc'),
},
{
"src": email,
"alt": t("alt-icon-email"),
"title": t('advantage-2-title'),
"desc": t('advantage-2-desc'),
"alt": t("home.alt-icon-envelop"),
"title": t('home.advantage-2-title'),
"desc": t('home.advantage-2-desc'),
},
{
"src": respect,
"alt": t("alt-icon-respect"),
"title": t('advantage-3-title'),
"desc": t('advantage-3-desc'),
"alt": t("home.alt-icon-respect"),
"title": t('home.advantage-3-title'),
"desc": t('home.advantage-3-desc'),
}
]
return (<Row className="sectionTwoRowOne">
@ -121,19 +133,19 @@ const ExperienceRow = () => {
return (
<Row className="sectionTwoRowTwo">
<Row className="sectionTwoHomeImage">
<Image src={vote} alt={t('alt-icon-ballot')} />
<Image src={vote} alt={t('home.alt-icon-ballot')} />
</Row>
<Row className="sectionTwoRowTwoCol">
<h3 className="col-md-8">{t('experience-title')}</h3>
<h3 className="col-md-8">{t('home.experience-title')}</h3>
</Row>
<Row className="sectionTwoRowTwoCol">
<Col className="sectionTwoRowTwoColText col-md-4">
<h5 className="">{t('experience-1-title')}</h5>
<p>{t('experience-1-desc')}</p>
<h5 className="">{t('home.experience-1-title')}</h5>
<p>{t('home.experience-1-desc')}</p>
</Col>
<Col className="sectionTwoRowTwoColText col-md-4 offset-md-1">
<h5 className="">{t('experience-2-title')}</h5>
<p>{t('experience-2-desc')}</p>
<h5 className="">{t('home.experience-2-title')}</h5>
<p>{t('home.experience-2-desc')}</p>
<p></p>
</Col>
</Row>
@ -142,7 +154,7 @@ const ExperienceRow = () => {
<Button
color="primary"
>
{t('experience-call-to-action')}
{t('home.experience-call-to-action')}
<Image src={arrowRight} width={22} height={22} className="mr-2" />
</Button>
</Col>
@ -157,7 +169,7 @@ const ShareRow = () => {
return (
<Row className="sharing justify-content-md-center">
<Col className="col-auto">
{t('share')}
{t('home.share')}
</Col>
<Col className="col-auto">
<a
@ -182,6 +194,7 @@ const ShareRow = () => {
}
const Home = () => {
const {t} = useTranslation('resource');
return (
<Container className="homePage">
<section><StartForm /></section>

@ -1,17 +1,17 @@
import Link from "next/link";
import { Container, Row, Col } from "reactstrap";
import { useTranslation } from "next-i18next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
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 }) => ({
export const getStaticProps = async ({locale}) => ({
props: {
...(await serverSideTranslations(locale, [], config)),
},
});
const LegalNotices = (props) => {
const { t } = useTranslation();
const {t} = useTranslation();
return (
<Container>
<Row>
@ -70,7 +70,7 @@ const LegalNotices = (props) => {
<Row className="mt-4">
<Col className="text-center">
<Link href="/" className="btn btn-secondary">
{t("common.backHomepage")}
{t("common.back-homepage")}
</Link>
</Col>
</Row>

@ -1,8 +1,8 @@
import { useState } from "react";
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 {useTranslation} from "next-i18next";
import {serverSideTranslations} from "next-i18next/serverSideTranslations";
import {useRouter} from "next/router";
import Link from "next/link";
import {
Container,
@ -15,18 +15,18 @@ import {
Table,
Button
} from "reactstrap";
import { getResults, getDetails, apiErrors } from "@services/api";
import { grades } from "@services/grades";
import { translateGrades } from "@services/grades";
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";
import Footer from '@components/layouts/Footer'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown, faChevronRight, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faChevronRight, faChevronUp} from "@fortawesome/free-solid-svg-icons";
export async function getServerSideProps({ query, locale }) {
const { pid, tid } = query;
export async function getServerSideProps({query, locale}) {
const {pid, tid} = query;
const [res, details, translations] = await Promise.all([
getResults(pid),
@ -35,15 +35,15 @@ export async function getServerSideProps({ query, locale }) {
]);
if (typeof res === "string" || res instanceof String) {
return { props: { err: res.slice(1, -1), ...translations } };
return {props: {err: res.slice(1, -1), ...translations}};
}
if (typeof details === "string" || details instanceof String) {
return { props: { err: res.slice(1, -1), ...translations } };
return {props: {err: res.slice(1, -1), ...translations}};
}
if (!details.candidates || !Array.isArray(details.candidates)) {
return { props: { err: "Unknown error", ...translations } };
return {props: {err: "Unknown error", ...translations}};
}
return {
@ -58,8 +58,8 @@ export async function getServerSideProps({ query, locale }) {
};
}
const Result = ({ candidates, numGrades, title, pid, err, finish }) => {
const { t } = useTranslation();
const Result = ({candidates, numGrades, title, pid, err, finish}) => {
const {t} = useTranslation();
const newstart = new Date(finish * 1000).toLocaleDateString("fr-FR");
@ -227,21 +227,21 @@ const Result = ({ candidates, numGrades, title, pid, err, finish }) => {
<CardBody className="pt-5">
<Row className="column">
<Col>
{t("Preference profile")}
{t("Preference profile")}
<div>
<div
className="median"
style={{ height: "40px" }}
style={{height: "40px"}}
/>
<div style={{ width: "100%" }}>
<div style={{width: "100%"}}>
<div key={i}>
{/*candidate.label*/}
<div style={{ width: "100%" }}>
<div style={{width: "100%"}}>
{gradeIds
.slice(0)
@ -277,7 +277,7 @@ const Result = ({ candidates, numGrades, title, pid, err, finish }) => {
</Row>
<Row className="linkResult my-3">
<Link href="/"><a className="mx-auto">{t("Comment interpréter les résultats")}<FontAwesomeIcon icon={faChevronRight} className="ml-2 closeIcon" /></a></Link>
<Link href="/" className="mx-auto">{t("Comment interpréter les résultats")}<FontAwesomeIcon icon={faChevronRight} className="ml-2 closeIcon" /></Link>
</Row>
</CardBody>
</Collapse>
@ -286,17 +286,17 @@ const Result = ({ candidates, numGrades, title, pid, err, finish }) => {
})}
</Col>
</Row>
<div className="componentMobile mt-5">
<Row>
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary"><img src="/arrowUpload.svg" /><p>Télécharger les résultats</p></Button>
</Row>
<Row>
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary"><img src="/arrowL.svg" /><p>Partagez les résultats</p></Button>
</Row>
<div className="componentMobile mt-5">
<Row>
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary"><img src="/arrowUpload.svg" /><p>Télécharger les résultats</p></Button>
</Row>
<Row>
<Button className="cursorPointer btn-result btn-validation mb-5 btn btn-secondary"><img src="/arrowL.svg" /><p>Partagez les résultats</p></Button>
</Row>
</div>
</section>
<Footer />
</Container>
);
};
export default Result;
export default Result;

@ -1,100 +0,0 @@
{
"title": "Plattform mit Mehrheitswahl",
"Homepage": "Homepage",
"Source code": "Quellcode",
"Who are we?": "Wer wir sind?",
"Privacy policy": "Datenschutzerklärung",
"resource.legalNotices": "Rechtliche Hinweise",
"FAQ": "FAQ",
"resource.help": "Brauchen Sie Hilfe?",
"BetterVote": " BetterVote",
"Voting platform": "Wahlplattform",
"Majority Judgment": " Mehrheitswahl ",
"Start an election": "Eine Wahl beginnen",
"resource.candidatePlaceholder": "Name des Kandidaten/Abstimmungsvorschlags",
"Delete?": "Löschen?",
"Are you sure to delete": "Sind Sie sich sicher, dass Sie dies löschen möchten?",
"the row": "die Zeile",
"Write here your question or introduce simple your election (250 characters max.)": "Schreiben Sie hier Ihre Frage oder erklären Sie kurz ihre Wahl (bis 250 Zeichen)",
"Enter the name of your candidate or proposal here (250 characters max.)": "Geben Sie hier den Namen Ihres Kandidaten oder Antrags ein (max. 250 Zeichen)",
"Please add at least 2 candidates.": "Bitte geben Sie mindestens zwei Kandidaten vor. ",
"Question of the election": "Zur Wahl stehende Frage",
"Write here the question of your election": "Schreiben Sie hier die zur Wahl stehenden Frage",
"For example:": "Zum Beispiel",
"For the role of my representative, I judge this candidate...": "Meine Einschätzung des Kandidaten als meinen Repräsentanten ist …",
"Candidates/Proposals": "Kandidaten/Abstimmungsvorschlag ",
"Add a proposal": "Weiteren hinzufügen",
"Advanced options": "Weitere Optionen",
"Starting date": "Anfangsdatum",
"Ending date": "Enddatum",
"Defined period" : "Definierte Periode",
"Unlimited" : "Unbegrenzt",
"Voting time" : "Abstimmungszeit",
"Grades": "Note",
"You can select here the number of grades for your election": " Sie können hier die Anzahl der Noten für Ihre Wahl auswählen ",
"5 = Excellent, Very good, Good, Fair, Passable": "5 = hervorragend, sehr gut, gut, befriedigend, ausreichend",
"Participants": "Teilnehmer",
"Add here participants' emails": "Fügen Sie hier die Email Adressen der Teilnehmer hinzu.",
"List voters' emails in case the election is not opened": "Falls die Wahl noch nicht sofort geöffnet werden soll, fügen Sie die Email Adressen der Teilnehmer hier zu.",
"Validate": "Ok",
"Submit my vote": "Ok",
"Confirm your vote": "Bestätigen Sie Ihre Wahl",
"The form contains no address.": "Keine Email Adresse wurde hinzugefügt.",
"The election will be opened to anyone with the link": "Die Wahl ist offen afür jeden, der diesen Link hat.",
"Start the election": "Mit der Wahl beginnen.",
"Cancel": "Abbrechen",
"Confirm": "Ok",
"Successful election creation!": "Die Wahl wurde erfolgreich erstellt!",
"You can now share the election link to participants:": "Sie können nun den Teilnehmern den Link zukommen lassen.",
"Copy": "Kopieren",
"Here is the link for the results in real time:": " Hier ist der Link für die Ergebnisse in Echtzeit:",
"Keep these links carefully": "Speichern Sie diesen Link an einem sicheren Ort.",
"resource.participateBtn": "Machen Sie jetzt mit!",
"t": "<0>Achtung</0> : Sie werden zu einem späteren Zeitpunkt keine Möglichkeit haben, diese Links abzurufen, auch wir haben keinen Zugriff darauf. Sie können aber beispielsweise diese Seite in Ihrem Browser als Lesezeichen speichern.",
"Simple and free: organize an election with Majority Judgment.": "Einfach und kostenlos: Organisation von Mehrheitswahlen.",
"Start": "Start",
"No advertising or ad cookies": "Keine Werbung und auch keine Cookies zu Werbezwecken.",
"Oops! This election does not exist or it is not available anymore.": "Ups! Diese Wahl existiert nicht oder ist nicht mehr verfügbar. ",
"You can start another election.": "Sie können eine neue Umfrage starten.",
"Go back to homepage": "Zurück zur Hompage",
"You have to judge every candidate/proposal!": "Sie müssen jeden Kandidaten/Abstimmungsvorschlag bewerten!",
"resource.voteSuccess": " Ihre Teilnahme wurde gespeichert!",
"resource.thanks": " Vielen Dank für Ihre Teilnahme.",
"Support us !": "Unterstützen Sie uns!",
"PayPal - The safer, easier way to pay online!": "PayPal - Die sicherere und einfachere Art, online zu bezahlen!",
"resource.numVotes": "Anzahl der Stimmen:",
"Unknown error. Try again please.": "Unbekannter Fehler. Bitte versuchen Sie es erneut.",
"Ending date:": "Enddatum:",
"If you list voters' emails, only them will be able to access the election": "Wenn Sie die E-Mails der Wähler auflisten, haben nur diese Zugriff auf die Wahl",
"Dates": "Datum",
"The election will take place from": "Die Wahl findet von",
"at": "um",
"to": "bis",
"Voters' list": "Wählerliste",
"Voters received a link to vote by email. Each link can be used only once!": "Die Wähler erhielten per E-Mail einen Link zur Stimmabgabe. Jeder Link kann nur einmal verwendet werden!",
"Results of the election:": "Ergebnisse der Wahl",
"Graph": "Grafik",
"Preference profile": "Präferenz-Profil",
"Oops... The election is unknown.": "Hoppla... Die Wahl ist unbekannt.",
"The election is still going on. You can't access now to the results.": "Die Wahl dauert noch an. Sie können jetzt nicht auf die Ergebnisse zugreifen.",
"No votes have been recorded yet. Come back later.": "Es wurden noch keine Abstimmungen registriert. Kommen Sie später wieder.",
"The election has not started yet.": "Die Wahlen haben noch nicht begonnen.",
"The election is over. You can't vote anymore": "Die Wahl ist vorbei. Sie können nicht mehr wählen",
"You need a token to vote in this election": "Sie brauchen eine Wertmarke, um an dieser Wahl teilzunehmen",
"You seem to have already voted.": "Sie scheinen bereits abgestimmt zu haben.",
"The parameters of the election are incorrect.": "Die Parameter der Wahl sind falsch.",
"Access to results" : "Zugang zu den Ergebnissen",
"Immediately": "Sofort",
"At the end of the election": "Am Ende der Wahl",
"Results available at the close of the vote": "Ergebnisse am Ende der Abstimmung verfügbar",
"The results page will not be accessible until all participants have voted.":"Die Ergebnisseite wird nicht zugänglich sein, bis alle Teilnehmer abgestimmt haben.",
"The results page will not be accessible until the end date is reached.": "Die Ergebnisseite wird nicht zugänglich sein, bis das Enddatum erreicht ist.",
"No one will be able to see the result until the end date is reached or until all participants have voted.": "Niemand wird das Ergebnis sehen können, bis das Enddatum erreicht ist oder bis alle Teilnehmer abgestimmt haben.",
"Send me this link" : "Senden Sie mir diesen Link",
"Send me these links" : "Schicken Sie mir diesen Link",
"Open" : "Öffnen Sie",
"Voting address" : "Abstimmungs-URL",
"Results address" : "Ergebnis-URL",
"Share election on Facebook" : "Wahl auf Facebook teilen",
"Share results on Facebook" : "Ergebnisse auf Facebook teilen"
}

@ -1,8 +0,0 @@
{
"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"
}

@ -1,10 +0,0 @@
{
"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,11 +0,0 @@
{
"error.e1": "Oops... The election is unknown",
"error.e2": "The election is still going on. You can't access now to the results.",
"error.e3": "No votes have been recorded yet. Come back later.",
"error.e4": "The election has not started yet.",
"error.e5": "The election is over. You can't vote anymore",
"error.e6": "You need a token to vote in this election",
"error.e7": "You seem to have already voted.",
"error.e8": "The parameters of the election are incorrect.",
"error.catch22": "Unknown error"
}

@ -1,27 +1,40 @@
{
"motto": "Simple and free",
"slogan": "Organize a vote with majority judgment",
"logo-alt": "Logo of Better Vote",
"writeQuestion": "Write here your question or describe your vote.",
"start": "Start a vote",
"noAds": "No advertising or ad cookies",
"advantage-1-title": "Simple",
"advantage-1-desc": "Create a vote in less than 1 minute!",
"advantage-2-title": "Free",
"advantage-2-desc": "Send invites without any limitations!",
"advantage-3-title": "Respecting your privacy",
"advantage-3-desc": "No personal data is recorded",
"experience-title": "A democratic and intuitive voting experience",
"experience-1-title": "Express your full opinion.",
"experience-1-desc": "With majority judgment, each candidate is evaluated on a grid of mentions. Strategic voting has no use anymore.",
"experience-2-title": "Get the best possible consensus.",
"experience-2-desc": "The merit profile provides an accurate picture of the voters' opinions. The winner of the vote is the one with the best majority rating.",
"experience-call-to-action": "Find out about the majority judgment",
"alt-icon-ballot-box": "icon of a ballot box",
"alt-icon-envelop": "icon of an envelop",
"alt-icon-respect": "icon of hands holding each other",
"alt-icon-ballot": "icon of a ballot",
"share": "Share the application Better Vote",
"home.motto": "Simple and free",
"home.slogan": "Organize a vote with majority judgment",
"logo.alt": "Logo of Better Vote",
"home.writeQuestion": "Write here your question or describe your vote.",
"home.start": "Start a vote",
"home.noAds": "No advertising or ad cookies",
"home.advantage-1-title": "Simple",
"home.advantage-1-desc": "Create a vote in less than 1 minute!",
"home.advantage-2-title": "Free",
"home.advantage-2-desc": "Send invites without any limitations!",
"home.advantage-3-title": "Respecting your privacy",
"home.advantage-3-desc": "No personal data is recorded",
"home.experience-title": "A democratic and intuitive voting experience",
"home.experience-1-title": "Express your full opinion.",
"home.experience-1-desc": "With majority judgment, each candidate is evaluated on a grid of mentions. Strategic voting has no use anymore.",
"home.experience-2-title": "Get the best possible consensus.",
"home.experience-2-desc": "The merit profile provides an accurate picture of the voters' opinions. The winner of the vote is the one with the best majority rating.",
"home.experience-call-to-action": "Find out about the majority judgment",
"home.alt-icon-ballot-box": "icon of a ballot box",
"home.alt-icon-envelop": "icon of an envelop",
"home.alt-icon-respect": "icon of hands holding each other",
"home.alt-icon-ballot": "icon of a ballot",
"home.share": "Share the application Better Vote",
"menu.majority-judgment": "Majority judgment",
"menu.whoarewe": "Who are we?",
"menu.faq": "FAQ",
"menu.news": "News",
"common.back-homepage": "Return to home page",
"error.help": "Ask for our help",
"error.at-least-2-candidates": "At least two candidates are required.",
"error.no-title": "Please add a title to your election.",
"grades.very-good": "Very good",
"grades.good": "Good",
"grades.passable": "Passable",
"grades.inadequate": "Inadequate",
"grades.mediocre": "Mediocre",
"Homepage": "Homepage",
"Source code": "Source code",
"Who are we?": "Who are we?",

@ -1,108 +0,0 @@
{
"title": "Plataforma de Juicio Mayoritario",
"Homepage": "Página de inicio",
"Source code": "Código fuente",
"Who are we": "Quiénes somos",
"Privacy policy": "Política de privacidad",
"resource.legalNotices": "Avisos legales",
"FAQ": "FAQ",
"resource.help": "¿Necesitas ayuda?",
"BetterVote": "VotarMejor",
"Voting platform": "Plataforma de votación",
"Majority Judgment": "Juicio Mayoritario",
"Start an election": "Iniciar una elección",
"resource.candidatePlaceholder": "Nombre del(la) candidato(a)/propuesta...",
"Delete?": "Borrar?",
"Are you sure to delete": "Estás seguro de querer borrar",
"the row": "la fila",
"Write here your question or introduce simple your election (250 characters max.)": "Escriba aquí su pregunta o introduzca simplemente su elección (250 caracteres máx.)",
"Enter the name of your candidate or proposal here (250 characters max.)": "Escriba aquí el nombre de su candidato o propuesta (250 caracteres como máximo)",
"Please add at least 2 candidates.": "Por favor, añada al menos dos canidatos(as).",
"Question of the election": "Pregunta de su elección",
"Write here the question of your election": "Escriba aquí la pregunta de su elección",
"For example:": "Por ejemplo:",
"For the role of my representative, I judge this candidate...": "Para ser mi representante, yo elijo a este(a) candidato(a)....",
"Candidates/Proposals": "Candidatos(as)/Propuestas",
"Add a proposal": "Añadir una propuesta",
"Advanced options": "Opciones avanzadas",
"Starting date": "Fecha de inicio",
"Ending date": "Fecha de finalización",
"Defined period" : "Período definido",
"Unlimited" : "Ilimitado",
"Voting time" : "Hora de la votación",
"Grades": "Escala",
"You can select here the number of grades for your election": "Puede seleccionar aquí el número de niveles de la escala para su elección",
"5 = Excellent, Very good, Good, Fair, Passable": "5 == Excelente, Muy bien, Bien, Regular, Pasable",
"Participants:": "Participantes",
"Add here participants' emails": "Añadir aquí los correos electrónicos de los(as) participantes",
"List voters' emails in case the election is not opened": "Enumere los correos electrónicos de los(as) votantes en caso de que la elección no se abra",
"Validate": "Validar",
"Submit my vote": "Validar",
"Confirm your vote": "Confirme su voto",
"The form contains no address.": "El formulario no contiene ningún correo electrónico",
"The election will be opened to anyone with the link": "La elección se abrirá a cualquiera que tenga el enlace",
"Start the election": "Iniciar la elección",
"Cancel": "Cancelar",
"Confirm": "Confirmar",
"Successful election creation!": "La elección ha sido creada con éxito!",
"You can now share the election link to participants:": "Ahora puede compartir el enlace de la elección con los(as) participantes",
"Copy": "Copiar",
"Here is the link for the results in real time:": "En este enlace puedes revisar los resultados en tiempo real",
"Keep these links carefully": "Guarda cuidadosamente estos enlaces",
"resource.participateBtn": "¡Participa ahora!",
"t": "<0>Advertencia</0>: No tendrás otras opciones para recuperar los enlaces, y no podremos compartirlos contigo. Por ejemplo, puedes agregarlos a favoritos de tu buscador.",
"Simple and free: organize an election with Majority Judgment.": "Simple y gratuito: organiza una elección con Juicio Mayoritario",
"Start": "Comenzar",
"No advertising or ad cookies": "No contiene publicidad ni cookies publicitarias",
"Oops! This election does not exist or it is not available anymore.": "¡Uy! Esta elección no existe o ya no está disponible",
"You can start another election.": "Puedes empezar otra elección",
"Go back to homepage": "Vuelve a la página de inicio",
"You have to judge every candidate/proposal!": "¡Tienes que evaluar a todos(as) los(as) candidatos(as)/propuestas",
"resource.voteSuccess": "¡Su participación fue registrada con éxito!",
"resource.thanks": "Muchas gracias por participar",
"Ending date:": "Fecha de finalización:",
"Excellent": "Excelente",
"Very good": "Muy bien",
"Good": "Bien",
"Fair": "Regular",
"Passable": "Pasable",
"Insufficient": "Insuficiente",
"To reject": "Rechazar",
"Dates": "Fechas",
"The election will take place from": "La elección tendrá lugar desde",
"at": "a las",
"to": "hasta",
"Voters' list": "Lista de votantes",
"Graph": "Gráfico",
"Preference profile": "Perfil de preferencia",
"Results of the election:": "Resultados de la elección",
"PayPal - The safer, easier way to pay online!": "PayPal la forma más segura y fácil de pagar en linea!",
"Support us !": "¡apórtanos!",
"Who are we?": "¿Quiénes somos?",
"Unknown error. Try again please.": "Error desconocido. Inténtelo de nuevo, por favor.",
"If you list voters' emails, only them will be able to access the election": "Si enumera los correos electrónicos de los votantes, sólo ellos podrán acceder a la elección",
"Voters received a link to vote by email. Each link can be used only once!": "Los votantes recibieron un enlace para votar por correo electrónico. ¡Cada enlace puede ser usado sólo una vez!",
"resource.numVotes": "Número de votos:",
"Oops... The election is unknown.": "Oops... La elección es desconocida",
"The election is still going on. You can't access now to the results.": "La elección sigue en marcha. No puedes acceder ahora a los resultados.",
"No votes have been recorded yet. Come back later.": "Aún no se han registrado votos. Vuelva más tarde.",
"The election has not started yet.": "Las elecciones aún no han comenzado.",
"The election is over. You can't vote anymore": "La elección ha terminado. Ya no puedes votar.",
"You need a token to vote in this election": "Necesitas una ficha para votar en esta elección",
"You seem to have already voted.": "Parece que ya has votado.",
"The parameters of the election are incorrect.": "Los parámetros de la elección son incorrectos.",
"Access to results" : "Acceso a los resultados",
"Immediately": "Inmediatamente",
"At the end of the election": "Al final de la elección",
"Results available at the close of the vote": "Resultados disponibles al cierre de la votación",
"The results page will not be accessible until all participants have voted.":"La página de resultados no será accesible hasta que todos los participantes hayan votado.",
"The results page will not be accessible until the end date is reached.": "No se podrá acceder a la página de resultados hasta que se alcance la fecha de finalización.",
"No one will be able to see the result until the end date is reached or until all participants have voted.": "Nadie podrá ver el resultado hasta que se alcance la fecha final o hasta que todos los participantes hayan votado.",
"Send me this link" : "Envíame este enlace",
"Send me these links" : "Envíame estos enlaces",
"Open" : "Abrir",
"Voting address" : "URL de la votación",
"Results address" : "URL de los resultados",
"Share election on Facebook" : "Compartir la elección en Facebook",
"Share results on Facebook" : "Comparte los resultados en Facebook"
}

@ -1,8 +0,0 @@
{
"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"
}

@ -1,10 +0,0 @@
{
"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."
}

@ -1,11 +0,0 @@
{
"error.e1": "Impossible de retrouver le vote en question",
"error.e2": "L'élection est encore en cours. Revenez plus tard.",
"error.e3": "Aucun vote n'a encore été enregistré. Revenez plus tard.",
"error.e4": "L'élection n'a pas encore démarrée.",
"error.e5": "L'élection est terminée. Vous ne pouvez plus voter.",
"error.e6": "Vous avez besoin d'un jeton pour participer à cette élection.",
"error.e7": "Vous avez déjà voté pour cette élection.",
"error.e8": "Les paramètres de l'élection sont inconnues.",
"error.catch22": "Erreur inconnue."
}

@ -1,53 +0,0 @@
{
"Homepage": "Accueil",
"Source code": "Code source",
"Who are we": "Qui sommes-nous",
"BetterVote": "MieuxVoter",
"Voting platform": "Plateforme de vote",
"Majority Judgment": "Jugement Majoritaire",
"Start an election": "Lancer une élection",
"resource.candidatePlaceholder": "Name du candidat/proposition",
"Delete?": "Supprimer ?",
"Are you sure to delete": "Êtes-vous sûr(e) de supprimer",
"the row": "la ligne",
"Write here your question or introduce simple your election (250 characters max.)": "Décrire ici votre question ou introduire simplement votre élection (250 caractères max.)",
"Please add at least 2 candidates.": "Merci d'ajouter au moins 2 candidats.",
"Question of the election": "Question de votre élection",
"Write here the question of your election": "Ecrire ici la question de votre élection",
"For example:": "Par exemple",
"For the role of my representative, I judge this candidate...": "Pour être mon représentant, je juge ce candidat...",
"Candidates/Proposals": "Candidats/Propositions",
"Add a proposal": "Ajouter une proposition",
"Advanced options": "Options avancées",
"Starting date:": "Date de début :",
"Ending date: ": "Date de fin : ",
"Grades:": "Mentions",
"You can select here the number of grades for your election": "You pouvez choisir ici le nombre de mentions de votre élection",
"5 = Excellent, Very good, Good, Fair, Passable": "5 = Excellent, Très bien, Bien, Assez bien, Passable",
"Participants:": "Participants :",
"Add here participants' emails": "Ajouter ici les emails des participants",
"List voters' emails in case the election is not opened": "Lister ici les emails des électeurs dans le cas où l'élection n'est pas ouverte.",
"Validate": "Valider",
"Confirm your vote": "Confirmer votre vote",
"The form contains no address.": "Aucune adresse email n'a été ajoutée.",
"The election will be opened to anyone with the link": "L'élection sera accessible à tous ceux qui disposent de ce lien",
"Start the election": "Démarrer l'élection",
"Cancel": "Annuler",
"Confirm": "Valider",
"Successful election creation!": "L'élection a été créée avec succès !",
"You can now share the election link to participants:": "Vous pouvez maintenant partager ce lien à tous les participants",
"Copy": "Copier",
"Here is the link for the results in real time:": "Voici le lien pour afficher les résultats en temps réel :",
"Keep these links carefully": "Gardez ces liens précieusement",
"resource.participateBtn": "Participez maintenant !",
"t": "<0>Attention</0> : vous n'aurez pas d'autres moyens pour récupérer ces liens par la suite, et nous ne serons pas capables de les partager avec vous. Vous pouvez, par exemple, ajouter ces liens à vos favoris dans votre navigateur.",
"Simple and free: organize an election with Majority Judgment.": "Simple et grauit: organiser une élection avec le Jugement Majoritaire.",
"Start": "Démarrer",
"No advertising or ad cookies": "Pas de publictés, ni de cookies publicitaires",
"Oops! This election does not exist or it is not available anymore.": "Oups ! L'élection n'existe pas ou n'est plus disponible.",
"You can start another election.": "Vous pouvez démarrer une autre élection.",
"Go back to homepage": "Revenir à la page d'accueil",
"You have to judge every candidate/proposal!": "Vous devez évaluer tous les candidats/propositions !",
"resource.voteSuccess": "Votre participation a été enregistrée avec succès !",
"resource.thanks": "Merci de votre participation."
}

@ -1,27 +1,35 @@
{
"motto": "Simple et gratuit",
"slogan": "Organisez un vote avec le jugement majoritaire",
"logo-alt": "Logo de Mieux Voter",
"writeQuestion": "Posez la question de votre vote ici.",
"start": "C'est parti",
"noAds": "Pas de publicités, ni de cookies publicitaires",
"advantage-1-title": "Simple",
"advantage-1-desc": "Créez un vote en moins dune minute.",
"advantage-2-title": "Gratuit",
"advantage-2-desc": "Envoyez des invitations par courriel sans limite d'envoi.",
"advantage-3-title": "Respect de votre vie privée",
"advantage-3-desc": "Aucune donnée personnelle n'est enregistrée",
"experience-title": "Une expérience de vote démocratique et intuitive",
"experience-1-title": "Exprimez toute votre opinion.",
"experience-1-desc": "Au jugement majoritaire, chaque candidat est évalué sur une grille de mention. Vous n'aurez plus besoin de faire un vote stratégique.",
"experience-2-title": "Obtenez le meilleur consensus.",
"experience-2-desc": "Le profil des mérites dresse un panorama précis de lopinion des électeurs. Le gagnant du vote est celui qui est la meilleure mention majoritaire.",
"experience-call-to-action": "Découvrez le jugement majoritaire",
"share": "Partagez lapplication Mieux voter",
"alt-icon-ballot-box": "icone d'urne",
"alt-icon-envelop": "icone d'enveloppe",
"alt-icon-respect": "icone de mains qui se serrent",
"alt-icon-ballot": "icone d'un bulletin de vote",
"home.motto": "Simple et gratuit",
"home.slogan": "Organisez un vote avec le jugement majoritaire",
"logo.alt": "Logo de Mieux Voter",
"home.writeQuestion": "Posez la question de votre vote ici.",
"home.start": "C'est parti",
"home.noAds": "Pas de publicités, ni de cookies publicitaires",
"home.advantage-1-title": "Simple",
"home.advantage-1-desc": "Créez un vote en moins dune minute.",
"home.advantage-2-title": "Gratuit",
"home.advantage-2-desc": "Envoyez des invitations par courriel sans limite d'envoi.",
"home.advantage-3-title": "Respect de votre vie privée",
"home.advantage-3-desc": "Aucune donnée personnelle n'est enregistrée",
"home.experience-title": "Une expérience de vote démocratique et intuitive",
"home.experience-1-title": "Exprimez toute votre opinion.",
"home.experience-1-desc": "Au jugement majoritaire, chaque candidat est évalué sur une grille de mention. Vous n'aurez plus besoin de faire un vote stratégique.",
"home.experience-2-title": "Obtenez le meilleur consensus.",
"home.experience-2-desc": "Le profil des mérites dresse un panorama précis de lopinion des électeurs. Le gagnant du vote est celui qui est la meilleure mention majoritaire.",
"home.experience-call-to-action": "Découvrez le jugement majoritaire",
"home.share": "Partagez lapplication Mieux voter",
"home.alt-icon-ballot-box": "icone d'urne",
"home.alt-icon-envelop": "icone d'enveloppe",
"home.alt-icon-respect": "icone de mains qui se serrent",
"home.alt-icon-ballot": "icone d'un bulletin de vote",
"menu.majority-judgment": "Jugement majoritaire",
"menu.whoarewe": "Qui sommes-nous ?",
"menu.faq": "FAQ",
"menu.news": "Actualités",
"common.backHomepage": "Revenir sur la page d'accueil",
"error.help": "Besoin d'aide ?",
"error.at-least-2-candidates": "Ajoutez au moins deux candidats.",
"error.no-title": "Ajoutez un titre à l'élection.",
"Homepage": "Accueil",
"Source code": "Code source",
"Who are we?": "Qui sommes-nous ?",

@ -1,107 +0,0 @@
{
"title": "Решение Большинства",
"Homepage": "Главная страница",
"Source code": "Исходный код",
"Who are we?": "Кто мы?",
"Privacy policy": "Политика конфиденциальности",
"resource.legalNotices": "Официальные уведомления",
"FAQ": "Часто задаваемые вопросы",
"resource.help": "Нужна помощь?",
"BetterVote": "BetterVote",
"Voting platform": "Платформа голосования",
"Majority Judgment": "Решение Большинства",
"Start an election": "Создать голосование",
"resource.candidatePlaceholder": "Имя кандидата/предлжения...",
"Delete?": "Удалить?",
"Are you sure to delete": "Вы уверены в удалении",
"the row": "ряд",
"Write here your question or introduce simple your election (250 characters max.)": "Напишите свой вопрос или опишите голосование (250 символов максимум.)",
"Enter the name of your candidate or proposal here (250 characters max.)": "Введите имя вашего кандидата или предложение здесь (не более 250 символов).",
"Please add at least 2 candidates.": "Пожалуйста добавьте как минимум 2 кандидатов",
"Question of the election": "Суть голосования",
"Write here the question of your election": "Напишите вопрос вашего голосования",
"For example:": "Например:",
"For the role of my representative, I judge this candidate...": "На роль моего представителя, я считаю этого кандидата...",
"Candidates/Proposals": "Кандидаты/Предложения",
"Add a proposal": "Добавьте предложение",
"Advanced options": "Расширенные настройки",
"Starting date": "Дата начала",
"Ending date": "Дата окончания",
"Defined period" : "Определенный период",
"Unlimited" : "Безлимитный",
"Voting time" : "Время голосования",
"Grades": "Оценки",
"You can select here the number of grades for your election": "Здесь вы можете выбрать количество оценок для вашего голосования",
"5 = Excellent, Very good, Good, Fair, Passable": "5 = Отлично, Очень хорошо, Хорошо, Удовлетворительно, Допустимо",
"Participants:": "Участники:",
"Add here participants' emails": "Добавьте электронную почту участников",
"List voters' emails in case the election is not opened": "Укажите электронные адреса голосующих на случай, если голосование не откроется",
"Validate": "Подтвердить",
"Submit my vote": "Подтвердить",
"Confirm your vote": "Подтвердите свой голос",
"The form contains no address.": "Адрес не указан.",
"The election will be opened to anyone with the link": "Голосование будет доступно любому, у кого есть ссылка",
"Start the election": "Начать голосование",
"Cancel": "Отменить",
"Confirm": "Подтвердить",
"Successful election creation!": "Голосование создано!",
"You can now share the election link to participants:": "Теперь вы можете поделить ссылкой с участниками:",
"Copy": "Скопировать",
"Here is the link for the results in real time:": "Ссылка на результаты в режиме онлайн:",
"Keep these links carefully": "Сохраните эти ссылки, чтобы не потерять",
"resource.participateBtn": "Участвуйте сейчас!",
"t": "<0>Предупреждение</0>: вы не сможете восстановить ссылки и не сможете ими поделиться. Например, вы можете добавить их в избранное в вашем браузере.",
"Simple and free: organize an election with Majority Judgment.": "Просто и бесплатно: создайте голосование с Решением Большинства.",
"Start": "Начать",
"No advertising or ad cookies": "Никакой рекламы",
"Oops! This election does not exist or it is not available anymore.": "Упс! Это голосование не существует или больше не доступно.",
"You can start another election.": "Вы можете создать другое голосование.",
"Go back to homepage": "Вернуться на главную",
"You have to judge every candidate/proposal!": "Вам нужно проголосовать за каждого кандидата/предложение!",
"resource.voteSuccess": "Ваш голос был учтен!",
"resource.thanks": "Спасибо за ваше участие.",
"Ending date:": "Дата окончания:",
"Excellent": "Отлично",
"Very good": "Очень хорошо",
"Good": "Хорошо",
"Fair": "Удовлетворительно",
"Passable": "Допустимо",
"Insufficient": "Неудовлетворительно",
"To reject": "Отклонять",
"Dates": "Даты",
"The election will take place from": "Голосование состоится",
"at": "в",
"to": "к",
"Voters' list": "Список проголосовавших",
"Graph": "Кривая",
"Preference profile": "Профиль заслуг",
"Results of the election:": "Результаты голосования:",
"Unknown error. Try again please.": "Неизвестная ошикба. Пожалуйста, попробуйте позже.",
"If you list voters' emails, only them will be able to access the election": "Если вы укажете электронные адреса участников, голосовать смогут только они",
"Voters received a link to vote by email. Each link can be used only once!": "Участники голосования получили ссылки на указанные адреса. Каждая ссылка может быть использована только один раз!",
"Oops... The election is unknown.": "Упс... Неизвестное голосование.",
"The election is still going on. You can't access now to the results.": "Голосование все еще в процессе. Вы не можете увидеть результаты сейчас.",
"No votes have been recorded yet. Come back later.": "Ни одного голоса не было записано. Пожалуйста, вернитесь позже.",
"The election has not started yet.": "Голосование еще не началось.",
"The election is over. You can't vote anymore": "Голосвание закончено. Вы больше не можете проголосовать.",
"You need a token to vote in this election": "Для участия вам необходим жетон",
"You seem to have already voted.": "Кажется, вы уже проголосовали.",
"The parameters of the election are incorrect.": "Параметры голосвания неверны.",
"Support us !": "Поддержите нас !",
"PayPal - The safer, easier way to pay online!": "PayPal - Безопасный и простой способ платить онлайн!",
"resource.numVotes": "Количество голосов:",
"Access to results" : "Доступ к результатам",
"Immediately": "Немедленно",
"At the end of the election": "По окончании выборов",
"Results available at the close of the vote": "Результаты, доступные по окончании голосования",
"The results page will not be accessible until all participants have voted.":"Страница результатов не будет доступна до тех пор, пока все участники не проголосуют.",
"The results page will not be accessible until the end date is reached.": "Страница результатов не будет доступна до тех пор, пока не будет достигнута конечная дата.",
"No one will be able to see the result until the end date is reached or until all participants have voted.": "Никто не сможет увидеть результат до тех пор, пока не будет достигнут конечный срок или пока все участники не проголосуют.",
"Send me this link" : "Пришлите мне эту ссылку",
"Send me these links" : "Пришлите мне эти ссылки",
"Open" : "Открыть",
"Voting address" : "URL-адрес для голосования",
"Results address" : "URL-адрес результатов",
"Share election on Facebook" : "Поделиться выборами на Facebook",
"Share results on Facebook" : "Поделиться результатами на Facebook"
}

@ -0,0 +1,7 @@
/**
* This file provides useful constants for the project
*/
const MAX_NUM_CANDIDATES = process.env.MAX_NUM_CANDIDATES || 1000;
const CONTACT_MAIL = process.env.CONTACT_MAIL || "app@mieuxvoter.fr";
const DEFAULT_GRADES = process.env.DEFAULT_GRADES || ['grades.very-good', 'grades.good', 'grades.passable', 'grades.inadequate', 'grades.mediocre'];

@ -0,0 +1,37 @@
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);
/**
* Extract only the time from a date.
* Date can be a string or a Date.
* Return result in timestamp seconds.
*/
const extractTime = (date) => {
if (typeof date === "string") {
date = Date.parse(date);
}
if (!isValidDate) {
throw Error("The date is not valid.")
}
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
}
/**
* Extract only the day from a date.
* Return result in timestamp seconds.
*/
const extractDay = (date) => {
if (typeof date === "string") {
date = Date.parse(date);
}
if (!isValidDate) {
throw Error("The date is not valid.")
}
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
}

@ -0,0 +1,5 @@
/**
* This file provides the paths to the pages
*/
const CREATE_ELECTION = '/admin/new/';

@ -76,22 +76,6 @@ header {
width: 100%;
}
footer {
background-color: $mv-blue-color;
color: $mv-light-color;
padding: 16px;
min-height: 7vh;
}
footer a {
color: $mv-light-color;
font-size: 14px;
}
footer a:hover {
color: #fff;
}
hr {
border: none;
border-top: 1px solid $mv-light-color;
@ -310,25 +294,6 @@ li.sortable {
padding: 0.25em !important;
}
/** flag selector **/
.flag-select > button {
height: 35px;
}
.flag-select__options {
width: 65px;
text-align: center;
background-color: $mv-light-color !important;
}
.flag-select__options .flag-select__option {
padding: 0;
margin: 0;
}
.flag-select__options .flag-select__option__icon {
top: 0;
}
/** GLOBALS **/
section {
width: 100%;
@ -376,44 +341,9 @@ h5 {
margin: 0px 16px;
}*/
.footerRow > .col {
flex-basis: auto !important;
width: auto !important;
align-self: center;
}
.footerLogo {
padding-right: 0px;
}
.footerButton {
padding-right: 0px;
}
.btn-footer {
width: auto;
padding: 7px;
border: none;
box-shadow: none;
background: #4a2fef;
color: white;
font-size: 14px;
width: 100%;
}
.cursorPointer {
cursor: pointer;
}
.ReactFlagsSelect-module_selectFlag__2q5gC {
display: none !important;
}
.ReactFlagsSelect-module_selectBtn__19wW7 {
border: none !important;
}
.menu-flags {
width: fit-content !important;
margin-left: 0px !important;
padding-bottom: 0px !important;
}
.footerRow div {
margin: auto 8px;
}
.sectionAjouterCandidat {
display: flex;

@ -1,2 +1 @@
@import "~bootstrap/scss/bootstrap.scss";
// @import "~bootstrap/scss/_flex.scss";
@import "../../node_modules/bootstrap/scss/bootstrap";

@ -38,3 +38,8 @@
border: 2px solid #2400fd;
box-shadow: 0px 5px 0px #7a64f9;
}
.btn:hover {
background-color: rgb(255,255,255,0.2);
border-color: white;
}

@ -1,30 +1,37 @@
// .tacky {
// margin: 0;
// padding: 0;
// list-style-type: none;
// flex-basis: auto;
// }
//
// .tacky col {
// display: inline-block;
// }
//
// .tacky col:after {
// margin: 0 5px;
// }
//
// .tacky col:last-of-type:after {
// content: "";
// margin: 0;
// }
//
// .tacky col.no-tack:after {
// content: "";
// margin: 0;
// display: none;
// }
footer {
background-color: $mv-blue-color;
color: $mv-light-color;
padding: 16px;
min-height: 7vh;
}
footer a {
color: $mv-light-color;
font-size: 14px;
text-decoration: unset;
}
footer a:hover {
color: white;
text-decoration: underline;
}
.menu-flags {
padding-bottom: 0px !important;
}
.menu-flags > ul {
bottom: 30px;
color: $mv-blue-color;
}
.menu-flags > button {
color: white;
border: unset;
&:after {
border-top-color: white;
}
}
@include media-breakpoint-down('md') {
@include media-breakpoint-down("md") {
footer {
display: none !important;
}

@ -139,23 +139,6 @@
display: flex;
justify-content: flex-start;
}
.menu-flags {
width: 25%;
margin-left: 15px;
margin-top: 12px;
}
.menu-flags button {
border-color: white;
color: white;
}
.menu-flags button:after {
border-top: 5px solid white;
color: white;
}
.menu-flags button[aria-expanded="true"]:after {
border-bottom: 5px solid white;
color: white;
}
.navbar,
body {
scrollbar-width: none; /* Firefox */

@ -26,11 +26,6 @@ $desktop: 1680px;
padding-right: 8%;
flex-basis: auto;
}
.sectionOneHomeContent img {
width: 200px;
height: auto;
margin: 7% 0%;
}
.sectionOneHomeContent h2 {
margin-bottom: 10%;
}
@ -108,20 +103,6 @@ $desktop: 1680px;
margin: 0% 15px;
display: flex;
}
.sharing img {
margin: 16px 15px 0px;
width: 22px;
}
/* @include media-breakpoint-up('md') {
.sectionOneHomeForm {
background-image: url('/chevron-bicolore.svg');
background-size: contain;
background-position: 120% center;
background-repeat: no-repeat;
}
}
*/
@include media-breakpoint-up("md") {
.sectionOneHomeForm {

Loading…
Cancel
Save