forked from vergnet/application-amicale
Improve Amicale screen components to match linter
This commit is contained in:
parent
483970c9a8
commit
0a64f5fcd7
6 changed files with 1028 additions and 944 deletions
|
@ -4,137 +4,157 @@ import * as React from 'react';
|
||||||
import {FlatList, Image, Linking, View} from 'react-native';
|
import {FlatList, Image, Linking, View} from 'react-native';
|
||||||
import {Card, List, Text, withTheme} from 'react-native-paper';
|
import {Card, List, Text, withTheme} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
|
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
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 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
|
constructor() {
|
||||||
CONTACT_DATASET: Array<DatasetItem>;
|
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) {
|
keyExtractor = (item: DatasetItemType): string => item.email;
|
||||||
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: 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'}/>;
|
getRenderItem = ({item}: {item: DatasetItemType}): React.Node => {
|
||||||
|
const onPress = () => {
|
||||||
renderItem = ({item}: { item: DatasetItem }) => {
|
Linking.openURL(`mailto:${item.email}`);
|
||||||
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}
|
|
||||||
/>
|
|
||||||
};
|
};
|
||||||
|
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 = () => {
|
getScreen = (): React.Node => {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={{
|
<View
|
||||||
width: '100%',
|
style={{
|
||||||
height: 100,
|
width: '100%',
|
||||||
marginTop: 20,
|
height: 100,
|
||||||
marginBottom: 20,
|
marginTop: 20,
|
||||||
justifyContent: 'center',
|
marginBottom: 20,
|
||||||
alignItems: 'center'
|
justifyContent: 'center',
|
||||||
}}>
|
alignItems: 'center',
|
||||||
<Image
|
}}>
|
||||||
source={require('../../../assets/amicale.png')}
|
<Image
|
||||||
style={{flex: 1, resizeMode: "contain"}}
|
source={AMICALE_LOGO}
|
||||||
resizeMode="contain"/>
|
style={{flex: 1, resizeMode: 'contain'}}
|
||||||
</View>
|
resizeMode="contain"
|
||||||
<Card style={{margin: 5}}>
|
/>
|
||||||
<Card.Title
|
</View>
|
||||||
title={i18n.t("screens.amicaleAbout.title")}
|
<Card style={{margin: 5}}>
|
||||||
subtitle={i18n.t("screens.amicaleAbout.subtitle")}
|
<Card.Title
|
||||||
left={props => <List.Icon {...props} icon={'information'}/>}
|
title={i18n.t('screens.amicaleAbout.title')}
|
||||||
/>
|
subtitle={i18n.t('screens.amicaleAbout.subtitle')}
|
||||||
<Card.Content>
|
left={({
|
||||||
<Text>{i18n.t("screens.amicaleAbout.message")}</Text>
|
size,
|
||||||
{/*$FlowFixMe*/}
|
color,
|
||||||
<FlatList
|
}: {
|
||||||
data={this.CONTACT_DATASET}
|
size: number,
|
||||||
keyExtractor={this.keyExtractor}
|
color: string,
|
||||||
renderItem={this.renderItem}
|
}): React.Node => (
|
||||||
/>
|
<List.Icon size={size} color={color} icon="information" />
|
||||||
</Card.Content>
|
)}
|
||||||
</Card>
|
/>
|
||||||
</View>
|
<Card.Content>
|
||||||
);
|
<Text>{i18n.t('screens.amicaleAbout.message')}</Text>
|
||||||
};
|
<FlatList
|
||||||
|
data={this.CONTACT_DATASET}
|
||||||
render() {
|
keyExtractor={this.keyExtractor}
|
||||||
return (
|
renderItem={this.getRenderItem}
|
||||||
<CollapsibleFlatList
|
|
||||||
data={[{key: "1"}]}
|
|
||||||
renderItem={this.getScreen}
|
|
||||||
hasTab={true}
|
|
||||||
/>
|
/>
|
||||||
);
|
</Card.Content>
|
||||||
}
|
</Card>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
return (
|
||||||
|
<CollapsibleFlatList
|
||||||
|
data={[{key: '1'}]}
|
||||||
|
renderItem={this.getScreen}
|
||||||
|
hasTab
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(AmicaleContactScreen);
|
export default withTheme(AmicaleContactScreen);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen
|
||||||
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||||
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
||||||
import type {ClubCategoryType, ClubType} from './ClubListScreen';
|
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 {ERROR_TYPE} from '../../../utils/WebData';
|
||||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
import type {ApiGenericDataType} from '../../../utils/WebData';
|
import type {ApiGenericDataType} from '../../../utils/WebData';
|
||||||
|
@ -32,7 +32,7 @@ type PropsType = {
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
},
|
},
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AMICALE_MAIL = 'clubs@amicale-insat.fr';
|
const AMICALE_MAIL = 'clubs@amicale-insat.fr';
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
import type {DeviceType} from './EquipmentListScreen';
|
import type {DeviceType} from './EquipmentListScreen';
|
||||||
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
|
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
|
||||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
@ -23,7 +23,7 @@ type PropsType = {
|
||||||
dates: [string, string],
|
dates: [string, string],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
class EquipmentConfirmScreen extends React.Component<PropsType> {
|
class EquipmentConfirmScreen extends React.Component<PropsType> {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import * as Animatable from 'react-native-animatable';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import {CalendarList} from 'react-native-calendars';
|
import {CalendarList} from 'react-native-calendars';
|
||||||
import type {DeviceType} from './EquipmentListScreen';
|
import type {DeviceType} from './EquipmentListScreen';
|
||||||
import type {CustomTheme} from '../../../managers/ThemeManager';
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
||||||
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
||||||
import {
|
import {
|
||||||
|
@ -36,7 +36,7 @@ type PropsType = {
|
||||||
item?: DeviceType,
|
item?: DeviceType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MarkedDatesObjectType = {
|
export type MarkedDatesObjectType = {
|
||||||
|
|
|
@ -1,417 +1,446 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Image, KeyboardAvoidingView, StyleSheet, View} from "react-native";
|
import {Image, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
|
||||||
import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper';
|
import {
|
||||||
import ConnectionManager from "../../managers/ConnectionManager";
|
Button,
|
||||||
|
Card,
|
||||||
|
HelperText,
|
||||||
|
TextInput,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import ErrorDialog from "../../components/Dialogs/ErrorDialog";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
||||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||||
import MascotPopup from "../../components/Mascot/MascotPopup";
|
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
|
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
route: { params: { nextScreen: string } },
|
route: {params: {nextScreen: string}},
|
||||||
theme: CustomTheme
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
isEmailValidated: boolean,
|
isEmailValidated: boolean,
|
||||||
isPasswordValidated: boolean,
|
isPasswordValidated: boolean,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
dialogVisible: boolean,
|
dialogVisible: boolean,
|
||||||
dialogError: number,
|
dialogError: number,
|
||||||
mascotDialogVisible: boolean,
|
mascotDialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
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 = /^.+@.+\..+$/;
|
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({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
marginTop: 'auto',
|
marginTop: 'auto',
|
||||||
marginBottom: 'auto',
|
marginBottom: 'auto',
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
marginBottom: 48
|
marginBottom: 48,
|
||||||
},
|
},
|
||||||
textInput: {},
|
textInput: {},
|
||||||
btnContainer: {
|
btnContainer: {
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
marginBottom: 10,
|
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);
|
export default withTheme(LoginScreen);
|
||||||
|
|
|
@ -1,432 +1,467 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {FlatList, StyleSheet, View} from "react-native";
|
import {FlatList, StyleSheet, View} from 'react-native';
|
||||||
import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper';
|
import {
|
||||||
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
Paragraph,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import LogoutDialog from "../../components/Amicale/LogoutDialog";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
|
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
|
||||||
import type {cardList} from "../../components/Lists/CardList/CardList";
|
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||||
import CardList from "../../components/Lists/CardList/CardList";
|
import MaterialHeaderButtons, {
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
Item,
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
} from '../../components/Overrides/CustomHeaderButton';
|
||||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
import CardList from '../../components/Lists/CardList/CardList';
|
||||||
import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager";
|
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
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 = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
dialogVisible: boolean,
|
dialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
type ProfileData = {
|
type ClubType = {
|
||||||
first_name: string,
|
id: number,
|
||||||
last_name: string,
|
name: string,
|
||||||
email: string,
|
is_manager: boolean,
|
||||||
birthday: string,
|
};
|
||||||
phone: string,
|
|
||||||
branch: string,
|
|
||||||
link: string,
|
|
||||||
validity: boolean,
|
|
||||||
clubs: Array<Club>,
|
|
||||||
}
|
|
||||||
type Club = {
|
|
||||||
id: number,
|
|
||||||
name: string,
|
|
||||||
is_manager: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProfileScreen extends React.Component<Props, State> {
|
type ProfileDataType = {
|
||||||
|
first_name: string,
|
||||||
state = {
|
last_name: string,
|
||||||
dialogVisible: false,
|
email: string,
|
||||||
};
|
birthday: string,
|
||||||
|
phone: string,
|
||||||
data: ProfileData;
|
branch: string,
|
||||||
|
link: string,
|
||||||
flatListData: Array<{ id: string }>;
|
validity: boolean,
|
||||||
amicaleDataset: cardList;
|
clubs: Array<ClubType>,
|
||||||
|
};
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
margin: 10,
|
margin: 10,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
editButton: {
|
editButton: {
|
||||||
marginLeft: 'auto'
|
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);
|
export default withTheme(ProfileScreen);
|
||||||
|
|
Loading…
Reference in a new issue