Improve Amicale screen components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-05 11:54:13 +02:00
parent 483970c9a8
commit 0a64f5fcd7
6 changed files with 1028 additions and 944 deletions

View file

@ -4,137 +4,157 @@ import * as React from 'react';
import {FlatList, Image, Linking, View} from 'react-native';
import {Card, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import AMICALE_LOGO from '../../../assets/amicale.png';
type Props = {
type DatasetItemType = {
name: string,
email: string,
icon: MaterialCommunityIconsGlyphs,
};
type DatasetItem = {
name: string,
email: string,
icon: MaterialCommunityIconsGlyphs,
}
/**
* Class defining a planning event information page.
*/
class AmicaleContactScreen extends React.Component<Props> {
class AmicaleContactScreen extends React.Component<null> {
// Dataset containing information about contacts
CONTACT_DATASET: Array<DatasetItemType>;
// Dataset containing information about contacts
CONTACT_DATASET: Array<DatasetItem>;
constructor() {
super();
this.CONTACT_DATASET = [
{
name: i18n.t('screens.amicaleAbout.roles.interSchools'),
email: 'inter.ecoles@amicale-insat.fr',
icon: 'share-variant',
},
{
name: i18n.t('screens.amicaleAbout.roles.culture'),
email: 'culture@amicale-insat.fr',
icon: 'book',
},
{
name: i18n.t('screens.amicaleAbout.roles.animation'),
email: 'animation@amicale-insat.fr',
icon: 'emoticon',
},
{
name: i18n.t('screens.amicaleAbout.roles.clubs'),
email: 'clubs@amicale-insat.fr',
icon: 'account-group',
},
{
name: i18n.t('screens.amicaleAbout.roles.event'),
email: 'evenements@amicale-insat.fr',
icon: 'calendar-range',
},
{
name: i18n.t('screens.amicaleAbout.roles.tech'),
email: 'technique@amicale-insat.fr',
icon: 'cog',
},
{
name: i18n.t('screens.amicaleAbout.roles.communication'),
email: 'amicale@amicale-insat.fr',
icon: 'comment-account',
},
{
name: i18n.t('screens.amicaleAbout.roles.intraSchools'),
email: 'intra.ecoles@amicale-insat.fr',
icon: 'school',
},
{
name: i18n.t('screens.amicaleAbout.roles.publicRelations'),
email: 'rp@amicale-insat.fr',
icon: 'account-tie',
},
];
}
constructor(props: Props) {
super(props);
this.CONTACT_DATASET = [
{
name: i18n.t("screens.amicaleAbout.roles.interSchools"),
email: "inter.ecoles@amicale-insat.fr",
icon: "share-variant"
},
{
name: i18n.t("screens.amicaleAbout.roles.culture"),
email: "culture@amicale-insat.fr",
icon: "book"
},
{
name: i18n.t("screens.amicaleAbout.roles.animation"),
email: "animation@amicale-insat.fr",
icon: "emoticon"
},
{
name: i18n.t("screens.amicaleAbout.roles.clubs"),
email: "clubs@amicale-insat.fr",
icon: "account-group"
},
{
name: i18n.t("screens.amicaleAbout.roles.event"),
email: "evenements@amicale-insat.fr",
icon: "calendar-range"
},
{
name: i18n.t("screens.amicaleAbout.roles.tech"),
email: "technique@amicale-insat.fr",
icon: "cog"
},
{
name: i18n.t("screens.amicaleAbout.roles.communication"),
email: "amicale@amicale-insat.fr",
icon: "comment-account"
},
{
name: i18n.t("screens.amicaleAbout.roles.intraSchools"),
email: "intra.ecoles@amicale-insat.fr",
icon: "school"
},
{
name: i18n.t("screens.amicaleAbout.roles.publicRelations"),
email: "rp@amicale-insat.fr",
icon: "account-tie"
},
];
}
keyExtractor = (item: DatasetItemType): string => item.email;
keyExtractor = (item: DatasetItem) => item.email;
getChevronIcon = ({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
);
getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
renderItem = ({item}: { item: DatasetItem }) => {
const onPress = () => Linking.openURL('mailto:' + item.email);
return <List.Item
title={item.name}
description={item.email}
left={(props) => <List.Icon {...props} icon={item.icon}/>}
right={this.getChevronIcon}
onPress={onPress}
/>
getRenderItem = ({item}: {item: DatasetItemType}): React.Node => {
const onPress = () => {
Linking.openURL(`mailto:${item.email}`);
};
return (
<List.Item
title={item.name}
description={item.email}
left={({size, color}: {size: number, color: string}): React.Node => (
<List.Icon size={size} color={color} icon={item.icon} />
)}
right={this.getChevronIcon}
onPress={onPress}
/>
);
};
getScreen = () => {
return (
<View>
<View style={{
width: '100%',
height: 100,
marginTop: 20,
marginBottom: 20,
justifyContent: 'center',
alignItems: 'center'
}}>
<Image
source={require('../../../assets/amicale.png')}
style={{flex: 1, resizeMode: "contain"}}
resizeMode="contain"/>
</View>
<Card style={{margin: 5}}>
<Card.Title
title={i18n.t("screens.amicaleAbout.title")}
subtitle={i18n.t("screens.amicaleAbout.subtitle")}
left={props => <List.Icon {...props} icon={'information'}/>}
/>
<Card.Content>
<Text>{i18n.t("screens.amicaleAbout.message")}</Text>
{/*$FlowFixMe*/}
<FlatList
data={this.CONTACT_DATASET}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
/>
</Card.Content>
</Card>
</View>
);
};
render() {
return (
<CollapsibleFlatList
data={[{key: "1"}]}
renderItem={this.getScreen}
hasTab={true}
getScreen = (): React.Node => {
return (
<View>
<View
style={{
width: '100%',
height: 100,
marginTop: 20,
marginBottom: 20,
justifyContent: 'center',
alignItems: 'center',
}}>
<Image
source={AMICALE_LOGO}
style={{flex: 1, resizeMode: 'contain'}}
resizeMode="contain"
/>
</View>
<Card style={{margin: 5}}>
<Card.Title
title={i18n.t('screens.amicaleAbout.title')}
subtitle={i18n.t('screens.amicaleAbout.subtitle')}
left={({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="information" />
)}
/>
<Card.Content>
<Text>{i18n.t('screens.amicaleAbout.message')}</Text>
<FlatList
data={this.CONTACT_DATASET}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
/>
);
}
</Card.Content>
</Card>
</View>
);
};
render(): React.Node {
return (
<CollapsibleFlatList
data={[{key: '1'}]}
renderItem={this.getScreen}
hasTab
/>
);
}
}
export default withTheme(AmicaleContactScreen);

View file

@ -17,7 +17,7 @@ import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen
import CustomHTML from '../../../components/Overrides/CustomHTML';
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
import type {ClubCategoryType, ClubType} from './ClubListScreen';
import type {CustomTheme} from '../../../managers/ThemeManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import {ERROR_TYPE} from '../../../utils/WebData';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import type {ApiGenericDataType} from '../../../utils/WebData';
@ -32,7 +32,7 @@ type PropsType = {
},
...
},
theme: CustomTheme,
theme: CustomThemeType,
};
const AMICALE_MAIL = 'clubs@amicale-insat.fr';

View file

@ -11,7 +11,7 @@ import {
} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomTheme} from '../../../managers/ThemeManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {DeviceType} from './EquipmentListScreen';
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
@ -23,7 +23,7 @@ type PropsType = {
dates: [string, string],
},
},
theme: CustomTheme,
theme: CustomThemeType,
};
class EquipmentConfirmScreen extends React.Component<PropsType> {

View file

@ -15,7 +15,7 @@ import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js';
import {CalendarList} from 'react-native-calendars';
import type {DeviceType} from './EquipmentListScreen';
import type {CustomTheme} from '../../../managers/ThemeManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import {
@ -36,7 +36,7 @@ type PropsType = {
item?: DeviceType,
},
},
theme: CustomTheme,
theme: CustomThemeType,
};
export type MarkedDatesObjectType = {

View file

@ -1,417 +1,446 @@
// @flow
import * as React from 'react';
import {Image, KeyboardAvoidingView, StyleSheet, View} from "react-native";
import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper';
import ConnectionManager from "../../managers/ConnectionManager";
import {Image, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
import {
Button,
Card,
HelperText,
TextInput,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js';
import ErrorDialog from "../../components/Dialogs/ErrorDialog";
import type {CustomTheme} from "../../managers/ThemeManager";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {StackNavigationProp} from "@react-navigation/stack";
import AvailableWebsites from "../../constants/AvailableWebsites";
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import MascotPopup from "../../components/Mascot/MascotPopup";
import LinearGradient from "react-native-linear-gradient";
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
import {StackNavigationProp} from '@react-navigation/stack';
import LinearGradient from 'react-native-linear-gradient';
import ConnectionManager from '../../managers/ConnectionManager';
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
import type {CustomThemeType} from '../../managers/ThemeManager';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import AvailableWebsites from '../../constants/AvailableWebsites';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type Props = {
navigation: StackNavigationProp,
route: { params: { nextScreen: string } },
theme: CustomTheme
}
type PropsType = {
navigation: StackNavigationProp,
route: {params: {nextScreen: string}},
theme: CustomThemeType,
};
type State = {
email: string,
password: string,
isEmailValidated: boolean,
isPasswordValidated: boolean,
loading: boolean,
dialogVisible: boolean,
dialogError: number,
mascotDialogVisible: boolean,
}
type StateType = {
email: string,
password: string,
isEmailValidated: boolean,
isPasswordValidated: boolean,
loading: boolean,
dialogVisible: boolean,
dialogError: number,
mascotDialogVisible: boolean,
};
const ICON_AMICALE = require('../../../assets/amicale.png');
const RESET_PASSWORD_PATH = "https://www.amicale-insat.fr/password/reset";
const RESET_PASSWORD_PATH = 'https://www.amicale-insat.fr/password/reset';
const emailRegex = /^.+@.+\..+$/;
class LoginScreen extends React.Component<Props, State> {
state = {
email: '',
password: '',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
dialogVisible: false,
dialogError: 0,
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.loginShowBanner.key),
};
onEmailChange: (value: string) => null;
onPasswordChange: (value: string) => null;
passwordInputRef: { current: null | TextInput };
nextScreen: string | null;
constructor(props) {
super(props);
this.passwordInputRef = React.createRef();
this.onEmailChange = this.onInputChange.bind(this, true);
this.onPasswordChange = this.onInputChange.bind(this, false);
this.props.navigation.addListener('focus', this.onScreenFocus);
}
onScreenFocus = () => {
this.handleNavigationParams();
};
/**
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/
handleNavigationParams() {
if (this.props.route.params != null) {
if (this.props.route.params.nextScreen != null)
this.nextScreen = this.props.route.params.nextScreen;
else
this.nextScreen = null;
}
}
hideMascotDialog = () => {
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.loginShowBanner.key, false);
this.setState({mascotDialogVisible: false})
};
showMascotDialog = () => {
this.setState({mascotDialogVisible: true})
};
/**
* Shows an error dialog with the corresponding login error
*
* @param error The error given by the login request
*/
showErrorDialog = (error: number) =>
this.setState({
dialogVisible: true,
dialogError: error,
});
hideErrorDialog = () => this.setState({dialogVisible: false});
/**
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
* Saves in user preferences to not show the login banner again.
*/
handleSuccess = () => {
// Do not show the home login banner again
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.homeShowBanner.key, false);
if (this.nextScreen == null)
this.props.navigation.goBack();
else
this.props.navigation.replace(this.nextScreen);
};
/**
* Navigates to the Amicale website screen with the reset password link as navigation parameters
*/
onResetPasswordClick = () => this.props.navigation.navigate("website", {
host: AvailableWebsites.websites.AMICALE,
path: RESET_PASSWORD_PATH,
title: i18n.t('screens.websites.amicale')
});
/**
* The user has unfocused the input, his email is ready to be validated
*/
validateEmail = () => this.setState({isEmailValidated: true});
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid() {
return emailRegex.test(this.state.email);
}
/**
* Checks if we should tell the user his email is invalid.
* We should only show this if his email is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
shouldShowEmailError() {
return this.state.isEmailValidated && !this.isEmailValid();
}
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => this.setState({isPasswordValidated: true});
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid() {
return this.state.password !== '';
}
/**
* Checks if we should tell the user his password is invalid.
* We should only show this if his password is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
shouldShowPasswordError() {
return this.state.isPasswordValidated && !this.isPasswordValid();
}
/**
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
*
* @returns {boolean}
*/
shouldEnableLogin() {
return this.isEmailValid() && this.isPasswordValid() && !this.state.loading;
}
/**
* Called when the user input changes in the email or password field.
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
*
* @param isEmail True if the field is the email field
* @param value The new field value
*/
onInputChange(isEmail: boolean, value: string) {
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
/**
* Focuses the password field when the email field is done
*
* @returns {*}
*/
onEmailSubmit = () => {
if (this.passwordInputRef.current != null)
this.passwordInputRef.current.focus();
}
/**
* Called when the user clicks on login or finishes to type his password.
*
* Checks if we should allow the user to login,
* then makes the login request and enters a loading state until the request finishes
*
*/
onSubmit = () => {
if (this.shouldEnableLogin()) {
this.setState({loading: true});
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
.then(this.handleSuccess)
.catch(this.showErrorDialog)
.finally(() => {
this.setState({loading: false});
});
}
};
/**
* Gets the form input
*
* @returns {*}
*/
getFormInput() {
return (
<View>
<TextInput
label={i18n.t("screens.login.email")}
mode='outlined'
value={this.state.email}
onChangeText={this.onEmailChange}
onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()}
textContentType={'emailAddress'}
autoCapitalize={'none'}
autoCompleteType={'email'}
autoCorrect={false}
keyboardType={'email-address'}
returnKeyType={'next'}
secureTextEntry={false}
/>
<HelperText
type="error"
visible={this.shouldShowEmailError()}
>
{i18n.t("screens.login.emailError")}
</HelperText>
<TextInput
ref={this.passwordInputRef}
label={i18n.t("screens.login.password")}
mode='outlined'
value={this.state.password}
onChangeText={this.onPasswordChange}
onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()}
textContentType={'password'}
autoCapitalize={'none'}
autoCompleteType={'password'}
autoCorrect={false}
keyboardType={'default'}
returnKeyType={'done'}
secureTextEntry={true}
/>
<HelperText
type="error"
visible={this.shouldShowPasswordError()}
>
{i18n.t("screens.login.passwordError")}
</HelperText>
</View>
);
}
/**
* Gets the card containing the input form
* @returns {*}
*/
getMainCard() {
return (
<View style={styles.card}>
<Card.Title
title={i18n.t("screens.login.title")}
titleStyle={{color: "#fff"}}
subtitle={i18n.t("screens.login.subtitle")}
subtitleStyle={{color: "#fff"}}
left={(props) => <Image
{...props}
source={ICON_AMICALE}
style={{
width: props.size,
height: props.size,
}}/>}
/>
<Card.Content>
{this.getFormInput()}
<Card.Actions style={{flexWrap: "wrap"}}>
<Button
icon="lock-question"
mode="contained"
onPress={this.onResetPasswordClick}
color={this.props.theme.colors.warning}
style={{marginRight: 'auto', marginBottom: 20}}>
{i18n.t("screens.login.resetPassword")}
</Button>
<Button
icon="send"
mode="contained"
disabled={!this.shouldEnableLogin()}
loading={this.state.loading}
onPress={this.onSubmit}
style={{marginLeft: 'auto'}}>
{i18n.t("screens.login.title")}
</Button>
</Card.Actions>
<Card.Actions>
<Button
icon="help-circle"
mode="contained"
onPress={this.showMascotDialog}
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}>
{i18n.t("screens.login.mascotDialog.title")}
</Button>
</Card.Actions>
</Card.Content>
</View>
);
}
render() {
return (
<LinearGradient
style={{
height: "100%"
}}
colors={['#9e0d18', '#530209']}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}>
<KeyboardAvoidingView
behavior={"height"}
contentContainerStyle={styles.container}
style={styles.container}
enabled
keyboardVerticalOffset={100}
>
<CollapsibleScrollView>
<View style={{height: "100%"}}>
{this.getMainCard()}
</View>
<MascotPopup
visible={this.state.mascotDialogVisible}
title={i18n.t("screens.login.mascotDialog.title")}
message={i18n.t("screens.login.mascotDialog.message")}
icon={"help"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.login.mascotDialog.button"),
icon: "check",
onPress: this.hideMascotDialog,
}
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<ErrorDialog
visible={this.state.dialogVisible}
onDismiss={this.hideErrorDialog}
errorCode={this.state.dialogError}
/>
</CollapsibleScrollView>
</KeyboardAvoidingView>
</LinearGradient>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
card: {
marginTop: 'auto',
marginBottom: 'auto',
},
header: {
fontSize: 36,
marginBottom: 48
},
textInput: {},
btnContainer: {
marginTop: 5,
marginBottom: 10,
}
container: {
flex: 1,
},
card: {
marginTop: 'auto',
marginBottom: 'auto',
},
header: {
fontSize: 36,
marginBottom: 48,
},
textInput: {},
btnContainer: {
marginTop: 5,
marginBottom: 10,
},
});
class LoginScreen extends React.Component<PropsType, StateType> {
onEmailChange: (value: string) => void;
onPasswordChange: (value: string) => void;
passwordInputRef: {current: null | TextInput};
nextScreen: string | null;
constructor(props: PropsType) {
super(props);
this.passwordInputRef = React.createRef();
this.onEmailChange = (value: string) => {
this.onInputChange(true, value);
};
this.onPasswordChange = (value: string) => {
this.onInputChange(false, value);
};
props.navigation.addListener('focus', this.onScreenFocus);
this.state = {
email: '',
password: '',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
dialogVisible: false,
dialogError: 0,
mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
),
};
}
onScreenFocus = () => {
this.handleNavigationParams();
};
/**
* Navigates to the Amicale website screen with the reset password link as navigation parameters
*/
onResetPasswordClick = () => {
const {navigation} = this.props;
navigation.navigate('website', {
host: AvailableWebsites.websites.AMICALE,
path: RESET_PASSWORD_PATH,
title: i18n.t('screens.websites.amicale'),
});
};
/**
* Called when the user input changes in the email or password field.
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
*
* @param isEmail True if the field is the email field
* @param value The new field value
*/
onInputChange(isEmail: boolean, value: string) {
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
/**
* Focuses the password field when the email field is done
*
* @returns {*}
*/
onEmailSubmit = () => {
if (this.passwordInputRef.current != null)
this.passwordInputRef.current.focus();
};
/**
* Called when the user clicks on login or finishes to type his password.
*
* Checks if we should allow the user to login,
* then makes the login request and enters a loading state until the request finishes
*
*/
onSubmit = () => {
const {email, password} = this.state;
if (this.shouldEnableLogin()) {
this.setState({loading: true});
ConnectionManager.getInstance()
.connect(email, password)
.then(this.handleSuccess)
.catch(this.showErrorDialog)
.finally(() => {
this.setState({loading: false});
});
}
};
/**
* Gets the form input
*
* @returns {*}
*/
getFormInput(): React.Node {
const {email, password} = this.state;
return (
<View>
<TextInput
label={i18n.t('screens.login.email')}
mode="outlined"
value={email}
onChangeText={this.onEmailChange}
onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()}
textContentType="emailAddress"
autoCapitalize="none"
autoCompleteType="email"
autoCorrect={false}
keyboardType="email-address"
returnKeyType="next"
secureTextEntry={false}
/>
<HelperText type="error" visible={this.shouldShowEmailError()}>
{i18n.t('screens.login.emailError')}
</HelperText>
<TextInput
ref={this.passwordInputRef}
label={i18n.t('screens.login.password')}
mode="outlined"
value={password}
onChangeText={this.onPasswordChange}
onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()}
textContentType="password"
autoCapitalize="none"
autoCompleteType="password"
autoCorrect={false}
keyboardType="default"
returnKeyType="done"
secureTextEntry
/>
<HelperText type="error" visible={this.shouldShowPasswordError()}>
{i18n.t('screens.login.passwordError')}
</HelperText>
</View>
);
}
/**
* Gets the card containing the input form
* @returns {*}
*/
getMainCard(): React.Node {
const {props, state} = this;
return (
<View style={styles.card}>
<Card.Title
title={i18n.t('screens.login.title')}
titleStyle={{color: '#fff'}}
subtitle={i18n.t('screens.login.subtitle')}
subtitleStyle={{color: '#fff'}}
left={({size}: {size: number}): React.Node => (
<Image
source={ICON_AMICALE}
style={{
width: size,
height: size,
}}
/>
)}
/>
<Card.Content>
{this.getFormInput()}
<Card.Actions style={{flexWrap: 'wrap'}}>
<Button
icon="lock-question"
mode="contained"
onPress={this.onResetPasswordClick}
color={props.theme.colors.warning}
style={{marginRight: 'auto', marginBottom: 20}}>
{i18n.t('screens.login.resetPassword')}
</Button>
<Button
icon="send"
mode="contained"
disabled={!this.shouldEnableLogin()}
loading={state.loading}
onPress={this.onSubmit}
style={{marginLeft: 'auto'}}>
{i18n.t('screens.login.title')}
</Button>
</Card.Actions>
<Card.Actions>
<Button
icon="help-circle"
mode="contained"
onPress={this.showMascotDialog}
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}>
{i18n.t('screens.login.mascotDialog.title')}
</Button>
</Card.Actions>
</Card.Content>
</View>
);
}
/**
* The user has unfocused the input, his email is ready to be validated
*/
validateEmail = () => {
this.setState({isEmailValidated: true});
};
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => {
this.setState({isPasswordValidated: true});
};
hideMascotDialog = () => {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
false,
);
this.setState({mascotDialogVisible: false});
};
showMascotDialog = () => {
this.setState({mascotDialogVisible: true});
};
/**
* Shows an error dialog with the corresponding login error
*
* @param error The error given by the login request
*/
showErrorDialog = (error: number) => {
this.setState({
dialogVisible: true,
dialogError: error,
});
};
hideErrorDialog = () => {
this.setState({dialogVisible: false});
};
/**
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
* Saves in user preferences to not show the login banner again.
*/
handleSuccess = () => {
const {navigation} = this.props;
// Do not show the home login banner again
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.homeShowBanner.key,
false,
);
if (this.nextScreen == null) navigation.goBack();
else navigation.replace(this.nextScreen);
};
/**
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/
handleNavigationParams() {
const {route} = this.props;
if (route.params != null) {
if (route.params.nextScreen != null)
this.nextScreen = route.params.nextScreen;
else this.nextScreen = null;
}
}
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid(): boolean {
const {email} = this.state;
return emailRegex.test(email);
}
/**
* Checks if we should tell the user his email is invalid.
* We should only show this if his email is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
shouldShowEmailError(): boolean {
const {isEmailValidated} = this.state;
return isEmailValidated && !this.isEmailValid();
}
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid(): boolean {
const {password} = this.state;
return password !== '';
}
/**
* Checks if we should tell the user his password is invalid.
* We should only show this if his password is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
shouldShowPasswordError(): boolean {
const {isPasswordValidated} = this.state;
return isPasswordValidated && !this.isPasswordValid();
}
/**
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
*
* @returns {boolean}
*/
shouldEnableLogin(): boolean {
const {loading} = this.state;
return this.isEmailValid() && this.isPasswordValid() && !loading;
}
render(): React.Node {
const {mascotDialogVisible, dialogVisible, dialogError} = this.state;
return (
<LinearGradient
style={{
height: '100%',
}}
colors={['#9e0d18', '#530209']}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}>
<KeyboardAvoidingView
behavior="height"
contentContainerStyle={styles.container}
style={styles.container}
enabled
keyboardVerticalOffset={100}>
<CollapsibleScrollView>
<View style={{height: '100%'}}>{this.getMainCard()}</View>
<MascotPopup
visible={mascotDialogVisible}
title={i18n.t('screens.login.mascotDialog.title')}
message={i18n.t('screens.login.mascotDialog.message')}
icon="help"
buttons={{
action: null,
cancel: {
message: i18n.t('screens.login.mascotDialog.button'),
icon: 'check',
onPress: this.hideMascotDialog,
},
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<ErrorDialog
visible={dialogVisible}
onDismiss={this.hideErrorDialog}
errorCode={dialogError}
/>
</CollapsibleScrollView>
</KeyboardAvoidingView>
</LinearGradient>
);
}
}
export default withTheme(LoginScreen);

