Compare commits

...

5 commits

16 changed files with 674 additions and 402 deletions

View file

@ -3,8 +3,7 @@
import * as React from 'react';
import {withTheme} from 'react-native-paper';
import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
import NetworkErrorComponent from "../Custom/NetworkErrorComponent";
import i18n from 'i18n-js';
import ErrorView from "../Custom/ErrorView";
import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
type Props = {
@ -49,7 +48,7 @@ class AuthenticatedScreen extends React.Component<Props, State> {
this.setState({loading: true});
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < this.props.links.length; i++) {
this.connectionManager.authenticatedRequest(this.props.links[i].link)
this.connectionManager.authenticatedRequest(this.props.links[i].link, null, null)
.then((data) => {
this.onFinishedLoading(data, i, -1);
})
@ -98,37 +97,18 @@ class AuthenticatedScreen extends React.Component<Props, State> {
}
getErrorRender() {
let message;
let icon;
switch (this.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
message = i18n.t("loginScreen.errors.credentials");
icon = "account-alert-outline";
break;
case ERROR_TYPE.BAD_TOKEN:
message = "BAD TOKEN"; // TODO translate
icon = "access-point-network-off";
break;
case ERROR_TYPE.CONNECTION_ERROR:
message = i18n.t("loginScreen.errors.connection");
icon = "access-point-network-off";
break;
default:
message = i18n.t("loginScreen.errors.unknown");
icon = "alert-circle-outline";
break;
}
return (
<NetworkErrorComponent
{...this.props}
icon={icon}
message={message}
<ErrorView
errorCode={this.errorCode}
onRefresh={this.fetchData}
/>
);
}
reload() {
this.fetchData();
}
render() {
return (
this.state.loading

View file

@ -0,0 +1,105 @@
// @flow
import * as React from 'react';
import {Avatar, Card, List, ProgressBar, Subheading, withTheme} from "react-native-paper";
import {FlatList, StyleSheet} from "react-native";
import i18n from 'i18n-js';
type Props = {
teams: Array<Object>,
dateEnd: string,
}
class VoteResults extends React.Component<Props> {
totalVotes: number;
winnerId: number;
colors: Object;
constructor(props) {
super();
this.colors = props.theme.colors;
props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams);
this.getWinnerId(props.teams);
}
shouldComponentUpdate() {
return false;
}
sortByVotes = (a: Object, b: Object) => b.votes - a.votes;
getTotalVotes(teams: Array<Object>) {
this.totalVotes = 0;
for (let i = 0; i < teams.length; i++) {
this.totalVotes += teams[i].votes;
}
}
getWinnerId(teams: Array<Object>) {
this.winnerId = teams[0].id;
}
voteKeyExtractor = (item: Object) => item.id.toString();
resultRenderItem = ({item}: Object) => {
const isWinner = this.winnerId === item.id;
return (
<Card style={{
marginTop: 10,
elevation: isWinner ? 5 : 3,
}}>
<List.Item
title={item.name}
description={item.votes + ' ' + i18n.t('voteScreen.results.votes')}
left={props => isWinner
? <List.Icon {...props} icon="trophy" color={this.colors.primary}/>
: null}
titleStyle={{
color: isWinner
? this.colors.primary
: this.colors.text
}}
style={{padding: 0}}
/>
<ProgressBar progress={item.votes / this.totalVotes} color={this.colors.primary}/>
</Card>
);
};
render() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('voteScreen.results.title')}
subtitle={i18n.t('voteScreen.results.subtitle') + ' ' + this.props.dateEnd}
left={(props) => <Avatar.Icon
{...props}
icon={"podium-gold"}
/>}
/>
<Card.Content>
<Subheading>{i18n.t('voteScreen.results.totalVotes') + ' ' +this.totalVotes}</Subheading>
{/*$FlowFixMe*/}
<FlatList
data={this.props.teams}
keyExtractor={this.voteKeyExtractor}
renderItem={this.resultRenderItem}
/>
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});
export default withTheme(VoteResults);

View file

@ -0,0 +1,136 @@
// @flow
import * as React from 'react';
import {Avatar, Button, Card, RadioButton} from "react-native-paper";
import {FlatList, StyleSheet, View} from "react-native";
import ConnectionManager from "../../../managers/ConnectionManager";
import LoadingConfirmDialog from "../../Dialog/LoadingConfirmDialog";
import ErrorDialog from "../../Dialog/ErrorDialog";
import i18n from 'i18n-js';
type Props = {
teams: Array<Object>,
onVoteSuccess: Function,
onVoteError: Function,
}
type State = {
selectedTeam: string,
voteDialogVisible: boolean,
errorDialogVisible: boolean,
currentError: number,
}
export default class VoteSelect extends React.PureComponent<Props, State> {
state = {
selectedTeam: "none",
voteDialogVisible: false,
errorDialogVisible: false,
currentError: 0,
};
onVoteSelectionChange = (team: string) => this.setState({selectedTeam: team});
voteKeyExtractor = (item: Object) => item.id.toString();
voteRenderItem = ({item}: Object) => <RadioButton.Item label={item.name} value={item.id.toString()}/>;
showVoteDialog = () => this.setState({voteDialogVisible: true});
onVoteDialogDismiss = () => this.setState({voteDialogVisible: false,});
onVoteDialogAccept = async () => {
return new Promise((resolve, reject) => {
ConnectionManager.getInstance().authenticatedRequest(
"elections/vote",
["vote"],
[parseInt(this.state.selectedTeam)])
.then(() => {
this.onVoteDialogDismiss();
this.props.onVoteSuccess();
resolve();
})
.catch((error: number) => {
this.onVoteDialogDismiss();
this.showErrorDialog(error);
resolve();
});
});
};
showErrorDialog = (error: number) => this.setState({
errorDialogVisible: true,
currentError: error,
});
onErrorDialogDismiss = () => {
this.setState({errorDialogVisible: false});
this.props.onVoteError();
};
render() {
return (
<View>
<Card style={styles.card}>
<Card.Title
title={i18n.t('voteScreen.select.title')}
subtitle={i18n.t('voteScreen.select.subtitle')}
left={(props) => <Avatar.Icon
{...props}
icon={"alert-decagram"}
/>}
/>
<Card.Content>
<RadioButton.Group
onValueChange={this.onVoteSelectionChange}
value={this.state.selectedTeam}
>
{/*$FlowFixMe*/}
<FlatList
data={this.props.teams}
keyExtractor={this.voteKeyExtractor}
extraData={this.state.selectedTeam}
renderItem={this.voteRenderItem}
/>
</RadioButton.Group>
</Card.Content>
<Card.Actions>
<Button
icon="send"
mode="contained"
onPress={this.showVoteDialog}
style={{marginLeft: 'auto'}}
disabled={this.state.selectedTeam === "none"}
>
{i18n.t('voteScreen.select.sendButton')}
</Button>
</Card.Actions>
</Card>
<LoadingConfirmDialog
visible={this.state.voteDialogVisible}
onDismiss={this.onVoteDialogDismiss}
onAccept={this.onVoteDialogAccept}
title={i18n.t('voteScreen.select.dialogTitle')}
titleLoading={i18n.t('voteScreen.select.dialogTitleLoading')}
message={i18n.t('voteScreen.select.dialogMessage')}
/>
<ErrorDialog
visible={this.state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={this.state.currentError}
/>
</View>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});

View file

@ -0,0 +1,45 @@
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph} from "react-native-paper";
import {StyleSheet} from "react-native";
import i18n from 'i18n-js';
type Props = {
startDate: string,
}
export default class VoteTease extends React.Component<Props> {
shouldComponentUpdate() {
return false;
}
render() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('voteScreen.tease.title')}
subtitle={i18n.t('voteScreen.tease.subtitle')}
left={props => <Avatar.Icon
{...props}
icon="vote"/>}
/>
<Card.Content>
<Paragraph>
{i18n.t('voteScreen.tease.message') + ' ' + this.props.startDate}
</Paragraph>
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});

