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