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,48 +2,48 @@
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 {
return false;
}
colors: Object; render(): React.Node {
const {props} = this;
constructor(props) { return (
super(props); <List.Item
this.colors = props.theme.colors; title={props.item.name}
} description={`${props.item.quantity} ${i18n.t(
'screens.proximo.inStock',
shouldComponentUpdate() { )}`}
return false; descriptionStyle={{color: props.color}}
} onPress={props.onPress}
left={(): React.Node => (
render() { <Avatar.Image
return ( style={{backgroundColor: 'transparent'}}
<List.Item size={64}
title={this.props.item.name} source={{uri: props.item.image}}
description={this.props.item.quantity + ' ' + i18n.t('screens.proximo.inStock')} />
descriptionStyle={{color: this.props.color}} )}
onPress={this.props.onPress} right={(): React.Node => (
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64} <Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
source={{uri: this.props.item.image}}/>} )}
right={() => style={{
<Text style={{fontWeight: "bold"}}> height: props.height,
{this.props.item.price} justifyContent: 'center',
</Text>} }}
style={{ />
height: this.props.height, );
justifyContent: 'center', }
}}
/>
);
}
} }
export default withTheme(ProximoListItem); export default withTheme(ProximoListItem);

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<{
navigation: StackNavigationProp, title: string,
fetchUrl: string, data: Array<T>,
autoRefreshTime: number, keyExtractor?: (T) => string,
refreshOnFocus: boolean, }>;
renderItem: (data: { [key: string]: any }) => React.Node,
createDataset: (data: { [key: string]: any } | null, isLoading?: boolean) => Array<Object>,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible,
showError: boolean, type PropsType<T> = {
itemHeight?: number, navigation: StackNavigationProp,
updateData?: number, fetchUrl: string,
renderListHeaderComponent?: (data: { [key: string]: any } | null) => React.Node, autoRefreshTime: number,
renderSectionHeader?: (data: { section: { [key: string]: any } }, isLoading?: boolean) => React.Node, refreshOnFocus: boolean,
stickyHeader?: boolean, renderItem: (data: {item: T}) => React.Node,
} createDataset: (
data: ApiGenericDataType | null,
isLoading?: boolean,
) => SectionListDataType<T>,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible,
type State = { showError?: boolean,
refreshing: boolean, itemHeight?: number | null,
firstLoading: boolean, updateData?: number,
fetchedData: { [key: string]: any } | null, renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
snackbarVisible: boolean 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; 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. * 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 = {
showError: true,
itemHeight: null,
updateData: 0,
renderListHeaderComponent: (): React.Node => null,
renderSectionHeader: (): React.Node => null,
stickyHeader: false,
};
static defaultProps = { refreshInterval: IntervalID;
stickyHeader: false,
updateData: 0, lastRefresh: Date | null;
showError: true,
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, * Refreshes data when focusing the screen and setup a refresh interval if asked to
firstLoading: true, */
fetchedData: null, onScreenFocus = () => {
snackbarVisible: false 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,
}; };
};
/** getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
* Registers react navigation events on first screen load. const {renderSectionHeader} = this.props;
* Allows to detect when the screen is focused const {refreshing} = this.state;
*/ if (renderSectionHeader != null) {
componentDidMount() { return (
this.props.navigation.addListener('focus', this.onScreenFocus); <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
this.props.navigation.addListener('blur', this.onScreenBlur); {renderSectionHeader(data, refreshing)}
this.lastRefresh = null; </Animatable.View>
this.onRefresh(); );
} }
return null;
};
/** getRenderItem = (data: {item: T}): React.Node => {
* Refreshes data when focusing the screen and setup a refresh interval if asked to const {renderItem} = this.props;
*/ return (
onScreenFocus = () => { <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
if (this.props.refreshOnFocus && this.lastRefresh) {renderItem(data)}
this.onRefresh(); </Animatable.View>
if (this.props.autoRefreshTime > 0) );
this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime) };
}
/** onScroll = (event: SyntheticEvent<EventTarget>) => {
* Removes any interval on un-focus const {onScroll} = this.props;
*/ if (onScroll != null) onScroll(event);
onScreenBlur = () => { };
clearInterval(this.refreshInterval);
}
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);
/** const {containerPaddingTop} = props.collapsibleStack;
* Callback used when fetch is successful. return (
* It will update the displayed data and stop the refresh animation <View>
* <CollapsibleSectionList
* @param fetchedData The newly fetched data sections={dataset}
*/ extraData={props.updateData}
onFetchSuccess = (fetchedData: { [key: string]: any }) => { refreshControl={
this.setState({ <RefreshControl
fetchedData: fetchedData, progressViewOffset={containerPaddingTop}
refreshing: false, refreshing={state.refreshing}
firstLoading: false onRefresh={this.onRefresh}
}); />
this.lastRefresh = new Date(); }
}; renderSectionHeader={this.getRenderSectionHeader}
renderItem={this.getRenderItem}
/** stickySectionHeadersEnabled={props.stickyHeader}
* Callback used when fetch encountered an error. style={{minHeight: '100%'}}
* It will reset the displayed data and show an error. ListHeaderComponent={
*/ props.renderListHeaderComponent != null
onFetchError = () => { ? props.renderListHeaderComponent(state.fetchedData)
this.setState({ : null
fetchedData: null, }
refreshing: false, ListEmptyComponent={
firstLoading: false state.refreshing ? (
}); <BasicLoadingScreen />
this.showSnackBar(); ) : (
}; <ErrorView
navigation={props.navigation}
/** errorCode={ERROR_TYPE.CONNECTION_ERROR}
* Refreshes data and shows an animations while doing it onRefresh={this.onRefresh}
*/ />
onRefresh = () => { )
let canRefresh; }
if (this.lastRefresh != null) { getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
const last = this.lastRefresh; onScroll={this.onScroll}
canRefresh = (new Date().getTime() - last.getTime()) > MIN_REFRESH_TIME; hasTab
} else />
canRefresh = true; <Snackbar
if (canRefresh) { visible={state.snackbarVisible}
this.setState({refreshing: true}); onDismiss={this.hideSnackBar}
readData(this.props.fetchUrl) action={{
.then(this.onFetchSuccess) label: 'OK',
.catch(this.onFetchError); onPress: () => {},
} }}
}; duration={4000}
style={{
/** bottom: CustomTabBar.TAB_BAR_HEIGHT,
* Shows the error popup }}>
*/ {i18n.t('general.listUpdateFail')}
showSnackBar = () => this.setState({snackbarVisible: true}); </Snackbar>
</View>
/** );
* 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 (
<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,
section: { [key: string]: any },
separators: { [key: string]: any },
}) => {
return (
<Animatable.View
animation={"fadeInUp"}
duration={500}
useNativeDriver
>
{this.props.renderItem(data)}
</Animatable.View>
);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
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 (
<View>
<CollapsibleSectionList
sections={dataset}
extraData={this.props.updateData}
refreshControl={
<RefreshControl
progressViewOffset={containerPaddingTop}
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
/>
}
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
? <BasicLoadingScreen/>
: <ErrorView
{...this.props}
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefresh}/>
}
getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined}
onScroll={this.onScroll}
hasTab={true}
/>
<Snackbar
visible={this.state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: () => {
},
}}
duration={4000}
style={{
bottom: CustomTabBar.TAB_BAR_HEIGHT
}}
>
{i18n.t("general.listUpdateFail")}
</Snackbar>
</View>
);
}
} }
export default withCollapsible(WebSectionList); export default withCollapsible(WebSectionList);

View file

@ -2,58 +2,74 @@
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
width: '100%', style={{
height: 100, width: '100%',
marginTop: 20, height: 100,
marginBottom: 20, marginTop: 20,
justifyContent: 'center', marginBottom: 20,
alignItems: 'center' justifyContent: 'center',
}}> alignItems: 'center',
<Image }}>
source={{uri: LOGO}} <Image
style={{height: '100%', width: '100%', resizeMode: "contain"}}/> source={{uri: LOGO}}
</View> style={{height: '100%', width: '100%', resizeMode: 'contain'}}
<Text>{i18n.t('screens.proximo.description')}</Text> />
<Card style={{margin: 5}}> </View>
<Card.Title <Text>{i18n.t('screens.proximo.description')}</Text>
title={i18n.t('screens.proximo.openingHours')} <Card style={{margin: 5}}>
left={props => <List.Icon {...props} icon={'clock-outline'}/>} <Card.Title
/> title={i18n.t('screens.proximo.openingHours')}
<Card.Content> left={({
<Paragraph>18h30 - 19h30</Paragraph> size,
</Card.Content> color,
</Card> }: {
<Card style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> size: number,
<Card.Title color: string,
title={i18n.t('screens.proximo.paymentMethods')} }): React.Node => (
left={props => <List.Icon {...props} icon={'cash'}/>} <List.Icon size={size} color={color} icon="clock-outline" />
/> )}
<Card.Content> />
<Paragraph>{i18n.t('screens.proximo.paymentMethodsDescription')}</Paragraph> <Card.Content>
</Card.Content> <Paragraph>18h30 - 19h30</Paragraph>
</Card> </Card.Content>
</CollapsibleScrollView> </Card>
); <Card
} style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.proximo.paymentMethods')}
left={({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="cash" />
)}
/>
<Card.Content>
<Paragraph>
{i18n.t('screens.proximo.paymentMethodsDescription')}
</Paragraph>
</Card.Content>
</Card>
</CollapsibleScrollView>
);
}
} }

View file

@ -1,323 +1,381 @@
// @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 0;
return 1;
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 0;
return -1;
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: {
theme: CustomTheme, params: {
} data: {data: Array<ProximoArticleType>},
shouldFocusSearchBar: boolean,
},
},
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;
modalRef: Object; listData: Array<ProximoArticleType>;
listData: Array<Object>;
shouldFocusSearchBar: boolean;
constructor(props) { shouldFocusSearchBar: boolean;
super(props);
this.listData = this.props.route.params['data']['data'].sort(sortName); constructor(props: PropsType) {
this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar']; super(props);
this.state = { this.listData = props.route.params.data.data.sort(sortName);
currentSearchString: '', this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar;
currentSortMode: 3, this.state = {
modalCurrentDisplayItem: null, 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 * Callback used when clicking an article in the list.
*/ * It opens the modal to show detailed information about the article
componentDidMount() { *
this.props.navigation.setOptions({ * @param item The article pressed
headerRight: this.getSortMenuButton, */
headerTitle: this.getSearchBar, onListItemPress(item: ProximoArticleType) {
headerBackTitleVisible: false, this.setState({
headerTitleContainerStyle: Platform.OS === 'ios' ? modalCurrentDisplayItem: this.getModalItemContent(item),
{marginHorizontal: 0, width: '70%'} : });
{marginHorizontal: 0, right: 50, left: 50}, if (this.modalRef) {
}); this.modalRef.open();
} }
}
/** /**
* Gets the header search bar * Sets the current sort mode.
* *
* @return {*} * @param mode The number representing the mode
*/ */
getSearchBar = () => { setSortMode(mode: string) {
return ( const {currentSortMode} = this.state;
<Searchbar const currentMode = parseInt(mode, 10);
placeholder={i18n.t('screens.proximo.search')} this.setState({
onChangeText={this.onSearchStringChange} 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;
}
if (this.modalRef && currentMode !== currentSortMode) this.modalRef.close();
}
/**
* Gets a color depending on the quantity available
*
* @param availableStock The quantity available
* @return
*/
getStockColor(availableStock: number): string {
const {theme} = this.props;
let color: string;
if (availableStock > 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 (
<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}
/>
);
};
/**
* Gets the modal content depending on the given article
*
* @param item The article to display
* @return {*}
*/
getModalItemContent(item: ProximoArticleType): React.Node {
return (
<View
style={{
flex: 1,
padding: 20,
}}>
<Title>{item.name}</Title>
<View
style={{
flexDirection: 'row',
width: '100%',
marginTop: 10,
}}>
<Subheading
style={{
color: this.getStockColor(parseInt(item.quantity, 10)),
}}>
{`${item.quantity} ${i18n.t('screens.proximo.inStock')}`}
</Subheading>
<Subheading style={{marginLeft: 'auto'}}>{item.price}</Subheading>
</View>
<ScrollView>
<View
style={{
width: '100%',
height: 150,
marginTop: 20,
marginBottom: 20,
}}>
<Image
style={{flex: 1, resizeMode: 'contain'}}
source={{uri: item.image}}
/> />
); </View>
}; <Text>{item.description}</Text>
</ScrollView>
</View>
);
}
/** /**
* Gets the sort menu header button * Gets the modal content to display a sort menu
* *
* @return {*} * @return {*}
*/ */
getSortMenuButton = () => { getModalSortMenu(): React.Node {
return <MaterialHeaderButtons> const {currentSortMode} = this.state;
<Item title="main" iconName="sort" onPress={this.onSortMenuPress}/> return (
</MaterialHeaderButtons>; <View
}; style={{
flex: 1,
padding: 20,
}}>
<Title style={{marginBottom: 10}}>
{i18n.t('screens.proximo.sortOrder')}
</Title>
<RadioButton.Group
onValueChange={(value: string) => {
this.setSortMode(value);
}}
value={currentSortMode}>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPrice')}
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>
</View>
);
}
/** /**
* Callback used when clicking on the sort menu button. * Gets a render item for the given article
* It will open the modal to show a sort selection *
*/ * @param item The article to render
onSortMenuPress = () => { * @return {*}
this.setState({ */
modalCurrentDisplayItem: this.getModalSortMenu() getRenderItem = ({item}: {item: ProximoArticleType}): React.Node => {
}); const {currentSearchString} = this.state;
if (this.modalRef) { if (stringMatchQuery(item.name, currentSearchString)) {
this.modalRef.open(); const onPress = () => {
} this.onListItemPress(item);
}; };
const color = this.getStockColor(parseInt(item.quantity, 10));
/** return (
* Sets the current sort mode. <ProximoListItem
* item={item}
* @param mode The number representing the mode onPress={onPress}
*/ color={color}
setSortMode(mode: number) { height={LIST_ITEM_HEIGHT}
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();
}
} }
return null;
};
/** /**
* Gets a color depending on the quantity available * Extracts a key for the given article
* *
* @param availableStock The quantity available * @param item The article to extract the key from
* @return * @return {string} The extracted key
*/ */
getStockColor(availableStock: number) { keyExtractor = (item: ProximoArticleType): string => item.name + item.code;
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;
}
/** /**
* Callback used when the search changes * Callback used when receiving the modal ref
* *
* @param str The new search string * @param ref
*/ */
onSearchStringChange = (str: string) => { onModalRef = (ref: Modalize) => {
this.setState({currentSearchString: str}) this.modalRef = ref;
}; };
/** itemLayout = (
* Gets the modal content depending on the given article data: ProximoArticleType,
* index: number,
* @param item The article to display ): {length: number, offset: number, index: number} => ({
* @return {*} length: LIST_ITEM_HEIGHT,
*/ offset: LIST_ITEM_HEIGHT * index,
getModalItemContent(item: Object) { index,
return ( });
<View style={{
flex: 1,
padding: 20
}}>
<Title>{item.name}</Title>
<View style={{
flexDirection: 'row',
width: '100%',
marginTop: 10,
}}>
<Subheading style={{
color: this.getStockColor(parseInt(item.quantity)),
}}>
{item.quantity + ' ' + i18n.t('screens.proximo.inStock')}
</Subheading>
<Subheading style={{marginLeft: 'auto'}}>{item.price}</Subheading>
</View>
<ScrollView> render(): React.Node {
<View style={{width: '100%', height: 150, marginTop: 20, marginBottom: 20}}> const {state} = this;
<Image style={{flex: 1, resizeMode: "contain"}} return (
source={{uri: item.image}}/> <View
</View> style={{
<Text>{item.description}</Text> height: '100%',
</ScrollView> }}>
</View> <CustomModal onRef={this.onModalRef}>
); {state.modalCurrentDisplayItem}
} </CustomModal>
<CollapsibleFlatList
/** data={this.listData}
* Gets the modal content to display a sort menu extraData={state.currentSearchString + state.currentSortMode}
* keyExtractor={this.keyExtractor}
* @return {*} renderItem={this.getRenderItem}
*/ // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
getModalSortMenu() { removeClippedSubviews
return ( getItemLayout={this.itemLayout}
<View style={{ initialNumToRender={10}
flex: 1, />
padding: 20 </View>
}}> );
<Title style={{marginBottom: 10}}>{i18n.t('screens.proximo.sortOrder')}</Title> }
<RadioButton.Group
onValueChange={value => this.setSortMode(value)}
value={this.state.currentSortMode}
>
<RadioButton.Item label={i18n.t('screens.proximo.sortPrice')} 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>
</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
*
* @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 (
<ProximoListItem
item={item}
onPress={onPress}
color={color}
height={LIST_ITEM_HEIGHT}
/>
);
} 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 (
<View style={{
height: '100%'
}}>
<CustomModal onRef={this.onModalRef}>
{this.state.modalCurrentDisplayItem}
</CustomModal>
<CollapsibleFlatList
data={this.listData}
extraData={this.state.currentSearchString + this.state.currentSortMode}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true}
getItemLayout={this.itemLayout}
initialNumToRender={10}
/>
</View>
);
}
} }
export default withTheme(ProximoListScreen); export default withTheme(ProximoListScreen);