View file

@ -0,0 +1,50 @@
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph} from "react-native-paper";
import {StyleSheet} from "react-native";
import i18n from 'i18n-js';
const ICON_AMICALE = require('../../../../assets/amicale.png');
type Props = {}
export default class VoteTitle extends React.Component<Props> {
shouldComponentUpdate() {
return false;
}
render() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('voteScreen.title.title')}
subtitle={i18n.t('voteScreen.title.subtitle')}
left={(props) => <Avatar.Image
{...props}
source={ICON_AMICALE}
style={styles.icon}
/>}
/>
<Card.Content>
<Paragraph>
{i18n.t('voteScreen.title.paragraph1')}
</Paragraph>
<Paragraph>
{i18n.t('voteScreen.title.paragraph2')}
</Paragraph>
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});

View file

@ -0,0 +1,75 @@
// @flow
import * as React from 'react';
import {ActivityIndicator, Card, Paragraph, withTheme} from "react-native-paper";
import {StyleSheet} from "react-native";
import i18n from 'i18n-js';
type Props = {
startDate: string | null,
justVoted: boolean,
hasVoted: boolean,
isVoteRunning: boolean,
}
class VoteWait extends React.Component<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
shouldComponentUpdate() {
return false;
}
render() {
return (
<Card style={styles.card}>
<Card.Title
title={this.props.isVoteRunning
? i18n.t('voteScreen.wait.titleSubmitted')
: i18n.t('voteScreen.wait.titleEnded')}
subtitle={i18n.t('voteScreen.wait.subtitle')}
left={(props) => <ActivityIndicator {...props}/>}
/>
<Card.Content>
{
this.props.justVoted
? <Paragraph style={{color: this.colors.success}}>
{i18n.t('voteScreen.wait.messageSubmitted')}
</Paragraph>
: null
}
{
this.props.hasVoted
? <Paragraph style={{color: this.colors.success}}>
{i18n.t('voteScreen.wait.messageVoted')}
</Paragraph>
: null
}
{
this.props.startDate !== null
? <Paragraph>
{i18n.t('voteScreen.wait.messageDate') + ' ' + this.props.startDate}
</Paragraph>
: <Paragraph>{i18n.t('voteScreen.wait.messageDateUndefined')}</Paragraph>
}
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});
export default withTheme(VoteWait);

