Improve Proximo components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-04 18:00:45 +02:00
parent ab86c1c85c
commit 547af66977
5 changed files with 949 additions and 803 deletions

View file

@ -2,43 +2,43 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, List, Text, withTheme} from 'react-native-paper'; 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 = { type PropsType = {
onPress: Function, onPress: () => void,
color: string, color: string,
item: Object, item: ProximoArticleType,
height: number, height: number,
} };
class ProximoListItem extends React.Component<Props> { class ProximoListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
shouldComponentUpdate() {
return false; return false;
} }
render() { render(): React.Node {
const {props} = this;
return ( return (
<List.Item <List.Item
title={this.props.item.name} title={props.item.name}
description={this.props.item.quantity + ' ' + i18n.t('screens.proximo.inStock')} description={`${props.item.quantity} ${i18n.t(
descriptionStyle={{color: this.props.color}} 'screens.proximo.inStock',
onPress={this.props.onPress} )}`}
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64} descriptionStyle={{color: props.color}}
source={{uri: this.props.item.image}}/>} onPress={props.onPress}
right={() => left={(): React.Node => (
<Text style={{fontWeight: "bold"}}> <Avatar.Image
{this.props.item.price} style={{backgroundColor: 'transparent'}}
</Text>} size={64}
source={{uri: props.item.image}}
/>
)}
right={(): React.Node => (
<Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
)}
style={{ style={{
height: this.props.height, height: props.height,
justifyContent: 'center', justifyContent: 'center',
}} }}
/> />

View file

@ -1,44 +1,55 @@
// @flow // @flow
import * as React from 'react'; 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 {Snackbar} from 'react-native-paper';
import {RefreshControl, View} from "react-native"; import {RefreshControl, View} from 'react-native';
import ErrorView from "./ErrorView";
import BasicLoadingScreen from "./BasicLoadingScreen";
import {withCollapsible} from "../../utils/withCollapsible";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import {Collapsible} from 'react-navigation-collapsible';
import {Collapsible} from "react-navigation-collapsible"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import ErrorView from './ErrorView';
import CollapsibleSectionList from "../Collapsible/CollapsibleSectionList"; 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 = { export type SectionListDataType<T> = Array<{
title: string,
data: Array<T>,
keyExtractor?: (T) => string,
}>;
type PropsType<T> = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
fetchUrl: string, fetchUrl: string,
autoRefreshTime: number, autoRefreshTime: number,
refreshOnFocus: boolean, refreshOnFocus: boolean,
renderItem: (data: { [key: string]: any }) => React.Node, renderItem: (data: {item: T}) => React.Node,
createDataset: (data: { [key: string]: any } | null, isLoading?: boolean) => Array<Object>, createDataset: (
data: ApiGenericDataType | null,
isLoading?: boolean,
) => SectionListDataType<T>,
onScroll: (event: SyntheticEvent<EventTarget>) => void, onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
showError: boolean, showError?: boolean,
itemHeight?: number, itemHeight?: number | null,
updateData?: number, updateData?: number,
renderListHeaderComponent?: (data: { [key: string]: any } | null) => React.Node, renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
renderSectionHeader?: (data: { section: { [key: string]: any } }, isLoading?: boolean) => React.Node, renderSectionHeader?: (
data: {section: {title: string}},
isLoading?: boolean,
) => React.Node,
stickyHeader?: boolean, stickyHeader?: boolean,
}
type State = {
refreshing: boolean,
firstLoading: boolean,
fetchedData: { [key: string]: any } | null,
snackbarVisible: boolean
}; };
type StateType = {
refreshing: boolean,
fetchedData: ApiGenericDataType | null,
snackbarVisible: boolean,
};
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
@ -48,31 +59,37 @@ 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. * 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. * To force the component to update, change the value of updateData.
*/ */
class WebSectionList extends React.PureComponent<Props, State> { class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
static defaultProps = { static defaultProps = {
stickyHeader: false,
updateData: 0,
showError: true, showError: true,
itemHeight: null,
updateData: 0,
renderListHeaderComponent: (): React.Node => null,
renderSectionHeader: (): React.Node => null,
stickyHeader: false,
}; };
refreshInterval: IntervalID; refreshInterval: IntervalID;
lastRefresh: Date | null; lastRefresh: Date | null;
state = { constructor() {
super();
this.state = {
refreshing: false, refreshing: false,
firstLoading: true,
fetchedData: null, fetchedData: null,
snackbarVisible: false snackbarVisible: false,
}; };
}
/** /**
* Registers react navigation events on first screen load. * Registers react navigation events on first screen load.
* Allows to detect when the screen is focused * Allows to detect when the screen is focused
*/ */
componentDidMount() { componentDidMount() {
this.props.navigation.addListener('focus', this.onScreenFocus); const {navigation} = this.props;
this.props.navigation.addListener('blur', this.onScreenBlur); navigation.addListener('focus', this.onScreenFocus);
navigation.addListener('blur', this.onScreenBlur);
this.lastRefresh = null; this.lastRefresh = null;
this.onRefresh(); this.onRefresh();
} }
@ -81,19 +98,18 @@ class WebSectionList extends React.PureComponent<Props, State> {
* Refreshes data when focusing the screen and setup a refresh interval if asked to * Refreshes data when focusing the screen and setup a refresh interval if asked to
*/ */
onScreenFocus = () => { onScreenFocus = () => {
if (this.props.refreshOnFocus && this.lastRefresh) const {props} = this;
this.onRefresh(); if (props.refreshOnFocus && this.lastRefresh) this.onRefresh();
if (this.props.autoRefreshTime > 0) if (props.autoRefreshTime > 0)
this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime) this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
} };
/** /**
* Removes any interval on un-focus * Removes any interval on un-focus
*/ */
onScreenBlur = () => { onScreenBlur = () => {
clearInterval(this.refreshInterval); clearInterval(this.refreshInterval);
} };
/** /**
* Callback used when fetch is successful. * Callback used when fetch is successful.
@ -101,11 +117,10 @@ class WebSectionList extends React.PureComponent<Props, State> {
* *
* @param fetchedData The newly fetched data * @param fetchedData The newly fetched data
*/ */
onFetchSuccess = (fetchedData: { [key: string]: any }) => { onFetchSuccess = (fetchedData: ApiGenericDataType) => {
this.setState({ this.setState({
fetchedData: fetchedData, fetchedData,
refreshing: false, refreshing: false,
firstLoading: false
}); });
this.lastRefresh = new Date(); this.lastRefresh = new Date();
}; };
@ -118,7 +133,6 @@ class WebSectionList extends React.PureComponent<Props, State> {
this.setState({ this.setState({
fetchedData: null, fetchedData: null,
refreshing: false, refreshing: false,
firstLoading: false
}); });
this.showSnackBar(); this.showSnackBar();
}; };
@ -127,128 +141,130 @@ class WebSectionList extends React.PureComponent<Props, State> {
* Refreshes data and shows an animations while doing it * Refreshes data and shows an animations while doing it
*/ */
onRefresh = () => { onRefresh = () => {
const {fetchUrl} = this.props;
let canRefresh; let canRefresh;
if (this.lastRefresh != null) { if (this.lastRefresh != null) {
const last = this.lastRefresh; const last = this.lastRefresh;
canRefresh = (new Date().getTime() - last.getTime()) > MIN_REFRESH_TIME; canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
} else } else canRefresh = true;
canRefresh = true;
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
readData(this.props.fetchUrl) readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
.then(this.onFetchSuccess)
.catch(this.onFetchError);
} }
}; };
/** /**
* Shows the error popup * Shows the error popup
*/ */
showSnackBar = () => this.setState({snackbarVisible: true}); showSnackBar = () => {
this.setState({snackbarVisible: true});
};
/** /**
* Hides the error popup * Hides the error popup
*/ */
hideSnackBar = () => this.setState({snackbarVisible: false}); 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 } }) => { getItemLayout = (
if (this.props.renderSectionHeader != null) { data: T,
return (
<Animatable.View
animation={"fadeInUp"}
duration={500}
useNativeDriver
>
{this.props.renderSectionHeader(data, this.state.refreshing)}
</Animatable.View>
);
} else
return null;
}
renderItem = (data: {
item: { [key: string]: any },
index: number, index: number,
section: { [key: string]: any }, ): {length: number, offset: number, index: number} | null => {
separators: { [key: string]: any }, const {itemHeight} = this.props;
}) => { if (itemHeight == null) return null;
return {
length: itemHeight,
offset: itemHeight * index,
index,
};
};
getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
const {renderSectionHeader} = this.props;
const {refreshing} = this.state;
if (renderSectionHeader != null) {
return ( return (
<Animatable.View <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
animation={"fadeInUp"} {renderSectionHeader(data, refreshing)}
duration={500}
useNativeDriver
>
{this.props.renderItem(data)}
</Animatable.View> </Animatable.View>
); );
} }
return null;
};
getRenderItem = (data: {item: T}): React.Node => {
const {renderItem} = this.props;
return (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
{renderItem(data)}
</Animatable.View>
);
};
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
if (this.props.onScroll) const {onScroll} = this.props;
this.props.onScroll(event); if (onScroll != null) onScroll(event);
} };
render() { render(): React.Node {
const {props, state} = this;
let dataset = []; let dataset = [];
if (this.state.fetchedData != null || (this.state.fetchedData == null && !this.props.showError)) { if (
dataset = this.props.createDataset(this.state.fetchedData, this.state.refreshing); state.fetchedData != null ||
} (state.fetchedData == null && !props.showError)
const {containerPaddingTop} = this.props.collapsibleStack; )
dataset = props.createDataset(state.fetchedData, state.refreshing);
const {containerPaddingTop} = props.collapsibleStack;
return ( return (
<View> <View>
<CollapsibleSectionList <CollapsibleSectionList
sections={dataset} sections={dataset}
extraData={this.props.updateData} extraData={props.updateData}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
progressViewOffset={containerPaddingTop} progressViewOffset={containerPaddingTop}
refreshing={this.state.refreshing} refreshing={state.refreshing}
onRefresh={this.onRefresh} onRefresh={this.onRefresh}
/> />
} }
renderSectionHeader={this.renderSectionHeader} renderSectionHeader={this.getRenderSectionHeader}
renderItem={this.renderItem} renderItem={this.getRenderItem}
stickySectionHeadersEnabled={this.props.stickyHeader} stickySectionHeadersEnabled={props.stickyHeader}
style={{minHeight: '100%'}} style={{minHeight: '100%'}}
ListHeaderComponent={this.props.renderListHeaderComponent != null ListHeaderComponent={
? this.props.renderListHeaderComponent(this.state.fetchedData) props.renderListHeaderComponent != null
: null} ? props.renderListHeaderComponent(state.fetchedData)
ListEmptyComponent={this.state.refreshing : null
? <BasicLoadingScreen/>
: <ErrorView
{...this.props}
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefresh}/>
} }
getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined} ListEmptyComponent={
state.refreshing ? (
<BasicLoadingScreen />
) : (
<ErrorView
navigation={props.navigation}
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefresh}
/>
)
}
getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
onScroll={this.onScroll} onScroll={this.onScroll}
hasTab={true} hasTab
/> />
<Snackbar <Snackbar
visible={this.state.snackbarVisible} visible={state.snackbarVisible}
onDismiss={this.hideSnackBar} onDismiss={this.hideSnackBar}
action={{ action={{
label: 'OK', label: 'OK',
onPress: () => { onPress: () => {},
},
}} }}
duration={4000} duration={4000}
style={{ style={{
bottom: CustomTabBar.TAB_BAR_HEIGHT bottom: CustomTabBar.TAB_BAR_HEIGHT,
}} }}>
> {i18n.t('general.listUpdateFail')}
{i18n.t("general.listUpdateFail")}
</Snackbar> </Snackbar>
</View> </View>
); );

