Update Amicale and related components to use TypeScript

This commit is contained in:
Arnaud Vergnet 2020-09-22 11:34:18 +02:00
parent 18ec6e0a59
commit f95635136e
17 changed files with 400 additions and 476 deletions

View file

@ -17,37 +17,34 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {StackNavigationProp} from '@react-navigation/stack';
import ConnectionManager from '../../managers/ConnectionManager';
import type {ApiGenericDataType} from '../../utils/WebData';
import {ERROR_TYPE} from '../../utils/WebData';
import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
type PropsType = {
navigation: StackNavigationProp,
type PropsType<T> = {
navigation: StackNavigationProp<any>;
requests: Array<{
link: string,
params: {...},
mandatory: boolean,
}>,
renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
link: string;
params: object;
mandatory: boolean;
}>;
renderFunction: (data: Array<T | null>) => React.ReactNode;
errorViewOverride?: Array<{
errorCode: number,
message: string,
icon: string,
showRetryButton: boolean,
}> | null,
errorCode: number;
message: string;
icon: string;
showRetryButton: boolean;
}> | null;
};
type StateType = {
loading: boolean,
loading: boolean;
};
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
static defaultProps = {
errorViewOverride: null,
};
@ -58,13 +55,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
errors: Array<number>;
fetchedData: Array<ApiGenericDataType | null>;
fetchedData: Array<T | null>;
constructor(props: PropsType) {
constructor(props: PropsType<T>) {
super(props);
this.state = {
loading: true,
};
this.currentUserToken = null;
this.connectionManager = ConnectionManager.getInstance();
props.navigation.addListener('focus', this.onScreenFocus);
this.fetchedData = new Array(props.requests.length);
@ -91,20 +89,20 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
* @param index The index for the data
* @param error The error code received
*/
onRequestFinished(
data: ApiGenericDataType | null,
index: number,
error?: number,
) {
onRequestFinished(data: T | null, index: number, error?: number) {
const {props} = this;
if (index >= 0 && index < props.requests.length) {
this.fetchedData[index] = data;
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
}
// Token expired, logout user
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect();
if (error === ERROR_TYPE.BAD_TOKEN) {
this.connectionManager.disconnect();
}
if (this.allRequestsFinished()) this.setState({loading: false});
if (this.allRequestsFinished()) {
this.setState({loading: false});
}
}
/**
@ -132,7 +130,7 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
*
* @return {*}
*/
getErrorRender(): React.Node {
getErrorRender() {
const {props} = this;
const errorCode = this.getError();
let shouldOverride = false;
@ -169,18 +167,18 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
*/
fetchData = () => {
const {state, props} = this;
if (!state.loading) this.setState({loading: true});
if (!state.loading) {
this.setState({loading: true});
}
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < props.requests.length; i += 1) {
this.connectionManager
.authenticatedRequest(
.authenticatedRequest<T>(
props.requests[i].link,
props.requests[i].params,
)
.then((response: ApiGenericDataType): void =>
this.onRequestFinished(response, i),
)
.then((response: T): void => this.onRequestFinished(response, i))
.catch((error: number): void =>
this.onRequestFinished(null, i, error),
);
@ -200,7 +198,9 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
allRequestsFinished(): boolean {
let finished = true;
this.errors.forEach((error: number | null) => {
if (error == null) finished = false;
if (error == null) {
finished = false;
}
});
return finished;
}
@ -212,11 +212,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
this.fetchData();
}
render(): React.Node {
render() {
const {state, props} = this;
if (state.loading) return <BasicLoadingScreen />;
if (this.getError() === ERROR_TYPE.SUCCESS)
if (state.loading) {
return <BasicLoadingScreen />;
}
if (this.getError() === ERROR_TYPE.SUCCESS) {
return props.renderFunction(this.fetchedData);
}
return this.getErrorRender();
}
}

View file

@ -17,28 +17,25 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import ConnectionManager from '../../managers/ConnectionManager';
import {useNavigation} from '@react-navigation/native';
type PropsType = {
navigation: StackNavigationProp,
visible: boolean,
onDismiss: () => void,
visible: boolean;
onDismiss: () => void;
};
class LogoutDialog extends React.PureComponent<PropsType> {
onClickAccept = async (): Promise<void> => {
const {props} = this;
function LogoutDialog(props: PropsType) {
const navigation = useNavigation();
const onClickAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => {
ConnectionManager.getInstance()
.disconnect()
.then(() => {
props.navigation.reset({
navigation.reset({
index: 0,
routes: [{name: 'main'}],
});
@ -48,19 +45,16 @@ class LogoutDialog extends React.PureComponent<PropsType> {
});
};
render(): React.Node {
const {props} = this;
return (
<LoadingConfirmDialog
visible={props.visible}
onDismiss={props.onDismiss}
onAccept={this.onClickAccept}
title={i18n.t('dialog.disconnect.title')}
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
message={i18n.t('dialog.disconnect.message')}
/>
);
}
return (
<LoadingConfirmDialog
visible={props.visible}
onDismiss={props.onDismiss}
onAccept={onClickAccept}
title={i18n.t('dialog.disconnect.title')}
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
message={i18n.t('dialog.disconnect.message')}
/>
);
}
export default LogoutDialog;

View file

@ -17,42 +17,29 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import React from 'react';
import {View} from 'react-native';
import {Headline, withTheme} from 'react-native-paper';
import {Headline, useTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
theme: CustomThemeType,
};
class VoteNotAvailable extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<View
function VoteNotAvailable() {
const theme = useTheme();
return (
<View
style={{
width: '100%',
marginTop: 10,
marginBottom: 10,
}}>
<Headline
style={{
width: '100%',
marginTop: 10,
marginBottom: 10,
color: theme.colors.textDisabled,
textAlign: 'center',
}}>
<Headline
style={{
color: props.theme.colors.textDisabled,
textAlign: 'center',
}}>
{i18n.t('screens.vote.noVote')}
</Headline>
</View>
);
}
{i18n.t('screens.vote.noVote')}
</Headline>
</View>
);
}
export default withTheme(VoteNotAvailable);
export default VoteNotAvailable;

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {
Avatar,
@ -31,16 +29,11 @@ import {
import {FlatList, StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {
CardTitleIconPropsType,
ListIconPropsType,
} from '../../../constants/PaperStyles';
type PropsType = {
teams: Array<VoteTeamType>,
dateEnd: string,
theme: CustomThemeType,
teams: Array<VoteTeamType>;
dateEnd: string;
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({
@ -58,10 +51,10 @@ class VoteResults extends React.Component<PropsType> {
winnerIds: Array<number>;
constructor(props: PropsType) {
super();
super(props);
props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams);
this.totalVotes = this.getTotalVotes(props.teams);
this.winnerIds = this.getWinnerIds(props.teams);
}
shouldComponentUpdate(): boolean {
@ -69,26 +62,31 @@ class VoteResults extends React.Component<PropsType> {
}
getTotalVotes(teams: Array<VoteTeamType>) {
this.totalVotes = 0;
let totalVotes = 0;
for (let i = 0; i < teams.length; i += 1) {
this.totalVotes += teams[i].votes;
totalVotes += teams[i].votes;
}
return totalVotes;
}
getWinnerIds(teams: Array<VoteTeamType>) {
const max = teams[0].votes;
this.winnerIds = [];
let winnerIds = [];
for (let i = 0; i < teams.length; i += 1) {
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
else break;
if (teams[i].votes === max) {
winnerIds.push(teams[i].id);
} else {
break;
}
}
return winnerIds;
}
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
resultRenderItem = ({item}: {item: VoteTeamType}): React.Node => {
resultRenderItem = ({item}: {item: VoteTeamType}) => {
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
const isDraw = this.winnerIds.length > 1;
const {props} = this;
@ -101,7 +99,7 @@ class VoteResults extends React.Component<PropsType> {
<List.Item
title={item.name}
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
left={(iconProps: ListIconPropsType): React.Node =>
left={(iconProps) =>
isWinner ? (
<List.Icon
style={iconProps.style}
@ -125,7 +123,7 @@ class VoteResults extends React.Component<PropsType> {
);
};
render(): React.Node {
render() {
const {props} = this;
return (
<Card style={styles.card}>
@ -134,15 +132,14 @@ class VoteResults extends React.Component<PropsType> {
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
props.dateEnd
}`}
left={(iconProps: CardTitleIconPropsType): React.Node => (
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="podium-gold" />
)}
/>
<Card.Content>
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
this.totalVotes
}`}</Subheading>
{/* $FlowFixMe */}
<Subheading>
{`${i18n.t('screens.vote.results.totalVotes')} ${this.totalVotes}`}
</Subheading>
<FlatList
data={props.teams}
keyExtractor={this.voteKeyExtractor}

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
import {FlatList, StyleSheet, View} from 'react-native';
@ -27,19 +25,18 @@ import ConnectionManager from '../../../managers/ConnectionManager';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
teams: Array<VoteTeamType>,
onVoteSuccess: () => void,
onVoteError: () => void,
teams: Array<VoteTeamType>;
onVoteSuccess: () => void;
onVoteError: () => void;
};
type StateType = {
selectedTeam: string,
voteDialogVisible: boolean,
errorDialogVisible: boolean,
currentError: number,
selectedTeam: string;
voteDialogVisible: boolean;
errorDialogVisible: boolean;
currentError: number;
};
const styles = StyleSheet.create({
@ -53,10 +50,10 @@ const styles = StyleSheet.create({
export default class VoteSelect extends React.PureComponent<
PropsType,
StateType,
StateType
> {
constructor() {
super();
constructor(props: PropsType) {
super(props);
this.state = {
selectedTeam: 'none',
voteDialogVisible: false,
@ -70,7 +67,7 @@ export default class VoteSelect extends React.PureComponent<
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
voteRenderItem = ({item}: {item: VoteTeamType}): React.Node => (
voteRenderItem = ({item}: {item: VoteTeamType}) => (
<RadioButton.Item label={item.name} value={item.id.toString()} />
);
@ -111,7 +108,7 @@ export default class VoteSelect extends React.PureComponent<
props.onVoteError();
};
render(): React.Node {
render() {
const {state, props} = this;
return (
<View>
@ -119,7 +116,7 @@ export default class VoteSelect extends React.PureComponent<
<Card.Title
title={i18n.t('screens.vote.select.title')}
subtitle={i18n.t('screens.vote.select.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => (
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
)}
/>
@ -127,7 +124,6 @@ export default class VoteSelect extends React.PureComponent<
<RadioButton.Group
onValueChange={this.onVoteSelectionChange}
value={state.selectedTeam}>
{/* $FlowFixMe */}
<FlatList
data={props.teams}
keyExtractor={this.voteKeyExtractor}

View file

@ -17,16 +17,13 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
startDate: string,
startDate: string;
};
const styles = StyleSheet.create({
@ -38,28 +35,19 @@ const styles = StyleSheet.create({
},
});
export default class VoteTease extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon size={iconProps.size} icon="vote" />
)}
/>
<Card.Content>
<Paragraph>
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
</Paragraph>
</Card.Content>
</Card>
);
}
export default function VoteTease(props: PropsType) {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={(iconProps) => <Avatar.Icon size={iconProps.size} icon="vote" />}
/>
<Card.Content>
<Paragraph>
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
</Paragraph>
</Card.Content>
</Card>
);
}

View file

@ -1,93 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
startDate: string | null,
justVoted: boolean,
hasVoted: boolean,
isVoteRunning: boolean,
theme: CustomThemeType,
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
class VoteWait extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
const {startDate} = props;
return (
<Card style={styles.card}>
<Card.Title
title={
props.isVoteRunning
? i18n.t('screens.vote.wait.titleSubmitted')
: i18n.t('screens.vote.wait.titleEnded')
}
subtitle={i18n.t('screens.vote.wait.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon size={iconProps.size} icon="progress-check" />
)}
/>
<Card.Content>
{props.justVoted ? (
<Paragraph style={{color: props.theme.colors.success}}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
) : null}
{props.hasVoted ? (
<Paragraph style={{color: props.theme.colors.success}}>
{i18n.t('screens.vote.wait.messageVoted')}
</Paragraph>
) : null}
{startDate != null ? (
<Paragraph>
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
</Paragraph>
) : (
<Paragraph>
{i18n.t('screens.vote.wait.messageDateUndefined')}
</Paragraph>
)}
</Card.Content>
</Card>
);
}
}
export default withTheme(VoteWait);

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Avatar, Card, Paragraph, useTheme} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
type PropsType = {
startDate: string | null;
justVoted: boolean;
hasVoted: boolean;
isVoteRunning: boolean;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
export default function VoteWait(props: PropsType) {
const theme = useTheme();
const {startDate} = props;
return (
<Card style={styles.card}>
<Card.Title
title={
props.isVoteRunning
? i18n.t('screens.vote.wait.titleSubmitted')
: i18n.t('screens.vote.wait.titleEnded')
}
subtitle={i18n.t('screens.vote.wait.subtitle')}
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="progress-check" />
)}
/>
<Card.Content>
{props.justVoted ? (
<Paragraph style={{color: theme.colors.success}}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
) : null}
{props.hasVoted ? (
<Paragraph style={{color: theme.colors.success}}>
{i18n.t('screens.vote.wait.messageVoted')}
</Paragraph>
) : null}
{startDate != null ? (
<Paragraph>
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
</Paragraph>
) : (
<Paragraph>
{i18n.t('screens.vote.wait.messageDateUndefined')}
</Paragraph>
)}
</Card.Content>
</Card>
);
}

View file

@ -1,78 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Collapsible} from 'react-navigation-collapsible';
import withCollapsible from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar';
export type CollapsibleComponentPropsType = {
children?: React.Node,
hasTab?: boolean,
onScroll?: (event: SyntheticEvent<EventTarget>) => void,
};
type PropsType = {
...CollapsibleComponentPropsType,
collapsibleStack: Collapsible,
// eslint-disable-next-line flowtype/no-weak-types
component: any,
};
class CollapsibleComponent extends React.Component<PropsType> {
static defaultProps = {
children: null,
hasTab: false,
onScroll: null,
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
const {props} = this;
if (props.onScroll) props.onScroll(event);
};
render(): React.Node {
const {props} = this;
const Comp = props.component;
const {
containerPaddingTop,
scrollIndicatorInsetTop,
onScrollWithListener,
} = props.collapsibleStack;
return (
<Comp
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onScroll={onScrollWithListener(this.onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
minHeight: '100%',
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
{props.children}
</Comp>
);
}
}
export default withCollapsible(CollapsibleComponent);

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {useCollapsibleStack} from 'react-navigation-collapsible';
import CustomTabBar from '../Tabbar/CustomTabBar';
import {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
export interface CollapsibleComponentPropsType {
children?: React.ReactNode;
hasTab?: boolean;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}
interface PropsType extends CollapsibleComponentPropsType {
component: React.ComponentType<any>;
}
function CollapsibleComponent(props: PropsType) {
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (props.onScroll) {
props.onScroll(event);
}
};
const Comp = props.component;
const {
containerPaddingTop,
scrollIndicatorInsetTop,
onScrollWithListener,
} = useCollapsibleStack();
return (
<Comp
{...props}
onScroll={onScrollWithListener(onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
minHeight: '100%',
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
{props.children}
</Comp>
);
}
export default CollapsibleComponent;

View file

@ -17,29 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Animated} from 'react-native';
import {Animated, FlatListProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type PropsType = {
...CollapsibleComponentPropsType,
};
type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
// eslint-disable-next-line react/prefer-stateless-function
class CollapsibleFlatList extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
{...props}
component={Animated.FlatList}>
{props.children}
</CollapsibleComponent>
);
}
function CollapsibleFlatList<T>(props: Props<T>) {
return (
<CollapsibleComponent {...props} component={Animated.FlatList}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleFlatList;

View file

@ -17,29 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Animated} from 'react-native';
import {Animated, ScrollViewProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type PropsType = {
...CollapsibleComponentPropsType,
};
type Props = ScrollViewProps & CollapsibleComponentPropsType;
// eslint-disable-next-line react/prefer-stateless-function
class CollapsibleScrollView extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
{...props}
component={Animated.ScrollView}>
{props.children}
</CollapsibleComponent>
);
}
function CollapsibleScrollView(props: Props) {
return (
<CollapsibleComponent {...props} component={Animated.ScrollView}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleScrollView;

View file

@ -17,29 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Animated} from 'react-native';
import {Animated, SectionListProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type PropsType = {
...CollapsibleComponentPropsType,
};
type Props<T> = SectionListProps<T> & CollapsibleComponentPropsType;
// eslint-disable-next-line react/prefer-stateless-function
class CollapsibleSectionList extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
{...props}
component={Animated.SectionList}>
{props.children}
</CollapsibleComponent>
);
}
function CollapsibleSectionList<T>(props: Props<T>) {
return (
<CollapsibleComponent {...props} component={Animated.SectionList}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleSectionList;

View file

@ -21,8 +21,11 @@
import * as React from 'react';
import {View} from 'react-native';
import {ActivityIndicator, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../managers/ThemeManager';
import {ActivityIndicator, useTheme} from 'react-native-paper';
type Props = {
isAbsolute?: boolean;
};
/**
* Component used to display a header button
@ -30,29 +33,21 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @param props Props to pass to the component
* @return {*}
*/
function BasicLoadingScreen(props: {
theme: CustomThemeType,
isAbsolute: boolean,
}): React.Node {
const {theme, isAbsolute} = props;
const {colors} = theme;
let position;
if (isAbsolute != null && isAbsolute) position = 'absolute';
export default function BasicLoadingScreen(props: Props) {
const theme = useTheme();
const {isAbsolute} = props;
return (
<View
style={{
backgroundColor: colors.background,
position,
backgroundColor: theme.colors.background,
position: isAbsolute ? 'absolute' : 'relative',
top: 0,
right: 0,
width: '100%',
height: '100%',
justifyContent: 'center',
}}>
<ActivityIndicator animating size="large" color={colors.primary} />
<ActivityIndicator animating size="large" color={theme.colors.primary} />
</View>
);
}
export default withTheme(BasicLoadingScreen);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Button, Subheading, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
@ -27,17 +25,16 @@ import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack';
import {ERROR_TYPE} from '../../utils/WebData';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
route: {name: string},
onRefresh?: () => void,
errorCode?: number,
icon?: string,
message?: string,
showRetryButton?: boolean,
navigation?: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
route?: {name: string};
onRefresh?: () => void;
errorCode?: number;
icon?: string;
message?: string;
showRetryButton?: boolean;
};
const styles = StyleSheet.create({
@ -82,9 +79,11 @@ class ErrorView extends React.PureComponent<PropsType> {
constructor(props: PropsType) {
super(props);
this.icon = '';
this.showLoginButton = false;
this.message = '';
}
getRetryButton(): React.Node {
getRetryButton() {
const {props} = this;
return (
<Button
@ -97,7 +96,7 @@ class ErrorView extends React.PureComponent<PropsType> {
);
}
getLoginButton(): React.Node {
getLoginButton() {
return (
<Button
mode="contained"
@ -111,10 +110,12 @@ class ErrorView extends React.PureComponent<PropsType> {
goToLogin = () => {
const {props} = this;
props.navigation.navigate('login', {
screen: 'login',
params: {nextScreen: props.route.name},
});
if (props.navigation) {
props.navigation.navigate('login', {
screen: 'login',
params: {nextScreen: props.route ? props.route.name : undefined},
});
}
};
generateMessage() {
@ -169,13 +170,17 @@ class ErrorView extends React.PureComponent<PropsType> {
}
}
render(): React.Node {
render() {
const {props} = this;
this.generateMessage();
let button;
if (this.showLoginButton) button = this.getLoginButton();
else if (props.showRetryButton) button = this.getRetryButton();
else button = null;
if (this.showLoginButton) {
button = this.getLoginButton();
} else if (props.showRetryButton) {
button = this.getRetryButton();
} else {
button = null;
}
return (
<Animatable.View

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {RefreshControl, View} from 'react-native';
import {StackNavigationProp} from '@react-navigation/stack';
@ -35,31 +33,30 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ApiGenericDataType} from '../../utils/WebData';
export type VoteTeamType = {
id: number,
name: string,
votes: number,
id: number;
name: string;
votes: number;
};
type TeamResponseType = {
has_voted: boolean,
teams: Array<VoteTeamType>,
has_voted: boolean;
teams: Array<VoteTeamType>;
};
type VoteDatesStringType = {
date_begin: string,
date_end: string,
date_result_begin: string,
date_result_end: string,
date_begin: string;
date_end: string;
date_result_begin: string;
date_result_end: string;
};
type VoteDatesObjectType = {
date_begin: Date,
date_end: Date,
date_result_begin: Date,
date_result_end: Date,
date_begin: Date;
date_end: Date;
date_result_begin: Date;
date_result_end: Date;
};
// const FAKE_DATE = {
@ -113,12 +110,12 @@ type VoteDatesObjectType = {
const MIN_REFRESH_TIME = 5 * 1000;
type PropsType = {
navigation: StackNavigationProp,
navigation: StackNavigationProp<any>;
};
type StateType = {
hasVoted: boolean,
mascotDialogVisible: boolean,
hasVoted: boolean;
mascotDialogVisible: boolean;
};
/**
@ -139,10 +136,13 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
lastRefresh: Date | null;
authRef: {current: null | AuthenticatedScreen};
authRef: {current: null | AuthenticatedScreen<any>};
constructor() {
super();
constructor(props: PropsType) {
super(props);
this.teams = [];
this.datesString = null;
this.dates = null;
this.state = {
hasVoted: false,
mascotDialogVisible: AsyncStorageManager.getBool(
@ -174,8 +174,8 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
return dateString;
}
getMainRenderItem = ({item}: {item: {key: string}}): React.Node => {
if (item.key === 'info')
getMainRenderItem = ({item}: {item: {key: string}}) => {
if (item.key === 'info') {
return (
<View>
<Button
@ -191,21 +191,24 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
</Button>
</View>
);
}
return this.getContent();
};
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
getScreen = (data: Array<TeamResponseType | VoteDatesStringType | null>) => {
const {state} = this;
// data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE;
this.lastRefresh = new Date();
const teams: TeamResponseType | null = data[0];
const dateStrings: VoteDatesStringType | null = data[1];
const teams = data[0] as TeamResponseType | null;
const dateStrings = data[1] as VoteDatesStringType | null;
if (dateStrings != null && dateStrings.date_begin == null)
if (dateStrings != null && dateStrings.date_begin == null) {
this.datesString = null;
else this.datesString = dateStrings;
} else {
this.datesString = dateStrings;
}
if (teams != null) {
this.teams = teams.teams;
@ -225,13 +228,20 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
);
};
getContent(): React.Node {
getContent() {
const {state} = this;
if (!this.isVoteStarted()) return this.getTeaseVoteCard();
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted)
if (!this.isVoteStarted()) {
return this.getTeaseVoteCard();
}
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
return this.getVoteCard();
if (!this.isResultStarted()) return this.getWaitVoteCard();
if (this.isResultRunning()) return this.getVoteResultCard();
}
if (!this.isResultStarted()) {
return this.getWaitVoteCard();
}
if (this.isResultRunning()) {
return this.getVoteResultCard();
}
return <VoteNotAvailable />;
}
@ -240,7 +250,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
/**
* The user has not voted yet, and the votes are open
*/
getVoteCard(): React.Node {
getVoteCard() {
return (
<VoteSelect
teams={this.teams}
@ -253,8 +263,8 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
/**
* Votes have ended, results can be displayed
*/
getVoteResultCard(): React.Node {
if (this.dates != null && this.datesString != null)
getVoteResultCard() {
if (this.dates != null && this.datesString != null) {
return (
<VoteResults
teams={this.teams}
@ -264,14 +274,15 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
)}
/>
);
}
return <VoteNotAvailable />;
}
/**
* Vote will open shortly
*/
getTeaseVoteCard(): React.Node {
if (this.dates != null && this.datesString != null)
getTeaseVoteCard() {
if (this.dates != null && this.datesString != null) {
return (
<VoteTease
startDate={this.getDateString(
@ -280,24 +291,26 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
)}
/>
);
}
return <VoteNotAvailable />;
}
/**
* Votes have ended, or user has voted waiting for results
*/
getWaitVoteCard(): React.Node {
getWaitVoteCard() {
const {state} = this;
let startDate = null;
if (
this.dates != null &&
this.datesString != null &&
this.dates.date_result_begin != null
)
) {
startDate = this.getDateString(
this.dates.date_result_begin,
this.datesString.date_result_begin,
);
}
return (
<VoteWait
startDate={startDate}
@ -314,12 +327,15 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
reloadData = () => {
let canRefresh;
const {lastRefresh} = this;
if (lastRefresh != null)
if (lastRefresh != null) {
canRefresh =
new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
else canRefresh = true;
if (canRefresh && this.authRef.current != null)
} else {
canRefresh = true;
}
if (canRefresh && this.authRef.current != null) {
this.authRef.current.reload();
}
};
showMascotDialog = () => {
@ -380,8 +396,12 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
date_result_begin: dateResultBegin,
date_result_end: dateResultEnd,
};
} else this.dates = null;
} else this.dates = null;
} else {
this.dates = null;
}
} else {
this.dates = null;
}
}
/**
@ -391,11 +411,11 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
*
* @returns {*}
*/
render(): React.Node {
render() {
const {props, state} = this;
return (
<View style={{flex: 1}}>
<AuthenticatedScreen
<AuthenticatedScreen<TeamResponseType | VoteDatesStringType>
navigation={props.navigation}
ref={this.authRef}
requests={[
@ -418,7 +438,6 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
message={i18n.t('screens.vote.mascotDialog.message')}
icon="vote"
buttons={{
action: null,
cancel: {
message: i18n.t('screens.vote.mascotDialog.button'),
icon: 'check',

View file

@ -35,8 +35,6 @@ export type ApiDataLoginType = {
token: string;
};
export type ApiGenericDataType = {[key: string]: any};
type ApiResponseType<T> = {
error: number;
data: T;
@ -70,7 +68,7 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean {
* @param path The API path from the API endpoint
* @param method The HTTP method to use (GET or POST)
* @param params The params to use for this request
* @returns {Promise<ApiGenericDataType>}
* @returns {Promise<T>}
*/
export async function apiRequest<T>(
path: string,