View file

@ -1,432 +1,467 @@
// @flow
import * as React from 'react';
import {FlatList, StyleSheet, View} from "react-native";
import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
import {FlatList, StyleSheet, View} from 'react-native';
import {
Avatar,
Button,
Card,
Divider,
List,
Paragraph,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js';
import LogoutDialog from "../../components/Amicale/LogoutDialog";
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import type {cardList} from "../../components/Lists/CardList/CardList";
import CardList from "../../components/Lists/CardList/CardList";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
import AvailableWebsites from "../../constants/AvailableWebsites";
import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager";
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import {StackNavigationProp} from '@react-navigation/stack';
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
import LogoutDialog from '../../components/Amicale/LogoutDialog';
import MaterialHeaderButtons, {
Item,
} from '../../components/Overrides/CustomHeaderButton';
import CardList from '../../components/Lists/CardList/CardList';
import type {CustomThemeType} from '../../managers/ThemeManager';
import AvailableWebsites from '../../constants/AvailableWebsites';
import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ServiceItemType} from '../../managers/ServicesManager';
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme,
}
type PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
};
type State = {
dialogVisible: boolean,
}
type StateType = {
dialogVisible: boolean,
};
type ProfileData = {
first_name: string,
last_name: string,
email: string,
birthday: string,
phone: string,
branch: string,
link: string,
validity: boolean,
clubs: Array<Club>,
}
type Club = {
id: number,
name: string,
is_manager: boolean,
}
type ClubType = {
id: number,
name: string,
is_manager: boolean,
};
class ProfileScreen extends React.Component<Props, State> {
state = {
dialogVisible: false,
};
data: ProfileData;
flatListData: Array<{ id: string }>;
amicaleDataset: cardList;
constructor(props: Props) {
super(props);
this.flatListData = [
{id: '0'},
{id: '1'},
{id: '2'},
{id: '3'},
]
const services = new ServicesManager(props.navigation);
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
}
componentDidMount() {
this.props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({dialogVisible: false});
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = () => <MaterialHeaderButtons>
<Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
</MaterialHeaderButtons>;
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<{ [key: string]: any } | null>) => {
if (data[0] != null) {
this.data = data[0];
}
return (
<View style={{flex: 1}}>
<CollapsibleFlatList
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog
{...this.props}
visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
)
};
getRenderItem = ({item}: { item: { id: string } }) => {
switch (item.id) {
case '0':
return this.getWelcomeCard();
case '1':
return this.getPersonalCard();
case '2':
return this.getClubCard();
default:
return this.getMembershipCar();
}
};
/**
* Gets the list of services available with the Amicale account
*
* @returns {*}
*/
getServicesList() {
return (
<CardList
dataset={this.amicaleDataset}
isHorizontal={true}
/>
);
}
/**
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
left={() =>
<Mascot
style={{
width: 60
}}
emotion={MASCOT_STYLE.COOL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 1000
}}
/>}
titleStyle={{marginLeft: 10}}
/>
<Card.Content>
<Divider/>
<Paragraph>
{i18n.t("screens.profile.welcomeDescription")}
</Paragraph>
{this.getServicesList()}
<Paragraph>
{i18n.t("screens.profile.welcomeFeedback")}
</Paragraph>
<Divider/>
<Card.Actions>
<Button
icon="bug"
mode="contained"
onPress={() => this.props.navigation.navigate('feedback')}
style={styles.editButton}>
{i18n.t("screens.feedback.homeButtonTitle")}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Checks if the given field is available
*
* @param field The field to check
* @return {boolean}
*/
isFieldAvailable(field: ?string) {
return field !== null;
}
/**
* Gets the given field value.
* If the field does not have a value, returns a placeholder text
*
* @param field The field to get the value from
* @return {*}
*/
getFieldValue(field: ?string) {
return this.isFieldAvailable(field)
? field
: i18n.t("screens.profile.noData");
}
/**
* Gets a list item showing personal information
*
* @param field The field to display
* @param icon The icon to use
* @return {*}
*/
getPersonalListItem(field: ?string, icon: string) {
let title = this.isFieldAvailable(field) ? this.getFieldValue(field) : ':(';
let subtitle = this.isFieldAvailable(field) ? '' : this.getFieldValue(field);
return (
<List.Item
title={title}
description={subtitle}
left={props => <List.Icon
{...props}
icon={icon}
color={this.isFieldAvailable(field) ? undefined : this.props.theme.colors.textDisabled}
/>}
/>
);
}
/**
* Gets a card containing user personal information
*
* @return {*}
*/
getPersonalCard() {
return (
<Card style={styles.card}>
<Card.Title
title={this.data.first_name + ' ' + this.data.last_name}
subtitle={this.data.email}
left={(props) => <Avatar.Icon
{...props}
icon="account"
color={this.props.theme.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<Divider/>
<List.Section>
<List.Subheader>{i18n.t("screens.profile.personalInformation")}</List.Subheader>
{this.getPersonalListItem(this.data.birthday, "cake-variant")}
{this.getPersonalListItem(this.data.phone, "phone")}
{this.getPersonalListItem(this.data.email, "email")}
{this.getPersonalListItem(this.data.branch, "school")}
</List.Section>
<Divider/>
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => this.props.navigation.navigate("website", {
host: AvailableWebsites.websites.AMICALE,
path: this.data.link,
title: i18n.t('screens.websites.amicale')
})}
style={styles.editButton}>
{i18n.t("screens.profile.editInformation")}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets a cars containing clubs the user is part of
*
* @return {*}
*/
getClubCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.clubs")}
subtitle={i18n.t("screens.profile.clubsSubtitle")}
left={(props) => <Avatar.Icon
{...props}
icon="account-group"
color={this.props.theme.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<Divider/>
{this.getClubList(this.data.clubs)}
</Card.Content>
</Card>
);
}
/**
* Gets a card showing if the user has payed his membership
*
* @return {*}
*/
getMembershipCar() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.membership")}
subtitle={i18n.t("screens.profile.membershipSubtitle")}
left={(props) => <Avatar.Icon
{...props}
icon="credit-card"
color={this.props.theme.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<List.Section>
{this.getMembershipItem(this.data.validity)}
</List.Section>
</Card.Content>
</Card>
);
}
/**
* Gets the item showing if the user has payed his membership
*
* @return {*}
*/
getMembershipItem(state: boolean) {
return (
<List.Item
title={state ? i18n.t("screens.profile.membershipPayed") : i18n.t("screens.profile.membershipNotPayed")}
left={props => <List.Icon
{...props}
color={state ? this.props.theme.colors.success : this.props.theme.colors.danger}
icon={state ? 'check' : 'close'}
/>}
/>
);
}
/**
* Opens the club details screen for the club of given ID
* @param id The club's id to open
*/
openClubDetailsScreen(id: number) {
this.props.navigation.navigate("club-information", {clubId: id});
}
/**
* Gets a list item for the club list
*
* @param item The club to render
* @return {*}
*/
clubListItem = ({item}: { item: Club }) => {
const onPress = () => this.openClubDetailsScreen(item.id);
let description = i18n.t("screens.profile.isMember");
let icon = (props) => <List.Icon {...props} icon="chevron-right"/>;
if (item.is_manager) {
description = i18n.t("screens.profile.isManager");
icon = (props) => <List.Icon {...props} icon="star" color={this.props.theme.colors.primary}/>;
}
return <List.Item
title={item.name}
description={description}
left={icon}
onPress={onPress}
/>;
};
clubKeyExtractor = (item: Club) => item.name;
sortClubList = (a: Club, b: Club) => a.is_manager ? -1 : 1;
/**
* Renders the list of clubs the user is part of
*
* @param list The club list
* @return {*}
*/
getClubList(list: Array<Club>) {
list.sort(this.sortClubList);
return (
//$FlowFixMe
<FlatList
renderItem={this.clubListItem}
keyExtractor={this.clubKeyExtractor}
data={list}
/>
);
}
render() {
return (
<AuthenticatedScreen
{...this.props}
requests={[
{
link: 'user/profile',
params: {},
mandatory: true,
}
]}
renderFunction={this.getScreen}
/>
);
}
}
type ProfileDataType = {
first_name: string,
last_name: string,
email: string,
birthday: string,
phone: string,
branch: string,
link: string,
validity: boolean,
clubs: Array<ClubType>,
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
editButton: {
marginLeft: 'auto'
}
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
editButton: {
marginLeft: 'auto',
},
});
class ProfileScreen extends React.Component<PropsType, StateType> {
data: ProfileDataType;
flatListData: Array<{id: string}>;
amicaleDataset: Array<ServiceItemType>;
constructor(props: PropsType) {
super(props);
this.flatListData = [{id: '0'}, {id: '1'}, {id: '2'}, {id: '3'}];
const services = new ServicesManager(props.navigation);
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
this.state = {
dialogVisible: false,
};
}
componentDidMount() {
const {navigation} = this.props;
navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = (): React.Node => (
<MaterialHeaderButtons>
<Item
title="logout"
iconName="logout"
onPress={this.showDisconnectDialog}
/>
</MaterialHeaderButtons>
);
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<ProfileDataType | null>): React.Node => {
const {dialogVisible} = this.state;
const {navigation} = this.props;
// eslint-disable-next-line prefer-destructuring
if (data[0] != null) this.data = data[0];
return (
<View style={{flex: 1}}>
<CollapsibleFlatList
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog
navigation={navigation}
visible={dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
);
};
getRenderItem = ({item}: {item: {id: string}}): React.Node => {
switch (item.id) {
case '0':
return this.getWelcomeCard();
case '1':
return this.getPersonalCard();
case '2':
return this.getClubCard();
default:
return this.getMembershipCar();
}
};
/**
* Gets the list of services available with the Amicale account
*
* @returns {*}
*/
getServicesList(): React.Node {
return <CardList dataset={this.amicaleDataset} isHorizontal />;
}
/**
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard(): React.Node {
const {navigation} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.welcomeTitle', {
name: this.data.first_name,
})}
left={(): React.Node => (
<Mascot
style={{
width: 60,
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
/>
)}
titleStyle={{marginLeft: 10}}
/>
<Card.Content>
<Divider />
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
{this.getServicesList()}
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
<Divider />
<Card.Actions>
<Button
icon="bug"
mode="contained"
onPress={() => {
navigation.navigate('feedback');
}}
style={styles.editButton}>
{i18n.t('screens.feedback.homeButtonTitle')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets the given field value.
* If the field does not have a value, returns a placeholder text
*
* @param field The field to get the value from
* @return {*}
*/
static getFieldValue(field: ?string): string {
return field != null ? field : i18n.t('screens.profile.noData');
}
/**
* Gets a list item showing personal information
*
* @param field The field to display
* @param icon The icon to use
* @return {*}
*/
getPersonalListItem(field: ?string, icon: string): React.Node {
const {theme} = this.props;
const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
return (
<List.Item
title={title}
description={subtitle}
left={({size}: {size: number}): React.Node => (
<List.Icon
size={size}
icon={icon}
color={field != null ? null : theme.colors.textDisabled}
/>
)}
/>
);
}
/**
* Gets a card containing user personal information
*
* @return {*}
*/
getPersonalCard(): React.Node {
const {theme, navigation} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={`${this.data.first_name} ${this.data.last_name}`}
subtitle={this.data.email}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
icon="account"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<Divider />
<List.Section>
<List.Subheader>
{i18n.t('screens.profile.personalInformation')}
</List.Subheader>
{this.getPersonalListItem(this.data.birthday, 'cake-variant')}
{this.getPersonalListItem(this.data.phone, 'phone')}
{this.getPersonalListItem(this.data.email, 'email')}
{this.getPersonalListItem(this.data.branch, 'school')}
</List.Section>
<Divider />
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => {
navigation.navigate('website', {
host: AvailableWebsites.websites.AMICALE,
path: this.data.link,
title: i18n.t('screens.websites.amicale'),
});
}}
style={styles.editButton}>
{i18n.t('screens.profile.editInformation')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets a cars containing clubs the user is part of
*
* @return {*}
*/
getClubCard(): React.Node {
const {theme} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.clubs')}
subtitle={i18n.t('screens.profile.clubsSubtitle')}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
icon="account-group"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<Divider />
{this.getClubList(this.data.clubs)}
</Card.Content>
</Card>
);
}
/**
* Gets a card showing if the user has payed his membership
*
* @return {*}
*/
getMembershipCar(): React.Node {
const {theme} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.membership')}
subtitle={i18n.t('screens.profile.membershipSubtitle')}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
icon="credit-card"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<List.Section>
{this.getMembershipItem(this.data.validity)}
</List.Section>
</Card.Content>
</Card>
);
}
/**
* Gets the item showing if the user has payed his membership
*
* @return {*}
*/
getMembershipItem(state: boolean): React.Node {
const {theme} = this.props;
return (
<List.Item
title={
state
? i18n.t('screens.profile.membershipPayed')
: i18n.t('screens.profile.membershipNotPayed')
}
left={({size}: {size: number}): React.Node => (
<List.Icon
size={size}
color={state ? theme.colors.success : theme.colors.danger}
icon={state ? 'check' : 'close'}
/>
)}
/>
);
}
/**
* Gets a list item for the club list
*
* @param item The club to render
* @return {*}
*/
getClubListItem = ({item}: {item: ClubType}): React.Node => {
const {theme} = this.props;
const onPress = () => {
this.openClubDetailsScreen(item.id);
};
let description = i18n.t('screens.profile.isMember');
let icon = ({size, color}: {size: number, color: string}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
);
if (item.is_manager) {
description = i18n.t('screens.profile.isManager');
icon = ({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="star" color={theme.colors.primary} />
);
}
return (
<List.Item
title={item.name}
description={description}
left={icon}
onPress={onPress}
/>
);
};
/**
* Renders the list of clubs the user is part of
*
* @param list The club list
* @return {*}
*/
getClubList(list: Array<ClubType>): React.Node {
list.sort(this.sortClubList);
return (
<FlatList
renderItem={this.getClubListItem}
keyExtractor={this.clubKeyExtractor}
data={list}
/>
);
}
clubKeyExtractor = (item: ClubType): string => item.name;
sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
showDisconnectDialog = () => {
this.setState({dialogVisible: true});
};
hideDisconnectDialog = () => {
this.setState({dialogVisible: false});
};
/**
* Opens the club details screen for the club of given ID
* @param id The club's id to open
*/
openClubDetailsScreen(id: number) {
const {navigation} = this.props;
navigation.navigate('club-information', {clubId: id});
}
render(): React.Node {
const {navigation} = this.props;
return (
<AuthenticatedScreen
navigation={navigation}
requests={[
{
link: 'user/profile',
params: {},
mandatory: true,
},
]}
renderFunction={this.getScreen}
/>
);
}
}
export default withTheme(ProfileScreen);