View file

@ -2,55 +2,71 @@
import * as React from 'react'; import * as React from 'react';
import {Image, View} from 'react-native'; 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 {Card, List, Paragraph, Text} from 'react-native-paper';
import CustomTabBar from "../../../components/Tabbar/CustomTabBar"; import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
import {StackNavigationProp} from "@react-navigation/stack"; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
type Props = { const LOGO = 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png';
navigation: StackNavigationProp,
};
const LOGO = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png";
/** /**
* Class defining the proximo about screen. * Class defining the proximo about screen.
*/ */
export default class ProximoAboutScreen extends React.Component<Props> { // eslint-disable-next-line react/prefer-stateless-function
export default class ProximoAboutScreen extends React.Component<null> {
render() { render(): React.Node {
return ( return (
<CollapsibleScrollView style={{padding: 5}}> <CollapsibleScrollView style={{padding: 5}}>
<View style={{ <View
style={{
width: '100%', width: '100%',
height: 100, height: 100,
marginTop: 20, marginTop: 20,
marginBottom: 20, marginBottom: 20,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center',
}}> }}>
<Image <Image
source={{uri: LOGO}} source={{uri: LOGO}}
style={{height: '100%', width: '100%', resizeMode: "contain"}}/> style={{height: '100%', width: '100%', resizeMode: 'contain'}}
/>
</View> </View>
<Text>{i18n.t('screens.proximo.description')}</Text> <Text>{i18n.t('screens.proximo.description')}</Text>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Title <Card.Title
title={i18n.t('screens.proximo.openingHours')} title={i18n.t('screens.proximo.openingHours')}
left={props => <List.Icon {...props} icon={'clock-outline'}/>} left={({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="clock-outline" />
)}
/> />
<Card.Content> <Card.Content>
<Paragraph>18h30 - 19h30</Paragraph> <Paragraph>18h30 - 19h30</Paragraph>
</Card.Content> </Card.Content>
</Card> </Card>
<Card style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> <Card
style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title <Card.Title
title={i18n.t('screens.proximo.paymentMethods')} title={i18n.t('screens.proximo.paymentMethods')}
left={props => <List.Icon {...props} icon={'cash'}/>} left={({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="cash" />
)}
/> />
<Card.Content> <Card.Content>
<Paragraph>{i18n.t('screens.proximo.paymentMethodsDescription')}</Paragraph> <Paragraph>
{i18n.t('screens.proximo.paymentMethodsDescription')}
</Paragraph>
</Card.Content> </Card.Content>
</Card> </Card>
</CollapsibleScrollView> </CollapsibleScrollView>

View file

@ -1,68 +1,84 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, Platform, ScrollView, View} from "react-native"; import {Image, Platform, ScrollView, View} from 'react-native';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import CustomModal from "../../../components/Overrides/CustomModal"; import {
import {RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper"; RadioButton,
import {stringMatchQuery} from "../../../utils/Search"; Searchbar,
import ProximoListItem from "../../../components/Lists/Proximo/ProximoListItem"; Subheading,
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; Text,
import {StackNavigationProp} from "@react-navigation/stack"; Title,
import type {CustomTheme} from "../../../managers/ThemeManager"; withTheme,
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; } 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) { function sortPrice(a: ProximoArticleType, b: ProximoArticleType): number {
return a.price - b.price; return parseInt(a.price, 10) - parseInt(b.price, 10);
} }
function sortPriceReverse(a, b) { function sortPriceReverse(
return b.price - a.price; a: ProximoArticleType,
b: ProximoArticleType,
): number {
return parseInt(b.price, 10) - parseInt(a.price, 10);
} }
function sortName(a, b) { function sortName(a: ProximoArticleType, b: ProximoArticleType): number {
if (a.name.toLowerCase() < b.name.toLowerCase()) if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
return -1; if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
if (a.name.toLowerCase() > b.name.toLowerCase())
return 1;
return 0; return 0;
} }
function sortNameReverse(a, b) { function sortNameReverse(a: ProximoArticleType, b: ProximoArticleType): number {
if (a.name.toLowerCase() < b.name.toLowerCase()) if (a.name.toLowerCase() < b.name.toLowerCase()) return 1;
return 1; if (a.name.toLowerCase() > b.name.toLowerCase()) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase())
return -1;
return 0; return 0;
} }
const LIST_ITEM_HEIGHT = 84; const LIST_ITEM_HEIGHT = 84;
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: { data: { data: Object }, shouldFocusSearchBar: boolean } }, route: {
params: {
data: {data: Array<ProximoArticleType>},
shouldFocusSearchBar: boolean,
},
},
theme: CustomTheme, theme: CustomTheme,
} };
type State = { type StateType = {
currentSortMode: number, currentSortMode: number,
modalCurrentDisplayItem: React.Node, modalCurrentDisplayItem: React.Node,
currentSearchString: string, 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<Props, State> { class ProximoListScreen extends React.Component<PropsType, StateType> {
modalRef: Modalize | null;
listData: Array<ProximoArticleType>;
modalRef: Object;
listData: Array<Object>;
shouldFocusSearchBar: boolean; shouldFocusSearchBar: boolean;
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
this.listData = this.props.route.params['data']['data'].sort(sortName); this.listData = props.route.params.data.data.sort(sortName);
this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar']; this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar;
this.state = { this.state = {
currentSearchString: '', currentSearchString: '',
currentSortMode: 3, currentSortMode: 3,
@ -70,69 +86,71 @@ class ProximoListScreen extends React.Component<Props, State> {
}; };
} }
/** /**
* Creates the header content * Creates the header content
*/ */
componentDidMount() { componentDidMount() {
this.props.navigation.setOptions({ const {navigation} = this.props;
navigation.setOptions({
headerRight: this.getSortMenuButton, headerRight: this.getSortMenuButton,
headerTitle: this.getSearchBar, headerTitle: this.getSearchBar,
headerBackTitleVisible: false, headerBackTitleVisible: false,
headerTitleContainerStyle: Platform.OS === 'ios' ? headerTitleContainerStyle:
{marginHorizontal: 0, width: '70%'} : Platform.OS === 'ios'
{marginHorizontal: 0, right: 50, left: 50}, ? {marginHorizontal: 0, width: '70%'}
: {marginHorizontal: 0, right: 50, left: 50},
}); });
} }
/**
* Gets the header search bar
*
* @return {*}
*/
getSearchBar = () => {
return (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
};
/**
* Gets the sort menu header button
*
* @return {*}
*/
getSortMenuButton = () => {
return <MaterialHeaderButtons>
<Item title="main" iconName="sort" onPress={this.onSortMenuPress}/>
</MaterialHeaderButtons>;
};
/** /**
* Callback used when clicking on the sort menu button. * Callback used when clicking on the sort menu button.
* It will open the modal to show a sort selection * It will open the modal to show a sort selection
*/ */
onSortMenuPress = () => { onSortMenuPress = () => {
this.setState({ this.setState({
modalCurrentDisplayItem: this.getModalSortMenu() modalCurrentDisplayItem: this.getModalSortMenu(),
}); });
if (this.modalRef) { if (this.modalRef) {
this.modalRef.open(); this.modalRef.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();
}
}
/** /**
* Sets the current sort mode. * Sets the current sort mode.
* *
* @param mode The number representing the mode * @param mode The number representing the mode
*/ */
setSortMode(mode: number) { setSortMode(mode: string) {
const {currentSortMode} = this.state;
const currentMode = parseInt(mode, 10);
this.setState({ this.setState({
currentSortMode: mode, currentSortMode: currentMode,
}); });
switch (mode) { switch (currentMode) {
case 1: case 1:
this.listData.sort(sortPrice); this.listData.sort(sortPrice);
break; break;
@ -145,10 +163,11 @@ class ProximoListScreen extends React.Component<Props, State> {
case 4: case 4:
this.listData.sort(sortNameReverse); this.listData.sort(sortNameReverse);
break; break;
default:
this.listData.sort(sortName);
break;
} }
if (this.modalRef && mode !== this.state.currentSortMode) { if (this.modalRef && currentMode !== currentSortMode) this.modalRef.close();
this.modalRef.close();
}
} }
/** /**
@ -157,24 +176,40 @@ class ProximoListScreen extends React.Component<Props, State> {
* @param availableStock The quantity available * @param availableStock The quantity available
* @return * @return
*/ */
getStockColor(availableStock: number) { getStockColor(availableStock: number): string {
const {theme} = this.props;
let color: string; let color: string;
if (availableStock > 3) if (availableStock > 3) color = theme.colors.success;
color = this.props.theme.colors.success; else if (availableStock > 0) color = theme.colors.warning;
else if (availableStock > 0) else color = theme.colors.danger;
color = this.props.theme.colors.warning;
else
color = this.props.theme.colors.danger;
return color; return color;
} }
/** /**
* Callback used when the search changes * Gets the sort menu header button
* *
* @param str The new search string * @return {*}
*/ */
onSearchStringChange = (str: string) => { getSortMenuButton = (): React.Node => {
this.setState({currentSearchString: str}) return (
<MaterialHeaderButtons>
<Item title="main" iconName="sort" onPress={this.onSortMenuPress} />
</MaterialHeaderButtons>
);
};
/**
* Gets the header search bar
*
* @return {*}
*/
getSearchBar = (): React.Node => {
return (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
}; };
/** /**
@ -183,30 +218,41 @@ class ProximoListScreen extends React.Component<Props, State> {
* @param item The article to display * @param item The article to display
* @return {*} * @return {*}
*/ */
getModalItemContent(item: Object) { getModalItemContent(item: ProximoArticleType): React.Node {
return ( return (
<View style={{ <View
style={{
flex: 1, flex: 1,
padding: 20 padding: 20,
}}> }}>
<Title>{item.name}</Title> <Title>{item.name}</Title>
<View style={{ <View
style={{
flexDirection: 'row', flexDirection: 'row',
width: '100%', width: '100%',
marginTop: 10, marginTop: 10,
}}> }}>
<Subheading style={{ <Subheading
color: this.getStockColor(parseInt(item.quantity)), style={{
color: this.getStockColor(parseInt(item.quantity, 10)),
}}> }}>
{item.quantity + ' ' + i18n.t('screens.proximo.inStock')} {`${item.quantity} ${i18n.t('screens.proximo.inStock')}`}
</Subheading> </Subheading>
<Subheading style={{marginLeft: 'auto'}}>{item.price}</Subheading> <Subheading style={{marginLeft: 'auto'}}>{item.price}</Subheading>
</View> </View>
<ScrollView> <ScrollView>
<View style={{width: '100%', height: 150, marginTop: 20, marginBottom: 20}}> <View
<Image style={{flex: 1, resizeMode: "contain"}} style={{
source={{uri: item.image}}/> width: '100%',
height: 150,
marginTop: 20,
marginBottom: 20,
}}>
<Image
style={{flex: 1, resizeMode: 'contain'}}
source={{uri: item.image}}
/>
</View> </View>
<Text>{item.description}</Text> <Text>{item.description}</Text>
</ScrollView> </ScrollView>
@ -219,51 +265,56 @@ class ProximoListScreen extends React.Component<Props, State> {
* *
* @return {*} * @return {*}
*/ */
getModalSortMenu() { getModalSortMenu(): React.Node {
const {currentSortMode} = this.state;
return ( return (
<View style={{ <View
style={{
flex: 1, flex: 1,
padding: 20 padding: 20,
}}> }}>
<Title style={{marginBottom: 10}}>{i18n.t('screens.proximo.sortOrder')}</Title> <Title style={{marginBottom: 10}}>
{i18n.t('screens.proximo.sortOrder')}
</Title>
<RadioButton.Group <RadioButton.Group
onValueChange={value => this.setSortMode(value)} onValueChange={(value: string) => {
value={this.state.currentSortMode} this.setSortMode(value);
> }}
<RadioButton.Item label={i18n.t('screens.proximo.sortPrice')} value={1}/> value={currentSortMode}>
<RadioButton.Item label={i18n.t('screens.proximo.sortPriceReverse')} value={2}/> <RadioButton.Item
<RadioButton.Item label={i18n.t('screens.proximo.sortName')} value={3}/> label={i18n.t('screens.proximo.sortPrice')}
<RadioButton.Item label={i18n.t('screens.proximo.sortNameReverse')} value={4}/> value={1}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPriceReverse')}
value={2}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortName')}
value={3}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortNameReverse')}
value={4}
/>
</RadioButton.Group> </RadioButton.Group>
</View> </View>
); );
} }
/**
* 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 * Gets a render item for the given article
* *
* @param item The article to render * @param item The article to render
* @return {*} * @return {*}
*/ */
renderItem = ({item}: Object) => { getRenderItem = ({item}: {item: ProximoArticleType}): React.Node => {
if (stringMatchQuery(item.name, this.state.currentSearchString)) { const {currentSearchString} = this.state;
const onPress = this.onListItemPress.bind(this, item); if (stringMatchQuery(item.name, currentSearchString)) {
const color = this.getStockColor(parseInt(item.quantity)); const onPress = () => {
this.onListItemPress(item);
};
const color = this.getStockColor(parseInt(item.quantity, 10));
return ( return (
<ProximoListItem <ProximoListItem
item={item} item={item}
@ -272,7 +323,7 @@ class ProximoListScreen extends React.Component<Props, State> {
height={LIST_ITEM_HEIGHT} height={LIST_ITEM_HEIGHT}
/> />
); );
} else }
return null; return null;
}; };
@ -280,38 +331,45 @@ class ProximoListScreen extends React.Component<Props, State> {
* Extracts a key for the given article * Extracts a key for the given article
* *
* @param item The article to extract the key from * @param item The article to extract the key from
* @return {*} The extracted key * @return {string} The extracted key
*/ */
keyExtractor(item: Object) { keyExtractor = (item: ProximoArticleType): string => item.name + item.code;
return item.name + item.code;
}
/** /**
* Callback used when receiving the modal ref * Callback used when receiving the modal ref
* *
* @param ref * @param ref
*/ */
onModalRef = (ref: Object) => { onModalRef = (ref: Modalize) => {
this.modalRef = ref; this.modalRef = ref;
}; };
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); itemLayout = (
data: ProximoArticleType,
index: number,
): {length: number, offset: number, index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
render() { render(): React.Node {
const {state} = this;
return ( return (
<View style={{ <View
height: '100%' style={{
height: '100%',
}}> }}>
<CustomModal onRef={this.onModalRef}> <CustomModal onRef={this.onModalRef}>
{this.state.modalCurrentDisplayItem} {state.modalCurrentDisplayItem}
</CustomModal> </CustomModal>
<CollapsibleFlatList <CollapsibleFlatList
data={this.listData} data={this.listData}
extraData={this.state.currentSearchString + this.state.currentSortMode} extraData={state.currentSearchString + state.currentSortMode}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
renderItem={this.renderItem} renderItem={this.getRenderItem}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true} removeClippedSubviews
getItemLayout={this.itemLayout} getItemLayout={this.itemLayout}
initialNumToRender={10} initialNumToRender={10}
/> />

View file

@ -1,168 +1,81 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native' import i18n from 'i18n-js';
import i18n from "i18n-js";
import WebSectionList from "../../../components/Screens/WebSectionList";
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import WebSectionList from '../../../components/Screens/WebSectionList';
import type {CustomTheme} from "../../../managers/ThemeManager"; 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; const LIST_ITEM_HEIGHT = 84;
type Props = { export type ProximoCategoryType = {
name: string,
icon: string,
id: string,
};
export type ProximoArticleType = {
name: string,
description: string,
quantity: string,
price: string,
code: string,
id: string,
type: Array<string>,
image: string,
};
export type ProximoMainListItemType = {
type: ProximoCategoryType,
data: Array<ProximoArticleType>,
};
export type ProximoDataType = {
types: Array<ProximoCategoryType>,
articles: Array<ProximoArticleType>,
};
type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
} };
type State = {
fetchedData: Object,
}
/** /**
* Class defining the main proximo screen. * Class defining the main proximo screen.
* This screen shows the different categories of articles offered by proximo. * This screen shows the different categories of articles offered by proximo.
*/ */
class ProximoMainScreen extends React.Component<Props, State> { class ProximoMainScreen extends React.Component<PropsType> {
articles: Object;
/** /**
* Function used to sort items in the list. * Function used to sort items in the list.
* Makes the All category stick to the top and sorts the others by name ascending * Makes the All category sticks to the top and sorts the others by name ascending
* *
* @param a * @param a
* @param b * @param b
* @return {number} * @return {number}
*/ */
static sortFinalData(a: Object, b: Object) { static sortFinalData(
let str1 = a.type.name.toLowerCase(); a: ProximoMainListItemType,
let str2 = b.type.name.toLowerCase(); b: ProximoMainListItemType,
): number {
const str1 = a.type.name.toLowerCase();
const str2 = b.type.name.toLowerCase();
// Make 'All' category with id -1 stick to the top // Make 'All' category with id -1 stick to the top
if (a.type.id === -1) if (a.type.id === -1) return -1;
return -1; if (b.type.id === -1) return 1;
if (b.type.id === -1)
return 1;
// Sort others by name ascending // Sort others by name ascending
if (str1 < str2) if (str1 < str2) return -1;
return -1; if (str1 > str2) return 1;
if (str1 > str2)
return 1;
return 0; return 0;
} }
/**
* Creates header button
*/
componentDidMount() {
const rightButton = this.getHeaderButtons.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
}
/**
* 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);
};
/**
* Callback used when the about button is pressed.
* This will open the ProximoAboutScreen
*/
onPressAboutBtn = () => {
this.props.navigation.navigate('proximo-about');
}
/**
* Gets the header buttons
* @return {*}
*/
getHeaderButtons() {
return <MaterialHeaderButtons>
<Item title="magnify" iconName="magnify" onPress={this.onPressSearchBtn}/>
<Item title="information" iconName="information" onPress={this.onPressAboutBtn}/>
</MaterialHeaderButtons>;
}
/**
* 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 * Get an array of available articles (in stock) of the given type
* *
@ -170,62 +83,205 @@ class ProximoMainScreen extends React.Component<Props, State> {
* @param type The type of articles to find (undefined for any type) * @param type The type of articles to find (undefined for any type)
* @return {Array} The array of available articles * @return {Array} The array of available articles
*/ */
getAvailableArticles(articles: Array<Object>, type: ?Object) { static getAvailableArticles(
let availableArticles = []; articles: Array<ProximoArticleType> | null,
for (let k = 0; k < articles.length; k++) { type: ?ProximoCategoryType,
if ((type !== undefined && type !== null && articles[k]['type'].includes(type['id']) ): Array<ProximoArticleType> {
|| type === undefined) const availableArticles = [];
&& parseInt(articles[k]['quantity']) > 0) { if (articles != null) {
availableArticles.push(articles[k]); articles.forEach((article: ProximoArticleType) => {
} if (
((type != null && article.type.includes(type.id)) || type == null) &&
parseInt(article.quantity, 10) > 0
)
availableArticles.push(article);
});
} }
return availableArticles; return availableArticles;
} }
articles: Array<ProximoArticleType> | null;
/**
* 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 = () => {
const {navigation} = this.props;
navigation.navigate('proximo-about');
};
/**
* Gets the header buttons
* @return {*}
*/
getHeaderButtons(): React.Node {
return (
<MaterialHeaderButtons>
<Item
title="magnify"
iconName="magnify"
onPress={this.onPressSearchBtn}
/>
<Item
title="information"
iconName="information"
onPress={this.onPressAboutBtn}
/>
</MaterialHeaderButtons>
);
}
/**
* 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 * Gets the given category render item
* *
* @param item The category to render * @param item The category to render
* @return {*} * @return {*}
*/ */
getRenderItem = ({item}: Object) => { getRenderItem = ({item}: {item: ProximoMainListItemType}): React.Node => {
let dataToSend = { const {navigation, theme} = this.props;
const dataToSend = {
shouldFocusSearchBar: false, shouldFocusSearchBar: false,
data: item, data: item,
}; };
const subtitle = item.data.length + " " + (item.data.length > 1 ? i18n.t('screens.proximo.articles') : i18n.t('screens.proximo.article')); const subtitle = `${item.data.length} ${
const onPress = this.props.navigation.navigate.bind(this, 'proximo-list', dataToSend); 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) { if (item.data.length > 0) {
return ( return (
<List.Item <List.Item
title={item.type.name} title={item.type.name}
description={subtitle} description={subtitle}
onPress={onPress} onPress={onPress}
left={props => <List.Icon left={({size}: {size: number}): React.Node => (
{...props} <List.Icon
size={size}
icon={item.type.icon} icon={item.type.icon}
color={this.props.theme.colors.primary}/>} color={theme.colors.primary}
right={props => <List.Icon {...props} icon={'chevron-right'}/>} />
)}
right={({size, color}: {size: number, color: string}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
)}
style={{ style={{
height: LIST_ITEM_HEIGHT, height: LIST_ITEM_HEIGHT,
justifyContent: 'center', justifyContent: 'center',
}} }}
/> />
); );
} else }
return <View/>; return null;
};
/**
* Creates the dataset to be used in the FlatList
*
* @param fetchedData
* @return {*}
* */
createDataset = (
fetchedData: ProximoDataType | null,
): SectionListDataType<ProximoMainListItemType> => {
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<ProximoMainListItemType> {
const finalData: Array<ProximoMainListItemType> = [];
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;
} }
render() { render(): React.Node {
const nav = this.props.navigation; const {navigation} = this.props;
return ( return (
<WebSectionList <WebSectionList
createDataset={this.createDataset} createDataset={this.createDataset}
navigation={nav} navigation={navigation}
autoRefreshTime={0} autoRefreshTime={0}
refreshOnFocus={false} refreshOnFocus={false}
fetchUrl={DATA_URL} fetchUrl={DATA_URL}
renderItem={this.getRenderItem}/> renderItem={this.getRenderItem}
/>
); );
} }
} }