convert connection manager to context
This commit is contained in:
parent
44aa52b3aa
commit
541c002558
24 changed files with 1610 additions and 1965 deletions
25
App.tsx
25
App.tsx
|
@ -21,7 +21,6 @@ import React from 'react';
|
|||
import { LogBox, Platform } from 'react-native';
|
||||
import { setSafeBounceHeight } from 'react-navigation-collapsible';
|
||||
import SplashScreen from 'react-native-splash-screen';
|
||||
import ConnectionManager from './src/managers/ConnectionManager';
|
||||
import type { ParsedUrlDataType } from './src/utils/URLHandler';
|
||||
import URLHandler from './src/utils/URLHandler';
|
||||
import initLocales from './src/utils/Locales';
|
||||
|
@ -48,6 +47,8 @@ import {
|
|||
ProxiwashPreferencesProvider,
|
||||
} from './src/components/providers/PreferencesProvider';
|
||||
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
|
||||
// Crashes app when navigating away from webview on android 9+
|
||||
|
@ -67,6 +68,7 @@ type StateType = {
|
|||
proxiwash: ProxiwashPreferencesType;
|
||||
mascot: MascotPreferencesType;
|
||||
};
|
||||
loginToken?: string;
|
||||
};
|
||||
|
||||
export default class App extends React.Component<{}, StateType> {
|
||||
|
@ -88,6 +90,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
proxiwash: defaultProxiwashPreferences,
|
||||
mascot: defaultMascotPreferences,
|
||||
},
|
||||
loginToken: undefined,
|
||||
};
|
||||
initLocales();
|
||||
this.navigatorRef = React.createRef();
|
||||
|
@ -136,10 +139,11 @@ export default class App extends React.Component<{}, StateType> {
|
|||
| PlanexPreferencesType
|
||||
| ProxiwashPreferencesType
|
||||
| MascotPreferencesType
|
||||
| void
|
||||
| string
|
||||
| undefined
|
||||
>
|
||||
) => {
|
||||
const [general, planex, proxiwash, mascot] = values;
|
||||
const [general, planex, proxiwash, mascot, token] = values;
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
initialPreferences: {
|
||||
|
@ -148,6 +152,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
proxiwash: proxiwash as ProxiwashPreferencesType,
|
||||
mascot: mascot as MascotPreferencesType,
|
||||
},
|
||||
loginToken: token as string | undefined,
|
||||
});
|
||||
SplashScreen.hide();
|
||||
};
|
||||
|
@ -175,7 +180,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
Object.values(MascotPreferenceKeys),
|
||||
defaultMascotPreferences
|
||||
),
|
||||
ConnectionManager.getInstance().recoverLogin(),
|
||||
retrieveLoginToken(),
|
||||
])
|
||||
.then(this.onLoadFinished)
|
||||
.catch(this.onLoadFinished);
|
||||
|
@ -202,11 +207,13 @@ export default class App extends React.Component<{}, StateType> {
|
|||
<MascotPreferencesProvider
|
||||
initialPreferences={this.state.initialPreferences.mascot}
|
||||
>
|
||||
<MainApp
|
||||
ref={this.navigatorRef}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
/>
|
||||
<LoginProvider initialToken={this.state.loginToken}>
|
||||
<MainApp
|
||||
ref={this.navigatorRef}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
/>
|
||||
</LoginProvider>
|
||||
</MascotPreferencesProvider>
|
||||
</ProxiwashPreferencesProvider>
|
||||
</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 i18n from 'i18n-js';
|
||||
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useLogout } from '../../utils/logout';
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean;
|
||||
|
@ -29,19 +28,12 @@ type 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> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
ConnectionManager.getInstance()
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'main' }],
|
||||
});
|
||||
props.onDismiss();
|
||||
resolve();
|
||||
});
|
||||
onLogout();
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
||||
import { FlatList, StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
||||
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
||||
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
import { REQUEST_STATUS } from '../../../utils/Requests';
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
type PropsType = {
|
||||
type Props = {
|
||||
teams: Array<VoteTeamType>;
|
||||
onVoteSuccess: () => void;
|
||||
onVoteError: () => void;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
selectedTeam: string;
|
||||
voteDialogVisible: boolean;
|
||||
errorDialogVisible: boolean;
|
||||
currentError: ApiRejectType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
|
@ -50,118 +43,98 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export default class VoteSelect extends React.PureComponent<
|
||||
PropsType,
|
||||
StateType
|
||||
> {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedTeam: 'none',
|
||||
voteDialogVisible: false,
|
||||
errorDialogVisible: false,
|
||||
currentError: { status: REQUEST_STATUS.SUCCESS },
|
||||
};
|
||||
}
|
||||
function VoteSelect(props: Props) {
|
||||
const [selectedTeam, setSelectedTeam] = useState('none');
|
||||
const [voteDialogVisible, setVoteDialogVisible] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||
status: REQUEST_STATUS.SUCCESS,
|
||||
});
|
||||
const request = useAuthenticatedRequest('elections/vote', {
|
||||
team: parseInt(selectedTeam, 10),
|
||||
});
|
||||
|
||||
onVoteSelectionChange = (teamName: string): void =>
|
||||
this.setState({ selectedTeam: teamName });
|
||||
const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
|
||||
|
||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||
|
||||
voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||
const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||
<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) => {
|
||||
const { state } = this;
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest('elections/vote', {
|
||||
team: parseInt(state.selectedTeam, 10),
|
||||
})
|
||||
request()
|
||||
.then(() => {
|
||||
this.onVoteDialogDismiss();
|
||||
const { props } = this;
|
||||
onVoteDialogDismiss();
|
||||
props.onVoteSuccess();
|
||||
resolve();
|
||||
})
|
||||
.catch((error: ApiRejectType) => {
|
||||
this.onVoteDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
onVoteDialogDismiss();
|
||||
setCurrentError(error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showErrorDialog = (error: ApiRejectType): void =>
|
||||
this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
|
||||
onErrorDialogDismiss = () => {
|
||||
this.setState({ errorDialogVisible: false });
|
||||
const { props } = this;
|
||||
const onErrorDialogDismiss = () => {
|
||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||
props.onVoteError();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, props } = this;
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.vote.select.title')}
|
||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={this.onVoteSelectionChange}
|
||||
value={state.selectedTeam}
|
||||
>
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
extraData={state.selectedTeam}
|
||||
renderItem={this.voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
onPress={this.showVoteDialog}
|
||||
style={styles.button}
|
||||
disabled={state.selectedTeam === 'none'}
|
||||
>
|
||||
{i18n.t('screens.vote.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.voteDialogVisible}
|
||||
onDismiss={this.onVoteDialogDismiss}
|
||||
onAccept={this.onVoteDialogAccept}
|
||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.vote.select.title')}
|
||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||
)}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
status={state.currentError.status}
|
||||
code={state.currentError.code}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={setSelectedTeam}
|
||||
value={selectedTeam}
|
||||
>
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={voteKeyExtractor}
|
||||
extraData={selectedTeam}
|
||||
renderItem={voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon={'send'}
|
||||
mode={'contained'}
|
||||
onPress={showVoteDialog}
|
||||
style={styles.button}
|
||||
disabled={selectedTeam === 'none'}
|
||||
>
|
||||
{i18n.t('screens.vote.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={voteDialogVisible}
|
||||
onDismiss={onVoteDialogDismiss}
|
||||
onAccept={onVoteDialogAccept}
|
||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={currentError.status !== REQUEST_STATUS.SUCCESS}
|
||||
onDismiss={onErrorDialogDismiss}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default VoteSelect;
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import * as React from 'react';
|
||||
import { Avatar, List, useTheme } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import {
|
||||
getFirstEquipmentAvailability,
|
||||
|
@ -29,9 +28,9 @@ import {
|
|||
} from '../../../utils/EquipmentBooking';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
userDeviceRentDates: [string, string] | null;
|
||||
item: DeviceType;
|
||||
height: number;
|
||||
|
@ -48,7 +47,8 @@ const styles = StyleSheet.create({
|
|||
|
||||
function EquipmentListItem(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const { item, userDeviceRentDates, navigation, height } = props;
|
||||
const navigation = useNavigation();
|
||||
const { item, userDeviceRentDates, height } = props;
|
||||
const isRented = userDeviceRentDates != null;
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
|
|
|
@ -11,7 +11,7 @@ import i18n from 'i18n-js';
|
|||
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import { useLogout } from '../../utils/logout';
|
||||
|
||||
export type RequestScreenProps<T> = {
|
||||
request: () => Promise<T>;
|
||||
|
@ -44,6 +44,7 @@ type Props<T> = RequestScreenProps<T>;
|
|||
const MIN_REFRESH_TIME = 3 * 1000;
|
||||
|
||||
export default function RequestScreen<T>(props: Props<T>) {
|
||||
const onLogout = useLogout();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const route = useRoute();
|
||||
const refreshInterval = useRef<number>();
|
||||
|
@ -103,13 +104,10 @@ export default function RequestScreen<T>(props: Props<T>) {
|
|||
|
||||
useEffect(() => {
|
||||
if (isErrorCritical(code)) {
|
||||
ConnectionManager.getInstance()
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
||||
});
|
||||
onLogout();
|
||||
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
||||
}
|
||||
}, [code, navigation, route]);
|
||||
}, [code, navigation, route, onLogout]);
|
||||
|
||||
if (data === undefined && loading && props.showLoading !== false) {
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Linking, StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
|
@ -25,20 +25,21 @@ import {
|
|||
Card,
|
||||
Chip,
|
||||
Paragraph,
|
||||
withTheme,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
|
||||
import type { ClubCategoryType, ClubType } from './ClubListScreen';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
|
||||
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 = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
type Props = {
|
||||
route: {
|
||||
params?: {
|
||||
data?: ClubType;
|
||||
|
@ -46,7 +47,6 @@ type PropsType = {
|
|||
clubId?: number;
|
||||
};
|
||||
};
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
|
||||
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 clubId parameter, will fetch the information on the server
|
||||
*/
|
||||
class ClubDisplayScreen extends React.Component<PropsType> {
|
||||
displayData: ClubType | undefined;
|
||||
function ClubDisplayScreen(props: Props) {
|
||||
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;
|
||||
|
||||
shouldFetchData: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.displayData = undefined;
|
||||
this.categories = null;
|
||||
this.clubId = props.route.params?.clubId ? props.route.params.clubId : 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;
|
||||
}
|
||||
}
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (props.route.params?.data && props.route.params?.categories) {
|
||||
setDisplayData(props.route.params.data);
|
||||
setCategories(props.route.params.categories);
|
||||
setClubId(props.route.params.data.id);
|
||||
} else {
|
||||
const id = props.route.params?.clubId;
|
||||
setClubId(id ? id : 0);
|
||||
}
|
||||
}, [props.route.params])
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {string|*}
|
||||
*/
|
||||
getCategoryName(id: number): string {
|
||||
const getCategoryName = (id: number): string => {
|
||||
let categoryName = '';
|
||||
if (this.categories !== null) {
|
||||
this.categories.forEach((item: ClubCategoryType) => {
|
||||
if (categories) {
|
||||
categories.forEach((item: ClubCategoryType) => {
|
||||
if (id === item.id) {
|
||||
categoryName = item.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
return categoryName;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the view for rendering categories
|
||||
|
@ -141,23 +136,23 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
* @param categories The categories to display (max 2)
|
||||
* @returns {null|*}
|
||||
*/
|
||||
getCategoriesRender(categories: Array<number | null>) {
|
||||
if (this.categories == null) {
|
||||
const getCategoriesRender = (c: Array<number | null>) => {
|
||||
if (!categories) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const final: Array<React.ReactNode> = [];
|
||||
categories.forEach((cat: number | null) => {
|
||||
c.forEach((cat: number | null) => {
|
||||
if (cat != null) {
|
||||
final.push(
|
||||
<Chip style={styles.category} key={cat}>
|
||||
{this.getCategoryName(cat)}
|
||||
{getCategoryName(cat)}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
});
|
||||
return <View style={styles.categoryContainer}>{final}</View>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {*}
|
||||
*/
|
||||
getManagersRender(managers: Array<string>, email: string | null) {
|
||||
const { props } = this;
|
||||
const getManagersRender = (managers: Array<string>, email: string | null) => {
|
||||
const managersListView: Array<React.ReactNode> = [];
|
||||
managers.forEach((item: string) => {
|
||||
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
|
||||
|
@ -191,22 +185,18 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
style={styles.icon}
|
||||
color={
|
||||
hasManagers
|
||||
? props.theme.colors.success
|
||||
: props.theme.colors.primary
|
||||
}
|
||||
color={hasManagers ? theme.colors.success : theme.colors.primary}
|
||||
icon="account-tie"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
{managersListView}
|
||||
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
|
||||
{getEmailButton(email, hasManagers)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {*}
|
||||
*/
|
||||
static getEmailButton(email: string | null, hasManagers: boolean) {
|
||||
const getEmailButton = (email: string | null, hasManagers: boolean) => {
|
||||
const destinationEmail =
|
||||
email != null && hasManagers ? email : AMICALE_MAIL;
|
||||
const text =
|
||||
|
@ -236,14 +226,14 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
</Button>
|
||||
</Card.Actions>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getScreen = (data: ResponseType | undefined) => {
|
||||
const getScreen = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
this.updateHeaderTitle(data);
|
||||
updateHeaderTitle(data);
|
||||
return (
|
||||
<CollapsibleScrollView style={styles.scroll} hasTab>
|
||||
{this.getCategoriesRender(data.category)}
|
||||
{getCategoriesRender(data.category)}
|
||||
{data.logo !== null ? (
|
||||
<ImageGalleryButton
|
||||
images={[{ url: data.logo }]}
|
||||
|
@ -261,7 +251,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
) : (
|
||||
<View />
|
||||
)}
|
||||
{this.getManagersRender(data.responsibles, data.email)}
|
||||
{getManagersRender(data.responsibles, data.email)}
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
|
@ -273,27 +263,22 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
*
|
||||
* @param data The club data
|
||||
*/
|
||||
updateHeaderTitle(data: ClubType) {
|
||||
const { props } = this;
|
||||
props.navigation.setOptions({ title: data.name });
|
||||
}
|
||||
const updateHeaderTitle = (data: ClubType) => {
|
||||
navigation.setOptions({ title: data.name });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.shouldFetchData) {
|
||||
return (
|
||||
<RequestScreen
|
||||
request={() =>
|
||||
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
||||
'clubs/info',
|
||||
{ id: this.clubId }
|
||||
)
|
||||
}
|
||||
render={this.getScreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.getScreen(this.displayData);
|
||||
}
|
||||
const request = useAuthenticatedRequest<ClubType>('clubs/info', {
|
||||
id: clubId,
|
||||
});
|
||||
|
||||
return (
|
||||
<RequestScreen
|
||||
request={request}
|
||||
render={getScreen}
|
||||
cache={displayData}
|
||||
onCacheUpdate={setDisplayData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(ClubDisplayScreen);
|
||||
export default ClubDisplayScreen;
|
||||
|
|
|
@ -17,11 +17,10 @@
|
|||
* 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 { Searchbar } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
|
||||
import {
|
||||
isItemInCategoryFilter,
|
||||
|
@ -31,8 +30,9 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
|
|||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../../components/Overrides/CustomHeaderButton';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import WebSectionList from '../../../components/Screens/WebSectionList';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
export type ClubCategoryType = {
|
||||
id: number;
|
||||
|
@ -49,15 +49,6 @@ export type ClubType = {
|
|||
responsibles: Array<string>;
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
currentlySelectedCategories: Array<number>;
|
||||
currentSearchString: string;
|
||||
};
|
||||
|
||||
type ResponseType = {
|
||||
categories: Array<ClubCategoryType>;
|
||||
clubs: Array<ClubType>;
|
||||
|
@ -65,33 +56,52 @@ type ResponseType = {
|
|||
|
||||
const LIST_ITEM_HEIGHT = 96;
|
||||
|
||||
class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||
categories: Array<ClubCategoryType>;
|
||||
function ClubListScreen() {
|
||||
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) {
|
||||
super(props);
|
||||
this.categories = [];
|
||||
this.state = {
|
||||
currentlySelectedCategories: [],
|
||||
currentSearchString: '',
|
||||
useLayoutEffect(() => {
|
||||
const getSearchBar = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Searchbar
|
||||
placeholder={i18n.t('screens.proximo.search')}
|
||||
onChangeText={onSearchStringChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the header content
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { props } = this;
|
||||
props.navigation.setOptions({
|
||||
headerTitle: this.getSearchBar,
|
||||
headerRight: this.getHeaderButtons,
|
||||
const getHeaderButtons = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="main"
|
||||
iconName="information"
|
||||
onPress={() => navigation.navigate('club-about')}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
navigation.setOptions({
|
||||
headerTitle: getSearchBar,
|
||||
headerRight: getHeaderButtons,
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleContainerStyle:
|
||||
Platform.OS === 'ios'
|
||||
? { marginHorizontal: 0, width: '70%' }
|
||||
: { 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.
|
||||
|
@ -99,61 +109,20 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
*
|
||||
* @param item The article pressed
|
||||
*/
|
||||
onListItemPress(item: ClubType) {
|
||||
const { props } = this;
|
||||
props.navigation.navigate('club-information', {
|
||||
const onListItemPress = (item: ClubType) => {
|
||||
navigation.navigate('club-information', {
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the header search bar
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getSearchBar = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Searchbar
|
||||
placeholder={i18n.t('screens.proximo.search')}
|
||||
onChangeText={this.onSearchStringChange}
|
||||
/>
|
||||
);
|
||||
const onChipSelect = (id: number) => {
|
||||
updateFilteredData(null, id);
|
||||
};
|
||||
|
||||
onChipSelect = (id: number) => {
|
||||
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) => {
|
||||
const createDataset = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
this.categories = data?.categories;
|
||||
categories.current = data.categories;
|
||||
return [{ title: '', data: data.clubs }];
|
||||
} else {
|
||||
return [];
|
||||
|
@ -165,30 +134,23 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getListHeader(data: ResponseType | undefined) {
|
||||
const { state } = this;
|
||||
const getListHeader = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
return (
|
||||
<ClubListHeader
|
||||
categories={this.categories}
|
||||
selectedCategories={state.currentlySelectedCategories}
|
||||
onChipSelect={this.onChipSelect}
|
||||
categories={categories.current}
|
||||
selectedCategories={currentlySelectedCategories}
|
||||
onChipSelect={onChipSelect}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the category object of the given ID
|
||||
*
|
||||
* @param id The ID of the category to find
|
||||
* @returns {*}
|
||||
*/
|
||||
getCategoryOfId = (id: number): ClubCategoryType | null => {
|
||||
const getCategoryOfId = (id: number): ClubCategoryType | null => {
|
||||
let cat = null;
|
||||
this.categories.forEach((item: ClubCategoryType) => {
|
||||
categories.current.forEach((item: ClubCategoryType) => {
|
||||
if (id === item.id) {
|
||||
cat = item;
|
||||
}
|
||||
|
@ -196,14 +158,14 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
return cat;
|
||||
};
|
||||
|
||||
getRenderItem = ({ item }: { item: ClubType }) => {
|
||||
const getRenderItem = ({ item }: { item: ClubType }) => {
|
||||
const onPress = () => {
|
||||
this.onListItemPress(item);
|
||||
onListItemPress(item);
|
||||
};
|
||||
if (this.shouldRenderItem(item)) {
|
||||
if (shouldRenderItem(item)) {
|
||||
return (
|
||||
<ClubListItem
|
||||
categoryTranslator={this.getCategoryOfId}
|
||||
categoryTranslator={getCategoryOfId}
|
||||
item={item}
|
||||
onPress={onPress}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
|
@ -213,7 +175,7 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
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.
|
||||
|
@ -224,10 +186,12 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
* @param filterStr The new filter string to use
|
||||
* @param categoryId The category to add/remove from the filter
|
||||
*/
|
||||
updateFilteredData(filterStr: string | null, categoryId: number | null) {
|
||||
const { state } = this;
|
||||
const newCategoriesState = [...state.currentlySelectedCategories];
|
||||
let newStrState = state.currentSearchString;
|
||||
const updateFilteredData = (
|
||||
filterStr: string | null,
|
||||
categoryId: number | null
|
||||
) => {
|
||||
const newCategoriesState = [...currentlySelectedCategories];
|
||||
let newStrState = currentSearchString;
|
||||
if (filterStr !== null) {
|
||||
newStrState = filterStr;
|
||||
}
|
||||
|
@ -240,12 +204,10 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
}
|
||||
if (filterStr !== null || categoryId !== null) {
|
||||
this.setState({
|
||||
currentSearchString: newStrState,
|
||||
currentlySelectedCategories: newCategoriesState,
|
||||
});
|
||||
setCurrentSearchString(newStrState);
|
||||
setCurrentlySelectedCategories(newCategoriesState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldRenderItem(item: ClubType): boolean {
|
||||
const { state } = this;
|
||||
const shouldRenderItem = (item: ClubType): boolean => {
|
||||
let shouldRender =
|
||||
state.currentlySelectedCategories.length === 0 ||
|
||||
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
|
||||
currentlySelectedCategories.length === 0 ||
|
||||
isItemInCategoryFilter(currentlySelectedCategories, item.category);
|
||||
if (shouldRender) {
|
||||
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
|
||||
shouldRender = stringMatchQuery(item.name, currentSearchString);
|
||||
}
|
||||
return shouldRender;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WebSectionList
|
||||
request={() =>
|
||||
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
||||
'clubs/list'
|
||||
)
|
||||
}
|
||||
createDataset={this.createDataset}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getRenderItem}
|
||||
renderListHeaderComponent={(data) => this.getListHeader(data)}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
removeClippedSubviews={true}
|
||||
itemHeight={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WebSectionList
|
||||
request={request}
|
||||
createDataset={createDataset}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={getRenderItem}
|
||||
renderListHeaderComponent={getListHeader}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
removeClippedSubviews={true}
|
||||
itemHeight={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClubListScreen;
|
||||
|
|
|
@ -17,26 +17,17 @@
|
|||
* 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 { Button } from 'react-native-paper';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import i18n from 'i18n-js';
|
||||
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
|
||||
import MascotPopup from '../../../components/Mascot/MascotPopup';
|
||||
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
import WebSectionList from '../../../components/Screens/WebSectionList';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
mascotDialogVisible: boolean | undefined;
|
||||
};
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
export type DeviceType = {
|
||||
id: number;
|
||||
|
@ -67,69 +58,62 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||
userRents: null | Array<RentedDeviceType>;
|
||||
function EquipmentListScreen() {
|
||||
const userRents = useRef<undefined | Array<RentedDeviceType>>();
|
||||
const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.userRents = null;
|
||||
this.state = {
|
||||
mascotDialogVisible: undefined,
|
||||
};
|
||||
}
|
||||
const requestAll = useAuthenticatedRequest<{ devices: Array<DeviceType> }>(
|
||||
'location/all'
|
||||
);
|
||||
const requestOwn = useAuthenticatedRequest<{
|
||||
locations: Array<RentedDeviceType>;
|
||||
}>('location/my');
|
||||
|
||||
getRenderItem = ({ item }: { item: DeviceType }) => {
|
||||
const { navigation } = this.props;
|
||||
const getRenderItem = ({ item }: { item: DeviceType }) => {
|
||||
return (
|
||||
<EquipmentListItem
|
||||
navigation={navigation}
|
||||
item={item}
|
||||
userDeviceRentDates={this.getUserDeviceRentDates(item)}
|
||||
userDeviceRentDates={getUserDeviceRentDates(item)}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getUserDeviceRentDates(item: DeviceType): [string, string] | null {
|
||||
const getUserDeviceRentDates = (
|
||||
item: DeviceType
|
||||
): [string, string] | null => {
|
||||
let dates = null;
|
||||
if (this.userRents != null) {
|
||||
this.userRents.forEach((device: RentedDeviceType) => {
|
||||
if (userRents.current) {
|
||||
userRents.current.forEach((device: RentedDeviceType) => {
|
||||
if (item.id === device.device_id) {
|
||||
dates = [device.begin, device.end];
|
||||
}
|
||||
});
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list header, with explains this screen's purpose
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getListHeader() {
|
||||
const getListHeader = () => {
|
||||
return (
|
||||
<View style={styles.headerContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="help-circle"
|
||||
onPress={this.showMascotDialog}
|
||||
onPress={showMascotDialog}
|
||||
style={GENERAL_STYLES.centerHorizontal}
|
||||
>
|
||||
{i18n.t('screens.equipment.mascotDialog.title')}
|
||||
</Button>
|
||||
</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) {
|
||||
const userRents = data.locations;
|
||||
|
||||
if (userRents) {
|
||||
this.userRents = userRents;
|
||||
if (data.locations) {
|
||||
userRents.current = data.locations;
|
||||
}
|
||||
return [{ title: '', data: data.devices }];
|
||||
} else {
|
||||
|
@ -137,27 +121,19 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
};
|
||||
|
||||
showMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: true });
|
||||
};
|
||||
const showMascotDialog = () => setMascotDialogVisible(true);
|
||||
|
||||
hideMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: false });
|
||||
};
|
||||
const hideMascotDialog = () => setMascotDialogVisible(false);
|
||||
|
||||
request = () => {
|
||||
const request = () => {
|
||||
return new Promise(
|
||||
(
|
||||
resolve: (data: ResponseType) => void,
|
||||
reject: (error: ApiRejectType) => void
|
||||
) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
|
||||
requestAll()
|
||||
.then((devicesData) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<{
|
||||
locations: Array<RentedDeviceType>;
|
||||
}>('location/my')
|
||||
requestOwn()
|
||||
.then((rentsData) => {
|
||||
resolve({
|
||||
devices: devicesData.devices,
|
||||
|
@ -175,34 +151,31 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
|||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state } = this;
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebSectionList
|
||||
request={this.request}
|
||||
createDataset={this.createDataset}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getRenderItem}
|
||||
renderListHeaderComponent={() => this.getListHeader()}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={state.mascotDialogVisible}
|
||||
title={i18n.t('screens.equipment.mascotDialog.title')}
|
||||
message={i18n.t('screens.equipment.mascotDialog.message')}
|
||||
icon="vote"
|
||||
buttons={{
|
||||
cancel: {
|
||||
message: i18n.t('screens.equipment.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.WINK}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebSectionList
|
||||
request={request}
|
||||
createDataset={createDataset}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={getRenderItem}
|
||||
renderListHeaderComponent={getListHeader}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.equipment.mascotDialog.title')}
|
||||
message={i18n.t('screens.equipment.mascotDialog.message')}
|
||||
icon="vote"
|
||||
buttons={{
|
||||
cancel: {
|
||||
message: i18n.t('screens.equipment.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.WINK}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default EquipmentListScreen;
|
||||
|
|
|
@ -17,21 +17,20 @@
|
|||
* 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 {
|
||||
Button,
|
||||
Caption,
|
||||
Card,
|
||||
Headline,
|
||||
Subheading,
|
||||
withTheme,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||
import { BackHandler, StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import i18n from 'i18n-js';
|
||||
import { CalendarList, PeriodMarking } from 'react-native-calendars';
|
||||
import type { DeviceType } from './EquipmentListScreen';
|
||||
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
||||
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
||||
import {
|
||||
|
@ -42,34 +41,21 @@ import {
|
|||
getValidRange,
|
||||
isEquipmentAvailable,
|
||||
} from '../../../utils/EquipmentBooking';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
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<
|
||||
MainStackParamsList,
|
||||
'equipment-rent'
|
||||
>;
|
||||
|
||||
type Props = EquipmentRentScreenNavigationProp & {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
type Props = StackScreenProps<MainStackParamsList, 'equipment-rent'>;
|
||||
|
||||
export type MarkedDatesObjectType = {
|
||||
[key: string]: PeriodMarking;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
dialogVisible: boolean;
|
||||
errorDialogVisible: boolean;
|
||||
markedDates: MarkedDatesObjectType;
|
||||
currentError: ApiRejectType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
marginLeft: 'auto',
|
||||
|
@ -114,98 +100,101 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||
item: DeviceType | null;
|
||||
function EquipmentRentScreen(props: Props) {
|
||||
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;
|
||||
};
|
||||
} = {};
|
||||
|
||||
constructor(props: Props) {
|
||||
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 }) => {
|
||||
const range = getValidRange(
|
||||
new Date(date.begin),
|
||||
new Date(date.end),
|
||||
null
|
||||
);
|
||||
this.lockedDates = {
|
||||
...this.lockedDates,
|
||||
...generateMarkedDates(false, props.theme, range),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (item) {
|
||||
item.booked_at.forEach((date: { begin: string; end: string }) => {
|
||||
const range = getValidRange(
|
||||
new Date(date.begin),
|
||||
new Date(date.end),
|
||||
null
|
||||
);
|
||||
lockedDates = {
|
||||
...lockedDates,
|
||||
...generateMarkedDates(false, theme, range),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures focus and blur events to hook on android back button
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { navigation } = this.props;
|
||||
navigation.addListener('focus', () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
});
|
||||
navigation.addListener('blur', () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
);
|
||||
|
||||
/**
|
||||
* Overrides default android back button behaviour to deselect date if any is selected.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
if (this.bookedDates.length > 0) {
|
||||
this.resetSelection();
|
||||
this.updateMarkedSelection();
|
||||
const onBackButtonPressAndroid = (): boolean => {
|
||||
if (bookedDates.current.length > 0) {
|
||||
resetSelection();
|
||||
updateMarkedSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
onDialogDismiss = () => {
|
||||
this.setState({ dialogVisible: false });
|
||||
const showDialog = () => setDialogVisible(true);
|
||||
|
||||
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 = () => {
|
||||
this.setState({ errorDialogVisible: false });
|
||||
const getBookEndDate = (): Date | null => {
|
||||
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.
|
||||
* 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>}
|
||||
*/
|
||||
onDialogAccept = (): Promise<void> => {
|
||||
const onDialogAccept = (): Promise<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) {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest('location/booking', {
|
||||
device: item.id,
|
||||
begin: getISODate(start),
|
||||
end: getISODate(end),
|
||||
})
|
||||
request()
|
||||
.then(() => {
|
||||
this.onDialogDismiss();
|
||||
props.navigation.replace('equipment-confirm', {
|
||||
item: this.item,
|
||||
onDialogDismiss();
|
||||
navigation.replace('equipment-confirm', {
|
||||
item: item,
|
||||
dates: [getISODate(start), getISODate(end)],
|
||||
});
|
||||
resolve();
|
||||
})
|
||||
.catch((error: ApiRejectType) => {
|
||||
this.onDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
onDialogDismiss();
|
||||
setCurrentError(error);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
this.onDialogDismiss();
|
||||
onDialogDismiss();
|
||||
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.
|
||||
* If both start and end dates are already selected, unselect all.
|
||||
*
|
||||
* @param day The day selected
|
||||
*/
|
||||
selectNewDate = (day: {
|
||||
const selectNewDate = (day: {
|
||||
dateString: string;
|
||||
day: number;
|
||||
month: number;
|
||||
|
@ -268,222 +240,196 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
year: number;
|
||||
}) => {
|
||||
const selected = new Date(day.dateString);
|
||||
const start = this.getBookStartDate();
|
||||
|
||||
if (!this.lockedDates[day.dateString] != null) {
|
||||
if (!lockedDates[day.dateString] != null) {
|
||||
if (start === null) {
|
||||
this.updateSelectionRange(selected, selected);
|
||||
this.enableBooking();
|
||||
updateSelectionRange(selected, selected);
|
||||
enableBooking();
|
||||
} else if (start.getTime() === selected.getTime()) {
|
||||
this.resetSelection();
|
||||
} else if (this.bookedDates.length === 1) {
|
||||
this.updateSelectionRange(start, selected);
|
||||
this.enableBooking();
|
||||
resetSelection();
|
||||
} else if (bookedDates.current.length === 1) {
|
||||
updateSelectionRange(start, selected);
|
||||
enableBooking();
|
||||
} else {
|
||||
this.resetSelection();
|
||||
resetSelection();
|
||||
}
|
||||
this.updateMarkedSelection();
|
||||
updateMarkedSelection();
|
||||
}
|
||||
};
|
||||
|
||||
showErrorDialog = (error: ApiRejectType) => {
|
||||
this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
const showBookButton = () => {
|
||||
if (bookRef.current && bookRef.current.fadeInUp) {
|
||||
bookRef.current.fadeInUp(500);
|
||||
}
|
||||
};
|
||||
|
||||
showDialog = () => {
|
||||
this.setState({ dialogVisible: true });
|
||||
const hideBookButton = () => {
|
||||
if (bookRef.current && bookRef.current.fadeOutDown) {
|
||||
bookRef.current.fadeOutDown(500);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the book button by plying a fade animation
|
||||
*/
|
||||
showBookButton() {
|
||||
if (this.bookRef.current && this.bookRef.current.fadeInUp) {
|
||||
this.bookRef.current.fadeInUp(500);
|
||||
const enableBooking = () => {
|
||||
if (!canBookEquipment.current) {
|
||||
showBookButton();
|
||||
canBookEquipment.current = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the book button by plying a fade animation
|
||||
*/
|
||||
hideBookButton() {
|
||||
if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
|
||||
this.bookRef.current.fadeOutDown(500);
|
||||
const resetSelection = () => {
|
||||
if (canBookEquipment.current) {
|
||||
hideBookButton();
|
||||
}
|
||||
}
|
||||
canBookEquipment.current = false;
|
||||
bookedDates.current = [];
|
||||
};
|
||||
|
||||
enableBooking() {
|
||||
if (!this.canBookEquipment) {
|
||||
this.showBookButton();
|
||||
this.canBookEquipment = true;
|
||||
}
|
||||
}
|
||||
|
||||
resetSelection() {
|
||||
if (this.canBookEquipment) {
|
||||
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;
|
||||
if (start == null) {
|
||||
subHeadingText = i18n.t('screens.equipment.booking');
|
||||
} else if (end != null && start.getTime() !== end.getTime()) {
|
||||
subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
|
||||
begin: getRelativeDateString(start),
|
||||
end: getRelativeDateString(end),
|
||||
});
|
||||
const updateSelectionRange = (s: Date, e: Date) => {
|
||||
if (item) {
|
||||
bookedDates.current = getValidRange(s, e, item);
|
||||
} else {
|
||||
subHeadingText = i18n.t('screens.equipment.bookingDay', {
|
||||
date: getRelativeDateString(start),
|
||||
});
|
||||
bookedDates.current = [];
|
||||
}
|
||||
if (item != null) {
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CollapsibleScrollView>
|
||||
<Card style={styles.card}>
|
||||
<Card.Content>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Headline style={styles.title}>{item.name}</Headline>
|
||||
<Caption style={styles.caption}>
|
||||
(
|
||||
{i18n.t('screens.equipment.bail', { cost: item.caution })}
|
||||
)
|
||||
</Caption>
|
||||
</View>
|
||||
</View>
|
||||
};
|
||||
|
||||
<Button
|
||||
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
||||
color={
|
||||
isAvailable
|
||||
? props.theme.colors.success
|
||||
: props.theme.colors.primary
|
||||
}
|
||||
mode="text"
|
||||
>
|
||||
{i18n.t('screens.equipment.available', {
|
||||
date: getRelativeDateString(firstAvailability),
|
||||
})}
|
||||
</Button>
|
||||
<Subheading style={styles.subtitle}>
|
||||
{subHeadingText}
|
||||
</Subheading>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<CalendarList
|
||||
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
||||
minDate={new Date()}
|
||||
// Max amount of months allowed to scroll to the past. Default = 50
|
||||
pastScrollRange={0}
|
||||
// Max amount of months allowed to scroll to the future. Default = 50
|
||||
futureScrollRange={3}
|
||||
// Enable horizontal scrolling, default = false
|
||||
horizontal
|
||||
// Enable paging on horizontal, default = false
|
||||
pagingEnabled
|
||||
// Handler which gets executed on day press. Default = undefined
|
||||
onDayPress={this.selectNewDate}
|
||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||
firstDay={1}
|
||||
// Hide month navigation arrows.
|
||||
hideArrows={false}
|
||||
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
||||
markingType={'period'}
|
||||
markedDates={{ ...this.lockedDates, ...state.markedDates }}
|
||||
theme={{
|
||||
'backgroundColor': props.theme.colors.agendaBackgroundColor,
|
||||
'calendarBackground': props.theme.colors.background,
|
||||
'textSectionTitleColor': props.theme.colors.agendaDayTextColor,
|
||||
'selectedDayBackgroundColor': props.theme.colors.primary,
|
||||
'selectedDayTextColor': '#ffffff',
|
||||
'todayTextColor': props.theme.colors.text,
|
||||
'dayTextColor': props.theme.colors.text,
|
||||
'textDisabledColor': props.theme.colors.agendaDayTextColor,
|
||||
'dotColor': props.theme.colors.primary,
|
||||
'selectedDotColor': '#ffffff',
|
||||
'arrowColor': props.theme.colors.primary,
|
||||
'monthTextColor': props.theme.colors.text,
|
||||
'indicatorColor': props.theme.colors.primary,
|
||||
'textDayFontFamily': 'monospace',
|
||||
'textMonthFontFamily': 'monospace',
|
||||
'textDayHeaderFontFamily': 'monospace',
|
||||
'textDayFontWeight': '300',
|
||||
'textMonthFontWeight': 'bold',
|
||||
'textDayHeaderFontWeight': '300',
|
||||
'textDayFontSize': 16,
|
||||
'textMonthFontSize': 16,
|
||||
'textDayHeaderFontSize': 16,
|
||||
'stylesheet.day.period': {
|
||||
base: {
|
||||
overflow: 'hidden',
|
||||
height: 34,
|
||||
width: 34,
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={styles.calendar}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.dialogVisible}
|
||||
onDismiss={this.onDialogDismiss}
|
||||
onAccept={this.onDialogAccept}
|
||||
title={i18n.t('screens.equipment.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
||||
message={i18n.t('screens.equipment.dialogMessage')}
|
||||
/>
|
||||
const updateMarkedSelection = () => {
|
||||
setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
|
||||
};
|
||||
|
||||
<ErrorDialog
|
||||
visible={state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
status={state.currentError.status}
|
||||
code={state.currentError.code}
|
||||
/>
|
||||
<Animatable.View
|
||||
ref={this.bookRef}
|
||||
useNativeDriver
|
||||
style={styles.buttonContainer}
|
||||
>
|
||||
<Button
|
||||
icon="bookmark-check"
|
||||
mode="contained"
|
||||
onPress={this.showDialog}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t('screens.equipment.bookButton')}
|
||||
</Button>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
let subHeadingText;
|
||||
|
||||
if (start == null) {
|
||||
subHeadingText = i18n.t('screens.equipment.booking');
|
||||
} else if (end != null && start.getTime() !== end.getTime()) {
|
||||
subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
|
||||
begin: getRelativeDateString(start),
|
||||
end: getRelativeDateString(end),
|
||||
});
|
||||
} else {
|
||||
subHeadingText = i18n.t('screens.equipment.bookingDay', {
|
||||
date: getRelativeDateString(start),
|
||||
});
|
||||
}
|
||||
|
||||
if (item) {
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CollapsibleScrollView>
|
||||
<Card style={styles.card}>
|
||||
<Card.Content>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Headline style={styles.title}>{item.name}</Headline>
|
||||
<Caption style={styles.caption}>
|
||||
({i18n.t('screens.equipment.bail', { cost: item.caution })})
|
||||
</Caption>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
||||
color={
|
||||
isAvailable ? theme.colors.success : theme.colors.primary
|
||||
}
|
||||
mode="text"
|
||||
>
|
||||
{i18n.t('screens.equipment.available', {
|
||||
date: getRelativeDateString(firstAvailability),
|
||||
})}
|
||||
</Button>
|
||||
<Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<CalendarList
|
||||
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
||||
minDate={new Date()}
|
||||
// Max amount of months allowed to scroll to the past. Default = 50
|
||||
pastScrollRange={0}
|
||||
// Max amount of months allowed to scroll to the future. Default = 50
|
||||
futureScrollRange={3}
|
||||
// Enable horizontal scrolling, default = false
|
||||
horizontal
|
||||
// Enable paging on horizontal, default = false
|
||||
pagingEnabled
|
||||
// Handler which gets executed on day press. Default = undefined
|
||||
onDayPress={selectNewDate}
|
||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||
firstDay={1}
|
||||
// Hide month navigation arrows.
|
||||
hideArrows={false}
|
||||
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
||||
markingType={'period'}
|
||||
markedDates={{ ...lockedDates, ...markedDates }}
|
||||
theme={{
|
||||
'backgroundColor': theme.colors.agendaBackgroundColor,
|
||||
'calendarBackground': theme.colors.background,
|
||||
'textSectionTitleColor': theme.colors.agendaDayTextColor,
|
||||
'selectedDayBackgroundColor': theme.colors.primary,
|
||||
'selectedDayTextColor': '#ffffff',
|
||||
'todayTextColor': theme.colors.text,
|
||||
'dayTextColor': theme.colors.text,
|
||||
'textDisabledColor': theme.colors.agendaDayTextColor,
|
||||
'dotColor': theme.colors.primary,
|
||||
'selectedDotColor': '#ffffff',
|
||||
'arrowColor': theme.colors.primary,
|
||||
'monthTextColor': theme.colors.text,
|
||||
'indicatorColor': theme.colors.primary,
|
||||
'textDayFontFamily': 'monospace',
|
||||
'textMonthFontFamily': 'monospace',
|
||||
'textDayHeaderFontFamily': 'monospace',
|
||||
'textDayFontWeight': '300',
|
||||
'textMonthFontWeight': 'bold',
|
||||
'textDayHeaderFontWeight': '300',
|
||||
'textDayFontSize': 16,
|
||||
'textMonthFontSize': 16,
|
||||
'textDayHeaderFontSize': 16,
|
||||
'stylesheet.day.period': {
|
||||
base: {
|
||||
overflow: 'hidden',
|
||||
height: 34,
|
||||
width: 34,
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={styles.calendar}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
<LoadingConfirmDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={onDialogDismiss}
|
||||
onAccept={onDialogAccept}
|
||||
title={i18n.t('screens.equipment.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
||||
message={i18n.t('screens.equipment.dialogMessage')}
|
||||
/>
|
||||
|
||||
<ErrorDialog
|
||||
visible={currentError.status !== REQUEST_STATUS.SUCCESS}
|
||||
onDismiss={onErrorDialogDismiss}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
<Animatable.View
|
||||
ref={bookRef}
|
||||
useNativeDriver
|
||||
style={styles.buttonContainer}
|
||||
>
|
||||
<Button
|
||||
icon="bookmark-check"
|
||||
mode="contained"
|
||||
onPress={showDialog}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t('screens.equipment.bookButton')}
|
||||
</Button>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
HelperText,
|
||||
TextInput,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { KeyboardAvoidingView, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
|
@ -37,99 +29,32 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol
|
|||
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { ApiRejectType } from '../../utils/WebData';
|
||||
import { ApiRejectType, connectToAmicale } from '../../utils/WebData';
|
||||
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 & {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
function LoginScreen(props: Props) {
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nextScreen, setNextScreen] = useState<string | undefined>(undefined);
|
||||
const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||
status: REQUEST_STATUS.SUCCESS,
|
||||
});
|
||||
const homeMascot = useShouldShowMascot(TabRoutes.Home);
|
||||
|
||||
type StateType = {
|
||||
email: string;
|
||||
password: string;
|
||||
isEmailValidated: boolean;
|
||||
isPasswordValidated: boolean;
|
||||
loading: boolean;
|
||||
dialogVisible: boolean;
|
||||
dialogError: ApiRejectType;
|
||||
mascotDialogVisible: boolean | undefined;
|
||||
};
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setNextScreen(props.route.params?.nextScreen);
|
||||
}, [props.route.params])
|
||||
);
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
class LoginScreen extends React.Component<Props, StateType> {
|
||||
onEmailChange: (value: string) => void;
|
||||
|
||||
onPasswordChange: (value: string) => void;
|
||||
|
||||
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;
|
||||
const onResetPasswordClick = () => {
|
||||
navigation.navigate('website', {
|
||||
host: Urls.websites.amicale,
|
||||
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.
|
||||
*
|
||||
|
@ -176,294 +69,84 @@ class LoginScreen extends React.Component<Props, StateType> {
|
|||
* then makes the login request and enters a loading state until the request finishes
|
||||
*
|
||||
*/
|
||||
onSubmit = () => {
|
||||
const { email, password } = this.state;
|
||||
if (this.shouldEnableLogin()) {
|
||||
this.setState({ loading: true });
|
||||
ConnectionManager.getInstance()
|
||||
.connect(email, password)
|
||||
.then(this.handleSuccess)
|
||||
.catch(this.showErrorDialog)
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
const onSubmit = (email: string, password: string) => {
|
||||
setLoading(true);
|
||||
connectToAmicale(email, password)
|
||||
.then(handleSuccess)
|
||||
.catch(setCurrentError)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 hideMascotDialog = () => setMascotDialogVisible(true);
|
||||
|
||||
/**
|
||||
* 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 showMascotDialog = () => setMascotDialogVisible(false);
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his email is ready to be validated
|
||||
*/
|
||||
validateEmail = () => {
|
||||
this.setState({ isEmailValidated: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his password is ready to be validated
|
||||
*/
|
||||
validatePassword = () => {
|
||||
this.setState({ isPasswordValidated: true });
|
||||
};
|
||||
|
||||
hideMascotDialog = () => {
|
||||
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 });
|
||||
};
|
||||
const hideErrorDialog = () =>
|
||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||
|
||||
/**
|
||||
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
|
||||
* Saves in user preferences to not show the login banner again.
|
||||
*/
|
||||
handleSuccess = () => {
|
||||
const { navigation } = this.props;
|
||||
const handleSuccess = () => {
|
||||
// Do not show the home login banner again
|
||||
// TODO
|
||||
// AsyncStorageManager.set(
|
||||
// AsyncStorageManager.PREFERENCES.homeShowMascot.key,
|
||||
// false
|
||||
// );
|
||||
if (this.nextScreen == null) {
|
||||
if (homeMascot.shouldShow) {
|
||||
homeMascot.setShouldShow(false);
|
||||
}
|
||||
if (!nextScreen) {
|
||||
navigation.goBack();
|
||||
} 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 (
|
||||
<LinearGradient
|
||||
return (
|
||||
<LinearGradient
|
||||
style={GENERAL_STYLES.flex}
|
||||
colors={['#9e0d18', '#530209']}
|
||||
start={{ x: 0, y: 0.1 }}
|
||||
end={{ x: 0.1, y: 1 }}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={'height'}
|
||||
contentContainerStyle={GENERAL_STYLES.flex}
|
||||
style={GENERAL_STYLES.flex}
|
||||
colors={['#9e0d18', '#530209']}
|
||||
start={{ x: 0, y: 0.1 }}
|
||||
end={{ x: 0.1, y: 1 }}
|
||||
enabled={true}
|
||||
keyboardVerticalOffset={100}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={'height'}
|
||||
contentContainerStyle={GENERAL_STYLES.flex}
|
||||
style={GENERAL_STYLES.flex}
|
||||
enabled={true}
|
||||
keyboardVerticalOffset={100}
|
||||
>
|
||||
<CollapsibleScrollView headerColors={'transparent'}>
|
||||
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.login.mascotDialog.title')}
|
||||
message={i18n.t('screens.login.mascotDialog.message')}
|
||||
icon={'help'}
|
||||
buttons={{
|
||||
cancel: {
|
||||
message: i18n.t('screens.login.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
<CollapsibleScrollView headerColors={'transparent'}>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<LoginForm
|
||||
loading={loading}
|
||||
onSubmit={onSubmit}
|
||||
onResetPasswordPress={onResetPasswordClick}
|
||||
onHelpPress={showMascotDialog}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={this.hideErrorDialog}
|
||||
status={dialogError.status}
|
||||
code={dialogError.code}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.login.mascotDialog.title')}
|
||||
message={i18n.t('screens.login.mascotDialog.message')}
|
||||
icon={'help'}
|
||||
buttons={{
|
||||
cancel: {
|
||||
message: i18n.t('screens.login.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={currentError.status !== REQUEST_STATUS.SUCCESS}
|
||||
onDismiss={hideErrorDialog}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(LoginScreen);
|
||||
export default LoginScreen;
|
||||
|
|
|
@ -17,52 +17,29 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { FlatList, StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
List,
|
||||
Paragraph,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import React, { useLayoutEffect, useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} 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 GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import RequestScreen from '../../components/Screens/RequestScreen';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import {
|
||||
getAmicaleServices,
|
||||
ServiceItemType,
|
||||
SERVICES_KEY,
|
||||
} from '../../utils/Services';
|
||||
import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
|
||||
import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
|
||||
import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
|
||||
import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { useAuthenticatedRequest } from '../../context/loginContext';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
dialogVisible: boolean;
|
||||
};
|
||||
|
||||
type ClubType = {
|
||||
export type ProfileClubType = {
|
||||
id: number;
|
||||
name: string;
|
||||
is_manager: boolean;
|
||||
};
|
||||
|
||||
type ProfileDataType = {
|
||||
export type ProfileDataType = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
|
@ -71,87 +48,68 @@ type ProfileDataType = {
|
|||
branch: string;
|
||||
link: string;
|
||||
validity: boolean;
|
||||
clubs: Array<ClubType>;
|
||||
clubs: Array<ProfileClubType>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mascot: {
|
||||
width: 60,
|
||||
},
|
||||
title: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
function ProfileScreen() {
|
||||
const navigation = useNavigation();
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const request = useAuthenticatedRequest<ProfileDataType>('user/profile');
|
||||
|
||||
class ProfileScreen extends React.Component<PropsType, StateType> {
|
||||
data: ProfileDataType | undefined;
|
||||
|
||||
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;
|
||||
useLayoutEffect(() => {
|
||||
const getHeaderButton = () => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title={'logout'}
|
||||
iconName={'logout'}
|
||||
onPress={showDisconnectDialog}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
navigation.setOptions({
|
||||
headerRight: this.getHeaderButton,
|
||||
headerRight: getHeaderButton,
|
||||
});
|
||||
}
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Gets the logout header button
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getHeaderButton = () => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="logout"
|
||||
iconName="logout"
|
||||
onPress={this.showDisconnectDialog}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the main screen component with the fetched data
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @returns {*}
|
||||
*/
|
||||
getScreen = (data: ProfileDataType | undefined) => {
|
||||
const { dialogVisible } = this.state;
|
||||
const getScreen = (data: ProfileDataType | undefined) => {
|
||||
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 (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CollapsibleFlatList
|
||||
renderItem={this.getRenderItem}
|
||||
data={this.flatListData}
|
||||
/>
|
||||
<CollapsibleFlatList renderItem={getRenderItem} data={flatListData} />
|
||||
<LogoutDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={this.hideDisconnectDialog}
|
||||
onDismiss={hideDisconnectDialog}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
@ -160,346 +118,17 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
};
|
||||
|
||||
getRenderItem = ({ item }: { item: { id: string } }) => {
|
||||
switch (item.id) {
|
||||
case '0':
|
||||
return this.getWelcomeCard();
|
||||
case '1':
|
||||
return this.getPersonalCard();
|
||||
case '2':
|
||||
return this.getClubCard();
|
||||
default:
|
||||
return this.getMembershipCar();
|
||||
}
|
||||
};
|
||||
const getRenderItem = ({
|
||||
item,
|
||||
}: {
|
||||
item: { id: string; render: () => React.ReactElement };
|
||||
}) => item.render();
|
||||
|
||||
/**
|
||||
* Gets the list of services available with the Amicale account
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getServicesList() {
|
||||
return <CardList dataset={this.amicaleDataset} isHorizontal />;
|
||||
}
|
||||
const showDisconnectDialog = () => setDialogVisible(true);
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
const hideDisconnectDialog = () => setDialogVisible(false);
|
||||
|
||||
/**
|
||||
* 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <RequestScreen request={request} render={getScreen} />;
|
||||
}
|
||||
|
||||
export default withTheme(ProfileScreen);
|
||||
export default ProfileScreen;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* 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 i18n from 'i18n-js';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
@ -30,10 +30,10 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
|||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import WebSectionList, {
|
||||
SectionListDataType,
|
||||
} from '../../components/Screens/WebSectionList';
|
||||
import { useAuthenticatedRequest } from '../../context/loginContext';
|
||||
|
||||
export type VoteTeamType = {
|
||||
id: number;
|
||||
|
@ -65,6 +65,13 @@ type ResponseType = {
|
|||
dates?: VoteDatesStringType;
|
||||
};
|
||||
|
||||
type FlatlistType = {
|
||||
teams: Array<VoteTeamType>;
|
||||
hasVoted: boolean;
|
||||
datesString?: VoteDatesStringType;
|
||||
dates?: VoteDatesObjectType;
|
||||
};
|
||||
|
||||
// const FAKE_DATE = {
|
||||
// "date_begin": "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({
|
||||
button: {
|
||||
marginLeft: 'auto',
|
||||
|
@ -131,38 +131,19 @@ const styles = StyleSheet.create({
|
|||
/**
|
||||
* Screen displaying vote information and controls
|
||||
*/
|
||||
export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||
teams: Array<VoteTeamType>;
|
||||
export default function VoteScreen() {
|
||||
const [hasVoted, setHasVoted] = useState(false);
|
||||
const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
|
||||
|
||||
hasVoted: boolean;
|
||||
|
||||
datesString: undefined | VoteDatesStringType;
|
||||
|
||||
dates: undefined | VoteDatesObjectType;
|
||||
|
||||
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 datesRequest = useAuthenticatedRequest<VoteDatesStringType>(
|
||||
'elections/dates'
|
||||
);
|
||||
const teamsRequest = useAuthenticatedRequest<TeamResponseType>(
|
||||
'elections/teams'
|
||||
);
|
||||
|
||||
const today = new Date();
|
||||
const refresh = useRef<() => void | undefined>();
|
||||
/**
|
||||
* 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
|
||||
* @returns {string}
|
||||
*/
|
||||
getDateString(date: Date, dateString: string): string {
|
||||
if (this.today.getDate() === date.getDate()) {
|
||||
const getDateString = (date: Date, dateString: string) => {
|
||||
if (today.getDate() === date.getDate()) {
|
||||
const str = getTimeOnlyString(dateString);
|
||||
return str != null ? str : '';
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
getMainRenderItem = ({ item }: { item: { key: string } }) => {
|
||||
const getMainRenderItem = ({
|
||||
item,
|
||||
}: {
|
||||
item: { key: string; data?: FlatlistType };
|
||||
}) => {
|
||||
if (item.key === 'info') {
|
||||
return (
|
||||
<View>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="help-circle"
|
||||
onPress={this.showMascotDialog}
|
||||
onPress={showMascotDialog}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t('screens.vote.mascotDialog.title')}
|
||||
|
@ -196,10 +181,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
return this.getContent();
|
||||
if (item.data) {
|
||||
return getContent(item.data);
|
||||
} else {
|
||||
return <View />;
|
||||
}
|
||||
};
|
||||
|
||||
createDataset = (
|
||||
const createDataset = (
|
||||
data: ResponseType | undefined,
|
||||
_loading: boolean,
|
||||
_lastRefreshDate: Date | undefined,
|
||||
|
@ -207,157 +196,158 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
) => {
|
||||
// data[0] = FAKE_TEAMS2;
|
||||
// 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) {
|
||||
const { teams, dates } = data;
|
||||
|
||||
if (dates && dates.date_begin == null) {
|
||||
this.datesString = undefined;
|
||||
} else {
|
||||
this.datesString = dates;
|
||||
const flatlistData: FlatlistType = {
|
||||
teams: [],
|
||||
hasVoted: false,
|
||||
};
|
||||
if (dates && dates.date_begin != null) {
|
||||
flatlistData.datesString = dates;
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
this.teams = teams.teams;
|
||||
this.hasVoted = teams.has_voted;
|
||||
flatlistData.teams = teams.teams;
|
||||
flatlistData.hasVoted = teams.has_voted;
|
||||
}
|
||||
|
||||
this.generateDateObject();
|
||||
flatlistData.dates = generateDateObject(flatlistData.datesString);
|
||||
}
|
||||
return this.mainFlatListData;
|
||||
return mainFlatListData;
|
||||
};
|
||||
|
||||
getContent() {
|
||||
const { state } = this;
|
||||
if (!this.isVoteStarted()) {
|
||||
return this.getTeaseVoteCard();
|
||||
const getContent = (data: FlatlistType) => {
|
||||
const { dates } = data;
|
||||
if (!isVoteStarted(dates)) {
|
||||
return getTeaseVoteCard(data);
|
||||
}
|
||||
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
|
||||
return this.getVoteCard();
|
||||
if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
|
||||
return getVoteCard(data);
|
||||
}
|
||||
if (!this.isResultStarted()) {
|
||||
return this.getWaitVoteCard();
|
||||
if (!isResultStarted(dates)) {
|
||||
return getWaitVoteCard(data);
|
||||
}
|
||||
if (this.isResultRunning()) {
|
||||
return this.getVoteResultCard();
|
||||
if (isResultRunning(dates)) {
|
||||
return getVoteResultCard(data);
|
||||
}
|
||||
return <VoteNotAvailable />;
|
||||
}
|
||||
|
||||
onVoteSuccess = (): void => this.setState({ hasVoted: true });
|
||||
};
|
||||
|
||||
const onVoteSuccess = () => setHasVoted(true);
|
||||
/**
|
||||
* The user has not voted yet, and the votes are open
|
||||
*/
|
||||
getVoteCard() {
|
||||
const getVoteCard = (data: FlatlistType) => {
|
||||
return (
|
||||
<VoteSelect
|
||||
teams={this.teams}
|
||||
onVoteSuccess={this.onVoteSuccess}
|
||||
onVoteError={this.refreshData}
|
||||
teams={data.teams}
|
||||
onVoteSuccess={onVoteSuccess}
|
||||
onVoteError={() => {
|
||||
if (refresh.current) {
|
||||
refresh.current();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Votes have ended, results can be displayed
|
||||
*/
|
||||
getVoteResultCard() {
|
||||
if (this.dates != null && this.datesString != null) {
|
||||
const getVoteResultCard = (data: FlatlistType) => {
|
||||
if (data.dates != null && data.datesString != null) {
|
||||
return (
|
||||
<VoteResults
|
||||
teams={this.teams}
|
||||
dateEnd={this.getDateString(
|
||||
this.dates.date_result_end,
|
||||
this.datesString.date_result_end
|
||||
teams={data.teams}
|
||||
dateEnd={getDateString(
|
||||
data.dates.date_result_end,
|
||||
data.datesString.date_result_end
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <VoteNotAvailable />;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Vote will open shortly
|
||||
*/
|
||||
getTeaseVoteCard() {
|
||||
if (this.dates != null && this.datesString != null) {
|
||||
const getTeaseVoteCard = (data: FlatlistType) => {
|
||||
if (data.dates != null && data.datesString != null) {
|
||||
return (
|
||||
<VoteTease
|
||||
startDate={this.getDateString(
|
||||
this.dates.date_begin,
|
||||
this.datesString.date_begin
|
||||
startDate={getDateString(
|
||||
data.dates.date_begin,
|
||||
data.datesString.date_begin
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <VoteNotAvailable />;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Votes have ended, or user has voted waiting for results
|
||||
*/
|
||||
getWaitVoteCard() {
|
||||
const { state } = this;
|
||||
const getWaitVoteCard = (data: FlatlistType) => {
|
||||
let startDate = null;
|
||||
if (
|
||||
this.dates != null &&
|
||||
this.datesString != null &&
|
||||
this.dates.date_result_begin != null
|
||||
data.dates != null &&
|
||||
data.datesString != null &&
|
||||
data.dates.date_result_begin != null
|
||||
) {
|
||||
startDate = this.getDateString(
|
||||
this.dates.date_result_begin,
|
||||
this.datesString.date_result_begin
|
||||
startDate = getDateString(
|
||||
data.dates.date_result_begin,
|
||||
data.datesString.date_result_begin
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VoteWait
|
||||
startDate={startDate}
|
||||
hasVoted={this.hasVoted || state.hasVoted}
|
||||
justVoted={state.hasVoted}
|
||||
isVoteRunning={this.isVoteRunning()}
|
||||
hasVoted={data.hasVoted}
|
||||
justVoted={hasVoted}
|
||||
isVoteRunning={isVoteRunning()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
showMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: true });
|
||||
};
|
||||
|
||||
hideMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: false });
|
||||
const showMascotDialog = () => setMascotDialogVisible(true);
|
||||
|
||||
const hideMascotDialog = () => setMascotDialogVisible(false);
|
||||
|
||||
const isVoteStarted = (dates?: VoteDatesObjectType) => {
|
||||
return dates != null && today > dates.date_begin;
|
||||
};
|
||||
|
||||
isVoteStarted(): boolean {
|
||||
return this.dates != null && this.today > this.dates.date_begin;
|
||||
}
|
||||
|
||||
isResultRunning(): boolean {
|
||||
const isResultRunning = (dates?: VoteDatesObjectType) => {
|
||||
return (
|
||||
this.dates != null &&
|
||||
this.today > this.dates.date_result_begin &&
|
||||
this.today < this.dates.date_result_end
|
||||
dates != null &&
|
||||
today > dates.date_result_begin &&
|
||||
today < dates.date_result_end
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
isResultStarted(): boolean {
|
||||
return this.dates != null && this.today > this.dates.date_result_begin;
|
||||
}
|
||||
const isResultStarted = (dates?: VoteDatesObjectType) => {
|
||||
return dates != null && today > dates.date_result_begin;
|
||||
};
|
||||
|
||||
isVoteRunning(): boolean {
|
||||
return (
|
||||
this.dates != null &&
|
||||
this.today > this.dates.date_begin &&
|
||||
this.today < this.dates.date_end
|
||||
);
|
||||
}
|
||||
const isVoteRunning = (dates?: VoteDatesObjectType) => {
|
||||
return dates != null && today > dates.date_begin && today < dates.date_end;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the objects containing string and Date representations of key vote dates
|
||||
*/
|
||||
generateDateObject() {
|
||||
const strings = this.datesString;
|
||||
if (strings != null) {
|
||||
const generateDateObject = (
|
||||
strings?: VoteDatesStringType
|
||||
): VoteDatesObjectType | undefined => {
|
||||
if (strings) {
|
||||
const dateBegin = stringToDate(strings.date_begin);
|
||||
const dateEnd = stringToDate(strings.date_end);
|
||||
const dateResultBegin = stringToDate(strings.date_result_begin);
|
||||
|
@ -368,27 +358,25 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
dateResultBegin != null &&
|
||||
dateResultEnd != null
|
||||
) {
|
||||
this.dates = {
|
||||
return {
|
||||
date_begin: dateBegin,
|
||||
date_end: dateEnd,
|
||||
date_result_begin: dateResultBegin,
|
||||
date_result_end: dateResultEnd,
|
||||
};
|
||||
} else {
|
||||
this.dates = undefined;
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
this.dates = undefined;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request = () => {
|
||||
const request = () => {
|
||||
return new Promise((resolve: (data: ResponseType) => void) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<VoteDatesStringType>('elections/dates')
|
||||
datesRequest()
|
||||
.then((datesData) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<TeamResponseType>('elections/teams')
|
||||
teamsRequest()
|
||||
.then((teamsData) => {
|
||||
resolve({
|
||||
dates: datesData,
|
||||
|
@ -405,38 +393,28 @@ 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 (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebSectionList
|
||||
request={this.request}
|
||||
createDataset={this.createDataset}
|
||||
extraData={state.hasVoted.toString()}
|
||||
renderItem={this.getMainRenderItem}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={state.mascotDialogVisible}
|
||||
title={i18n.t('screens.vote.mascotDialog.title')}
|
||||
message={i18n.t('screens.vote.mascotDialog.message')}
|
||||
icon="vote"
|
||||
buttons={{
|
||||
cancel: {
|
||||
message: i18n.t('screens.vote.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.CUTE}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebSectionList
|
||||
request={request}
|
||||
createDataset={createDataset}
|
||||
extraData={hasVoted.toString()}
|
||||
renderItem={getMainRenderItem}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.vote.mascotDialog.title')}
|
||||
message={i18n.t('screens.vote.mascotDialog.message')}
|
||||
icon="vote"
|
||||
buttons={{
|
||||
cancel: {
|
||||
message: i18n.t('screens.vote.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.CUTE}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ import MaterialHeaderButtons, {
|
|||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
|
@ -59,6 +58,7 @@ import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
|
|||
import { ServiceItemType } from '../../utils/Services';
|
||||
import { useCurrentDashboard } from '../../context/preferencesContext';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
import { useLoginState } from '../../context/loginContext';
|
||||
|
||||
const FEED_ITEM_HEIGHT = 500;
|
||||
|
||||
|
@ -146,9 +146,7 @@ function HomeScreen(props: Props) {
|
|||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const fabRef = useRef<AnimatedFAB>(null);
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(
|
||||
ConnectionManager.getInstance().isLoggedIn()
|
||||
);
|
||||
const isLoggedIn = useLoginState();
|
||||
const { currentDashboard } = useCurrentDashboard();
|
||||
|
||||
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
|
||||
handleNavigationParams();
|
||||
return () => {};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoggedIn])
|
||||
);
|
||||
|
|
|
@ -80,7 +80,8 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean {
|
|||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
params?: object
|
||||
params?: object,
|
||||
token?: string
|
||||
): Promise<T> {
|
||||
return new Promise(
|
||||
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
|
||||
|
@ -88,7 +89,9 @@ export async function apiRequest<T>(
|
|||
if (params != null) {
|
||||
requestParams = { ...params };
|
||||
}
|
||||
console.log(Urls.amicale.api + path);
|
||||
if (token) {
|
||||
requestParams = { ...requestParams, token: token };
|
||||
}
|
||||
|
||||
fetch(Urls.amicale.api + path, {
|
||||
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.
|
||||
*
|
||||
|
|
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