Improve websectionlist and update proximo api

This commit is contained in:
Arnaud Vergnet 2021-05-10 14:55:21 +02:00
parent aed58f8749
commit a94006d18a
24 changed files with 902 additions and 842 deletions

View file

@ -92,6 +92,9 @@
"prettier" "prettier"
], ],
"rules": { "rules": {
"no-undef": 0,
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"prettier/prettier": [ "prettier/prettier": [
"error", "error",
{ {

View file

@ -23,6 +23,7 @@ import ConnectionManager from '../../managers/ConnectionManager';
import { ERROR_TYPE } from '../../utils/WebData'; import { ERROR_TYPE } from '../../utils/WebData';
import ErrorView from '../Screens/ErrorView'; import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen'; import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
import i18n from 'i18n-js';
type PropsType<T> = { type PropsType<T> = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
@ -151,11 +152,28 @@ class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
<ErrorView <ErrorView
icon={override.icon} icon={override.icon}
message={override.message} message={override.message}
showRetryButton={override.showRetryButton} button={
override.showRetryButton
? {
icon: 'refresh',
text: i18n.t('general.retry'),
onPress: this.fetchData,
}
: undefined
}
/> />
); );
} }
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />; return (
<ErrorView
status={errorCode}
button={{
icon: 'refresh',
text: i18n.t('general.retry'),
onPress: this.fetchData,
}}
/>
);
} }
/** /**

View file

@ -22,6 +22,7 @@ import { Avatar, List, Text } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type { ProximoArticleType } from '../../../screens/Services/Proximo/ProximoMainScreen'; import type { ProximoArticleType } from '../../../screens/Services/Proximo/ProximoMainScreen';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Urls from '../../../constants/Urls';
type PropsType = { type PropsType = {
onPress: () => void; onPress: () => void;
@ -43,6 +44,8 @@ const styles = StyleSheet.create({
}); });
function ProximoListItem(props: PropsType) { function ProximoListItem(props: PropsType) {
// console.log(Urls.proximo.images + props.item.image);
return ( return (
<List.Item <List.Item
title={props.item.name} title={props.item.name}
@ -55,7 +58,7 @@ function ProximoListItem(props: PropsType) {
<Avatar.Image <Avatar.Image
style={styles.avatar} style={styles.avatar}
size={64} size={64}
source={{ uri: props.item.image }} source={{ uri: Urls.proximo.images + props.item.image }}
/> />
)} )}
right={() => <Text style={styles.text}>{props.item.price}</Text>} right={() => <Text style={styles.text}>{props.item.price}</Text>}

View file

@ -18,28 +18,29 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Button, Subheading, withTheme } from 'react-native-paper'; import { Button, Subheading, useTheme } from 'react-native-paper';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import { StackNavigationProp } from '@react-navigation/stack'; import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import { ERROR_TYPE } from '../../utils/WebData';
type PropsType = { type Props = {
navigation?: StackNavigationProp<any>; status?: Exclude<REQUEST_STATUS, REQUEST_STATUS.SUCCESS>;
theme: ReactNativePaper.Theme; code?: Exclude<REQUEST_CODES, REQUEST_CODES.SUCCESS>;
route?: { name: string };
onRefresh?: () => void;
errorCode?: number;
icon?: string; icon?: string;
message?: string; message?: string;
showRetryButton?: boolean; loading?: boolean;
button?: {
text: string;
icon: string;
onPress: () => void;
};
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
height: '100%', flex: 1,
}, },
inner: { inner: {
marginTop: 'auto', marginTop: 'auto',
@ -61,134 +62,96 @@ const styles = StyleSheet.create({
}, },
}); });
class ErrorView extends React.PureComponent<PropsType> { function getMessage(props: Props) {
static defaultProps = { let fullMessage = {
onRefresh: () => {},
errorCode: 0,
icon: '',
message: '', message: '',
showRetryButton: true, icon: '',
}; };
if (props.code === undefined) {
message: string; switch (props.status) {
case REQUEST_STATUS.BAD_INPUT:
icon: string; fullMessage.message = i18n.t('errors.badInput');
fullMessage.icon = 'alert-circle-outline';
showLoginButton: boolean; break;
case REQUEST_STATUS.FORBIDDEN:
constructor(props: PropsType) { fullMessage.message = i18n.t('errors.forbidden');
super(props); fullMessage.icon = 'lock';
this.icon = ''; break;
this.showLoginButton = false; case REQUEST_STATUS.CONNECTION_ERROR:
this.message = ''; fullMessage.message = i18n.t('errors.connectionError');
} fullMessage.icon = 'access-point-network-off';
break;
getRetryButton() { case REQUEST_STATUS.SERVER_ERROR:
const { props } = this; fullMessage.message = i18n.t('errors.serverError');
return ( fullMessage.icon = 'server-network-off';
<Button break;
mode="contained" default:
icon="refresh" fullMessage.message = i18n.t('errors.unknown');
onPress={props.onRefresh} fullMessage.icon = 'alert-circle-outline';
style={styles.button} break;
>
{i18n.t('general.retry')}
</Button>
);
}
getLoginButton() {
return (
<Button
mode="contained"
icon="login"
onPress={this.goToLogin}
style={styles.button}
>
{i18n.t('screens.login.title')}
</Button>
);
}
goToLogin = () => {
const { props } = this;
if (props.navigation) {
props.navigation.navigate('login', {
screen: 'login',
params: { nextScreen: props.route ? props.route.name : undefined },
});
} }
}; } else {
switch (props.code) {
generateMessage() { case REQUEST_CODES.BAD_CREDENTIALS:
const { props } = this; fullMessage.message = i18n.t('errors.badCredentials');
this.showLoginButton = false; fullMessage.icon = 'account-alert-outline';
if (props.errorCode !== 0) { break;
switch (props.errorCode) { case REQUEST_CODES.BAD_TOKEN:
case ERROR_TYPE.BAD_CREDENTIALS: fullMessage.message = i18n.t('errors.badToken');
this.message = i18n.t('errors.badCredentials'); fullMessage.icon = 'account-alert-outline';
this.icon = 'account-alert-outline'; break;
break; case REQUEST_CODES.NO_CONSENT:
case ERROR_TYPE.BAD_TOKEN: fullMessage.message = i18n.t('errors.noConsent');
this.message = i18n.t('errors.badToken'); fullMessage.icon = 'account-remove-outline';
this.icon = 'account-alert-outline'; break;
this.showLoginButton = true; case REQUEST_CODES.TOKEN_SAVE:
break; fullMessage.message = i18n.t('errors.tokenSave');
case ERROR_TYPE.NO_CONSENT: fullMessage.icon = 'alert-circle-outline';
this.message = i18n.t('errors.noConsent'); break;
this.icon = 'account-remove-outline'; case REQUEST_CODES.BAD_INPUT:
break; fullMessage.message = i18n.t('errors.badInput');
case ERROR_TYPE.TOKEN_SAVE: fullMessage.icon = 'alert-circle-outline';
this.message = i18n.t('errors.tokenSave'); break;
this.icon = 'alert-circle-outline'; case REQUEST_CODES.FORBIDDEN:
break; fullMessage.message = i18n.t('errors.forbidden');
case ERROR_TYPE.BAD_INPUT: fullMessage.icon = 'lock';
this.message = i18n.t('errors.badInput'); break;
this.icon = 'alert-circle-outline'; case REQUEST_CODES.CONNECTION_ERROR:
break; fullMessage.message = i18n.t('errors.connectionError');
case ERROR_TYPE.FORBIDDEN: fullMessage.icon = 'access-point-network-off';
this.message = i18n.t('errors.forbidden'); break;
this.icon = 'lock'; case REQUEST_CODES.SERVER_ERROR:
break; fullMessage.message = i18n.t('errors.serverError');
case ERROR_TYPE.CONNECTION_ERROR: fullMessage.icon = 'server-network-off';
this.message = i18n.t('errors.connectionError'); break;
this.icon = 'access-point-network-off'; default:
break; fullMessage.message = i18n.t('errors.unknown');
case ERROR_TYPE.SERVER_ERROR: fullMessage.icon = 'alert-circle-outline';
this.message = i18n.t('errors.serverError'); break;
this.icon = 'server-network-off';
break;
default:
this.message = i18n.t('errors.unknown');
this.icon = 'alert-circle-outline';
break;
}
this.message += `\n\nCode ${
props.errorCode != null ? props.errorCode : -1
}`;
} else {
this.message = props.message != null ? props.message : '';
this.icon = props.icon != null ? props.icon : '';
} }
} }
render() { fullMessage.message += `\n\nCode {${props.status}:${props.code}}`;
const { props } = this; if (props.message != null) {
this.generateMessage(); fullMessage.message = props.message;
let button; }
if (this.showLoginButton) { if (props.icon != null) {
button = this.getLoginButton(); fullMessage.icon = props.icon;
} else if (props.showRetryButton) { }
button = this.getRetryButton(); return fullMessage;
} else { }
button = null;
}
return ( function ErrorView(props: Props) {
const theme = useTheme();
const fullMessage = getMessage(props);
const { button } = props;
return (
<View style={styles.outer}>
<Animatable.View <Animatable.View
style={{ style={{
...styles.outer, ...styles.outer,
backgroundColor: props.theme.colors.background, backgroundColor: theme.colors.background,
}} }}
animation="zoomIn" animation="zoomIn"
duration={200} duration={200}
@ -197,25 +160,33 @@ class ErrorView extends React.PureComponent<PropsType> {
<View style={styles.inner}> <View style={styles.inner}>
<View style={styles.iconContainer}> <View style={styles.iconContainer}>
<MaterialCommunityIcons <MaterialCommunityIcons
// $FlowFixMe name={fullMessage.icon}
name={this.icon}
size={150} size={150}
color={props.theme.colors.textDisabled} color={theme.colors.disabled}
/> />
</View> </View>
<Subheading <Subheading
style={{ style={{
...styles.subheading, ...styles.subheading,
color: props.theme.colors.textDisabled, color: theme.colors.disabled,
}} }}
> >
{this.message} {fullMessage.message}
</Subheading> </Subheading>
{button} {button ? (
<Button
mode={'contained'}
icon={button.icon}
onPress={button.onPress}
style={styles.button}
>
{button.text}
</Button>
) : null}
</View> </View>
</Animatable.View> </Animatable.View>
); </View>
} );
} }
export default withTheme(ErrorView); export default ErrorView;

View file

@ -0,0 +1,115 @@
import React, { useEffect, useRef } from 'react';
import ErrorView from './ErrorView';
import { useRequestLogic } from '../../utils/customHooks';
import { useFocusEffect } from '@react-navigation/native';
import BasicLoadingScreen from './BasicLoadingScreen';
import i18n from 'i18n-js';
import { REQUEST_STATUS } from '../../utils/Requests';
export type RequestScreenProps<T> = {
request: () => Promise<T>;
render: (
data: T | undefined,
loading: boolean,
refreshData: (newRequest?: () => Promise<T>) => void,
status: REQUEST_STATUS,
code: number | undefined
) => React.ReactElement;
cache?: T;
onCacheUpdate?: (newCache: T) => void;
onMajorError?: (status: number, code?: number) => void;
showLoading?: boolean;
showError?: boolean;
refreshOnFocus?: boolean;
autoRefreshTime?: number;
refresh?: boolean;
onFinish?: () => void;
};
export type RequestProps = {
refreshData: () => void;
loading: boolean;
};
type Props<T> = RequestScreenProps<T>;
const MIN_REFRESH_TIME = 5 * 1000;
export default function RequestScreen<T>(props: Props<T>) {
const refreshInterval = useRef<number>();
const [loading, status, code, data, refreshData] = useRequestLogic<T>(
() => props.request(),
props.cache,
props.onCacheUpdate,
props.refreshOnFocus,
MIN_REFRESH_TIME
);
// Store last refresh prop value
const lastRefresh = useRef<boolean>(false);
useEffect(() => {
// Refresh data if refresh prop changed and we are not loading
if (props.refresh && !lastRefresh.current && !loading) {
refreshData();
// Call finish callback if refresh prop was set and we finished loading
} else if (lastRefresh.current && !loading && props.onFinish) {
props.onFinish();
}
// Update stored refresh prop value
if (props.refresh !== lastRefresh.current) {
lastRefresh.current = props.refresh === true;
}
}, [props, loading, refreshData]);
useFocusEffect(
React.useCallback(() => {
if (!props.cache && props.refreshOnFocus !== false) {
refreshData();
}
if (props.autoRefreshTime && props.autoRefreshTime > 0) {
refreshInterval.current = setInterval(
refreshData,
props.autoRefreshTime
);
}
return () => {
if (refreshInterval.current) {
clearInterval(refreshInterval.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.cache, props.refreshOnFocus])
);
// useEffect(() => {
// if (status === REQUEST_STATUS.BAD_TOKEN && props.onMajorError) {
// props.onMajorError(status, code);
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [status, code]);
// if (status === REQUEST_STATUS.BAD_TOKEN && props.onMajorError) {
// return <View />;
// } else
if (data === undefined && loading && props.showLoading !== false) {
return <BasicLoadingScreen />;
} else if (
data === undefined &&
status !== REQUEST_STATUS.SUCCESS &&
props.showError !== false
) {
return (
<ErrorView
status={status}
loading={loading}
button={{
icon: 'refresh',
text: i18n.t('general.retry'),
onPress: () => refreshData(),
}}
/>
);
} else {
return props.render(data, loading, refreshData, status, code);
}
}

View file

@ -17,25 +17,26 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useState } from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Snackbar } from 'react-native-paper'; import { Snackbar } from 'react-native-paper';
import { import {
NativeScrollEvent,
NativeSyntheticEvent, NativeSyntheticEvent,
RefreshControl, RefreshControl,
SectionListData, SectionListData,
SectionListRenderItemInfo,
StyleSheet, StyleSheet,
View, View,
} from 'react-native'; } from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import { Collapsible } from 'react-navigation-collapsible';
import { StackNavigationProp } from '@react-navigation/stack';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
import { ERROR_TYPE, readData } from '../../utils/WebData'; import { ERROR_TYPE } from '../../utils/WebData';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import RequestScreen from './RequestScreen';
export type SectionListDataType<ItemT> = Array<{ export type SectionListDataType<ItemT> = Array<{
title: string; title: string;
@ -44,39 +45,30 @@ export type SectionListDataType<ItemT> = Array<{
keyExtractor?: (data: ItemT) => string; keyExtractor?: (data: ItemT) => string;
}>; }>;
type PropsType<ItemT, RawData> = { type Props<ItemT, RawData> = {
navigation: StackNavigationProp<any>; request: () => Promise<RawData>;
fetchUrl: string;
autoRefreshTime: number;
refreshOnFocus: boolean; refreshOnFocus: boolean;
renderItem: (data: { item: ItemT }) => React.ReactNode; renderItem: (data: SectionListRenderItemInfo<ItemT>) => React.ReactNode;
createDataset: ( createDataset: (
data: RawData | null, data: RawData | undefined,
isLoading?: boolean isLoading: boolean
) => SectionListDataType<ItemT>; ) => SectionListDataType<ItemT>;
onScroll?: (event: NativeSyntheticEvent<EventTarget>) => void; onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
showError?: boolean; showError?: boolean;
itemHeight?: number | null; itemHeight?: number | null;
updateData?: number; autoRefreshTime?: number;
updateData?: number | string;
renderListHeaderComponent?: ( renderListHeaderComponent?: (
data: RawData | null data?: RawData
) => React.ComponentType<any> | React.ReactElement | null; ) => React.ComponentType<any> | React.ReactElement | null;
renderSectionHeader?: ( renderSectionHeader?: (
data: { section: SectionListData<ItemT> }, data: { section: SectionListData<ItemT> },
isLoading?: boolean isLoading: boolean
) => React.ReactElement | null; ) => React.ReactElement | null;
stickyHeader?: boolean; stickyHeader?: boolean;
}; };
type StateType<RawData> = {
refreshing: boolean;
fetchedData: RawData | null;
snackbarVisible: boolean;
};
const MIN_REFRESH_TIME = 5 * 1000;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
minHeight: '100%', minHeight: '100%',
@ -85,131 +77,18 @@ const styles = StyleSheet.create({
/** /**
* Component used to render a SectionList with data fetched from the web * Component used to render a SectionList with data fetched from the web
*
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData. * To force the component to update, change the value of updateData.
*/ */
class WebSectionList<ItemT, RawData> extends React.PureComponent< function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
PropsType<ItemT, RawData>, const [snackbarVisible, setSnackbarVisible] = useState(false);
StateType<RawData>
> {
static defaultProps = {
showError: true,
itemHeight: null,
updateData: 0,
renderListHeaderComponent: () => null,
renderSectionHeader: () => null,
stickyHeader: false,
};
refreshInterval: NodeJS.Timeout | undefined; const showSnackBar = () => setSnackbarVisible(true);
lastRefresh: Date | undefined; const hideSnackBar = () => setSnackbarVisible(false);
constructor(props: PropsType<ItemT, RawData>) { const getItemLayout = (
super(props);
this.state = {
refreshing: false,
fetchedData: null,
snackbarVisible: false,
};
}
/**
* 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 = undefined;
this.onRefresh();
}
/**
* Refreshes data when focusing the screen and setup a refresh interval if asked to
*/
onScreenFocus = () => {
const { props } = this;
if (props.refreshOnFocus && this.lastRefresh) {
setTimeout(this.onRefresh, 200);
}
if (props.autoRefreshTime > 0) {
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
}
};
/**
* Removes any interval on un-focus
*/
onScreenBlur = () => {
if (this.refreshInterval) {
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: RawData) => {
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 = (
height: number, height: number,
data: Array<SectionListData<ItemT>> | null, _data: Array<SectionListData<ItemT>> | null,
index: number index: number
): { length: number; offset: number; index: number } => { ): { length: number; offset: number; index: number } => {
return { return {
@ -219,105 +98,125 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
}; };
}; };
getRenderSectionHeader = (data: { section: SectionListData<ItemT> }) => { const getRenderSectionHeader = (
const { renderSectionHeader } = this.props; data: { section: SectionListData<ItemT> },
const { refreshing } = this.state; loading: boolean
if (renderSectionHeader != null) { ) => {
const { renderSectionHeader } = props;
if (renderSectionHeader) {
return ( return (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver> <Animatable.View
{renderSectionHeader(data, refreshing)} animation={'fadeInUp'}
duration={500}
useNativeDriver={true}
>
{renderSectionHeader(data, loading)}
</Animatable.View> </Animatable.View>
); );
} }
return null; return null;
}; };
getRenderItem = (data: { item: ItemT }) => { const getRenderItem = (data: SectionListRenderItemInfo<ItemT>) => {
const { renderItem } = this.props; const { renderItem } = props;
return ( return (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver> <Animatable.View
animation={'fadeInUp'}
duration={500}
useNativeDriver={true}
>
{renderItem(data)} {renderItem(data)}
</Animatable.View> </Animatable.View>
); );
}; };
onScroll = (event: NativeSyntheticEvent<EventTarget>) => { const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { onScroll } = this.props; if (props.onScroll) {
if (onScroll != null) { props.onScroll(event);
onScroll(event);
} }
}; };
render() { const render = (
const { props, state } = this; data: RawData | undefined,
loading: boolean,
refreshData: (newRequest?: () => Promise<RawData>) => void
) => {
const { itemHeight } = props; const { itemHeight } = props;
let dataset: SectionListDataType<ItemT> = []; const dataset = props.createDataset(data, loading);
if ( if (!data && !loading) {
state.fetchedData != null || showSnackBar();
(state.fetchedData == null && !props.showError)
) {
dataset = props.createDataset(state.fetchedData, state.refreshing);
} }
return ( return (
<View style={GENERAL_STYLES.flex}> <CollapsibleSectionList
<CollapsibleSectionList sections={dataset}
sections={dataset} extraData={props.updateData}
extraData={props.updateData} paddedProps={(paddingTop) => ({
paddedProps={(paddingTop) => ({ refreshControl: (
refreshControl: ( <RefreshControl
<RefreshControl progressViewOffset={paddingTop}
progressViewOffset={paddingTop} refreshing={loading}
refreshing={state.refreshing} onRefresh={refreshData}
onRefresh={this.onRefresh} />
/> ),
), })}
})} renderSectionHeader={(info) => getRenderSectionHeader(info, loading)}
renderSectionHeader={this.getRenderSectionHeader} renderItem={getRenderItem}
renderItem={this.getRenderItem} stickySectionHeadersEnabled={props.stickyHeader}
stickySectionHeadersEnabled={props.stickyHeader} style={styles.container}
style={styles.container} ListHeaderComponent={
ListHeaderComponent={ props.renderListHeaderComponent != null
props.renderListHeaderComponent != null ? props.renderListHeaderComponent(data)
? props.renderListHeaderComponent(state.fetchedData) : null
: null }
} ListEmptyComponent={
ListEmptyComponent={ loading ? (
state.refreshing ? ( <BasicLoadingScreen />
<BasicLoadingScreen /> ) : (
) : ( <ErrorView
<ErrorView status={ERROR_TYPE.CONNECTION_ERROR}
navigation={props.navigation} button={{
errorCode={ERROR_TYPE.CONNECTION_ERROR} icon: 'refresh',
onRefresh={this.onRefresh} text: i18n.t('general.retry'),
/> onPress: refreshData,
) }}
} />
getItemLayout={ )
itemHeight }
? (data, index) => this.getItemLayout(itemHeight, data, index) getItemLayout={
: undefined itemHeight ? (d, i) => getItemLayout(itemHeight, d, i) : undefined
} }
onScroll={this.onScroll} onScroll={onScroll}
hasTab={true} hasTab={true}
/> />
<Snackbar
visible={state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: () => {},
}}
duration={4000}
style={{
bottom: TAB_BAR_HEIGHT,
}}
>
{i18n.t('general.listUpdateFail')}
</Snackbar>
</View>
); );
} };
return (
<View style={GENERAL_STYLES.flex}>
<RequestScreen<RawData>
request={props.request}
render={render}
showError={false}
showLoading={false}
autoRefreshTime={props.autoRefreshTime}
refreshOnFocus={props.refreshOnFocus}
/>
<Snackbar
visible={snackbarVisible}
onDismiss={hideSnackBar}
action={{
label: 'OK',
onPress: hideSnackBar,
}}
duration={4000}
style={{
bottom: TAB_BAR_HEIGHT,
}}
>
{i18n.t('general.listUpdateFail')}
</Snackbar>
</View>
);
} }
export default WebSectionList; export default WebSectionList;