View file

@ -1,233 +1,289 @@
// @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 = {
navigation: StackNavigationProp, name: string,
theme: CustomTheme, icon: string,
} id: string,
};
type State = { export type ProximoArticleType = {
fetchedData: Object, 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,
theme: CustomTheme,
};
/** /**
* 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> {
/**
* 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;
/** // Sort others by name ascending
* Function used to sort items in the list. if (str1 < str2) return -1;
* Makes the All category stick to the top and sorts the others by name ascending if (str1 > str2) return 1;
* return 0;
* @param a }
* @param b
* @return {number}
*/
static sortFinalData(a: Object, b: Object) {
let str1 = a.type.name.toLowerCase();
let str2 = b.type.name.toLowerCase();
// Make 'All' category with id -1 stick to the top /**
if (a.type.id === -1) * Get an array of available articles (in stock) of the given type
return -1; *
if (b.type.id === -1) * @param articles The list of all articles
return 1; * @param type The type of articles to find (undefined for any type)
* @return {Array} The array of available articles
// Sort others by name ascending */
if (str1 < str2) static getAvailableArticles(
return -1; articles: Array<ProximoArticleType> | null,
if (str1 > str2) type: ?ProximoCategoryType,
return 1; ): Array<ProximoArticleType> {
return 0; 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;
}
/** articles: Array<ProximoArticleType> | null;
* Creates header button
*/
componentDidMount() {
const rightButton = this.getHeaderButtons.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
}
/** /**
* Callback used when the search button is pressed. * Creates header button
* This will open a new ProximoListScreen with all items displayed */
*/ componentDidMount() {
onPressSearchBtn = () => { const {navigation} = this.props;
let searchScreenData = { navigation.setOptions({
shouldFocusSearchBar: true, headerRight: (): React.Node => this.getHeaderButtons(),
data: { });
type: { }
id: "0",
name: i18n.t('screens.proximo.all'), /**
icon: 'star' * Callback used when the search button is pressed.
}, * This will open a new ProximoListScreen with all items displayed
data: this.articles !== undefined ? */
this.getAvailableArticles(this.articles, undefined) : [] onPressSearchBtn = () => {
}, const {navigation} = this.props;
}; const searchScreenData = {
this.props.navigation.navigate('proximo-list', 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. * Callback used when the about button is pressed.
* This will open the ProximoAboutScreen * This will open the ProximoAboutScreen
*/ */
onPressAboutBtn = () => { onPressAboutBtn = () => {
this.props.navigation.navigate('proximo-about'); 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
*
* @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 (
<List.Item
title={item.type.name}
description={subtitle}
onPress={onPress}
left={({size}: {size: number}): React.Node => (
<List.Icon
size={size}
icon={item.type.icon}
color={theme.colors.primary}
/>
)}
right={({size, color}: {size: number, color: string}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
)}
style={{
height: LIST_ITEM_HEIGHT,
justifyContent: 'center',
}}
/>
);
} }
return null;
};
/** /**
* Gets the header buttons * Creates the dataset to be used in the FlatList
* @return {*} *
*/ * @param fetchedData
getHeaderButtons() { * @return {*}
return <MaterialHeaderButtons> * */
<Item title="magnify" iconName="magnify" onPress={this.onPressSearchBtn}/> createDataset = (
<Item title="information" iconName="information" onPress={this.onPressAboutBtn}/> fetchedData: ProximoDataType | null,
</MaterialHeaderButtons>; ): 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(): React.Node {
* Extracts a key for the given category const {navigation} = this.props;
* return (
* @param item The category to extract the key from <WebSectionList
* @return {*} The extracted key createDataset={this.createDataset}
*/ navigation={navigation}
getKeyExtractor(item: Object) { autoRefreshTime={0}
return item !== undefined ? item.type['id'] : undefined; refreshOnFocus={false}
} fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
/** />
* 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<Object>, 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 (
<List.Item
title={item.type.name}
description={subtitle}
onPress={onPress}
left={props => <List.Icon
{...props}
icon={item.type.icon}
color={this.props.theme.colors.primary}/>}
right={props => <List.Icon {...props} icon={'chevron-right'}/>}
style={{
height: LIST_ITEM_HEIGHT,
justifyContent: 'center',
}}
/>
);
} else
return <View/>;
}
render() {
const nav = this.props.navigation;
return (
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
autoRefreshTime={0}
refreshOnFocus={false}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}/>
);
}
} }
export default withTheme(ProximoMainScreen); export default withTheme(ProximoMainScreen);