convert connection manager to context
This commit is contained in:
parent
44aa52b3aa
commit
541c002558
24 changed files with 1610 additions and 1965 deletions
15
App.tsx
15
App.tsx
|
@ -21,7 +21,6 @@ import React from 'react';
|
||||||
import { LogBox, Platform } from 'react-native';
|
import { LogBox, Platform } from 'react-native';
|
||||||
import { setSafeBounceHeight } from 'react-navigation-collapsible';
|
import { setSafeBounceHeight } from 'react-navigation-collapsible';
|
||||||
import SplashScreen from 'react-native-splash-screen';
|
import SplashScreen from 'react-native-splash-screen';
|
||||||
import ConnectionManager from './src/managers/ConnectionManager';
|
|
||||||
import type { ParsedUrlDataType } from './src/utils/URLHandler';
|
import type { ParsedUrlDataType } from './src/utils/URLHandler';
|
||||||
import URLHandler from './src/utils/URLHandler';
|
import URLHandler from './src/utils/URLHandler';
|
||||||
import initLocales from './src/utils/Locales';
|
import initLocales from './src/utils/Locales';
|
||||||
|
@ -48,6 +47,8 @@ import {
|
||||||
ProxiwashPreferencesProvider,
|
ProxiwashPreferencesProvider,
|
||||||
} from './src/components/providers/PreferencesProvider';
|
} from './src/components/providers/PreferencesProvider';
|
||||||
import MainApp from './src/screens/MainApp';
|
import MainApp from './src/screens/MainApp';
|
||||||
|
import LoginProvider from './src/components/providers/LoginProvider';
|
||||||
|
import { retrieveLoginToken } from './src/utils/loginToken';
|
||||||
|
|
||||||
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
||||||
// Crashes app when navigating away from webview on android 9+
|
// Crashes app when navigating away from webview on android 9+
|
||||||
|
@ -67,6 +68,7 @@ type StateType = {
|
||||||
proxiwash: ProxiwashPreferencesType;
|
proxiwash: ProxiwashPreferencesType;
|
||||||
mascot: MascotPreferencesType;
|
mascot: MascotPreferencesType;
|
||||||
};
|
};
|
||||||
|
loginToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class App extends React.Component<{}, StateType> {
|
export default class App extends React.Component<{}, StateType> {
|
||||||
|
@ -88,6 +90,7 @@ export default class App extends React.Component<{}, StateType> {
|
||||||
proxiwash: defaultProxiwashPreferences,
|
proxiwash: defaultProxiwashPreferences,
|
||||||
mascot: defaultMascotPreferences,
|
mascot: defaultMascotPreferences,
|
||||||
},
|
},
|
||||||
|
loginToken: undefined,
|
||||||
};
|
};
|
||||||
initLocales();
|
initLocales();
|
||||||
this.navigatorRef = React.createRef();
|
this.navigatorRef = React.createRef();
|
||||||
|
@ -136,10 +139,11 @@ export default class App extends React.Component<{}, StateType> {
|
||||||
| PlanexPreferencesType
|
| PlanexPreferencesType
|
||||||
| ProxiwashPreferencesType
|
| ProxiwashPreferencesType
|
||||||
| MascotPreferencesType
|
| MascotPreferencesType
|
||||||
| void
|
| string
|
||||||
|
| undefined
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const [general, planex, proxiwash, mascot] = values;
|
const [general, planex, proxiwash, mascot, token] = values;
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
initialPreferences: {
|
initialPreferences: {
|
||||||
|
@ -148,6 +152,7 @@ export default class App extends React.Component<{}, StateType> {
|
||||||
proxiwash: proxiwash as ProxiwashPreferencesType,
|
proxiwash: proxiwash as ProxiwashPreferencesType,
|
||||||
mascot: mascot as MascotPreferencesType,
|
mascot: mascot as MascotPreferencesType,
|
||||||
},
|
},
|
||||||
|
loginToken: token as string | undefined,
|
||||||
});
|
});
|
||||||
SplashScreen.hide();
|
SplashScreen.hide();
|
||||||
};
|
};
|
||||||
|
@ -175,7 +180,7 @@ export default class App extends React.Component<{}, StateType> {
|
||||||
Object.values(MascotPreferenceKeys),
|
Object.values(MascotPreferenceKeys),
|
||||||
defaultMascotPreferences
|
defaultMascotPreferences
|
||||||
),
|
),
|
||||||
ConnectionManager.getInstance().recoverLogin(),
|
retrieveLoginToken(),
|
||||||
])
|
])
|
||||||
.then(this.onLoadFinished)
|
.then(this.onLoadFinished)
|
||||||
.catch(this.onLoadFinished);
|
.catch(this.onLoadFinished);
|
||||||
|
@ -202,11 +207,13 @@ export default class App extends React.Component<{}, StateType> {
|
||||||
<MascotPreferencesProvider
|
<MascotPreferencesProvider
|
||||||
initialPreferences={this.state.initialPreferences.mascot}
|
initialPreferences={this.state.initialPreferences.mascot}
|
||||||
>
|
>
|
||||||
|
<LoginProvider initialToken={this.state.loginToken}>
|
||||||
<MainApp
|
<MainApp
|
||||||
ref={this.navigatorRef}
|
ref={this.navigatorRef}
|
||||||
defaultHomeData={this.defaultHomeData}
|
defaultHomeData={this.defaultHomeData}
|
||||||
defaultHomeRoute={this.defaultHomeRoute}
|
defaultHomeRoute={this.defaultHomeRoute}
|
||||||
/>
|
/>
|
||||||
|
</LoginProvider>
|
||||||
</MascotPreferencesProvider>
|
</MascotPreferencesProvider>
|
||||||
</ProxiwashPreferencesProvider>
|
</ProxiwashPreferencesProvider>
|
||||||
</PlanexPreferencesProvider>
|
</PlanexPreferencesProvider>
|
||||||
|
|
231
src/components/Amicale/Login/LoginForm.tsx
Normal file
231
src/components/Amicale/Login/LoginForm.tsx
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Image,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
TextInput as RNTextInput,
|
||||||
|
} from 'react-native';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
HelperText,
|
||||||
|
TextInput,
|
||||||
|
useTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import GENERAL_STYLES from '../../../constants/Styles';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean;
|
||||||
|
onSubmit: (email: string, password: string) => void;
|
||||||
|
onHelpPress: () => void;
|
||||||
|
onResetPasswordPress: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
fontSize: 36,
|
||||||
|
marginBottom: 48,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
lockButton: {
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
sendButton: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailRegex = /^.+@.+\..+$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the entered email is valid (matches the regex)
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isEmailValid(email: string): boolean {
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user has entered a password
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isPasswordValid(password: string): boolean {
|
||||||
|
return password !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginForm(props: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isEmailValidated, setIsEmailValidated] = useState(false);
|
||||||
|
const [isPasswordValidated, setIsPasswordValidated] = useState(false);
|
||||||
|
const passwordRef = useRef<RNTextInput>(null);
|
||||||
|
/**
|
||||||
|
* 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}
|
||||||
|
*/
|
||||||
|
const shouldShowEmailError = () => {
|
||||||
|
return isEmailValidated && !isEmailValid(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}
|
||||||
|
*/
|
||||||
|
const shouldShowPasswordError = () => {
|
||||||
|
return isPasswordValidated && !isPasswordValid(password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEmailSubmit = () => {
|
||||||
|
if (passwordRef.current) {
|
||||||
|
passwordRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has unfocused the input, his email is ready to be validated
|
||||||
|
*/
|
||||||
|
const validateEmail = () => setIsEmailValidated(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has unfocused the input, his password is ready to be validated
|
||||||
|
*/
|
||||||
|
const validatePassword = () => setIsPasswordValidated(true);
|
||||||
|
|
||||||
|
const onEmailChange = (value: string) => {
|
||||||
|
if (isEmailValidated) {
|
||||||
|
setIsEmailValidated(false);
|
||||||
|
}
|
||||||
|
setEmail(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPasswordChange = (value: string) => {
|
||||||
|
if (isPasswordValidated) {
|
||||||
|
setIsPasswordValidated(false);
|
||||||
|
}
|
||||||
|
setPassword(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldEnableLogin = () => {
|
||||||
|
return isEmailValid(email) && isPasswordValid(password) && !props.loading;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (shouldEnableLogin()) {
|
||||||
|
props.onSubmit(email, password);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={i18n.t('screens.login.title')}
|
||||||
|
titleStyle={styles.text}
|
||||||
|
subtitle={i18n.t('screens.login.subtitle')}
|
||||||
|
subtitleStyle={styles.text}
|
||||||
|
left={({ size }) => (
|
||||||
|
<Image
|
||||||
|
source={ICON_AMICALE}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<View>
|
||||||
|
<TextInput
|
||||||
|
label={i18n.t('screens.login.email')}
|
||||||
|
mode={'outlined'}
|
||||||
|
value={email}
|
||||||
|
onChangeText={onEmailChange}
|
||||||
|
onBlur={validateEmail}
|
||||||
|
onSubmitEditing={onEmailSubmit}
|
||||||
|
error={shouldShowEmailError()}
|
||||||
|
textContentType={'emailAddress'}
|
||||||
|
autoCapitalize={'none'}
|
||||||
|
autoCompleteType={'email'}
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardType={'email-address'}
|
||||||
|
returnKeyType={'next'}
|
||||||
|
secureTextEntry={false}
|
||||||
|
/>
|
||||||
|
<HelperText type={'error'} visible={shouldShowEmailError()}>
|
||||||
|
{i18n.t('screens.login.emailError')}
|
||||||
|
</HelperText>
|
||||||
|
<TextInput
|
||||||
|
ref={passwordRef}
|
||||||
|
label={i18n.t('screens.login.password')}
|
||||||
|
mode={'outlined'}
|
||||||
|
value={password}
|
||||||
|
onChangeText={onPasswordChange}
|
||||||
|
onBlur={validatePassword}
|
||||||
|
onSubmitEditing={onSubmit}
|
||||||
|
error={shouldShowPasswordError()}
|
||||||
|
textContentType={'password'}
|
||||||
|
autoCapitalize={'none'}
|
||||||
|
autoCompleteType={'password'}
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardType={'default'}
|
||||||
|
returnKeyType={'done'}
|
||||||
|
secureTextEntry={true}
|
||||||
|
/>
|
||||||
|
<HelperText type={'error'} visible={shouldShowPasswordError()}>
|
||||||
|
{i18n.t('screens.login.passwordError')}
|
||||||
|
</HelperText>
|
||||||
|
</View>
|
||||||
|
<Card.Actions style={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
icon="lock-question"
|
||||||
|
mode="contained"
|
||||||
|
onPress={props.onResetPasswordPress}
|
||||||
|
color={theme.colors.warning}
|
||||||
|
style={styles.lockButton}
|
||||||
|
>
|
||||||
|
{i18n.t('screens.login.resetPassword')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon="send"
|
||||||
|
mode="contained"
|
||||||
|
disabled={!shouldEnableLogin()}
|
||||||
|
loading={props.loading}
|
||||||
|
onPress={onSubmit}
|
||||||
|
style={styles.sendButton}
|
||||||
|
>
|
||||||
|
{i18n.t('screens.login.title')}
|
||||||
|
</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
<Card.Actions>
|
||||||
|
<Button
|
||||||
|
icon="help-circle"
|
||||||
|
mode="contained"
|
||||||
|
onPress={props.onHelpPress}
|
||||||
|
style={GENERAL_STYLES.centerHorizontal}
|
||||||
|
>
|
||||||
|
{i18n.t('screens.login.mascotDialog.title')}
|
||||||
|
</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
</Card.Content>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
|
@ -20,8 +20,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
import { useLogout } from '../../utils/logout';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -29,20 +28,13 @@ type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function LogoutDialog(props: PropsType) {
|
function LogoutDialog(props: PropsType) {
|
||||||
const navigation = useNavigation();
|
const onLogout = useLogout();
|
||||||
|
// Use a loading dialog as it can take some time to update the context
|
||||||
const onClickAccept = async (): Promise<void> => {
|
const onClickAccept = async (): Promise<void> => {
|
||||||
return new Promise((resolve: () => void) => {
|
return new Promise((resolve: () => void) => {
|
||||||
ConnectionManager.getInstance()
|
onLogout();
|
||||||
.disconnect()
|
|
||||||
.then(() => {
|
|
||||||
navigation.reset({
|
|
||||||
index: 0,
|
|
||||||
routes: [{ name: 'main' }],
|
|
||||||
});
|
|
||||||
props.onDismiss();
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
100
src/components/Amicale/Profile/ProfileClubCard.tsx
Normal file
100
src/components/Amicale/Profile/ProfileClubCard.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, Avatar, Divider, useTheme, List } from 'react-native-paper';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import { FlatList, StyleSheet } from 'react-native';
|
||||||
|
import { ProfileClubType } from '../../../screens/Amicale/ProfileScreen';
|
||||||
|
import { useNavigation } from '@react-navigation/core';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
clubs?: Array<ProfileClubType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ProfileClubCard(props: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const clubKeyExtractor = (item: ProfileClubType) => item.name;
|
||||||
|
|
||||||
|
const getClubListItem = ({ item }: { item: ProfileClubType }) => {
|
||||||
|
const onPress = () =>
|
||||||
|
navigation.navigate('club-information', { clubId: item.id });
|
||||||
|
let description = i18n.t('screens.profile.isMember');
|
||||||
|
let icon = (leftProps: {
|
||||||
|
color: string;
|
||||||
|
style: {
|
||||||
|
marginLeft: number;
|
||||||
|
marginRight: number;
|
||||||
|
marginVertical?: number;
|
||||||
|
};
|
||||||
|
}) => (
|
||||||
|
<List.Icon
|
||||||
|
color={leftProps.color}
|
||||||
|
style={leftProps.style}
|
||||||
|
icon="chevron-right"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (item.is_manager) {
|
||||||
|
description = i18n.t('screens.profile.isManager');
|
||||||
|
icon = (leftProps) => (
|
||||||
|
<List.Icon
|
||||||
|
style={leftProps.style}
|
||||||
|
icon="star"
|
||||||
|
color={theme.colors.primary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
title={item.name}
|
||||||
|
description={description}
|
||||||
|
left={icon}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getClubList(list: Array<ProfileClubType> | undefined) {
|
||||||
|
if (!list) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sort((a) => (a.is_manager ? -1 : 1));
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
renderItem={getClubListItem}
|
||||||
|
keyExtractor={clubKeyExtractor}
|
||||||
|
data={list}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={i18n.t('screens.profile.clubs')}
|
||||||
|
subtitle={i18n.t('screens.profile.clubsSubtitle')}
|
||||||
|
left={(iconProps) => (
|
||||||
|
<Avatar.Icon
|
||||||
|
size={iconProps.size}
|
||||||
|
icon="account-group"
|
||||||
|
color={theme.colors.primary}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<Divider />
|
||||||
|
{getClubList(props.clubs)}
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
56
src/components/Amicale/Profile/ProfileMembershipCard.tsx
Normal file
56
src/components/Amicale/Profile/ProfileMembershipCard.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Avatar, Card, List, useTheme } from 'react-native-paper';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
valid?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ProfileMembershipCard(props: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const state = props.valid === true;
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={i18n.t('screens.profile.membership')}
|
||||||
|
subtitle={i18n.t('screens.profile.membershipSubtitle')}
|
||||||
|
left={(iconProps) => (
|
||||||
|
<Avatar.Icon
|
||||||
|
size={iconProps.size}
|
||||||
|
icon="credit-card"
|
||||||
|
color={theme.colors.primary}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<List.Section>
|
||||||
|
<List.Item
|
||||||
|
title={
|
||||||
|
state
|
||||||
|
? i18n.t('screens.profile.membershipPayed')
|
||||||
|
: i18n.t('screens.profile.membershipNotPayed')
|
||||||
|
}
|
||||||
|
left={(leftProps) => (
|
||||||
|
<List.Icon
|
||||||
|
style={leftProps.style}
|
||||||
|
color={state ? theme.colors.success : theme.colors.danger}
|
||||||
|
icon={state ? 'check' : 'close'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</List.Section>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
110
src/components/Amicale/Profile/ProfilePersonalCard.tsx
Normal file
110
src/components/Amicale/Profile/ProfilePersonalCard.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { useNavigation } from '@react-navigation/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
useTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import Urls from '../../../constants/Urls';
|
||||||
|
import { ProfileDataType } from '../../../screens/Amicale/ProfileScreen';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
profile?: ProfileDataType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
mascot: {
|
||||||
|
width: 60,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFieldValue(field?: string): string {
|
||||||
|
return field ? field : i18n.t('screens.profile.noData');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfilePersonalCard(props: Props) {
|
||||||
|
const { profile } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
function getPersonalListItem(field: string | undefined, icon: string) {
|
||||||
|
const title = field != null ? getFieldValue(field) : ':(';
|
||||||
|
const subtitle = field != null ? '' : getFieldValue(field);
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
title={title}
|
||||||
|
description={subtitle}
|
||||||
|
left={(leftProps) => (
|
||||||
|
<List.Icon
|
||||||
|
style={leftProps.style}
|
||||||
|
icon={icon}
|
||||||
|
color={field != null ? leftProps.color : theme.colors.textDisabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={`${profile?.first_name} ${profile?.last_name}`}
|
||||||
|
subtitle={profile?.email}
|
||||||
|
left={(iconProps) => (
|
||||||
|
<Avatar.Icon
|
||||||
|
size={iconProps.size}
|
||||||
|
icon="account"
|
||||||
|
color={theme.colors.primary}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<Divider />
|
||||||
|
<List.Section>
|
||||||
|
<List.Subheader>
|
||||||
|
{i18n.t('screens.profile.personalInformation')}
|
||||||
|
</List.Subheader>
|
||||||
|
{getPersonalListItem(profile?.birthday, 'cake-variant')}
|
||||||
|
{getPersonalListItem(profile?.phone, 'phone')}
|
||||||
|
{getPersonalListItem(profile?.email, 'email')}
|
||||||
|
{getPersonalListItem(profile?.branch, 'school')}
|
||||||
|
</List.Section>
|
||||||
|
<Divider />
|
||||||
|
<Card.Actions>
|
||||||
|
<Button
|
||||||
|
icon="account-edit"
|
||||||
|
mode="contained"
|
||||||
|
onPress={() => {
|
||||||
|
navigation.navigate('website', {
|
||||||
|
host: Urls.websites.amicale,
|
||||||
|
path: profile?.link,
|
||||||
|
title: i18n.t('screens.websites.amicale'),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={styles.editButton}
|
||||||
|
>
|
||||||
|
{i18n.t('screens.profile.editInformation')}
|
||||||
|
</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
81
src/components/Amicale/Profile/ProfileWelcomeCard.tsx
Normal file
81
src/components/Amicale/Profile/ProfileWelcomeCard.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { useNavigation } from '@react-navigation/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Card, Divider, Paragraph } from 'react-native-paper';
|
||||||
|
import Mascot, { MASCOT_STYLE } from '../../Mascot/Mascot';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import CardList from '../../Lists/CardList/CardList';
|
||||||
|
import { getAmicaleServices, SERVICES_KEY } from '../../../utils/Services';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
firstname?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
mascot: {
|
||||||
|
width: 60,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function ProfileWelcomeCard(props: Props) {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={i18n.t('screens.profile.welcomeTitle', {
|
||||||
|
name: props.firstname,
|
||||||
|
})}
|
||||||
|
left={() => (
|
||||||
|
<Mascot
|
||||||
|
style={styles.mascot}
|
||||||
|
emotion={MASCOT_STYLE.COOL}
|
||||||
|
animated
|
||||||
|
entryAnimation={{
|
||||||
|
animation: 'bounceIn',
|
||||||
|
duration: 1000,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
titleStyle={styles.title}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<Divider />
|
||||||
|
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
|
||||||
|
<CardList
|
||||||
|
dataset={getAmicaleServices(navigation.navigate, [
|
||||||
|
SERVICES_KEY.PROFILE,
|
||||||
|
])}
|
||||||
|
isHorizontal={true}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(
|
||||||
|
ProfileWelcomeCard,
|
||||||
|
(pp, np) => pp.firstname === np.firstname
|
||||||
|
);
|
|
@ -17,30 +17,23 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
||||||
import { FlatList, StyleSheet, View } from 'react-native';
|
import { FlatList, StyleSheet, View } from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
|
||||||
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
||||||
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
||||||
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
|
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
|
||||||
import { ApiRejectType } from '../../../utils/WebData';
|
import { ApiRejectType } from '../../../utils/WebData';
|
||||||
import { REQUEST_STATUS } from '../../../utils/Requests';
|
import { REQUEST_STATUS } from '../../../utils/Requests';
|
||||||
|
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||||
|
|
||||||
type PropsType = {
|
type Props = {
|
||||||
teams: Array<VoteTeamType>;
|
teams: Array<VoteTeamType>;
|
||||||
onVoteSuccess: () => void;
|
onVoteSuccess: () => void;
|
||||||
onVoteError: () => void;
|
onVoteError: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
selectedTeam: string;
|
|
||||||
voteDialogVisible: boolean;
|
|
||||||
errorDialogVisible: boolean;
|
|
||||||
currentError: ApiRejectType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
margin: 10,
|
margin: 10,
|
||||||
|
@ -50,68 +43,47 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class VoteSelect extends React.PureComponent<
|
function VoteSelect(props: Props) {
|
||||||
PropsType,
|
const [selectedTeam, setSelectedTeam] = useState('none');
|
||||||
StateType
|
const [voteDialogVisible, setVoteDialogVisible] = useState(false);
|
||||||
> {
|
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||||
constructor(props: PropsType) {
|
status: REQUEST_STATUS.SUCCESS,
|
||||||
super(props);
|
});
|
||||||
this.state = {
|
const request = useAuthenticatedRequest('elections/vote', {
|
||||||
selectedTeam: 'none',
|
team: parseInt(selectedTeam, 10),
|
||||||
voteDialogVisible: false,
|
});
|
||||||
errorDialogVisible: false,
|
|
||||||
currentError: { status: REQUEST_STATUS.SUCCESS },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onVoteSelectionChange = (teamName: string): void =>
|
const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
|
||||||
this.setState({ selectedTeam: teamName });
|
|
||||||
|
|
||||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||||
|
|
||||||
voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
|
||||||
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||||
);
|
);
|
||||||
|
|
||||||
showVoteDialog = (): void => this.setState({ voteDialogVisible: true });
|
const showVoteDialog = () => setVoteDialogVisible(true);
|
||||||
|
|
||||||
onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false });
|
const onVoteDialogDismiss = () => setVoteDialogVisible(false);
|
||||||
|
|
||||||
onVoteDialogAccept = async (): Promise<void> => {
|
const onVoteDialogAccept = async (): Promise<void> => {
|
||||||
return new Promise((resolve: () => void) => {
|
return new Promise((resolve: () => void) => {
|
||||||
const { state } = this;
|
request()
|
||||||
ConnectionManager.getInstance()
|
|
||||||
.authenticatedRequest('elections/vote', {
|
|
||||||
team: parseInt(state.selectedTeam, 10),
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.onVoteDialogDismiss();
|
onVoteDialogDismiss();
|
||||||
const { props } = this;
|
|
||||||
props.onVoteSuccess();
|
props.onVoteSuccess();
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error: ApiRejectType) => {
|
.catch((error: ApiRejectType) => {
|
||||||
this.onVoteDialogDismiss();
|
onVoteDialogDismiss();
|
||||||
this.showErrorDialog(error);
|
setCurrentError(error);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
showErrorDialog = (error: ApiRejectType): void =>
|
const onErrorDialogDismiss = () => {
|
||||||
this.setState({
|
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||||
errorDialogVisible: true,
|
|
||||||
currentError: error,
|
|
||||||
});
|
|
||||||
|
|
||||||
onErrorDialogDismiss = () => {
|
|
||||||
this.setState({ errorDialogVisible: false });
|
|
||||||
const { props } = this;
|
|
||||||
props.onVoteError();
|
props.onVoteError();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const { state, props } = this;
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Card style={styles.card}>
|
<Card style={styles.card}>
|
||||||
|
@ -124,44 +96,45 @@ export default class VoteSelect extends React.PureComponent<
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<RadioButton.Group
|
<RadioButton.Group
|
||||||
onValueChange={this.onVoteSelectionChange}
|
onValueChange={setSelectedTeam}
|
||||||
value={state.selectedTeam}
|
value={selectedTeam}
|
||||||
>
|
>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={props.teams}
|
data={props.teams}
|
||||||
keyExtractor={this.voteKeyExtractor}
|
keyExtractor={voteKeyExtractor}
|
||||||
extraData={state.selectedTeam}
|
extraData={selectedTeam}
|
||||||
renderItem={this.voteRenderItem}
|
renderItem={voteRenderItem}
|
||||||
/>
|
/>
|
||||||
</RadioButton.Group>
|
</RadioButton.Group>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
<Card.Actions>
|
<Card.Actions>
|
||||||
<Button
|
<Button
|
||||||
icon="send"
|
icon={'send'}
|
||||||
mode="contained"
|
mode={'contained'}
|
||||||
onPress={this.showVoteDialog}
|
onPress={showVoteDialog}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
disabled={state.selectedTeam === 'none'}
|
disabled={selectedTeam === 'none'}
|
||||||
>
|
>
|
||||||
{i18n.t('screens.vote.select.sendButton')}
|
{i18n.t('screens.vote.select.sendButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Actions>
|
</Card.Actions>
|
||||||
</Card>
|
</Card>
|
||||||
<LoadingConfirmDialog
|
<LoadingConfirmDialog
|
||||||
visible={state.voteDialogVisible}
|
visible={voteDialogVisible}
|
||||||
onDismiss={this.onVoteDialogDismiss}
|
onDismiss={onVoteDialogDismiss}
|
||||||
onAccept={this.onVoteDialogAccept}
|
onAccept={onVoteDialogAccept}
|
||||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||||
/>
|
/>
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
visible={state.errorDialogVisible}
|
visible={currentError.status !== REQUEST_STATUS.SUCCESS}
|
||||||
onDismiss={this.onErrorDialogDismiss}
|
onDismiss={onErrorDialogDismiss}
|
||||||
status={state.currentError.status}
|
status={currentError.status}
|
||||||
code={state.currentError.code}
|
code={currentError.code}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
export default VoteSelect;
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Avatar, List, useTheme } from 'react-native-paper';
|
import { Avatar, List, useTheme } from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||||
import {
|
import {
|
||||||
getFirstEquipmentAvailability,
|
getFirstEquipmentAvailability,
|
||||||
|
@ -29,9 +28,9 @@ import {
|
||||||
} from '../../../utils/EquipmentBooking';
|
} from '../../../utils/EquipmentBooking';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import GENERAL_STYLES from '../../../constants/Styles';
|
import GENERAL_STYLES from '../../../constants/Styles';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp<any>;
|
|
||||||
userDeviceRentDates: [string, string] | null;
|
userDeviceRentDates: [string, string] | null;
|
||||||
item: DeviceType;
|
item: DeviceType;
|
||||||
height: number;
|
height: number;
|
||||||
|
@ -48,7 +47,8 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
function EquipmentListItem(props: PropsType) {
|
function EquipmentListItem(props: PropsType) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { item, userDeviceRentDates, navigation, height } = props;
|
const navigation = useNavigation();
|
||||||
|
const { item, userDeviceRentDates, height } = props;
|
||||||
const isRented = userDeviceRentDates != null;
|
const isRented = userDeviceRentDates != null;
|
||||||
const isAvailable = isEquipmentAvailable(item);
|
const isAvailable = isEquipmentAvailable(item);
|
||||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||||
|
|
|
@ -11,7 +11,7 @@ import i18n from 'i18n-js';
|
||||||
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
import { useLogout } from '../../utils/logout';
|
||||||
|
|
||||||
export type RequestScreenProps<T> = {
|
export type RequestScreenProps<T> = {
|
||||||
request: () => Promise<T>;
|
request: () => Promise<T>;
|
||||||
|
@ -44,6 +44,7 @@ type Props<T> = RequestScreenProps<T>;
|
||||||
const MIN_REFRESH_TIME = 3 * 1000;
|
const MIN_REFRESH_TIME = 3 * 1000;
|
||||||
|
|
||||||
export default function RequestScreen<T>(props: Props<T>) {
|
export default function RequestScreen<T>(props: Props<T>) {
|
||||||
|
const onLogout = useLogout();
|
||||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const refreshInterval = useRef<number>();
|
const refreshInterval = useRef<number>();
|
||||||
|
@ -103,13 +104,10 @@ export default function RequestScreen<T>(props: Props<T>) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isErrorCritical(code)) {
|
if (isErrorCritical(code)) {
|
||||||
ConnectionManager.getInstance()
|
onLogout();
|
||||||
.disconnect()
|
|
||||||
.then(() => {
|
|
||||||
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [code, navigation, route]);
|
}, [code, navigation, route, onLogout]);
|
||||||
|
|
||||||
if (data === undefined && loading && props.showLoading !== false) {
|
if (data === undefined && loading && props.showLoading !== false) {
|
||||||
return <BasicLoadingScreen />;
|
return <BasicLoadingScreen />;
|
||||||
|
|
27
src/components/providers/LoginProvider.tsx
Normal file
27
src/components/providers/LoginProvider.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { LoginContext, LoginContextType } from '../../context/loginContext';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactChild;
|
||||||
|
initialToken: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LoginProvider(props: Props) {
|
||||||
|
const setLogin = (token: string | undefined) => {
|
||||||
|
setLoginState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
token,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [loginState, setLoginState] = useState<LoginContextType>({
|
||||||
|
token: props.initialToken,
|
||||||
|
setLogin: setLogin,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoginContext.Provider value={loginState}>
|
||||||
|
{props.children}
|
||||||
|
</LoginContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
46
src/context/loginContext.ts
Normal file
46
src/context/loginContext.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { apiRequest } from '../utils/WebData';
|
||||||
|
|
||||||
|
export type LoginContextType = {
|
||||||
|
token: string | undefined;
|
||||||
|
setLogin: (token: string | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoginContext = React.createContext<LoginContextType>({
|
||||||
|
token: undefined,
|
||||||
|
setLogin: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook used to retrieve the user token and puid.
|
||||||
|
* @returns Login context with token and puid to undefined if user is not logged in
|
||||||
|
*/
|
||||||
|
export function useLogin() {
|
||||||
|
return useContext(LoginContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user is connected
|
||||||
|
* @returns True if the user is connected
|
||||||
|
*/
|
||||||
|
export function useLoginState() {
|
||||||
|
const { token } = useLogin();
|
||||||
|
return token !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current user token.
|
||||||
|
* @returns The token, or empty string if the user is not logged in
|
||||||
|
*/
|
||||||
|
export function useLoginToken() {
|
||||||
|
const { token } = useLogin();
|
||||||
|
return token ? token : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuthenticatedRequest<T>(
|
||||||
|
path: string,
|
||||||
|
params?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
const token = useLoginToken();
|
||||||
|
return () => apiRequest<T>(path, 'POST', params, token);
|
||||||
|
}
|
|
@ -1,205 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
|
||||||
*
|
|
||||||
* This file is part of Campus INSAT.
|
|
||||||
*
|
|
||||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Campus INSAT is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as Keychain from 'react-native-keychain';
|
|
||||||
import { REQUEST_STATUS } from '../utils/Requests';
|
|
||||||
import type { ApiDataLoginType, ApiRejectType } from '../utils/WebData';
|
|
||||||
import { apiRequest } from '../utils/WebData';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* champ: error
|
|
||||||
*
|
|
||||||
* 0 : SUCCESS -> pas d'erreurs
|
|
||||||
* 1 : BAD_CREDENTIALS -> email ou mdp invalide
|
|
||||||
* 2 : BAD_TOKEN -> session expirée
|
|
||||||
* 3 : NO_CONSENT
|
|
||||||
* 403 : FORBIDDEN -> accès a la ressource interdit
|
|
||||||
* 500 : SERVER_ERROR -> pb coté serveur
|
|
||||||
*/
|
|
||||||
|
|
||||||
const AUTH_PATH = 'password';
|
|
||||||
|
|
||||||
export default class ConnectionManager {
|
|
||||||
static instance: ConnectionManager | null = null;
|
|
||||||
|
|
||||||
private token: string | null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets this class instance or create one if none is found
|
|
||||||
*
|
|
||||||
* @returns {ConnectionManager}
|
|
||||||
*/
|
|
||||||
static getInstance(): ConnectionManager {
|
|
||||||
if (ConnectionManager.instance == null) {
|
|
||||||
ConnectionManager.instance = new ConnectionManager();
|
|
||||||
}
|
|
||||||
return ConnectionManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current token
|
|
||||||
*
|
|
||||||
* @returns {string | null}
|
|
||||||
*/
|
|
||||||
getToken(): string | null {
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to recover login token from the secure keychain
|
|
||||||
*
|
|
||||||
* @returns Promise<void>
|
|
||||||
*/
|
|
||||||
async recoverLogin(): Promise<void> {
|
|
||||||
return new Promise((resolve: () => void) => {
|
|
||||||
const token = this.getToken();
|
|
||||||
if (token != null) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
Keychain.getGenericPassword()
|
|
||||||
.then((data: Keychain.UserCredentials | false) => {
|
|
||||||
if (data && data.password != null) {
|
|
||||||
this.token = data.password;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(() => resolve());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the user has a valid token
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isLoggedIn(): boolean {
|
|
||||||
return this.getToken() !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the login token in the secure keychain
|
|
||||||
*
|
|
||||||
* @param email
|
|
||||||
* @param token
|
|
||||||
* @returns Promise<void>
|
|
||||||
*/
|
|
||||||
async saveLogin(_email: string, token: string): Promise<void> {
|
|
||||||
return new Promise((resolve: () => void, reject: () => void) => {
|
|
||||||
Keychain.setGenericPassword('token', token)
|
|
||||||
.then(() => {
|
|
||||||
this.token = token;
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((): void => reject());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the login token from the keychain
|
|
||||||
*
|
|
||||||
* @returns Promise<void>
|
|
||||||
*/
|
|
||||||
async disconnect(): Promise<void> {
|
|
||||||
return new Promise((resolve: () => void, reject: () => void) => {
|
|
||||||
Keychain.resetGenericPassword()
|
|
||||||
.then(() => {
|
|
||||||
this.token = null;
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((): void => reject());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the given login and password to the api.
|
|
||||||
* If the combination is valid, the login token is received and saved in the secure keychain.
|
|
||||||
* If not, the promise is rejected with the corresponding error code.
|
|
||||||
*
|
|
||||||
* @param email
|
|
||||||
* @param password
|
|
||||||
* @returns Promise<void>
|
|
||||||
*/
|
|
||||||
async connect(email: string, password: string): Promise<void> {
|
|
||||||
return new Promise(
|
|
||||||
(resolve: () => void, reject: (error: ApiRejectType) => void) => {
|
|
||||||
const data = {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
};
|
|
||||||
apiRequest<ApiDataLoginType>(AUTH_PATH, 'POST', data)
|
|
||||||
.then((response: ApiDataLoginType) => {
|
|
||||||
if (response.token != null) {
|
|
||||||
this.saveLogin(email, response.token)
|
|
||||||
.then(() => resolve())
|
|
||||||
.catch(() =>
|
|
||||||
reject({
|
|
||||||
status: REQUEST_STATUS.TOKEN_SAVE,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
reject({
|
|
||||||
status: REQUEST_STATUS.SERVER_ERROR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an authenticated request with the login token to the API
|
|
||||||
*
|
|
||||||
* @param path
|
|
||||||
* @param params
|
|
||||||
* @returns Promise<ApiGenericDataType>
|
|
||||||
*/
|
|
||||||
async authenticatedRequest<T>(
|
|
||||||
path: string,
|
|
||||||
params?: { [key: string]: any }
|
|
||||||
): Promise<T> {
|
|
||||||
return new Promise(
|
|
||||||
(
|
|
||||||
resolve: (response: T) => void,
|
|
||||||
reject: (error: ApiRejectType) => void
|
|
||||||
) => {
|
|
||||||
if (this.getToken() !== null) {
|
|
||||||
const data = {
|
|
||||||
...params,
|
|
||||||
token: this.getToken(),
|
|
||||||
};
|
|
||||||
apiRequest<T>(path, 'POST', data)
|
|
||||||
.then((response: T) => resolve(response))
|
|
||||||
.catch(reject);
|
|
||||||
} else {
|
|
||||||
reject({
|
|
||||||
status: REQUEST_STATUS.TOKEN_RETRIEVE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,7 +17,7 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Linking, StyleSheet, View } from 'react-native';
|
import { Linking, StyleSheet, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -25,20 +25,21 @@ import {
|
||||||
Card,
|
Card,
|
||||||
Chip,
|
Chip,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
withTheme,
|
useTheme,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||||
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
|
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
|
||||||
import type { ClubCategoryType, ClubType } from './ClubListScreen';
|
import type { ClubCategoryType, ClubType } from './ClubListScreen';
|
||||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
|
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
|
||||||
import RequestScreen from '../../../components/Screens/RequestScreen';
|
import RequestScreen from '../../../components/Screens/RequestScreen';
|
||||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
import { useFocusEffect } from '@react-navigation/core';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||||
|
|
||||||
type PropsType = {
|
type Props = {
|
||||||
navigation: StackNavigationProp<any>;
|
|
||||||
route: {
|
route: {
|
||||||
params?: {
|
params?: {
|
||||||
data?: ClubType;
|
data?: ClubType;
|
||||||
|
@ -46,7 +47,6 @@ type PropsType = {
|
||||||
clubId?: number;
|
clubId?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
theme: ReactNativePaper.Theme;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResponseType = ClubType;
|
type ResponseType = ClubType;
|
||||||
|
@ -89,33 +89,28 @@ const styles = StyleSheet.create({
|
||||||
* If called with data and categories navigation parameters, will use those to display the data.
|
* If called with data and categories navigation parameters, will use those to display the data.
|
||||||
* If called with clubId parameter, will fetch the information on the server
|
* If called with clubId parameter, will fetch the information on the server
|
||||||
*/
|
*/
|
||||||
class ClubDisplayScreen extends React.Component<PropsType> {
|
function ClubDisplayScreen(props: Props) {
|
||||||
displayData: ClubType | undefined;
|
const navigation = useNavigation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
categories: Array<ClubCategoryType> | null;
|
const [displayData, setDisplayData] = useState<ClubType | undefined>();
|
||||||
|
const [categories, setCategories] = useState<
|
||||||
|
Array<ClubCategoryType> | undefined
|
||||||
|
>();
|
||||||
|
const [clubId, setClubId] = useState<number | undefined>();
|
||||||
|
|
||||||
clubId: number;
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
shouldFetchData: boolean;
|
if (props.route.params?.data && props.route.params?.categories) {
|
||||||
|
setDisplayData(props.route.params.data);
|
||||||
constructor(props: PropsType) {
|
setCategories(props.route.params.categories);
|
||||||
super(props);
|
setClubId(props.route.params.data.id);
|
||||||
this.displayData = undefined;
|
} else {
|
||||||
this.categories = null;
|
const id = props.route.params?.clubId;
|
||||||
this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
|
setClubId(id ? id : 0);
|
||||||
this.shouldFetchData = true;
|
|
||||||
|
|
||||||
if (
|
|
||||||
props.route.params &&
|
|
||||||
props.route.params.data &&
|
|
||||||
props.route.params.categories
|
|
||||||
) {
|
|
||||||
this.displayData = props.route.params.data;
|
|
||||||
this.categories = props.route.params.categories;
|
|
||||||
this.clubId = props.route.params.data.id;
|
|
||||||
this.shouldFetchData = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [props.route.params])
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the name of the category with the given ID
|
* Gets the name of the category with the given ID
|
||||||
|
@ -123,17 +118,17 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
* @param id The category's ID
|
* @param id The category's ID
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
*/
|
*/
|
||||||
getCategoryName(id: number): string {
|
const getCategoryName = (id: number): string => {
|
||||||
let categoryName = '';
|
let categoryName = '';
|
||||||
if (this.categories !== null) {
|
if (categories) {
|
||||||
this.categories.forEach((item: ClubCategoryType) => {
|
categories.forEach((item: ClubCategoryType) => {
|
||||||
if (id === item.id) {
|
if (id === item.id) {
|
||||||
categoryName = item.name;
|
categoryName = item.name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return categoryName;
|
return categoryName;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the view for rendering categories
|
* Gets the view for rendering categories
|
||||||
|
@ -141,23 +136,23 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
* @param categories The categories to display (max 2)
|
* @param categories The categories to display (max 2)
|
||||||
* @returns {null|*}
|
* @returns {null|*}
|
||||||
*/
|
*/
|
||||||
getCategoriesRender(categories: Array<number | null>) {
|
const getCategoriesRender = (c: Array<number | null>) => {
|
||||||
if (this.categories == null) {
|
if (!categories) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const final: Array<React.ReactNode> = [];
|
const final: Array<React.ReactNode> = [];
|
||||||
categories.forEach((cat: number | null) => {
|
c.forEach((cat: number | null) => {
|
||||||
if (cat != null) {
|
if (cat != null) {
|
||||||
final.push(
|
final.push(
|
||||||
<Chip style={styles.category} key={cat}>
|
<Chip style={styles.category} key={cat}>
|
||||||
{this.getCategoryName(cat)}
|
{getCategoryName(cat)}
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return <View style={styles.categoryContainer}>{final}</View>;
|
return <View style={styles.categoryContainer}>{final}</View>;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the view for rendering club managers if any
|
* Gets the view for rendering club managers if any
|
||||||
|
@ -166,8 +161,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
* @param email The club contact email
|
* @param email The club contact email
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getManagersRender(managers: Array<string>, email: string | null) {
|
const getManagersRender = (managers: Array<string>, email: string | null) => {
|
||||||
const { props } = this;
|
|
||||||
const managersListView: Array<React.ReactNode> = [];
|
const managersListView: Array<React.ReactNode> = [];
|
||||||
managers.forEach((item: string) => {
|
managers.forEach((item: string) => {
|
||||||
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
|
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
|
||||||
|
@ -191,22 +185,18 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
<Avatar.Icon
|
<Avatar.Icon
|
||||||
size={iconProps.size}
|
size={iconProps.size}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
color={
|
color={hasManagers ? theme.colors.success : theme.colors.primary}
|
||||||
hasManagers
|
|
||||||
? props.theme.colors.success
|
|
||||||
: props.theme.colors.primary
|
|
||||||
}
|
|
||||||
icon="account-tie"
|
icon="account-tie"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{managersListView}
|
{managersListView}
|
||||||
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
|
{getEmailButton(email, hasManagers)}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the email button to contact the club, or the amicale if the club does not have any managers
|
* Gets the email button to contact the club, or the amicale if the club does not have any managers
|
||||||
|
@ -215,7 +205,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
* @param hasManagers True if the club has managers
|
* @param hasManagers True if the club has managers
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
static getEmailButton(email: string | null, hasManagers: boolean) {
|
const getEmailButton = (email: string | null, hasManagers: boolean) => {
|
||||||
const destinationEmail =
|
const destinationEmail =
|
||||||
email != null && hasManagers ? email : AMICALE_MAIL;
|
email != null && hasManagers ? email : AMICALE_MAIL;
|
||||||
const text =
|
const text =
|
||||||
|
@ -236,14 +226,14 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Actions>
|
</Card.Actions>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
getScreen = (data: ResponseType | undefined) => {
|
const getScreen = (data: ResponseType | undefined) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
this.updateHeaderTitle(data);
|
updateHeaderTitle(data);
|
||||||
return (
|
return (
|
||||||
<CollapsibleScrollView style={styles.scroll} hasTab>
|
<CollapsibleScrollView style={styles.scroll} hasTab>
|
||||||
{this.getCategoriesRender(data.category)}
|
{getCategoriesRender(data.category)}
|
||||||
{data.logo !== null ? (
|
{data.logo !== null ? (
|
||||||
<ImageGalleryButton
|
<ImageGalleryButton
|
||||||
images={[{ url: data.logo }]}
|
images={[{ url: data.logo }]}
|
||||||
|
@ -261,7 +251,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
) : (
|
) : (
|
||||||
<View />
|
<View />
|
||||||
)}
|
)}
|
||||||
{this.getManagersRender(data.responsibles, data.email)}
|
{getManagersRender(data.responsibles, data.email)}
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -273,27 +263,22 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
*
|
*
|
||||||
* @param data The club data
|
* @param data The club data
|
||||||
*/
|
*/
|
||||||
updateHeaderTitle(data: ClubType) {
|
const updateHeaderTitle = (data: ClubType) => {
|
||||||
const { props } = this;
|
navigation.setOptions({ title: data.name });
|
||||||
props.navigation.setOptions({ title: data.name });
|
};
|
||||||
}
|
|
||||||
|
const request = useAuthenticatedRequest<ClubType>('clubs/info', {
|
||||||
|
id: clubId,
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.shouldFetchData) {
|
|
||||||
return (
|
return (
|
||||||
<RequestScreen
|
<RequestScreen
|
||||||
request={() =>
|
request={request}
|
||||||
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
render={getScreen}
|
||||||
'clubs/info',
|
cache={displayData}
|
||||||
{ id: this.clubId }
|
onCacheUpdate={setDisplayData}
|
||||||
)
|
|
||||||
}
|
|
||||||
render={this.getScreen}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.getScreen(this.displayData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(ClubDisplayScreen);
|
export default ClubDisplayScreen;
|
||||||
|
|
|
@ -17,11 +17,10 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { Searchbar } from 'react-native-paper';
|
import { Searchbar } from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
|
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
|
||||||
import {
|
import {
|
||||||
isItemInCategoryFilter,
|
isItemInCategoryFilter,
|
||||||
|
@ -31,8 +30,9 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
|
||||||
import MaterialHeaderButtons, {
|
import MaterialHeaderButtons, {
|
||||||
Item,
|
Item,
|
||||||
} from '../../../components/Overrides/CustomHeaderButton';
|
} from '../../../components/Overrides/CustomHeaderButton';
|
||||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
|
||||||
import WebSectionList from '../../../components/Screens/WebSectionList';
|
import WebSectionList from '../../../components/Screens/WebSectionList';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||||
|
|
||||||
export type ClubCategoryType = {
|
export type ClubCategoryType = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -49,15 +49,6 @@ export type ClubType = {
|
||||||
responsibles: Array<string>;
|
responsibles: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsType = {
|
|
||||||
navigation: StackNavigationProp<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
currentlySelectedCategories: Array<number>;
|
|
||||||
currentSearchString: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResponseType = {
|
type ResponseType = {
|
||||||
categories: Array<ClubCategoryType>;
|
categories: Array<ClubCategoryType>;
|
||||||
clubs: Array<ClubType>;
|
clubs: Array<ClubType>;
|
||||||
|
@ -65,33 +56,52 @@ type ResponseType = {
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 96;
|
const LIST_ITEM_HEIGHT = 96;
|
||||||
|
|
||||||
class ClubListScreen extends React.Component<PropsType, StateType> {
|
function ClubListScreen() {
|
||||||
categories: Array<ClubCategoryType>;
|
const navigation = useNavigation();
|
||||||
|
const request = useAuthenticatedRequest<ResponseType>('clubs/list');
|
||||||
|
const [
|
||||||
|
currentlySelectedCategories,
|
||||||
|
setCurrentlySelectedCategories,
|
||||||
|
] = useState<Array<number>>([]);
|
||||||
|
const [currentSearchString, setCurrentSearchString] = useState('');
|
||||||
|
const categories = useRef<Array<ClubCategoryType>>([]);
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
useLayoutEffect(() => {
|
||||||
super(props);
|
const getSearchBar = () => {
|
||||||
this.categories = [];
|
return (
|
||||||
this.state = {
|
// @ts-ignore
|
||||||
currentlySelectedCategories: [],
|
<Searchbar
|
||||||
currentSearchString: '',
|
placeholder={i18n.t('screens.proximo.search')}
|
||||||
|
onChangeText={onSearchStringChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
const getHeaderButtons = () => {
|
||||||
|
return (
|
||||||
/**
|
<MaterialHeaderButtons>
|
||||||
* Creates the header content
|
<Item
|
||||||
*/
|
title="main"
|
||||||
componentDidMount() {
|
iconName="information"
|
||||||
const { props } = this;
|
onPress={() => navigation.navigate('club-about')}
|
||||||
props.navigation.setOptions({
|
/>
|
||||||
headerTitle: this.getSearchBar,
|
</MaterialHeaderButtons>
|
||||||
headerRight: this.getHeaderButtons,
|
);
|
||||||
|
};
|
||||||
|
navigation.setOptions({
|
||||||
|
headerTitle: getSearchBar,
|
||||||
|
headerRight: getHeaderButtons,
|
||||||
headerBackTitleVisible: false,
|
headerBackTitleVisible: false,
|
||||||
headerTitleContainerStyle:
|
headerTitleContainerStyle:
|
||||||
Platform.OS === 'ios'
|
Platform.OS === 'ios'
|
||||||
? { marginHorizontal: 0, width: '70%' }
|
? { marginHorizontal: 0, width: '70%' }
|
||||||
: { marginHorizontal: 0, right: 50, left: 50 },
|
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||||
});
|
});
|
||||||
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
const onSearchStringChange = (str: string) => {
|
||||||
|
updateFilteredData(str, null);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback used when clicking an article in the list.
|
* Callback used when clicking an article in the list.
|
||||||
|
@ -99,61 +109,20 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
*
|
*
|
||||||
* @param item The article pressed
|
* @param item The article pressed
|
||||||
*/
|
*/
|
||||||
onListItemPress(item: ClubType) {
|
const onListItemPress = (item: ClubType) => {
|
||||||
const { props } = this;
|
navigation.navigate('club-information', {
|
||||||
props.navigation.navigate('club-information', {
|
|
||||||
data: item,
|
data: item,
|
||||||
categories: this.categories,
|
categories: categories.current,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback used when the search changes
|
|
||||||
*
|
|
||||||
* @param str The new search string
|
|
||||||
*/
|
|
||||||
onSearchStringChange = (str: string) => {
|
|
||||||
this.updateFilteredData(str, null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const onChipSelect = (id: number) => {
|
||||||
* Gets the header search bar
|
updateFilteredData(null, id);
|
||||||
*
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
getSearchBar = () => {
|
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<Searchbar
|
|
||||||
placeholder={i18n.t('screens.proximo.search')}
|
|
||||||
onChangeText={this.onSearchStringChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onChipSelect = (id: number) => {
|
const createDataset = (data: ResponseType | undefined) => {
|
||||||
this.updateFilteredData(null, id);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the header button
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
getHeaderButtons = () => {
|
|
||||||
const onPress = () => {
|
|
||||||
const { props } = this;
|
|
||||||
props.navigation.navigate('club-about');
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<MaterialHeaderButtons>
|
|
||||||
<Item title="main" iconName="information" onPress={onPress} />
|
|
||||||
</MaterialHeaderButtons>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
createDataset = (data: ResponseType | undefined) => {
|
|
||||||
if (data) {
|
if (data) {
|
||||||
this.categories = data?.categories;
|
categories.current = data.categories;
|
||||||
return [{ title: '', data: data.clubs }];
|
return [{ title: '', data: data.clubs }];
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
@ -165,30 +134,23 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getListHeader(data: ResponseType | undefined) {
|
const getListHeader = (data: ResponseType | undefined) => {
|
||||||
const { state } = this;
|
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
<ClubListHeader
|
<ClubListHeader
|
||||||
categories={this.categories}
|
categories={categories.current}
|
||||||
selectedCategories={state.currentlySelectedCategories}
|
selectedCategories={currentlySelectedCategories}
|
||||||
onChipSelect={this.onChipSelect}
|
onChipSelect={onChipSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
const getCategoryOfId = (id: number): ClubCategoryType | null => {
|
||||||
* Gets the category object of the given ID
|
|
||||||
*
|
|
||||||
* @param id The ID of the category to find
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getCategoryOfId = (id: number): ClubCategoryType | null => {
|
|
||||||
let cat = null;
|
let cat = null;
|
||||||
this.categories.forEach((item: ClubCategoryType) => {
|
categories.current.forEach((item: ClubCategoryType) => {
|
||||||
if (id === item.id) {
|
if (id === item.id) {
|
||||||
cat = item;
|
cat = item;
|
||||||
}
|
}
|
||||||
|
@ -196,14 +158,14 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
return cat;
|
return cat;
|
||||||
};
|
};
|
||||||
|
|
||||||
getRenderItem = ({ item }: { item: ClubType }) => {
|
const getRenderItem = ({ item }: { item: ClubType }) => {
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
this.onListItemPress(item);
|
onListItemPress(item);
|
||||||
};
|
};
|
||||||
if (this.shouldRenderItem(item)) {
|
if (shouldRenderItem(item)) {
|
||||||
return (
|
return (
|
||||||
<ClubListItem
|
<ClubListItem
|
||||||
categoryTranslator={this.getCategoryOfId}
|
categoryTranslator={getCategoryOfId}
|
||||||
item={item}
|
item={item}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
height={LIST_ITEM_HEIGHT}
|
height={LIST_ITEM_HEIGHT}
|
||||||
|
@ -213,7 +175,7 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
keyExtractor = (item: ClubType): string => item.id.toString();
|
const keyExtractor = (item: ClubType): string => item.id.toString();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the search string and category filter, saving them to the State.
|
* Updates the search string and category filter, saving them to the State.
|
||||||
|
@ -224,10 +186,12 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
* @param filterStr The new filter string to use
|
* @param filterStr The new filter string to use
|
||||||
* @param categoryId The category to add/remove from the filter
|
* @param categoryId The category to add/remove from the filter
|
||||||
*/
|
*/
|
||||||
updateFilteredData(filterStr: string | null, categoryId: number | null) {
|
const updateFilteredData = (
|
||||||
const { state } = this;
|
filterStr: string | null,
|
||||||
const newCategoriesState = [...state.currentlySelectedCategories];
|
categoryId: number | null
|
||||||
let newStrState = state.currentSearchString;
|
) => {
|
||||||
|
const newCategoriesState = [...currentlySelectedCategories];
|
||||||
|
let newStrState = currentSearchString;
|
||||||
if (filterStr !== null) {
|
if (filterStr !== null) {
|
||||||
newStrState = filterStr;
|
newStrState = filterStr;
|
||||||
}
|
}
|
||||||
|
@ -240,12 +204,10 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filterStr !== null || categoryId !== null) {
|
if (filterStr !== null || categoryId !== null) {
|
||||||
this.setState({
|
setCurrentSearchString(newStrState);
|
||||||
currentSearchString: newStrState,
|
setCurrentlySelectedCategories(newCategoriesState);
|
||||||
currentlySelectedCategories: newCategoriesState,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given item should be rendered according to current name and category filters
|
* Checks if the given item should be rendered according to current name and category filters
|
||||||
|
@ -253,35 +215,28 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
* @param item The club to check
|
* @param item The club to check
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
shouldRenderItem(item: ClubType): boolean {
|
const shouldRenderItem = (item: ClubType): boolean => {
|
||||||
const { state } = this;
|
|
||||||
let shouldRender =
|
let shouldRender =
|
||||||
state.currentlySelectedCategories.length === 0 ||
|
currentlySelectedCategories.length === 0 ||
|
||||||
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
|
isItemInCategoryFilter(currentlySelectedCategories, item.category);
|
||||||
if (shouldRender) {
|
if (shouldRender) {
|
||||||
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
|
shouldRender = stringMatchQuery(item.name, currentSearchString);
|
||||||
}
|
}
|
||||||
return shouldRender;
|
return shouldRender;
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<WebSectionList
|
<WebSectionList
|
||||||
request={() =>
|
request={request}
|
||||||
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
createDataset={createDataset}
|
||||||
'clubs/list'
|
keyExtractor={keyExtractor}
|
||||||
)
|
renderItem={getRenderItem}
|
||||||
}
|
renderListHeaderComponent={getListHeader}
|
||||||
createDataset={this.createDataset}
|
|
||||||
keyExtractor={this.keyExtractor}
|
|
||||||
renderItem={this.getRenderItem}
|
|
||||||
renderListHeaderComponent={(data) => this.getListHeader(data)}
|
|
||||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
itemHeight={LIST_ITEM_HEIGHT}
|
itemHeight={LIST_ITEM_HEIGHT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default ClubListScreen;
|
export default ClubListScreen;
|
||||||
|
|
|
@ -17,26 +17,17 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
|
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
|
||||||
import MascotPopup from '../../../components/Mascot/MascotPopup';
|
import MascotPopup from '../../../components/Mascot/MascotPopup';
|
||||||
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
|
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
|
||||||
import GENERAL_STYLES from '../../../constants/Styles';
|
import GENERAL_STYLES from '../../../constants/Styles';
|
||||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
|
||||||
import { ApiRejectType } from '../../../utils/WebData';
|
import { ApiRejectType } from '../../../utils/WebData';
|
||||||
import WebSectionList from '../../../components/Screens/WebSectionList';
|
import WebSectionList from '../../../components/Screens/WebSectionList';
|
||||||
|
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||||
type PropsType = {
|
|
||||||
navigation: StackNavigationProp<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
mascotDialogVisible: boolean | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeviceType = {
|
export type DeviceType = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -67,69 +58,62 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
function EquipmentListScreen() {
|
||||||
userRents: null | Array<RentedDeviceType>;
|
const userRents = useRef<undefined | Array<RentedDeviceType>>();
|
||||||
|
const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
const requestAll = useAuthenticatedRequest<{ devices: Array<DeviceType> }>(
|
||||||
super(props);
|
'location/all'
|
||||||
this.userRents = null;
|
);
|
||||||
this.state = {
|
const requestOwn = useAuthenticatedRequest<{
|
||||||
mascotDialogVisible: undefined,
|
locations: Array<RentedDeviceType>;
|
||||||
};
|
}>('location/my');
|
||||||
}
|
|
||||||
|
|
||||||
getRenderItem = ({ item }: { item: DeviceType }) => {
|
const getRenderItem = ({ item }: { item: DeviceType }) => {
|
||||||
const { navigation } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<EquipmentListItem
|
<EquipmentListItem
|
||||||
navigation={navigation}
|
|
||||||
item={item}
|
item={item}
|
||||||
userDeviceRentDates={this.getUserDeviceRentDates(item)}
|
userDeviceRentDates={getUserDeviceRentDates(item)}
|
||||||
height={LIST_ITEM_HEIGHT}
|
height={LIST_ITEM_HEIGHT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getUserDeviceRentDates(item: DeviceType): [string, string] | null {
|
const getUserDeviceRentDates = (
|
||||||
|
item: DeviceType
|
||||||
|
): [string, string] | null => {
|
||||||
let dates = null;
|
let dates = null;
|
||||||
if (this.userRents != null) {
|
if (userRents.current) {
|
||||||
this.userRents.forEach((device: RentedDeviceType) => {
|
userRents.current.forEach((device: RentedDeviceType) => {
|
||||||
if (item.id === device.device_id) {
|
if (item.id === device.device_id) {
|
||||||
dates = [device.begin, device.end];
|
dates = [device.begin, device.end];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return dates;
|
return dates;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
const getListHeader = () => {
|
||||||
* Gets the list header, with explains this screen's purpose
|
|
||||||
*
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getListHeader() {
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
icon="help-circle"
|
icon="help-circle"
|
||||||
onPress={this.showMascotDialog}
|
onPress={showMascotDialog}
|
||||||
style={GENERAL_STYLES.centerHorizontal}
|
style={GENERAL_STYLES.centerHorizontal}
|
||||||
>
|
>
|
||||||
{i18n.t('screens.equipment.mascotDialog.title')}
|
{i18n.t('screens.equipment.mascotDialog.title')}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
keyExtractor = (item: DeviceType): string => item.id.toString();
|
const keyExtractor = (item: DeviceType): string => item.id.toString();
|
||||||
|
|
||||||
createDataset = (data: ResponseType | undefined) => {
|
const createDataset = (data: ResponseType | undefined) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const userRents = data.locations;
|
if (data.locations) {
|
||||||
|
userRents.current = data.locations;
|
||||||
if (userRents) {
|
|
||||||
this.userRents = userRents;
|
|
||||||
}
|
}
|
||||||
return [{ title: '', data: data.devices }];
|
return [{ title: '', data: data.devices }];
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,27 +121,19 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
showMascotDialog = () => {
|
const showMascotDialog = () => setMascotDialogVisible(true);
|
||||||
this.setState({ mascotDialogVisible: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
hideMascotDialog = () => {
|
const hideMascotDialog = () => setMascotDialogVisible(false);
|
||||||
this.setState({ mascotDialogVisible: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
request = () => {
|
const request = () => {
|
||||||
return new Promise(
|
return new Promise(
|
||||||
(
|
(
|
||||||
resolve: (data: ResponseType) => void,
|
resolve: (data: ResponseType) => void,
|
||||||
reject: (error: ApiRejectType) => void
|
reject: (error: ApiRejectType) => void
|
||||||
) => {
|
) => {
|
||||||
ConnectionManager.getInstance()
|
requestAll()
|
||||||
.authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
|
|
||||||
.then((devicesData) => {
|
.then((devicesData) => {
|
||||||
ConnectionManager.getInstance()
|
requestOwn()
|
||||||
.authenticatedRequest<{
|
|
||||||
locations: Array<RentedDeviceType>;
|
|
||||||
}>('location/my')
|
|
||||||
.then((rentsData) => {
|
.then((rentsData) => {
|
||||||
resolve({
|
resolve({
|
||||||
devices: devicesData.devices,
|
devices: devicesData.devices,
|
||||||
|
@ -175,19 +151,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const { state } = this;
|
|
||||||
return (
|
return (
|
||||||
<View style={GENERAL_STYLES.flex}>
|
<View style={GENERAL_STYLES.flex}>
|
||||||
<WebSectionList
|
<WebSectionList
|
||||||
request={this.request}
|
request={request}
|
||||||
createDataset={this.createDataset}
|
createDataset={createDataset}
|
||||||
keyExtractor={this.keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
renderItem={this.getRenderItem}
|
renderItem={getRenderItem}
|
||||||
renderListHeaderComponent={() => this.getListHeader()}
|
renderListHeaderComponent={getListHeader}
|
||||||
/>
|
/>
|
||||||
<MascotPopup
|
<MascotPopup
|
||||||
visible={state.mascotDialogVisible}
|
visible={mascotDialogVisible}
|
||||||
title={i18n.t('screens.equipment.mascotDialog.title')}
|
title={i18n.t('screens.equipment.mascotDialog.title')}
|
||||||
message={i18n.t('screens.equipment.mascotDialog.message')}
|
message={i18n.t('screens.equipment.mascotDialog.message')}
|
||||||
icon="vote"
|
icon="vote"
|
||||||
|
@ -195,7 +169,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||||
cancel: {
|
cancel: {
|
||||||
message: i18n.t('screens.equipment.mascotDialog.button'),
|
message: i18n.t('screens.equipment.mascotDialog.button'),
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
onPress: this.hideMascotDialog,
|
onPress: hideMascotDialog,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
emotion={MASCOT_STYLE.WINK}
|
emotion={MASCOT_STYLE.WINK}
|
||||||
|
@ -203,6 +177,5 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default EquipmentListScreen;
|
export default EquipmentListScreen;
|
||||||
|
|
|
@ -17,21 +17,20 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Caption,
|
Caption,
|
||||||
Card,
|
Card,
|
||||||
Headline,
|
Headline,
|
||||||
Subheading,
|
Subheading,
|
||||||
withTheme,
|
useTheme,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||||
import { BackHandler, StyleSheet, View } from 'react-native';
|
import { BackHandler, StyleSheet, View } from 'react-native';
|
||||||
import * as Animatable from 'react-native-animatable';
|
import * as Animatable from 'react-native-animatable';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { CalendarList, PeriodMarking } from 'react-native-calendars';
|
import { CalendarList, PeriodMarking } from 'react-native-calendars';
|
||||||
import type { DeviceType } from './EquipmentListScreen';
|
|
||||||
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 {
|
||||||
|
@ -42,34 +41,21 @@ import {
|
||||||
getValidRange,
|
getValidRange,
|
||||||
isEquipmentAvailable,
|
isEquipmentAvailable,
|
||||||
} from '../../../utils/EquipmentBooking';
|
} from '../../../utils/EquipmentBooking';
|
||||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
|
||||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
||||||
import GENERAL_STYLES from '../../../constants/Styles';
|
import GENERAL_STYLES from '../../../constants/Styles';
|
||||||
import { ApiRejectType } from '../../../utils/WebData';
|
import { ApiRejectType } from '../../../utils/WebData';
|
||||||
import { REQUEST_STATUS } from '../../../utils/Requests';
|
import { REQUEST_STATUS } from '../../../utils/Requests';
|
||||||
|
import { useFocusEffect } from '@react-navigation/core';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||||
|
|
||||||
type EquipmentRentScreenNavigationProp = StackScreenProps<
|
type Props = StackScreenProps<MainStackParamsList, 'equipment-rent'>;
|
||||||
MainStackParamsList,
|
|
||||||
'equipment-rent'
|
|
||||||
>;
|
|
||||||
|
|
||||||
type Props = EquipmentRentScreenNavigationProp & {
|
|
||||||
navigation: StackNavigationProp<any>;
|
|
||||||
theme: ReactNativePaper.Theme;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MarkedDatesObjectType = {
|
export type MarkedDatesObjectType = {
|
||||||
[key: string]: PeriodMarking;
|
[key: string]: PeriodMarking;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
dialogVisible: boolean;
|
|
||||||
errorDialogVisible: boolean;
|
|
||||||
markedDates: MarkedDatesObjectType;
|
|
||||||
currentError: ApiRejectType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
@ -114,98 +100,101 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
class EquipmentRentScreen extends React.Component<Props, StateType> {
|
function EquipmentRentScreen(props: Props) {
|
||||||
item: DeviceType | null;
|
const theme = useTheme();
|
||||||
|
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||||
|
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||||
|
status: REQUEST_STATUS.SUCCESS,
|
||||||
|
});
|
||||||
|
const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({});
|
||||||
|
const [dialogVisible, setDialogVisible] = useState(false);
|
||||||
|
|
||||||
bookedDates: Array<string>;
|
const item = props.route.params.item;
|
||||||
|
|
||||||
bookRef: { current: null | (Animatable.View & View) };
|
const bookedDates = useRef<Array<string>>([]);
|
||||||
|
const canBookEquipment = useRef(false);
|
||||||
|
|
||||||
canBookEquipment: boolean;
|
const bookRef = useRef<Animatable.View & View>(null);
|
||||||
|
|
||||||
lockedDates: {
|
let lockedDates: {
|
||||||
[key: string]: PeriodMarking;
|
[key: string]: PeriodMarking;
|
||||||
};
|
} = {};
|
||||||
|
|
||||||
constructor(props: Props) {
|
if (item) {
|
||||||
super(props);
|
|
||||||
this.item = null;
|
|
||||||
this.lockedDates = {};
|
|
||||||
this.state = {
|
|
||||||
dialogVisible: false,
|
|
||||||
errorDialogVisible: false,
|
|
||||||
markedDates: {},
|
|
||||||
currentError: { status: REQUEST_STATUS.SUCCESS },
|
|
||||||
};
|
|
||||||
this.resetSelection();
|
|
||||||
this.bookRef = React.createRef();
|
|
||||||
this.canBookEquipment = false;
|
|
||||||
this.bookedDates = [];
|
|
||||||
if (props.route.params != null) {
|
|
||||||
if (props.route.params.item != null) {
|
|
||||||
this.item = props.route.params.item;
|
|
||||||
} else {
|
|
||||||
this.item = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { item } = this;
|
|
||||||
if (item != null) {
|
|
||||||
this.lockedDates = {};
|
|
||||||
item.booked_at.forEach((date: { begin: string; end: string }) => {
|
item.booked_at.forEach((date: { begin: string; end: string }) => {
|
||||||
const range = getValidRange(
|
const range = getValidRange(
|
||||||
new Date(date.begin),
|
new Date(date.begin),
|
||||||
new Date(date.end),
|
new Date(date.end),
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
this.lockedDates = {
|
lockedDates = {
|
||||||
...this.lockedDates,
|
...lockedDates,
|
||||||
...generateMarkedDates(false, props.theme, range),
|
...generateMarkedDates(false, theme, range),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
useFocusEffect(
|
||||||
* Captures focus and blur events to hook on android back button
|
useCallback(() => {
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
const { navigation } = this.props;
|
|
||||||
navigation.addListener('focus', () => {
|
|
||||||
BackHandler.addEventListener(
|
BackHandler.addEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
this.onBackButtonPressAndroid
|
onBackButtonPressAndroid
|
||||||
);
|
);
|
||||||
});
|
return () => {
|
||||||
navigation.addListener('blur', () => {
|
|
||||||
BackHandler.removeEventListener(
|
BackHandler.removeEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
this.onBackButtonPressAndroid
|
onBackButtonPressAndroid
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides default android back button behaviour to deselect date if any is selected.
|
* Overrides default android back button behaviour to deselect date if any is selected.
|
||||||
*
|
*
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
onBackButtonPressAndroid = (): boolean => {
|
const onBackButtonPressAndroid = (): boolean => {
|
||||||
if (this.bookedDates.length > 0) {
|
if (bookedDates.current.length > 0) {
|
||||||
this.resetSelection();
|
resetSelection();
|
||||||
this.updateMarkedSelection();
|
updateMarkedSelection();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onDialogDismiss = () => {
|
const showDialog = () => setDialogVisible(true);
|
||||||
this.setState({ dialogVisible: false });
|
|
||||||
|
const onDialogDismiss = () => setDialogVisible(false);
|
||||||
|
|
||||||
|
const onErrorDialogDismiss = () =>
|
||||||
|
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||||
|
|
||||||
|
const getBookStartDate = (): Date | null => {
|
||||||
|
return bookedDates.current.length > 0
|
||||||
|
? new Date(bookedDates.current[0])
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
onErrorDialogDismiss = () => {
|
const getBookEndDate = (): Date | null => {
|
||||||
this.setState({ errorDialogVisible: false });
|
const { length } = bookedDates.current;
|
||||||
|
return length > 0 ? new Date(bookedDates.current[length - 1]) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const start = getBookStartDate();
|
||||||
|
const end = getBookEndDate();
|
||||||
|
const request = useAuthenticatedRequest(
|
||||||
|
'location/booking',
|
||||||
|
item && start && end
|
||||||
|
? {
|
||||||
|
device: item.id,
|
||||||
|
begin: getISODate(start),
|
||||||
|
end: getISODate(end),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the selected data to the server and waits for a response.
|
* Sends the selected data to the server and waits for a response.
|
||||||
* If the request is a success, navigate to the recap screen.
|
* If the request is a success, navigate to the recap screen.
|
||||||
|
@ -213,54 +202,37 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
*
|
*
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
onDialogAccept = (): Promise<void> => {
|
const onDialogAccept = (): Promise<void> => {
|
||||||
return new Promise((resolve: () => void) => {
|
return new Promise((resolve: () => void) => {
|
||||||
const { item, props } = this;
|
|
||||||
const start = this.getBookStartDate();
|
|
||||||
const end = this.getBookEndDate();
|
|
||||||
if (item != null && start != null && end != null) {
|
if (item != null && start != null && end != null) {
|
||||||
ConnectionManager.getInstance()
|
request()
|
||||||
.authenticatedRequest('location/booking', {
|
|
||||||
device: item.id,
|
|
||||||
begin: getISODate(start),
|
|
||||||
end: getISODate(end),
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.onDialogDismiss();
|
onDialogDismiss();
|
||||||
props.navigation.replace('equipment-confirm', {
|
navigation.replace('equipment-confirm', {
|
||||||
item: this.item,
|
item: item,
|
||||||
dates: [getISODate(start), getISODate(end)],
|
dates: [getISODate(start), getISODate(end)],
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error: ApiRejectType) => {
|
.catch((error: ApiRejectType) => {
|
||||||
this.onDialogDismiss();
|
onDialogDismiss();
|
||||||
this.showErrorDialog(error);
|
setCurrentError(error);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.onDialogDismiss();
|
onDialogDismiss();
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
getBookStartDate(): Date | null {
|
|
||||||
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBookEndDate(): Date | null {
|
|
||||||
const { length } = this.bookedDates;
|
|
||||||
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects a new date on the calendar.
|
* Selects a new date on the calendar.
|
||||||
* If both start and end dates are already selected, unselect all.
|
* If both start and end dates are already selected, unselect all.
|
||||||
*
|
*
|
||||||
* @param day The day selected
|
* @param day The day selected
|
||||||
*/
|
*/
|
||||||
selectNewDate = (day: {
|
const selectNewDate = (day: {
|
||||||
dateString: string;
|
dateString: string;
|
||||||
day: number;
|
day: number;
|
||||||
month: number;
|
month: number;
|
||||||
|
@ -268,84 +240,64 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
year: number;
|
year: number;
|
||||||
}) => {
|
}) => {
|
||||||
const selected = new Date(day.dateString);
|
const selected = new Date(day.dateString);
|
||||||
const start = this.getBookStartDate();
|
|
||||||
|
|
||||||
if (!this.lockedDates[day.dateString] != null) {
|
if (!lockedDates[day.dateString] != null) {
|
||||||
if (start === null) {
|
if (start === null) {
|
||||||
this.updateSelectionRange(selected, selected);
|
updateSelectionRange(selected, selected);
|
||||||
this.enableBooking();
|
enableBooking();
|
||||||
} else if (start.getTime() === selected.getTime()) {
|
} else if (start.getTime() === selected.getTime()) {
|
||||||
this.resetSelection();
|
resetSelection();
|
||||||
} else if (this.bookedDates.length === 1) {
|
} else if (bookedDates.current.length === 1) {
|
||||||
this.updateSelectionRange(start, selected);
|
updateSelectionRange(start, selected);
|
||||||
this.enableBooking();
|
enableBooking();
|
||||||
} else {
|
} else {
|
||||||
this.resetSelection();
|
resetSelection();
|
||||||
}
|
}
|
||||||
this.updateMarkedSelection();
|
updateMarkedSelection();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
showErrorDialog = (error: ApiRejectType) => {
|
const showBookButton = () => {
|
||||||
this.setState({
|
if (bookRef.current && bookRef.current.fadeInUp) {
|
||||||
errorDialogVisible: true,
|
bookRef.current.fadeInUp(500);
|
||||||
currentError: error,
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showDialog = () => {
|
const hideBookButton = () => {
|
||||||
this.setState({ dialogVisible: true });
|
if (bookRef.current && bookRef.current.fadeOutDown) {
|
||||||
|
bookRef.current.fadeOutDown(500);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const enableBooking = () => {
|
||||||
* Shows the book button by plying a fade animation
|
if (!canBookEquipment.current) {
|
||||||
*/
|
showBookButton();
|
||||||
showBookButton() {
|
canBookEquipment.current = true;
|
||||||
if (this.bookRef.current && this.bookRef.current.fadeInUp) {
|
|
||||||
this.bookRef.current.fadeInUp(500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
const resetSelection = () => {
|
||||||
* Hides the book button by plying a fade animation
|
if (canBookEquipment.current) {
|
||||||
*/
|
hideBookButton();
|
||||||
hideBookButton() {
|
|
||||||
if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
|
|
||||||
this.bookRef.current.fadeOutDown(500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
canBookEquipment.current = false;
|
||||||
|
bookedDates.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
enableBooking() {
|
const updateSelectionRange = (s: Date, e: Date) => {
|
||||||
if (!this.canBookEquipment) {
|
if (item) {
|
||||||
this.showBookButton();
|
bookedDates.current = getValidRange(s, e, item);
|
||||||
this.canBookEquipment = true;
|
} else {
|
||||||
}
|
bookedDates.current = [];
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
resetSelection() {
|
const updateMarkedSelection = () => {
|
||||||
if (this.canBookEquipment) {
|
setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
|
||||||
this.hideBookButton();
|
};
|
||||||
}
|
|
||||||
this.canBookEquipment = false;
|
|
||||||
this.bookedDates = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectionRange(start: Date, end: Date) {
|
|
||||||
this.bookedDates = getValidRange(start, end, this.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMarkedSelection() {
|
|
||||||
const { theme } = this.props;
|
|
||||||
this.setState({
|
|
||||||
markedDates: generateMarkedDates(true, theme, this.bookedDates),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { item, props, state } = this;
|
|
||||||
const start = this.getBookStartDate();
|
|
||||||
const end = this.getBookEndDate();
|
|
||||||
let subHeadingText;
|
let subHeadingText;
|
||||||
|
|
||||||
if (start == null) {
|
if (start == null) {
|
||||||
subHeadingText = i18n.t('screens.equipment.booking');
|
subHeadingText = i18n.t('screens.equipment.booking');
|
||||||
} else if (end != null && start.getTime() !== end.getTime()) {
|
} else if (end != null && start.getTime() !== end.getTime()) {
|
||||||
|
@ -358,7 +310,8 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
date: getRelativeDateString(start),
|
date: getRelativeDateString(start),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (item != null) {
|
|
||||||
|
if (item) {
|
||||||
const isAvailable = isEquipmentAvailable(item);
|
const isAvailable = isEquipmentAvailable(item);
|
||||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||||
return (
|
return (
|
||||||
|
@ -370,9 +323,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Headline style={styles.title}>{item.name}</Headline>
|
<Headline style={styles.title}>{item.name}</Headline>
|
||||||
<Caption style={styles.caption}>
|
<Caption style={styles.caption}>
|
||||||
(
|
({i18n.t('screens.equipment.bail', { cost: item.caution })})
|
||||||
{i18n.t('screens.equipment.bail', { cost: item.caution })}
|
|
||||||
)
|
|
||||||
</Caption>
|
</Caption>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -380,9 +331,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
<Button
|
<Button
|
||||||
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
||||||
color={
|
color={
|
||||||
isAvailable
|
isAvailable ? theme.colors.success : theme.colors.primary
|
||||||
? props.theme.colors.success
|
|
||||||
: props.theme.colors.primary
|
|
||||||
}
|
}
|
||||||
mode="text"
|
mode="text"
|
||||||
>
|
>
|
||||||
|
@ -390,9 +339,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
date: getRelativeDateString(firstAvailability),
|
date: getRelativeDateString(firstAvailability),
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
<Subheading style={styles.subtitle}>
|
<Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
|
||||||
{subHeadingText}
|
|
||||||
</Subheading>
|
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
<CalendarList
|
<CalendarList
|
||||||
|
@ -407,28 +354,28 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
// Enable paging on horizontal, default = false
|
// Enable paging on horizontal, default = false
|
||||||
pagingEnabled
|
pagingEnabled
|
||||||
// Handler which gets executed on day press. Default = undefined
|
// Handler which gets executed on day press. Default = undefined
|
||||||
onDayPress={this.selectNewDate}
|
onDayPress={selectNewDate}
|
||||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||||
firstDay={1}
|
firstDay={1}
|
||||||
// Hide month navigation arrows.
|
// Hide month navigation arrows.
|
||||||
hideArrows={false}
|
hideArrows={false}
|
||||||
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
||||||
markingType={'period'}
|
markingType={'period'}
|
||||||
markedDates={{ ...this.lockedDates, ...state.markedDates }}
|
markedDates={{ ...lockedDates, ...markedDates }}
|
||||||
theme={{
|
theme={{
|
||||||
'backgroundColor': props.theme.colors.agendaBackgroundColor,
|
'backgroundColor': theme.colors.agendaBackgroundColor,
|
||||||
'calendarBackground': props.theme.colors.background,
|
'calendarBackground': theme.colors.background,
|
||||||
'textSectionTitleColor': props.theme.colors.agendaDayTextColor,
|
'textSectionTitleColor': theme.colors.agendaDayTextColor,
|
||||||
'selectedDayBackgroundColor': props.theme.colors.primary,
|
'selectedDayBackgroundColor': theme.colors.primary,
|
||||||
'selectedDayTextColor': '#ffffff',
|
'selectedDayTextColor': '#ffffff',
|
||||||
'todayTextColor': props.theme.colors.text,
|
'todayTextColor': theme.colors.text,
|
||||||
'dayTextColor': props.theme.colors.text,
|
'dayTextColor': theme.colors.text,
|
||||||
'textDisabledColor': props.theme.colors.agendaDayTextColor,
|
'textDisabledColor': theme.colors.agendaDayTextColor,
|
||||||
'dotColor': props.theme.colors.primary,
|
'dotColor': theme.colors.primary,
|
||||||
'selectedDotColor': '#ffffff',
|
'selectedDotColor': '#ffffff',
|
||||||
'arrowColor': props.theme.colors.primary,
|
'arrowColor': theme.colors.primary,
|
||||||
'monthTextColor': props.theme.colors.text,
|
'monthTextColor': theme.colors.text,
|
||||||
'indicatorColor': props.theme.colors.primary,
|
'indicatorColor': theme.colors.primary,
|
||||||
'textDayFontFamily': 'monospace',
|
'textDayFontFamily': 'monospace',
|
||||||
'textMonthFontFamily': 'monospace',
|
'textMonthFontFamily': 'monospace',
|
||||||
'textDayHeaderFontFamily': 'monospace',
|
'textDayHeaderFontFamily': 'monospace',
|
||||||
|
@ -451,29 +398,29 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
/>
|
/>
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
<LoadingConfirmDialog
|
<LoadingConfirmDialog
|
||||||
visible={state.dialogVisible}
|
visible={dialogVisible}
|
||||||
onDismiss={this.onDialogDismiss}
|
onDismiss={onDialogDismiss}
|
||||||
onAccept={this.onDialogAccept}
|
onAccept={onDialogAccept}
|
||||||
title={i18n.t('screens.equipment.dialogTitle')}
|
title={i18n.t('screens.equipment.dialogTitle')}
|
||||||
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
||||||
message={i18n.t('screens.equipment.dialogMessage')}
|
message={i18n.t('screens.equipment.dialogMessage')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
visible={state.errorDialogVisible}
|
visible={currentError.status !== REQUEST_STATUS.SUCCESS}
|
||||||
onDismiss={this.onErrorDialogDismiss}
|
onDismiss={onErrorDialogDismiss}
|
||||||
status={state.currentError.status}
|
status={currentError.status}
|
||||||
code={state.currentError.code}
|
code={currentError.code}
|
||||||
/>
|
/>
|
||||||
<Animatable.View
|
<Animatable.View
|
||||||
ref={this.bookRef}
|
ref={bookRef}
|
||||||
useNativeDriver
|
useNativeDriver
|
||||||
style={styles.buttonContainer}
|
style={styles.buttonContainer}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon="bookmark-check"
|
icon="bookmark-check"
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={this.showDialog}
|
onPress={showDialog}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
{i18n.t('screens.equipment.bookButton')}
|
{i18n.t('screens.equipment.bookButton')}
|
||||||
|
@ -484,6 +431,5 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(EquipmentRentScreen);
|
export default EquipmentRentScreen;
|
||||||
|
|
|
@ -17,19 +17,11 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
|
import { KeyboardAvoidingView, View } from 'react-native';
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
HelperText,
|
|
||||||
TextInput,
|
|
||||||
withTheme,
|
|
||||||
} from 'react-native-paper';
|
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
|
||||||
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
||||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
|
@ -37,99 +29,32 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol
|
||||||
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
||||||
import GENERAL_STYLES from '../../constants/Styles';
|
import GENERAL_STYLES from '../../constants/Styles';
|
||||||
import Urls from '../../constants/Urls';
|
import Urls from '../../constants/Urls';
|
||||||
import { ApiRejectType } from '../../utils/WebData';
|
import { ApiRejectType, connectToAmicale } from '../../utils/WebData';
|
||||||
import { REQUEST_STATUS } from '../../utils/Requests';
|
import { REQUEST_STATUS } from '../../utils/Requests';
|
||||||
|
import LoginForm from '../../components/Amicale/Login/LoginForm';
|
||||||
|
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||||
|
import { TabRoutes } from '../../navigation/TabNavigator';
|
||||||
|
import { useShouldShowMascot } from '../../context/preferencesContext';
|
||||||
|
|
||||||
type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>;
|
type Props = StackScreenProps<MainStackParamsList, 'login'>;
|
||||||
|
|
||||||
type Props = LoginScreenNavigationProp & {
|
function LoginScreen(props: Props) {
|
||||||
navigation: StackNavigationProp<any>;
|
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||||
theme: ReactNativePaper.Theme;
|
const [loading, setLoading] = useState(false);
|
||||||
};
|
const [nextScreen, setNextScreen] = useState<string | undefined>(undefined);
|
||||||
|
const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
|
||||||
type StateType = {
|
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||||
email: string;
|
status: REQUEST_STATUS.SUCCESS,
|
||||||
password: string;
|
|
||||||
isEmailValidated: boolean;
|
|
||||||
isPasswordValidated: boolean;
|
|
||||||
loading: boolean;
|
|
||||||
dialogVisible: boolean;
|
|
||||||
dialogError: ApiRejectType;
|
|
||||||
mascotDialogVisible: boolean | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
|
||||||
|
|
||||||
const emailRegex = /^.+@.+\..+$/;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
marginTop: 'auto',
|
|
||||||
marginBottom: 'auto',
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
fontSize: 36,
|
|
||||||
marginBottom: 48,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
color: '#ffffff',
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
},
|
|
||||||
lockButton: {
|
|
||||||
marginRight: 'auto',
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
sendButton: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
const homeMascot = useShouldShowMascot(TabRoutes.Home);
|
||||||
|
|
||||||
class LoginScreen extends React.Component<Props, StateType> {
|
useFocusEffect(
|
||||||
onEmailChange: (value: string) => void;
|
useCallback(() => {
|
||||||
|
setNextScreen(props.route.params?.nextScreen);
|
||||||
|
}, [props.route.params])
|
||||||
|
);
|
||||||
|
|
||||||
onPasswordChange: (value: string) => void;
|
const onResetPasswordClick = () => {
|
||||||
|
|
||||||
passwordInputRef: {
|
|
||||||
// @ts-ignore
|
|
||||||
current: null | TextInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
nextScreen: string | null;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.nextScreen = null;
|
|
||||||
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: { status: REQUEST_STATUS.SUCCESS },
|
|
||||||
mascotDialogVisible: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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', {
|
navigation.navigate('website', {
|
||||||
host: Urls.websites.amicale,
|
host: Urls.websites.amicale,
|
||||||
path: Urls.amicale.resetPassword,
|
path: Urls.amicale.resetPassword,
|
||||||
|
@ -137,38 +62,6 @@ class LoginScreen extends React.Component<Props, StateType> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Called when the user clicks on login or finishes to type his password.
|
||||||
*
|
*
|
||||||
|
@ -176,253 +69,37 @@ class LoginScreen extends React.Component<Props, StateType> {
|
||||||
* then makes the login request and enters a loading state until the request finishes
|
* then makes the login request and enters a loading state until the request finishes
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onSubmit = () => {
|
const onSubmit = (email: string, password: string) => {
|
||||||
const { email, password } = this.state;
|
setLoading(true);
|
||||||
if (this.shouldEnableLogin()) {
|
connectToAmicale(email, password)
|
||||||
this.setState({ loading: true });
|
.then(handleSuccess)
|
||||||
ConnectionManager.getInstance()
|
.catch(setCurrentError)
|
||||||
.connect(email, password)
|
.finally(() => setLoading(false));
|
||||||
.then(this.handleSuccess)
|
|
||||||
.catch(this.showErrorDialog)
|
|
||||||
.finally(() => {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const hideMascotDialog = () => setMascotDialogVisible(true);
|
||||||
* Gets the form input
|
|
||||||
*
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getFormInput() {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const showMascotDialog = () => setMascotDialogVisible(false);
|
||||||
* Gets the card containing the input form
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getMainCard() {
|
|
||||||
const { props, state } = this;
|
|
||||||
return (
|
|
||||||
<View style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={i18n.t('screens.login.title')}
|
|
||||||
titleStyle={styles.text}
|
|
||||||
subtitle={i18n.t('screens.login.subtitle')}
|
|
||||||
subtitleStyle={styles.text}
|
|
||||||
left={({ size }) => (
|
|
||||||
<Image
|
|
||||||
source={ICON_AMICALE}
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Card.Content>
|
|
||||||
{this.getFormInput()}
|
|
||||||
<Card.Actions style={styles.buttonContainer}>
|
|
||||||
<Button
|
|
||||||
icon="lock-question"
|
|
||||||
mode="contained"
|
|
||||||
onPress={this.onResetPasswordClick}
|
|
||||||
color={props.theme.colors.warning}
|
|
||||||
style={styles.lockButton}
|
|
||||||
>
|
|
||||||
{i18n.t('screens.login.resetPassword')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon="send"
|
|
||||||
mode="contained"
|
|
||||||
disabled={!this.shouldEnableLogin()}
|
|
||||||
loading={state.loading}
|
|
||||||
onPress={this.onSubmit}
|
|
||||||
style={styles.sendButton}
|
|
||||||
>
|
|
||||||
{i18n.t('screens.login.title')}
|
|
||||||
</Button>
|
|
||||||
</Card.Actions>
|
|
||||||
<Card.Actions>
|
|
||||||
<Button
|
|
||||||
icon="help-circle"
|
|
||||||
mode="contained"
|
|
||||||
onPress={this.showMascotDialog}
|
|
||||||
style={GENERAL_STYLES.centerHorizontal}
|
|
||||||
>
|
|
||||||
{i18n.t('screens.login.mascotDialog.title')}
|
|
||||||
</Button>
|
|
||||||
</Card.Actions>
|
|
||||||
</Card.Content>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const hideErrorDialog = () =>
|
||||||
* The user has unfocused the input, his email is ready to be validated
|
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||||
*/
|
|
||||||
validateEmail = () => {
|
|
||||||
this.setState({ isEmailValidated: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The user has unfocused the input, his password is ready to be validated
|
|
||||||
*/
|
|
||||||
validatePassword = () => {
|
|
||||||
this.setState({ isPasswordValidated: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
hideMascotDialog = () => {
|
|
||||||
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: ApiRejectType) => {
|
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
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.
|
* 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.
|
* Saves in user preferences to not show the login banner again.
|
||||||
*/
|
*/
|
||||||
handleSuccess = () => {
|
const handleSuccess = () => {
|
||||||
const { navigation } = this.props;
|
|
||||||
// Do not show the home login banner again
|
// Do not show the home login banner again
|
||||||
// TODO
|
if (homeMascot.shouldShow) {
|
||||||
// AsyncStorageManager.set(
|
homeMascot.setShouldShow(false);
|
||||||
// AsyncStorageManager.PREFERENCES.homeShowMascot.key,
|
}
|
||||||
// false
|
if (!nextScreen) {
|
||||||
// );
|
|
||||||
if (this.nextScreen == null) {
|
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
} else {
|
} else {
|
||||||
navigation.replace(this.nextScreen);
|
navigation.replace(nextScreen);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
|
|
||||||
*/
|
|
||||||
handleNavigationParams() {
|
|
||||||
this.nextScreen = this.props.route.params?.nextScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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() {
|
|
||||||
const { mascotDialogVisible, dialogVisible, dialogError } = this.state;
|
|
||||||
return (
|
return (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
style={GENERAL_STYLES.flex}
|
style={GENERAL_STYLES.flex}
|
||||||
|
@ -438,7 +115,14 @@ class LoginScreen extends React.Component<Props, StateType> {
|
||||||
keyboardVerticalOffset={100}
|
keyboardVerticalOffset={100}
|
||||||
>
|
>
|
||||||
<CollapsibleScrollView headerColors={'transparent'}>
|
<CollapsibleScrollView headerColors={'transparent'}>
|
||||||
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
|
<View style={GENERAL_STYLES.flex}>
|
||||||
|
<LoginForm
|
||||||
|
loading={loading}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onResetPasswordPress={onResetPasswordClick}
|
||||||
|
onHelpPress={showMascotDialog}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<MascotPopup
|
<MascotPopup
|
||||||
visible={mascotDialogVisible}
|
visible={mascotDialogVisible}
|
||||||
title={i18n.t('screens.login.mascotDialog.title')}
|
title={i18n.t('screens.login.mascotDialog.title')}
|
||||||
|
@ -448,22 +132,21 @@ class LoginScreen extends React.Component<Props, StateType> {
|
||||||
cancel: {
|
cancel: {
|
||||||
message: i18n.t('screens.login.mascotDialog.button'),
|
message: i18n.t('screens.login.mascotDialog.button'),
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
onPress: this.hideMascotDialog,
|
onPress: hideMascotDialog,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
emotion={MASCOT_STYLE.NORMAL}
|
emotion={MASCOT_STYLE.NORMAL}
|
||||||
/>
|
/>
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
visible={dialogVisible}
|
visible={currentError.status !== REQUEST_STATUS.SUCCESS}
|
||||||
onDismiss={this.hideErrorDialog}
|
onDismiss={hideErrorDialog}
|
||||||
status={dialogError.status}
|
status={currentError.status}
|
||||||
code={dialogError.code}
|
code={currentError.code}
|
||||||
/>
|
/>
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(LoginScreen);
|
export default LoginScreen;
|
||||||
|
|
|
@ -17,52 +17,29 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useLayoutEffect, useState } from 'react';
|
||||||
import { FlatList, StyleSheet, View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
Paragraph,
|
|
||||||
withTheme,
|
|
||||||
} from 'react-native-paper';
|
|
||||||
import i18n from 'i18n-js';
|
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||||
import MaterialHeaderButtons, {
|
import MaterialHeaderButtons, {
|
||||||
Item,
|
Item,
|
||||||
} from '../../components/Overrides/CustomHeaderButton';
|
} from '../../components/Overrides/CustomHeaderButton';
|
||||||
import CardList from '../../components/Lists/CardList/CardList';
|
|
||||||
import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
|
||||||
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
import GENERAL_STYLES from '../../constants/Styles';
|
import GENERAL_STYLES from '../../constants/Styles';
|
||||||
import Urls from '../../constants/Urls';
|
|
||||||
import RequestScreen from '../../components/Screens/RequestScreen';
|
import RequestScreen from '../../components/Screens/RequestScreen';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
|
||||||
import {
|
import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
|
||||||
getAmicaleServices,
|
import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
|
||||||
ServiceItemType,
|
import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
|
||||||
SERVICES_KEY,
|
import { useNavigation } from '@react-navigation/core';
|
||||||
} from '../../utils/Services';
|
import { useAuthenticatedRequest } from '../../context/loginContext';
|
||||||
|
|
||||||
type PropsType = {
|
export type ProfileClubType = {
|
||||||
navigation: StackNavigationProp<any>;
|
|
||||||
theme: ReactNativePaper.Theme;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
dialogVisible: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClubType = {
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
is_manager: boolean;
|
is_manager: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProfileDataType = {
|
export type ProfileDataType = {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -71,87 +48,68 @@ type ProfileDataType = {
|
||||||
branch: string;
|
branch: string;
|
||||||
link: string;
|
link: string;
|
||||||
validity: boolean;
|
validity: boolean;
|
||||||
clubs: Array<ClubType>;
|
clubs: Array<ProfileClubType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function ProfileScreen() {
|
||||||
card: {
|
const navigation = useNavigation();
|
||||||
margin: 10,
|
const [dialogVisible, setDialogVisible] = useState(false);
|
||||||
},
|
const request = useAuthenticatedRequest<ProfileDataType>('user/profile');
|
||||||
icon: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
},
|
|
||||||
editButton: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
},
|
|
||||||
mascot: {
|
|
||||||
width: 60,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class ProfileScreen extends React.Component<PropsType, StateType> {
|
useLayoutEffect(() => {
|
||||||
data: ProfileDataType | undefined;
|
const getHeaderButton = () => (
|
||||||
|
|
||||||
flatListData: Array<{ id: string }>;
|
|
||||||
|
|
||||||
amicaleDataset: Array<ServiceItemType>;
|
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
|
||||||
super(props);
|
|
||||||
this.data = undefined;
|
|
||||||
this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
|
|
||||||
this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [
|
|
||||||
SERVICES_KEY.PROFILE,
|
|
||||||
]);
|
|
||||||
this.state = {
|
|
||||||
dialogVisible: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { navigation } = this.props;
|
|
||||||
navigation.setOptions({
|
|
||||||
headerRight: this.getHeaderButton,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the logout header button
|
|
||||||
*
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getHeaderButton = () => (
|
|
||||||
<MaterialHeaderButtons>
|
<MaterialHeaderButtons>
|
||||||
<Item
|
<Item
|
||||||
title="logout"
|
title={'logout'}
|
||||||
iconName="logout"
|
iconName={'logout'}
|
||||||
onPress={this.showDisconnectDialog}
|
onPress={showDisconnectDialog}
|
||||||
/>
|
/>
|
||||||
</MaterialHeaderButtons>
|
</MaterialHeaderButtons>
|
||||||
);
|
);
|
||||||
|
navigation.setOptions({
|
||||||
|
headerRight: getHeaderButton,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
/**
|
const getScreen = (data: ProfileDataType | undefined) => {
|
||||||
* Gets the main screen component with the fetched data
|
|
||||||
*
|
|
||||||
* @param data The data fetched from the server
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getScreen = (data: ProfileDataType | undefined) => {
|
|
||||||
const { dialogVisible } = this.state;
|
|
||||||
if (data) {
|
if (data) {
|
||||||
this.data = data;
|
const flatListData: Array<{
|
||||||
|
id: string;
|
||||||
|
render: () => React.ReactElement;
|
||||||
|
}> = [];
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
flatListData.push({
|
||||||
|
id: i.toString(),
|
||||||
|
render: () => <ProfileWelcomeCard firstname={data?.first_name} />,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
flatListData.push({
|
||||||
|
id: i.toString(),
|
||||||
|
render: () => <ProfilePersonalCard profile={data} />,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
flatListData.push({
|
||||||
|
id: i.toString(),
|
||||||
|
render: () => <ProfileClubCard clubs={data?.clubs} />,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
flatListData.push({
|
||||||
|
id: i.toString(),
|
||||||
|
render: () => <ProfileMembershipCard valid={data?.validity} />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<View style={GENERAL_STYLES.flex}>
|
<View style={GENERAL_STYLES.flex}>
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList renderItem={getRenderItem} data={flatListData} />
|
||||||
renderItem={this.getRenderItem}
|
|
||||||
data={this.flatListData}
|
|
||||||
/>
|
|
||||||
<LogoutDialog
|
<LogoutDialog
|
||||||
visible={dialogVisible}
|
visible={dialogVisible}
|
||||||
onDismiss={this.hideDisconnectDialog}
|
onDismiss={hideDisconnectDialog}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -160,346 +118,17 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getRenderItem = ({ item }: { item: { id: string } }) => {
|
const getRenderItem = ({
|
||||||
switch (item.id) {
|
item,
|
||||||
case '0':
|
}: {
|
||||||
return this.getWelcomeCard();
|
item: { id: string; render: () => React.ReactElement };
|
||||||
case '1':
|
}) => item.render();
|
||||||
return this.getPersonalCard();
|
|
||||||
case '2':
|
|
||||||
return this.getClubCard();
|
|
||||||
default:
|
|
||||||
return this.getMembershipCar();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const showDisconnectDialog = () => setDialogVisible(true);
|
||||||
* Gets the list of services available with the Amicale account
|
|
||||||
*
|
const hideDisconnectDialog = () => setDialogVisible(false);
|
||||||
* @returns {*}
|
|
||||||
*/
|
return <RequestScreen request={request} render={getScreen} />;
|
||||||
getServicesList() {
|
|
||||||
return <CardList dataset={this.amicaleDataset} isHorizontal />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default ProfileScreen;
|
||||||
* Gets a card welcoming the user to his account
|
|
||||||
*
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
getWelcomeCard() {
|
|
||||||
const { navigation } = this.props;
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={i18n.t('screens.profile.welcomeTitle', {
|
|
||||||
name: this.data?.first_name,
|
|
||||||
})}
|
|
||||||
left={() => (
|
|
||||||
<Mascot
|
|
||||||
style={styles.mascot}
|
|
||||||
emotion={MASCOT_STYLE.COOL}
|
|
||||||
animated
|
|
||||||
entryAnimation={{
|
|
||||||
animation: 'bounceIn',
|
|
||||||
duration: 1000,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
titleStyle={styles.title}
|
|
||||||
/>
|
|
||||||
<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 ? 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 | undefined, icon: string) {
|
|
||||||
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={(props) => (
|
|
||||||
<List.Icon
|
|
||||||
style={props.style}
|
|
||||||
icon={icon}
|
|
||||||
color={field != null ? props.color : theme.colors.textDisabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a card containing user personal information
|
|
||||||
*
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
getPersonalCard() {
|
|
||||||
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={(iconProps) => (
|
|
||||||
<Avatar.Icon
|
|
||||||
size={iconProps.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: Urls.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() {
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={i18n.t('screens.profile.clubs')}
|
|
||||||
subtitle={i18n.t('screens.profile.clubsSubtitle')}
|
|
||||||
left={(iconProps) => (
|
|
||||||
<Avatar.Icon
|
|
||||||
size={iconProps.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() {
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={i18n.t('screens.profile.membership')}
|
|
||||||
subtitle={i18n.t('screens.profile.membershipSubtitle')}
|
|
||||||
left={(iconProps) => (
|
|
||||||
<Avatar.Icon
|
|
||||||
size={iconProps.size}
|
|
||||||
icon="credit-card"
|
|
||||||
color={theme.colors.primary}
|
|
||||||
style={styles.icon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Card.Content>
|
|
||||||
<List.Section>
|
|
||||||
{this.getMembershipItem(this.data?.validity === true)}
|
|
||||||
</List.Section>
|
|
||||||
</Card.Content>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the item showing if the user has payed his membership
|
|
||||||
*
|
|
||||||
* @return {*}
|
|
||||||
*/
|
|
||||||
getMembershipItem(state: boolean) {
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
title={
|
|
||||||
state
|
|
||||||
? i18n.t('screens.profile.membershipPayed')
|
|
||||||
: i18n.t('screens.profile.membershipNotPayed')
|
|
||||||
}
|
|
||||||
left={(props) => (
|
|
||||||
<List.Icon
|
|
||||||
style={props.style}
|
|
||||||
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 }) => {
|
|
||||||
const { theme } = this.props;
|
|
||||||
const onPress = () => {
|
|
||||||
this.openClubDetailsScreen(item.id);
|
|
||||||
};
|
|
||||||
let description = i18n.t('screens.profile.isMember');
|
|
||||||
let icon = (props: {
|
|
||||||
color: string;
|
|
||||||
style: {
|
|
||||||
marginLeft: number;
|
|
||||||
marginRight: number;
|
|
||||||
marginVertical?: number;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<List.Icon color={props.color} style={props.style} icon="chevron-right" />
|
|
||||||
);
|
|
||||||
if (item.is_manager) {
|
|
||||||
description = i18n.t('screens.profile.isManager');
|
|
||||||
icon = (props) => (
|
|
||||||
<List.Icon
|
|
||||||
style={props.style}
|
|
||||||
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> | undefined) {
|
|
||||||
if (!list) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
return (
|
|
||||||
<RequestScreen<ProfileDataType>
|
|
||||||
request={() =>
|
|
||||||
ConnectionManager.getInstance().authenticatedRequest('user/profile')
|
|
||||||
}
|
|
||||||
render={this.getScreen}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTheme(ProfileScreen);
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
|
@ -30,10 +30,10 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
||||||
import GENERAL_STYLES from '../../constants/Styles';
|
import GENERAL_STYLES from '../../constants/Styles';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
|
||||||
import WebSectionList, {
|
import WebSectionList, {
|
||||||
SectionListDataType,
|
SectionListDataType,
|
||||||
} from '../../components/Screens/WebSectionList';
|
} from '../../components/Screens/WebSectionList';
|
||||||
|
import { useAuthenticatedRequest } from '../../context/loginContext';
|
||||||
|
|
||||||
export type VoteTeamType = {
|
export type VoteTeamType = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -65,6 +65,13 @@ type ResponseType = {
|
||||||
dates?: VoteDatesStringType;
|
dates?: VoteDatesStringType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FlatlistType = {
|
||||||
|
teams: Array<VoteTeamType>;
|
||||||
|
hasVoted: boolean;
|
||||||
|
datesString?: VoteDatesStringType;
|
||||||
|
dates?: VoteDatesObjectType;
|
||||||
|
};
|
||||||
|
|
||||||
// const FAKE_DATE = {
|
// const FAKE_DATE = {
|
||||||
// "date_begin": "2020-08-19 15:50",
|
// "date_begin": "2020-08-19 15:50",
|
||||||
// "date_end": "2020-08-19 15:50",
|
// "date_end": "2020-08-19 15:50",
|
||||||
|
@ -113,13 +120,6 @@ type ResponseType = {
|
||||||
// ],
|
// ],
|
||||||
// };
|
// };
|
||||||
|
|
||||||
type PropsType = {};
|
|
||||||
|
|
||||||
type StateType = {
|
|
||||||
hasVoted: boolean;
|
|
||||||
mascotDialogVisible: boolean | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
button: {
|
button: {
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
@ -131,38 +131,19 @@ const styles = StyleSheet.create({
|
||||||
/**
|
/**
|
||||||
* Screen displaying vote information and controls
|
* Screen displaying vote information and controls
|
||||||
*/
|
*/
|
||||||
export default class VoteScreen extends React.Component<PropsType, StateType> {
|
export default function VoteScreen() {
|
||||||
teams: Array<VoteTeamType>;
|
const [hasVoted, setHasVoted] = useState(false);
|
||||||
|
const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
|
||||||
|
|
||||||
hasVoted: boolean;
|
const datesRequest = useAuthenticatedRequest<VoteDatesStringType>(
|
||||||
|
'elections/dates'
|
||||||
datesString: undefined | VoteDatesStringType;
|
);
|
||||||
|
const teamsRequest = useAuthenticatedRequest<TeamResponseType>(
|
||||||
dates: undefined | VoteDatesObjectType;
|
'elections/teams'
|
||||||
|
);
|
||||||
today: Date;
|
|
||||||
|
|
||||||
mainFlatListData: SectionListDataType<{ key: string }>;
|
|
||||||
|
|
||||||
refreshData: () => void;
|
|
||||||
|
|
||||||
constructor(props: PropsType) {
|
|
||||||
super(props);
|
|
||||||
this.teams = [];
|
|
||||||
this.datesString = undefined;
|
|
||||||
this.dates = undefined;
|
|
||||||
this.state = {
|
|
||||||
hasVoted: false,
|
|
||||||
mascotDialogVisible: undefined,
|
|
||||||
};
|
|
||||||
this.hasVoted = false;
|
|
||||||
this.today = new Date();
|
|
||||||
this.refreshData = () => undefined;
|
|
||||||
this.mainFlatListData = [
|
|
||||||
{ title: '', data: [{ key: 'main' }, { key: 'info' }] },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const refresh = useRef<() => void | undefined>();
|
||||||
/**
|
/**
|
||||||
* Gets the string representation of the given date.
|
* Gets the string representation of the given date.
|
||||||
*
|
*
|
||||||
|
@ -173,22 +154,26 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
* @param dateString The string representation of the wanted date
|
* @param dateString The string representation of the wanted date
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getDateString(date: Date, dateString: string): string {
|
const getDateString = (date: Date, dateString: string) => {
|
||||||
if (this.today.getDate() === date.getDate()) {
|
if (today.getDate() === date.getDate()) {
|
||||||
const str = getTimeOnlyString(dateString);
|
const str = getTimeOnlyString(dateString);
|
||||||
return str != null ? str : '';
|
return str != null ? str : '';
|
||||||
}
|
}
|
||||||
return dateString;
|
return dateString;
|
||||||
}
|
};
|
||||||
|
|
||||||
getMainRenderItem = ({ item }: { item: { key: string } }) => {
|
const getMainRenderItem = ({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: { key: string; data?: FlatlistType };
|
||||||
|
}) => {
|
||||||
if (item.key === 'info') {
|
if (item.key === 'info') {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
icon="help-circle"
|
icon="help-circle"
|
||||||
onPress={this.showMascotDialog}
|
onPress={showMascotDialog}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
{i18n.t('screens.vote.mascotDialog.title')}
|
{i18n.t('screens.vote.mascotDialog.title')}
|
||||||
|
@ -196,10 +181,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.getContent();
|
if (item.data) {
|
||||||
|
return getContent(item.data);
|
||||||
|
} else {
|
||||||
|
return <View />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createDataset = (
|
const createDataset = (
|
||||||
data: ResponseType | undefined,
|
data: ResponseType | undefined,
|
||||||
_loading: boolean,
|
_loading: boolean,
|
||||||
_lastRefreshDate: Date | undefined,
|
_lastRefreshDate: Date | undefined,
|
||||||
|
@ -207,157 +196,158 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
) => {
|
) => {
|
||||||
// data[0] = FAKE_TEAMS2;
|
// data[0] = FAKE_TEAMS2;
|
||||||
// data[1] = FAKE_DATE;
|
// data[1] = FAKE_DATE;
|
||||||
this.refreshData = refreshData;
|
|
||||||
|
const mainFlatListData: SectionListDataType<{
|
||||||
|
key: string;
|
||||||
|
data?: FlatlistType;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
data: [{ key: 'main' }, { key: 'info' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
refresh.current = refreshData;
|
||||||
if (data) {
|
if (data) {
|
||||||
const { teams, dates } = data;
|
const { teams, dates } = data;
|
||||||
|
const flatlistData: FlatlistType = {
|
||||||
if (dates && dates.date_begin == null) {
|
teams: [],
|
||||||
this.datesString = undefined;
|
hasVoted: false,
|
||||||
} else {
|
};
|
||||||
this.datesString = dates;
|
if (dates && dates.date_begin != null) {
|
||||||
|
flatlistData.datesString = dates;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teams) {
|
if (teams) {
|
||||||
this.teams = teams.teams;
|
flatlistData.teams = teams.teams;
|
||||||
this.hasVoted = teams.has_voted;
|
flatlistData.hasVoted = teams.has_voted;
|
||||||
}
|
}
|
||||||
|
flatlistData.dates = generateDateObject(flatlistData.datesString);
|
||||||
this.generateDateObject();
|
|
||||||
}
|
}
|
||||||
return this.mainFlatListData;
|
return mainFlatListData;
|
||||||
};
|
};
|
||||||
|
|
||||||
getContent() {
|
const getContent = (data: FlatlistType) => {
|
||||||
const { state } = this;
|
const { dates } = data;
|
||||||
if (!this.isVoteStarted()) {
|
if (!isVoteStarted(dates)) {
|
||||||
return this.getTeaseVoteCard();
|
return getTeaseVoteCard(data);
|
||||||
}
|
}
|
||||||
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
|
if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
|
||||||
return this.getVoteCard();
|
return getVoteCard(data);
|
||||||
}
|
}
|
||||||
if (!this.isResultStarted()) {
|
if (!isResultStarted(dates)) {
|
||||||
return this.getWaitVoteCard();
|
return getWaitVoteCard(data);
|
||||||
}
|
}
|
||||||
if (this.isResultRunning()) {
|
if (isResultRunning(dates)) {
|
||||||
return this.getVoteResultCard();
|
return getVoteResultCard(data);
|
||||||
}
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
};
|
||||||
|
|
||||||
onVoteSuccess = (): void => this.setState({ hasVoted: true });
|
|
||||||
|
|
||||||
|
const onVoteSuccess = () => setHasVoted(true);
|
||||||
/**
|
/**
|
||||||
* The user has not voted yet, and the votes are open
|
* The user has not voted yet, and the votes are open
|
||||||
*/
|
*/
|
||||||
getVoteCard() {
|
const getVoteCard = (data: FlatlistType) => {
|
||||||
return (
|
return (
|
||||||
<VoteSelect
|
<VoteSelect
|
||||||
teams={this.teams}
|
teams={data.teams}
|
||||||
onVoteSuccess={this.onVoteSuccess}
|
onVoteSuccess={onVoteSuccess}
|
||||||
onVoteError={this.refreshData}
|
onVoteError={() => {
|
||||||
|
if (refresh.current) {
|
||||||
|
refresh.current();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Votes have ended, results can be displayed
|
* Votes have ended, results can be displayed
|
||||||
*/
|
*/
|
||||||
getVoteResultCard() {
|
const getVoteResultCard = (data: FlatlistType) => {
|
||||||
if (this.dates != null && this.datesString != null) {
|
if (data.dates != null && data.datesString != null) {
|
||||||
return (
|
return (
|
||||||
<VoteResults
|
<VoteResults
|
||||||
teams={this.teams}
|
teams={data.teams}
|
||||||
dateEnd={this.getDateString(
|
dateEnd={getDateString(
|
||||||
this.dates.date_result_end,
|
data.dates.date_result_end,
|
||||||
this.datesString.date_result_end
|
data.datesString.date_result_end
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vote will open shortly
|
* Vote will open shortly
|
||||||
*/
|
*/
|
||||||
getTeaseVoteCard() {
|
const getTeaseVoteCard = (data: FlatlistType) => {
|
||||||
if (this.dates != null && this.datesString != null) {
|
if (data.dates != null && data.datesString != null) {
|
||||||
return (
|
return (
|
||||||
<VoteTease
|
<VoteTease
|
||||||
startDate={this.getDateString(
|
startDate={getDateString(
|
||||||
this.dates.date_begin,
|
data.dates.date_begin,
|
||||||
this.datesString.date_begin
|
data.datesString.date_begin
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Votes have ended, or user has voted waiting for results
|
* Votes have ended, or user has voted waiting for results
|
||||||
*/
|
*/
|
||||||
getWaitVoteCard() {
|
const getWaitVoteCard = (data: FlatlistType) => {
|
||||||
const { state } = this;
|
|
||||||
let startDate = null;
|
let startDate = null;
|
||||||
if (
|
if (
|
||||||
this.dates != null &&
|
data.dates != null &&
|
||||||
this.datesString != null &&
|
data.datesString != null &&
|
||||||
this.dates.date_result_begin != null
|
data.dates.date_result_begin != null
|
||||||
) {
|
) {
|
||||||
startDate = this.getDateString(
|
startDate = getDateString(
|
||||||
this.dates.date_result_begin,
|
data.dates.date_result_begin,
|
||||||
this.datesString.date_result_begin
|
data.datesString.date_result_begin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VoteWait
|
<VoteWait
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
hasVoted={this.hasVoted || state.hasVoted}
|
hasVoted={data.hasVoted}
|
||||||
justVoted={state.hasVoted}
|
justVoted={hasVoted}
|
||||||
isVoteRunning={this.isVoteRunning()}
|
isVoteRunning={isVoteRunning()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
showMascotDialog = () => {
|
|
||||||
this.setState({ mascotDialogVisible: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hideMascotDialog = () => {
|
const showMascotDialog = () => setMascotDialogVisible(true);
|
||||||
this.setState({ mascotDialogVisible: false });
|
|
||||||
|
const hideMascotDialog = () => setMascotDialogVisible(false);
|
||||||
|
|
||||||
|
const isVoteStarted = (dates?: VoteDatesObjectType) => {
|
||||||
|
return dates != null && today > dates.date_begin;
|
||||||
};
|
};
|
||||||
|
|
||||||
isVoteStarted(): boolean {
|
const isResultRunning = (dates?: VoteDatesObjectType) => {
|
||||||
return this.dates != null && this.today > this.dates.date_begin;
|
|
||||||
}
|
|
||||||
|
|
||||||
isResultRunning(): boolean {
|
|
||||||
return (
|
return (
|
||||||
this.dates != null &&
|
dates != null &&
|
||||||
this.today > this.dates.date_result_begin &&
|
today > dates.date_result_begin &&
|
||||||
this.today < this.dates.date_result_end
|
today < dates.date_result_end
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
isResultStarted(): boolean {
|
const isResultStarted = (dates?: VoteDatesObjectType) => {
|
||||||
return this.dates != null && this.today > this.dates.date_result_begin;
|
return dates != null && today > dates.date_result_begin;
|
||||||
}
|
};
|
||||||
|
|
||||||
isVoteRunning(): boolean {
|
const isVoteRunning = (dates?: VoteDatesObjectType) => {
|
||||||
return (
|
return dates != null && today > dates.date_begin && today < dates.date_end;
|
||||||
this.dates != null &&
|
};
|
||||||
this.today > this.dates.date_begin &&
|
|
||||||
this.today < this.dates.date_end
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the objects containing string and Date representations of key vote dates
|
* Generates the objects containing string and Date representations of key vote dates
|
||||||
*/
|
*/
|
||||||
generateDateObject() {
|
const generateDateObject = (
|
||||||
const strings = this.datesString;
|
strings?: VoteDatesStringType
|
||||||
if (strings != null) {
|
): VoteDatesObjectType | undefined => {
|
||||||
|
if (strings) {
|
||||||
const dateBegin = stringToDate(strings.date_begin);
|
const dateBegin = stringToDate(strings.date_begin);
|
||||||
const dateEnd = stringToDate(strings.date_end);
|
const dateEnd = stringToDate(strings.date_end);
|
||||||
const dateResultBegin = stringToDate(strings.date_result_begin);
|
const dateResultBegin = stringToDate(strings.date_result_begin);
|
||||||
|
@ -368,27 +358,25 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
dateResultBegin != null &&
|
dateResultBegin != null &&
|
||||||
dateResultEnd != null
|
dateResultEnd != null
|
||||||
) {
|
) {
|
||||||
this.dates = {
|
return {
|
||||||
date_begin: dateBegin,
|
date_begin: dateBegin,
|
||||||
date_end: dateEnd,
|
date_end: dateEnd,
|
||||||
date_result_begin: dateResultBegin,
|
date_result_begin: dateResultBegin,
|
||||||
date_result_end: dateResultEnd,
|
date_result_end: dateResultEnd,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this.dates = undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.dates = undefined;
|
return undefined;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
request = () => {
|
const request = () => {
|
||||||
return new Promise((resolve: (data: ResponseType) => void) => {
|
return new Promise((resolve: (data: ResponseType) => void) => {
|
||||||
ConnectionManager.getInstance()
|
datesRequest()
|
||||||
.authenticatedRequest<VoteDatesStringType>('elections/dates')
|
|
||||||
.then((datesData) => {
|
.then((datesData) => {
|
||||||
ConnectionManager.getInstance()
|
teamsRequest()
|
||||||
.authenticatedRequest<TeamResponseType>('elections/teams')
|
|
||||||
.then((teamsData) => {
|
.then((teamsData) => {
|
||||||
resolve({
|
resolve({
|
||||||
dates: datesData,
|
dates: datesData,
|
||||||
|
@ -405,25 +393,16 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the authenticated screen.
|
|
||||||
*
|
|
||||||
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
|
|
||||||
*
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const { state } = this;
|
|
||||||
return (
|
return (
|
||||||
<View style={GENERAL_STYLES.flex}>
|
<View style={GENERAL_STYLES.flex}>
|
||||||
<WebSectionList
|
<WebSectionList
|
||||||
request={this.request}
|
request={request}
|
||||||
createDataset={this.createDataset}
|
createDataset={createDataset}
|
||||||
extraData={state.hasVoted.toString()}
|
extraData={hasVoted.toString()}
|
||||||
renderItem={this.getMainRenderItem}
|
renderItem={getMainRenderItem}
|
||||||
/>
|
/>
|
||||||
<MascotPopup
|
<MascotPopup
|
||||||
visible={state.mascotDialogVisible}
|
visible={mascotDialogVisible}
|
||||||
title={i18n.t('screens.vote.mascotDialog.title')}
|
title={i18n.t('screens.vote.mascotDialog.title')}
|
||||||
message={i18n.t('screens.vote.mascotDialog.message')}
|
message={i18n.t('screens.vote.mascotDialog.message')}
|
||||||
icon="vote"
|
icon="vote"
|
||||||
|
@ -431,7 +410,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
cancel: {
|
cancel: {
|
||||||
message: i18n.t('screens.vote.mascotDialog.button'),
|
message: i18n.t('screens.vote.mascotDialog.button'),
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
onPress: this.hideMascotDialog,
|
onPress: hideMascotDialog,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
emotion={MASCOT_STYLE.CUTE}
|
emotion={MASCOT_STYLE.CUTE}
|
||||||
|
@ -439,4 +418,3 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -46,7 +46,6 @@ import MaterialHeaderButtons, {
|
||||||
Item,
|
Item,
|
||||||
} from '../../components/Overrides/CustomHeaderButton';
|
} from '../../components/Overrides/CustomHeaderButton';
|
||||||
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
|
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
|
||||||
import ConnectionManager from '../../managers/ConnectionManager';
|
|
||||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
|
@ -59,6 +58,7 @@ import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
|
||||||
import { ServiceItemType } from '../../utils/Services';
|
import { ServiceItemType } from '../../utils/Services';
|
||||||
import { useCurrentDashboard } from '../../context/preferencesContext';
|
import { useCurrentDashboard } from '../../context/preferencesContext';
|
||||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||||
|
import { useLoginState } from '../../context/loginContext';
|
||||||
|
|
||||||
const FEED_ITEM_HEIGHT = 500;
|
const FEED_ITEM_HEIGHT = 500;
|
||||||
|
|
||||||
|
@ -146,9 +146,7 @@ function HomeScreen(props: Props) {
|
||||||
const [dialogVisible, setDialogVisible] = useState(false);
|
const [dialogVisible, setDialogVisible] = useState(false);
|
||||||
const fabRef = useRef<AnimatedFAB>(null);
|
const fabRef = useRef<AnimatedFAB>(null);
|
||||||
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(
|
const isLoggedIn = useLoginState();
|
||||||
ConnectionManager.getInstance().isLoggedIn()
|
|
||||||
);
|
|
||||||
const { currentDashboard } = useCurrentDashboard();
|
const { currentDashboard } = useCurrentDashboard();
|
||||||
|
|
||||||
let homeDashboard: FullDashboardType | null = null;
|
let homeDashboard: FullDashboardType | null = null;
|
||||||
|
@ -199,13 +197,8 @@ function HomeScreen(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ConnectionManager.getInstance().isLoggedIn() !== isLoggedIn) {
|
|
||||||
setIsLoggedIn(ConnectionManager.getInstance().isLoggedIn());
|
|
||||||
}
|
|
||||||
// handle link open when home is not focused or created
|
// handle link open when home is not focused or created
|
||||||
handleNavigationParams();
|
handleNavigationParams();
|
||||||
return () => {};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isLoggedIn])
|
}, [isLoggedIn])
|
||||||
);
|
);
|
||||||
|
|
|
@ -80,7 +80,8 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean {
|
||||||
export async function apiRequest<T>(
|
export async function apiRequest<T>(
|
||||||
path: string,
|
path: string,
|
||||||
method: string,
|
method: string,
|
||||||
params?: object
|
params?: object,
|
||||||
|
token?: string
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return new Promise(
|
return new Promise(
|
||||||
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
|
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
|
||||||
|
@ -88,7 +89,9 @@ export async function apiRequest<T>(
|
||||||
if (params != null) {
|
if (params != null) {
|
||||||
requestParams = { ...params };
|
requestParams = { ...params };
|
||||||
}
|
}
|
||||||
console.log(Urls.amicale.api + path);
|
if (token) {
|
||||||
|
requestParams = { ...requestParams, token: token };
|
||||||
|
}
|
||||||
|
|
||||||
fetch(Urls.amicale.api + path, {
|
fetch(Urls.amicale.api + path, {
|
||||||
method,
|
method,
|
||||||
|
@ -135,6 +138,33 @@ export async function apiRequest<T>(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function connectToAmicale(email: string, password: string) {
|
||||||
|
return new Promise(
|
||||||
|
(
|
||||||
|
resolve: (token: string) => void,
|
||||||
|
reject: (error: ApiRejectType) => void
|
||||||
|
) => {
|
||||||
|
const data = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
apiRequest<ApiDataLoginType>('password', 'POST', data)
|
||||||
|
.then((response: ApiDataLoginType) => {
|
||||||
|
if (response.token != null) {
|
||||||
|
resolve(response.token);
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
status: REQUEST_STATUS.SERVER_ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads data from the given url and returns it.
|
* Reads data from the given url and returns it.
|
||||||
*
|
*
|
||||||
|
|
46
src/utils/loginToken.ts
Normal file
46
src/utils/loginToken.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import * as Keychain from 'react-native-keychain';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to recover login token from the secure keychain
|
||||||
|
*
|
||||||
|
* @returns Promise<string | undefined>
|
||||||
|
*/
|
||||||
|
export async function retrieveLoginToken(): Promise<string | undefined> {
|
||||||
|
return new Promise((resolve: (token: string | undefined) => void) => {
|
||||||
|
Keychain.getGenericPassword()
|
||||||
|
.then((data: Keychain.UserCredentials | false) => {
|
||||||
|
if (data && data.password) {
|
||||||
|
resolve(data.password);
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => resolve(undefined));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Saves the login token in the secure keychain
|
||||||
|
*
|
||||||
|
* @param email
|
||||||
|
* @param token
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export async function saveLoginToken(
|
||||||
|
email: string,
|
||||||
|
token: string
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve: () => void, reject: () => void) => {
|
||||||
|
Keychain.setGenericPassword(email, token).then(resolve).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the login token from the keychain
|
||||||
|
*
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export async function deleteLoginToken(): Promise<void> {
|
||||||
|
return new Promise((resolve: () => void, reject: () => void) => {
|
||||||
|
Keychain.resetGenericPassword().then(resolve).catch(reject);
|
||||||
|
});
|
||||||
|
}
|
11
src/utils/logout.ts
Normal file
11
src/utils/logout.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useLogin } from '../context/loginContext';
|
||||||
|
|
||||||
|
export const useLogout = () => {
|
||||||
|
const { setLogin } = useLogin();
|
||||||
|
|
||||||
|
const onLogout = useCallback(() => {
|
||||||
|
setLogin(undefined);
|
||||||
|
}, [setLogin]);
|
||||||
|
return onLogout;
|
||||||
|
};
|
Loading…
Reference in a new issue