[VIEW] add invitationOnly

pull/73/head
Pierre-Louis Guhur 4 years ago
parent 0ba18fb27b
commit f6f979ddb2

@ -1,7 +1,4 @@
import React, {Suspense} from 'react'; import React, {Suspense} from 'react';
import {BrowserRouter as Router} from 'react-router-dom';
import Loader from './components/loader';
import Routes from './Routes'; import Routes from './Routes';
import Header from './components/layouts/Header'; import Header from './components/layouts/Header';
@ -10,17 +7,13 @@ import AppContextProvider from './AppContext';
function App() { function App() {
return ( return (
<Suspense fallback={<Loader/>} >
<AppContextProvider> <AppContextProvider>
<Router>
<div> <div>
<Header /> <Header />
<Routes /> <Routes />
<Footer /> <Footer />
</div> </div>
</Router>
</AppContextProvider> </AppContextProvider>
</Suspense>
); );
} }

@ -1,4 +1,7 @@
import React, { createContext } from "react"; import React, { createContext, Suspense } from "react";
import {BrowserRouter as Router} from 'react-router-dom';
import Loader from './components/loader';
export const AppContext = createContext(); export const AppContext = createContext();
@ -13,7 +16,13 @@ const AppContextProvider = ({ children }) => {
} }
}; };
return ( return (
<AppContext.Provider value={defaultState}>{children}</AppContext.Provider> <Suspense fallback={<Loader/>} >
<Router>
<AppContext.Provider value={defaultState}>
{children}
</AppContext.Provider>
</Router>
</Suspense>
); );
}; };
export default AppContextProvider; export default AppContextProvider;

@ -19,9 +19,8 @@ function Routes() {
<Route path="/create-election" component={CreateElection} /> <Route path="/create-election" component={CreateElection} />
<Route path="/vote/:slug" component={Vote} /> <Route path="/vote/:slug" component={Vote} />
<Route path="/result/:slug" component={Result} /> <Route path="/result/:slug" component={Result} />
<Route path="/create-success/:slug" component={CreateSuccess} /> <Route path="/link/:slug" component={props => <CreateSuccess invitationOnly={true} {...props} />} />
<Route path="/link/:slug" component={CreateSuccess} closed="true" /> <Route path="/links/:slug" component={props => <CreateSuccess invitationOnly={false} {...props} />} />
<Route path="/links/:slug" component={CreateSuccess} closed="false" />
<Route path="/vote-success/:slug" component={VoteSuccess} /> <Route path="/vote-success/:slug" component={VoteSuccess} />
<Route path="/unknown-election/:slug" component={UnknownElection} /> <Route path="/unknown-election/:slug" component={UnknownElection} />
<Route component={UnknownView} /> <Route component={UnknownView} />

@ -0,0 +1,45 @@
import React, {Component} 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 = event => {
const input = ref.current;
input.focus();
input.select();
document.execCommand('copy');
};
const {t, value, icon} = 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-outline-light"
onClick={handleClickOnButton}
type="button">
<FontAwesomeIcon icon={icon} className="mr-2" />
{t('Copy')}
</Button>
</div>
</div>
);
};
export default CopyField;

