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> = [];
|
||||