View file

@ -251,8 +251,12 @@ function WebViewScreen(props: Props) {
renderLoading={getRenderLoading} renderLoading={getRenderLoading}
renderError={() => ( renderError={() => (
<ErrorView <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR} status={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={onRefreshClicked} button={{
icon: 'refresh',
text: i18n.t('general.retry'),
onPress: onRefreshClicked,
}}
/> />
)} )}
onNavigationStateChange={setNavState} onNavigationStateChange={setNavState}

View file

@ -21,11 +21,14 @@ const STUDENT_SERVER = 'https://etud.insa-toulouse.fr/';
const AMICALE_SERVER = 'https://www.amicale-insat.fr/'; const AMICALE_SERVER = 'https://www.amicale-insat.fr/';
const GIT_SERVER = const GIT_SERVER =
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/'; 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/';
const PLANEX_SERVER = 'http://planex.insa-toulouse.fr/';
const AMICALE_ENDPOINT = AMICALE_SERVER + 'api/'; const AMICALE_ENDPOINT = AMICALE_SERVER + 'api/';
const APP_ENDPOINT = STUDENT_SERVER + '~amicale_app/v2/'; const APP_ENDPOINT = STUDENT_SERVER + '~amicale_app/v2/';
const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/data/stock-v2.json'; const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/v2/api/';
const PROXIMO_IMAGES_ENDPOINT =
STUDENT_SERVER + '~proximo/v2/api-proximo/public/storage/app/';
const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/'; const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/';
export default { export default {
@ -39,7 +42,16 @@ export default {
dashboard: APP_ENDPOINT + 'dashboard/dashboard_data.json', dashboard: APP_ENDPOINT + 'dashboard/dashboard_data.json',
menu: APP_ENDPOINT + 'menu/menu_data.json', menu: APP_ENDPOINT + 'menu/menu_data.json',
}, },
proximo: PROXIMO_ENDPOINT, proximo: {
articles: PROXIMO_ENDPOINT + 'articles',
categories: PROXIMO_ENDPOINT + 'categories',
images: PROXIMO_IMAGES_ENDPOINT + 'img/',
icons: PROXIMO_IMAGES_ENDPOINT + 'icon/',
},
planex: {
planning: PLANEX_SERVER,
groups: PLANEX_SERVER + 'wsAdeGrp.php?projectId=1',
},
images: { images: {
proxiwash: APP_IMAGES_ENDPOINT + 'Proxiwash.png', proxiwash: APP_IMAGES_ENDPOINT + 'Proxiwash.png',
washer: APP_IMAGES_ENDPOINT + 'ProxiwashLaveLinge.png', washer: APP_IMAGES_ENDPOINT + 'ProxiwashLaveLinge.png',

View file

@ -84,6 +84,10 @@ export type FullParamsList = DefaultParams & {
}; };
'equipment-rent': { item?: DeviceType }; 'equipment-rent': { item?: DeviceType };
'gallery': { images: Array<{ url: string }> }; 'gallery': { images: Array<{ url: string }> };
[MainRoutes.ProximoList]: {
shouldFocusSearchBar: boolean;
category: number;
};
}; };
// Don't know why but TS is complaining without this // Don't know why but TS is complaining without this

View file

@ -22,6 +22,7 @@ import {
FlatList, FlatList,
NativeScrollEvent, NativeScrollEvent,
NativeSyntheticEvent, NativeSyntheticEvent,
SectionListData,
StyleSheet, StyleSheet,
} from 'react-native'; } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
@ -52,6 +53,7 @@ import { getDisplayEvent, getFutureEvents } from '../../utils/Home';
import type { PlanningEventType } from '../../utils/Planning'; import type { PlanningEventType } from '../../utils/Planning';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { readData } from '../../utils/WebData';
const FEED_ITEM_HEIGHT = 500; const FEED_ITEM_HEIGHT = 500;
@ -314,12 +316,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item); getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item);
getRenderSectionHeader = ( getRenderSectionHeader = (
data: { data: { section: SectionListData<FeedItemType> },
section: {
data: Array<object>;
title: string;
};
},
isLoading: boolean isLoading: boolean
) => { ) => {
const { props } = this; const { props } = this;
@ -352,7 +349,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
); );
}; };
getListHeader = (fetchedData: RawDashboardType) => { getListHeader = (fetchedData: RawDashboardType | undefined) => {
let dashboard = null; let dashboard = null;
if (fetchedData != null) { if (fetchedData != null) {
dashboard = fetchedData.dashboard; dashboard = fetchedData.dashboard;
@ -404,21 +401,20 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @return {*} * @return {*}
*/ */
createDataset = ( createDataset = (
fetchedData: RawDashboardType | null, fetchedData: RawDashboardType | undefined,
isLoading: boolean isLoading: boolean
): Array<{ ): Array<{
title: string; title: string;
data: [] | Array<FeedItemType>; data: [] | Array<FeedItemType>;
id: string; id: string;
}> => { }> => {
// fetchedData = DATA; if (fetchedData) {
if (fetchedData != null) { if (fetchedData.news_feed) {
if (fetchedData.news_feed != null) {
this.currentNewFeed = HomeScreen.generateNewsFeed( this.currentNewFeed = HomeScreen.generateNewsFeed(
fetchedData.news_feed fetchedData.news_feed
); );
} }
if (fetchedData.dashboard != null) { if (fetchedData.dashboard) {
this.currentDashboard = fetchedData.dashboard; this.currentDashboard = fetchedData.dashboard;
} }
} }
@ -470,11 +466,10 @@ class HomeScreen extends React.Component<PropsType, StateType> {
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<View style={styles.content}> <View style={styles.content}>
<WebSectionList <WebSectionList
navigation={props.navigation} request={() => readData<RawDashboardType>(Urls.app.dashboard)}
createDataset={this.createDataset} createDataset={this.createDataset}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus refreshOnFocus={true}
fetchUrl={Urls.app.dashboard}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
itemHeight={FEED_ITEM_HEIGHT} itemHeight={FEED_ITEM_HEIGHT}
onScroll={this.onScroll} onScroll={this.onScroll}

View file

@ -26,6 +26,8 @@ import { stringMatchQuery } from '../../utils/Search';
import WebSectionList from '../../components/Screens/WebSectionList'; import WebSectionList from '../../components/Screens/WebSectionList';
import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion'; import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion';
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
import Urls from '../../constants/Urls';
import { readData } from '../../utils/WebData';
export type PlanexGroupType = { export type PlanexGroupType = {
name: string; name: string;
@ -60,8 +62,6 @@ function sortName(
return 0; return 0;
} }
const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1';
/** /**
* Class defining planex group selection screen. * Class defining planex group selection screen.
*/ */
@ -137,9 +137,13 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
* @param fetchedData * @param fetchedData
* @return {*} * @return {*}
* */ * */
createDataset = (fetchedData: { createDataset = (
[key: string]: PlanexGroupCategoryType; fetchedData:
}): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => { | {
[key: string]: PlanexGroupCategoryType;
}
| undefined
): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => {
return [ return [
{ {
title: '', title: '',
@ -236,20 +240,28 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
* @param fetchedData The raw data fetched from the server * @param fetchedData The raw data fetched from the server
* @returns {[]} * @returns {[]}
*/ */
generateData(fetchedData: { generateData(
[key: string]: PlanexGroupCategoryType; fetchedData:
}): Array<PlanexGroupCategoryType> { | {
[key: string]: PlanexGroupCategoryType;
}
| undefined
): Array<PlanexGroupCategoryType> {
const { favoriteGroups } = this.state; const { favoriteGroups } = this.state;
const data: Array<PlanexGroupCategoryType> = []; const data: Array<PlanexGroupCategoryType> = [];
Object.values(fetchedData).forEach((category: PlanexGroupCategoryType) => { if (fetchedData) {
data.push(category); Object.values(fetchedData).forEach(
}); (category: PlanexGroupCategoryType) => {
data.sort(sortName); data.push(category);
data.unshift({ }
name: i18n.t('screens.planex.favorites'), );
id: 0, data.sort(sortName);
content: favoriteGroups, data.unshift({
}); name: i18n.t('screens.planex.favorites'),
id: 0,
content: favoriteGroups,
});
}
return data; return data;
} }
@ -298,14 +310,16 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
} }
render() { render() {
const { props, state } = this; const { state } = this;
return ( return (
<WebSectionList <WebSectionList
navigation={props.navigation} request={() =>
readData<{ [key: string]: PlanexGroupCategoryType }>(
Urls.planex.groups
)
}
createDataset={this.createDataset} createDataset={this.createDataset}
autoRefreshTime={0} refreshOnFocus={true}
refreshOnFocus={false}
fetchUrl={GROUPS_URL}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
updateData={state.currentSearchString + state.favoriteGroups.length} updateData={state.currentSearchString + state.favoriteGroups.length}
/> />

View file

@ -42,6 +42,7 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import { getPrettierPlanexGroupName } from '../../utils/Utils'; import { getPrettierPlanexGroupName } from '../../utils/Utils';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
@ -57,8 +58,6 @@ type StateType = {
injectJS: string; injectJS: string;
}; };
const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
// // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing // // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing
// // Remove alpha from given Jquery node // // Remove alpha from given Jquery node
// function removeAlpha(node) { // function removeAlpha(node) {
@ -197,22 +196,19 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
* @returns {*} * @returns {*}
*/ */
getWebView() { getWebView() {
const { props, state } = this; const { state } = this;
const showWebview = state.currentGroup.id !== -1; const showWebview = state.currentGroup.id !== -1;
console.log(state.injectJS);
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
{!showWebview ? ( {!showWebview ? (
<ErrorView <ErrorView
navigation={props.navigation} icon={'account-clock'}
icon="account-clock"
message={i18n.t('screens.planex.noGroupSelected')} message={i18n.t('screens.planex.noGroupSelected')}
showRetryButton={false}
/> />
) : null} ) : null}
<WebViewScreen <WebViewScreen
url={PLANEX_URL} url={Urls.planex.planning}
initialJS={this.generateInjectedJS(this.state.currentGroup.id)} initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
injectJS={this.state.injectJS} injectJS={this.state.injectJS}
onMessage={this.onMessage} onMessage={this.onMessage}

View file

@ -28,9 +28,7 @@ import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
import { apiRequest, ERROR_TYPE } from '../../utils/WebData'; import { apiRequest, ERROR_TYPE } from '../../utils/WebData';
import ErrorView from '../../components/Screens/ErrorView'; import ErrorView from '../../components/Screens/ErrorView';
import CustomHTML from '../../components/Overrides/CustomHTML'; import CustomHTML from '../../components/Overrides/CustomHTML';
import CustomTabBar, { import { TAB_BAR_HEIGHT } from '../../components/Tabbar/CustomTabBar';
TAB_BAR_HEIGHT,
} from '../../components/Tabbar/CustomTabBar';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
import type { PlanningEventType } from '../../utils/Planning'; import type { PlanningEventType } from '../../utils/Planning';
import ImageGalleryButton from '../../components/Media/ImageGalleryButton'; import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
@ -163,12 +161,9 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
* @returns {*} * @returns {*}
*/ */
getErrorView() { getErrorView() {
const { navigation } = this.props;
if (this.errorCode === ERROR_TYPE.BAD_INPUT) { if (this.errorCode === ERROR_TYPE.BAD_INPUT) {
return ( return (
<ErrorView <ErrorView
navigation={navigation}
showRetryButton={false}
message={i18n.t('screens.planning.invalidEvent')} message={i18n.t('screens.planning.invalidEvent')}
icon="calendar-remove" icon="calendar-remove"
/> />
@ -176,9 +171,12 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
} }
return ( return (
<ErrorView <ErrorView
navigation={navigation} status={this.errorCode}
errorCode={this.errorCode} button={{
onRefresh={this.fetchData} icon: 'refresh',
text: i18n.t('general.retry'),
onPress: this.fetchData,
}}
/> />
); );
} }

View file

@ -18,7 +18,13 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Alert, StyleSheet, View } from 'react-native'; import {
Alert,
SectionListData,
SectionListRenderItemInfo,
StyleSheet,
View,
} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper'; import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
@ -46,6 +52,7 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
import type { SectionListDataType } from '../../components/Screens/WebSectionList'; import type { SectionListDataType } from '../../components/Screens/WebSectionList';
import type { LaundromatType } from './ProxiwashAboutScreen'; import type { LaundromatType } from './ProxiwashAboutScreen';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import { readData } from '../../utils/WebData';
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -72,6 +79,11 @@ type StateType = {
selectedWash: string; selectedWash: string;
}; };
type FetchedDataType = {
dryers: Array<ProxiwashMachineType>;
washers: Array<ProxiwashMachineType>;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
modalContainer: { modalContainer: {
flex: 1, flex: 1,
@ -277,7 +289,11 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param section The section to render * @param section The section to render
* @return {*} * @return {*}
*/ */
getRenderSectionHeader = ({ section }: { section: { title: string } }) => { getRenderSectionHeader = ({
section,
}: {
section: SectionListData<ProxiwashMachineType>;
}) => {
const isDryer = section.title === i18n.t('screens.proxiwash.dryers'); const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
const nbAvailable = this.getMachineAvailableNumber(isDryer); const nbAvailable = this.getMachineAvailableNumber(isDryer);
return ( return (
@ -296,20 +312,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param section The object describing the current SectionList section * @param section The object describing the current SectionList section
* @returns {React.Node} * @returns {React.Node}
*/ */
getRenderItem = ({ getRenderItem = (data: SectionListRenderItemInfo<ProxiwashMachineType>) => {
item,
section,
}: {
item: ProxiwashMachineType;
section: { title: string };
}) => {
const { machinesWatched } = this.state; const { machinesWatched } = this.state;
const isDryer = section.title === i18n.t('screens.proxiwash.dryers'); const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers');
return ( return (
<ProxiwashListItem <ProxiwashListItem
item={item} item={data.item}
onPress={this.showModal} onPress={this.showModal}
isWatched={isMachineWatched(item, machinesWatched)} isWatched={isMachineWatched(data.item, machinesWatched)}
isDryer={isDryer} isDryer={isDryer}
height={LIST_ITEM_HEIGHT} height={LIST_ITEM_HEIGHT}
/> />
@ -382,37 +392,40 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
* @param fetchedData * @param fetchedData
* @return {*} * @return {*}
*/ */
createDataset = (fetchedData: { createDataset = (
dryers: Array<ProxiwashMachineType>; fetchedData: FetchedDataType | undefined
washers: Array<ProxiwashMachineType>; ): SectionListDataType<ProxiwashMachineType> => {
}): SectionListDataType<ProxiwashMachineType> => {
const { state } = this; const { state } = this;
let data = fetchedData; if (fetchedData) {
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { let data = fetchedData;
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers); data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers); AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
}
this.fetchedData = data;
// TODO dirty, should be refactored
this.state.machinesWatched = getCleanedMachineWatched(
state.machinesWatched,
[...data.dryers, ...data.washers]
);
return [
{
title: i18n.t('screens.proxiwash.dryers'),
icon: 'tumble-dryer',
data: data.dryers === undefined ? [] : data.dryers,
keyExtractor: this.getKeyExtractor,
},
{
title: i18n.t('screens.proxiwash.washers'),
icon: 'washing-machine',
data: data.washers === undefined ? [] : data.washers,
keyExtractor: this.getKeyExtractor,
},
];
} else {
return [];
} }
this.fetchedData = data;
// TODO dirty, should be refactored
this.state.machinesWatched = getCleanedMachineWatched(
state.machinesWatched,
[...data.dryers, ...data.washers]
);
return [
{
title: i18n.t('screens.proxiwash.dryers'),
icon: 'tumble-dryer',
data: data.dryers === undefined ? [] : data.dryers,
keyExtractor: this.getKeyExtractor,
},
{
title: i18n.t('screens.proxiwash.washers'),
icon: 'washing-machine',
data: data.washers === undefined ? [] : data.washers,
keyExtractor: this.getKeyExtractor,
},
];
}; };
/** /**
@ -481,7 +494,6 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
render() { render() {
const { state } = this; const { state } = this;
const { navigation } = this.props;
let data: LaundromatType; let data: LaundromatType;
switch (state.selectedWash) { switch (state.selectedWash) {
case 'tripodeB': case 'tripodeB':
@ -494,13 +506,12 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<View style={styles.container}> <View style={styles.container}>
<WebSectionList <WebSectionList
request={() => readData<FetchedDataType>(data.url)}
createDataset={this.createDataset} createDataset={this.createDataset}
navigation={navigation}
fetchUrl={data.url}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
renderSectionHeader={this.getRenderSectionHeader} renderSectionHeader={this.getRenderSectionHeader}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus refreshOnFocus={true}
updateData={state.machinesWatched.length} updateData={state.machinesWatched.length}
/> />
</View> </View>

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useLayoutEffect, useRef, useState } from 'react';
import { Image, Platform, ScrollView, StyleSheet, View } from 'react-native'; import { Image, Platform, ScrollView, StyleSheet, View } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { import {
@ -26,9 +26,8 @@ import {
Subheading, Subheading,
Text, Text,
Title, Title,
withTheme, useTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack';
import { Modalize } from 'react-native-modalize'; import { Modalize } from 'react-native-modalize';
import CustomModal from '../../../components/Overrides/CustomModal'; import CustomModal from '../../../components/Overrides/CustomModal';
import { stringMatchQuery } from '../../../utils/Search'; import { stringMatchQuery } from '../../../utils/Search';
@ -36,19 +35,29 @@ import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../../components/Overrides/CustomHeaderButton'; } from '../../../components/Overrides/CustomHeaderButton';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import type { ProximoArticleType } from './ProximoMainScreen'; import type { ProximoArticleType } from './ProximoMainScreen';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { useNavigation } from '@react-navigation/core';
import Urls from '../../../constants/Urls';
import WebSectionList, {
SectionListDataType,
} from '../../../components/Screens/WebSectionList';
import { readData } from '../../../utils/WebData';
import { StackScreenProps } from '@react-navigation/stack';
import {
MainRoutes,
MainStackParamsList,
} from '../../../navigation/MainNavigator';
function sortPrice(a: ProximoArticleType, b: ProximoArticleType): number { function sortPrice(a: ProximoArticleType, b: ProximoArticleType): number {
return parseInt(a.price, 10) - parseInt(b.price, 10); return a.price - b.price;
} }
function sortPriceReverse( function sortPriceReverse(
a: ProximoArticleType, a: ProximoArticleType,
b: ProximoArticleType b: ProximoArticleType
): number { ): number {
return parseInt(b.price, 10) - parseInt(a.price, 10); return b.price - a.price;
} }
function sortName(a: ProximoArticleType, b: ProximoArticleType): number { function sortName(a: ProximoArticleType, b: ProximoArticleType): number {
@ -73,23 +82,6 @@ function sortNameReverse(a: ProximoArticleType, b: ProximoArticleType): number {
const LIST_ITEM_HEIGHT = 84; const LIST_ITEM_HEIGHT = 84;
type PropsType = {
navigation: StackNavigationProp<any>;
route: {
params: {
data: { data: Array<ProximoArticleType> };
shouldFocusSearchBar: boolean;
};
};
theme: ReactNativePaper.Theme;
};
type StateType = {
currentSortMode: number;
modalCurrentDisplayItem: React.ReactNode;
currentSearchString: string;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
modalContainer: { modalContainer: {
flex: 1, flex: 1,
@ -118,113 +110,72 @@ const styles = StyleSheet.create({
}, },
}); });
/** type ArticlesType = Array<ProximoArticleType>;
* Class defining Proximo article list of a certain category.
*/
class ProximoListScreen extends React.Component<PropsType, StateType> {
modalRef: Modalize | null;
listData: Array<ProximoArticleType>; type Props = StackScreenProps<MainStackParamsList, MainRoutes.ProximoList>;
shouldFocusSearchBar: boolean; function ProximoListScreen(props: Props) {
const navigation = useNavigation();
const theme = useTheme();
const modalRef = useRef<Modalize>();
constructor(props: PropsType) { const [currentSearchString, setCurrentSearchString] = useState('');
super(props); const [currentSortMode, setCurrentSortMode] = useState(2);
this.modalRef = null; const [modalCurrentDisplayItem, setModalCurrentDisplayItem] = useState<
this.listData = props.route.params.data.data.sort(sortName); React.ReactNode | undefined
this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar; >();
this.state = {
currentSearchString: '',
currentSortMode: 3,
modalCurrentDisplayItem: null,
};
}
/** const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
* Creates the header content
*/ useLayoutEffect(() => {
componentDidMount() {
const { navigation } = this.props;
navigation.setOptions({ navigation.setOptions({
headerRight: this.getSortMenuButton, headerRight: getSortMenuButton,
headerTitle: this.getSearchBar, headerTitle: getSearchBar,
headerBackTitleVisible: false, headerBackTitleVisible: false,
headerTitleContainerStyle: headerTitleContainerStyle:
Platform.OS === 'ios' Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' } ? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 }, : { marginHorizontal: 0, right: 50, left: 50 },
}); });
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, currentSortMode]);
/** /**
* Callback used when clicking on the sort menu button. * Callback used when clicking on the sort menu button.
* It will open the modal to show a sort selection * It will open the modal to show a sort selection
*/ */
onSortMenuPress = () => { const onSortMenuPress = () => {
this.setState({ setModalCurrentDisplayItem(getModalSortMenu());
modalCurrentDisplayItem: this.getModalSortMenu(), if (modalRef.current) {
}); modalRef.current.open();
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 });
};
/** /**
* Callback used when clicking an article in the list. * Callback used when clicking an article in the list.
* It opens the modal to show detailed information about the article * It opens the modal to show detailed information about the article
* *
* @param item The article pressed * @param item The article pressed
*/ */
onListItemPress(item: ProximoArticleType) { const onListItemPress = (item: ProximoArticleType) => {
this.setState({ setModalCurrentDisplayItem(getModalItemContent(item));
modalCurrentDisplayItem: this.getModalItemContent(item), if (modalRef.current) {
}); modalRef.current.open();
if (this.modalRef) {
this.modalRef.open();
} }
} };
/** /**
* Sets the current sort mode. * Sets the current sort mode.
* *
* @param mode The number representing the mode * @param mode The number representing the mode
*/ */
setSortMode(mode: string) { const setSortMode = (mode: string) => {
const { currentSortMode } = this.state;
const currentMode = parseInt(mode, 10); const currentMode = parseInt(mode, 10);
this.setState({ setCurrentSortMode(currentMode);
currentSortMode: currentMode, if (modalRef.current && currentMode !== currentSortMode) {
}); modalRef.current.close();
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 * Gets a color depending on the quantity available
@ -232,8 +183,7 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
* @param availableStock The quantity available * @param availableStock The quantity available
* @return * @return
*/ */
getStockColor(availableStock: number): string { const getStockColor = (availableStock: number): string => {
const { theme } = this.props;
let color: string; let color: string;
if (availableStock > 3) { if (availableStock > 3) {
color = theme.colors.success; color = theme.colors.success;
@ -243,17 +193,17 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
color = theme.colors.danger; color = theme.colors.danger;
} }
return color; return color;
} };
/** /**
* Gets the sort menu header button * Gets the sort menu header button
* *
* @return {*} * @return {*}
*/ */
getSortMenuButton = () => { const getSortMenuButton = () => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item title="main" iconName="sort" onPress={this.onSortMenuPress} /> <Item title="main" iconName="sort" onPress={onSortMenuPress} />
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
}; };
@ -263,12 +213,13 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getSearchBar = () => { const getSearchBar = () => {
return ( return (
// @ts-ignore // @ts-ignore
<Searchbar <Searchbar
placeholder={i18n.t('screens.proximo.search')} placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange} onChangeText={setCurrentSearchString}
autoFocus={props.route.params.shouldFocusSearchBar}
/> />
); );
}; };
@ -279,14 +230,14 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
* @param item The article to display * @param item The article to display
* @return {*} * @return {*}
*/ */
getModalItemContent(item: ProximoArticleType) { const getModalItemContent = (item: ProximoArticleType) => {
return ( return (
<View style={styles.modalContainer}> <View style={styles.modalContainer}>
<Title>{item.name}</Title> <Title>{item.name}</Title>
<View style={styles.modalTitleContainer}> <View style={styles.modalTitleContainer}>
<Subheading <Subheading
style={{ style={{
color: this.getStockColor(parseInt(item.quantity, 10)), color: getStockColor(item.quantity),
}} }}
> >
{`${item.quantity} ${i18n.t('screens.proximo.inStock')}`} {`${item.quantity} ${i18n.t('screens.proximo.inStock')}`}
@ -302,46 +253,43 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
</ScrollView> </ScrollView>
</View> </View>
); );
} };
/** /**
* Gets the modal content to display a sort menu * Gets the modal content to display a sort menu
* *
* @return {*} * @return {*}
*/ */
getModalSortMenu() { const getModalSortMenu = () => {
const { currentSortMode } = this.state;
return ( return (
<View style={styles.modalContainer}> <View style={styles.modalContainer}>
<Title style={styles.sortTitle}> <Title style={styles.sortTitle}>
{i18n.t('screens.proximo.sortOrder')} {i18n.t('screens.proximo.sortOrder')}
</Title> </Title>
<RadioButton.Group <RadioButton.Group
onValueChange={(value: string) => { onValueChange={setSortMode}
this.setSortMode(value);
}}
value={currentSortMode.toString()} value={currentSortMode.toString()}
> >
<RadioButton.Item <RadioButton.Item
label={i18n.t('screens.proximo.sortPrice')} label={i18n.t('screens.proximo.sortPrice')}
value={'1'} value={'0'}
/> />
<RadioButton.Item <RadioButton.Item
label={i18n.t('screens.proximo.sortPriceReverse')} label={i18n.t('screens.proximo.sortPriceReverse')}
value={'2'} value={'1'}
/> />
<RadioButton.Item <RadioButton.Item
label={i18n.t('screens.proximo.sortName')} label={i18n.t('screens.proximo.sortName')}
value={'3'} value={'2'}
/> />
<RadioButton.Item <RadioButton.Item
label={i18n.t('screens.proximo.sortNameReverse')} label={i18n.t('screens.proximo.sortNameReverse')}
value={'4'} value={'3'}
/> />
</RadioButton.Group> </RadioButton.Group>
</View> </View>
); );
} };
/** /**
* Gets a render item for the given article * Gets a render item for the given article
@ -349,13 +297,12 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
* @param item The article to render * @param item The article to render
* @return {*} * @return {*}
*/ */
getRenderItem = ({ item }: { item: ProximoArticleType }) => { const getRenderItem = ({ item }: { item: ProximoArticleType }) => {
const { currentSearchString } = this.state;
if (stringMatchQuery(item.name, currentSearchString)) { if (stringMatchQuery(item.name, currentSearchString)) {
const onPress = () => { const onPress = () => {
this.onListItemPress(item); onListItemPress(item);
}; };
const color = this.getStockColor(parseInt(item.quantity, 10)); const color = getStockColor(item.quantity);
return ( return (
<ProximoListItem <ProximoListItem
item={item} item={item}
@ -374,46 +321,55 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
* @param item The article to extract the key from * @param item The article to extract the key from
* @return {string} The extracted key * @return {string} The extracted key
*/ */
keyExtractor = (item: ProximoArticleType): string => item.name + item.code; const keyExtractor = (item: ProximoArticleType): string =>
item.name + item.code;
/** const createDataset = (
* Callback used when receiving the modal ref data: ArticlesType | undefined
* ): SectionListDataType<ProximoArticleType> => {
* @param ref if (data) {
*/ console.log(data);
onModalRef = (ref: Modalize) => { console.log(props.route.params.category);
this.modalRef = ref;
return [
{
title: '',
data: data
.filter(
(d) =>
props.route.params.category === -1 ||
props.route.params.category === d.category_id
)
.sort(sortModes[currentSortMode]),
keyExtractor: keyExtractor,
},
];
} else {
return [
{
title: '',
data: [],
keyExtractor: keyExtractor,
},
];
}
}; };
itemLayout = ( return (
data: Array<ProximoArticleType> | null | undefined, <View style={GENERAL_STYLES.flex}>
index: number <CustomModal onRef={(ref) => (modalRef.current = ref)}>
): { length: number; offset: number; index: number } => ({ {modalCurrentDisplayItem}
length: LIST_ITEM_HEIGHT, </CustomModal>
offset: LIST_ITEM_HEIGHT * index, <WebSectionList
index, request={() => readData<ArticlesType>(Urls.proximo.articles)}
}); createDataset={createDataset}
refreshOnFocus={true}
render() { renderItem={getRenderItem}
const { state } = this; updateData={currentSearchString + currentSortMode}
return ( itemHeight={LIST_ITEM_HEIGHT}
<View style={GENERAL_STYLES.flex}> />
<CustomModal onRef={this.onModalRef}> </View>
{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); export default ProximoListScreen;

View file

@ -19,8 +19,7 @@
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { List, withTheme } from 'react-native-paper'; import { Avatar, List, useTheme, withTheme } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack';
import WebSectionList from '../../../components/Screens/WebSectionList'; import WebSectionList from '../../../components/Screens/WebSectionList';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
@ -28,40 +27,35 @@ import MaterialHeaderButtons, {
import type { SectionListDataType } from '../../../components/Screens/WebSectionList'; import type { SectionListDataType } from '../../../components/Screens/WebSectionList';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Urls from '../../../constants/Urls'; import Urls from '../../../constants/Urls';
import { readData } from '../../../utils/WebData';
import { useNavigation } from '@react-navigation/core';
import { useLayoutEffect } from 'react';
const LIST_ITEM_HEIGHT = 84; const LIST_ITEM_HEIGHT = 84;
export type ProximoCategoryType = { export type ProximoCategoryType = {
id: number;
name: string; name: string;
icon: string; icon: string;
id: string; created_at: string;
updated_at: string;
}; };
export type ProximoArticleType = { export type ProximoArticleType = {
id: number;
name: string; name: string;
description: string; description: string;
quantity: string; quantity: number;
price: string; price: number;
code: string; code: string;
id: string;
type: Array<string>;
image: string; image: string;
category_id: number;
created_at: string;
updated_at: string;
category: ProximoCategoryType;
}; };
export type ProximoMainListItemType = { type CategoriesType = Array<ProximoCategoryType>;
type: ProximoCategoryType;
data: Array<ProximoArticleType>;
};
export type ProximoDataType = {
types: Array<ProximoCategoryType>;
articles: Array<ProximoArticleType>;
};
type PropsType = {
navigation: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
item: { item: {
@ -69,138 +63,69 @@ const styles = StyleSheet.create({
}, },
}); });
function sortFinalData(a: ProximoCategoryType, b: ProximoCategoryType): number {
const str1 = a.name.toLowerCase();
const str2 = b.name.toLowerCase();
// Make 'All' category with id -1 stick to the top
if (a.id === -1) {
return -1;
}
if (b.id === -1) {
return 1;
}
// Sort others by name ascending
if (str1 < str2) {
return -1;
}
if (str1 > str2) {
return 1;
}
return 0;
}
/** /**
* Class defining the main proximo screen. * Class defining the main proximo screen.
* This screen shows the different categories of articles offered by proximo. * This screen shows the different categories of articles offered by proximo.
*/ */
class ProximoMainScreen extends React.Component<PropsType> { function ProximoMainScreen() {
/** const navigation = useNavigation();
* Function used to sort items in the list. const theme = useTheme();
* 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();
// Make 'All' category with id -1 stick to the top useLayoutEffect(() => {
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: Array<ProximoArticleType> = [];
if (articles != null) {
articles.forEach((article: ProximoArticleType) => {
if (
((type != null && article.type.includes(type.id)) || type == null) &&
parseInt(article.quantity, 10) > 0
) {
availableArticles.push(article);
}
});
}
return availableArticles;
}
articles: Array<ProximoArticleType> | null;
constructor(props: PropsType) {
super(props);
this.articles = null;
}
/**
* Creates header button
*/
componentDidMount() {
const { navigation } = this.props;
navigation.setOptions({ navigation.setOptions({
headerRight: () => this.getHeaderButtons(), headerRight: () => getHeaderButtons(),
}); });
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation]);
/** /**
* Callback used when the search button is pressed. * Callback used when the search button is pressed.
* This will open a new ProximoListScreen with all items displayed * This will open a new ProximoListScreen with all items displayed
*/ */
onPressSearchBtn = () => { const onPressSearchBtn = () => {
const { navigation } = this.props;
const searchScreenData = { const searchScreenData = {
shouldFocusSearchBar: true, shouldFocusSearchBar: true,
data: { category: -1,
type: {
id: '0',
name: i18n.t('screens.proximo.all'),
icon: 'star',
},
data:
this.articles != null
? ProximoMainScreen.getAvailableArticles(this.articles)
: [],
},
}; };
navigation.navigate('proximo-list', searchScreenData); navigation.navigate('proximo-list', searchScreenData);
}; };
/** const onPressAboutBtn = () => 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');
};
/** const getHeaderButtons = () => {
* Gets the header buttons
* @return {*}
*/
getHeaderButtons() {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item title="magnify" iconName="magnify" onPress={onPressSearchBtn} />
title="magnify"
iconName="magnify"
onPress={this.onPressSearchBtn}
/>
<Item <Item
title="information" title="information"
iconName="information" iconName="information"
onPress={this.onPressAboutBtn} onPress={onPressAboutBtn}
/> />
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
} };
/** /**
* Extracts a key for the given category * Extracts a key for the given category
@ -208,7 +133,8 @@ class ProximoMainScreen extends React.Component<PropsType> {
* @param item The category to extract the key from * @param item The category to extract the key from
* @return {*} The extracted key * @return {*} The extracted key
*/ */
getKeyExtractor = (item: ProximoMainListItemType): string => item.type.id; const getKeyExtractor = (item: ProximoCategoryType): string =>
item.id.toString();
/** /**
* Gets the given category render item * Gets the given category render item
@ -216,33 +142,36 @@ class ProximoMainScreen extends React.Component<PropsType> {
* @param item The category to render * @param item The category to render
* @return {*} * @return {*}
*/ */
getRenderItem = ({ item }: { item: ProximoMainListItemType }) => { const getRenderItem = ({ item }: { item: ProximoCategoryType }) => {
const { navigation, theme } = this.props;
const dataToSend = { const dataToSend = {
shouldFocusSearchBar: false, shouldFocusSearchBar: false,
data: item, category: item.id,
}; };
const subtitle = `${item.data.length} ${ // TODO get article number
item.data.length > 1 const article_number = 1;
const subtitle = `${article_number} ${
article_number > 1
? i18n.t('screens.proximo.articles') ? i18n.t('screens.proximo.articles')
: i18n.t('screens.proximo.article') : i18n.t('screens.proximo.article')
}`; }`;
const onPress = () => { const onPress = () => navigation.navigate('proximo-list', dataToSend);
navigation.navigate('proximo-list', dataToSend); if (article_number > 0) {
};
if (item.data.length > 0) {
return ( return (
<List.Item <List.Item
title={item.type.name} title={item.name}
description={subtitle} description={subtitle}
onPress={onPress} onPress={onPress}
left={(props) => ( left={(props) =>
<List.Icon item.icon.endsWith('.png') ? (
style={props.style} <Avatar.Image style={props.style} source={{ uri: item.icon }} />
icon={item.type.icon} ) : (
color={theme.colors.primary} <List.Icon
/> style={props.style}
)} icon={item.icon}
color={theme.colors.primary}
/>
)
}
right={(props) => ( right={(props) => (
<List.Icon <List.Icon
color={props.color} color={props.color}
@ -266,65 +195,46 @@ class ProximoMainScreen extends React.Component<PropsType> {
* @param fetchedData * @param fetchedData
* @return {*} * @return {*}
* */ * */
createDataset = ( const createDataset = (
fetchedData: ProximoDataType | null data: CategoriesType | undefined
): SectionListDataType<ProximoMainListItemType> => { ): SectionListDataType<ProximoCategoryType> => {
return [ if (data) {
{ const finalData: CategoriesType = [
title: '', {
data: this.generateData(fetchedData), id: -1,
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'), name: i18n.t('screens.proximo.all'),
icon: 'star', icon: 'star',
created_at: '',
updated_at: '',
}, },
data: ProximoMainScreen.getAvailableArticles(this.articles), ...data,
}); ];
types.forEach((type: ProximoCategoryType) => { return [
finalData.push({ {
type, title: '',
data: ProximoMainScreen.getAvailableArticles(this.articles, type), data: finalData.sort(sortFinalData),
}); keyExtractor: getKeyExtractor,
}); },
];
} else {
return [
{
title: '',
data: [],
keyExtractor: getKeyExtractor,
},
];
} }
finalData.sort(ProximoMainScreen.sortFinalData); };
return finalData;
}
render() { return (
const { navigation } = this.props; <WebSectionList
return ( request={() => readData<CategoriesType>(Urls.proximo.categories)}
<WebSectionList createDataset={createDataset}
createDataset={this.createDataset} refreshOnFocus={true}
navigation={navigation} renderItem={getRenderItem}
autoRefreshTime={0} />
refreshOnFocus={false} );
fetchUrl={Urls.proximo}
renderItem={this.getRenderItem}
/>
);
}
} }
export default withTheme(ProximoMainScreen); export default withTheme(ProximoMainScreen);

View file

@ -18,7 +18,7 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { StyleSheet, View } from 'react-native'; import { SectionListData, StyleSheet, View } from 'react-native';
import { Card, Text, withTheme } from 'react-native-paper'; import { Card, Text, withTheme } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
@ -26,6 +26,7 @@ import DateManager from '../../managers/DateManager';
import WebSectionList from '../../components/Screens/WebSectionList'; import WebSectionList from '../../components/Screens/WebSectionList';
import type { SectionListDataType } from '../../components/Screens/WebSectionList'; import type { SectionListDataType } from '../../components/Screens/WebSectionList';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { readData } from '../../utils/WebData';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
@ -108,7 +109,7 @@ class SelfMenuScreen extends React.Component<PropsType> {
* @return {[]} * @return {[]}
*/ */
createDataset = ( createDataset = (
fetchedData: Array<RawRuMenuType> fetchedData: Array<RawRuMenuType> | undefined
): SectionListDataType<RuFoodCategoryType> => { ): SectionListDataType<RuFoodCategoryType> => {
let result: SectionListDataType<RuFoodCategoryType> = []; let result: SectionListDataType<RuFoodCategoryType> = [];
if (fetchedData == null || fetchedData.length === 0) { if (fetchedData == null || fetchedData.length === 0) {
@ -137,7 +138,11 @@ class SelfMenuScreen extends React.Component<PropsType> {
* @param section The section to render the header from * @param section The section to render the header from
* @return {*} * @return {*}
*/ */
getRenderSectionHeader = ({ section }: { section: { title: string } }) => { getRenderSectionHeader = ({
section,
}: {
section: SectionListData<RuFoodCategoryType>;
}) => {
return ( return (
<Card style={styles.headerCard}> <Card style={styles.headerCard}>
<Card.Title <Card.Title
@ -189,17 +194,14 @@ class SelfMenuScreen extends React.Component<PropsType> {
getKeyExtractor = (item: RuFoodCategoryType): string => item.name; getKeyExtractor = (item: RuFoodCategoryType): string => item.name;
render() { render() {
const { navigation } = this.props;
return ( return (
<WebSectionList <WebSectionList
request={() => readData<Array<RawRuMenuType>>(Urls.app.menu)}
createDataset={this.createDataset} createDataset={this.createDataset}
navigation={navigation} refreshOnFocus={true}
autoRefreshTime={0}
refreshOnFocus={false}
fetchUrl={Urls.app.menu}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
renderSectionHeader={this.getRenderSectionHeader} renderSectionHeader={this.getRenderSectionHeader}
stickyHeader stickyHeader={true}
/> />
); );
} }

View file

@ -105,7 +105,6 @@ class WebsiteScreen extends React.Component<Props, State> {
const { route, navigation } = this.props; const { route, navigation } = this.props;
if (route.params != null) { if (route.params != null) {
console.log(route.params);
this.host = route.params.host; this.host = route.params.host;
let { path } = route.params; let { path } = route.params;
const { title } = route.params; const { title } = route.params;

View file

@ -141,7 +141,6 @@ class Test extends React.Component<Props> {
// ); // );
return ( return (
<WebSectionList <WebSectionList
navigation={props.navigation}
createDataset={this.createDataset} createDataset={this.createDataset}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus refreshOnFocus

22
src/utils/Requests.tsx Normal file
View file

@ -0,0 +1,22 @@
export enum REQUEST_STATUS {
SUCCESS = 200,
BAD_INPUT = 400,
FORBIDDEN = 403,
CONNECTION_ERROR = 404,
SERVER_ERROR = 500,
UNKNOWN = 999,
}
export enum REQUEST_CODES {
SUCCESS = 0,
BAD_CREDENTIALS = 1,
BAD_TOKEN = 2,
NO_CONSENT = 3,
TOKEN_SAVE = 4,
TOKEN_RETRIEVE = 5,
BAD_INPUT = 400,
FORBIDDEN = 403,
CONNECTION_ERROR = 404,
SERVER_ERROR = 500,
UNKNOWN = 999,
}

View file

@ -120,11 +120,11 @@ export async function apiRequest<T>(
* @param url The urls to fetch data from * @param url The urls to fetch data from
* @return Promise<any> * @return Promise<any>
*/ */
export async function readData(url: string): Promise<any> { export async function readData<T>(url: string): Promise<T> {
return new Promise((resolve: (response: any) => void, reject: () => void) => { return new Promise((resolve: (response: T) => void, reject: () => void) => {
fetch(url) fetch(url)
.then(async (response: Response): Promise<any> => response.json()) .then(async (response: Response): Promise<any> => response.json())
.then((data: any): void => resolve(data)) .then((data: T) => resolve(data))
.catch((): void => reject()); .catch(() => reject());
}); });
} }

21
src/utils/cacheContext.ts Normal file
View file

@ -0,0 +1,21 @@
import React, { useContext } from 'react';
export type CacheContextType<T> = {
cache: T | undefined;
setCache: (newCache: T) => void;
resetCache: () => void;
};
export const CacheContext = React.createContext<CacheContextType<any>>({
cache: undefined,
setCache: () => undefined,
resetCache: () => undefined,
});
function getCacheContext<T>() {
return CacheContext as React.Context<CacheContextType<T>>;
}
export function useCache<T>() {
return useContext(getCacheContext<T>());
}

106
src/utils/customHooks.tsx Normal file
View file

@ -0,0 +1,106 @@
import { DependencyList, useEffect, useRef, useState } from 'react';
import { REQUEST_STATUS } from './Requests';
export function useMountEffect(func: () => void) {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(func, []);
}
/**
* Effect that does not run on first render
*
* @param effect
* @param deps
*/
export function useSubsequentEffect(effect: () => void, deps?: DependencyList) {
const didMountRef = useRef(false);
useEffect(
() => {
if (didMountRef.current) {
effect();
} else {
didMountRef.current = true;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
deps ? deps : []
);
}
export function useRequestLogic<T>(
request: () => Promise<T>,
cache?: T,
onCacheUpdate?: (newCache: T) => void,
startLoading?: boolean,
minRefreshTime?: number
) {
const [response, setResponse] = useState<{
loading: boolean;
status: REQUEST_STATUS;
code?: number;
data: T | undefined;
}>({
loading: startLoading !== false && cache === undefined,
status: REQUEST_STATUS.SUCCESS,
code: undefined,
data: undefined,
});
const [lastRefreshDate, setLastRefreshDate] = useState<Date | undefined>(
undefined
);
const refreshData = (newRequest?: () => Promise<T>) => {
let canRefresh;
if (lastRefreshDate && minRefreshTime) {
const last = lastRefreshDate;
canRefresh = new Date().getTime() - last.getTime() > minRefreshTime;
} else {
canRefresh = true;
}
if (canRefresh) {
if (!response.loading) {
setResponse((prevState) => ({
...prevState,
loading: true,
}));
}
setLastRefreshDate(new Date());
const r = newRequest ? newRequest : request;
r()
.then((requestResponse: T) => {
setResponse({
loading: false,
status: REQUEST_STATUS.SUCCESS,
code: undefined,
data: requestResponse,
});
if (onCacheUpdate) {
onCacheUpdate(requestResponse);
}
})
.catch(() => {
setResponse((prevState) => ({
loading: false,
status: REQUEST_STATUS.CONNECTION_ERROR,
code: 0,
data: prevState.data,
}));
});
}
};
const value: [
boolean,
REQUEST_STATUS,
number | undefined,
T | undefined,
(newRequest?: () => Promise<T>) => void
] = [
response.loading,
response.status,
response.code,
cache ? cache : response.data,
refreshData,
];
return value;
}

View file

@ -30,10 +30,10 @@
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */ "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
@ -45,13 +45,15 @@
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
"resolveJsonModule": true /* Allow import of JSON files */ "resolveJsonModule": true, /* Allow import of JSON files */
/* Source Map Options */ /* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
"skipLibCheck": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false
/* Experimental Options */ /* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */