From 541c00255895713f926e41c7cca11aa2d0ef91f4 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Sun, 23 May 2021 14:14:20 +0200 Subject: [PATCH] convert connection manager to context --- App.tsx | 25 +- src/components/Amicale/Login/LoginForm.tsx | 231 +++++++ src/components/Amicale/LogoutDialog.tsx | 18 +- .../Amicale/Profile/ProfileClubCard.tsx | 100 ++++ .../Amicale/Profile/ProfileMembershipCard.tsx | 56 ++ .../Amicale/Profile/ProfilePersonalCard.tsx | 110 ++++ .../Amicale/Profile/ProfileWelcomeCard.tsx | 81 +++ src/components/Amicale/Vote/VoteSelect.tsx | 177 +++--- .../Lists/Equipment/EquipmentListItem.tsx | 6 +- src/components/Screens/RequestScreen.tsx | 12 +- src/components/providers/LoginProvider.tsx | 27 + src/context/loginContext.ts | 46 ++ src/managers/ConnectionManager.ts | 205 ------- .../Amicale/Clubs/ClubDisplayScreen.tsx | 139 ++--- src/screens/Amicale/Clubs/ClubListScreen.tsx | 217 +++---- .../Amicale/Equipment/EquipmentListScreen.tsx | 139 ++--- .../Amicale/Equipment/EquipmentRentScreen.tsx | 562 ++++++++---------- src/screens/Amicale/LoginScreen.tsx | 485 +++------------ src/screens/Amicale/ProfileScreen.tsx | 511 +++------------- src/screens/Amicale/VoteScreen.tsx | 326 +++++----- src/screens/Home/HomeScreen.tsx | 11 +- src/utils/WebData.ts | 34 +- src/utils/loginToken.ts | 46 ++ src/utils/logout.ts | 11 + 24 files changed, 1610 insertions(+), 1965 deletions(-) create mode 100644 src/components/Amicale/Login/LoginForm.tsx create mode 100644 src/components/Amicale/Profile/ProfileClubCard.tsx create mode 100644 src/components/Amicale/Profile/ProfileMembershipCard.tsx create mode 100644 src/components/Amicale/Profile/ProfilePersonalCard.tsx create mode 100644 src/components/Amicale/Profile/ProfileWelcomeCard.tsx create mode 100644 src/components/providers/LoginProvider.tsx create mode 100644 src/context/loginContext.ts delete mode 100644 src/managers/ConnectionManager.ts create mode 100644 src/utils/loginToken.ts create mode 100644 src/utils/logout.ts diff --git a/App.tsx b/App.tsx index 9d5a93f..bcea3ee 100644 --- a/App.tsx +++ b/App.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { LogBox, Platform } from 'react-native'; import { setSafeBounceHeight } from 'react-navigation-collapsible'; import SplashScreen from 'react-native-splash-screen'; -import ConnectionManager from './src/managers/ConnectionManager'; import type { ParsedUrlDataType } from './src/utils/URLHandler'; import URLHandler from './src/utils/URLHandler'; import initLocales from './src/utils/Locales'; @@ -48,6 +47,8 @@ import { ProxiwashPreferencesProvider, } from './src/components/providers/PreferencesProvider'; import MainApp from './src/screens/MainApp'; +import LoginProvider from './src/components/providers/LoginProvider'; +import { retrieveLoginToken } from './src/utils/loginToken'; // Native optimizations https://reactnavigation.org/docs/react-native-screens // Crashes app when navigating away from webview on android 9+ @@ -67,6 +68,7 @@ type StateType = { proxiwash: ProxiwashPreferencesType; mascot: MascotPreferencesType; }; + loginToken?: string; }; export default class App extends React.Component<{}, StateType> { @@ -88,6 +90,7 @@ export default class App extends React.Component<{}, StateType> { proxiwash: defaultProxiwashPreferences, mascot: defaultMascotPreferences, }, + loginToken: undefined, }; initLocales(); this.navigatorRef = React.createRef(); @@ -136,10 +139,11 @@ export default class App extends React.Component<{}, StateType> { | PlanexPreferencesType | ProxiwashPreferencesType | MascotPreferencesType - | void + | string + | undefined > ) => { - const [general, planex, proxiwash, mascot] = values; + const [general, planex, proxiwash, mascot, token] = values; this.setState({ isLoading: false, initialPreferences: { @@ -148,6 +152,7 @@ export default class App extends React.Component<{}, StateType> { proxiwash: proxiwash as ProxiwashPreferencesType, mascot: mascot as MascotPreferencesType, }, + loginToken: token as string | undefined, }); SplashScreen.hide(); }; @@ -175,7 +180,7 @@ export default class App extends React.Component<{}, StateType> { Object.values(MascotPreferenceKeys), defaultMascotPreferences ), - ConnectionManager.getInstance().recoverLogin(), + retrieveLoginToken(), ]) .then(this.onLoadFinished) .catch(this.onLoadFinished); @@ -202,11 +207,13 @@ export default class App extends React.Component<{}, StateType> { - + + + diff --git a/src/components/Amicale/Login/LoginForm.tsx b/src/components/Amicale/Login/LoginForm.tsx new file mode 100644 index 0000000..4fbcf60 --- /dev/null +++ b/src/components/Amicale/Login/LoginForm.tsx @@ -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(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 ( + + ( + + )} + /> + + + + + {i18n.t('screens.login.emailError')} + + + + {i18n.t('screens.login.passwordError')} + + + + + + + + + + + + ); +} diff --git a/src/components/Amicale/LogoutDialog.tsx b/src/components/Amicale/LogoutDialog.tsx index 6e060a3..8d2759c 100644 --- a/src/components/Amicale/LogoutDialog.tsx +++ b/src/components/Amicale/LogoutDialog.tsx @@ -20,8 +20,7 @@ import * as React from 'react'; import i18n from 'i18n-js'; import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog'; -import ConnectionManager from '../../managers/ConnectionManager'; -import { useNavigation } from '@react-navigation/native'; +import { useLogout } from '../../utils/logout'; type PropsType = { visible: boolean; @@ -29,19 +28,12 @@ type PropsType = { }; function LogoutDialog(props: PropsType) { - const navigation = useNavigation(); + const onLogout = useLogout(); + // Use a loading dialog as it can take some time to update the context const onClickAccept = async (): Promise => { return new Promise((resolve: () => void) => { - ConnectionManager.getInstance() - .disconnect() - .then(() => { - navigation.reset({ - index: 0, - routes: [{ name: 'main' }], - }); - props.onDismiss(); - resolve(); - }); + onLogout(); + resolve(); }); }; diff --git a/src/components/Amicale/Profile/ProfileClubCard.tsx b/src/components/Amicale/Profile/ProfileClubCard.tsx new file mode 100644 index 0000000..efb7876 --- /dev/null +++ b/src/components/Amicale/Profile/ProfileClubCard.tsx @@ -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; +}; + +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; + }; + }) => ( + + ); + if (item.is_manager) { + description = i18n.t('screens.profile.isManager'); + icon = (leftProps) => ( + + ); + } + return ( + + ); + }; + + function getClubList(list: Array | undefined) { + if (!list) { + return null; + } + + list.sort((a) => (a.is_manager ? -1 : 1)); + return ( + + ); + } + + return ( + + ( + + )} + /> + + + {getClubList(props.clubs)} + + + ); +} diff --git a/src/components/Amicale/Profile/ProfileMembershipCard.tsx b/src/components/Amicale/Profile/ProfileMembershipCard.tsx new file mode 100644 index 0000000..1b11902 --- /dev/null +++ b/src/components/Amicale/Profile/ProfileMembershipCard.tsx @@ -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 ( + + ( + + )} + /> + + + ( + + )} + /> + + + + ); +} diff --git a/src/components/Amicale/Profile/ProfilePersonalCard.tsx b/src/components/Amicale/Profile/ProfilePersonalCard.tsx new file mode 100644 index 0000000..2c8f3b6 --- /dev/null +++ b/src/components/Amicale/Profile/ProfilePersonalCard.tsx @@ -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 ( + ( + + )} + /> + ); + } + + return ( + + ( + + )} + /> + + + + + {i18n.t('screens.profile.personalInformation')} + + {getPersonalListItem(profile?.birthday, 'cake-variant')} + {getPersonalListItem(profile?.phone, 'phone')} + {getPersonalListItem(profile?.email, 'email')} + {getPersonalListItem(profile?.branch, 'school')} + + + + + + + + ); +} diff --git a/src/components/Amicale/Profile/ProfileWelcomeCard.tsx b/src/components/Amicale/Profile/ProfileWelcomeCard.tsx new file mode 100644 index 0000000..ca564ca --- /dev/null +++ b/src/components/Amicale/Profile/ProfileWelcomeCard.tsx @@ -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 ( + + ( + + )} + titleStyle={styles.title} + /> + + + {i18n.t('screens.profile.welcomeDescription')} + + {i18n.t('screens.profile.welcomeFeedback')} + + + + + + + ); +} + +export default React.memo( + ProfileWelcomeCard, + (pp, np) => pp.firstname === np.firstname +); diff --git a/src/components/Amicale/Vote/VoteSelect.tsx b/src/components/Amicale/Vote/VoteSelect.tsx index f0d1f81..fc290c0 100644 --- a/src/components/Amicale/Vote/VoteSelect.tsx +++ b/src/components/Amicale/Vote/VoteSelect.tsx @@ -17,30 +17,23 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useState } from 'react'; import { Avatar, Button, Card, RadioButton } from 'react-native-paper'; import { FlatList, StyleSheet, View } from 'react-native'; import i18n from 'i18n-js'; -import ConnectionManager from '../../../managers/ConnectionManager'; import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog'; import ErrorDialog from '../../Dialogs/ErrorDialog'; import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen'; import { ApiRejectType } from '../../../utils/WebData'; import { REQUEST_STATUS } from '../../../utils/Requests'; +import { useAuthenticatedRequest } from '../../../context/loginContext'; -type PropsType = { +type Props = { teams: Array; onVoteSuccess: () => void; onVoteError: () => void; }; -type StateType = { - selectedTeam: string; - voteDialogVisible: boolean; - errorDialogVisible: boolean; - currentError: ApiRejectType; -}; - const styles = StyleSheet.create({ card: { margin: 10, @@ -50,118 +43,98 @@ const styles = StyleSheet.create({ }, }); -export default class VoteSelect extends React.PureComponent< - PropsType, - StateType -> { - constructor(props: PropsType) { - super(props); - this.state = { - selectedTeam: 'none', - voteDialogVisible: false, - errorDialogVisible: false, - currentError: { status: REQUEST_STATUS.SUCCESS }, - }; - } +function VoteSelect(props: Props) { + const [selectedTeam, setSelectedTeam] = useState('none'); + const [voteDialogVisible, setVoteDialogVisible] = useState(false); + const [currentError, setCurrentError] = useState({ + status: REQUEST_STATUS.SUCCESS, + }); + const request = useAuthenticatedRequest('elections/vote', { + team: parseInt(selectedTeam, 10), + }); - onVoteSelectionChange = (teamName: string): void => - this.setState({ selectedTeam: teamName }); + const voteKeyExtractor = (item: VoteTeamType) => item.id.toString(); - voteKeyExtractor = (item: VoteTeamType): string => item.id.toString(); - - voteRenderItem = ({ item }: { item: VoteTeamType }) => ( + const voteRenderItem = ({ item }: { item: VoteTeamType }) => ( ); - showVoteDialog = (): void => this.setState({ voteDialogVisible: true }); + const showVoteDialog = () => setVoteDialogVisible(true); - onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false }); + const onVoteDialogDismiss = () => setVoteDialogVisible(false); - onVoteDialogAccept = async (): Promise => { + const onVoteDialogAccept = async (): Promise => { return new Promise((resolve: () => void) => { - const { state } = this; - ConnectionManager.getInstance() - .authenticatedRequest('elections/vote', { - team: parseInt(state.selectedTeam, 10), - }) + request() .then(() => { - this.onVoteDialogDismiss(); - const { props } = this; + onVoteDialogDismiss(); props.onVoteSuccess(); resolve(); }) .catch((error: ApiRejectType) => { - this.onVoteDialogDismiss(); - this.showErrorDialog(error); + onVoteDialogDismiss(); + setCurrentError(error); resolve(); }); }); }; - showErrorDialog = (error: ApiRejectType): void => - this.setState({ - errorDialogVisible: true, - currentError: error, - }); - - onErrorDialogDismiss = () => { - this.setState({ errorDialogVisible: false }); - const { props } = this; + const onErrorDialogDismiss = () => { + setCurrentError({ status: REQUEST_STATUS.SUCCESS }); props.onVoteError(); }; - render() { - const { state, props } = this; - return ( - - - ( - - )} - /> - - - - - - - - - - + + ( + + )} /> - - - ); - } + + + + + + + + + + + + + ); } + +export default VoteSelect; diff --git a/src/components/Lists/Equipment/EquipmentListItem.tsx b/src/components/Lists/Equipment/EquipmentListItem.tsx index f76414a..9797643 100644 --- a/src/components/Lists/Equipment/EquipmentListItem.tsx +++ b/src/components/Lists/Equipment/EquipmentListItem.tsx @@ -20,7 +20,6 @@ import * as React from 'react'; import { Avatar, List, useTheme } from 'react-native-paper'; import i18n from 'i18n-js'; -import { StackNavigationProp } from '@react-navigation/stack'; import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen'; import { getFirstEquipmentAvailability, @@ -29,9 +28,9 @@ import { } from '../../../utils/EquipmentBooking'; import { StyleSheet } from 'react-native'; import GENERAL_STYLES from '../../../constants/Styles'; +import { useNavigation } from '@react-navigation/native'; type PropsType = { - navigation: StackNavigationProp; userDeviceRentDates: [string, string] | null; item: DeviceType; height: number; @@ -48,7 +47,8 @@ const styles = StyleSheet.create({ function EquipmentListItem(props: PropsType) { const theme = useTheme(); - const { item, userDeviceRentDates, navigation, height } = props; + const navigation = useNavigation(); + const { item, userDeviceRentDates, height } = props; const isRented = userDeviceRentDates != null; const isAvailable = isEquipmentAvailable(item); const firstAvailability = getFirstEquipmentAvailability(item); diff --git a/src/components/Screens/RequestScreen.tsx b/src/components/Screens/RequestScreen.tsx index 6d6550d..f886c75 100644 --- a/src/components/Screens/RequestScreen.tsx +++ b/src/components/Screens/RequestScreen.tsx @@ -11,7 +11,7 @@ import i18n from 'i18n-js'; import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests'; import { StackNavigationProp } from '@react-navigation/stack'; import { MainRoutes } from '../../navigation/MainNavigator'; -import ConnectionManager from '../../managers/ConnectionManager'; +import { useLogout } from '../../utils/logout'; export type RequestScreenProps = { request: () => Promise; @@ -44,6 +44,7 @@ type Props = RequestScreenProps; const MIN_REFRESH_TIME = 3 * 1000; export default function RequestScreen(props: Props) { + const onLogout = useLogout(); const navigation = useNavigation>(); const route = useRoute(); const refreshInterval = useRef(); @@ -103,13 +104,10 @@ export default function RequestScreen(props: Props) { useEffect(() => { if (isErrorCritical(code)) { - ConnectionManager.getInstance() - .disconnect() - .then(() => { - navigation.replace(MainRoutes.Login, { nextScreen: route.name }); - }); + onLogout(); + navigation.replace(MainRoutes.Login, { nextScreen: route.name }); } - }, [code, navigation, route]); + }, [code, navigation, route, onLogout]); if (data === undefined && loading && props.showLoading !== false) { return ; diff --git a/src/components/providers/LoginProvider.tsx b/src/components/providers/LoginProvider.tsx new file mode 100644 index 0000000..f355cd6 --- /dev/null +++ b/src/components/providers/LoginProvider.tsx @@ -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({ + token: props.initialToken, + setLogin: setLogin, + }); + + return ( + + {props.children} + + ); +} diff --git a/src/context/loginContext.ts b/src/context/loginContext.ts new file mode 100644 index 0000000..4792281 --- /dev/null +++ b/src/context/loginContext.ts @@ -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({ + 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( + path: string, + params?: { [key: string]: any } +) { + const token = useLoginToken(); + return () => apiRequest(path, 'POST', params, token); +} diff --git a/src/managers/ConnectionManager.ts b/src/managers/ConnectionManager.ts deleted file mode 100644 index ee11ff2..0000000 --- a/src/managers/ConnectionManager.ts +++ /dev/null @@ -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 . - */ - -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 - */ - async recoverLogin(): Promise { - 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 - */ - async saveLogin(_email: string, token: string): Promise { - 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 - */ - async disconnect(): Promise { - 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 - */ - async connect(email: string, password: string): Promise { - return new Promise( - (resolve: () => void, reject: (error: ApiRejectType) => void) => { - const data = { - email, - password, - }; - apiRequest(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 - */ - async authenticatedRequest( - path: string, - params?: { [key: string]: any } - ): Promise { - return new Promise( - ( - resolve: (response: T) => void, - reject: (error: ApiRejectType) => void - ) => { - if (this.getToken() !== null) { - const data = { - ...params, - token: this.getToken(), - }; - apiRequest(path, 'POST', data) - .then((response: T) => resolve(response)) - .catch(reject); - } else { - reject({ - status: REQUEST_STATUS.TOKEN_RETRIEVE, - }); - } - } - ); - } -} diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx b/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx index 65cd4b0..e0b14b5 100644 --- a/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx +++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx @@ -17,7 +17,7 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useState } from 'react'; import { Linking, StyleSheet, View } from 'react-native'; import { Avatar, @@ -25,20 +25,21 @@ import { Card, Chip, Paragraph, - withTheme, + useTheme, } from 'react-native-paper'; import i18n from 'i18n-js'; -import { StackNavigationProp } from '@react-navigation/stack'; import CustomHTML from '../../../components/Overrides/CustomHTML'; import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar'; import type { ClubCategoryType, ClubType } from './ClubListScreen'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import ImageGalleryButton from '../../../components/Media/ImageGalleryButton'; import RequestScreen from '../../../components/Screens/RequestScreen'; -import ConnectionManager from '../../../managers/ConnectionManager'; +import { useFocusEffect } from '@react-navigation/core'; +import { useCallback } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { useAuthenticatedRequest } from '../../../context/loginContext'; -type PropsType = { - navigation: StackNavigationProp; +type Props = { route: { params?: { data?: ClubType; @@ -46,7 +47,6 @@ type PropsType = { clubId?: number; }; }; - theme: ReactNativePaper.Theme; }; type ResponseType = ClubType; @@ -89,33 +89,28 @@ const styles = StyleSheet.create({ * If called with data and categories navigation parameters, will use those to display the data. * If called with clubId parameter, will fetch the information on the server */ -class ClubDisplayScreen extends React.Component { - displayData: ClubType | undefined; +function ClubDisplayScreen(props: Props) { + const navigation = useNavigation(); + const theme = useTheme(); - categories: Array | null; + const [displayData, setDisplayData] = useState(); + const [categories, setCategories] = useState< + Array | undefined + >(); + const [clubId, setClubId] = useState(); - clubId: number; - - shouldFetchData: boolean; - - constructor(props: PropsType) { - super(props); - this.displayData = undefined; - this.categories = null; - this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0; - this.shouldFetchData = true; - - if ( - props.route.params && - props.route.params.data && - props.route.params.categories - ) { - this.displayData = props.route.params.data; - this.categories = props.route.params.categories; - this.clubId = props.route.params.data.id; - this.shouldFetchData = false; - } - } + useFocusEffect( + useCallback(() => { + if (props.route.params?.data && props.route.params?.categories) { + setDisplayData(props.route.params.data); + setCategories(props.route.params.categories); + setClubId(props.route.params.data.id); + } else { + const id = props.route.params?.clubId; + setClubId(id ? id : 0); + } + }, [props.route.params]) + ); /** * Gets the name of the category with the given ID @@ -123,17 +118,17 @@ class ClubDisplayScreen extends React.Component { * @param id The category's ID * @returns {string|*} */ - getCategoryName(id: number): string { + const getCategoryName = (id: number): string => { let categoryName = ''; - if (this.categories !== null) { - this.categories.forEach((item: ClubCategoryType) => { + if (categories) { + categories.forEach((item: ClubCategoryType) => { if (id === item.id) { categoryName = item.name; } }); } return categoryName; - } + }; /** * Gets the view for rendering categories @@ -141,23 +136,23 @@ class ClubDisplayScreen extends React.Component { * @param categories The categories to display (max 2) * @returns {null|*} */ - getCategoriesRender(categories: Array) { - if (this.categories == null) { + const getCategoriesRender = (c: Array) => { + if (!categories) { return null; } const final: Array = []; - categories.forEach((cat: number | null) => { + c.forEach((cat: number | null) => { if (cat != null) { final.push( - {this.getCategoryName(cat)} + {getCategoryName(cat)} ); } }); return {final}; - } + }; /** * Gets the view for rendering club managers if any @@ -166,8 +161,7 @@ class ClubDisplayScreen extends React.Component { * @param email The club contact email * @returns {*} */ - getManagersRender(managers: Array, email: string | null) { - const { props } = this; + const getManagersRender = (managers: Array, email: string | null) => { const managersListView: Array = []; managers.forEach((item: string) => { managersListView.push({item}); @@ -191,22 +185,18 @@ class ClubDisplayScreen extends React.Component { )} /> {managersListView} - {ClubDisplayScreen.getEmailButton(email, hasManagers)} + {getEmailButton(email, hasManagers)} ); - } + }; /** * 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 { * @param hasManagers True if the club has managers * @returns {*} */ - static getEmailButton(email: string | null, hasManagers: boolean) { + const getEmailButton = (email: string | null, hasManagers: boolean) => { const destinationEmail = email != null && hasManagers ? email : AMICALE_MAIL; const text = @@ -236,14 +226,14 @@ class ClubDisplayScreen extends React.Component { ); - } + }; - getScreen = (data: ResponseType | undefined) => { + const getScreen = (data: ResponseType | undefined) => { if (data) { - this.updateHeaderTitle(data); + updateHeaderTitle(data); return ( - {this.getCategoriesRender(data.category)} + {getCategoriesRender(data.category)} {data.logo !== null ? ( { ) : ( )} - {this.getManagersRender(data.responsibles, data.email)} + {getManagersRender(data.responsibles, data.email)} ); } @@ -273,27 +263,22 @@ class ClubDisplayScreen extends React.Component { * * @param data The club data */ - updateHeaderTitle(data: ClubType) { - const { props } = this; - props.navigation.setOptions({ title: data.name }); - } + const updateHeaderTitle = (data: ClubType) => { + navigation.setOptions({ title: data.name }); + }; - render() { - if (this.shouldFetchData) { - return ( - - ConnectionManager.getInstance().authenticatedRequest( - 'clubs/info', - { id: this.clubId } - ) - } - render={this.getScreen} - /> - ); - } - return this.getScreen(this.displayData); - } + const request = useAuthenticatedRequest('clubs/info', { + id: clubId, + }); + + return ( + + ); } -export default withTheme(ClubDisplayScreen); +export default ClubDisplayScreen; diff --git a/src/screens/Amicale/Clubs/ClubListScreen.tsx b/src/screens/Amicale/Clubs/ClubListScreen.tsx index c86ca82..048c343 100644 --- a/src/screens/Amicale/Clubs/ClubListScreen.tsx +++ b/src/screens/Amicale/Clubs/ClubListScreen.tsx @@ -17,11 +17,10 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; import { Platform } from 'react-native'; import { Searchbar } from 'react-native-paper'; import i18n from 'i18n-js'; -import { StackNavigationProp } from '@react-navigation/stack'; import ClubListItem from '../../../components/Lists/Clubs/ClubListItem'; import { isItemInCategoryFilter, @@ -31,8 +30,9 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader'; import MaterialHeaderButtons, { Item, } from '../../../components/Overrides/CustomHeaderButton'; -import ConnectionManager from '../../../managers/ConnectionManager'; import WebSectionList from '../../../components/Screens/WebSectionList'; +import { useNavigation } from '@react-navigation/native'; +import { useAuthenticatedRequest } from '../../../context/loginContext'; export type ClubCategoryType = { id: number; @@ -49,15 +49,6 @@ export type ClubType = { responsibles: Array; }; -type PropsType = { - navigation: StackNavigationProp; -}; - -type StateType = { - currentlySelectedCategories: Array; - currentSearchString: string; -}; - type ResponseType = { categories: Array; clubs: Array; @@ -65,33 +56,52 @@ type ResponseType = { const LIST_ITEM_HEIGHT = 96; -class ClubListScreen extends React.Component { - categories: Array; +function ClubListScreen() { + const navigation = useNavigation(); + const request = useAuthenticatedRequest('clubs/list'); + const [ + currentlySelectedCategories, + setCurrentlySelectedCategories, + ] = useState>([]); + const [currentSearchString, setCurrentSearchString] = useState(''); + const categories = useRef>([]); - constructor(props: PropsType) { - super(props); - this.categories = []; - this.state = { - currentlySelectedCategories: [], - currentSearchString: '', + useLayoutEffect(() => { + const getSearchBar = () => { + return ( + // @ts-ignore + + ); }; - } - - /** - * Creates the header content - */ - componentDidMount() { - const { props } = this; - props.navigation.setOptions({ - headerTitle: this.getSearchBar, - headerRight: this.getHeaderButtons, + const getHeaderButtons = () => { + return ( + + navigation.navigate('club-about')} + /> + + ); + }; + navigation.setOptions({ + headerTitle: getSearchBar, + headerRight: getHeaderButtons, headerBackTitleVisible: false, headerTitleContainerStyle: Platform.OS === 'ios' ? { marginHorizontal: 0, width: '70%' } : { marginHorizontal: 0, right: 50, left: 50 }, }); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigation]); + + const onSearchStringChange = (str: string) => { + updateFilteredData(str, null); + }; /** * Callback used when clicking an article in the list. @@ -99,61 +109,20 @@ class ClubListScreen extends React.Component { * * @param item The article pressed */ - onListItemPress(item: ClubType) { - const { props } = this; - props.navigation.navigate('club-information', { + const onListItemPress = (item: ClubType) => { + navigation.navigate('club-information', { data: item, - categories: this.categories, + categories: categories.current, }); - } - - /** - * Callback used when the search changes - * - * @param str The new search string - */ - onSearchStringChange = (str: string) => { - this.updateFilteredData(str, null); }; - /** - * Gets the header search bar - * - * @return {*} - */ - getSearchBar = () => { - return ( - // @ts-ignore - - ); + const onChipSelect = (id: number) => { + updateFilteredData(null, id); }; - onChipSelect = (id: number) => { - this.updateFilteredData(null, id); - }; - - /** - * Gets the header button - * @return {*} - */ - getHeaderButtons = () => { - const onPress = () => { - const { props } = this; - props.navigation.navigate('club-about'); - }; - return ( - - - - ); - }; - - createDataset = (data: ResponseType | undefined) => { + const createDataset = (data: ResponseType | undefined) => { if (data) { - this.categories = data?.categories; + categories.current = data.categories; return [{ title: '', data: data.clubs }]; } else { return []; @@ -165,30 +134,23 @@ class ClubListScreen extends React.Component { * * @returns {*} */ - getListHeader(data: ResponseType | undefined) { - const { state } = this; + const getListHeader = (data: ResponseType | undefined) => { if (data) { return ( ); } else { return null; } - } + }; - /** - * Gets the category object of the given ID - * - * @param id The ID of the category to find - * @returns {*} - */ - getCategoryOfId = (id: number): ClubCategoryType | null => { + const getCategoryOfId = (id: number): ClubCategoryType | null => { let cat = null; - this.categories.forEach((item: ClubCategoryType) => { + categories.current.forEach((item: ClubCategoryType) => { if (id === item.id) { cat = item; } @@ -196,14 +158,14 @@ class ClubListScreen extends React.Component { return cat; }; - getRenderItem = ({ item }: { item: ClubType }) => { + const getRenderItem = ({ item }: { item: ClubType }) => { const onPress = () => { - this.onListItemPress(item); + onListItemPress(item); }; - if (this.shouldRenderItem(item)) { + if (shouldRenderItem(item)) { return ( { return null; }; - keyExtractor = (item: ClubType): string => item.id.toString(); + const keyExtractor = (item: ClubType): string => item.id.toString(); /** * Updates the search string and category filter, saving them to the State. @@ -224,10 +186,12 @@ class ClubListScreen extends React.Component { * @param filterStr The new filter string to use * @param categoryId The category to add/remove from the filter */ - updateFilteredData(filterStr: string | null, categoryId: number | null) { - const { state } = this; - const newCategoriesState = [...state.currentlySelectedCategories]; - let newStrState = state.currentSearchString; + const updateFilteredData = ( + filterStr: string | null, + categoryId: number | null + ) => { + const newCategoriesState = [...currentlySelectedCategories]; + let newStrState = currentSearchString; if (filterStr !== null) { newStrState = filterStr; } @@ -240,12 +204,10 @@ class ClubListScreen extends React.Component { } } if (filterStr !== null || categoryId !== null) { - this.setState({ - currentSearchString: newStrState, - currentlySelectedCategories: newCategoriesState, - }); + setCurrentSearchString(newStrState); + setCurrentlySelectedCategories(newCategoriesState); } - } + }; /** * Checks if the given item should be rendered according to current name and category filters @@ -253,35 +215,28 @@ class ClubListScreen extends React.Component { * @param item The club to check * @returns {boolean} */ - shouldRenderItem(item: ClubType): boolean { - const { state } = this; + const shouldRenderItem = (item: ClubType): boolean => { let shouldRender = - state.currentlySelectedCategories.length === 0 || - isItemInCategoryFilter(state.currentlySelectedCategories, item.category); + currentlySelectedCategories.length === 0 || + isItemInCategoryFilter(currentlySelectedCategories, item.category); if (shouldRender) { - shouldRender = stringMatchQuery(item.name, state.currentSearchString); + shouldRender = stringMatchQuery(item.name, currentSearchString); } return shouldRender; - } + }; - render() { - return ( - - ConnectionManager.getInstance().authenticatedRequest( - 'clubs/list' - ) - } - createDataset={this.createDataset} - keyExtractor={this.keyExtractor} - renderItem={this.getRenderItem} - renderListHeaderComponent={(data) => this.getListHeader(data)} - // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration - removeClippedSubviews={true} - itemHeight={LIST_ITEM_HEIGHT} - /> - ); - } + return ( + + ); } export default ClubListScreen; diff --git a/src/screens/Amicale/Equipment/EquipmentListScreen.tsx b/src/screens/Amicale/Equipment/EquipmentListScreen.tsx index 8dfe8af..f437168 100644 --- a/src/screens/Amicale/Equipment/EquipmentListScreen.tsx +++ b/src/screens/Amicale/Equipment/EquipmentListScreen.tsx @@ -17,26 +17,17 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useRef, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { Button } from 'react-native-paper'; -import { StackNavigationProp } from '@react-navigation/stack'; import i18n from 'i18n-js'; import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem'; import MascotPopup from '../../../components/Mascot/MascotPopup'; import { MASCOT_STYLE } from '../../../components/Mascot/Mascot'; import GENERAL_STYLES from '../../../constants/Styles'; -import ConnectionManager from '../../../managers/ConnectionManager'; import { ApiRejectType } from '../../../utils/WebData'; import WebSectionList from '../../../components/Screens/WebSectionList'; - -type PropsType = { - navigation: StackNavigationProp; -}; - -type StateType = { - mascotDialogVisible: boolean | undefined; -}; +import { useAuthenticatedRequest } from '../../../context/loginContext'; export type DeviceType = { id: number; @@ -67,69 +58,62 @@ const styles = StyleSheet.create({ }, }); -class EquipmentListScreen extends React.Component { - userRents: null | Array; +function EquipmentListScreen() { + const userRents = useRef>(); + const [mascotDialogVisible, setMascotDialogVisible] = useState(false); - constructor(props: PropsType) { - super(props); - this.userRents = null; - this.state = { - mascotDialogVisible: undefined, - }; - } + const requestAll = useAuthenticatedRequest<{ devices: Array }>( + 'location/all' + ); + const requestOwn = useAuthenticatedRequest<{ + locations: Array; + }>('location/my'); - getRenderItem = ({ item }: { item: DeviceType }) => { - const { navigation } = this.props; + const getRenderItem = ({ item }: { item: DeviceType }) => { return ( ); }; - getUserDeviceRentDates(item: DeviceType): [string, string] | null { + const getUserDeviceRentDates = ( + item: DeviceType + ): [string, string] | null => { let dates = null; - if (this.userRents != null) { - this.userRents.forEach((device: RentedDeviceType) => { + if (userRents.current) { + userRents.current.forEach((device: RentedDeviceType) => { if (item.id === device.device_id) { dates = [device.begin, device.end]; } }); } return dates; - } + }; - /** - * Gets the list header, with explains this screen's purpose - * - * @returns {*} - */ - getListHeader() { + const getListHeader = () => { return ( ); - } + }; - keyExtractor = (item: DeviceType): string => item.id.toString(); + const keyExtractor = (item: DeviceType): string => item.id.toString(); - createDataset = (data: ResponseType | undefined) => { + const createDataset = (data: ResponseType | undefined) => { if (data) { - const userRents = data.locations; - - if (userRents) { - this.userRents = userRents; + if (data.locations) { + userRents.current = data.locations; } return [{ title: '', data: data.devices }]; } else { @@ -137,27 +121,19 @@ class EquipmentListScreen extends React.Component { } }; - showMascotDialog = () => { - this.setState({ mascotDialogVisible: true }); - }; + const showMascotDialog = () => setMascotDialogVisible(true); - hideMascotDialog = () => { - this.setState({ mascotDialogVisible: false }); - }; + const hideMascotDialog = () => setMascotDialogVisible(false); - request = () => { + const request = () => { return new Promise( ( resolve: (data: ResponseType) => void, reject: (error: ApiRejectType) => void ) => { - ConnectionManager.getInstance() - .authenticatedRequest<{ devices: Array }>('location/all') + requestAll() .then((devicesData) => { - ConnectionManager.getInstance() - .authenticatedRequest<{ - locations: Array; - }>('location/my') + requestOwn() .then((rentsData) => { resolve({ devices: devicesData.devices, @@ -175,34 +151,31 @@ class EquipmentListScreen extends React.Component { ); }; - render() { - const { state } = this; - return ( - - this.getListHeader()} - /> - - - ); - } + return ( + + + + + ); } export default EquipmentListScreen; diff --git a/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx b/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx index e81fbb8..a145c98 100644 --- a/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx +++ b/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx @@ -17,21 +17,20 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { Button, Caption, Card, Headline, Subheading, - withTheme, + useTheme, } from 'react-native-paper'; import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'; import { BackHandler, StyleSheet, View } from 'react-native'; import * as Animatable from 'react-native-animatable'; import i18n from 'i18n-js'; import { CalendarList, PeriodMarking } from 'react-native-calendars'; -import type { DeviceType } from './EquipmentListScreen'; import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog'; import ErrorDialog from '../../../components/Dialogs/ErrorDialog'; import { @@ -42,34 +41,21 @@ import { getValidRange, isEquipmentAvailable, } from '../../../utils/EquipmentBooking'; -import ConnectionManager from '../../../managers/ConnectionManager'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import { MainStackParamsList } from '../../../navigation/MainNavigator'; import GENERAL_STYLES from '../../../constants/Styles'; import { ApiRejectType } from '../../../utils/WebData'; import { REQUEST_STATUS } from '../../../utils/Requests'; +import { useFocusEffect } from '@react-navigation/core'; +import { useNavigation } from '@react-navigation/native'; +import { useAuthenticatedRequest } from '../../../context/loginContext'; -type EquipmentRentScreenNavigationProp = StackScreenProps< - MainStackParamsList, - 'equipment-rent' ->; - -type Props = EquipmentRentScreenNavigationProp & { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; -}; +type Props = StackScreenProps; export type MarkedDatesObjectType = { [key: string]: PeriodMarking; }; -type StateType = { - dialogVisible: boolean; - errorDialogVisible: boolean; - markedDates: MarkedDatesObjectType; - currentError: ApiRejectType; -}; - const styles = StyleSheet.create({ titleContainer: { marginLeft: 'auto', @@ -114,98 +100,101 @@ const styles = StyleSheet.create({ }, }); -class EquipmentRentScreen extends React.Component { - item: DeviceType | null; +function EquipmentRentScreen(props: Props) { + const theme = useTheme(); + const navigation = useNavigation>(); + const [currentError, setCurrentError] = useState({ + status: REQUEST_STATUS.SUCCESS, + }); + const [markedDates, setMarkedDates] = useState({}); + const [dialogVisible, setDialogVisible] = useState(false); - bookedDates: Array; + const item = props.route.params.item; - bookRef: { current: null | (Animatable.View & View) }; + const bookedDates = useRef>([]); + const canBookEquipment = useRef(false); - canBookEquipment: boolean; + const bookRef = useRef(null); - lockedDates: { + let lockedDates: { [key: string]: PeriodMarking; - }; + } = {}; - constructor(props: Props) { - super(props); - this.item = null; - this.lockedDates = {}; - this.state = { - dialogVisible: false, - errorDialogVisible: false, - markedDates: {}, - currentError: { status: REQUEST_STATUS.SUCCESS }, - }; - this.resetSelection(); - this.bookRef = React.createRef(); - this.canBookEquipment = false; - this.bookedDates = []; - if (props.route.params != null) { - if (props.route.params.item != null) { - this.item = props.route.params.item; - } else { - this.item = null; - } - } - const { item } = this; - if (item != null) { - this.lockedDates = {}; - item.booked_at.forEach((date: { begin: string; end: string }) => { - const range = getValidRange( - new Date(date.begin), - new Date(date.end), - null - ); - this.lockedDates = { - ...this.lockedDates, - ...generateMarkedDates(false, props.theme, range), - }; - }); - } + if (item) { + item.booked_at.forEach((date: { begin: string; end: string }) => { + const range = getValidRange( + new Date(date.begin), + new Date(date.end), + null + ); + lockedDates = { + ...lockedDates, + ...generateMarkedDates(false, theme, range), + }; + }); } - /** - * Captures focus and blur events to hook on android back button - */ - componentDidMount() { - const { navigation } = this.props; - navigation.addListener('focus', () => { + useFocusEffect( + useCallback(() => { BackHandler.addEventListener( 'hardwareBackPress', - this.onBackButtonPressAndroid + onBackButtonPressAndroid ); - }); - navigation.addListener('blur', () => { - BackHandler.removeEventListener( - 'hardwareBackPress', - this.onBackButtonPressAndroid - ); - }); - } + return () => { + BackHandler.removeEventListener( + 'hardwareBackPress', + onBackButtonPressAndroid + ); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + ); /** * Overrides default android back button behaviour to deselect date if any is selected. * * @return {boolean} */ - onBackButtonPressAndroid = (): boolean => { - if (this.bookedDates.length > 0) { - this.resetSelection(); - this.updateMarkedSelection(); + const onBackButtonPressAndroid = (): boolean => { + if (bookedDates.current.length > 0) { + resetSelection(); + updateMarkedSelection(); return true; } return false; }; - onDialogDismiss = () => { - this.setState({ dialogVisible: false }); + const showDialog = () => setDialogVisible(true); + + const onDialogDismiss = () => setDialogVisible(false); + + const onErrorDialogDismiss = () => + setCurrentError({ status: REQUEST_STATUS.SUCCESS }); + + const getBookStartDate = (): Date | null => { + return bookedDates.current.length > 0 + ? new Date(bookedDates.current[0]) + : null; }; - onErrorDialogDismiss = () => { - this.setState({ errorDialogVisible: false }); + const getBookEndDate = (): Date | null => { + const { length } = bookedDates.current; + return length > 0 ? new Date(bookedDates.current[length - 1]) : null; }; + const start = getBookStartDate(); + const end = getBookEndDate(); + const request = useAuthenticatedRequest( + 'location/booking', + item && start && end + ? { + device: item.id, + begin: getISODate(start), + end: getISODate(end), + } + : undefined + ); + /** * Sends the selected data to the server and waits for a response. * If the request is a success, navigate to the recap screen. @@ -213,54 +202,37 @@ class EquipmentRentScreen extends React.Component { * * @returns {Promise} */ - onDialogAccept = (): Promise => { + const onDialogAccept = (): Promise => { return new Promise((resolve: () => void) => { - const { item, props } = this; - const start = this.getBookStartDate(); - const end = this.getBookEndDate(); if (item != null && start != null && end != null) { - ConnectionManager.getInstance() - .authenticatedRequest('location/booking', { - device: item.id, - begin: getISODate(start), - end: getISODate(end), - }) + request() .then(() => { - this.onDialogDismiss(); - props.navigation.replace('equipment-confirm', { - item: this.item, + onDialogDismiss(); + navigation.replace('equipment-confirm', { + item: item, dates: [getISODate(start), getISODate(end)], }); resolve(); }) .catch((error: ApiRejectType) => { - this.onDialogDismiss(); - this.showErrorDialog(error); + onDialogDismiss(); + setCurrentError(error); resolve(); }); } else { - this.onDialogDismiss(); + onDialogDismiss(); resolve(); } }); }; - getBookStartDate(): Date | null { - return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null; - } - - getBookEndDate(): Date | null { - const { length } = this.bookedDates; - return length > 0 ? new Date(this.bookedDates[length - 1]) : null; - } - /** * Selects a new date on the calendar. * If both start and end dates are already selected, unselect all. * * @param day The day selected */ - selectNewDate = (day: { + const selectNewDate = (day: { dateString: string; day: number; month: number; @@ -268,222 +240,196 @@ class EquipmentRentScreen extends React.Component { year: number; }) => { const selected = new Date(day.dateString); - const start = this.getBookStartDate(); - if (!this.lockedDates[day.dateString] != null) { + if (!lockedDates[day.dateString] != null) { if (start === null) { - this.updateSelectionRange(selected, selected); - this.enableBooking(); + updateSelectionRange(selected, selected); + enableBooking(); } else if (start.getTime() === selected.getTime()) { - this.resetSelection(); - } else if (this.bookedDates.length === 1) { - this.updateSelectionRange(start, selected); - this.enableBooking(); + resetSelection(); + } else if (bookedDates.current.length === 1) { + updateSelectionRange(start, selected); + enableBooking(); } else { - this.resetSelection(); + resetSelection(); } - this.updateMarkedSelection(); + updateMarkedSelection(); } }; - showErrorDialog = (error: ApiRejectType) => { - this.setState({ - errorDialogVisible: true, - currentError: error, - }); + const showBookButton = () => { + if (bookRef.current && bookRef.current.fadeInUp) { + bookRef.current.fadeInUp(500); + } }; - showDialog = () => { - this.setState({ dialogVisible: true }); + const hideBookButton = () => { + if (bookRef.current && bookRef.current.fadeOutDown) { + bookRef.current.fadeOutDown(500); + } }; - /** - * Shows the book button by plying a fade animation - */ - showBookButton() { - if (this.bookRef.current && this.bookRef.current.fadeInUp) { - this.bookRef.current.fadeInUp(500); + const enableBooking = () => { + if (!canBookEquipment.current) { + showBookButton(); + canBookEquipment.current = true; } - } + }; - /** - * Hides the book button by plying a fade animation - */ - hideBookButton() { - if (this.bookRef.current && this.bookRef.current.fadeOutDown) { - this.bookRef.current.fadeOutDown(500); + const resetSelection = () => { + if (canBookEquipment.current) { + hideBookButton(); } - } + canBookEquipment.current = false; + bookedDates.current = []; + }; - enableBooking() { - if (!this.canBookEquipment) { - this.showBookButton(); - this.canBookEquipment = true; - } - } - - resetSelection() { - if (this.canBookEquipment) { - this.hideBookButton(); - } - this.canBookEquipment = false; - this.bookedDates = []; - } - - updateSelectionRange(start: Date, end: Date) { - this.bookedDates = getValidRange(start, end, this.item); - } - - updateMarkedSelection() { - const { theme } = this.props; - this.setState({ - markedDates: generateMarkedDates(true, theme, this.bookedDates), - }); - } - - render() { - const { item, props, state } = this; - const start = this.getBookStartDate(); - const end = this.getBookEndDate(); - let subHeadingText; - if (start == null) { - subHeadingText = i18n.t('screens.equipment.booking'); - } else if (end != null && start.getTime() !== end.getTime()) { - subHeadingText = i18n.t('screens.equipment.bookingPeriod', { - begin: getRelativeDateString(start), - end: getRelativeDateString(end), - }); + const updateSelectionRange = (s: Date, e: Date) => { + if (item) { + bookedDates.current = getValidRange(s, e, item); } else { - subHeadingText = i18n.t('screens.equipment.bookingDay', { - date: getRelativeDateString(start), - }); + bookedDates.current = []; } - if (item != null) { - const isAvailable = isEquipmentAvailable(item); - const firstAvailability = getFirstEquipmentAvailability(item); - return ( - - - - - - - {item.name} - - ( - {i18n.t('screens.equipment.bail', { cost: item.caution })} - ) - - - + }; - - - {subHeadingText} - - - - - - + const updateMarkedSelection = () => { + setMarkedDates(generateMarkedDates(true, theme, bookedDates.current)); + }; - - - - - - ); - } - return null; + let subHeadingText; + + if (start == null) { + subHeadingText = i18n.t('screens.equipment.booking'); + } else if (end != null && start.getTime() !== end.getTime()) { + subHeadingText = i18n.t('screens.equipment.bookingPeriod', { + begin: getRelativeDateString(start), + end: getRelativeDateString(end), + }); + } else { + subHeadingText = i18n.t('screens.equipment.bookingDay', { + date: getRelativeDateString(start), + }); } + + if (item) { + const isAvailable = isEquipmentAvailable(item); + const firstAvailability = getFirstEquipmentAvailability(item); + return ( + + + + + + + {item.name} + + ({i18n.t('screens.equipment.bail', { cost: item.caution })}) + + + + + + {subHeadingText} + + + + + + + + + + + + ); + } + return null; } -export default withTheme(EquipmentRentScreen); +export default EquipmentRentScreen; diff --git a/src/screens/Amicale/LoginScreen.tsx b/src/screens/Amicale/LoginScreen.tsx index 8bf3185..cfcc7c8 100644 --- a/src/screens/Amicale/LoginScreen.tsx +++ b/src/screens/Amicale/LoginScreen.tsx @@ -17,19 +17,11 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; -import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native'; -import { - Button, - Card, - HelperText, - TextInput, - withTheme, -} from 'react-native-paper'; +import React, { useCallback, useState } from 'react'; +import { KeyboardAvoidingView, View } from 'react-native'; import i18n from 'i18n-js'; import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'; import LinearGradient from 'react-native-linear-gradient'; -import ConnectionManager from '../../managers/ConnectionManager'; import ErrorDialog from '../../components/Dialogs/ErrorDialog'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import MascotPopup from '../../components/Mascot/MascotPopup'; @@ -37,99 +29,32 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol import { MainStackParamsList } from '../../navigation/MainNavigator'; import GENERAL_STYLES from '../../constants/Styles'; import Urls from '../../constants/Urls'; -import { ApiRejectType } from '../../utils/WebData'; +import { ApiRejectType, connectToAmicale } from '../../utils/WebData'; import { REQUEST_STATUS } from '../../utils/Requests'; +import LoginForm from '../../components/Amicale/Login/LoginForm'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import { TabRoutes } from '../../navigation/TabNavigator'; +import { useShouldShowMascot } from '../../context/preferencesContext'; -type LoginScreenNavigationProp = StackScreenProps; +type Props = StackScreenProps; -type Props = LoginScreenNavigationProp & { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; -}; +function LoginScreen(props: Props) { + const navigation = useNavigation>(); + const [loading, setLoading] = useState(false); + const [nextScreen, setNextScreen] = useState(undefined); + const [mascotDialogVisible, setMascotDialogVisible] = useState(false); + const [currentError, setCurrentError] = useState({ + status: REQUEST_STATUS.SUCCESS, + }); + const homeMascot = useShouldShowMascot(TabRoutes.Home); -type StateType = { - email: string; - password: string; - isEmailValidated: boolean; - isPasswordValidated: boolean; - loading: boolean; - dialogVisible: boolean; - dialogError: ApiRejectType; - mascotDialogVisible: boolean | undefined; -}; + useFocusEffect( + useCallback(() => { + setNextScreen(props.route.params?.nextScreen); + }, [props.route.params]) + ); -const ICON_AMICALE = require('../../../assets/amicale.png'); - -const emailRegex = /^.+@.+\..+$/; - -const styles = StyleSheet.create({ - card: { - marginTop: 'auto', - marginBottom: 'auto', - }, - header: { - fontSize: 36, - marginBottom: 48, - }, - text: { - color: '#ffffff', - }, - buttonContainer: { - flexWrap: 'wrap', - }, - lockButton: { - marginRight: 'auto', - marginBottom: 20, - }, - sendButton: { - marginLeft: 'auto', - }, -}); - -class LoginScreen extends React.Component { - onEmailChange: (value: string) => void; - - onPasswordChange: (value: string) => void; - - passwordInputRef: { - // @ts-ignore - current: null | TextInput; - }; - - nextScreen: string | null; - - constructor(props: Props) { - super(props); - this.nextScreen = null; - this.passwordInputRef = React.createRef(); - this.onEmailChange = (value: string) => { - this.onInputChange(true, value); - }; - this.onPasswordChange = (value: string) => { - this.onInputChange(false, value); - }; - props.navigation.addListener('focus', this.onScreenFocus); - this.state = { - email: '', - password: '', - isEmailValidated: false, - isPasswordValidated: false, - loading: false, - dialogVisible: false, - dialogError: { status: REQUEST_STATUS.SUCCESS }, - mascotDialogVisible: undefined, - }; - } - - onScreenFocus = () => { - this.handleNavigationParams(); - }; - - /** - * Navigates to the Amicale website screen with the reset password link as navigation parameters - */ - onResetPasswordClick = () => { - const { navigation } = this.props; + const onResetPasswordClick = () => { navigation.navigate('website', { host: Urls.websites.amicale, path: Urls.amicale.resetPassword, @@ -137,38 +62,6 @@ class LoginScreen extends React.Component { }); }; - /** - * Called when the user input changes in the email or password field. - * This saves the new value in the State and disabled input validation (to prevent errors to show while typing) - * - * @param isEmail True if the field is the email field - * @param value The new field value - */ - onInputChange(isEmail: boolean, value: string) { - if (isEmail) { - this.setState({ - email: value, - isEmailValidated: false, - }); - } else { - this.setState({ - password: value, - isPasswordValidated: false, - }); - } - } - - /** - * Focuses the password field when the email field is done - * - * @returns {*} - */ - onEmailSubmit = () => { - if (this.passwordInputRef.current != null) { - this.passwordInputRef.current.focus(); - } - }; - /** * Called when the user clicks on login or finishes to type his password. * @@ -176,294 +69,84 @@ class LoginScreen extends React.Component { * then makes the login request and enters a loading state until the request finishes * */ - onSubmit = () => { - const { email, password } = this.state; - if (this.shouldEnableLogin()) { - this.setState({ loading: true }); - ConnectionManager.getInstance() - .connect(email, password) - .then(this.handleSuccess) - .catch(this.showErrorDialog) - .finally(() => { - this.setState({ loading: false }); - }); - } + const onSubmit = (email: string, password: string) => { + setLoading(true); + connectToAmicale(email, password) + .then(handleSuccess) + .catch(setCurrentError) + .finally(() => setLoading(false)); }; - /** - * Gets the form input - * - * @returns {*} - */ - getFormInput() { - const { email, password } = this.state; - return ( - - - - {i18n.t('screens.login.emailError')} - - - - {i18n.t('screens.login.passwordError')} - - - ); - } + const hideMascotDialog = () => setMascotDialogVisible(true); - /** - * Gets the card containing the input form - * @returns {*} - */ - getMainCard() { - const { props, state } = this; - return ( - - ( - - )} - /> - - {this.getFormInput()} - - - - - - - - - - ); - } + const showMascotDialog = () => setMascotDialogVisible(false); - /** - * The user has unfocused the input, his email is ready to be validated - */ - validateEmail = () => { - this.setState({ isEmailValidated: true }); - }; - - /** - * The user has unfocused the input, his password is ready to be validated - */ - validatePassword = () => { - this.setState({ isPasswordValidated: true }); - }; - - hideMascotDialog = () => { - this.setState({ mascotDialogVisible: false }); - }; - - showMascotDialog = () => { - this.setState({ mascotDialogVisible: true }); - }; - - /** - * Shows an error dialog with the corresponding login error - * - * @param error The error given by the login request - */ - showErrorDialog = (error: ApiRejectType) => { - console.log(error); - - this.setState({ - dialogVisible: true, - dialogError: error, - }); - }; - - hideErrorDialog = () => { - this.setState({ dialogVisible: false }); - }; + const hideErrorDialog = () => + setCurrentError({ status: REQUEST_STATUS.SUCCESS }); /** * Navigates to the screen specified in navigation parameters or simply go back tha stack. * Saves in user preferences to not show the login banner again. */ - handleSuccess = () => { - const { navigation } = this.props; + const handleSuccess = () => { // Do not show the home login banner again - // TODO - // AsyncStorageManager.set( - // AsyncStorageManager.PREFERENCES.homeShowMascot.key, - // false - // ); - if (this.nextScreen == null) { + if (homeMascot.shouldShow) { + homeMascot.setShouldShow(false); + } + if (!nextScreen) { navigation.goBack(); } else { - navigation.replace(this.nextScreen); + navigation.replace(nextScreen); } }; - /** - * Saves the screen to navigate to after a successful login if one was provided in navigation parameters - */ - handleNavigationParams() { - this.nextScreen = this.props.route.params?.nextScreen; - } - - /** - * Checks if the entered email is valid (matches the regex) - * - * @returns {boolean} - */ - isEmailValid(): boolean { - const { email } = this.state; - return emailRegex.test(email); - } - - /** - * Checks if we should tell the user his email is invalid. - * We should only show this if his email is invalid and has been checked when un-focusing the input - * - * @returns {boolean|boolean} - */ - shouldShowEmailError(): boolean { - const { isEmailValidated } = this.state; - return isEmailValidated && !this.isEmailValid(); - } - - /** - * Checks if the user has entered a password - * - * @returns {boolean} - */ - isPasswordValid(): boolean { - const { password } = this.state; - return password !== ''; - } - - /** - * Checks if we should tell the user his password is invalid. - * We should only show this if his password is invalid and has been checked when un-focusing the input - * - * @returns {boolean|boolean} - */ - shouldShowPasswordError(): boolean { - const { isPasswordValidated } = this.state; - return isPasswordValidated && !this.isPasswordValid(); - } - - /** - * If the email and password are valid, and we are not loading a request, then the login button can be enabled - * - * @returns {boolean} - */ - shouldEnableLogin(): boolean { - const { loading } = this.state; - return this.isEmailValid() && this.isPasswordValid() && !loading; - } - - render() { - const { mascotDialogVisible, dialogVisible, dialogError } = this.state; - return ( - + - - - {this.getMainCard()} - + + - - - - - ); - } + + + + + + + ); } -export default withTheme(LoginScreen); +export default LoginScreen; diff --git a/src/screens/Amicale/ProfileScreen.tsx b/src/screens/Amicale/ProfileScreen.tsx index ed7004d..fa3a57c 100644 --- a/src/screens/Amicale/ProfileScreen.tsx +++ b/src/screens/Amicale/ProfileScreen.tsx @@ -17,52 +17,29 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; -import { FlatList, StyleSheet, View } from 'react-native'; -import { - Avatar, - Button, - Card, - Divider, - List, - Paragraph, - withTheme, -} from 'react-native-paper'; -import i18n from 'i18n-js'; -import { StackNavigationProp } from '@react-navigation/stack'; +import React, { useLayoutEffect, useState } from 'react'; +import { View } from 'react-native'; import LogoutDialog from '../../components/Amicale/LogoutDialog'; import MaterialHeaderButtons, { Item, } from '../../components/Overrides/CustomHeaderButton'; -import CardList from '../../components/Lists/CardList/CardList'; -import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import GENERAL_STYLES from '../../constants/Styles'; -import Urls from '../../constants/Urls'; import RequestScreen from '../../components/Screens/RequestScreen'; -import ConnectionManager from '../../managers/ConnectionManager'; -import { - getAmicaleServices, - ServiceItemType, - SERVICES_KEY, -} from '../../utils/Services'; +import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard'; +import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard'; +import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard'; +import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard'; +import { useNavigation } from '@react-navigation/core'; +import { useAuthenticatedRequest } from '../../context/loginContext'; -type PropsType = { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; -}; - -type StateType = { - dialogVisible: boolean; -}; - -type ClubType = { +export type ProfileClubType = { id: number; name: string; is_manager: boolean; }; -type ProfileDataType = { +export type ProfileDataType = { first_name: string; last_name: string; email: string; @@ -71,87 +48,68 @@ type ProfileDataType = { branch: string; link: string; validity: boolean; - clubs: Array; + clubs: Array; }; -const styles = StyleSheet.create({ - card: { - margin: 10, - }, - icon: { - backgroundColor: 'transparent', - }, - editButton: { - marginLeft: 'auto', - }, - mascot: { - width: 60, - }, - title: { - marginLeft: 10, - }, -}); +function ProfileScreen() { + const navigation = useNavigation(); + const [dialogVisible, setDialogVisible] = useState(false); + const request = useAuthenticatedRequest('user/profile'); -class ProfileScreen extends React.Component { - data: ProfileDataType | undefined; - - flatListData: Array<{ id: string }>; - - amicaleDataset: Array; - - constructor(props: PropsType) { - super(props); - this.data = undefined; - this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }]; - this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [ - SERVICES_KEY.PROFILE, - ]); - this.state = { - dialogVisible: false, - }; - } - - componentDidMount() { - const { navigation } = this.props; + useLayoutEffect(() => { + const getHeaderButton = () => ( + + + + ); navigation.setOptions({ - headerRight: this.getHeaderButton, + headerRight: getHeaderButton, }); - } + }, [navigation]); - /** - * Gets the logout header button - * - * @returns {*} - */ - getHeaderButton = () => ( - - - - ); - - /** - * Gets the main screen component with the fetched data - * - * @param data The data fetched from the server - * @returns {*} - */ - getScreen = (data: ProfileDataType | undefined) => { - const { dialogVisible } = this.state; + const getScreen = (data: ProfileDataType | undefined) => { if (data) { - this.data = data; + const flatListData: Array<{ + id: string; + render: () => React.ReactElement; + }> = []; + for (let i = 0; i < 4; i++) { + switch (i) { + case 0: + flatListData.push({ + id: i.toString(), + render: () => , + }); + break; + case 1: + flatListData.push({ + id: i.toString(), + render: () => , + }); + break; + case 2: + flatListData.push({ + id: i.toString(), + render: () => , + }); + break; + default: + flatListData.push({ + id: i.toString(), + render: () => , + }); + } + } return ( - + ); @@ -160,346 +118,17 @@ class ProfileScreen extends React.Component { } }; - getRenderItem = ({ item }: { item: { id: string } }) => { - switch (item.id) { - case '0': - return this.getWelcomeCard(); - case '1': - return this.getPersonalCard(); - case '2': - return this.getClubCard(); - default: - return this.getMembershipCar(); - } - }; + const getRenderItem = ({ + item, + }: { + item: { id: string; render: () => React.ReactElement }; + }) => item.render(); - /** - * Gets the list of services available with the Amicale account - * - * @returns {*} - */ - getServicesList() { - return ; - } + const showDisconnectDialog = () => setDialogVisible(true); - /** - * Gets a card welcoming the user to his account - * - * @returns {*} - */ - getWelcomeCard() { - const { navigation } = this.props; - return ( - - ( - - )} - titleStyle={styles.title} - /> - - - {i18n.t('screens.profile.welcomeDescription')} - {this.getServicesList()} - {i18n.t('screens.profile.welcomeFeedback')} - - - - - - - ); - } + const hideDisconnectDialog = () => setDialogVisible(false); - /** - * Gets the given field value. - * If the field does not have a value, returns a placeholder text - * - * @param field The field to get the value from - * @return {*} - */ - static getFieldValue(field?: string): string { - return field ? field : i18n.t('screens.profile.noData'); - } - - /** - * Gets a list item showing personal information - * - * @param field The field to display - * @param icon The icon to use - * @return {*} - */ - getPersonalListItem(field: string | undefined, icon: string) { - const { theme } = this.props; - const title = field != null ? ProfileScreen.getFieldValue(field) : ':('; - const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field); - return ( - ( - - )} - /> - ); - } - - /** - * Gets a card containing user personal information - * - * @return {*} - */ - getPersonalCard() { - const { theme, navigation } = this.props; - return ( - - ( - - )} - /> - - - - - {i18n.t('screens.profile.personalInformation')} - - {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')} - - - - - - - - ); - } - - /** - * Gets a cars containing clubs the user is part of - * - * @return {*} - */ - getClubCard() { - const { theme } = this.props; - return ( - - ( - - )} - /> - - - {this.getClubList(this.data?.clubs)} - - - ); - } - - /** - * Gets a card showing if the user has payed his membership - * - * @return {*} - */ - getMembershipCar() { - const { theme } = this.props; - return ( - - ( - - )} - /> - - - {this.getMembershipItem(this.data?.validity === true)} - - - - ); - } - - /** - * Gets the item showing if the user has payed his membership - * - * @return {*} - */ - getMembershipItem(state: boolean) { - const { theme } = this.props; - return ( - ( - - )} - /> - ); - } - - /** - * 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; - }; - }) => ( - - ); - if (item.is_manager) { - description = i18n.t('screens.profile.isManager'); - icon = (props) => ( - - ); - } - return ( - - ); - }; - - /** - * Renders the list of clubs the user is part of - * - * @param list The club list - * @return {*} - */ - getClubList(list: Array | undefined) { - if (!list) { - return null; - } - - list.sort(this.sortClubList); - return ( - - ); - } - - 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 ( - - request={() => - ConnectionManager.getInstance().authenticatedRequest('user/profile') - } - render={this.getScreen} - /> - ); - } + return ; } -export default withTheme(ProfileScreen); +export default ProfileScreen; diff --git a/src/screens/Amicale/VoteScreen.tsx b/src/screens/Amicale/VoteScreen.tsx index 8309911..49a0686 100644 --- a/src/screens/Amicale/VoteScreen.tsx +++ b/src/screens/Amicale/VoteScreen.tsx @@ -17,7 +17,7 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useRef, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import i18n from 'i18n-js'; import { Button } from 'react-native-paper'; @@ -30,10 +30,10 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import MascotPopup from '../../components/Mascot/MascotPopup'; import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable'; import GENERAL_STYLES from '../../constants/Styles'; -import ConnectionManager from '../../managers/ConnectionManager'; import WebSectionList, { SectionListDataType, } from '../../components/Screens/WebSectionList'; +import { useAuthenticatedRequest } from '../../context/loginContext'; export type VoteTeamType = { id: number; @@ -65,6 +65,13 @@ type ResponseType = { dates?: VoteDatesStringType; }; +type FlatlistType = { + teams: Array; + hasVoted: boolean; + datesString?: VoteDatesStringType; + dates?: VoteDatesObjectType; +}; + // const FAKE_DATE = { // "date_begin": "2020-08-19 15:50", // "date_end": "2020-08-19 15:50", @@ -113,13 +120,6 @@ type ResponseType = { // ], // }; -type PropsType = {}; - -type StateType = { - hasVoted: boolean; - mascotDialogVisible: boolean | undefined; -}; - const styles = StyleSheet.create({ button: { marginLeft: 'auto', @@ -131,38 +131,19 @@ const styles = StyleSheet.create({ /** * Screen displaying vote information and controls */ -export default class VoteScreen extends React.Component { - teams: Array; +export default function VoteScreen() { + const [hasVoted, setHasVoted] = useState(false); + const [mascotDialogVisible, setMascotDialogVisible] = useState(false); - hasVoted: boolean; - - datesString: undefined | VoteDatesStringType; - - dates: undefined | VoteDatesObjectType; - - today: Date; - - mainFlatListData: SectionListDataType<{ key: string }>; - - refreshData: () => void; - - constructor(props: PropsType) { - super(props); - this.teams = []; - this.datesString = undefined; - this.dates = undefined; - this.state = { - hasVoted: false, - mascotDialogVisible: undefined, - }; - this.hasVoted = false; - this.today = new Date(); - this.refreshData = () => undefined; - this.mainFlatListData = [ - { title: '', data: [{ key: 'main' }, { key: 'info' }] }, - ]; - } + const datesRequest = useAuthenticatedRequest( + 'elections/dates' + ); + const teamsRequest = useAuthenticatedRequest( + 'elections/teams' + ); + const today = new Date(); + const refresh = useRef<() => void | undefined>(); /** * Gets the string representation of the given date. * @@ -173,22 +154,26 @@ export default class VoteScreen extends React.Component { * @param dateString The string representation of the wanted date * @returns {string} */ - getDateString(date: Date, dateString: string): string { - if (this.today.getDate() === date.getDate()) { + const getDateString = (date: Date, dateString: string) => { + if (today.getDate() === date.getDate()) { const str = getTimeOnlyString(dateString); return str != null ? str : ''; } return dateString; - } + }; - getMainRenderItem = ({ item }: { item: { key: string } }) => { + const getMainRenderItem = ({ + item, + }: { + item: { key: string; data?: FlatlistType }; + }) => { if (item.key === 'info') { return (