Improve websectionlist and update proximo api
This commit is contained in:
parent
aed58f8749
commit
a94006d18a
24 changed files with 902 additions and 842 deletions
|
@ -92,6 +92,9 @@
|
|||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-undef": 0,
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
|
|
|
@ -23,6 +23,7 @@ import ConnectionManager from '../../managers/ConnectionManager';
|
|||
import { ERROR_TYPE } from '../../utils/WebData';
|
||||
import ErrorView from '../Screens/ErrorView';
|
||||
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type PropsType<T> = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
|
@ -151,11 +152,28 @@ class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
|
|||
<ErrorView
|
||||
icon={override.icon}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Avatar, List, Text } from 'react-native-paper';
|
|||
import i18n from 'i18n-js';
|
||||
import type { ProximoArticleType } from '../../../screens/Services/Proximo/ProximoMainScreen';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Urls from '../../../constants/Urls';
|
||||
|
||||
type PropsType = {
|
||||
onPress: () => void;
|
||||
|
@ -43,6 +44,8 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
function ProximoListItem(props: PropsType) {
|
||||
// console.log(Urls.proximo.images + props.item.image);
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
title={props.item.name}
|
||||
|
@ -55,7 +58,7 @@ function ProximoListItem(props: PropsType) {
|
|||
<Avatar.Image
|
||||
style={styles.avatar}
|
||||
size={64}
|
||||
source={{ uri: props.item.image }}
|
||||
source={{ uri: Urls.proximo.images + props.item.image }}
|
||||
/>
|
||||
)}
|
||||
right={() => <Text style={styles.text}>{props.item.price}€</Text>}
|
||||
|
|
|
@ -18,28 +18,29 @@
|
|||
*/
|
||||
|
||||
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 MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import i18n from 'i18n-js';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { ERROR_TYPE } from '../../utils/WebData';
|
||||
import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||
|
||||
type PropsType = {
|
||||
navigation?: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
route?: { name: string };
|
||||
onRefresh?: () => void;
|
||||
errorCode?: number;
|
||||
type Props = {
|
||||
status?: Exclude<REQUEST_STATUS, REQUEST_STATUS.SUCCESS>;
|
||||
code?: Exclude<REQUEST_CODES, REQUEST_CODES.SUCCESS>;
|
||||
icon?: string;
|
||||
message?: string;
|
||||
showRetryButton?: boolean;
|
||||
loading?: boolean;
|
||||
button?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
},
|
||||
inner: {
|
||||
marginTop: 'auto',
|
||||
|
@ -61,134 +62,96 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class ErrorView extends React.PureComponent<PropsType> {
|
||||
static defaultProps = {
|
||||
onRefresh: () => {},
|
||||
errorCode: 0,
|
||||
icon: '',
|
||||
function getMessage(props: Props) {
|
||||
let fullMessage = {
|
||||
message: '',
|
||||
showRetryButton: true,
|
||||
icon: '',
|
||||
};
|
||||
|
||||
message: string;
|
||||
|
||||
icon: string;
|
||||
|
||||
showLoginButton: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.icon = '';
|
||||
this.showLoginButton = false;
|
||||
this.message = '';
|
||||
}
|
||||
|
||||
getRetryButton() {
|
||||
const { props } = this;
|
||||
return (
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="refresh"
|
||||
onPress={props.onRefresh}
|
||||
style={styles.button}
|
||||
>
|
||||
{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 },
|
||||
});
|
||||
if (props.code === undefined) {
|
||||
switch (props.status) {
|
||||
case REQUEST_STATUS.BAD_INPUT:
|
||||
fullMessage.message = i18n.t('errors.badInput');
|
||||
fullMessage.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case REQUEST_STATUS.FORBIDDEN:
|
||||
fullMessage.message = i18n.t('errors.forbidden');
|
||||
fullMessage.icon = 'lock';
|
||||
break;
|
||||
case REQUEST_STATUS.CONNECTION_ERROR:
|
||||
fullMessage.message = i18n.t('errors.connectionError');
|
||||
fullMessage.icon = 'access-point-network-off';
|
||||
break;
|
||||
case REQUEST_STATUS.SERVER_ERROR:
|
||||
fullMessage.message = i18n.t('errors.serverError');
|
||||
fullMessage.icon = 'server-network-off';
|
||||
break;
|
||||
default:
|
||||
fullMessage.message = i18n.t('errors.unknown');
|
||||
fullMessage.icon = 'alert-circle-outline';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
generateMessage() {
|
||||
const { props } = this;
|
||||
this.showLoginButton = false;
|
||||
if (props.errorCode !== 0) {
|
||||
switch (props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t('errors.badCredentials');
|
||||
this.icon = 'account-alert-outline';
|
||||
break;
|
||||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t('errors.badToken');
|
||||
this.icon = 'account-alert-outline';
|
||||
this.showLoginButton = true;
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t('errors.noConsent');
|
||||
this.icon = 'account-remove-outline';
|
||||
break;
|
||||
case ERROR_TYPE.TOKEN_SAVE:
|
||||
this.message = i18n.t('errors.tokenSave');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case ERROR_TYPE.BAD_INPUT:
|
||||
this.message = i18n.t('errors.badInput');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case ERROR_TYPE.FORBIDDEN:
|
||||
this.message = i18n.t('errors.forbidden');
|
||||
this.icon = 'lock';
|
||||
break;
|
||||
case ERROR_TYPE.CONNECTION_ERROR:
|
||||
this.message = i18n.t('errors.connectionError');
|
||||
this.icon = 'access-point-network-off';
|
||||
break;
|
||||
case ERROR_TYPE.SERVER_ERROR:
|
||||
this.message = i18n.t('errors.serverError');
|
||||
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 : '';
|
||||
} else {
|
||||
switch (props.code) {
|
||||
case REQUEST_CODES.BAD_CREDENTIALS:
|
||||
fullMessage.message = i18n.t('errors.badCredentials');
|
||||
fullMessage.icon = 'account-alert-outline';
|
||||
break;
|
||||
case REQUEST_CODES.BAD_TOKEN:
|
||||
fullMessage.message = i18n.t('errors.badToken');
|
||||
fullMessage.icon = 'account-alert-outline';
|
||||
break;
|
||||
case REQUEST_CODES.NO_CONSENT:
|
||||
fullMessage.message = i18n.t('errors.noConsent');
|
||||
fullMessage.icon = 'account-remove-outline';
|
||||
break;
|
||||
case REQUEST_CODES.TOKEN_SAVE:
|
||||
fullMessage.message = i18n.t('errors.tokenSave');
|
||||
fullMessage.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case REQUEST_CODES.BAD_INPUT:
|
||||
fullMessage.message = i18n.t('errors.badInput');
|
||||
fullMessage.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case REQUEST_CODES.FORBIDDEN:
|
||||
fullMessage.message = i18n.t('errors.forbidden');
|
||||
fullMessage.icon = 'lock';
|
||||
break;
|
||||
case REQUEST_CODES.CONNECTION_ERROR:
|
||||
fullMessage.message = i18n.t('errors.connectionError');
|
||||
fullMessage.icon = 'access-point-network-off';
|
||||
break;
|
||||
case REQUEST_CODES.SERVER_ERROR:
|
||||
fullMessage.message = i18n.t('errors.serverError');
|
||||
fullMessage.icon = 'server-network-off';
|
||||
break;
|
||||
default:
|
||||
fullMessage.message = i18n.t('errors.unknown');
|
||||
fullMessage.icon = 'alert-circle-outline';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
this.generateMessage();
|
||||
let button;
|
||||
if (this.showLoginButton) {
|
||||
button = this.getLoginButton();
|
||||
} else if (props.showRetryButton) {
|
||||
button = this.getRetryButton();
|
||||
} else {
|
||||
button = null;
|
||||
}
|
||||
fullMessage.message += `\n\nCode {${props.status}:${props.code}}`;
|
||||
if (props.message != null) {
|
||||
fullMessage.message = props.message;
|
||||
}
|
||||
if (props.icon != null) {
|
||||
fullMessage.icon = props.icon;
|
||||
}
|
||||
return fullMessage;
|
||||
}
|
||||
|
||||
return (
|
||||
function ErrorView(props: Props) {
|
||||
const theme = useTheme();
|
||||
const fullMessage = getMessage(props);
|
||||
const { button } = props;
|
||||
|
||||
return (
|
||||
<View style={styles.outer}>
|
||||
<Animatable.View
|
||||
style={{
|
||||
...styles.outer,
|
||||
backgroundColor: props.theme.colors.background,
|
||||
backgroundColor: theme.colors.background,
|
||||
}}
|
||||
animation="zoomIn"
|
||||
duration={200}
|
||||
|
@ -197,25 +160,33 @@ class ErrorView extends React.PureComponent<PropsType> {
|
|||
<View style={styles.inner}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
// $FlowFixMe
|
||||
name={this.icon}
|
||||
name={fullMessage.icon}
|
||||
size={150}
|
||||
color={props.theme.colors.textDisabled}
|
||||
color={theme.colors.disabled}
|
||||
/>
|
||||
</View>
|
||||
<Subheading
|
||||
style={{
|
||||
...styles.subheading,
|
||||
color: props.theme.colors.textDisabled,
|
||||
color: theme.colors.disabled,
|
||||
}}
|
||||
>
|
||||
{this.message}
|
||||
{fullMessage.message}
|
||||
</Subheading>
|
||||
{button}
|
||||
{button ? (
|
||||
<Button
|
||||
mode={'contained'}
|
||||
icon={button.icon}
|
||||
onPress={button.onPress}
|
||||
style={styles.button}
|
||||
>
|
||||
{button.text}
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(ErrorView);
|
||||
export default ErrorView;
|
||||
|
|
115
src/components/Screens/RequestScreen.tsx
Normal file
115
src/components/Screens/RequestScreen.tsx
Normal 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);
|
||||
}
|
||||
}
|
|
@ -17,25 +17,26 @@
|
|||
* 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 { Snackbar } from 'react-native-paper';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
RefreshControl,
|
||||
SectionListData,
|
||||
SectionListRenderItemInfo,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import { Collapsible } from 'react-navigation-collapsible';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
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 GENERAL_STYLES from '../../constants/Styles';
|
||||
import RequestScreen from './RequestScreen';
|
||||
|
||||
export type SectionListDataType<ItemT> = Array<{
|
||||
title: string;
|
||||
|
@ -44,39 +45,30 @@ export type SectionListDataType<ItemT> = Array<{
|
|||
keyExtractor?: (data: ItemT) => string;
|
||||
}>;
|
||||
|
||||
type PropsType<ItemT, RawData> = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
fetchUrl: string;
|
||||
autoRefreshTime: number;
|
||||
type Props<ItemT, RawData> = {
|
||||
request: () => Promise<RawData>;
|
||||
refreshOnFocus: boolean;
|
||||
renderItem: (data: { item: ItemT }) => React.ReactNode;
|
||||
renderItem: (data: SectionListRenderItemInfo<ItemT>) => React.ReactNode;
|
||||
createDataset: (
|
||||
data: RawData | null,
|
||||
isLoading?: boolean
|
||||
data: RawData | undefined,
|
||||
isLoading: boolean
|
||||
) => SectionListDataType<ItemT>;
|
||||
|
||||
onScroll?: (event: NativeSyntheticEvent<EventTarget>) => void;
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
showError?: boolean;
|
||||
itemHeight?: number | null;
|
||||
updateData?: number;
|
||||
autoRefreshTime?: number;
|
||||
updateData?: number | string;
|
||||
renderListHeaderComponent?: (
|
||||
data: RawData | null
|
||||
data?: RawData
|
||||
) => React.ComponentType<any> | React.ReactElement | null;
|
||||
renderSectionHeader?: (
|
||||
data: { section: SectionListData<ItemT> },
|
||||
isLoading?: boolean
|
||||
isLoading: boolean
|
||||
) => React.ReactElement | null;
|
||||
stickyHeader?: boolean;
|
||||
};
|
||||
|
||||
type StateType<RawData> = {
|
||||
refreshing: boolean;
|
||||
fetchedData: RawData | null;
|
||||
snackbarVisible: boolean;
|
||||
};
|
||||
|
||||
const MIN_REFRESH_TIME = 5 * 1000;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: '100%',
|
||||
|
@ -85,131 +77,18 @@ const styles = StyleSheet.create({
|
|||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
||||
PropsType<ItemT, RawData>,
|
||||
StateType<RawData>
|
||||
> {
|
||||
static defaultProps = {
|
||||
showError: true,
|
||||
itemHeight: null,
|
||||
updateData: 0,
|
||||
renderListHeaderComponent: () => null,
|
||||
renderSectionHeader: () => null,
|
||||
stickyHeader: false,
|
||||
};
|
||||
function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
|
||||
refreshInterval: NodeJS.Timeout | undefined;
|
||||
const showSnackBar = () => setSnackbarVisible(true);
|
||||
|
||||
lastRefresh: Date | undefined;
|
||||
const hideSnackBar = () => setSnackbarVisible(false);
|
||||
|
||||
constructor(props: PropsType<ItemT, RawData>) {
|
||||
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 = (
|
||||
const getItemLayout = (
|
||||
height: number,
|
||||
data: Array<SectionListData<ItemT>> | null,
|
||||
_data: Array<SectionListData<ItemT>> | null,
|
||||
index: number
|
||||
): { length: number; offset: number; index: number } => {
|
||||
return {
|
||||
|
@ -219,105 +98,125 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
};
|
||||
};
|
||||
|
||||
getRenderSectionHeader = (data: { section: SectionListData<ItemT> }) => {
|
||||
const { renderSectionHeader } = this.props;
|
||||
const { refreshing } = this.state;
|
||||
if (renderSectionHeader != null) {
|
||||
const getRenderSectionHeader = (
|
||||
data: { section: SectionListData<ItemT> },
|
||||
loading: boolean
|
||||
) => {
|
||||
const { renderSectionHeader } = props;
|
||||
if (renderSectionHeader) {
|
||||
return (
|
||||
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
|
||||
{renderSectionHeader(data, refreshing)}
|
||||
<Animatable.View
|
||||
animation={'fadeInUp'}
|
||||
duration={500}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
{renderSectionHeader(data, loading)}
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
getRenderItem = (data: { item: ItemT }) => {
|
||||
const { renderItem } = this.props;
|
||||
const getRenderItem = (data: SectionListRenderItemInfo<ItemT>) => {
|
||||
const { renderItem } = props;
|
||||
return (
|
||||
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
|
||||
<Animatable.View
|
||||
animation={'fadeInUp'}
|
||||
duration={500}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
{renderItem(data)}
|
||||
</Animatable.View>
|
||||
);
|
||||
};
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<EventTarget>) => {
|
||||
const { onScroll } = this.props;
|
||||
if (onScroll != null) {
|
||||
onScroll(event);
|
||||
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (props.onScroll) {
|
||||
props.onScroll(event);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const render = (
|
||||
data: RawData | undefined,
|
||||
loading: boolean,
|
||||
refreshData: (newRequest?: () => Promise<RawData>) => void
|
||||
) => {
|
||||
const { itemHeight } = props;
|
||||
let dataset: SectionListDataType<ItemT> = [];
|
||||
if (
|
||||
state.fetchedData != null ||
|
||||
(state.fetchedData == null && !props.showError)
|
||||
) {
|
||||
dataset = props.createDataset(state.fetchedData, state.refreshing);
|
||||
const dataset = props.createDataset(data, loading);
|
||||
if (!data && !loading) {
|
||||
showSnackBar();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CollapsibleSectionList
|
||||
sections={dataset}
|
||||
extraData={props.updateData}
|
||||
paddedProps={(paddingTop) => ({
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
progressViewOffset={paddingTop}
|
||||
refreshing={state.refreshing}
|
||||
onRefresh={this.onRefresh}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
renderItem={this.getRenderItem}
|
||||
stickySectionHeadersEnabled={props.stickyHeader}
|
||||
style={styles.container}
|
||||
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={
|
||||
itemHeight
|
||||
? (data, index) => this.getItemLayout(itemHeight, data, index)
|
||||
: undefined
|
||||
}
|
||||
onScroll={this.onScroll}
|
||||
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>
|
||||
<CollapsibleSectionList
|
||||
sections={dataset}
|
||||
extraData={props.updateData}
|
||||
paddedProps={(paddingTop) => ({
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
progressViewOffset={paddingTop}
|
||||
refreshing={loading}
|
||||
onRefresh={refreshData}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
renderSectionHeader={(info) => getRenderSectionHeader(info, loading)}
|
||||
renderItem={getRenderItem}
|
||||
stickySectionHeadersEnabled={props.stickyHeader}
|
||||
style={styles.container}
|
||||
ListHeaderComponent={
|
||||
props.renderListHeaderComponent != null
|
||||
? props.renderListHeaderComponent(data)
|
||||
: null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
loading ? (
|
||||
<BasicLoadingScreen />
|
||||
) : (
|
||||
<ErrorView
|
||||
status={ERROR_TYPE.CONNECTION_ERROR}
|
||||
button={{
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: refreshData,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
getItemLayout={
|
||||
itemHeight ? (d, i) => getItemLayout(itemHeight, d, i) : undefined
|
||||
}
|
||||
onScroll={onScroll}
|
||||
hasTab={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
|
@ -251,8 +251,12 @@ function WebViewScreen(props: Props) {
|
|||
renderLoading={getRenderLoading}
|
||||
renderError={() => (
|
||||
<ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={onRefreshClicked}
|
||||
status={ERROR_TYPE.CONNECTION_ERROR}
|
||||
button={{
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: onRefreshClicked,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onNavigationStateChange={setNavState}
|
||||
|
|
|
@ -21,11 +21,14 @@ const STUDENT_SERVER = 'https://etud.insa-toulouse.fr/';
|
|||
const AMICALE_SERVER = 'https://www.amicale-insat.fr/';
|
||||
const GIT_SERVER =
|
||||
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/';
|
||||
const PLANEX_SERVER = 'http://planex.insa-toulouse.fr/';
|
||||
|
||||
const AMICALE_ENDPOINT = AMICALE_SERVER + 'api/';
|
||||
|
||||
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/';
|
||||
|
||||
export default {
|
||||
|
@ -39,7 +42,16 @@ export default {
|
|||
dashboard: APP_ENDPOINT + 'dashboard/dashboard_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: {
|
||||
proxiwash: APP_IMAGES_ENDPOINT + 'Proxiwash.png',
|
||||
washer: APP_IMAGES_ENDPOINT + 'ProxiwashLaveLinge.png',
|
||||
|
|
|
@ -84,6 +84,10 @@ export type FullParamsList = DefaultParams & {
|
|||
};
|
||||
'equipment-rent': { item?: DeviceType };
|
||||
'gallery': { images: Array<{ url: string }> };
|
||||
[MainRoutes.ProximoList]: {
|
||||
shouldFocusSearchBar: boolean;
|
||||
category: number;
|
||||
};
|
||||
};
|
||||
|
||||
// Don't know why but TS is complaining without this
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
FlatList,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
SectionListData,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
|
@ -52,6 +53,7 @@ import { getDisplayEvent, getFutureEvents } from '../../utils/Home';
|
|||
import type { PlanningEventType } from '../../utils/Planning';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { readData } from '../../utils/WebData';
|
||||
|
||||
const FEED_ITEM_HEIGHT = 500;
|
||||
|
||||
|
@ -314,12 +316,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
|
|||
getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item);
|
||||
|
||||
getRenderSectionHeader = (
|
||||
data: {
|
||||
section: {
|
||||
data: Array<object>;
|
||||
title: string;
|
||||
};
|
||||
},
|
||||
data: { section: SectionListData<FeedItemType> },
|
||||
isLoading: boolean
|
||||
) => {
|
||||
const { props } = this;
|
||||
|
@ -352,7 +349,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
|
|||
);
|
||||
};
|
||||
|
||||
getListHeader = (fetchedData: RawDashboardType) => {
|
||||
getListHeader = (fetchedData: RawDashboardType | undefined) => {
|
||||
let dashboard = null;
|
||||
if (fetchedData != null) {
|
||||
dashboard = fetchedData.dashboard;
|
||||
|
@ -404,21 +401,20 @@ class HomeScreen extends React.Component<PropsType, StateType> {
|
|||
* @return {*}
|
||||
*/
|
||||
createDataset = (
|
||||
fetchedData: RawDashboardType | null,
|
||||
fetchedData: RawDashboardType | undefined,
|
||||
isLoading: boolean
|
||||
): Array<{
|
||||
title: string;
|
||||
data: [] | Array<FeedItemType>;
|
||||
id: string;
|
||||
}> => {
|
||||
// fetchedData = DATA;
|
||||
if (fetchedData != null) {
|
||||
if (fetchedData.news_feed != null) {
|
||||
if (fetchedData) {
|
||||
if (fetchedData.news_feed) {
|
||||
this.currentNewFeed = HomeScreen.generateNewsFeed(
|
||||
fetchedData.news_feed
|
||||
);
|
||||
}
|
||||
if (fetchedData.dashboard != null) {
|
||||
if (fetchedData.dashboard) {
|
||||
this.currentDashboard = fetchedData.dashboard;
|
||||
}
|
||||
}
|
||||
|
@ -470,11 +466,10 @@ class HomeScreen extends React.Component<PropsType, StateType> {
|
|||
<View style={GENERAL_STYLES.flex}>
|
||||
<View style={styles.content}>
|
||||
<WebSectionList
|
||||
navigation={props.navigation}
|
||||
request={() => readData<RawDashboardType>(Urls.app.dashboard)}
|
||||
createDataset={this.createDataset}
|
||||
autoRefreshTime={REFRESH_TIME}
|
||||
refreshOnFocus
|
||||
fetchUrl={Urls.app.dashboard}
|
||||
refreshOnFocus={true}
|
||||
renderItem={this.getRenderItem}
|
||||
itemHeight={FEED_ITEM_HEIGHT}
|
||||
onScroll={this.onScroll}
|
||||
|
|
|
@ -26,6 +26,8 @@ import { stringMatchQuery } from '../../utils/Search';
|
|||
import WebSectionList from '../../components/Screens/WebSectionList';
|
||||
import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { readData } from '../../utils/WebData';
|
||||
|
||||
export type PlanexGroupType = {
|
||||
name: string;
|
||||
|
@ -60,8 +62,6 @@ function sortName(
|
|||
return 0;
|
||||
}
|
||||
|
||||
const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1';
|
||||
|
||||
/**
|
||||
* Class defining planex group selection screen.
|
||||
*/
|
||||
|
@ -137,9 +137,13 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
|
|||
* @param fetchedData
|
||||
* @return {*}
|
||||
* */
|
||||
createDataset = (fetchedData: {
|
||||
[key: string]: PlanexGroupCategoryType;
|
||||
}): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => {
|
||||
createDataset = (
|
||||
fetchedData:
|
||||
| {
|
||||
[key: string]: PlanexGroupCategoryType;
|
||||
}
|
||||
| undefined
|
||||
): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => {
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
|
@ -236,20 +240,28 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
|
|||
* @param fetchedData The raw data fetched from the server
|
||||
* @returns {[]}
|
||||
*/
|
||||
generateData(fetchedData: {
|
||||
[key: string]: PlanexGroupCategoryType;
|
||||
}): Array<PlanexGroupCategoryType> {
|
||||
generateData(
|
||||
fetchedData:
|
||||
| {
|
||||
[key: string]: PlanexGroupCategoryType;
|
||||
}
|
||||
| undefined
|
||||
): Array<PlanexGroupCategoryType> {
|
||||
const { favoriteGroups } = this.state;
|
||||
const data: Array<PlanexGroupCategoryType> = [];
|
||||
Object.values(fetchedData).forEach((category: PlanexGroupCategoryType) => {
|
||||
data.push(category);
|
||||
});
|
||||
data.sort(sortName);
|
||||
data.unshift({
|
||||
name: i18n.t('screens.planex.favorites'),
|
||||
id: 0,
|
||||
content: favoriteGroups,
|
||||
});
|
||||
if (fetchedData) {
|
||||
Object.values(fetchedData).forEach(
|
||||
(category: PlanexGroupCategoryType) => {
|
||||
data.push(category);
|
||||
}
|
||||
);
|
||||
data.sort(sortName);
|
||||
data.unshift({
|
||||
name: i18n.t('screens.planex.favorites'),
|
||||
id: 0,
|
||||
content: favoriteGroups,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -298,14 +310,16 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const { state } = this;
|
||||
return (
|
||||
<WebSectionList
|
||||
navigation={props.navigation}
|
||||
request={() =>
|
||||
readData<{ [key: string]: PlanexGroupCategoryType }>(
|
||||
Urls.planex.groups
|
||||
)
|
||||
}
|
||||
createDataset={this.createDataset}
|
||||
autoRefreshTime={0}
|
||||
refreshOnFocus={false}
|
||||
fetchUrl={GROUPS_URL}
|
||||
refreshOnFocus={true}
|
||||
renderItem={this.getRenderItem}
|
||||
updateData={state.currentSearchString + state.favoriteGroups.length}
|
||||
/>
|
||||
|
|
|
@ -42,6 +42,7 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
|||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import { getPrettierPlanexGroupName } from '../../utils/Utils';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
|
@ -57,8 +58,6 @@ type StateType = {
|
|||
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
|
||||
// // Remove alpha from given Jquery node
|
||||
// function removeAlpha(node) {
|
||||
|
@ -197,22 +196,19 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
|
|||
* @returns {*}
|
||||
*/
|
||||
getWebView() {
|
||||
const { props, state } = this;
|
||||
const { state } = this;
|
||||
const showWebview = state.currentGroup.id !== -1;
|
||||
console.log(state.injectJS);
|
||||
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
{!showWebview ? (
|
||||
<ErrorView
|
||||
navigation={props.navigation}
|
||||
icon="account-clock"
|
||||
icon={'account-clock'}
|
||||
message={i18n.t('screens.planex.noGroupSelected')}
|
||||
showRetryButton={false}
|
||||
/>
|
||||
) : null}
|
||||
<WebViewScreen
|
||||
url={PLANEX_URL}
|
||||
url={Urls.planex.planning}
|
||||
initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
|
||||
injectJS={this.state.injectJS}
|
||||
onMessage={this.onMessage}
|
||||
|
|
|
@ -28,9 +28,7 @@ import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
|||
import { apiRequest, ERROR_TYPE } from '../../utils/WebData';
|
||||
import ErrorView from '../../components/Screens/ErrorView';
|
||||
import CustomHTML from '../../components/Overrides/CustomHTML';
|
||||
import CustomTabBar, {
|
||||
TAB_BAR_HEIGHT,
|
||||
} from '../../components/Tabbar/CustomTabBar';
|
||||
import { TAB_BAR_HEIGHT } from '../../components/Tabbar/CustomTabBar';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
import type { PlanningEventType } from '../../utils/Planning';
|
||||
import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
|
||||
|
@ -163,12 +161,9 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
|
|||
* @returns {*}
|
||||
*/
|
||||
getErrorView() {
|
||||
const { navigation } = this.props;
|
||||
if (this.errorCode === ERROR_TYPE.BAD_INPUT) {
|
||||
return (
|
||||
<ErrorView
|
||||
navigation={navigation}
|
||||
showRetryButton={false}
|
||||
message={i18n.t('screens.planning.invalidEvent')}
|
||||
icon="calendar-remove"
|
||||
/>
|
||||
|
@ -176,9 +171,12 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
return (
|
||||
<ErrorView
|
||||
navigation={navigation}
|
||||
errorCode={this.errorCode}
|
||||
onRefresh={this.fetchData}
|
||||
status={this.errorCode}
|
||||
button={{
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: this.fetchData,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,13 @@
|
|||
*/
|
||||
|
||||
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 { Avatar, Button, Card, Text, withTheme } from 'react-native-paper';
|
||||
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 { LaundromatType } from './ProxiwashAboutScreen';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import { readData } from '../../utils/WebData';
|
||||
|
||||
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
@ -72,6 +79,11 @@ type StateType = {
|
|||
selectedWash: string;
|
||||
};
|
||||
|
||||
type FetchedDataType = {
|
||||
dryers: Array<ProxiwashMachineType>;
|
||||
washers: Array<ProxiwashMachineType>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
|
@ -277,7 +289,11 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
|
|||
* @param section The section to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderSectionHeader = ({ section }: { section: { title: string } }) => {
|
||||
getRenderSectionHeader = ({
|
||||
section,
|
||||
}: {
|
||||
section: SectionListData<ProxiwashMachineType>;
|
||||
}) => {
|
||||
const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
|
||||
const nbAvailable = this.getMachineAvailableNumber(isDryer);
|
||||
return (
|
||||
|
@ -296,20 +312,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
|
|||
* @param section The object describing the current SectionList section
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
getRenderItem = ({
|
||||
item,
|
||||
section,
|
||||
}: {
|
||||
item: ProxiwashMachineType;
|
||||
section: { title: string };
|
||||
}) => {
|
||||
getRenderItem = (data: SectionListRenderItemInfo<ProxiwashMachineType>) => {
|
||||
const { machinesWatched } = this.state;
|
||||
const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
|
||||
const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers');
|
||||
return (
|
||||
<ProxiwashListItem
|
||||
item={item}
|
||||
item={data.item}
|
||||
onPress={this.showModal}
|
||||
isWatched={isMachineWatched(item, machinesWatched)}
|
||||
isWatched={isMachineWatched(data.item, machinesWatched)}
|
||||
isDryer={isDryer}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
|
@ -382,37 +392,40 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
|
|||
* @param fetchedData
|
||||
* @return {*}
|
||||
*/
|
||||
createDataset = (fetchedData: {
|
||||
dryers: Array<ProxiwashMachineType>;
|
||||
washers: Array<ProxiwashMachineType>;
|
||||
}): SectionListDataType<ProxiwashMachineType> => {
|
||||
createDataset = (
|
||||
fetchedData: FetchedDataType | undefined
|
||||
): SectionListDataType<ProxiwashMachineType> => {
|
||||
const { state } = this;
|
||||
let data = fetchedData;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
|
||||
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
|
||||
AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
|
||||
AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
|
||||
if (fetchedData) {
|
||||
let data = fetchedData;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
|
||||
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
|
||||
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() {
|
||||
const { state } = this;
|
||||
const { navigation } = this.props;
|
||||
let data: LaundromatType;
|
||||
switch (state.selectedWash) {
|
||||
case 'tripodeB':
|
||||
|
@ -494,13 +506,12 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
|
|||
<View style={GENERAL_STYLES.flex}>
|
||||
<View style={styles.container}>
|
||||
<WebSectionList
|
||||
request={() => readData<FetchedDataType>(data.url)}
|
||||
createDataset={this.createDataset}
|
||||
navigation={navigation}
|
||||
fetchUrl={data.url}
|
||||
renderItem={this.getRenderItem}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
autoRefreshTime={REFRESH_TIME}
|
||||
refreshOnFocus
|
||||
refreshOnFocus={true}
|
||||
updateData={state.machinesWatched.length}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* 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 i18n from 'i18n-js';
|
||||
import {
|
||||
|
@ -26,9 +26,8 @@ import {
|
|||
Subheading,
|
||||
Text,
|
||||
Title,
|
||||
withTheme,
|
||||
useTheme,
|
||||
} 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';
|
||||
|
@ -36,19 +35,29 @@ import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem';
|
|||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../../components/Overrides/CustomHeaderButton';
|
||||
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
|
||||
import type { ProximoArticleType } from './ProximoMainScreen';
|
||||
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 {
|
||||
return parseInt(a.price, 10) - parseInt(b.price, 10);
|
||||
return a.price - b.price;
|
||||
}
|
||||
|
||||
function sortPriceReverse(
|
||||
a: ProximoArticleType,
|
||||
b: ProximoArticleType
|
||||
): number {
|
||||
return parseInt(b.price, 10) - parseInt(a.price, 10);
|
||||
return b.price - a.price;
|
||||
}
|
||||
|
||||
function sortName(a: ProximoArticleType, b: ProximoArticleType): number {
|
||||
|
@ -73,23 +82,6 @@ function sortNameReverse(a: ProximoArticleType, b: ProximoArticleType): number {
|
|||
|
||||
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({
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
|
@ -118,113 +110,72 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Class defining Proximo article list of a certain category.
|
||||
*/
|
||||
class ProximoListScreen extends React.Component<PropsType, StateType> {
|
||||
modalRef: Modalize | null;
|
||||
type ArticlesType = Array<ProximoArticleType>;
|
||||
|
||||
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) {
|
||||
super(props);
|
||||
this.modalRef = null;
|
||||
this.listData = props.route.params.data.data.sort(sortName);
|
||||
this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar;
|
||||
this.state = {
|
||||
currentSearchString: '',
|
||||
currentSortMode: 3,
|
||||
modalCurrentDisplayItem: null,
|
||||
};
|
||||
}
|
||||
const [currentSearchString, setCurrentSearchString] = useState('');
|
||||
const [currentSortMode, setCurrentSortMode] = useState(2);
|
||||
const [modalCurrentDisplayItem, setModalCurrentDisplayItem] = useState<
|
||||
React.ReactNode | undefined
|
||||
>();
|
||||
|
||||
/**
|
||||
* Creates the header content
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { navigation } = this.props;
|
||||
const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: this.getSortMenuButton,
|
||||
headerTitle: this.getSearchBar,
|
||||
headerRight: getSortMenuButton,
|
||||
headerTitle: getSearchBar,
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleContainerStyle:
|
||||
Platform.OS === 'ios'
|
||||
? { marginHorizontal: 0, width: '70%' }
|
||||
: { 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.
|
||||
* It will open the modal to show a sort selection
|
||||
*/
|
||||
onSortMenuPress = () => {
|
||||
this.setState({
|
||||
modalCurrentDisplayItem: this.getModalSortMenu(),
|
||||
});
|
||||
if (this.modalRef) {
|
||||
this.modalRef.open();
|
||||
const onSortMenuPress = () => {
|
||||
setModalCurrentDisplayItem(getModalSortMenu());
|
||||
if (modalRef.current) {
|
||||
modalRef.current.open();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when the search changes
|
||||
*
|
||||
* @param str The new search string
|
||||
*/
|
||||
onSearchStringChange = (str: string) => {
|
||||
this.setState({ currentSearchString: str });
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when clicking an article in the list.
|
||||
* It opens the modal to show detailed information about the article
|
||||
*
|
||||
* @param item The article pressed
|
||||
*/
|
||||
onListItemPress(item: ProximoArticleType) {
|
||||
this.setState({
|
||||
modalCurrentDisplayItem: this.getModalItemContent(item),
|
||||
});
|
||||
if (this.modalRef) {
|
||||
this.modalRef.open();
|
||||
const onListItemPress = (item: ProximoArticleType) => {
|
||||
setModalCurrentDisplayItem(getModalItemContent(item));
|
||||
if (modalRef.current) {
|
||||
modalRef.current.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the current sort mode.
|
||||
*
|
||||
* @param mode The number representing the mode
|
||||
*/
|
||||
setSortMode(mode: string) {
|
||||
const { currentSortMode } = this.state;
|
||||
const setSortMode = (mode: string) => {
|
||||
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;
|
||||
setCurrentSortMode(currentMode);
|
||||
if (modalRef.current && currentMode !== currentSortMode) {
|
||||
modalRef.current.close();
|
||||
}
|
||||
if (this.modalRef && currentMode !== currentSortMode) {
|
||||
this.modalRef.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a color depending on the quantity available
|
||||
|
@ -232,8 +183,7 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
|
|||
* @param availableStock The quantity available
|
||||
* @return
|
||||
*/
|
||||
getStockColor(availableStock: number): string {
|
||||
const { theme } = this.props;
|
||||
const getStockColor = (availableStock: number): string => {
|
||||
let color: string;
|
||||
if (availableStock > 3) {
|
||||
color = theme.colors.success;
|
||||
|
@ -243,17 +193,17 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
|
|||
color = theme.colors.danger;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the sort menu header button
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getSortMenuButton = () => {
|
||||
const getSortMenuButton = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item title="main" iconName="sort" onPress={this.onSortMenuPress} />
|
||||
<Item title="main" iconName="sort" onPress={onSortMenuPress} />
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
@ -263,12 +213,13 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
|
|||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getSearchBar = () => {
|
||||
const getSearchBar = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Searchbar
|
||||
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
|
||||
* @return {*}
|
||||
*/
|
||||
getModalItemContent(item: ProximoArticleType) {
|
||||
const getModalItemContent = (item: ProximoArticleType) => {
|
||||
return (
|
||||
<View style={styles.modalContainer}>
|
||||
<Title>{item.name}</Title>
|
||||
<View style={styles.modalTitleContainer}>
|
||||
<Subheading
|
||||
style={{
|
||||
color: this.getStockColor(parseInt(item.quantity, 10)),
|
||||
color: getStockColor(item.quantity),
|
||||
}}
|
||||
>
|
||||
{`${item.quantity} ${i18n.t('screens.proximo.inStock')}`}
|
||||
|
@ -302,46 +253,43 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
|
|||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the modal content to display a sort menu
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getModalSortMenu() {
|
||||
const { currentSortMode } = this.state;
|
||||
const getModalSortMenu = () => {
|
||||
return (
|
||||
<View style={styles.modalContainer}>
|
||||
<Title style={styles.sortTitle}>
|
||||
{i18n.t('screens.proximo.sortOrder')}
|
||||
</Title>
|
||||
<RadioButton.Group
|
||||
onValueChange={(value: string) => {
|
||||
this.setSortMode(value);
|
||||
}}
|
||||
onValueChange={setSortMode}
|
||||
value={currentSortMode.toString()}
|
||||
>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortPrice')}
|
||||
value={'1'}
|
||||
value={'0'}
|
||||
/>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortPriceReverse')}
|
||||
value={'2'}
|
||||
value={'1'}
|
||||
/>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortName')}
|
||||
value={'3'}
|
||||
value={'2'}
|
||||
/>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortNameReverse')}
|
||||
value={'4'}
|
||||
value={'3'}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderItem = ({ item }: { item: ProximoArticleType }) => {
|
||||
const { currentSearchString } = this.state;
|
||||
const getRenderItem = ({ item }: { item: ProximoArticleType }) => {
|
||||
if (stringMatchQuery(item.name, currentSearchString)) {
|
||||
const onPress = () => {
|
||||
this.onListItemPress(item);
|
||||
onListItemPress(item);
|
||||
};
|
||||
const color = this.getStockColor(parseInt(item.quantity, 10));
|
||||
const color = getStockColor(item.quantity);
|
||||
return (
|
||||
<ProximoListItem
|
||||
item={item}
|
||||
|
@ -374,46 +321,55 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
|
|||
* @param item The article to extract the key from
|
||||
* @return {string} The extracted key
|
||||
*/
|
||||
keyExtractor = (item: ProximoArticleType): string => item.name + item.code;
|
||||
const keyExtractor = (item: ProximoArticleType): string =>
|
||||
item.name + item.code;
|
||||
|
||||
/**
|
||||
* Callback used when receiving the modal ref
|
||||
*
|
||||
* @param ref
|
||||
*/
|
||||
onModalRef = (ref: Modalize) => {
|
||||
this.modalRef = ref;
|
||||
const createDataset = (
|
||||
data: ArticlesType | undefined
|
||||
): SectionListDataType<ProximoArticleType> => {
|
||||
if (data) {
|
||||
console.log(data);
|
||||
console.log(props.route.params.category);
|
||||
|
||||
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 = (
|
||||
data: Array<ProximoArticleType> | null | undefined,
|
||||
index: number
|
||||
): { length: number; offset: number; index: number } => ({
|
||||
length: LIST_ITEM_HEIGHT,
|
||||
offset: LIST_ITEM_HEIGHT * index,
|
||||
index,
|
||||
});
|
||||
|
||||
render() {
|
||||
const { state } = this;
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CustomModal onRef={(ref) => (modalRef.current = ref)}>
|
||||
{modalCurrentDisplayItem}
|
||||
</CustomModal>
|
||||
<WebSectionList
|
||||
request={() => readData<ArticlesType>(Urls.proximo.articles)}
|
||||
createDataset={createDataset}
|
||||
refreshOnFocus={true}
|
||||
renderItem={getRenderItem}
|
||||
updateData={currentSearchString + currentSortMode}
|
||||
itemHeight={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(ProximoListScreen);
|
||||
export default ProximoListScreen;
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import { List, withTheme } from 'react-native-paper';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { Avatar, List, useTheme, withTheme } from 'react-native-paper';
|
||||
import WebSectionList from '../../../components/Screens/WebSectionList';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
|
@ -28,40 +27,35 @@ import MaterialHeaderButtons, {
|
|||
import type { SectionListDataType } from '../../../components/Screens/WebSectionList';
|
||||
import { StyleSheet } from 'react-native';
|
||||
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;
|
||||
|
||||
export type ProximoCategoryType = {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type ProximoArticleType = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
quantity: string;
|
||||
price: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
code: string;
|
||||
id: string;
|
||||
type: Array<string>;
|
||||
image: string;
|
||||
category_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
category: ProximoCategoryType;
|
||||
};
|
||||
|
||||
export type ProximoMainListItemType = {
|
||||
type: ProximoCategoryType;
|
||||
data: Array<ProximoArticleType>;
|
||||
};
|
||||
|
||||
export type ProximoDataType = {
|
||||
types: Array<ProximoCategoryType>;
|
||||
articles: Array<ProximoArticleType>;
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
type CategoriesType = Array<ProximoCategoryType>;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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.
|
||||
* This screen shows the different categories of articles offered by proximo.
|
||||
*/
|
||||
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();
|
||||
function ProximoMainScreen() {
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
|
||||
// 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: 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;
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => this.getHeaderButtons(),
|
||||
headerRight: () => getHeaderButtons(),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Callback used when the search button is pressed.
|
||||
* This will open a new ProximoListScreen with all items displayed
|
||||
*/
|
||||
onPressSearchBtn = () => {
|
||||
const { navigation } = this.props;
|
||||
const onPressSearchBtn = () => {
|
||||
const searchScreenData = {
|
||||
shouldFocusSearchBar: true,
|
||||
data: {
|
||||
type: {
|
||||
id: '0',
|
||||
name: i18n.t('screens.proximo.all'),
|
||||
icon: 'star',
|
||||
},
|
||||
data:
|
||||
this.articles != null
|
||||
? ProximoMainScreen.getAvailableArticles(this.articles)
|
||||
: [],
|
||||
},
|
||||
category: -1,
|
||||
};
|
||||
navigation.navigate('proximo-list', searchScreenData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when the about button is pressed.
|
||||
* This will open the ProximoAboutScreen
|
||||
*/
|
||||
onPressAboutBtn = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('proximo-about');
|
||||
};
|
||||
const onPressAboutBtn = () => navigation.navigate('proximo-about');
|
||||
|
||||
/**
|
||||
* Gets the header buttons
|
||||
* @return {*}
|
||||
*/
|
||||
getHeaderButtons() {
|
||||
const getHeaderButtons = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="magnify"
|
||||
iconName="magnify"
|
||||
onPress={this.onPressSearchBtn}
|
||||
/>
|
||||
<Item title="magnify" iconName="magnify" onPress={onPressSearchBtn} />
|
||||
<Item
|
||||
title="information"
|
||||
iconName="information"
|
||||
onPress={this.onPressAboutBtn}
|
||||
onPress={onPressAboutBtn}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
|
@ -216,33 +142,36 @@ class ProximoMainScreen extends React.Component<PropsType> {
|
|||
* @param item The category to render
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderItem = ({ item }: { item: ProximoMainListItemType }) => {
|
||||
const { navigation, theme } = this.props;
|
||||
const getRenderItem = ({ item }: { item: ProximoCategoryType }) => {
|
||||
const dataToSend = {
|
||||
shouldFocusSearchBar: false,
|
||||
data: item,
|
||||
category: item.id,
|
||||
};
|
||||
const subtitle = `${item.data.length} ${
|
||||
item.data.length > 1
|
||||
// TODO get article number
|
||||
const article_number = 1;
|
||||
const subtitle = `${article_number} ${
|
||||
article_number > 1
|
||||
? i18n.t('screens.proximo.articles')
|
||||
: i18n.t('screens.proximo.article')
|
||||
}`;
|
||||
const onPress = () => {
|
||||
navigation.navigate('proximo-list', dataToSend);
|
||||
};
|
||||
if (item.data.length > 0) {
|
||||
const onPress = () => navigation.navigate('proximo-list', dataToSend);
|
||||
if (article_number > 0) {
|
||||
return (
|
||||
<List.Item
|
||||
title={item.type.name}
|
||||
title={item.name}
|
||||
description={subtitle}
|
||||
onPress={onPress}
|
||||
left={(props) => (
|
||||
<List.Icon
|
||||
style={props.style}
|
||||
icon={item.type.icon}
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
)}
|
||||
left={(props) =>
|
||||
item.icon.endsWith('.png') ? (
|
||||
<Avatar.Image style={props.style} source={{ uri: item.icon }} />
|
||||
) : (
|
||||
<List.Icon
|
||||
style={props.style}
|
||||
icon={item.icon}
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
)
|
||||
}
|
||||
right={(props) => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
|
@ -266,65 +195,46 @@ class ProximoMainScreen extends React.Component<PropsType> {
|
|||
* @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',
|
||||
const createDataset = (
|
||||
data: CategoriesType | undefined
|
||||
): SectionListDataType<ProximoCategoryType> => {
|
||||
if (data) {
|
||||
const finalData: CategoriesType = [
|
||||
{
|
||||
id: -1,
|
||||
name: i18n.t('screens.proximo.all'),
|
||||
icon: 'star',
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
data: ProximoMainScreen.getAvailableArticles(this.articles),
|
||||
});
|
||||
types.forEach((type: ProximoCategoryType) => {
|
||||
finalData.push({
|
||||
type,
|
||||
data: ProximoMainScreen.getAvailableArticles(this.articles, type),
|
||||
});
|
||||
});
|
||||
...data,
|
||||
];
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
data: finalData.sort(sortFinalData),
|
||||
keyExtractor: getKeyExtractor,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
data: [],
|
||||
keyExtractor: getKeyExtractor,
|
||||
},
|
||||
];
|
||||
}
|
||||
finalData.sort(ProximoMainScreen.sortFinalData);
|
||||
return finalData;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<WebSectionList
|
||||
createDataset={this.createDataset}
|
||||
navigation={navigation}
|
||||
autoRefreshTime={0}
|
||||
refreshOnFocus={false}
|
||||
fetchUrl={Urls.proximo}
|
||||
renderItem={this.getRenderItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<WebSectionList
|
||||
request={() => readData<CategoriesType>(Urls.proximo.categories)}
|
||||
createDataset={createDataset}
|
||||
refreshOnFocus={true}
|
||||
renderItem={getRenderItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(ProximoMainScreen);
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
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 { StackNavigationProp } from '@react-navigation/stack';
|
||||
import i18n from 'i18n-js';
|
||||
|
@ -26,6 +26,7 @@ import DateManager from '../../managers/DateManager';
|
|||
import WebSectionList from '../../components/Screens/WebSectionList';
|
||||
import type { SectionListDataType } from '../../components/Screens/WebSectionList';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { readData } from '../../utils/WebData';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
|
@ -108,7 +109,7 @@ class SelfMenuScreen extends React.Component<PropsType> {
|
|||
* @return {[]}
|
||||
*/
|
||||
createDataset = (
|
||||
fetchedData: Array<RawRuMenuType>
|
||||
fetchedData: Array<RawRuMenuType> | undefined
|
||||
): SectionListDataType<RuFoodCategoryType> => {
|
||||
let result: SectionListDataType<RuFoodCategoryType> = [];
|
||||
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
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderSectionHeader = ({ section }: { section: { title: string } }) => {
|
||||
getRenderSectionHeader = ({
|
||||
section,
|
||||
}: {
|
||||
section: SectionListData<RuFoodCategoryType>;
|
||||
}) => {
|
||||
return (
|
||||
<Card style={styles.headerCard}>
|
||||
<Card.Title
|
||||
|
@ -189,17 +194,14 @@ class SelfMenuScreen extends React.Component<PropsType> {
|
|||
getKeyExtractor = (item: RuFoodCategoryType): string => item.name;
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<WebSectionList
|
||||
request={() => readData<Array<RawRuMenuType>>(Urls.app.menu)}
|
||||
createDataset={this.createDataset}
|
||||
navigation={navigation}
|
||||
autoRefreshTime={0}
|
||||
refreshOnFocus={false}
|
||||
fetchUrl={Urls.app.menu}
|
||||
refreshOnFocus={true}
|
||||
renderItem={this.getRenderItem}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
stickyHeader
|
||||
stickyHeader={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -105,7 +105,6 @@ class WebsiteScreen extends React.Component<Props, State> {
|
|||
const { route, navigation } = this.props;
|
||||
|
||||
if (route.params != null) {
|
||||
console.log(route.params);
|
||||
this.host = route.params.host;
|
||||
let { path } = route.params;
|
||||
const { title } = route.params;
|
||||
|
|
|
@ -141,7 +141,6 @@ class Test extends React.Component<Props> {
|
|||
// );
|
||||
return (
|
||||
<WebSectionList
|
||||
navigation={props.navigation}
|
||||
createDataset={this.createDataset}
|
||||
autoRefreshTime={REFRESH_TIME}
|
||||
refreshOnFocus
|
||||
|
|
22
src/utils/Requests.tsx
Normal file
22
src/utils/Requests.tsx
Normal 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,
|
||||
}
|
|
@ -120,11 +120,11 @@ export async function apiRequest<T>(
|
|||
* @param url The urls to fetch data from
|
||||
* @return Promise<any>
|
||||
*/
|
||||
export async function readData(url: string): Promise<any> {
|
||||
return new Promise((resolve: (response: any) => void, reject: () => void) => {
|
||||
export async function readData<T>(url: string): Promise<T> {
|
||||
return new Promise((resolve: (response: T) => void, reject: () => void) => {
|
||||
fetch(url)
|
||||
.then(async (response: Response): Promise<any> => response.json())
|
||||
.then((data: any): void => resolve(data))
|
||||
.catch((): void => reject());
|
||||
.then((data: T) => resolve(data))
|
||||
.catch(() => reject());
|
||||
});
|
||||
}
|
||||
|
|
21
src/utils/cacheContext.ts
Normal file
21
src/utils/cacheContext.ts
Normal 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
106
src/utils/customHooks.tsx
Normal 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;
|
||||
}
|
|
@ -30,10 +30,10 @@
|
|||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"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. */
|
||||
"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. */
|
||||
"resolveJsonModule": true /* Allow import of JSON files */
|
||||
"resolveJsonModule": true, /* Allow import of JSON files */
|
||||
/* Source Map Options */
|
||||
// "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. */
|
||||
// "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. */
|
||||
|
||||
"skipLibCheck": true,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
|
Loading…
Reference in a new issue