View file

@ -5,10 +5,10 @@ import {Button, Subheading, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native";
import {MaterialCommunityIcons} from "@expo/vector-icons";
import i18n from 'i18n-js';
import {ERROR_TYPE} from "../../managers/ConnectionManager";
type Props = {
message: string,
icon: string,
errorCode: number,
onRefresh: Function,
}
@ -16,10 +16,13 @@ type State = {
refreshing: boolean,
}
class NetworkErrorComponent extends React.PureComponent<Props, State> {
class ErrorView extends React.PureComponent<Props, State> {
colors: Object;
message: string;
icon: string;
state = {
refreshing: false,
};
@ -29,7 +32,45 @@ class NetworkErrorComponent extends React.PureComponent<Props, State> {
this.colors = props.theme.colors;
}
generateMessage() {
switch (this.props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
this.message = i18n.t("errors.badCredentials");
this.icon = "account-alert-outline";
break;
case ERROR_TYPE.BAD_TOKEN:
this.message = i18n.t("errors.badToken");
this.icon = "account-alert-outline";
break;
case ERROR_TYPE.NO_CONSENT:
this.message = i18n.t("errors.noConsent");
this.icon = "account-remove-outline";
break;
case ERROR_TYPE.BAD_INPUT:
this.message = i18n.t("errors.badInput");
this.icon = "alert-circle-outline";
break;
case ERROR_TYPE.FORBIDDEN:
this.message = i18n.t("errors.forbidden");
this.icon = "lock";
break;
case ERROR_TYPE.CONNECTION_ERROR:
this.message = i18n.t("errors.connectionError");
this.icon = "access-point-network-off";
break;
case ERROR_TYPE.SERVER_ERROR:
this.message = i18n.t("errors.serverError");
this.icon = "server-network-off";
break;
default:
this.message = i18n.t("errors.unknown");
this.icon = "alert-circle-outline";
break;
}
}
render() {
this.generateMessage();
return (
<View style={{
...styles.outer,
@ -38,7 +79,7 @@ class NetworkErrorComponent extends React.PureComponent<Props, State> {
<View style={styles.inner}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={this.props.icon}
name={this.icon}
size={150}
color={this.colors.textDisabled}/>
</View>
@ -46,7 +87,7 @@ class NetworkErrorComponent extends React.PureComponent<Props, State> {
...styles.subheading,
color: this.colors.textDisabled
}}>
{this.props.message}
{this.message}
</Subheading>
<Button
mode={'contained'}
@ -86,4 +127,4 @@ const styles = StyleSheet.create({
});
export default withTheme(NetworkErrorComponent);
export default withTheme(ErrorView);

View file

@ -15,22 +15,31 @@ class ErrorDialog extends React.PureComponent<Props> {
message: string;
generateMessage() {
this.title = i18n.t("loginScreen.errors.title");
this.title = i18n.t("errors.title");
switch (this.props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
this.message = i18n.t("loginScreen.errors.credentials");
this.message = i18n.t("errors.badCredentials");
break;
case ERROR_TYPE.BAD_TOKEN:
this.message = i18n.t("errors.badToken");
break;
case ERROR_TYPE.NO_CONSENT:
this.message = i18n.t("loginScreen.errors.consent");
this.message = i18n.t("errors.noConsent");
break;
case ERROR_TYPE.BAD_INPUT:
this.message = i18n.t("errors.badInput");
break;
case ERROR_TYPE.FORBIDDEN:
this.message = i18n.t("errors.forbidden");
break;
case ERROR_TYPE.CONNECTION_ERROR:
this.message = i18n.t("loginScreen.errors.connection");
this.message = i18n.t("errors.connectionError");
break;
case ERROR_TYPE.SERVER_ERROR:
this.message = "SERVER ERROR"; // TODO translate
this.message = i18n.t("errors.serverError");
break;
default:
this.message = i18n.t("loginScreen.errors.unknown");
this.message = i18n.t("errors.unknown");
break;
}
}

View file

@ -5,8 +5,9 @@ import {readData} from "../../utils/WebData";
import i18n from "i18n-js";
import {Snackbar} from 'react-native-paper';
import {RefreshControl, SectionList, View} from "react-native";
import NetworkErrorComponent from "../Custom/NetworkErrorComponent";
import ErrorView from "../Custom/ErrorView";
import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
import {ERROR_TYPE} from "../../managers/ConnectionManager";
type Props = {
navigation: Object,
@ -192,9 +193,8 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
style={{minHeight: '100%'}}
ListEmptyComponent={this.state.refreshing
? <BasicLoadingScreen/>
: <NetworkErrorComponent
message={i18n.t('general.networkError')}
icon={"access-point-network-off"}
: <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefresh}/>
}
getItemLayout={this.props.itemHeight !== undefined ? this.itemLayout : undefined}

View file

@ -2,11 +2,10 @@
import * as React from 'react';
import WebView from "react-native-webview";
import {withTheme} from 'react-native-paper';
import HeaderButton from "../Custom/HeaderButton";
import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
import NetworkErrorComponent from "../Custom/NetworkErrorComponent";
import i18n from "i18n-js";
import ErrorView from "../Custom/ErrorView";
import {ERROR_TYPE} from "../../managers/ConnectionManager";
type Props = {
navigation: Object,
@ -32,20 +31,12 @@ class WebViewScreen extends React.PureComponent<Props> {
hasSideMenu: true,
hasFooter: true,
};
webviewRef: Object;
onRefreshClicked: Function;
onWebviewRef: Function;
getRenderLoading: Function;
colors: Object;
constructor(props) {
super(props);
this.onRefreshClicked = this.onRefreshClicked.bind(this);
this.onWebviewRef = this.onWebviewRef.bind(this);
this.getRenderLoading = this.getRenderLoading.bind(this);
this.colors = props.theme.colors;
constructor() {
super();
this.webviewRef = React.createRef();
}
/**
@ -70,33 +61,19 @@ class WebViewScreen extends React.PureComponent<Props> {
/**
* Callback to use when refresh button is clicked. Reloads the webview.
*/
onRefreshClicked() {
if (this.webviewRef !== null)
this.webviewRef.reload();
}
/**
* Callback used when receiving the webview ref. Stores the ref for later use
*
* @param ref
*/
onWebviewRef(ref: Object) {
this.webviewRef = ref
}
onRefreshClicked = () => this.webviewRef.current.reload();
/**
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading() {
return <BasicLoadingScreen/>;
}
getRenderLoading = () => <BasicLoadingScreen/>;
render() {
return (
<WebView
ref={this.onWebviewRef}
ref={this.webviewRef}
source={{uri: this.props.data[0]['url']}}
style={{
width: '100%',
@ -106,15 +83,13 @@ class WebViewScreen extends React.PureComponent<Props> {
injectedJavaScript={this.props.data[0]['customJS']}
javaScriptEnabled={true}
renderLoading={this.getRenderLoading}
renderError={() => <NetworkErrorComponent
{...this.props}
renderError={() => <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
message={i18n.t("loginScreen.errors.connection")}
icon={'access-point-network-off'}
/>}
/>
);
}
}
export default withTheme(WebViewScreen);
export default WebViewScreen;

View file

@ -70,7 +70,7 @@ class SideBar extends React.Component<Props, State> {
onlyWhenLoggedIn: true,
},
{
name: "VOTE",
name: i18n.t('screens.vote'),
route: "VoteScreen",
icon: "vote",
onlyWhenLoggedIn: true,

View file

@ -190,11 +190,11 @@ export default class ConnectionManager {
return data;
}
async authenticatedRequest(path: string, keys: Array<string>, values: Array<any>) {
async authenticatedRequest(path: string, keys: Array<string>|null, values: Array<any>|null) {
return new Promise((resolve, reject) => {
if (this.getToken() !== null) {
let data = {};
if (keys !== undefined && values !== undefined && keys.length === values.length)
if (keys !== null && values !== null && keys.length === values.length)
data = this.generatePostArguments(keys, values);
console.log(data);
fetch(API_ENDPOINT + path, {

View file

@ -256,7 +256,7 @@ function VoteStackComponent() {
options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: "VoteScreen",
title: i18n.t('screens.vote'),
headerLeft: openDrawer
};
}}

View file

@ -1,31 +1,20 @@
// @flow
import * as React from 'react';
import {FlatList, StyleSheet, View} from "react-native";
import {
ActivityIndicator,
Avatar,
Button,
Card,
List,
Paragraph,
ProgressBar,
RadioButton,
Subheading,
withTheme
} from 'react-native-paper';
import {FlatList, View} from "react-native";
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
import {getTimeOnlyString, stringToDate} from "../../utils/Planning";
import LoadingConfirmDialog from "../../components/Dialog/LoadingConfirmDialog";
import ConnectionManager from "../../managers/ConnectionManager";
const ICON_AMICALE = require('../../../assets/amicale.png');
import VoteTitle from "../../components/Amicale/Vote/VoteTitle";
import VoteTease from "../../components/Amicale/Vote/VoteTease";
import VoteSelect from "../../components/Amicale/Vote/VoteSelect";
import VoteResults from "../../components/Amicale/Vote/VoteResults";
import VoteWait from "../../components/Amicale/Vote/VoteWait";
const FAKE_DATE = {
"date_begin": "2020-04-06 21:50",
"date_end": "2020-04-07 23:50",
"date_result_begin": "2020-04-07 21:50",
"date_result_end": "2020-04-07 21:50",
"date_begin": "2020-04-07 21:50",
"date_end": "2020-04-06 23:50",
"date_result_begin": "2020-04-06 21:50",
"date_result_end": "2020-04-07 22:50",
};
const FAKE_DATE2 = {
@ -64,25 +53,18 @@ const FAKE_TEAMS2 = {
],
};
type Props = {
navigation: Object,
theme: Object,
}
type Props = {}
type State = {
selectedTeam: string,
voteDialogVisible: boolean,
hasVoted: boolean,
}
class VoteScreen extends React.Component<Props, State> {
export default class VoteScreen extends React.Component<Props, State> {
state = {
selectedTeam: "none",
voteDialogVisible: false,
hasVoted: false,
};
colors: Object;
teams: Array<Object>;
hasVoted: boolean;
datesString: Object;
@ -91,80 +73,21 @@ class VoteScreen extends React.Component<Props, State> {
today: Date;
mainFlatListData: Array<Object>;
totalVotes: number;
constructor(props) {
super(props);
this.colors = props.theme.colors;
authRef: Object;
constructor() {
super();
this.hasVoted = false;
this.today = new Date();
this.authRef = React.createRef();
this.mainFlatListData = [
{key: 'main'},
{key: 'info'},
]
}
mainRenderItem = ({item}: Object) => {
if (item.key === 'info')
return this.getTitleCard();
else if (item.key === 'main' && this.isVoteAvailable())
return this.getContent();
else
return null;
};
getScreen = (data: Array<Object>) => {
data[0] = FAKE_TEAMS2;
data[1] = FAKE_DATE;
if (data[0] !== null) {
this.teams = data[0].teams;
this.hasVoted = data[0].has_voted;
}
this.datesString = data[1];
this.generateDateObject();
return (
<View>
{/*$FlowFixMe*/}
<FlatList
data={this.mainFlatListData}
extraData={this.state.selectedTeam}
renderItem={this.mainRenderItem}
/>
<LoadingConfirmDialog
{...this.props}
visible={this.state.voteDialogVisible}
onDismiss={this.onVoteDialogDismiss}
onAccept={this.onVoteDialogAccept}
title={"VOTE?"}
titleLoading={"SENDING VOTE..."}
message={"SURE?"}
/>
</View>
);
};
onVoteDialogDismiss = () => this.setState({voteDialogVisible: false});
showVoteDialog = () => this.setState({voteDialogVisible: true});
onVoteDialogAccept = async () => {
return new Promise((resolve, reject) => {
ConnectionManager.getInstance().authenticatedRequest(
"elections/vote",
["vote"],
[parseInt(this.state.selectedTeam)])
.then(() => {
this.onVoteDialogDismiss();
resolve();
})
.catch(() => {
this.onVoteDialogDismiss();
resolve();
});
});
};
reloadData = () => this.authRef.current.reload();
generateDateObject() {
this.dates = {
@ -175,6 +98,14 @@ class VoteScreen extends React.Component<Props, State> {
};
}
getDateString(date: Date, dateString: string): string {
if (this.today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString);
return str !== null ? str : "";
} else
return dateString;
}
isVoteAvailable() {
return this.dates.date_begin !== null;
}
@ -195,10 +126,41 @@ class VoteScreen extends React.Component<Props, State> {
return this.today > this.dates.date_result_begin;
}
mainRenderItem = ({item}: Object) => {
if (item.key === 'info')
return <VoteTitle/>;
else if (item.key === 'main' && this.isVoteAvailable())
return this.getContent();
else
return null;
};
getScreen = (data: Array<Object>) => {
data[0] = FAKE_TEAMS2;
data[1] = FAKE_DATE;
if (data[0] !== null) {
this.teams = data[0].teams;
this.hasVoted = data[0].has_voted;
}
this.datesString = data[1];
this.generateDateObject();
return (
<View>
{/*$FlowFixMe*/}
<FlatList
data={this.mainFlatListData}
extraData={this.state.hasVoted.toString()}
renderItem={this.mainRenderItem}
/>
</View>
);
};
getContent() {
if (!this.isVoteStarted())
return this.getTeaseVoteCard();
else if (this.isVoteRunning() && !this.hasVoted)
else if (this.isVoteRunning() && (!this.hasVoted && !this.state.hasVoted))
return this.getVoteCard();
else if (!this.isResultStarted())
return this.getWaitVoteCard();
@ -208,222 +170,47 @@ class VoteScreen extends React.Component<Props, State> {
return null;
}
getTitleCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTE"}
subtitle={"WHY"}
left={(props) => <Avatar.Image
{...props}
source={ICON_AMICALE}
style={styles.icon}
/>}
/>
<Card.Content>
<Paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rhoncus porttitor
suscipit. Quisque hendrerit, quam id vestibulum vestibulum, lorem nisi hendrerit nisi, a
eleifend sapien diam ut elit. Curabitur sit amet vulputate lectus. Donec semper cursus sapien
vel finibus.
</Paragraph>
<Paragraph>
Sed et venenatis turpis. Fusce malesuada magna urna, sed vehicula sem luctus in. Vivamus
faucibus vel eros a ultricies. In sed laoreet ante, luctus mattis tellus. Etiam vitae ipsum
sagittis, consequat purus sed, blandit risus.
</Paragraph>
</Card.Content>
</Card>
);
}
onVoteSelectionChange = (team: string) => {
this.setState({selectedTeam: team})
};
onVotePress = () => {
this.showVoteDialog();
};
voteKeyExtractor = (item: Object) => item.id.toString();
voteRenderItem = ({item}: Object) => <RadioButton.Item label={item.name} value={item.id.toString()}/>;
onVoteSuccess = () => this.setState({hasVoted: true});
/**
* The user has not voted yet, and the votes are open
*/
getVoteCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTE OPEN"}
subtitle={"VOTE NOW"}
left={(props) => <Avatar.Icon
{...props}
icon={"alert-decagram"}
/>}
/>
<Card.Content>
<RadioButton.Group
onValueChange={this.onVoteSelectionChange}
value={this.state.selectedTeam}
>
{/*$FlowFixMe*/}
<FlatList
data={this.teams}
keyExtractor={this.voteKeyExtractor}
extraData={this.state.selectedTeam}
renderItem={this.voteRenderItem}
/>
</RadioButton.Group>
</Card.Content>
<Card.Actions>
<Button
icon="send"
mode="contained"
onPress={this.onVotePress}
style={{marginLeft: 'auto'}}
disabled={this.state.selectedTeam === "none"}
>
SEND VOTE
</Button>
</Card.Actions>
</Card>
);
}
sortByVotes = (a: Object, b: Object) => b.votes - a.votes;
getTotalVotes() {
let count = 0;
for (let i = 0; i < this.teams.length; i++) {
count += this.teams[i].votes;
}
return count;
}
getWinnerId() {
return this.teams[0].id;
return <VoteSelect teams={this.teams} onVoteSuccess={this.onVoteSuccess} onVoteError={this.reloadData}/>;
}
/**
* Votes have ended, results can be displayed
*/
getVoteResultCard() {
this.totalVotes = this.getTotalVotes();
this.teams.sort(this.sortByVotes);
return (
<Card style={styles.card}>
<Card.Title
title={"RESULTS"}
subtitle={"AVAILABLE UNTIL " + this.getDateString(this.dates.date_result_end, this.datesString.date_result_end)}
left={(props) => <Avatar.Icon
{...props}
icon={"podium-gold"}
/>}
/>
<Card.Content>
<Subheading>TOTAL VOTES : {this.totalVotes}</Subheading>
{/*$FlowFixMe*/}
<FlatList
data={this.teams}
keyExtractor={this.voteKeyExtractor}
renderItem={this.resultRenderItem}
/>
</Card.Content>
</Card>
);
return <VoteResults teams={this.teams}
dateEnd={this.getDateString(this.dates.date_result_end, this.datesString.date_result_end)}/>;
}
resultRenderItem = ({item}: Object) => {
const isWinner = this.getWinnerId() === item.id;
return (
<Card style={{
marginTop: 10,
elevation: isWinner ? 5 : 3,
}}>
<List.Item
title={item.name}
description={item.votes + " VOTES"}
left={props => isWinner
? <List.Icon {...props} icon="trophy" color={this.colors.primary}/>
: null}
titleStyle={{
color: isWinner
? this.colors.primary
: this.colors.text
}}
style={{padding: 0}}
/>
<ProgressBar progress={item.votes / this.totalVotes} color={this.colors.primary}/>
</Card>
);
};
/**
* Vote will open shortly
*/
getTeaseVoteCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTE INCOMING"}
subtitle={"GET READY"}
left={props => <Avatar.Icon
{...props}
icon="vote"/>}
/>
<Card.Content>
<Paragraph>
VOTE STARTS
AT {this.getDateString(this.dates.date_begin, this.datesString.date_begin)}
</Paragraph>
</Card.Content>
</Card>
);
return <VoteTease startDate={this.getDateString(this.dates.date_begin, this.datesString.date_begin)}/>;
}
/**
* Votes have ended waiting for results
* Votes have ended, or user has voted waiting for results
*/
getWaitVoteCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTES HAVE ENDED"}
subtitle={"WAITING FOR RESULTS"}
left={(props) => <ActivityIndicator {...props}/>}
/>
<Card.Content>
{
this.hasVoted
? <Paragraph>THX FOR THE VOTE</Paragraph>
: null
}
{
this.dates.date_result_begin !== null
? <Paragraph>
RESULTS AVAILABLE
AT {this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin)}
</Paragraph>
: <Paragraph>RESULTS AVAILABLE SHORTLY</Paragraph>
}
</Card.Content>
</Card>
);
}
getDateString(date: Date, dateString: string) {
if (this.today.getDate() === date.getDate())
return getTimeOnlyString(dateString);
else
return dateString;
let startDate = null;
if (this.dates.date_result_begin !== null)
startDate = this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin);
return <VoteWait startDate={startDate} hasVoted={this.hasVoted || this.state.hasVoted}
justVoted={this.state.hasVoted}
isVoteRunning={this.isVoteRunning()}/>;
}
render() {
return (
<AuthenticatedScreen
{...this.props}
ref={this.authRef}
links={[
{
link: 'elections/teams',
@ -439,14 +226,3 @@ class VoteScreen extends React.Component<Props, State> {
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});
export default withTheme(VoteScreen);

View file

@ -15,7 +15,8 @@
"debug": "Debug",
"login": "Login",
"logout": "Logout",
"profile": "Profile"
"profile": "Profile",
"vote": "Elections"
},
"sidenav": {
"divider1": "Student websites",
@ -236,15 +237,18 @@
"whyAccountSub": "What can you do wth an account",
"whyAccountParagraph": "An Amicale account allows you to take part in several activities around campus. You can join a club, or even create your own!",
"whyAccountParagraph2": "Logging into your Amicale account on the app will allow you to see all available clubs on the campus, vote for the upcoming elections, and more to come!",
"noAccount": "No Account? Go to the Amicale's building during open hours to create one.",
"errors": {
"title": "Error!",
"connection": "Network error. Please check your internet connection.",
"credentials": "Email or password invalid.",
"saveToken": "Failed to save connection information, please contact support.",
"consent": "You did not give your consent for data processing to the Amicale.",
"unknown": "Unknown error, please contact support."
}
"noAccount": "No Account? Go to the Amicale's building during open hours to create one."
},
"errors": {
"title": "Error!",
"badCredentials": "Email or password invalid.",
"badToken": "Session expired, please login again.",
"noConsent": "You did not give your consent for data processing to the Amicale.",
"badInput": "Invalid input. Please try again.",
"forbidden": "You do not have access to this data.",
"connectionError": "Network error. Please check your internet connection.",
"serverError": "Server error. Please contact support.",
"unknown": "Unknown error. Please contact support."
},
"clubs": {
"clubList": "Club list",
@ -254,6 +258,42 @@
"categories": "Categories",
"categoriesFilterMessage": "Click on a category to filter the list"
},
"voteScreen": {
"select": {
"title": "Elections open",
"subtitle": "Vote now!",
"sendButton": "Send Vote",
"dialogTitle": "Send Vote?",
"dialogTitleLoading": "Sending vote...",
"dialogMessage": "Are you sure you want to send your vote? You will not be able to change it."
},
"tease": {
"title": "Elections incoming",
"subtitle": "Be ready to vote!",
"message" : "Vote start:"
},
"wait": {
"titleSubmitted": "Vote submitted!",
"titleEnded": "Votes closed",
"subtitle" : "Waiting for results...",
"messageSubmitted" : "Vote submitted successfully.",
"messageVoted" : "Thank you for your participation.",
"messageDate" : "Results available:",
"messageDateUndefined" : "Results will be available shortly"
},
"results": {
"title": "Results",
"subtitle": "Available until:",
"totalVotes" : "Total votes:",
"votes" : "votes"
},
"title": {
"title": "The Elections",
"subtitle": "Why your vote is important",
"paragraph1" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rhoncus porttitor suscipit. Quisque hendrerit, quam id vestibulum vestibulum, lorem nisi hendrerit nisi, a eleifend sapien diam ut elit. Curabitur sit amet vulputate lectus. Donec semper cursus sapienvel finibus.",
"paragraph2" : "Sed et venenatis turpis. Fusce malesuada magna urna, sed vehicula sem luctus in. Vivamus faucibus vel eros a ultricies. In sed laoreet ante, luctus mattis tellus. Etiam vitae ipsum sagittis, consequat purus sed, blandit risus."
}
},
"dialog": {
"ok": "OK",
"yes": "Yes",

View file

@ -15,7 +15,8 @@
"debug": "Debug",
"login": "Se Connecter",
"logout": "Se Déconnecter",
"profile": "Profil"
"profile": "Profil",
"vote": "Élections"
},
"sidenav": {
"divider1": "Sites étudiants",
@ -236,15 +237,18 @@
"whyAccountSub": "Ce que vous pouvez faire avec un compte",
"whyAccountParagraph": "Un compte Amicale vous donne la possibilité de participer à diverses activités sur le campus. Vous pouvez rejoindre des clubs ou même créer le votre !",
"whyAccountParagraph2": "Vous connecter à votre compte Amicale sur l'appli vous permettra de voir tous les clubs en activité, de voter pour les prochaines élections, et plus à venir !",
"noAccount": "Pas de compte ? Passez à l'Amicale pendant une perm pour en créer un.",
"errors": {
"title": "Erreur !",
"connection": "Erreur de réseau. Merci de vérifier votre connexion Internet.",
"credentials": "Email ou mot de passe invalide.",
"saveToken": "Erreur de sauvegarde des informations de connexion, merci de contacter le support.",
"consent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
"unknown": "Erreur inconnue, merci de contacter le support."
}
"noAccount": "Pas de compte ? Passez à l'Amicale pendant une perm pour en créer un."
},
"errors": {
"title": "Erreur !",
"badCredentials": "Email ou mot de passe invalide.",
"badToken": "Session expirée, merci de vous reconnecter.",
"noConsent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
"badInput": "Entrée invalide. Merci de réessayer.",
"forbidden": "Vous n'avez pas accès à cette information.",
"connectionError": "Erreur de réseau. Merci de vérifier votre connexion Internet.",
"serverError": "Erreur de serveur. Merci de contacter le support.",
"unknown": "Erreur inconnue. Merci de contacter le support."
},
"clubs": {
"clubList": "Liste des clubs",
@ -254,6 +258,42 @@
"categories": "Catégories",
"categoriesFilterMessage": "Cliquez sur une catégorie pour filtrer la liste"
},
"voteScreen": {
"select": {
"title": "Élections ouvertes",
"subtitle": "Votez maintenant !",
"sendButton": "Envoyer votre vote",
"dialogTitle": "Envoyer votre vote ?",
"dialogTitleLoading": "Envoi du vote...",
"dialogMessage": "Êtes vous sûr de vouloir envoyer votre vote ? Vous ne pourrez plus le changer."
},
"tease": {
"title": "Les élections arrivent",
"subtitle": "Préparez vous à voter !",
"message" : "Début des votes :"
},
"wait": {
"titleSubmitted": "Vote envoyé !",
"titleEnded": "Votes fermés",
"subtitle" : "Attente des résultats...",
"messageSubmitted" : "Votre vote a bien été envoyé.",
"messageVoted" : "Merci pour votre participation.",
"messageDate" : "Disponibilité des résultats :",
"messageDateUndefined" : "les résultats seront disponibles sous peu."
},
"results": {
"title": "Résultats",
"subtitle": "Disponibles jusqu'à :",
"totalVotes" : "Nombre total de votes :",
"votes" : "votes"
},
"title": {
"title": "Les Élections",
"subtitle": "Pourquoi votre vote est important",
"paragraph1" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rhoncus porttitor suscipit. Quisque hendrerit, quam id vestibulum vestibulum, lorem nisi hendrerit nisi, a eleifend sapien diam ut elit. Curabitur sit amet vulputate lectus. Donec semper cursus sapienvel finibus.",
"paragraph2" : "Sed et venenatis turpis. Fusce malesuada magna urna, sed vehicula sem luctus in. Vivamus faucibus vel eros a ultricies. In sed laoreet ante, luctus mattis tellus. Etiam vitae ipsum sagittis, consequat purus sed, blandit risus."
}
},
"dialog": {
"ok": "OK",
"yes": "Oui",