@ -267,7 +267,7 @@ class CreateElection extends Component {
}, },
body: JSON.stringify({ body: JSON.stringify({
title: title, title: title,
candidates: candidates.map(c => c.label), candidates: candidates.map(c => c.label).filter( c => c !== ""),
on_invitation_only: electorEmails.length > 0, on_invitation_only: electorEmails.length > 0,
num_grades: numGrades, num_grades: numGrades,
elector_emails: electorEmails, elector_emails: electorEmails,
@ -279,13 +279,16 @@ class CreateElection extends Component {
.then(result => { .then(result => {
console.log(result); console.log(result);
if (result.id) { if (result.id) {
const nextPage =
electorEmails && electorEmails.length
? `/link/${result.id}`
: `/links/${result.id}`;
this.setState(state => ({ this.setState(state => ({
redirectTo: '/create-success/' + result.id, redirectTo: nextPage,
successCreate: true, successCreate: true,
waiting: false waiting: false,
})) }));
} } else {
else {
toast.error(t('Unknown error. Try again please.'), { toast.error(t('Unknown error. Try again please.'), {
position: toast.POSITION.TOP_CENTER, position: toast.POSITION.TOP_CENTER,
}); });
@ -314,7 +317,7 @@ class CreateElection extends Component {
numGrades, numGrades,
isAdvancedOptionsOpen, isAdvancedOptionsOpen,
numCandidatesWithLabel, numCandidatesWithLabel,
electorEmails electorEmails,
} = this.state; } = this.state;
const {t} = this.props; const {t} = this.props;

@ -6,34 +6,25 @@ import {faCopy, faUsers} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import logoLine from '../../logos/logo-line-white.svg'; import logoLine from '../../logos/logo-line-white.svg';
import {AppContext} from '../../AppContext'; import {AppContext} from '../../AppContext';
import CopyField from '../CopyField';
class CreateSuccess extends Component { class CreateSuccess extends Component {
static contextType = AppContext; static contextType = AppContext;
constructor(props) { constructor(props) {
super(props); super(props);
console.log(props);
const electionSlug = this.props.match.params.slug; const electionSlug = this.props.match.params.slug;
this.state = { this.state = {
urlOfVote: urlOfVote: `https://${window.location.hostname}/vote/${electionSlug}`,
'https://' + window.location.hostname + '/vote/' + electionSlug, urlOfResult: `https://${window.location.hostname}/result/${electionSlug}`,
urlOfResult:
'https://' + window.location.hostname + '/result/' + electionSlug,
}; };
this.urlVoteField = React.createRef(); this.urlVoteField = React.createRef();
this.urlResultField = React.createRef(); this.urlResultField = React.createRef();
} }
handleClickOnField = event => {
event.target.focus();
event.target.select();
};
handleClickOnCopyVote = event => {
const input = this.urlVoteField.current;
input.focus();
input.select();
document.execCommand('copy');
};
handleClickOnCopyResult = event => { handleClickOnCopyResult = event => {
const input = this.urlResultField.current; const input = this.urlResultField.current;
input.focus(); input.focus();
@ -43,6 +34,26 @@ class CreateSuccess extends Component {
render() { render() {
const {t} = this.props; const {t} = this.props;
console.log(this.props)
const electionLink = this.props.invitationOnly ? (
<>
<p className="mt-4 mb-1">
{t('Voters received a link to vote by email. Each link can be used only once!')}
</p>
</>
) : (
<>
<p className="mt-4 mb-1">
{t('You can now share the election link to participants:')}
</p>
<CopyField
value={this.state.urlOfVote}
icon={faCopy}
t={t}
/>
</>
);
return ( return (
<Container> <Container>
<Row> <Row>
@ -53,54 +64,17 @@ class CreateSuccess extends Component {
<Row className="mt-4"> <Row className="mt-4">
<Col className="text-center offset-lg-3" lg="6"> <Col className="text-center offset-lg-3" lg="6">
<h2>{t('Successful election creation!')}</h2> <h2>{t('Successful election creation!')}</h2>
<p className="mt-4 mb-1">
{t('You can now share the election link to participants:')}
</p>
<div className="input-group "> {electionLink}
<input
type="text"
className="form-control"
ref={this.urlVoteField}
value={this.state.urlOfVote}
readOnly
onClick={this.handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-outline-light"
onClick={this.handleClickOnCopyVote}
type="button">
<FontAwesomeIcon icon={faCopy} className="mr-2" />
{t('Copy')}
</Button>
</div>
</div>
<p className="mt-4 mb-1"> <p className="mt-4 mb-1">
{t('Here is the link for the results in real time:')} {t('Here is the link for the results in real time:')}
</p> </p>
<div className="input-group "> <CopyField
<input value={this.state.urlOfResult}
type="text" icon={faCopy}
className="form-control" t={t}
ref={this.urlResultField} />
value={this.state.urlOfResult}
readOnly
onClick={this.handleClickOnField}
/>
<div className="input-group-append">
<Button
className="btn btn-outline-light"
onClick={this.handleClickOnCopyResult}
type="button">
<FontAwesomeIcon icon={faCopy} className="mr-2" />
{t('Copy')}
</Button>
</div>
</div>
</Col> </Col>
</Row> </Row>
<Row className="mt-4 mb-4"> <Row className="mt-4 mb-4">
@ -126,7 +100,7 @@ class CreateSuccess extends Component {
to={'/vote/' + this.props.match.params.slug} to={'/vote/' + this.props.match.params.slug}
className="btn btn-success"> className="btn btn-success">
<FontAwesomeIcon icon={faUsers} className="mr-2" /> <FontAwesomeIcon icon={faUsers} className="mr-2" />
{t("Participate now!")} {t('Participate now!')}
</Link> </Link>
</Col> </Col>
</Row> </Row>

@ -1,19 +1,17 @@
import React, { Component } from "react"; import React, {Component} from 'react';
import { Redirect } from "react-router-dom"; import {Redirect} from 'react-router-dom';
import { withTranslation } from 'react-i18next'; import {withTranslation} from 'react-i18next';
import { Button, Col, Container, Row } from "reactstrap"; import {Button, Col, Container, Row} from 'reactstrap';
import { toast, ToastContainer } from "react-toastify"; import {toast, ToastContainer} from 'react-toastify';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { faCheck } from "@fortawesome/free-solid-svg-icons"; import {faCheck} from '@fortawesome/free-solid-svg-icons';
import { resolve } from "url"; import {resolve} from 'url';
import { i18nGrades } from "../../Util"; import {i18nGrades} from '../../Util';
import { AppContext } from "../../AppContext"; import {AppContext} from '../../AppContext';
import { errorMessage } from "../../Errors"; import {errorMessage} from '../../Errors';
const shuffle = array => array.sort(() => Math.random() - 0.5); const shuffle = array => array.sort(() => Math.random() - 0.5);
class Vote extends Component { class Vote extends Component {
static contextType = AppContext; static contextType = AppContext;
constructor(props) { constructor(props) {
@ -30,7 +28,7 @@ class Vote extends Component {
colSizeGradeMd: 1, colSizeGradeMd: 1,
colSizeGradeXs: 1, colSizeGradeXs: 1,
redirectTo: null, redirectTo: null,
electionGrades: i18nGrades() electionGrades: i18nGrades(),
}; };
} }
@ -53,18 +51,18 @@ class Vote extends Component {
const numGrades = response.num_grades; const numGrades = response.num_grades;
const candidates = response.candidates.map((c, i) => ({ const candidates = response.candidates.map((c, i) => ({
id: i, id: i,
label: c label: c,
})); }));
shuffle(candidates); shuffle(candidates);
const colSizeGradeLg = Math.floor( const colSizeGradeLg = Math.floor(
(12 - this.state.colSizeCandidateLg) / numGrades (12 - this.state.colSizeCandidateLg) / numGrades,
); );
const colSizeGradeMd = Math.floor( const colSizeGradeMd = Math.floor(
(12 - this.state.colSizeCandidateMd) / numGrades (12 - this.state.colSizeCandidateMd) / numGrades,
); );
const colSizeGradeXs = Math.floor( const colSizeGradeXs = Math.floor(
(12 - this.state.colSizeCandidateXs) / numGrades (12 - this.state.colSizeCandidateXs) / numGrades,
); );
this.setState(state => ({ this.setState(state => ({
@ -86,7 +84,7 @@ class Vote extends Component {
12 - colSizeGradeXs * numGrades > 0 12 - colSizeGradeXs * numGrades > 0
? 12 - colSizeGradeXs * numGrades ? 12 - colSizeGradeXs * numGrades
: 12, : 12,
electionGrades: i18nGrades().slice(0, numGrades) electionGrades: i18nGrades().slice(0, numGrades),
})); }));
return response; return response;
}; };
@ -97,9 +95,9 @@ class Vote extends Component {
const detailsEndpoint = resolve( const detailsEndpoint = resolve(
this.context.urlServer, this.context.urlServer,
this.context.routesServer.getElection.replace( this.context.routesServer.getElection.replace(
new RegExp(":slug", "g"), new RegExp(':slug', 'g'),
electionSlug electionSlug,
) ),
); );
fetch(detailsEndpoint) fetch(detailsEndpoint)
.then(this.handleErrors) .then(this.handleErrors)
@ -110,33 +108,33 @@ class Vote extends Component {
handleGradeClick = event => { handleGradeClick = event => {
let data = { let data = {
id: parseInt(event.currentTarget.getAttribute("data-id")), id: parseInt(event.currentTarget.getAttribute('data-id')),
value: parseInt(event.currentTarget.value) value: parseInt(event.currentTarget.value),
}; };
//remove candidate //remove candidate
let ratedCandidates = this.state.ratedCandidates.filter( let ratedCandidates = this.state.ratedCandidates.filter(
ratedCandidate => ratedCandidate.id !== data.id ratedCandidate => ratedCandidate.id !== data.id,
); );
ratedCandidates.push(data); ratedCandidates.push(data);
this.setState({ ratedCandidates: ratedCandidates }); this.setState({ratedCandidates});
}; };
handleSubmitWithoutAllRate = () => { handleSubmitWithoutAllRate = () => {
const {t} = this.props; const {t} = this.props;
toast.error(t("You have to judge every candidate/proposal!"), { toast.error(t('You have to judge every candidate/proposal!'), {
position: toast.POSITION.TOP_CENTER position: toast.POSITION.TOP_CENTER,
}); });
}; };
handleSubmit = event => { handleSubmit = event => {
event.preventDefault(); event.preventDefault();
const { ratedCandidates } = this.state; const {ratedCandidates} = this.state;
const electionSlug = this.props.match.params.slug; const electionSlug = this.props.match.params.slug;
const token = this.props.location.search.substr(7); const token = this.props.location.search.substr(7);
const endpoint = resolve( const endpoint = resolve(
this.context.urlServer, this.context.urlServer,
this.context.routesServer.voteElection this.context.routesServer.voteElection,
); );
const gradesById = {}; const gradesById = {};
@ -144,34 +142,33 @@ class Vote extends Component {
gradesById[c.id] = c.value; gradesById[c.id] = c.value;
}); });
const gradesByCandidate = []; const gradesByCandidate = [];
Object.keys(gradesById) Object.keys(gradesById).forEach(id => {
.forEach(id => { gradesByCandidate.push(gradesById[id]);
gradesByCandidate.push(gradesById[id]); });
});
const payload = { const payload = {
election: electionSlug, election: electionSlug,
grades_by_candidate: gradesByCandidate, grades_by_candidate: gradesByCandidate,
} };
if (token !== ""){ if (token !== '') {
payload["token"] = token; payload['token'] = token;
} }
fetch(endpoint, { fetch(endpoint, {
method: "POST", method: 'POST',
headers: { "Content-Type": "application/json" }, headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload) body: JSON.stringify(payload),
}) })
.then(this.handleErrors) .then(this.handleErrors)
.then(result => .then(result =>
this.setState({ redirectTo: "/vote-success/" + electionSlug }) this.setState({redirectTo: '/vote-success/' + electionSlug}),
) )
.catch(error => error); .catch(error => error);
}; };
render() { render() {
const {t} = this.props; const {t} = this.props;
const { redirectTo, candidates, electionGrades } = this.state; const {redirectTo, candidates, electionGrades} = this.state;
if (redirectTo) { if (redirectTo) {
return <Redirect to={redirectTo} />; return <Redirect to={redirectTo} />;
@ -190,8 +187,7 @@ class Vote extends Component {
<Col <Col
xs={this.state.colSizeCandidateXs} xs={this.state.colSizeCandidateXs}
md={this.state.colSizeCandidateMd} md={this.state.colSizeCandidateMd}
lg={this.state.colSizeCandidateLg} lg={this.state.colSizeCandidateLg}>
>
<h5>&nbsp;</h5> <h5>&nbsp;</h5>
</Col> </Col>
{electionGrades.map((grade, j) => { {electionGrades.map((grade, j) => {
@ -202,12 +198,10 @@ class Vote extends Component {
lg={this.state.colSizeGradeLg} lg={this.state.colSizeGradeLg}
key={j} key={j}
className="text-center p-0" className="text-center p-0"
style={{ lineHeight: 2 }} style={{lineHeight: 2}}>
>
<small <small
className="nowrap bold badge" className="nowrap bold badge"
style={{ backgroundColor: grade.color, color: "#fff" }} style={{backgroundColor: grade.color, color: '#fff'}}>
>
{grade.label} {grade.label}
</small> </small>
</Col> </Col>
@ -221,8 +215,7 @@ class Vote extends Component {
<Col <Col
xs={this.state.colSizeCandidateXs} xs={this.state.colSizeCandidateXs}
md={this.state.colSizeCandidateMd} md={this.state.colSizeCandidateMd}
lg={this.state.colSizeCandidateLg} lg={this.state.colSizeCandidateLg}>
>
<h5 className="m-0">{candidate.label}</h5> <h5 className="m-0">{candidate.label}</h5>
<hr className="d-lg-none" /> <hr className="d-lg-none" />
</Col> </Col>
@ -233,36 +226,33 @@ class Vote extends Component {
md={this.state.colSizeGradeMd} md={this.state.colSizeGradeMd}
lg={this.state.colSizeGradeLg} lg={this.state.colSizeGradeLg}
key={j} key={j}
className="text-lg-center" className="text-lg-center">
>
<label <label
htmlFor={"candidateGrade" + i + "-" + j} htmlFor={'candidateGrade' + i + '-' + j}
className="check" className="check">
>
<small <small
className="nowrap d-lg-none ml-2 bold badge" className="nowrap d-lg-none ml-2 bold badge"
style={ style={
this.state.ratedCandidates.find(function( this.state.ratedCandidates.find(function(
ratedCandidat ratedCandidat,
) { ) {
return ( return (
JSON.stringify(ratedCandidat) === JSON.stringify(ratedCandidat) ===
JSON.stringify({ id: candidate.id, value: j }) JSON.stringify({id: candidate.id, value: j})
); );
}) })
? { backgroundColor: grade.color, color: "#fff" } ? {backgroundColor: grade.color, color: '#fff'}
: { : {
backgroundColor: "transparent", backgroundColor: 'transparent',
color: "#000" color: '#000',
} }
} }>
>
{grade.label} {grade.label}
</small> </small>
<input <input
type="radio" type="radio"
name={"candidate" + i} name={'candidate' + i}
id={"candidateGrade" + i + "-" + j} id={'candidateGrade' + i + '-' + j}
data-index={i} data-index={i}
data-id={candidate.id} data-id={candidate.id}
value={j} value={j}
@ -271,26 +261,26 @@ class Vote extends Component {
function(element) { function(element) {
return ( return (
JSON.stringify(element) === JSON.stringify(element) ===
JSON.stringify({ id: candidate.id, value: j }) JSON.stringify({id: candidate.id, value: j})
); );
} },
)} )}
/> />
<span <span
className="checkmark" className="checkmark"
style={ style={
this.state.ratedCandidates.find(function( this.state.ratedCandidates.find(function(
ratedCandidat ratedCandidat,
) { ) {
return ( return (
JSON.stringify(ratedCandidat) === JSON.stringify(ratedCandidat) ===
JSON.stringify({ id: candidate.id, value: j }) JSON.stringify({id: candidate.id, value: j})
); );
}) })
? { backgroundColor: grade.color, color: "#fff" } ? {backgroundColor: grade.color, color: '#fff'}
: { : {
backgroundColor: "transparent", backgroundColor: 'transparent',
color: "#000" color: '#000',
} }
} }
/> />
@ -309,15 +299,14 @@ class Vote extends Component {
<Button <Button
type="button" type="button"
onClick={this.handleSubmitWithoutAllRate} onClick={this.handleSubmitWithoutAllRate}
className="btn btn-dark " className="btn btn-dark ">
>
<FontAwesomeIcon icon={faCheck} className="mr-2" /> <FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Validate")} {t('Validate')}
</Button> </Button>
) : ( ) : (
<Button type="submit" className="btn btn-success "> <Button type="submit" className="btn btn-success ">
<FontAwesomeIcon icon={faCheck} className="mr-2" /> <FontAwesomeIcon icon={faCheck} className="mr-2" />
{t("Validate")} {t('Validate')}
</Button> </Button>
)} )}
</Col> </Col>

Loading…
Cancel
Save