diff --git a/package.json b/package.json index ea2f0bf..f159022 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,9 @@ "prettier" ], "rules": { + "no-undef": 0, + "no-shadow": "off", + "@typescript-eslint/no-shadow": ["error"], "prettier/prettier": [ "error", { diff --git a/src/components/Amicale/AuthenticatedScreen.tsx b/src/components/Amicale/AuthenticatedScreen.tsx index 6683b80..4232496 100644 --- a/src/components/Amicale/AuthenticatedScreen.tsx +++ b/src/components/Amicale/AuthenticatedScreen.tsx @@ -23,6 +23,7 @@ import ConnectionManager from '../../managers/ConnectionManager'; import { ERROR_TYPE } from '../../utils/WebData'; import ErrorView from '../Screens/ErrorView'; import BasicLoadingScreen from '../Screens/BasicLoadingScreen'; +import i18n from 'i18n-js'; type PropsType = { navigation: StackNavigationProp; @@ -151,11 +152,28 @@ class AuthenticatedScreen extends React.Component, StateType> { ); } - return ; + return ( + + ); } /** diff --git a/src/components/Lists/Proximo/ProximoListItem.tsx b/src/components/Lists/Proximo/ProximoListItem.tsx index 6924563..5d6ec49 100644 --- a/src/components/Lists/Proximo/ProximoListItem.tsx +++ b/src/components/Lists/Proximo/ProximoListItem.tsx @@ -22,6 +22,7 @@ import { Avatar, List, Text } from 'react-native-paper'; import i18n from 'i18n-js'; import type { ProximoArticleType } from '../../../screens/Services/Proximo/ProximoMainScreen'; import { StyleSheet } from 'react-native'; +import Urls from '../../../constants/Urls'; type PropsType = { onPress: () => void; @@ -43,6 +44,8 @@ const styles = StyleSheet.create({ }); function ProximoListItem(props: PropsType) { + // console.log(Urls.proximo.images + props.item.image); + return ( )} right={() => {props.item.price}€} diff --git a/src/components/Screens/ErrorView.tsx b/src/components/Screens/ErrorView.tsx index 129fd34..f71ff7f 100644 --- a/src/components/Screens/ErrorView.tsx +++ b/src/components/Screens/ErrorView.tsx @@ -18,28 +18,29 @@ */ import * as React from 'react'; -import { Button, Subheading, withTheme } from 'react-native-paper'; +import { Button, Subheading, useTheme } from 'react-native-paper'; import { StyleSheet, View } from 'react-native'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import i18n from 'i18n-js'; import * as Animatable from 'react-native-animatable'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { ERROR_TYPE } from '../../utils/WebData'; +import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests'; -type PropsType = { - navigation?: StackNavigationProp; - theme: ReactNativePaper.Theme; - route?: { name: string }; - onRefresh?: () => void; - errorCode?: number; +type Props = { + status?: Exclude; + code?: Exclude; icon?: string; message?: string; - showRetryButton?: boolean; + loading?: boolean; + button?: { + text: string; + icon: string; + onPress: () => void; + }; }; const styles = StyleSheet.create({ outer: { - height: '100%', + flex: 1, }, inner: { marginTop: 'auto', @@ -61,134 +62,96 @@ const styles = StyleSheet.create({ }, }); -class ErrorView extends React.PureComponent { - static defaultProps = { - onRefresh: () => {}, - errorCode: 0, - icon: '', +function getMessage(props: Props) { + let fullMessage = { message: '', - showRetryButton: true, + icon: '', }; - - message: string; - - icon: string; - - showLoginButton: boolean; - - constructor(props: PropsType) { - super(props); - this.icon = ''; - this.showLoginButton = false; - this.message = ''; - } - - getRetryButton() { - const { props } = this; - return ( - - ); - } - - getLoginButton() { - return ( - - ); - } - - goToLogin = () => { - const { props } = this; - if (props.navigation) { - props.navigation.navigate('login', { - screen: 'login', - params: { nextScreen: props.route ? props.route.name : undefined }, - }); + if (props.code === undefined) { + switch (props.status) { + case REQUEST_STATUS.BAD_INPUT: + fullMessage.message = i18n.t('errors.badInput'); + fullMessage.icon = 'alert-circle-outline'; + break; + case REQUEST_STATUS.FORBIDDEN: + fullMessage.message = i18n.t('errors.forbidden'); + fullMessage.icon = 'lock'; + break; + case REQUEST_STATUS.CONNECTION_ERROR: + fullMessage.message = i18n.t('errors.connectionError'); + fullMessage.icon = 'access-point-network-off'; + break; + case REQUEST_STATUS.SERVER_ERROR: + fullMessage.message = i18n.t('errors.serverError'); + fullMessage.icon = 'server-network-off'; + break; + default: + fullMessage.message = i18n.t('errors.unknown'); + fullMessage.icon = 'alert-circle-outline'; + break; } - }; - - generateMessage() { - const { props } = this; - this.showLoginButton = false; - if (props.errorCode !== 0) { - switch (props.errorCode) { - case ERROR_TYPE.BAD_CREDENTIALS: - this.message = i18n.t('errors.badCredentials'); - this.icon = 'account-alert-outline'; - break; - case ERROR_TYPE.BAD_TOKEN: - this.message = i18n.t('errors.badToken'); - this.icon = 'account-alert-outline'; - this.showLoginButton = true; - break; - case ERROR_TYPE.NO_CONSENT: - this.message = i18n.t('errors.noConsent'); - this.icon = 'account-remove-outline'; - break; - case ERROR_TYPE.TOKEN_SAVE: - this.message = i18n.t('errors.tokenSave'); - this.icon = 'alert-circle-outline'; - break; - case ERROR_TYPE.BAD_INPUT: - this.message = i18n.t('errors.badInput'); - this.icon = 'alert-circle-outline'; - break; - case ERROR_TYPE.FORBIDDEN: - this.message = i18n.t('errors.forbidden'); - this.icon = 'lock'; - break; - case ERROR_TYPE.CONNECTION_ERROR: - this.message = i18n.t('errors.connectionError'); - this.icon = 'access-point-network-off'; - break; - case ERROR_TYPE.SERVER_ERROR: - this.message = i18n.t('errors.serverError'); - this.icon = 'server-network-off'; - break; - default: - this.message = i18n.t('errors.unknown'); - this.icon = 'alert-circle-outline'; - break; - } - this.message += `\n\nCode ${ - props.errorCode != null ? props.errorCode : -1 - }`; - } else { - this.message = props.message != null ? props.message : ''; - this.icon = props.icon != null ? props.icon : ''; + } else { + switch (props.code) { + case REQUEST_CODES.BAD_CREDENTIALS: + fullMessage.message = i18n.t('errors.badCredentials'); + fullMessage.icon = 'account-alert-outline'; + break; + case REQUEST_CODES.BAD_TOKEN: + fullMessage.message = i18n.t('errors.badToken'); + fullMessage.icon = 'account-alert-outline'; + break; + case REQUEST_CODES.NO_CONSENT: + fullMessage.message = i18n.t('errors.noConsent'); + fullMessage.icon = 'account-remove-outline'; + break; + case REQUEST_CODES.TOKEN_SAVE: + fullMessage.message = i18n.t('errors.tokenSave'); + fullMessage.icon = 'alert-circle-outline'; + break; + case REQUEST_CODES.BAD_INPUT: + fullMessage.message = i18n.t('errors.badInput'); + fullMessage.icon = 'alert-circle-outline'; + break; + case REQUEST_CODES.FORBIDDEN: + fullMessage.message = i18n.t('errors.forbidden'); + fullMessage.icon = 'lock'; + break; + case REQUEST_CODES.CONNECTION_ERROR: + fullMessage.message = i18n.t('errors.connectionError'); + fullMessage.icon = 'access-point-network-off'; + break; + case REQUEST_CODES.SERVER_ERROR: + fullMessage.message = i18n.t('errors.serverError'); + fullMessage.icon = 'server-network-off'; + break; + default: + fullMessage.message = i18n.t('errors.unknown'); + fullMessage.icon = 'alert-circle-outline'; + break; } } - render() { - const { props } = this; - this.generateMessage(); - let button; - if (this.showLoginButton) { - button = this.getLoginButton(); - } else if (props.showRetryButton) { - button = this.getRetryButton(); - } else { - button = null; - } + fullMessage.message += `\n\nCode {${props.status}:${props.code}}`; + if (props.message != null) { + fullMessage.message = props.message; + } + if (props.icon != null) { + fullMessage.icon = props.icon; + } + return fullMessage; +} - return ( +function ErrorView(props: Props) { + const theme = useTheme(); + const fullMessage = getMessage(props); + const { button } = props; + + return ( + { - {this.message} + {fullMessage.message} - {button} + {button ? ( + + ) : null} - ); - } + + ); } -export default withTheme(ErrorView); +export default ErrorView; diff --git a/src/components/Screens/RequestScreen.tsx b/src/components/Screens/RequestScreen.tsx new file mode 100644 index 0000000..0f9c66f --- /dev/null +++ b/src/components/Screens/RequestScreen.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useRef } from 'react'; +import ErrorView from './ErrorView'; +import { useRequestLogic } from '../../utils/customHooks'; +import { useFocusEffect } from '@react-navigation/native'; +import BasicLoadingScreen from './BasicLoadingScreen'; +import i18n from 'i18n-js'; +import { REQUEST_STATUS } from '../../utils/Requests'; + +export type RequestScreenProps = { + request: () => Promise; + render: ( + data: T | undefined, + loading: boolean, + refreshData: (newRequest?: () => Promise) => void, + status: REQUEST_STATUS, + code: number | undefined + ) => React.ReactElement; + cache?: T; + onCacheUpdate?: (newCache: T) => void; + onMajorError?: (status: number, code?: number) => void; + showLoading?: boolean; + showError?: boolean; + refreshOnFocus?: boolean; + autoRefreshTime?: number; + refresh?: boolean; + onFinish?: () => void; +}; + +export type RequestProps = { + refreshData: () => void; + loading: boolean; +}; + +type Props = RequestScreenProps; + +const MIN_REFRESH_TIME = 5 * 1000; + +export default function RequestScreen(props: Props) { + const refreshInterval = useRef(); + const [loading, status, code, data, refreshData] = useRequestLogic( + () => props.request(), + props.cache, + props.onCacheUpdate, + props.refreshOnFocus, + MIN_REFRESH_TIME + ); + // Store last refresh prop value + const lastRefresh = useRef(false); + + useEffect(() => { + // Refresh data if refresh prop changed and we are not loading + if (props.refresh && !lastRefresh.current && !loading) { + refreshData(); + // Call finish callback if refresh prop was set and we finished loading + } else if (lastRefresh.current && !loading && props.onFinish) { + props.onFinish(); + } + // Update stored refresh prop value + if (props.refresh !== lastRefresh.current) { + lastRefresh.current = props.refresh === true; + } + }, [props, loading, refreshData]); + + useFocusEffect( + React.useCallback(() => { + if (!props.cache && props.refreshOnFocus !== false) { + refreshData(); + } + if (props.autoRefreshTime && props.autoRefreshTime > 0) { + refreshInterval.current = setInterval( + refreshData, + props.autoRefreshTime + ); + } + return () => { + if (refreshInterval.current) { + clearInterval(refreshInterval.current); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.cache, props.refreshOnFocus]) + ); + + // useEffect(() => { + // if (status === REQUEST_STATUS.BAD_TOKEN && props.onMajorError) { + // props.onMajorError(status, code); + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [status, code]); + + // if (status === REQUEST_STATUS.BAD_TOKEN && props.onMajorError) { + // return ; + // } else + if (data === undefined && loading && props.showLoading !== false) { + return ; + } else if ( + data === undefined && + status !== REQUEST_STATUS.SUCCESS && + props.showError !== false + ) { + return ( + refreshData(), + }} + /> + ); + } else { + return props.render(data, loading, refreshData, status, code); + } +} diff --git a/src/components/Screens/WebSectionList.tsx b/src/components/Screens/WebSectionList.tsx index f8c0fe9..282eaa0 100644 --- a/src/components/Screens/WebSectionList.tsx +++ b/src/components/Screens/WebSectionList.tsx @@ -17,25 +17,26 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useState } from 'react'; import i18n from 'i18n-js'; import { Snackbar } from 'react-native-paper'; import { + NativeScrollEvent, NativeSyntheticEvent, RefreshControl, SectionListData, + SectionListRenderItemInfo, StyleSheet, View, } from 'react-native'; import * as Animatable from 'react-native-animatable'; -import { Collapsible } from 'react-navigation-collapsible'; -import { StackNavigationProp } from '@react-navigation/stack'; import ErrorView from './ErrorView'; import BasicLoadingScreen from './BasicLoadingScreen'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; -import { ERROR_TYPE, readData } from '../../utils/WebData'; +import { ERROR_TYPE } from '../../utils/WebData'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import GENERAL_STYLES from '../../constants/Styles'; +import RequestScreen from './RequestScreen'; export type SectionListDataType = Array<{ title: string; @@ -44,39 +45,30 @@ export type SectionListDataType = Array<{ keyExtractor?: (data: ItemT) => string; }>; -type PropsType = { - navigation: StackNavigationProp; - fetchUrl: string; - autoRefreshTime: number; +type Props = { + request: () => Promise; refreshOnFocus: boolean; - renderItem: (data: { item: ItemT }) => React.ReactNode; + renderItem: (data: SectionListRenderItemInfo) => React.ReactNode; createDataset: ( - data: RawData | null, - isLoading?: boolean + data: RawData | undefined, + isLoading: boolean ) => SectionListDataType; - onScroll?: (event: NativeSyntheticEvent) => void; + onScroll?: (event: NativeSyntheticEvent) => void; showError?: boolean; itemHeight?: number | null; - updateData?: number; + autoRefreshTime?: number; + updateData?: number | string; renderListHeaderComponent?: ( - data: RawData | null + data?: RawData ) => React.ComponentType | React.ReactElement | null; renderSectionHeader?: ( data: { section: SectionListData }, - isLoading?: boolean + isLoading: boolean ) => React.ReactElement | null; stickyHeader?: boolean; }; -type StateType = { - refreshing: boolean; - fetchedData: RawData | null; - snackbarVisible: boolean; -}; - -const MIN_REFRESH_TIME = 5 * 1000; - const styles = StyleSheet.create({ container: { minHeight: '100%', @@ -85,131 +77,18 @@ const styles = StyleSheet.create({ /** * Component used to render a SectionList with data fetched from the web - * - * This is a pure component, meaning it will only update if a shallow comparison of state and props is different. * To force the component to update, change the value of updateData. */ -class WebSectionList extends React.PureComponent< - PropsType, - StateType -> { - static defaultProps = { - showError: true, - itemHeight: null, - updateData: 0, - renderListHeaderComponent: () => null, - renderSectionHeader: () => null, - stickyHeader: false, - }; +function WebSectionList(props: Props) { + const [snackbarVisible, setSnackbarVisible] = useState(false); - refreshInterval: NodeJS.Timeout | undefined; + const showSnackBar = () => setSnackbarVisible(true); - lastRefresh: Date | undefined; + const hideSnackBar = () => setSnackbarVisible(false); - constructor(props: PropsType) { - super(props); - this.state = { - refreshing: false, - fetchedData: null, - snackbarVisible: false, - }; - } - - /** - * Registers react navigation events on first screen load. - * Allows to detect when the screen is focused - */ - componentDidMount() { - const { navigation } = this.props; - navigation.addListener('focus', this.onScreenFocus); - navigation.addListener('blur', this.onScreenBlur); - this.lastRefresh = undefined; - this.onRefresh(); - } - - /** - * Refreshes data when focusing the screen and setup a refresh interval if asked to - */ - onScreenFocus = () => { - const { props } = this; - if (props.refreshOnFocus && this.lastRefresh) { - setTimeout(this.onRefresh, 200); - } - if (props.autoRefreshTime > 0) { - this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime); - } - }; - - /** - * Removes any interval on un-focus - */ - onScreenBlur = () => { - if (this.refreshInterval) { - clearInterval(this.refreshInterval); - } - }; - - /** - * Callback used when fetch is successful. - * It will update the displayed data and stop the refresh animation - * - * @param fetchedData The newly fetched data - */ - onFetchSuccess = (fetchedData: RawData) => { - this.setState({ - fetchedData, - refreshing: false, - }); - this.lastRefresh = new Date(); - }; - - /** - * Callback used when fetch encountered an error. - * It will reset the displayed data and show an error. - */ - onFetchError = () => { - this.setState({ - fetchedData: null, - refreshing: false, - }); - this.showSnackBar(); - }; - - /** - * Refreshes data and shows an animations while doing it - */ - onRefresh = () => { - const { fetchUrl } = this.props; - let canRefresh; - if (this.lastRefresh != null) { - const last = this.lastRefresh; - canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME; - } else { - canRefresh = true; - } - if (canRefresh) { - this.setState({ refreshing: true }); - readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError); - } - }; - - /** - * Shows the error popup - */ - showSnackBar = () => { - this.setState({ snackbarVisible: true }); - }; - - /** - * Hides the error popup - */ - hideSnackBar = () => { - this.setState({ snackbarVisible: false }); - }; - - getItemLayout = ( + const getItemLayout = ( height: number, - data: Array> | null, + _data: Array> | null, index: number ): { length: number; offset: number; index: number } => { return { @@ -219,105 +98,125 @@ class WebSectionList extends React.PureComponent< }; }; - getRenderSectionHeader = (data: { section: SectionListData }) => { - const { renderSectionHeader } = this.props; - const { refreshing } = this.state; - if (renderSectionHeader != null) { + const getRenderSectionHeader = ( + data: { section: SectionListData }, + loading: boolean + ) => { + const { renderSectionHeader } = props; + if (renderSectionHeader) { return ( - - {renderSectionHeader(data, refreshing)} + + {renderSectionHeader(data, loading)} ); } return null; }; - getRenderItem = (data: { item: ItemT }) => { - const { renderItem } = this.props; + const getRenderItem = (data: SectionListRenderItemInfo) => { + const { renderItem } = props; return ( - + {renderItem(data)} ); }; - onScroll = (event: NativeSyntheticEvent) => { - const { onScroll } = this.props; - if (onScroll != null) { - onScroll(event); + const onScroll = (event: NativeSyntheticEvent) => { + if (props.onScroll) { + props.onScroll(event); } }; - render() { - const { props, state } = this; + const render = ( + data: RawData | undefined, + loading: boolean, + refreshData: (newRequest?: () => Promise) => void + ) => { const { itemHeight } = props; - let dataset: SectionListDataType = []; - if ( - state.fetchedData != null || - (state.fetchedData == null && !props.showError) - ) { - dataset = props.createDataset(state.fetchedData, state.refreshing); + const dataset = props.createDataset(data, loading); + if (!data && !loading) { + showSnackBar(); } - return ( - - ({ - refreshControl: ( - - ), - })} - renderSectionHeader={this.getRenderSectionHeader} - renderItem={this.getRenderItem} - stickySectionHeadersEnabled={props.stickyHeader} - style={styles.container} - ListHeaderComponent={ - props.renderListHeaderComponent != null - ? props.renderListHeaderComponent(state.fetchedData) - : null - } - ListEmptyComponent={ - state.refreshing ? ( - - ) : ( - - ) - } - getItemLayout={ - itemHeight - ? (data, index) => this.getItemLayout(itemHeight, data, index) - : undefined - } - onScroll={this.onScroll} - hasTab={true} - /> - {}, - }} - duration={4000} - style={{ - bottom: TAB_BAR_HEIGHT, - }} - > - {i18n.t('general.listUpdateFail')} - - + ({ + refreshControl: ( + + ), + })} + renderSectionHeader={(info) => getRenderSectionHeader(info, loading)} + renderItem={getRenderItem} + stickySectionHeadersEnabled={props.stickyHeader} + style={styles.container} + ListHeaderComponent={ + props.renderListHeaderComponent != null + ? props.renderListHeaderComponent(data) + : null + } + ListEmptyComponent={ + loading ? ( + + ) : ( + + ) + } + getItemLayout={ + itemHeight ? (d, i) => getItemLayout(itemHeight, d, i) : undefined + } + onScroll={onScroll} + hasTab={true} + /> ); - } + }; + + return ( + + + request={props.request} + render={render} + showError={false} + showLoading={false} + autoRefreshTime={props.autoRefreshTime} + refreshOnFocus={props.refreshOnFocus} + /> + + {i18n.t('general.listUpdateFail')} + + + ); } export default WebSectionList; diff --git a/src/components/Screens/WebViewScreen.tsx b/src/components/Screens/WebViewScreen.tsx index 6e32ef3..04e3a83 100644 --- a/src/components/Screens/WebViewScreen.tsx +++ b/src/components/Screens/WebViewScreen.tsx @@ -251,8 +251,12 @@ function WebViewScreen(props: Props) { renderLoading={getRenderLoading} renderError={() => ( )} onNavigationStateChange={setNavState} diff --git a/src/constants/Urls.tsx b/src/constants/Urls.tsx index d2c9343..c133e2b 100644 --- a/src/constants/Urls.tsx +++ b/src/constants/Urls.tsx @@ -21,11 +21,14 @@ const STUDENT_SERVER = 'https://etud.insa-toulouse.fr/'; const AMICALE_SERVER = 'https://www.amicale-insat.fr/'; const GIT_SERVER = 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/'; +const PLANEX_SERVER = 'http://planex.insa-toulouse.fr/'; const AMICALE_ENDPOINT = AMICALE_SERVER + 'api/'; const APP_ENDPOINT = STUDENT_SERVER + '~amicale_app/v2/'; -const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/data/stock-v2.json'; +const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/v2/api/'; +const PROXIMO_IMAGES_ENDPOINT = + STUDENT_SERVER + '~proximo/v2/api-proximo/public/storage/app/'; const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/'; export default { @@ -39,7 +42,16 @@ export default { dashboard: APP_ENDPOINT + 'dashboard/dashboard_data.json', menu: APP_ENDPOINT + 'menu/menu_data.json', }, - proximo: PROXIMO_ENDPOINT, + proximo: { + articles: PROXIMO_ENDPOINT + 'articles', + categories: PROXIMO_ENDPOINT + 'categories', + images: PROXIMO_IMAGES_ENDPOINT + 'img/', + icons: PROXIMO_IMAGES_ENDPOINT + 'icon/', + }, + planex: { + planning: PLANEX_SERVER, + groups: PLANEX_SERVER + 'wsAdeGrp.php?projectId=1', + }, images: { proxiwash: APP_IMAGES_ENDPOINT + 'Proxiwash.png', washer: APP_IMAGES_ENDPOINT + 'ProxiwashLaveLinge.png', diff --git a/src/navigation/MainNavigator.tsx b/src/navigation/MainNavigator.tsx index 4f3d931..d881cab 100644 --- a/src/navigation/MainNavigator.tsx +++ b/src/navigation/MainNavigator.tsx @@ -84,6 +84,10 @@ export type FullParamsList = DefaultParams & { }; 'equipment-rent': { item?: DeviceType }; 'gallery': { images: Array<{ url: string }> }; + [MainRoutes.ProximoList]: { + shouldFocusSearchBar: boolean; + category: number; + }; }; // Don't know why but TS is complaining without this diff --git a/src/screens/Home/HomeScreen.tsx b/src/screens/Home/HomeScreen.tsx index 9375b8f..0d2c7c4 100644 --- a/src/screens/Home/HomeScreen.tsx +++ b/src/screens/Home/HomeScreen.tsx @@ -22,6 +22,7 @@ import { FlatList, NativeScrollEvent, NativeSyntheticEvent, + SectionListData, StyleSheet, } from 'react-native'; import i18n from 'i18n-js'; @@ -52,6 +53,7 @@ import { getDisplayEvent, getFutureEvents } from '../../utils/Home'; import type { PlanningEventType } from '../../utils/Planning'; import GENERAL_STYLES from '../../constants/Styles'; import Urls from '../../constants/Urls'; +import { readData } from '../../utils/WebData'; const FEED_ITEM_HEIGHT = 500; @@ -314,12 +316,7 @@ class HomeScreen extends React.Component { getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item); getRenderSectionHeader = ( - data: { - section: { - data: Array; - title: string; - }; - }, + data: { section: SectionListData }, isLoading: boolean ) => { const { props } = this; @@ -352,7 +349,7 @@ class HomeScreen extends React.Component { ); }; - getListHeader = (fetchedData: RawDashboardType) => { + getListHeader = (fetchedData: RawDashboardType | undefined) => { let dashboard = null; if (fetchedData != null) { dashboard = fetchedData.dashboard; @@ -404,21 +401,20 @@ class HomeScreen extends React.Component { * @return {*} */ createDataset = ( - fetchedData: RawDashboardType | null, + fetchedData: RawDashboardType | undefined, isLoading: boolean ): Array<{ title: string; data: [] | Array; id: string; }> => { - // fetchedData = DATA; - if (fetchedData != null) { - if (fetchedData.news_feed != null) { + if (fetchedData) { + if (fetchedData.news_feed) { this.currentNewFeed = HomeScreen.generateNewsFeed( fetchedData.news_feed ); } - if (fetchedData.dashboard != null) { + if (fetchedData.dashboard) { this.currentDashboard = fetchedData.dashboard; } } @@ -470,11 +466,10 @@ class HomeScreen extends React.Component { readData(Urls.app.dashboard)} createDataset={this.createDataset} autoRefreshTime={REFRESH_TIME} - refreshOnFocus - fetchUrl={Urls.app.dashboard} + refreshOnFocus={true} renderItem={this.getRenderItem} itemHeight={FEED_ITEM_HEIGHT} onScroll={this.onScroll} diff --git a/src/screens/Planex/GroupSelectionScreen.tsx b/src/screens/Planex/GroupSelectionScreen.tsx index fa691b9..e5b3596 100644 --- a/src/screens/Planex/GroupSelectionScreen.tsx +++ b/src/screens/Planex/GroupSelectionScreen.tsx @@ -26,6 +26,8 @@ import { stringMatchQuery } from '../../utils/Search'; import WebSectionList from '../../components/Screens/WebSectionList'; import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion'; import AsyncStorageManager from '../../managers/AsyncStorageManager'; +import Urls from '../../constants/Urls'; +import { readData } from '../../utils/WebData'; export type PlanexGroupType = { name: string; @@ -60,8 +62,6 @@ function sortName( return 0; } -const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1'; - /** * Class defining planex group selection screen. */ @@ -137,9 +137,13 @@ class GroupSelectionScreen extends React.Component { * @param fetchedData * @return {*} * */ - createDataset = (fetchedData: { - [key: string]: PlanexGroupCategoryType; - }): Array<{ title: string; data: Array }> => { + createDataset = ( + fetchedData: + | { + [key: string]: PlanexGroupCategoryType; + } + | undefined + ): Array<{ title: string; data: Array }> => { return [ { title: '', @@ -236,20 +240,28 @@ class GroupSelectionScreen extends React.Component { * @param fetchedData The raw data fetched from the server * @returns {[]} */ - generateData(fetchedData: { - [key: string]: PlanexGroupCategoryType; - }): Array { + generateData( + fetchedData: + | { + [key: string]: PlanexGroupCategoryType; + } + | undefined + ): Array { const { favoriteGroups } = this.state; const data: Array = []; - Object.values(fetchedData).forEach((category: PlanexGroupCategoryType) => { - data.push(category); - }); - data.sort(sortName); - data.unshift({ - name: i18n.t('screens.planex.favorites'), - id: 0, - content: favoriteGroups, - }); + if (fetchedData) { + Object.values(fetchedData).forEach( + (category: PlanexGroupCategoryType) => { + data.push(category); + } + ); + data.sort(sortName); + data.unshift({ + name: i18n.t('screens.planex.favorites'), + id: 0, + content: favoriteGroups, + }); + } return data; } @@ -298,14 +310,16 @@ class GroupSelectionScreen extends React.Component { } render() { - const { props, state } = this; + const { state } = this; return ( + readData<{ [key: string]: PlanexGroupCategoryType }>( + Urls.planex.groups + ) + } createDataset={this.createDataset} - autoRefreshTime={0} - refreshOnFocus={false} - fetchUrl={GROUPS_URL} + refreshOnFocus={true} renderItem={this.getRenderItem} updateData={state.currentSearchString + state.favoriteGroups.length} /> diff --git a/src/screens/Planex/PlanexScreen.tsx b/src/screens/Planex/PlanexScreen.tsx index 8828864..14f5048 100644 --- a/src/screens/Planex/PlanexScreen.tsx +++ b/src/screens/Planex/PlanexScreen.tsx @@ -42,6 +42,7 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import MascotPopup from '../../components/Mascot/MascotPopup'; import { getPrettierPlanexGroupName } from '../../utils/Utils'; import GENERAL_STYLES from '../../constants/Styles'; +import Urls from '../../constants/Urls'; type PropsType = { navigation: StackNavigationProp; @@ -57,8 +58,6 @@ type StateType = { injectJS: string; }; -const PLANEX_URL = 'http://planex.insa-toulouse.fr/'; - // // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing // // Remove alpha from given Jquery node // function removeAlpha(node) { @@ -197,22 +196,19 @@ class PlanexScreen extends React.Component { * @returns {*} */ getWebView() { - const { props, state } = this; + const { state } = this; const showWebview = state.currentGroup.id !== -1; - console.log(state.injectJS); return ( {!showWebview ? ( ) : null} { * @returns {*} */ getErrorView() { - const { navigation } = this.props; if (this.errorCode === ERROR_TYPE.BAD_INPUT) { return ( @@ -176,9 +171,12 @@ class PlanningDisplayScreen extends React.Component { } return ( ); } diff --git a/src/screens/Proxiwash/ProxiwashScreen.tsx b/src/screens/Proxiwash/ProxiwashScreen.tsx index a0a8407..3296895 100644 --- a/src/screens/Proxiwash/ProxiwashScreen.tsx +++ b/src/screens/Proxiwash/ProxiwashScreen.tsx @@ -18,7 +18,13 @@ */ import * as React from 'react'; -import { Alert, StyleSheet, View } from 'react-native'; +import { + Alert, + SectionListData, + SectionListRenderItemInfo, + StyleSheet, + View, +} from 'react-native'; import i18n from 'i18n-js'; import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper'; import { StackNavigationProp } from '@react-navigation/stack'; @@ -46,6 +52,7 @@ import MascotPopup from '../../components/Mascot/MascotPopup'; import type { SectionListDataType } from '../../components/Screens/WebSectionList'; import type { LaundromatType } from './ProxiwashAboutScreen'; import GENERAL_STYLES from '../../constants/Styles'; +import { readData } from '../../utils/WebData'; const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const LIST_ITEM_HEIGHT = 64; @@ -72,6 +79,11 @@ type StateType = { selectedWash: string; }; +type FetchedDataType = { + dryers: Array; + washers: Array; +}; + const styles = StyleSheet.create({ modalContainer: { flex: 1, @@ -277,7 +289,11 @@ class ProxiwashScreen extends React.Component { * @param section The section to render * @return {*} */ - getRenderSectionHeader = ({ section }: { section: { title: string } }) => { + getRenderSectionHeader = ({ + section, + }: { + section: SectionListData; + }) => { const isDryer = section.title === i18n.t('screens.proxiwash.dryers'); const nbAvailable = this.getMachineAvailableNumber(isDryer); return ( @@ -296,20 +312,14 @@ class ProxiwashScreen extends React.Component { * @param section The object describing the current SectionList section * @returns {React.Node} */ - getRenderItem = ({ - item, - section, - }: { - item: ProxiwashMachineType; - section: { title: string }; - }) => { + getRenderItem = (data: SectionListRenderItemInfo) => { const { machinesWatched } = this.state; - const isDryer = section.title === i18n.t('screens.proxiwash.dryers'); + const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers'); return ( @@ -382,37 +392,40 @@ class ProxiwashScreen extends React.Component { * @param fetchedData * @return {*} */ - createDataset = (fetchedData: { - dryers: Array; - washers: Array; - }): SectionListDataType => { + createDataset = ( + fetchedData: FetchedDataType | undefined + ): SectionListDataType => { const { state } = this; - let data = fetchedData; - if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { - data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy - AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers); - AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers); + if (fetchedData) { + let data = fetchedData; + if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { + data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy + AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers); + AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers); + } + this.fetchedData = data; + // TODO dirty, should be refactored + this.state.machinesWatched = getCleanedMachineWatched( + state.machinesWatched, + [...data.dryers, ...data.washers] + ); + return [ + { + title: i18n.t('screens.proxiwash.dryers'), + icon: 'tumble-dryer', + data: data.dryers === undefined ? [] : data.dryers, + keyExtractor: this.getKeyExtractor, + }, + { + title: i18n.t('screens.proxiwash.washers'), + icon: 'washing-machine', + data: data.washers === undefined ? [] : data.washers, + keyExtractor: this.getKeyExtractor, + }, + ]; + } else { + return []; } - this.fetchedData = data; - // TODO dirty, should be refactored - this.state.machinesWatched = getCleanedMachineWatched( - state.machinesWatched, - [...data.dryers, ...data.washers] - ); - return [ - { - title: i18n.t('screens.proxiwash.dryers'), - icon: 'tumble-dryer', - data: data.dryers === undefined ? [] : data.dryers, - keyExtractor: this.getKeyExtractor, - }, - { - title: i18n.t('screens.proxiwash.washers'), - icon: 'washing-machine', - data: data.washers === undefined ? [] : data.washers, - keyExtractor: this.getKeyExtractor, - }, - ]; }; /** @@ -481,7 +494,6 @@ class ProxiwashScreen extends React.Component { render() { const { state } = this; - const { navigation } = this.props; let data: LaundromatType; switch (state.selectedWash) { case 'tripodeB': @@ -494,13 +506,12 @@ class ProxiwashScreen extends React.Component { readData(data.url)} createDataset={this.createDataset} - navigation={navigation} - fetchUrl={data.url} renderItem={this.getRenderItem} renderSectionHeader={this.getRenderSectionHeader} autoRefreshTime={REFRESH_TIME} - refreshOnFocus + refreshOnFocus={true} updateData={state.machinesWatched.length} /> diff --git a/src/screens/Services/Proximo/ProximoListScreen.tsx b/src/screens/Services/Proximo/ProximoListScreen.tsx index 86e57b0..eec4662 100644 --- a/src/screens/Services/Proximo/ProximoListScreen.tsx +++ b/src/screens/Services/Proximo/ProximoListScreen.tsx @@ -17,7 +17,7 @@ * along with Campus INSAT. If not, see . */ -import * as React from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; import { Image, Platform, ScrollView, StyleSheet, View } from 'react-native'; import i18n from 'i18n-js'; import { @@ -26,9 +26,8 @@ import { Subheading, Text, Title, - withTheme, + useTheme, } from 'react-native-paper'; -import { StackNavigationProp } from '@react-navigation/stack'; import { Modalize } from 'react-native-modalize'; import CustomModal from '../../../components/Overrides/CustomModal'; import { stringMatchQuery } from '../../../utils/Search'; @@ -36,19 +35,29 @@ import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem'; import MaterialHeaderButtons, { Item, } from '../../../components/Overrides/CustomHeaderButton'; -import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList'; import type { ProximoArticleType } from './ProximoMainScreen'; import GENERAL_STYLES from '../../../constants/Styles'; +import { useNavigation } from '@react-navigation/core'; +import Urls from '../../../constants/Urls'; +import WebSectionList, { + SectionListDataType, +} from '../../../components/Screens/WebSectionList'; +import { readData } from '../../../utils/WebData'; +import { StackScreenProps } from '@react-navigation/stack'; +import { + MainRoutes, + MainStackParamsList, +} from '../../../navigation/MainNavigator'; function sortPrice(a: ProximoArticleType, b: ProximoArticleType): number { - return parseInt(a.price, 10) - parseInt(b.price, 10); + return a.price - b.price; } function sortPriceReverse( a: ProximoArticleType, b: ProximoArticleType ): number { - return parseInt(b.price, 10) - parseInt(a.price, 10); + return b.price - a.price; } function sortName(a: ProximoArticleType, b: ProximoArticleType): number { @@ -73,23 +82,6 @@ function sortNameReverse(a: ProximoArticleType, b: ProximoArticleType): number { const LIST_ITEM_HEIGHT = 84; -type PropsType = { - navigation: StackNavigationProp; - route: { - params: { - data: { data: Array }; - shouldFocusSearchBar: boolean; - }; - }; - theme: ReactNativePaper.Theme; -}; - -type StateType = { - currentSortMode: number; - modalCurrentDisplayItem: React.ReactNode; - currentSearchString: string; -}; - const styles = StyleSheet.create({ modalContainer: { flex: 1, @@ -118,113 +110,72 @@ const styles = StyleSheet.create({ }, }); -/** - * Class defining Proximo article list of a certain category. - */ -class ProximoListScreen extends React.Component { - modalRef: Modalize | null; +type ArticlesType = Array; - listData: Array; +type Props = StackScreenProps; - shouldFocusSearchBar: boolean; +function ProximoListScreen(props: Props) { + const navigation = useNavigation(); + const theme = useTheme(); + const modalRef = useRef(); - constructor(props: PropsType) { - super(props); - this.modalRef = null; - this.listData = props.route.params.data.data.sort(sortName); - this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar; - this.state = { - currentSearchString: '', - currentSortMode: 3, - modalCurrentDisplayItem: null, - }; - } + const [currentSearchString, setCurrentSearchString] = useState(''); + const [currentSortMode, setCurrentSortMode] = useState(2); + const [modalCurrentDisplayItem, setModalCurrentDisplayItem] = useState< + React.ReactNode | undefined + >(); - /** - * Creates the header content - */ - componentDidMount() { - const { navigation } = this.props; + const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse]; + + useLayoutEffect(() => { navigation.setOptions({ - headerRight: this.getSortMenuButton, - headerTitle: this.getSearchBar, + headerRight: getSortMenuButton, + headerTitle: getSearchBar, 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, currentSortMode]); /** * Callback used when clicking on the sort menu button. * It will open the modal to show a sort selection */ - onSortMenuPress = () => { - this.setState({ - modalCurrentDisplayItem: this.getModalSortMenu(), - }); - if (this.modalRef) { - this.modalRef.open(); + const onSortMenuPress = () => { + setModalCurrentDisplayItem(getModalSortMenu()); + if (modalRef.current) { + modalRef.current.open(); } }; - /** - * Callback used when the search changes - * - * @param str The new search string - */ - onSearchStringChange = (str: string) => { - this.setState({ currentSearchString: str }); - }; - /** * Callback used when clicking an article in the list. * It opens the modal to show detailed information about the article * * @param item The article pressed */ - onListItemPress(item: ProximoArticleType) { - this.setState({ - modalCurrentDisplayItem: this.getModalItemContent(item), - }); - if (this.modalRef) { - this.modalRef.open(); + const onListItemPress = (item: ProximoArticleType) => { + setModalCurrentDisplayItem(getModalItemContent(item)); + if (modalRef.current) { + modalRef.current.open(); } - } + }; /** * Sets the current sort mode. * * @param mode The number representing the mode */ - setSortMode(mode: string) { - const { currentSortMode } = this.state; + const setSortMode = (mode: string) => { const currentMode = parseInt(mode, 10); - this.setState({ - currentSortMode: currentMode, - }); - switch (currentMode) { - case 1: - this.listData.sort(sortPrice); - break; - case 2: - this.listData.sort(sortPriceReverse); - break; - case 3: - this.listData.sort(sortName); - break; - case 4: - this.listData.sort(sortNameReverse); - break; - default: - this.listData.sort(sortName); - break; + setCurrentSortMode(currentMode); + if (modalRef.current && currentMode !== currentSortMode) { + modalRef.current.close(); } - if (this.modalRef && currentMode !== currentSortMode) { - this.modalRef.close(); - } - } + }; /** * Gets a color depending on the quantity available @@ -232,8 +183,7 @@ class ProximoListScreen extends React.Component { * @param availableStock The quantity available * @return */ - getStockColor(availableStock: number): string { - const { theme } = this.props; + const getStockColor = (availableStock: number): string => { let color: string; if (availableStock > 3) { color = theme.colors.success; @@ -243,17 +193,17 @@ class ProximoListScreen extends React.Component { color = theme.colors.danger; } return color; - } + }; /** * Gets the sort menu header button * * @return {*} */ - getSortMenuButton = () => { + const getSortMenuButton = () => { return ( - + ); }; @@ -263,12 +213,13 @@ class ProximoListScreen extends React.Component { * * @return {*} */ - getSearchBar = () => { + const getSearchBar = () => { return ( // @ts-ignore ); }; @@ -279,14 +230,14 @@ class ProximoListScreen extends React.Component { * @param item The article to display * @return {*} */ - getModalItemContent(item: ProximoArticleType) { + const getModalItemContent = (item: ProximoArticleType) => { return ( {item.name} {`${item.quantity} ${i18n.t('screens.proximo.inStock')}`} @@ -302,46 +253,43 @@ class ProximoListScreen extends React.Component { ); - } + }; /** * Gets the modal content to display a sort menu * * @return {*} */ - getModalSortMenu() { - const { currentSortMode } = this.state; + const getModalSortMenu = () => { return ( {i18n.t('screens.proximo.sortOrder')} { - this.setSortMode(value); - }} + onValueChange={setSortMode} value={currentSortMode.toString()} > ); - } + }; /** * Gets a render item for the given article @@ -349,13 +297,12 @@ class ProximoListScreen extends React.Component { * @param item The article to render * @return {*} */ - getRenderItem = ({ item }: { item: ProximoArticleType }) => { - const { currentSearchString } = this.state; + const getRenderItem = ({ item }: { item: ProximoArticleType }) => { if (stringMatchQuery(item.name, currentSearchString)) { const onPress = () => { - this.onListItemPress(item); + onListItemPress(item); }; - const color = this.getStockColor(parseInt(item.quantity, 10)); + const color = getStockColor(item.quantity); return ( { * @param item The article to extract the key from * @return {string} The extracted key */ - keyExtractor = (item: ProximoArticleType): string => item.name + item.code; + const keyExtractor = (item: ProximoArticleType): string => + item.name + item.code; - /** - * Callback used when receiving the modal ref - * - * @param ref - */ - onModalRef = (ref: Modalize) => { - this.modalRef = ref; + const createDataset = ( + data: ArticlesType | undefined + ): SectionListDataType => { + if (data) { + console.log(data); + console.log(props.route.params.category); + + return [ + { + title: '', + data: data + .filter( + (d) => + props.route.params.category === -1 || + props.route.params.category === d.category_id + ) + .sort(sortModes[currentSortMode]), + keyExtractor: keyExtractor, + }, + ]; + } else { + return [ + { + title: '', + data: [], + keyExtractor: keyExtractor, + }, + ]; + } }; - itemLayout = ( - data: Array | null | undefined, - index: number - ): { length: number; offset: number; index: number } => ({ - length: LIST_ITEM_HEIGHT, - offset: LIST_ITEM_HEIGHT * index, - index, - }); - - render() { - const { state } = this; - return ( - - - {state.modalCurrentDisplayItem} - - - - ); - } + return ( + + (modalRef.current = ref)}> + {modalCurrentDisplayItem} + + readData(Urls.proximo.articles)} + createDataset={createDataset} + refreshOnFocus={true} + renderItem={getRenderItem} + updateData={currentSearchString + currentSortMode} + itemHeight={LIST_ITEM_HEIGHT} + /> + + ); } -export default withTheme(ProximoListScreen); +export default ProximoListScreen; diff --git a/src/screens/Services/Proximo/ProximoMainScreen.tsx b/src/screens/Services/Proximo/ProximoMainScreen.tsx index a1bd6a7..1240530 100644 --- a/src/screens/Services/Proximo/ProximoMainScreen.tsx +++ b/src/screens/Services/Proximo/ProximoMainScreen.tsx @@ -19,8 +19,7 @@ import * as React from 'react'; import i18n from 'i18n-js'; -import { List, withTheme } from 'react-native-paper'; -import { StackNavigationProp } from '@react-navigation/stack'; +import { Avatar, List, useTheme, withTheme } from 'react-native-paper'; import WebSectionList from '../../../components/Screens/WebSectionList'; import MaterialHeaderButtons, { Item, @@ -28,40 +27,35 @@ import MaterialHeaderButtons, { import type { SectionListDataType } from '../../../components/Screens/WebSectionList'; import { StyleSheet } from 'react-native'; import Urls from '../../../constants/Urls'; +import { readData } from '../../../utils/WebData'; +import { useNavigation } from '@react-navigation/core'; +import { useLayoutEffect } from 'react'; const LIST_ITEM_HEIGHT = 84; export type ProximoCategoryType = { + id: number; name: string; icon: string; - id: string; + created_at: string; + updated_at: string; }; export type ProximoArticleType = { + id: number; name: string; description: string; - quantity: string; - price: string; + quantity: number; + price: number; code: string; - id: string; - type: Array; image: string; + category_id: number; + created_at: string; + updated_at: string; + category: ProximoCategoryType; }; -export type ProximoMainListItemType = { - type: ProximoCategoryType; - data: Array; -}; - -export type ProximoDataType = { - types: Array; - articles: Array; -}; - -type PropsType = { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; -}; +type CategoriesType = Array; const styles = StyleSheet.create({ item: { @@ -69,138 +63,69 @@ const styles = StyleSheet.create({ }, }); +function sortFinalData(a: ProximoCategoryType, b: ProximoCategoryType): number { + const str1 = a.name.toLowerCase(); + const str2 = b.name.toLowerCase(); + + // Make 'All' category with id -1 stick to the top + if (a.id === -1) { + return -1; + } + if (b.id === -1) { + return 1; + } + + // Sort others by name ascending + if (str1 < str2) { + return -1; + } + if (str1 > str2) { + return 1; + } + return 0; +} + /** * Class defining the main proximo screen. * This screen shows the different categories of articles offered by proximo. */ -class ProximoMainScreen extends React.Component { - /** - * Function used to sort items in the list. - * Makes the All category sticks to the top and sorts the others by name ascending - * - * @param a - * @param b - * @return {number} - */ - static sortFinalData( - a: ProximoMainListItemType, - b: ProximoMainListItemType - ): number { - const str1 = a.type.name.toLowerCase(); - const str2 = b.type.name.toLowerCase(); +function ProximoMainScreen() { + const navigation = useNavigation(); + const theme = useTheme(); - // Make 'All' category with id -1 stick to the top - if (a.type.id === '-1') { - return -1; - } - if (b.type.id === '-1') { - return 1; - } - - // Sort others by name ascending - if (str1 < str2) { - return -1; - } - if (str1 > str2) { - return 1; - } - return 0; - } - - /** - * Get an array of available articles (in stock) of the given type - * - * @param articles The list of all articles - * @param type The type of articles to find (undefined for any type) - * @return {Array} The array of available articles - */ - static getAvailableArticles( - articles: Array | null, - type?: ProximoCategoryType - ): Array { - const availableArticles: Array = []; - if (articles != null) { - articles.forEach((article: ProximoArticleType) => { - if ( - ((type != null && article.type.includes(type.id)) || type == null) && - parseInt(article.quantity, 10) > 0 - ) { - availableArticles.push(article); - } - }); - } - return availableArticles; - } - - articles: Array | null; - - constructor(props: PropsType) { - super(props); - this.articles = null; - } - - /** - * Creates header button - */ - componentDidMount() { - const { navigation } = this.props; + useLayoutEffect(() => { navigation.setOptions({ - headerRight: () => this.getHeaderButtons(), + headerRight: () => getHeaderButtons(), }); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigation]); /** * Callback used when the search button is pressed. * This will open a new ProximoListScreen with all items displayed */ - onPressSearchBtn = () => { - const { navigation } = this.props; + const onPressSearchBtn = () => { const searchScreenData = { shouldFocusSearchBar: true, - data: { - type: { - id: '0', - name: i18n.t('screens.proximo.all'), - icon: 'star', - }, - data: - this.articles != null - ? ProximoMainScreen.getAvailableArticles(this.articles) - : [], - }, + category: -1, }; navigation.navigate('proximo-list', searchScreenData); }; - /** - * Callback used when the about button is pressed. - * This will open the ProximoAboutScreen - */ - onPressAboutBtn = () => { - const { navigation } = this.props; - navigation.navigate('proximo-about'); - }; + const onPressAboutBtn = () => navigation.navigate('proximo-about'); - /** - * Gets the header buttons - * @return {*} - */ - getHeaderButtons() { + const getHeaderButtons = () => { return ( - + ); - } + }; /** * Extracts a key for the given category @@ -208,7 +133,8 @@ class ProximoMainScreen extends React.Component { * @param item The category to extract the key from * @return {*} The extracted key */ - getKeyExtractor = (item: ProximoMainListItemType): string => item.type.id; + const getKeyExtractor = (item: ProximoCategoryType): string => + item.id.toString(); /** * Gets the given category render item @@ -216,33 +142,36 @@ class ProximoMainScreen extends React.Component { * @param item The category to render * @return {*} */ - getRenderItem = ({ item }: { item: ProximoMainListItemType }) => { - const { navigation, theme } = this.props; + const getRenderItem = ({ item }: { item: ProximoCategoryType }) => { const dataToSend = { shouldFocusSearchBar: false, - data: item, + category: item.id, }; - const subtitle = `${item.data.length} ${ - item.data.length > 1 + // TODO get article number + const article_number = 1; + const subtitle = `${article_number} ${ + article_number > 1 ? i18n.t('screens.proximo.articles') : i18n.t('screens.proximo.article') }`; - const onPress = () => { - navigation.navigate('proximo-list', dataToSend); - }; - if (item.data.length > 0) { + const onPress = () => navigation.navigate('proximo-list', dataToSend); + if (article_number > 0) { return ( ( - - )} + left={(props) => + item.icon.endsWith('.png') ? ( + + ) : ( + + ) + } right={(props) => ( { * @param fetchedData * @return {*} * */ - createDataset = ( - fetchedData: ProximoDataType | null - ): SectionListDataType => { - return [ - { - title: '', - data: this.generateData(fetchedData), - keyExtractor: this.getKeyExtractor, - }, - ]; - }; - - /** - * Generate the data using types and FetchedData. - * This will group items under the same type. - * - * @param fetchedData The array of articles represented by objects - * @returns {Array} The formatted dataset - */ - generateData( - fetchedData: ProximoDataType | null - ): Array { - const finalData: Array = []; - this.articles = null; - if (fetchedData != null) { - const { types } = fetchedData; - this.articles = fetchedData.articles; - finalData.push({ - type: { - id: '-1', + const createDataset = ( + data: CategoriesType | undefined + ): SectionListDataType => { + if (data) { + const finalData: CategoriesType = [ + { + id: -1, name: i18n.t('screens.proximo.all'), icon: 'star', + created_at: '', + updated_at: '', }, - data: ProximoMainScreen.getAvailableArticles(this.articles), - }); - types.forEach((type: ProximoCategoryType) => { - finalData.push({ - type, - data: ProximoMainScreen.getAvailableArticles(this.articles, type), - }); - }); + ...data, + ]; + return [ + { + title: '', + data: finalData.sort(sortFinalData), + keyExtractor: getKeyExtractor, + }, + ]; + } else { + return [ + { + title: '', + data: [], + keyExtractor: getKeyExtractor, + }, + ]; } - finalData.sort(ProximoMainScreen.sortFinalData); - return finalData; - } + }; - render() { - const { navigation } = this.props; - return ( - - ); - } + return ( + readData(Urls.proximo.categories)} + createDataset={createDataset} + refreshOnFocus={true} + renderItem={getRenderItem} + /> + ); } export default withTheme(ProximoMainScreen); diff --git a/src/screens/Services/SelfMenuScreen.tsx b/src/screens/Services/SelfMenuScreen.tsx index e0e5b78..96a2e9f 100644 --- a/src/screens/Services/SelfMenuScreen.tsx +++ b/src/screens/Services/SelfMenuScreen.tsx @@ -18,7 +18,7 @@ */ import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { SectionListData, StyleSheet, View } from 'react-native'; import { Card, Text, withTheme } from 'react-native-paper'; import { StackNavigationProp } from '@react-navigation/stack'; import i18n from 'i18n-js'; @@ -26,6 +26,7 @@ import DateManager from '../../managers/DateManager'; import WebSectionList from '../../components/Screens/WebSectionList'; import type { SectionListDataType } from '../../components/Screens/WebSectionList'; import Urls from '../../constants/Urls'; +import { readData } from '../../utils/WebData'; type PropsType = { navigation: StackNavigationProp; @@ -108,7 +109,7 @@ class SelfMenuScreen extends React.Component { * @return {[]} */ createDataset = ( - fetchedData: Array + fetchedData: Array | undefined ): SectionListDataType => { let result: SectionListDataType = []; if (fetchedData == null || fetchedData.length === 0) { @@ -137,7 +138,11 @@ class SelfMenuScreen extends React.Component { * @param section The section to render the header from * @return {*} */ - getRenderSectionHeader = ({ section }: { section: { title: string } }) => { + getRenderSectionHeader = ({ + section, + }: { + section: SectionListData; + }) => { return ( { getKeyExtractor = (item: RuFoodCategoryType): string => item.name; render() { - const { navigation } = this.props; return ( readData>(Urls.app.menu)} createDataset={this.createDataset} - navigation={navigation} - autoRefreshTime={0} - refreshOnFocus={false} - fetchUrl={Urls.app.menu} + refreshOnFocus={true} renderItem={this.getRenderItem} renderSectionHeader={this.getRenderSectionHeader} - stickyHeader + stickyHeader={true} /> ); } diff --git a/src/screens/Services/WebsiteScreen.tsx b/src/screens/Services/WebsiteScreen.tsx index ec0430d..cd36522 100644 --- a/src/screens/Services/WebsiteScreen.tsx +++ b/src/screens/Services/WebsiteScreen.tsx @@ -105,7 +105,6 @@ class WebsiteScreen extends React.Component { const { route, navigation } = this.props; if (route.params != null) { - console.log(route.params); this.host = route.params.host; let { path } = route.params; const { title } = route.params; diff --git a/src/screens/Test.tsx b/src/screens/Test.tsx index ad99763..9edf271 100644 --- a/src/screens/Test.tsx +++ b/src/screens/Test.tsx @@ -141,7 +141,6 @@ class Test extends React.Component { // ); return ( ( * @param url The urls to fetch data from * @return Promise */ -export async function readData(url: string): Promise { - return new Promise((resolve: (response: any) => void, reject: () => void) => { +export async function readData(url: string): Promise { + return new Promise((resolve: (response: T) => void, reject: () => void) => { fetch(url) .then(async (response: Response): Promise => response.json()) - .then((data: any): void => resolve(data)) - .catch((): void => reject()); + .then((data: T) => resolve(data)) + .catch(() => reject()); }); } diff --git a/src/utils/cacheContext.ts b/src/utils/cacheContext.ts new file mode 100644 index 0000000..0a39723 --- /dev/null +++ b/src/utils/cacheContext.ts @@ -0,0 +1,21 @@ +import React, { useContext } from 'react'; + +export type CacheContextType = { + cache: T | undefined; + setCache: (newCache: T) => void; + resetCache: () => void; +}; + +export const CacheContext = React.createContext>({ + cache: undefined, + setCache: () => undefined, + resetCache: () => undefined, +}); + +function getCacheContext() { + return CacheContext as React.Context>; +} + +export function useCache() { + return useContext(getCacheContext()); +} diff --git a/src/utils/customHooks.tsx b/src/utils/customHooks.tsx new file mode 100644 index 0000000..86ec9a3 --- /dev/null +++ b/src/utils/customHooks.tsx @@ -0,0 +1,106 @@ +import { DependencyList, useEffect, useRef, useState } from 'react'; +import { REQUEST_STATUS } from './Requests'; + +export function useMountEffect(func: () => void) { + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(func, []); +} + +/** + * Effect that does not run on first render + * + * @param effect + * @param deps + */ +export function useSubsequentEffect(effect: () => void, deps?: DependencyList) { + const didMountRef = useRef(false); + useEffect( + () => { + if (didMountRef.current) { + effect(); + } else { + didMountRef.current = true; + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + deps ? deps : [] + ); +} + +export function useRequestLogic( + request: () => Promise, + cache?: T, + onCacheUpdate?: (newCache: T) => void, + startLoading?: boolean, + minRefreshTime?: number +) { + const [response, setResponse] = useState<{ + loading: boolean; + status: REQUEST_STATUS; + code?: number; + data: T | undefined; + }>({ + loading: startLoading !== false && cache === undefined, + status: REQUEST_STATUS.SUCCESS, + code: undefined, + data: undefined, + }); + const [lastRefreshDate, setLastRefreshDate] = useState( + undefined + ); + + const refreshData = (newRequest?: () => Promise) => { + let canRefresh; + if (lastRefreshDate && minRefreshTime) { + const last = lastRefreshDate; + canRefresh = new Date().getTime() - last.getTime() > minRefreshTime; + } else { + canRefresh = true; + } + if (canRefresh) { + if (!response.loading) { + setResponse((prevState) => ({ + ...prevState, + loading: true, + })); + } + setLastRefreshDate(new Date()); + const r = newRequest ? newRequest : request; + r() + .then((requestResponse: T) => { + setResponse({ + loading: false, + status: REQUEST_STATUS.SUCCESS, + code: undefined, + data: requestResponse, + }); + if (onCacheUpdate) { + onCacheUpdate(requestResponse); + } + }) + .catch(() => { + setResponse((prevState) => ({ + loading: false, + status: REQUEST_STATUS.CONNECTION_ERROR, + code: 0, + data: prevState.data, + })); + }); + } + }; + + const value: [ + boolean, + REQUEST_STATUS, + number | undefined, + T | undefined, + (newRequest?: () => Promise) => void + ] = [ + response.loading, + response.status, + response.code, + cache ? cache : response.data, + refreshData, + ]; + return value; +} diff --git a/tsconfig.json b/tsconfig.json index d2b93ba..7ca7672 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,10 +30,10 @@ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ @@ -45,13 +45,15 @@ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - "resolveJsonModule": true /* Allow import of JSON files */ + "resolveJsonModule": true, /* Allow import of JSON files */ /* Source Map Options */ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - + "skipLibCheck": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */