[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 {BrowserRouter as Router} from 'react-router-dom';
import Loader from './components/loader';
import Routes from './Routes';
import Header from './components/layouts/Header';
@ -10,17 +7,13 @@ import AppContextProvider from './AppContext';
function App() {
return (
<Suspense fallback={<Loader/>} >
<AppContextProvider>
<Router>
<div>
<Header />
<Routes />
<Footer />
</div>
</Router>
</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();
@ -13,7 +16,13 @@ const AppContextProvider = ({ children }) => {
}
};
return (
<AppContext.Provider value={defaultState}>{children}</AppContext.Provider>
<Suspense fallback={<Loader/>} >
<Router>
<AppContext.Provider value={defaultState}>
{children}
</AppContext.Provider>
</Router>
</Suspense>
);
};
export default AppContextProvider;

@ -19,9 +19,8 @@ function Routes() {
<Route path="/create-election" component={CreateElection} />
<Route path="/vote/:slug" component={Vote} />
<Route path="/result/:slug" component={Result} />
<Route path="/create-success/:slug" component={CreateSuccess} />
<Route path="/link/:slug" component={CreateSuccess} closed="true" />
<Route path="/links/:slug" component={CreateSuccess} closed="false" />
<Route path="/link/:slug" component={props => <CreateSuccess invitationOnly={true} {...props} />} />
<Route path="/links/:slug" component={props => <CreateSuccess invitationOnly={false} {...props} />} />
<Route path="/vote-success/:slug" component={VoteSuccess} />
<Route path="/unknown-election/:slug" component={UnknownElection} />
<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({
title: title,
candidates: candidates.map(c => c.label),
candidates: candidates.map(c => c.label).filter( c => c !== ""),
on_invitation_only: electorEmails.length > 0,
num_grades: numGrades,
elector_emails: electorEmails,
@ -279,13 +279,16 @@ class CreateElection extends Component {
.then(result => {
console.log(result);
if (result.id) {
const nextPage =
electorEmails && electorEmails.length
? `/link/${result.id}`
: `/links/${result.id}`;
this.setState(state => ({
redirectTo: '/create-success/' + result.id,
redirectTo: nextPage,
successCreate: true,
waiting: false
}))
}
else {
waiting: false,
}));
} else {
toast.error(t('Unknown error. Try again please.'), {
position: toast.POSITION.TOP_CENTER,
});
@ -314,7 +317,7 @@ class CreateElection extends Component {
numGrades,
isAdvancedOptionsOpen,
numCandidatesWithLabel,
electorEmails
electorEmails,
} = this.state;
const {t} = this.props;

@ -6,34 +6,25 @@ import {faCopy, faUsers} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import logoLine from '../../logos/logo-line-white.svg';
import {AppContext} from '../../AppContext';
import CopyField from '../CopyField';
class CreateSuccess extends Component {
static contextType = AppContext;
constructor(props) {
super(props);
console.log(props);
const electionSlug = this.props.match.params.slug;
this.state = {
urlOfVote:
'https://' + window.location.hostname + '/vote/' + electionSlug,
urlOfResult:
'https://' + window.location.hostname + '/result/' + electionSlug,
urlOfVote: `https://${window.location.hostname}/vote/${electionSlug}`,
urlOfResult: `https://${window.location.hostname}/result/${electionSlug}`,
};
this.urlVoteField = 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 => {
const input = this.urlResultField.current;
input.focus();
@ -43,6 +34,26 @@ class CreateSuccess extends Component {
render() {
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 (
<Container>
<Row>
@ -53,54 +64,17 @@ class CreateSuccess extends Component {
<Row className="mt-4">
<Col className="text-center offset-lg-3" lg="6">
<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 ">
<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>
{electionLink}
<p className="mt-4 mb-1">
{t('Here is the link for the results in real time:')}
</p>
<div className="input-group ">
<input
type="text"
className="form-control"
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>
<CopyField
value={this.state.urlOfResult}
icon={faCopy}
t={t}
/>
</Col>
</Row>
<Row className="mt-4 mb-4">
@ -126,7 +100,7 @@ class CreateSuccess extends Component {
to={'/vote/' + this.props.match.params.slug}
className="btn btn-success">
<FontAwesomeIcon icon={faUsers} className="mr-2" />
{t("Participate now!")}
{t('Participate now!')}
</Link>
</Col>
</Row>

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

Loading…
Cancel
Save