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

View file

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

View file

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

View file

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

View file

@ -17,16 +17,13 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, Paragraph} from 'react-native-paper'; import {Avatar, Card, Paragraph} from 'react-native-paper';
import {StyleSheet} from 'react-native'; import {StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = { type PropsType = {
startDate: string, startDate: string;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -38,28 +35,19 @@ const styles = StyleSheet.create({
}, },
}); });
export default class VoteTease extends React.Component<PropsType> { export default function VoteTease(props: PropsType) {
shouldComponentUpdate(): boolean { return (
return false; <Card style={styles.card}>
} <Card.Title
title={i18n.t('screens.vote.tease.title')}
render(): React.Node { subtitle={i18n.t('screens.vote.tease.subtitle')}
const {props} = this; left={(iconProps) => <Avatar.Icon size={iconProps.size} icon="vote" />}
return ( />
<Card style={styles.card}> <Card.Content>
<Card.Title <Paragraph>
title={i18n.t('screens.vote.tease.title')} {`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
subtitle={i18n.t('screens.vote.tease.subtitle')} </Paragraph>
left={(iconProps: CardTitleIconPropsType): React.Node => ( </Card.Content>
<Avatar.Icon size={iconProps.size} icon="vote" /> </Card>
)} );
/>
<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/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated, FlatListProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from './CollapsibleComponent';
type PropsType = { type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
...CollapsibleComponentPropsType,
};
// eslint-disable-next-line react/prefer-stateless-function function CollapsibleFlatList<T>(props: Props<T>) {
class CollapsibleFlatList extends React.Component<PropsType> { return (
render(): React.Node { <CollapsibleComponent {...props} component={Animated.FlatList}>
const {props} = this; {props.children}
return ( </CollapsibleComponent>
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading );
{...props}
component={Animated.FlatList}>
{props.children}
</CollapsibleComponent>
);
}
} }
export default CollapsibleFlatList; export default CollapsibleFlatList;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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