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

View file

@ -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<Object>,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible,
export type SectionListDataType<T> = Array<{
title: string,
data: Array<T>,
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<T> = {
navigation: StackNavigationProp,
fetchUrl: string,
autoRefreshTime: number,
refreshOnFocus: boolean,
renderItem: (data: {item: T}) => React.Node,
createDataset: (
data: ApiGenericDataType | null,
isLoading?: boolean,
) => SectionListDataType<T>,
onScroll: (event: SyntheticEvent<EventTarget>) => 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<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 = {
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 (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
{renderSectionHeader(data, refreshing)}
</Animatable.View>
);
}
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 (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
{renderItem(data)}
</Animatable.View>
);
};
/**
* Removes any interval on un-focus
*/
onScreenBlur = () => {
clearInterval(this.refreshInterval);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
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 (
<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>
);
}
const {containerPaddingTop} = props.collapsibleStack;
return (
<View>
<CollapsibleSectionList
sections={dataset}
extraData={props.updateData}
refreshControl={
<RefreshControl
progressViewOffset={containerPaddingTop}
refreshing={state.refreshing}
onRefresh={this.onRefresh}
/>
}
renderSectionHeader={this.getRenderSectionHeader}
renderItem={this.getRenderItem}
stickySectionHeadersEnabled={props.stickyHeader}
style={{minHeight: '100%'}}
ListHeaderComponent={
props.renderListHeaderComponent != null
? props.renderListHeaderComponent(state.fetchedData)
: null
}
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}
hasTab
/>
<Snackbar
visible={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);

View file

@ -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<Props> {
render() {
return (
<CollapsibleScrollView style={{padding: 5}}>
<View style={{
width: '100%',
height: 100,
marginTop: 20,
marginBottom: 20,
justifyContent: 'center',
alignItems: 'center'
}}>
<Image
source={{uri: LOGO}}
style={{height: '100%', width: '100%', resizeMode: "contain"}}/>
</View>
<Text>{i18n.t('screens.proximo.description')}</Text>
<Card style={{margin: 5}}>
<Card.Title
title={i18n.t('screens.proximo.openingHours')}
left={props => <List.Icon {...props} icon={'clock-outline'}/>}
/>
<Card.Content>
<Paragraph>18h30 - 19h30</Paragraph>
</Card.Content>
</Card>
<Card style={{margin: 5, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.proximo.paymentMethods')}
left={props => <List.Icon {...props} icon={'cash'}/>}
/>
<Card.Content>
<Paragraph>{i18n.t('screens.proximo.paymentMethodsDescription')}</Paragraph>
</Card.Content>
</Card>
</CollapsibleScrollView>
);
}
// eslint-disable-next-line react/prefer-stateless-function
export default class ProximoAboutScreen extends React.Component<null> {
render(): React.Node {
return (
<CollapsibleScrollView style={{padding: 5}}>
<View
style={{
width: '100%',
height: 100,
marginTop: 20,
marginBottom: 20,
justifyContent: 'center',
alignItems: 'center',
}}>
<Image
source={{uri: LOGO}}
style={{height: '100%', width: '100%', resizeMode: 'contain'}}
/>
</View>
<Text>{i18n.t('screens.proximo.description')}</Text>
<Card style={{margin: 5}}>
<Card.Title
title={i18n.t('screens.proximo.openingHours')}
left={({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="clock-outline" />
)}
/>
<Card.Content>
<Paragraph>18h30 - 19h30</Paragraph>
</Card.Content>
</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
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<ProximoArticleType>},
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<Props, State> {
class ProximoListScreen extends React.Component<PropsType, StateType> {
modalRef: Modalize | null;
modalRef: Object;
listData: Array<Object>;
shouldFocusSearchBar: boolean;
listData: Array<ProximoArticleType>;
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 (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/**
* Sets the current sort mode.
*
* @param mode The number representing the mode
*/
setSortMode(mode: string) {
const {currentSortMode} = this.state;
const currentMode = parseInt(mode, 10);
this.setState({
currentSortMode: currentMode,
});
switch (currentMode) {
case 1:
this.listData.sort(sortPrice);
break;
case 2:
this.listData.sort(sortPriceReverse);
break;
case 3:
this.listData.sort(sortName);
break;
case 4:
this.listData.sort(sortNameReverse);
break;
default:
this.listData.sort(sortName);
break;
}
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
*
* @return {*}
*/
getSortMenuButton = () => {
return <MaterialHeaderButtons>
<Item title="main" iconName="sort" onPress={this.onSortMenuPress}/>
</MaterialHeaderButtons>;
};
/**
* Gets the modal content to display a sort menu
*
* @return {*}
*/
getModalSortMenu(): React.Node {
const {currentSortMode} = this.state;
return (
<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.
* 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 (
<ProximoListItem
item={item}
onPress={onPress}
color={color}
height={LIST_ITEM_HEIGHT}
/>
);
}
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 (
<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>
itemLayout = (
data: ProximoArticleType,
index: number,
): {length: number, offset: number, index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
<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 modal content to display a sort menu
*
* @return {*}
*/
getModalSortMenu() {
return (
<View style={{
flex: 1,
padding: 20
}}>
<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>
);
}
render(): React.Node {
const {state} = this;
return (
<View
style={{
height: '100%',
}}>
<CustomModal onRef={this.onModalRef}>
{state.modalCurrentDisplayItem}
</CustomModal>
<CollapsibleFlatList
data={this.listData}
extraData={state.currentSearchString + state.currentSortMode}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews
getItemLayout={this.itemLayout}
initialNumToRender={10}
/>
</View>
);
}
}
export default withTheme(ProximoListScreen);

View file

@ -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<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.
* 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;
/**
* 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<ProximoArticleType> | null,
type: ?ProximoCategoryType,
): Array<ProximoArticleType> {
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<ProximoArticleType> | 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 (
<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
* @return {*}
*/
getHeaderButtons() {
return <MaterialHeaderButtons>
<Item title="magnify" iconName="magnify" onPress={this.onPressSearchBtn}/>
<Item title="information" iconName="information" onPress={this.onPressAboutBtn}/>
</MaterialHeaderButtons>;
/**
* 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;
}
/**
* 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<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}/>
);
}
render(): React.Node {
const {navigation} = this.props;
return (
<WebSectionList
createDataset={this.createDataset}
navigation={navigation}
autoRefreshTime={0}
refreshOnFocus={false}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}
/>
);
}
}
export default withTheme(ProximoMainScreen);