diff --git a/src/components/Lists/Proximo/ProximoListItem.js b/src/components/Lists/Proximo/ProximoListItem.js index db7d870..28b7def 100644 --- a/src/components/Lists/Proximo/ProximoListItem.js +++ b/src/components/Lists/Proximo/ProximoListItem.js @@ -2,48 +2,48 @@ import * as React from 'react'; import {Avatar, List, Text, withTheme} from 'react-native-paper'; -import i18n from "i18n-js"; +import i18n from 'i18n-js'; +import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen'; -type Props = { - onPress: Function, - color: string, - item: Object, - height: number, -} +type PropsType = { + onPress: () => void, + color: string, + item: ProximoArticleType, + height: number, +}; -class ProximoListItem extends React.Component { +class ProximoListItem extends React.Component { + shouldComponentUpdate(): boolean { + return false; + } - colors: Object; - - constructor(props) { - super(props); - this.colors = props.theme.colors; - } - - shouldComponentUpdate() { - return false; - } - - render() { - return ( - } - right={() => - - {this.props.item.price}€ - } - style={{ - height: this.props.height, - justifyContent: 'center', - }} - /> - ); - } + render(): React.Node { + const {props} = this; + return ( + ( + + )} + right={(): React.Node => ( + {props.item.price}€ + )} + style={{ + height: props.height, + justifyContent: 'center', + }} + /> + ); + } } export default withTheme(ProximoListItem); diff --git a/src/components/Screens/WebSectionList.js b/src/components/Screens/WebSectionList.js index 4f41bfa..88c545f 100644 --- a/src/components/Screens/WebSectionList.js +++ b/src/components/Screens/WebSectionList.js @@ -1,44 +1,55 @@ // @flow import * as React from 'react'; -import {ERROR_TYPE, readData} from "../../utils/WebData"; -import i18n from "i18n-js"; +import i18n from 'i18n-js'; import {Snackbar} from 'react-native-paper'; -import {RefreshControl, View} from "react-native"; -import ErrorView from "./ErrorView"; -import BasicLoadingScreen from "./BasicLoadingScreen"; -import {withCollapsible} from "../../utils/withCollapsible"; +import {RefreshControl, View} from 'react-native'; import * as Animatable from 'react-native-animatable'; -import CustomTabBar from "../Tabbar/CustomTabBar"; -import {Collapsible} from "react-navigation-collapsible"; -import {StackNavigationProp} from "@react-navigation/stack"; -import CollapsibleSectionList from "../Collapsible/CollapsibleSectionList"; +import {Collapsible} from 'react-navigation-collapsible'; +import {StackNavigationProp} from '@react-navigation/stack'; +import ErrorView from './ErrorView'; +import BasicLoadingScreen from './BasicLoadingScreen'; +import {withCollapsible} from '../../utils/withCollapsible'; +import CustomTabBar from '../Tabbar/CustomTabBar'; +import {ERROR_TYPE, readData} from '../../utils/WebData'; +import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; +import type {ApiGenericDataType} from '../../utils/WebData'; -type Props = { - navigation: StackNavigationProp, - fetchUrl: string, - autoRefreshTime: number, - refreshOnFocus: boolean, - renderItem: (data: { [key: string]: any }) => React.Node, - createDataset: (data: { [key: string]: any } | null, isLoading?: boolean) => Array, - onScroll: (event: SyntheticEvent) => void, - collapsibleStack: Collapsible, +export type SectionListDataType = Array<{ + title: string, + data: Array, + keyExtractor?: (T) => string, +}>; - showError: boolean, - itemHeight?: number, - updateData?: number, - renderListHeaderComponent?: (data: { [key: string]: any } | null) => React.Node, - renderSectionHeader?: (data: { section: { [key: string]: any } }, isLoading?: boolean) => React.Node, - stickyHeader?: boolean, -} +type PropsType = { + navigation: StackNavigationProp, + fetchUrl: string, + autoRefreshTime: number, + refreshOnFocus: boolean, + renderItem: (data: {item: T}) => React.Node, + createDataset: ( + data: ApiGenericDataType | null, + isLoading?: boolean, + ) => SectionListDataType, + onScroll: (event: SyntheticEvent) => void, + collapsibleStack: Collapsible, -type State = { - refreshing: boolean, - firstLoading: boolean, - fetchedData: { [key: string]: any } | null, - snackbarVisible: boolean + showError?: boolean, + itemHeight?: number | null, + updateData?: number, + renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node, + renderSectionHeader?: ( + data: {section: {title: string}}, + isLoading?: boolean, + ) => React.Node, + stickyHeader?: boolean, }; +type StateType = { + refreshing: boolean, + fetchedData: ApiGenericDataType | null, + snackbarVisible: boolean, +}; const MIN_REFRESH_TIME = 5 * 1000; @@ -48,211 +59,216 @@ const MIN_REFRESH_TIME = 5 * 1000; * 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 { +class WebSectionList extends React.PureComponent, StateType> { + static defaultProps = { + showError: true, + itemHeight: null, + updateData: 0, + renderListHeaderComponent: (): React.Node => null, + renderSectionHeader: (): React.Node => null, + stickyHeader: false, + }; - static defaultProps = { - stickyHeader: false, - updateData: 0, - showError: true, + refreshInterval: IntervalID; + + lastRefresh: Date | null; + + constructor() { + super(); + this.state = { + refreshing: false, + fetchedData: null, + snackbarVisible: false, }; + } - refreshInterval: IntervalID; - lastRefresh: Date | null; + /** + * 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 = null; + this.onRefresh(); + } - state = { - refreshing: false, - firstLoading: true, - fetchedData: null, - snackbarVisible: false + /** + * Refreshes data when focusing the screen and setup a refresh interval if asked to + */ + onScreenFocus = () => { + const {props} = this; + if (props.refreshOnFocus && this.lastRefresh) this.onRefresh(); + if (props.autoRefreshTime > 0) + this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime); + }; + + /** + * Removes any interval on un-focus + */ + onScreenBlur = () => { + 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: ApiGenericDataType) => { + 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 = ( + data: T, + index: number, + ): {length: number, offset: number, index: number} | null => { + const {itemHeight} = this.props; + if (itemHeight == null) return null; + return { + length: itemHeight, + offset: itemHeight * index, + index, }; + }; - /** - * Registers react navigation events on first screen load. - * Allows to detect when the screen is focused - */ - componentDidMount() { - this.props.navigation.addListener('focus', this.onScreenFocus); - this.props.navigation.addListener('blur', this.onScreenBlur); - this.lastRefresh = null; - this.onRefresh(); + getRenderSectionHeader = (data: {section: {title: string}}): React.Node => { + const {renderSectionHeader} = this.props; + const {refreshing} = this.state; + if (renderSectionHeader != null) { + return ( + + {renderSectionHeader(data, refreshing)} + + ); } + return null; + }; - /** - * Refreshes data when focusing the screen and setup a refresh interval if asked to - */ - onScreenFocus = () => { - if (this.props.refreshOnFocus && this.lastRefresh) - this.onRefresh(); - if (this.props.autoRefreshTime > 0) - this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime) - } + getRenderItem = (data: {item: T}): React.Node => { + const {renderItem} = this.props; + return ( + + {renderItem(data)} + + ); + }; - /** - * Removes any interval on un-focus - */ - onScreenBlur = () => { - clearInterval(this.refreshInterval); - } + onScroll = (event: SyntheticEvent) => { + const {onScroll} = this.props; + if (onScroll != null) onScroll(event); + }; + render(): React.Node { + const {props, state} = this; + let dataset = []; + if ( + state.fetchedData != null || + (state.fetchedData == null && !props.showError) + ) + dataset = props.createDataset(state.fetchedData, state.refreshing); - /** - * 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: { [key: string]: any }) => { - this.setState({ - fetchedData: fetchedData, - refreshing: false, - firstLoading: 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, - firstLoading: false - }); - this.showSnackBar(); - }; - - /** - * Refreshes data and shows an animations while doing it - */ - onRefresh = () => { - 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(this.props.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}); - - itemLayout = (data: { [key: string]: any }, index: number) => { - const height = this.props.itemHeight; - if (height == null) - return undefined; - return { - length: height, - offset: height * index, - index - } - }; - - renderSectionHeader = (data: { section: { [key: string]: any } }) => { - if (this.props.renderSectionHeader != null) { - return ( - - {this.props.renderSectionHeader(data, this.state.refreshing)} - - ); - } else - return null; - } - - renderItem = (data: { - item: { [key: string]: any }, - index: number, - section: { [key: string]: any }, - separators: { [key: string]: any }, - }) => { - return ( - - {this.props.renderItem(data)} - - ); - } - - onScroll = (event: SyntheticEvent) => { - if (this.props.onScroll) - this.props.onScroll(event); - } - - render() { - let dataset = []; - if (this.state.fetchedData != null || (this.state.fetchedData == null && !this.props.showError)) { - dataset = this.props.createDataset(this.state.fetchedData, this.state.refreshing); - } - const {containerPaddingTop} = this.props.collapsibleStack; - return ( - - - } - renderSectionHeader={this.renderSectionHeader} - renderItem={this.renderItem} - stickySectionHeadersEnabled={this.props.stickyHeader} - style={{minHeight: '100%'}} - ListHeaderComponent={this.props.renderListHeaderComponent != null - ? this.props.renderListHeaderComponent(this.state.fetchedData) - : null} - ListEmptyComponent={this.state.refreshing - ? - : - } - getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined} - onScroll={this.onScroll} - hasTab={true} - /> - { - }, - }} - duration={4000} - style={{ - bottom: CustomTabBar.TAB_BAR_HEIGHT - }} - > - {i18n.t("general.listUpdateFail")} - - - ); - } + const {containerPaddingTop} = props.collapsibleStack; + return ( + + + } + renderSectionHeader={this.getRenderSectionHeader} + renderItem={this.getRenderItem} + stickySectionHeadersEnabled={props.stickyHeader} + style={{minHeight: '100%'}} + ListHeaderComponent={ + props.renderListHeaderComponent != null + ? props.renderListHeaderComponent(state.fetchedData) + : null + } + ListEmptyComponent={ + state.refreshing ? ( + + ) : ( + + ) + } + getItemLayout={props.itemHeight != null ? this.getItemLayout : null} + onScroll={this.onScroll} + hasTab + /> + {}, + }} + duration={4000} + style={{ + bottom: CustomTabBar.TAB_BAR_HEIGHT, + }}> + {i18n.t('general.listUpdateFail')} + + + ); + } } export default withCollapsible(WebSectionList); diff --git a/src/screens/Services/Proximo/ProximoAboutScreen.js b/src/screens/Services/Proximo/ProximoAboutScreen.js index 7f4a7b3..30be2b6 100644 --- a/src/screens/Services/Proximo/ProximoAboutScreen.js +++ b/src/screens/Services/Proximo/ProximoAboutScreen.js @@ -2,58 +2,74 @@ import * as React from 'react'; import {Image, View} from 'react-native'; -import i18n from "i18n-js"; +import i18n from 'i18n-js'; import {Card, List, Paragraph, Text} from 'react-native-paper'; -import CustomTabBar from "../../../components/Tabbar/CustomTabBar"; -import {StackNavigationProp} from "@react-navigation/stack"; -import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; +import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; +import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; -type Props = { - navigation: StackNavigationProp, -}; - -const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png"; +const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png'; /** * Class defining the proximo about screen. */ -export default class ProximoAboutScreen extends React.Component { - - render() { - return ( - - - - - {i18n.t('screens.proximo.description')} - - } - /> - - 18h30 - 19h30 - - - - } - /> - - {i18n.t('screens.proximo.paymentMethodsDescription')} - - - - ); - } +// eslint-disable-next-line react/prefer-stateless-function +export default class ProximoAboutScreen extends React.Component { + render(): React.Node { + return ( + + + + + {i18n.t('screens.proximo.description')} + + ( + + )} + /> + + 18h30 - 19h30 + + + + ( + + )} + /> + + + {i18n.t('screens.proximo.paymentMethodsDescription')} + + + + + ); + } } diff --git a/src/screens/Services/Proximo/ProximoListScreen.js b/src/screens/Services/Proximo/ProximoListScreen.js index 7de8054..c405cc6 100644 --- a/src/screens/Services/Proximo/ProximoListScreen.js +++ b/src/screens/Services/Proximo/ProximoListScreen.js @@ -1,323 +1,381 @@ // @flow import * as React from 'react'; -import {Image, Platform, ScrollView, View} from "react-native"; -import i18n from "i18n-js"; -import CustomModal from "../../../components/Overrides/CustomModal"; -import {RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper"; -import {stringMatchQuery} from "../../../utils/Search"; -import ProximoListItem from "../../../components/Lists/Proximo/ProximoListItem"; -import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; +import {Image, Platform, ScrollView, View} from 'react-native'; +import i18n from 'i18n-js'; +import { + RadioButton, + Searchbar, + Subheading, + Text, + Title, + withTheme, +} 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'; +import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem'; +import MaterialHeaderButtons, { + Item, +} from '../../../components/Overrides/CustomHeaderButton'; +import type {CustomTheme} from '../../../managers/ThemeManager'; +import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList'; +import type {ProximoArticleType} from './ProximoMainScreen'; -function sortPrice(a, b) { - return a.price - b.price; +function sortPrice(a: ProximoArticleType, b: ProximoArticleType): number { + return parseInt(a.price, 10) - parseInt(b.price, 10); } -function sortPriceReverse(a, b) { - return b.price - a.price; +function sortPriceReverse( + a: ProximoArticleType, + b: ProximoArticleType, +): number { + return parseInt(b.price, 10) - parseInt(a.price, 10); } -function sortName(a, b) { - if (a.name.toLowerCase() < b.name.toLowerCase()) - return -1; - if (a.name.toLowerCase() > b.name.toLowerCase()) - return 1; - return 0; +function sortName(a: ProximoArticleType, b: ProximoArticleType): number { + if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; + return 0; } -function sortNameReverse(a, b) { - if (a.name.toLowerCase() < b.name.toLowerCase()) - return 1; - if (a.name.toLowerCase() > b.name.toLowerCase()) - return -1; - return 0; +function sortNameReverse(a: ProximoArticleType, b: ProximoArticleType): number { + if (a.name.toLowerCase() < b.name.toLowerCase()) return 1; + if (a.name.toLowerCase() > b.name.toLowerCase()) return -1; + return 0; } const LIST_ITEM_HEIGHT = 84; -type Props = { - navigation: StackNavigationProp, - route: { params: { data: { data: Object }, shouldFocusSearchBar: boolean } }, - theme: CustomTheme, -} +type PropsType = { + navigation: StackNavigationProp, + route: { + params: { + data: {data: Array}, + shouldFocusSearchBar: boolean, + }, + }, + theme: CustomTheme, +}; -type State = { - currentSortMode: number, - modalCurrentDisplayItem: React.Node, - currentSearchString: string, +type StateType = { + currentSortMode: number, + modalCurrentDisplayItem: React.Node, + currentSearchString: string, }; /** - * Class defining proximo's article list of a certain category. + * Class defining Proximo article list of a certain category. */ -class ProximoListScreen extends React.Component { +class ProximoListScreen extends React.Component { + modalRef: Modalize | null; - modalRef: Object; - listData: Array; - shouldFocusSearchBar: boolean; + listData: Array; - constructor(props) { - super(props); - this.listData = this.props.route.params['data']['data'].sort(sortName); - this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar']; - this.state = { - currentSearchString: '', - currentSortMode: 3, - modalCurrentDisplayItem: null, - }; + shouldFocusSearchBar: boolean; + + constructor(props: PropsType) { + super(props); + this.listData = props.route.params.data.data.sort(sortName); + this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar; + this.state = { + currentSearchString: '', + currentSortMode: 3, + modalCurrentDisplayItem: null, + }; + } + + /** + * Creates the header content + */ + componentDidMount() { + const {navigation} = this.props; + navigation.setOptions({ + headerRight: this.getSortMenuButton, + headerTitle: this.getSearchBar, + headerBackTitleVisible: false, + headerTitleContainerStyle: + Platform.OS === 'ios' + ? {marginHorizontal: 0, width: '70%'} + : {marginHorizontal: 0, right: 50, left: 50}, + }); + } + + /** + * 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(); } + }; + /** + * Callback used when the search changes + * + * @param str The new search string + */ + onSearchStringChange = (str: string) => { + this.setState({currentSearchString: str}); + }; - /** - * Creates the header content - */ - componentDidMount() { - this.props.navigation.setOptions({ - headerRight: this.getSortMenuButton, - headerTitle: this.getSearchBar, - headerBackTitleVisible: false, - headerTitleContainerStyle: Platform.OS === 'ios' ? - {marginHorizontal: 0, width: '70%'} : - {marginHorizontal: 0, right: 50, left: 50}, - }); + /** + * 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(); } + } - /** - * Gets the header search bar - * - * @return {*} - */ - getSearchBar = () => { - return ( - 3) color = theme.colors.success; + else if (availableStock > 0) color = theme.colors.warning; + else color = theme.colors.danger; + return color; + } + + /** + * Gets the sort menu header button + * + * @return {*} + */ + getSortMenuButton = (): React.Node => { + return ( + + + + ); + }; + + /** + * Gets the header search bar + * + * @return {*} + */ + getSearchBar = (): React.Node => { + return ( + + ); + }; + + /** + * Gets the modal content depending on the given article + * + * @param item The article to display + * @return {*} + */ + getModalItemContent(item: ProximoArticleType): React.Node { + return ( + + {item.name} + + + {`${item.quantity} ${i18n.t('screens.proximo.inStock')}`} + + {item.price}€ + + + + + - ); - }; + + {item.description} + + + ); + } - /** - * Gets the sort menu header button - * - * @return {*} - */ - getSortMenuButton = () => { - return - - ; - }; + /** + * Gets the modal content to display a sort menu + * + * @return {*} + */ + getModalSortMenu(): React.Node { + const {currentSortMode} = this.state; + return ( + + + {i18n.t('screens.proximo.sortOrder')} + + { + this.setSortMode(value); + }} + value={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(); - } - }; - - /** - * Sets the current sort mode. - * - * @param mode The number representing the mode - */ - setSortMode(mode: number) { - this.setState({ - currentSortMode: mode, - }); - switch (mode) { - 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; - } - if (this.modalRef && mode !== this.state.currentSortMode) { - this.modalRef.close(); - } + /** + * Gets a render item for the given article + * + * @param item The article to render + * @return {*} + */ + getRenderItem = ({item}: {item: ProximoArticleType}): React.Node => { + const {currentSearchString} = this.state; + if (stringMatchQuery(item.name, currentSearchString)) { + const onPress = () => { + this.onListItemPress(item); + }; + const color = this.getStockColor(parseInt(item.quantity, 10)); + return ( + + ); } + return null; + }; - /** - * Gets a color depending on the quantity available - * - * @param availableStock The quantity available - * @return - */ - getStockColor(availableStock: number) { - let color: string; - if (availableStock > 3) - color = this.props.theme.colors.success; - else if (availableStock > 0) - color = this.props.theme.colors.warning; - else - color = this.props.theme.colors.danger; - return color; - } + /** + * Extracts a key for the given article + * + * @param item The article to extract the key from + * @return {string} The extracted key + */ + keyExtractor = (item: ProximoArticleType): string => item.name + item.code; - /** - * Callback used when the search changes - * - * @param str The new search string - */ - onSearchStringChange = (str: string) => { - this.setState({currentSearchString: str}) - }; + /** + * Callback used when receiving the modal ref + * + * @param ref + */ + onModalRef = (ref: Modalize) => { + this.modalRef = ref; + }; - /** - * Gets the modal content depending on the given article - * - * @param item The article to display - * @return {*} - */ - getModalItemContent(item: Object) { - return ( - - {item.name} - - - {item.quantity + ' ' + i18n.t('screens.proximo.inStock')} - - {item.price}€ - + itemLayout = ( + data: ProximoArticleType, + index: number, + ): {length: number, offset: number, index: number} => ({ + length: LIST_ITEM_HEIGHT, + offset: LIST_ITEM_HEIGHT * index, + index, + }); - - - - - {item.description} - - - ); - } - - /** - * Gets the modal content to display a sort menu - * - * @return {*} - */ - getModalSortMenu() { - return ( - - {i18n.t('screens.proximo.sortOrder')} - this.setSortMode(value)} - value={this.state.currentSortMode} - > - - - - - - - ); - } - - /** - * 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: Object) { - this.setState({ - modalCurrentDisplayItem: this.getModalItemContent(item) - }); - if (this.modalRef) { - this.modalRef.open(); - } - } - - /** - * Gets a render item for the given article - * - * @param item The article to render - * @return {*} - */ - renderItem = ({item}: Object) => { - if (stringMatchQuery(item.name, this.state.currentSearchString)) { - const onPress = this.onListItemPress.bind(this, item); - const color = this.getStockColor(parseInt(item.quantity)); - return ( - - ); - } else - return null; - }; - - /** - * Extracts a key for the given article - * - * @param item The article to extract the key from - * @return {*} The extracted key - */ - keyExtractor(item: Object) { - return item.name + item.code; - } - - /** - * Callback used when receiving the modal ref - * - * @param ref - */ - onModalRef = (ref: Object) => { - this.modalRef = ref; - }; - - itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); - - render() { - return ( - - - {this.state.modalCurrentDisplayItem} - - - - ); - } + render(): React.Node { + const {state} = this; + return ( + + + {state.modalCurrentDisplayItem} + + + + ); + } } export default withTheme(ProximoListScreen); diff --git a/src/screens/Services/Proximo/ProximoMainScreen.js b/src/screens/Services/Proximo/ProximoMainScreen.js index 200a31c..ab9f546 100644 --- a/src/screens/Services/Proximo/ProximoMainScreen.js +++ b/src/screens/Services/Proximo/ProximoMainScreen.js @@ -1,233 +1,289 @@ // @flow import * as React from 'react'; -import {View} from 'react-native' -import i18n from "i18n-js"; -import WebSectionList from "../../../components/Screens/WebSectionList"; +import i18n from 'i18n-js'; import {List, withTheme} from 'react-native-paper'; -import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import {StackNavigationProp} from '@react-navigation/stack'; +import WebSectionList from '../../../components/Screens/WebSectionList'; +import MaterialHeaderButtons, { + Item, +} from '../../../components/Overrides/CustomHeaderButton'; +import type {CustomTheme} from '../../../managers/ThemeManager'; +import type {SectionListDataType} from '../../../components/Screens/WebSectionList'; -const DATA_URL = "https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json"; +const DATA_URL = 'https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json'; const LIST_ITEM_HEIGHT = 84; -type Props = { - navigation: StackNavigationProp, - theme: CustomTheme, -} +export type ProximoCategoryType = { + name: string, + icon: string, + id: string, +}; -type State = { - fetchedData: Object, -} +export type ProximoArticleType = { + name: string, + description: string, + quantity: string, + price: string, + code: string, + id: string, + type: Array, + image: string, +}; + +export type ProximoMainListItemType = { + type: ProximoCategoryType, + data: Array, +}; + +export type ProximoDataType = { + types: Array, + articles: Array, +}; + +type PropsType = { + navigation: StackNavigationProp, + theme: CustomTheme, +}; /** * Class defining the main proximo screen. * This screen shows the different categories of articles offered by proximo. */ -class ProximoMainScreen extends React.Component { +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(); - articles: Object; + // Make 'All' category with id -1 stick to the top + if (a.type.id === -1) return -1; + if (b.type.id === -1) return 1; - /** - * Function used to sort items in the list. - * Makes the All category stick to the top and sorts the others by name ascending - * - * @param a - * @param b - * @return {number} - */ - static sortFinalData(a: Object, b: Object) { - let str1 = a.type.name.toLowerCase(); - let str2 = b.type.name.toLowerCase(); + // Sort others by name ascending + if (str1 < str2) return -1; + if (str1 > str2) return 1; + return 0; + } - // 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 = []; + 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; + } - /** - * Creates header button - */ - componentDidMount() { - const rightButton = this.getHeaderButtons.bind(this); - this.props.navigation.setOptions({ - headerRight: rightButton, - }); - } + articles: Array | null; - /** - * Callback used when the search button is pressed. - * This will open a new ProximoListScreen with all items displayed - */ - onPressSearchBtn = () => { - let searchScreenData = { - shouldFocusSearchBar: true, - data: { - type: { - id: "0", - name: i18n.t('screens.proximo.all'), - icon: 'star' - }, - data: this.articles !== undefined ? - this.getAvailableArticles(this.articles, undefined) : [] - }, - }; - this.props.navigation.navigate('proximo-list', searchScreenData); + /** + * Creates header button + */ + componentDidMount() { + const {navigation} = this.props; + navigation.setOptions({ + headerRight: (): React.Node => this.getHeaderButtons(), + }); + } + + /** + * Callback used when the search button is pressed. + * This will open a new ProximoListScreen with all items displayed + */ + onPressSearchBtn = () => { + const {navigation} = this.props; + const searchScreenData = { + shouldFocusSearchBar: true, + data: { + type: { + id: '0', + name: i18n.t('screens.proximo.all'), + icon: 'star', + }, + data: + this.articles != null + ? ProximoMainScreen.getAvailableArticles(this.articles) + : [], + }, }; + navigation.navigate('proximo-list', searchScreenData); + }; - /** - * Callback used when the about button is pressed. - * This will open the ProximoAboutScreen - */ - onPressAboutBtn = () => { - this.props.navigation.navigate('proximo-about'); + /** + * Callback used when the about button is pressed. + * This will open the ProximoAboutScreen + */ + onPressAboutBtn = () => { + const {navigation} = this.props; + navigation.navigate('proximo-about'); + }; + + /** + * Gets the header buttons + * @return {*} + */ + getHeaderButtons(): React.Node { + return ( + + + + + ); + } + + /** + * Extracts a key for the given category + * + * @param item The category to extract the key from + * @return {*} The extracted key + */ + getKeyExtractor = (item: ProximoMainListItemType): string => item.type.id; + + /** + * Gets the given category render item + * + * @param item The category to render + * @return {*} + */ + getRenderItem = ({item}: {item: ProximoMainListItemType}): React.Node => { + const {navigation, theme} = this.props; + const dataToSend = { + shouldFocusSearchBar: false, + data: item, + }; + const subtitle = `${item.data.length} ${ + item.data.length > 1 + ? i18n.t('screens.proximo.articles') + : i18n.t('screens.proximo.article') + }`; + const onPress = () => { + navigation.navigate('proximo-list', dataToSend); + }; + if (item.data.length > 0) { + return ( + ( + + )} + right={({size, color}: {size: number, color: string}): React.Node => ( + + )} + style={{ + height: LIST_ITEM_HEIGHT, + justifyContent: 'center', + }} + /> + ); } + return null; + }; - /** - * Gets the header buttons - * @return {*} - */ - getHeaderButtons() { - return - - - ; + /** + * Creates the dataset to be used in the FlatList + * + * @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', + name: i18n.t('screens.proximo.all'), + icon: 'star', + }, + data: ProximoMainScreen.getAvailableArticles(this.articles), + }); + types.forEach((type: ProximoCategoryType) => { + finalData.push({ + type, + data: ProximoMainScreen.getAvailableArticles(this.articles, type), + }); + }); } + finalData.sort(ProximoMainScreen.sortFinalData); + return finalData; + } - /** - * Extracts a key for the given category - * - * @param item The category to extract the key from - * @return {*} The extracted key - */ - getKeyExtractor(item: Object) { - return item !== undefined ? item.type['id'] : undefined; - } - - /** - * Creates the dataset to be used in the FlatList - * - * @param fetchedData - * @return {*} - * */ - createDataset = (fetchedData: Object) => { - 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: Object) { - let finalData = []; - this.articles = undefined; - if (fetchedData.types !== undefined && fetchedData.articles !== undefined) { - let types = fetchedData.types; - this.articles = fetchedData.articles; - finalData.push({ - type: { - id: -1, - name: i18n.t('screens.proximo.all'), - icon: 'star' - }, - data: this.getAvailableArticles(this.articles, undefined) - }); - for (let i = 0; i < types.length; i++) { - finalData.push({ - type: types[i], - data: this.getAvailableArticles(this.articles, types[i]) - }); - - } - } - finalData.sort(ProximoMainScreen.sortFinalData); - return finalData; - } - - /** - * 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 - */ - getAvailableArticles(articles: Array, type: ?Object) { - let availableArticles = []; - for (let k = 0; k < articles.length; k++) { - if ((type !== undefined && type !== null && articles[k]['type'].includes(type['id']) - || type === undefined) - && parseInt(articles[k]['quantity']) > 0) { - availableArticles.push(articles[k]); - } - } - return availableArticles; - } - - /** - * Gets the given category render item - * - * @param item The category to render - * @return {*} - */ - getRenderItem = ({item}: Object) => { - let dataToSend = { - shouldFocusSearchBar: false, - data: item, - }; - const subtitle = item.data.length + " " + (item.data.length > 1 ? i18n.t('screens.proximo.articles') : i18n.t('screens.proximo.article')); - const onPress = this.props.navigation.navigate.bind(this, 'proximo-list', dataToSend); - if (item.data.length > 0) { - return ( - } - right={props => } - style={{ - height: LIST_ITEM_HEIGHT, - justifyContent: 'center', - }} - /> - ); - } else - return ; - } - - render() { - const nav = this.props.navigation; - return ( - - ); - } + render(): React.Node { + const {navigation} = this.props; + return ( + + ); + } } export default withTheme(ProximoMainScreen);