forked from vergnet/application-amicale
Improve Proximo components to match linter
This commit is contained in:
parent
ab86c1c85c
commit
547af66977
5 changed files with 949 additions and 803 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue