Browse Source

Improve websectionlist and update proximo api

Arnaud Vergnet 6 months ago
parent
commit
a94006d18a

+ 3
- 0
package.json View File

@@ -92,6 +92,9 @@
92 92
       "prettier"
93 93
     ],
94 94
     "rules": {
95
+      "no-undef": 0,
96
+      "no-shadow": "off",
97
+      "@typescript-eslint/no-shadow": ["error"],
95 98
       "prettier/prettier": [
96 99
         "error",
97 100
         {

+ 20
- 2
src/components/Amicale/AuthenticatedScreen.tsx View File

@@ -23,6 +23,7 @@ import ConnectionManager from '../../managers/ConnectionManager';
23 23
 import { ERROR_TYPE } from '../../utils/WebData';
24 24
 import ErrorView from '../Screens/ErrorView';
25 25
 import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
26
+import i18n from 'i18n-js';
26 27
 
27 28
 type PropsType<T> = {
28 29
   navigation: StackNavigationProp<any>;
@@ -151,11 +152,28 @@ class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
151 152
         <ErrorView
152 153
           icon={override.icon}
153 154
           message={override.message}
154
-          showRetryButton={override.showRetryButton}
155
+          button={
156
+            override.showRetryButton
157
+              ? {
158
+                  icon: 'refresh',
159
+                  text: i18n.t('general.retry'),
160
+                  onPress: this.fetchData,
161
+                }
162
+              : undefined
163
+          }
155 164
         />
156 165
       );
157 166
     }
158
-    return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
167
+    return (
168
+      <ErrorView
169
+        status={errorCode}
170
+        button={{
171
+          icon: 'refresh',
172
+          text: i18n.t('general.retry'),
173
+          onPress: this.fetchData,
174
+        }}
175
+      />
176
+    );
159 177
   }
160 178
 
161 179
   /**

+ 4
- 1
src/components/Lists/Proximo/ProximoListItem.tsx View File

@@ -22,6 +22,7 @@ import { Avatar, List, Text } from 'react-native-paper';
22 22
 import i18n from 'i18n-js';
23 23
 import type { ProximoArticleType } from '../../../screens/Services/Proximo/ProximoMainScreen';
24 24
 import { StyleSheet } from 'react-native';
25
+import Urls from '../../../constants/Urls';
25 26
 
26 27
 type PropsType = {
27 28
   onPress: () => void;
@@ -43,6 +44,8 @@ const styles = StyleSheet.create({
43 44
 });
44 45
 
45 46
 function ProximoListItem(props: PropsType) {
47
+  // console.log(Urls.proximo.images + props.item.image);
48
+
46 49
   return (
47 50
     <List.Item
48 51
       title={props.item.name}
@@ -55,7 +58,7 @@ function ProximoListItem(props: PropsType) {
55 58
         <Avatar.Image
56 59
           style={styles.avatar}
57 60
           size={64}
58
-          source={{ uri: props.item.image }}
61
+          source={{ uri: Urls.proximo.images + props.item.image }}
59 62
         />
60 63
       )}
61 64
       right={() => <Text style={styles.text}>{props.item.price}€</Text>}

+ 108
- 137
src/components/Screens/ErrorView.tsx View File

@@ -18,28 +18,29 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import { Button, Subheading, withTheme } from 'react-native-paper';
21
+import { Button, Subheading, useTheme } from 'react-native-paper';
22 22
 import { StyleSheet, View } from 'react-native';
23 23
 import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
24 24
 import i18n from 'i18n-js';
25 25
 import * as Animatable from 'react-native-animatable';
26
-import { StackNavigationProp } from '@react-navigation/stack';
27
-import { ERROR_TYPE } from '../../utils/WebData';
26
+import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
28 27
 
29
-type PropsType = {
30
-  navigation?: StackNavigationProp<any>;
31
-  theme: ReactNativePaper.Theme;
32
-  route?: { name: string };
33
-  onRefresh?: () => void;
34
-  errorCode?: number;
28
+type Props = {
29
+  status?: Exclude<REQUEST_STATUS, REQUEST_STATUS.SUCCESS>;
30
+  code?: Exclude<REQUEST_CODES, REQUEST_CODES.SUCCESS>;
35 31
   icon?: string;
36 32
   message?: string;
37
-  showRetryButton?: boolean;
33
+  loading?: boolean;
34
+  button?: {
35
+    text: string;
36
+    icon: string;
37
+    onPress: () => void;
38
+  };
38 39
 };
39 40
 
40 41
 const styles = StyleSheet.create({
41 42
   outer: {
42
-    height: '100%',
43
+    flex: 1,
43 44
   },
44 45
   inner: {
45 46
     marginTop: 'auto',
@@ -61,134 +62,96 @@ const styles = StyleSheet.create({
61 62
   },
62 63
 });
63 64
 
64
-class ErrorView extends React.PureComponent<PropsType> {
65
-  static defaultProps = {
66
-    onRefresh: () => {},
67
-    errorCode: 0,
68
-    icon: '',
65
+function getMessage(props: Props) {
66
+  let fullMessage = {
69 67
     message: '',
70
-    showRetryButton: true,
68
+    icon: '',
71 69
   };
72
-
73
-  message: string;
74
-
75
-  icon: string;
76
-
77
-  showLoginButton: boolean;
78
-
79
-  constructor(props: PropsType) {
80
-    super(props);
81
-    this.icon = '';
82
-    this.showLoginButton = false;
83
-    this.message = '';
84
-  }
85
-
86
-  getRetryButton() {
87
-    const { props } = this;
88
-    return (
89
-      <Button
90
-        mode="contained"
91
-        icon="refresh"
92
-        onPress={props.onRefresh}
93
-        style={styles.button}
94
-      >
95
-        {i18n.t('general.retry')}
96
-      </Button>
97
-    );
70
+  if (props.code === undefined) {
71
+    switch (props.status) {
72
+      case REQUEST_STATUS.BAD_INPUT:
73
+        fullMessage.message = i18n.t('errors.badInput');
74
+        fullMessage.icon = 'alert-circle-outline';
75
+        break;
76
+      case REQUEST_STATUS.FORBIDDEN:
77
+        fullMessage.message = i18n.t('errors.forbidden');
78
+        fullMessage.icon = 'lock';
79
+        break;
80
+      case REQUEST_STATUS.CONNECTION_ERROR:
81
+        fullMessage.message = i18n.t('errors.connectionError');
82
+        fullMessage.icon = 'access-point-network-off';
83
+        break;
84
+      case REQUEST_STATUS.SERVER_ERROR:
85
+        fullMessage.message = i18n.t('errors.serverError');
86
+        fullMessage.icon = 'server-network-off';
87
+        break;
88
+      default:
89
+        fullMessage.message = i18n.t('errors.unknown');
90
+        fullMessage.icon = 'alert-circle-outline';
91
+        break;
92
+    }
93
+  } else {
94
+    switch (props.code) {
95
+      case REQUEST_CODES.BAD_CREDENTIALS:
96
+        fullMessage.message = i18n.t('errors.badCredentials');
97
+        fullMessage.icon = 'account-alert-outline';
98
+        break;
99
+      case REQUEST_CODES.BAD_TOKEN:
100
+        fullMessage.message = i18n.t('errors.badToken');
101
+        fullMessage.icon = 'account-alert-outline';
102
+        break;
103
+      case REQUEST_CODES.NO_CONSENT:
104
+        fullMessage.message = i18n.t('errors.noConsent');
105
+        fullMessage.icon = 'account-remove-outline';
106
+        break;
107
+      case REQUEST_CODES.TOKEN_SAVE:
108
+        fullMessage.message = i18n.t('errors.tokenSave');
109
+        fullMessage.icon = 'alert-circle-outline';
110
+        break;
111
+      case REQUEST_CODES.BAD_INPUT:
112
+        fullMessage.message = i18n.t('errors.badInput');
113
+        fullMessage.icon = 'alert-circle-outline';
114
+        break;
115
+      case REQUEST_CODES.FORBIDDEN:
116
+        fullMessage.message = i18n.t('errors.forbidden');
117
+        fullMessage.icon = 'lock';
118
+        break;
119
+      case REQUEST_CODES.CONNECTION_ERROR:
120
+        fullMessage.message = i18n.t('errors.connectionError');
121
+        fullMessage.icon = 'access-point-network-off';
122
+        break;
123
+      case REQUEST_CODES.SERVER_ERROR:
124
+        fullMessage.message = i18n.t('errors.serverError');
125
+        fullMessage.icon = 'server-network-off';
126
+        break;
127
+      default:
128
+        fullMessage.message = i18n.t('errors.unknown');
129
+        fullMessage.icon = 'alert-circle-outline';
130
+        break;
131
+    }
98 132
   }
99 133
 
100
-  getLoginButton() {
101
-    return (
102
-      <Button
103
-        mode="contained"
104
-        icon="login"
105
-        onPress={this.goToLogin}
106
-        style={styles.button}
107
-      >
108
-        {i18n.t('screens.login.title')}
109
-      </Button>
110
-    );
134
+  fullMessage.message += `\n\nCode {${props.status}:${props.code}}`;
135
+  if (props.message != null) {
136
+    fullMessage.message = props.message;
111 137
   }
112
-
113
-  goToLogin = () => {
114
-    const { props } = this;
115
-    if (props.navigation) {
116
-      props.navigation.navigate('login', {
117
-        screen: 'login',
118
-        params: { nextScreen: props.route ? props.route.name : undefined },
119
-      });
120
-    }
121
-  };
122
-
123
-  generateMessage() {
124
-    const { props } = this;
125
-    this.showLoginButton = false;
126
-    if (props.errorCode !== 0) {
127
-      switch (props.errorCode) {
128
-        case ERROR_TYPE.BAD_CREDENTIALS:
129
-          this.message = i18n.t('errors.badCredentials');
130
-          this.icon = 'account-alert-outline';
131
-          break;
132
-        case ERROR_TYPE.BAD_TOKEN:
133
-          this.message = i18n.t('errors.badToken');
134
-          this.icon = 'account-alert-outline';
135
-          this.showLoginButton = true;
136
-          break;
137
-        case ERROR_TYPE.NO_CONSENT:
138
-          this.message = i18n.t('errors.noConsent');
139
-          this.icon = 'account-remove-outline';
140
-          break;
141
-        case ERROR_TYPE.TOKEN_SAVE:
142
-          this.message = i18n.t('errors.tokenSave');
143
-          this.icon = 'alert-circle-outline';
144
-          break;
145
-        case ERROR_TYPE.BAD_INPUT:
146
-          this.message = i18n.t('errors.badInput');
147
-          this.icon = 'alert-circle-outline';
148
-          break;
149
-        case ERROR_TYPE.FORBIDDEN:
150
-          this.message = i18n.t('errors.forbidden');
151
-          this.icon = 'lock';
152
-          break;
153
-        case ERROR_TYPE.CONNECTION_ERROR:
154
-          this.message = i18n.t('errors.connectionError');
155
-          this.icon = 'access-point-network-off';
156
-          break;
157
-        case ERROR_TYPE.SERVER_ERROR:
158
-          this.message = i18n.t('errors.serverError');
159
-          this.icon = 'server-network-off';
160
-          break;
161
-        default:
162
-          this.message = i18n.t('errors.unknown');
163
-          this.icon = 'alert-circle-outline';
164
-          break;
165
-      }
166
-      this.message += `\n\nCode ${
167
-        props.errorCode != null ? props.errorCode : -1
168
-      }`;
169
-    } else {
170
-      this.message = props.message != null ? props.message : '';
171
-      this.icon = props.icon != null ? props.icon : '';
172
-    }
138
+  if (props.icon != null) {
139
+    fullMessage.icon = props.icon;
173 140
   }
141
+  return fullMessage;
142
+}
174 143
 
175
-  render() {
176
-    const { props } = this;
177
-    this.generateMessage();
178
-    let button;
179
-    if (this.showLoginButton) {
180
-      button = this.getLoginButton();
181
-    } else if (props.showRetryButton) {
182
-      button = this.getRetryButton();
183
-    } else {
184
-      button = null;
185
-    }
144
+function ErrorView(props: Props) {
145
+  const theme = useTheme();
146
+  const fullMessage = getMessage(props);
147
+  const { button } = props;
186 148
 
187
-    return (
149
+  return (
150
+    <View style={styles.outer}>
188 151
       <Animatable.View
189 152
         style={{
190 153
           ...styles.outer,
191
-          backgroundColor: props.theme.colors.background,
154
+          backgroundColor: theme.colors.background,
192 155
         }}
193 156
         animation="zoomIn"
194 157
         duration={200}
@@ -197,25 +160,33 @@ class ErrorView extends React.PureComponent<PropsType> {
197 160
         <View style={styles.inner}>
198 161
           <View style={styles.iconContainer}>
199 162
             <MaterialCommunityIcons
200
-              // $FlowFixMe
201
-              name={this.icon}
163
+              name={fullMessage.icon}
202 164
               size={150}
203
-              color={props.theme.colors.textDisabled}
165
+              color={theme.colors.disabled}
204 166
             />
205 167
           </View>
206 168
           <Subheading
207 169
             style={{
208 170
               ...styles.subheading,
209
-              color: props.theme.colors.textDisabled,
171
+              color: theme.colors.disabled,
210 172
             }}
211 173
           >
212
-            {this.message}
174
+            {fullMessage.message}
213 175
           </Subheading>
214
-          {button}
176
+          {button ? (
177
+            <Button
178
+              mode={'contained'}
179
+              icon={button.icon}
180
+              onPress={button.onPress}
181
+              style={styles.button}
182
+            >
183
+              {button.text}
184
+            </Button>
185
+          ) : null}
215 186
         </View>
216 187
       </Animatable.View>
217
-    );
218
-  }
188
+    </View>
189
+  );
219 190
 }
220 191
 
221
-export default withTheme(ErrorView);
192
+export default ErrorView;

+ 115
- 0
src/components/Screens/RequestScreen.tsx View File

@@ -0,0 +1,115 @@
1
+import React, { useEffect, useRef } from 'react';
2
+import ErrorView from './ErrorView';
3
+import { useRequestLogic } from '../../utils/customHooks';
4
+import { useFocusEffect } from '@react-navigation/native';
5
+import BasicLoadingScreen from './BasicLoadingScreen';
6
+import i18n from 'i18n-js';
7
+import { REQUEST_STATUS } from '../../utils/Requests';
8
+
9
+export type RequestScreenProps<T> = {
10
+  request: () => Promise<T>;
11
+  render: (
12
+    data: T | undefined,
13
+    loading: boolean,
14
+    refreshData: (newRequest?: () => Promise<T>) => void,
15
+    status: REQUEST_STATUS,
16
+    code: number | undefined
17
+  ) => React.ReactElement;
18
+  cache?: T;
19
+  onCacheUpdate?: (newCache: T) => void;
20
+  onMajorError?: (status: number, code?: number) => void;
21
+  showLoading?: boolean;
22
+  showError?: boolean;
23
+  refreshOnFocus?: boolean;
24
+  autoRefreshTime?: number;
25
+  refresh?: boolean;
26
+  onFinish?: () => void;
27
+};
28
+
29
+export type RequestProps = {
30
+  refreshData: () => void;
31
+  loading: boolean;
32
+};
33
+
34
+type Props<T> = RequestScreenProps<T>;
35
+
36
+const MIN_REFRESH_TIME = 5 * 1000;
37
+
38
+export default function RequestScreen<T>(props: Props<T>) {
39
+  const refreshInterval = useRef<number>();
40
+  const [loading, status, code, data, refreshData] = useRequestLogic<T>(
41
+    () => props.request(),
42
+    props.cache,
43
+    props.onCacheUpdate,
44
+    props.refreshOnFocus,
45
+    MIN_REFRESH_TIME
46
+  );
47
+  // Store last refresh prop value
48
+  const lastRefresh = useRef<boolean>(false);
49
+
50
+  useEffect(() => {
51
+    // Refresh data if refresh prop changed and we are not loading
52
+    if (props.refresh && !lastRefresh.current && !loading) {
53
+      refreshData();
54
+      // Call finish callback if refresh prop was set and we finished loading
55
+    } else if (lastRefresh.current && !loading && props.onFinish) {
56
+      props.onFinish();
57
+    }
58
+    // Update stored refresh prop value
59
+    if (props.refresh !== lastRefresh.current) {
60
+      lastRefresh.current = props.refresh === true;
61
+    }
62
+  }, [props, loading, refreshData]);
63
+
64
+  useFocusEffect(
65
+    React.useCallback(() => {
66
+      if (!props.cache && props.refreshOnFocus !== false) {
67
+        refreshData();
68
+      }
69
+      if (props.autoRefreshTime && props.autoRefreshTime > 0) {
70
+        refreshInterval.current = setInterval(
71
+          refreshData,
72
+          props.autoRefreshTime
73
+        );
74
+      }
75
+      return () => {
76
+        if (refreshInterval.current) {
77
+          clearInterval(refreshInterval.current);
78
+        }
79
+      };
80
+      // eslint-disable-next-line react-hooks/exhaustive-deps
81
+    }, [props.cache, props.refreshOnFocus])
82
+  );
83
+
84
+  // useEffect(() => {
85
+  //   if (status === REQUEST_STATUS.BAD_TOKEN && props.onMajorError) {
86
+  //     props.onMajorError(status, code);
87
+  //   }
88
+  //   // eslint-disable-next-line react-hooks/exhaustive-deps
89
+  // }, [status, code]);
90
+
91
+  // if (status === REQUEST_STATUS.BAD_TOKEN && props.onMajorError) {
92
+  //   return <View />;
93
+  // } else
94
+  if (data === undefined && loading && props.showLoading !== false) {
95
+    return <BasicLoadingScreen />;
96
+  } else if (
97
+    data === undefined &&
98
+    status !== REQUEST_STATUS.SUCCESS &&
99
+    props.showError !== false
100
+  ) {
101
+    return (
102
+      <ErrorView
103
+        status={status}
104
+        loading={loading}
105
+        button={{
106
+          icon: 'refresh',
107
+          text: i18n.t('general.retry'),
108
+          onPress: () => refreshData(),
109
+        }}
110
+      />
111
+    );
112
+  } else {
113
+    return props.render(data, loading, refreshData, status, code);
114
+  }
115
+}

+ 120
- 221
src/components/Screens/WebSectionList.tsx View File

@@ -17,25 +17,26 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
20
+import React, { useState } from 'react';
21 21
 import i18n from 'i18n-js';
22 22
 import { Snackbar } from 'react-native-paper';
23 23
 import {
24
+  NativeScrollEvent,
24 25
   NativeSyntheticEvent,
25 26
   RefreshControl,
26 27
   SectionListData,
28
+  SectionListRenderItemInfo,
27 29
   StyleSheet,
28 30
   View,
29 31
 } from 'react-native';
30 32
 import * as Animatable from 'react-native-animatable';
31
-import { Collapsible } from 'react-navigation-collapsible';
32
-import { StackNavigationProp } from '@react-navigation/stack';
33 33
 import ErrorView from './ErrorView';
34 34
 import BasicLoadingScreen from './BasicLoadingScreen';
35 35
 import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
36
-import { ERROR_TYPE, readData } from '../../utils/WebData';
36
+import { ERROR_TYPE } from '../../utils/WebData';
37 37
 import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
38 38
 import GENERAL_STYLES from '../../constants/Styles';
39
+import RequestScreen from './RequestScreen';
39 40
 
40 41
 export type SectionListDataType<ItemT> = Array<{
41 42
   title: string;
@@ -44,39 +45,30 @@ export type SectionListDataType<ItemT> = Array<{
44 45
   keyExtractor?: (data: ItemT) => string;
45 46
 }>;
46 47
 
47
-type PropsType<ItemT, RawData> = {
48
-  navigation: StackNavigationProp<any>;
49
-  fetchUrl: string;
50
-  autoRefreshTime: number;
48
+type Props<ItemT, RawData> = {
49
+  request: () => Promise<RawData>;
51 50
   refreshOnFocus: boolean;
52
-  renderItem: (data: { item: ItemT }) => React.ReactNode;
51
+  renderItem: (data: SectionListRenderItemInfo<ItemT>) => React.ReactNode;
53 52
   createDataset: (
54
-    data: RawData | null,
55
-    isLoading?: boolean
53
+    data: RawData | undefined,
54
+    isLoading: boolean
56 55
   ) => SectionListDataType<ItemT>;
57 56
 
58
-  onScroll?: (event: NativeSyntheticEvent<EventTarget>) => void;
57
+  onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
59 58
   showError?: boolean;
60 59
   itemHeight?: number | null;
61
-  updateData?: number;
60
+  autoRefreshTime?: number;
61
+  updateData?: number | string;
62 62
   renderListHeaderComponent?: (
63
-    data: RawData | null
63
+    data?: RawData
64 64
   ) => React.ComponentType<any> | React.ReactElement | null;
65 65
   renderSectionHeader?: (
66 66
     data: { section: SectionListData<ItemT> },
67
-    isLoading?: boolean
67
+    isLoading: boolean
68 68
   ) => React.ReactElement | null;
69 69
   stickyHeader?: boolean;
70 70
 };
71 71
 
72
-type StateType<RawData> = {
73
-  refreshing: boolean;
74
-  fetchedData: RawData | null;
75
-  snackbarVisible: boolean;
76
-};
77
-
78
-const MIN_REFRESH_TIME = 5 * 1000;
79
-
80 72
 const styles = StyleSheet.create({
81 73
   container: {
82 74
     minHeight: '100%',
@@ -85,131 +77,18 @@ const styles = StyleSheet.create({
85 77
 
86 78
 /**
87 79
  * Component used to render a SectionList with data fetched from the web
88
- *
89
- * This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
90 80
  * To force the component to update, change the value of updateData.
91 81
  */
92
-class WebSectionList<ItemT, RawData> extends React.PureComponent<
93
-  PropsType<ItemT, RawData>,
94
-  StateType<RawData>
95
-> {
96
-  static defaultProps = {
97
-    showError: true,
98
-    itemHeight: null,
99
-    updateData: 0,
100
-    renderListHeaderComponent: () => null,
101
-    renderSectionHeader: () => null,
102
-    stickyHeader: false,
103
-  };
104
-
105
-  refreshInterval: NodeJS.Timeout | undefined;
106
-
107
-  lastRefresh: Date | undefined;
108
-
109
-  constructor(props: PropsType<ItemT, RawData>) {
110
-    super(props);
111
-    this.state = {
112
-      refreshing: false,
113
-      fetchedData: null,
114
-      snackbarVisible: false,
115
-    };
116
-  }
82
+function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
83
+  const [snackbarVisible, setSnackbarVisible] = useState(false);
117 84
 
118
-  /**
119
-   * Registers react navigation events on first screen load.
120
-   * Allows to detect when the screen is focused
121
-   */
122
-  componentDidMount() {
123
-    const { navigation } = this.props;
124
-    navigation.addListener('focus', this.onScreenFocus);
125
-    navigation.addListener('blur', this.onScreenBlur);
126
-    this.lastRefresh = undefined;
127
-    this.onRefresh();
128
-  }
85
+  const showSnackBar = () => setSnackbarVisible(true);
129 86
 
130
-  /**
131
-   * Refreshes data when focusing the screen and setup a refresh interval if asked to
132
-   */
133
-  onScreenFocus = () => {
134
-    const { props } = this;
135
-    if (props.refreshOnFocus && this.lastRefresh) {
136
-      setTimeout(this.onRefresh, 200);
137
-    }
138
-    if (props.autoRefreshTime > 0) {
139
-      this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
140
-    }
141
-  };
142
-
143
-  /**
144
-   * Removes any interval on un-focus
145
-   */
146
-  onScreenBlur = () => {
147
-    if (this.refreshInterval) {
148
-      clearInterval(this.refreshInterval);
149
-    }
150
-  };
151
-
152
-  /**
153
-   * Callback used when fetch is successful.
154
-   * It will update the displayed data and stop the refresh animation
155
-   *
156
-   * @param fetchedData The newly fetched data
157
-   */
158
-  onFetchSuccess = (fetchedData: RawData) => {
159
-    this.setState({
160
-      fetchedData,
161
-      refreshing: false,
162
-    });
163
-    this.lastRefresh = new Date();
164
-  };
87
+  const hideSnackBar = () => setSnackbarVisible(false);
165 88
 
166
-  /**
167
-   * Callback used when fetch encountered an error.
168
-   * It will reset the displayed data and show an error.
169
-   */
170
-  onFetchError = () => {
171
-    this.setState({
172
-      fetchedData: null,
173
-      refreshing: false,
174
-    });
175
-    this.showSnackBar();
176
-  };
177
-
178
-  /**
179
-   * Refreshes data and shows an animations while doing it
180
-   */
181
-  onRefresh = () => {
182
-    const { fetchUrl } = this.props;
183
-    let canRefresh;
184
-    if (this.lastRefresh != null) {
185
-      const last = this.lastRefresh;
186
-      canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
187
-    } else {
188
-      canRefresh = true;
189
-    }
190
-    if (canRefresh) {
191
-      this.setState({ refreshing: true });
192
-      readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
193
-    }
194
-  };
195
-
196
-  /**
197
-   * Shows the error popup
198
-   */
199
-  showSnackBar = () => {
200
-    this.setState({ snackbarVisible: true });
201
-  };
202
-
203
-  /**
204
-   * Hides the error popup
205
-   */
206
-  hideSnackBar = () => {
207
-    this.setState({ snackbarVisible: false });
208
-  };
209
-
210
-  getItemLayout = (
89
+  const getItemLayout = (
211 90
     height: number,
212
-    data: Array<SectionListData<ItemT>> | null,
91
+    _data: Array<SectionListData<ItemT>> | null,
213 92
     index: number
214 93
   ): { length: number; offset: number; index: number } => {
215 94
     return {
@@ -219,105 +98,125 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
219 98
     };
220 99
   };
221 100
 
222
-  getRenderSectionHeader = (data: { section: SectionListData<ItemT> }) => {
223
-    const { renderSectionHeader } = this.props;
224
-    const { refreshing } = this.state;
225
-    if (renderSectionHeader != null) {
101
+  const getRenderSectionHeader = (
102
+    data: { section: SectionListData<ItemT> },
103
+    loading: boolean
104
+  ) => {
105
+    const { renderSectionHeader } = props;
106
+    if (renderSectionHeader) {
226 107
       return (
227
-        <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
228
-          {renderSectionHeader(data, refreshing)}
108
+        <Animatable.View
109
+          animation={'fadeInUp'}
110
+          duration={500}
111
+          useNativeDriver={true}
112
+        >
113
+          {renderSectionHeader(data, loading)}
229 114
         </Animatable.View>
230 115
       );
231 116
     }
232 117
     return null;
233 118
   };
234 119
 
235
-  getRenderItem = (data: { item: ItemT }) => {
236
-    const { renderItem } = this.props;
120
+  const getRenderItem = (data: SectionListRenderItemInfo<ItemT>) => {
121
+    const { renderItem } = props;
237 122
     return (
238
-      <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
123
+      <Animatable.View
124
+        animation={'fadeInUp'}
125
+        duration={500}
126
+        useNativeDriver={true}
127
+      >
239 128
         {renderItem(data)}
240 129
       </Animatable.View>
241 130
     );
242 131
   };
243 132
 
244
-  onScroll = (event: NativeSyntheticEvent<EventTarget>) => {
245
-    const { onScroll } = this.props;
246
-    if (onScroll != null) {
247
-      onScroll(event);
133
+  const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
134
+    if (props.onScroll) {
135
+      props.onScroll(event);
248 136
     }
249 137
   };
250 138
 
251
-  render() {
252
-    const { props, state } = this;
139
+  const render = (
140
+    data: RawData | undefined,
141
+    loading: boolean,
142
+    refreshData: (newRequest?: () => Promise<RawData>) => void
143
+  ) => {
253 144
     const { itemHeight } = props;
254
-    let dataset: SectionListDataType<ItemT> = [];
255
-    if (
256
-      state.fetchedData != null ||
257
-      (state.fetchedData == null && !props.showError)
258
-    ) {
259
-      dataset = props.createDataset(state.fetchedData, state.refreshing);
145
+    const dataset = props.createDataset(data, loading);
146
+    if (!data && !loading) {
147
+      showSnackBar();
260 148
     }
261
-
262 149
     return (
263
-      <View style={GENERAL_STYLES.flex}>
264
-        <CollapsibleSectionList
265
-          sections={dataset}
266
-          extraData={props.updateData}
267
-          paddedProps={(paddingTop) => ({
268
-            refreshControl: (
269
-              <RefreshControl
270
-                progressViewOffset={paddingTop}
271
-                refreshing={state.refreshing}
272
-                onRefresh={this.onRefresh}
273
-              />
274
-            ),
275
-          })}
276
-          renderSectionHeader={this.getRenderSectionHeader}
277
-          renderItem={this.getRenderItem}
278
-          stickySectionHeadersEnabled={props.stickyHeader}
279
-          style={styles.container}
280
-          ListHeaderComponent={
281
-            props.renderListHeaderComponent != null
282
-              ? props.renderListHeaderComponent(state.fetchedData)
283
-              : null
284
-          }
285
-          ListEmptyComponent={
286
-            state.refreshing ? (
287
-              <BasicLoadingScreen />
288
-            ) : (
289
-              <ErrorView
290
-                navigation={props.navigation}
291
-                errorCode={ERROR_TYPE.CONNECTION_ERROR}
292
-                onRefresh={this.onRefresh}
293
-              />
294
-            )
295
-          }
296
-          getItemLayout={
297
-            itemHeight
298
-              ? (data, index) => this.getItemLayout(itemHeight, data, index)
299
-              : undefined
300
-          }
301
-          onScroll={this.onScroll}
302
-          hasTab={true}
303
-        />
304
-        <Snackbar
305
-          visible={state.snackbarVisible}
306
-          onDismiss={this.hideSnackBar}
307
-          action={{
308
-            label: 'OK',
309
-            onPress: () => {},
310
-          }}
311
-          duration={4000}
312
-          style={{
313
-            bottom: TAB_BAR_HEIGHT,
314
-          }}
315
-        >
316
-          {i18n.t('general.listUpdateFail')}
317
-        </Snackbar>
318
-      </View>
150
+      <CollapsibleSectionList
151
+        sections={dataset}
152
+        extraData={props.updateData}
153
+        paddedProps={(paddingTop) => ({
154
+          refreshControl: (
155
+            <RefreshControl
156
+              progressViewOffset={paddingTop}
157
+              refreshing={loading}
158
+              onRefresh={refreshData}
159
+            />
160
+          ),
161
+        })}
162
+        renderSectionHeader={(info) => getRenderSectionHeader(info, loading)}
163
+        renderItem={getRenderItem}
164
+        stickySectionHeadersEnabled={props.stickyHeader}
165
+        style={styles.container}
166
+        ListHeaderComponent={
167
+          props.renderListHeaderComponent != null
168
+            ? props.renderListHeaderComponent(data)
169
+            : null
170
+        }
171
+        ListEmptyComponent={
172
+          loading ? (
173
+            <BasicLoadingScreen />
174
+          ) : (
175
+            <ErrorView
176
+              status={ERROR_TYPE.CONNECTION_ERROR}
177
+              button={{
178
+                icon: 'refresh',
179
+                text: i18n.t('general.retry'),
180
+                onPress: refreshData,
181
+              }}
182
+            />
183
+          )
184
+        }
185
+        getItemLayout={
186
+          itemHeight ? (d, i) => getItemLayout(itemHeight, d, i) : undefined
187
+        }
188
+        onScroll={onScroll}
189
+        hasTab={true}
190
+      />
319 191
     );
320
-  }
192
+  };
193
+
194
+  return (
195
+    <View style={GENERAL_STYLES.flex}>
196
+      <RequestScreen<RawData>
197
+        request={props.request}
198
+        render={render}
199
+        showError={false}
200
+        showLoading={false}
201
+        autoRefreshTime={props.autoRefreshTime}
202
+        refreshOnFocus={props.refreshOnFocus}
203
+      />
204
+      <Snackbar
205
+        visible={snackbarVisible}
206
+        onDismiss={hideSnackBar}
207
+        action={{
208
+          label: 'OK',
209
+          onPress: hideSnackBar,
210
+        }}
211
+        duration={4000}
212
+        style={{
213
+          bottom: TAB_BAR_HEIGHT,
214
+        }}
215
+      >
216
+        {i18n.t('general.listUpdateFail')}
217
+      </Snackbar>
218
+    </View>
219
+  );
321 220
 }
322 221
 
323 222
 export default WebSectionList;

+ 6
- 2
src/components/Screens/WebViewScreen.tsx View File

@@ -251,8 +251,12 @@ function WebViewScreen(props: Props) {
251 251
       renderLoading={getRenderLoading}
252 252
       renderError={() => (
253 253
         <ErrorView
254
-          errorCode={ERROR_TYPE.CONNECTION_ERROR}
255
-          onRefresh={onRefreshClicked}
254
+          status={ERROR_TYPE.CONNECTION_ERROR}
255
+          button={{
256
+            icon: 'refresh',
257
+            text: i18n.t('general.retry'),
258
+            onPress: onRefreshClicked,
259
+          }}
256 260
         />
257 261
       )}
258 262
       onNavigationStateChange={setNavState}

+ 14
- 2
src/constants/Urls.tsx View File

@@ -21,11 +21,14 @@ const STUDENT_SERVER = 'https://etud.insa-toulouse.fr/';
21 21
 const AMICALE_SERVER = 'https://www.amicale-insat.fr/';
22 22
 const GIT_SERVER =
23 23
   'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/';
24
+const PLANEX_SERVER = 'http://planex.insa-toulouse.fr/';
24 25
 
25 26
 const AMICALE_ENDPOINT = AMICALE_SERVER + 'api/';
26 27
 
27 28
 const APP_ENDPOINT = STUDENT_SERVER + '~amicale_app/v2/';
28
-const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/data/stock-v2.json';
29
+const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/v2/api/';
30
+const PROXIMO_IMAGES_ENDPOINT =
31
+  STUDENT_SERVER + '~proximo/v2/api-proximo/public/storage/app/';
29 32
 const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/';
30 33
 
31 34
 export default {
@@ -39,7 +42,16 @@ export default {
39 42
     dashboard: APP_ENDPOINT + 'dashboard/dashboard_data.json',
40 43
     menu: APP_ENDPOINT + 'menu/menu_data.json',
41 44
   },
42
-  proximo: PROXIMO_ENDPOINT,
45
+  proximo: {
46
+    articles: PROXIMO_ENDPOINT + 'articles',
47
+    categories: PROXIMO_ENDPOINT + 'categories',
48
+    images: PROXIMO_IMAGES_ENDPOINT + 'img/',
49
+    icons: PROXIMO_IMAGES_ENDPOINT + 'icon/',
50
+  },
51
+  planex: {
52
+    planning: PLANEX_SERVER,
53
+    groups: PLANEX_SERVER + 'wsAdeGrp.php?projectId=1',
54
+  },
43 55
   images: {
44 56
     proxiwash: APP_IMAGES_ENDPOINT + 'Proxiwash.png',
45 57
     washer: APP_IMAGES_ENDPOINT + 'ProxiwashLaveLinge.png',

+ 4
- 0
src/navigation/MainNavigator.tsx View File

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

+ 10
- 15
src/screens/Home/HomeScreen.tsx View File

@@ -22,6 +22,7 @@ import {
22 22
   FlatList,
23 23
   NativeScrollEvent,
24 24
   NativeSyntheticEvent,
25
+  SectionListData,
25 26
   StyleSheet,
26 27
 } from 'react-native';
27 28
 import i18n from 'i18n-js';
@@ -52,6 +53,7 @@ import { getDisplayEvent, getFutureEvents } from '../../utils/Home';
52 53
 import type { PlanningEventType } from '../../utils/Planning';
53 54
 import GENERAL_STYLES from '../../constants/Styles';
54 55
 import Urls from '../../constants/Urls';
56
+import { readData } from '../../utils/WebData';
55 57
 
56 58
 const FEED_ITEM_HEIGHT = 500;
57 59
 
@@ -314,12 +316,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
314 316
   getRenderItem = ({ item }: { item: FeedItemType }) => this.getFeedItem(item);
315 317
 
316 318
   getRenderSectionHeader = (
317
-    data: {
318
-      section: {
319
-        data: Array<object>;
320
-        title: string;
321
-      };
322
-    },
319
+    data: { section: SectionListData<FeedItemType> },
323 320
     isLoading: boolean
324 321
   ) => {
325 322
     const { props } = this;
@@ -352,7 +349,7 @@ class HomeScreen extends React.Component<PropsType, StateType> {
352 349
     );
353 350
   };
354 351
 
355
-  getListHeader = (fetchedData: RawDashboardType) => {
352
+  getListHeader = (fetchedData: RawDashboardType | undefined) => {
356 353
     let dashboard = null;
357 354
     if (fetchedData != null) {
358 355
       dashboard = fetchedData.dashboard;
@@ -404,21 +401,20 @@ class HomeScreen extends React.Component<PropsType, StateType> {
404 401
    * @return {*}
405 402
    */
406 403
   createDataset = (
407
-    fetchedData: RawDashboardType | null,
404
+    fetchedData: RawDashboardType | undefined,
408 405
     isLoading: boolean
409 406
   ): Array<{
410 407
     title: string;
411 408
     data: [] | Array<FeedItemType>;
412 409
     id: string;
413 410
   }> => {
414
-    // fetchedData = DATA;
415
-    if (fetchedData != null) {
416
-      if (fetchedData.news_feed != null) {
411
+    if (fetchedData) {
412
+      if (fetchedData.news_feed) {
417 413
         this.currentNewFeed = HomeScreen.generateNewsFeed(
418 414
           fetchedData.news_feed
419 415
         );
420 416
       }
421
-      if (fetchedData.dashboard != null) {
417
+      if (fetchedData.dashboard) {
422 418
         this.currentDashboard = fetchedData.dashboard;
423 419
       }
424 420
     }
@@ -470,11 +466,10 @@ class HomeScreen extends React.Component<PropsType, StateType> {
470 466
       <View style={GENERAL_STYLES.flex}>
471 467
         <View style={styles.content}>
472 468
           <WebSectionList
473
-            navigation={props.navigation}
469
+            request={() => readData<RawDashboardType>(Urls.app.dashboard)}
474 470
             createDataset={this.createDataset}
475 471
             autoRefreshTime={REFRESH_TIME}
476
-            refreshOnFocus
477
-            fetchUrl={Urls.app.dashboard}
472
+            refreshOnFocus={true}
478 473
             renderItem={this.getRenderItem}
479 474
             itemHeight={FEED_ITEM_HEIGHT}
480 475
             onScroll={this.onScroll}

+ 36
- 22
src/screens/Planex/GroupSelectionScreen.tsx View File

@@ -26,6 +26,8 @@ import { stringMatchQuery } from '../../utils/Search';
26 26
 import WebSectionList from '../../components/Screens/WebSectionList';
27 27
 import GroupListAccordion from '../../components/Lists/PlanexGroups/GroupListAccordion';
28 28
 import AsyncStorageManager from '../../managers/AsyncStorageManager';
29
+import Urls from '../../constants/Urls';
30
+import { readData } from '../../utils/WebData';
29 31
 
30 32
 export type PlanexGroupType = {
31 33
   name: string;
@@ -60,8 +62,6 @@ function sortName(
60 62
   return 0;
61 63
 }
62 64
 
63
-const GROUPS_URL = 'http://planex.insa-toulouse.fr/wsAdeGrp.php?projectId=1';
64
-
65 65
 /**
66 66
  * Class defining planex group selection screen.
67 67
  */
@@ -137,9 +137,13 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
137 137
    * @param fetchedData
138 138
    * @return {*}
139 139
    * */
140
-  createDataset = (fetchedData: {
141
-    [key: string]: PlanexGroupCategoryType;
142
-  }): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => {
140
+  createDataset = (
141
+    fetchedData:
142
+      | {
143
+          [key: string]: PlanexGroupCategoryType;
144
+        }
145
+      | undefined
146
+  ): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => {
143 147
     return [
144 148
       {
145 149
         title: '',
@@ -236,20 +240,28 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
236 240
    * @param fetchedData The raw data fetched from the server
237 241
    * @returns {[]}
238 242
    */
239
-  generateData(fetchedData: {
240
-    [key: string]: PlanexGroupCategoryType;
241
-  }): Array<PlanexGroupCategoryType> {
243
+  generateData(
244
+    fetchedData:
245
+      | {
246
+          [key: string]: PlanexGroupCategoryType;
247
+        }
248
+      | undefined
249
+  ): Array<PlanexGroupCategoryType> {
242 250
     const { favoriteGroups } = this.state;
243 251
     const data: Array<PlanexGroupCategoryType> = [];
244
-    Object.values(fetchedData).forEach((category: PlanexGroupCategoryType) => {
245
-      data.push(category);
246
-    });
247
-    data.sort(sortName);
248
-    data.unshift({
249
-      name: i18n.t('screens.planex.favorites'),
250
-      id: 0,
251
-      content: favoriteGroups,
252
-    });
252
+    if (fetchedData) {
253
+      Object.values(fetchedData).forEach(
254
+        (category: PlanexGroupCategoryType) => {
255
+          data.push(category);
256
+        }
257
+      );
258
+      data.sort(sortName);
259
+      data.unshift({
260
+        name: i18n.t('screens.planex.favorites'),
261
+        id: 0,
262
+        content: favoriteGroups,
263
+      });
264
+    }
253 265
     return data;
254 266
   }
255 267
 
@@ -298,14 +310,16 @@ class GroupSelectionScreen extends React.Component<PropsType, StateType> {
298 310
   }
299 311
 
300 312
   render() {
301
-    const { props, state } = this;
313
+    const { state } = this;
302 314
     return (
303 315
       <WebSectionList
304
-        navigation={props.navigation}
316
+        request={() =>
317
+          readData<{ [key: string]: PlanexGroupCategoryType }>(
318
+            Urls.planex.groups
319
+          )
320
+        }
305 321
         createDataset={this.createDataset}
306
-        autoRefreshTime={0}
307
-        refreshOnFocus={false}
308
-        fetchUrl={GROUPS_URL}
322
+        refreshOnFocus={true}
309 323
         renderItem={this.getRenderItem}
310 324
         updateData={state.currentSearchString + state.favoriteGroups.length}
311 325
       />

+ 4
- 8
src/screens/Planex/PlanexScreen.tsx View File

@@ -42,6 +42,7 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
42 42
 import MascotPopup from '../../components/Mascot/MascotPopup';
43 43
 import { getPrettierPlanexGroupName } from '../../utils/Utils';
44 44
 import GENERAL_STYLES from '../../constants/Styles';
45
+import Urls from '../../constants/Urls';
45 46
 
46 47
 type PropsType = {
47 48
   navigation: StackNavigationProp<any>;
@@ -57,8 +58,6 @@ type StateType = {
57 58
   injectJS: string;
58 59
 };
59 60
 
60
-const PLANEX_URL = 'http://planex.insa-toulouse.fr/';
61
-
62 61
 // // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing
63 62
 // // Remove alpha from given Jquery node
64 63
 // function removeAlpha(node) {
@@ -197,22 +196,19 @@ class PlanexScreen extends React.Component<PropsType, StateType> {
197 196
    * @returns {*}
198 197
    */
199 198
   getWebView() {
200
-    const { props, state } = this;
199
+    const { state } = this;
201 200
     const showWebview = state.currentGroup.id !== -1;
202
-    console.log(state.injectJS);
203 201
 
204 202
     return (
205 203
       <View style={GENERAL_STYLES.flex}>
206 204
         {!showWebview ? (
207 205
           <ErrorView
208
-            navigation={props.navigation}
209
-            icon="account-clock"
206
+            icon={'account-clock'}
210 207
             message={i18n.t('screens.planex.noGroupSelected')}
211
-            showRetryButton={false}
212 208
           />
213 209
         ) : null}
214 210
         <WebViewScreen
215
-          url={PLANEX_URL}
211
+          url={Urls.planex.planning}
216 212
           initialJS={this.generateInjectedJS(this.state.currentGroup.id)}
217 213
           injectJS={this.state.injectJS}
218 214
           onMessage={this.onMessage}

+ 7
- 9
src/screens/Planning/PlanningDisplayScreen.tsx View File

@@ -28,9 +28,7 @@ import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
28 28
 import { apiRequest, ERROR_TYPE } from '../../utils/WebData';
29 29
 import ErrorView from '../../components/Screens/ErrorView';
30 30
 import CustomHTML from '../../components/Overrides/CustomHTML';
31
-import CustomTabBar, {
32
-  TAB_BAR_HEIGHT,
33
-} from '../../components/Tabbar/CustomTabBar';
31
+import { TAB_BAR_HEIGHT } from '../../components/Tabbar/CustomTabBar';
34 32
 import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
35 33
 import type { PlanningEventType } from '../../utils/Planning';
36 34
 import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
@@ -163,12 +161,9 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
163 161
    * @returns {*}
164 162
    */
165 163
   getErrorView() {
166
-    const { navigation } = this.props;
167 164
     if (this.errorCode === ERROR_TYPE.BAD_INPUT) {
168 165
       return (
169 166
         <ErrorView
170
-          navigation={navigation}
171
-          showRetryButton={false}
172 167
           message={i18n.t('screens.planning.invalidEvent')}
173 168
           icon="calendar-remove"
174 169
         />
@@ -176,9 +171,12 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
176 171
     }
177 172
     return (
178 173
       <ErrorView
179
-        navigation={navigation}
180
-        errorCode={this.errorCode}
181
-        onRefresh={this.fetchData}
174
+        status={this.errorCode}
175
+        button={{
176
+          icon: 'refresh',
177
+          text: i18n.t('general.retry'),
178
+          onPress: this.fetchData,
179
+        }}
182 180
       />
183 181
     );
184 182
   }

+ 56
- 45
src/screens/Proxiwash/ProxiwashScreen.tsx View File

@@ -18,7 +18,13 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import { Alert, StyleSheet, View } from 'react-native';
21
+import {
22
+  Alert,
23
+  SectionListData,
24
+  SectionListRenderItemInfo,
25
+  StyleSheet,
26
+  View,
27
+} from 'react-native';
22 28
 import i18n from 'i18n-js';
23 29
 import { Avatar, Button, Card, Text, withTheme } from 'react-native-paper';
24 30
 import { StackNavigationProp } from '@react-navigation/stack';
@@ -46,6 +52,7 @@ import MascotPopup from '../../components/Mascot/MascotPopup';
46 52
 import type { SectionListDataType } from '../../components/Screens/WebSectionList';
47 53
 import type { LaundromatType } from './ProxiwashAboutScreen';
48 54
 import GENERAL_STYLES from '../../constants/Styles';
55
+import { readData } from '../../utils/WebData';
49 56
 
50 57
 const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
51 58
 const LIST_ITEM_HEIGHT = 64;
@@ -72,6 +79,11 @@ type StateType = {
72 79
   selectedWash: string;
73 80
 };
74 81
 
82
+type FetchedDataType = {
83
+  dryers: Array<ProxiwashMachineType>;
84
+  washers: Array<ProxiwashMachineType>;
85
+};
86
+
75 87
 const styles = StyleSheet.create({
76 88
   modalContainer: {
77 89
     flex: 1,
@@ -277,7 +289,11 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
277 289
    * @param section The section to render
278 290
    * @return {*}
279 291
    */
280
-  getRenderSectionHeader = ({ section }: { section: { title: string } }) => {
292
+  getRenderSectionHeader = ({
293
+    section,
294
+  }: {
295
+    section: SectionListData<ProxiwashMachineType>;
296
+  }) => {
281 297
     const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
282 298
     const nbAvailable = this.getMachineAvailableNumber(isDryer);
283 299
     return (
@@ -296,20 +312,14 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
296 312
    * @param section The object describing the current SectionList section
297 313
    * @returns {React.Node}
298 314
    */
299
-  getRenderItem = ({
300
-    item,
301
-    section,
302
-  }: {
303
-    item: ProxiwashMachineType;
304
-    section: { title: string };
305
-  }) => {
315
+  getRenderItem = (data: SectionListRenderItemInfo<ProxiwashMachineType>) => {
306 316
     const { machinesWatched } = this.state;
307
-    const isDryer = section.title === i18n.t('screens.proxiwash.dryers');
317
+    const isDryer = data.section.title === i18n.t('screens.proxiwash.dryers');
308 318
     return (
309 319
       <ProxiwashListItem
310
-        item={item}
320
+        item={data.item}
311 321
         onPress={this.showModal}
312
-        isWatched={isMachineWatched(item, machinesWatched)}
322
+        isWatched={isMachineWatched(data.item, machinesWatched)}
313 323
         isDryer={isDryer}
314 324
         height={LIST_ITEM_HEIGHT}
315 325
       />
@@ -382,37 +392,40 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
382 392
    * @param fetchedData
383 393
    * @return {*}
384 394
    */
385
-  createDataset = (fetchedData: {
386
-    dryers: Array<ProxiwashMachineType>;
387
-    washers: Array<ProxiwashMachineType>;
388
-  }): SectionListDataType<ProxiwashMachineType> => {
395
+  createDataset = (
396
+    fetchedData: FetchedDataType | undefined
397
+  ): SectionListDataType<ProxiwashMachineType> => {
389 398
     const { state } = this;
390
-    let data = fetchedData;
391
-    if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
392
-      data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
393
-      AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
394
-      AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
399
+    if (fetchedData) {
400
+      let data = fetchedData;
401
+      if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
402
+        data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
403
+        AprilFoolsManager.getNewProxiwashDryerOrderedList(data.dryers);
404
+        AprilFoolsManager.getNewProxiwashWasherOrderedList(data.washers);
405
+      }
406
+      this.fetchedData = data;
407
+      // TODO dirty, should be refactored
408
+      this.state.machinesWatched = getCleanedMachineWatched(
409
+        state.machinesWatched,
410
+        [...data.dryers, ...data.washers]
411
+      );
412
+      return [
413
+        {
414
+          title: i18n.t('screens.proxiwash.dryers'),
415
+          icon: 'tumble-dryer',
416
+          data: data.dryers === undefined ? [] : data.dryers,
417
+          keyExtractor: this.getKeyExtractor,
418
+        },
419
+        {
420
+          title: i18n.t('screens.proxiwash.washers'),
421
+          icon: 'washing-machine',
422
+          data: data.washers === undefined ? [] : data.washers,
423
+          keyExtractor: this.getKeyExtractor,
424
+        },
425
+      ];
426
+    } else {
427
+      return [];
395 428
     }
396
-    this.fetchedData = data;
397
-    // TODO dirty, should be refactored
398
-    this.state.machinesWatched = getCleanedMachineWatched(
399
-      state.machinesWatched,
400
-      [...data.dryers, ...data.washers]
401
-    );
402
-    return [
403
-      {
404
-        title: i18n.t('screens.proxiwash.dryers'),
405
-        icon: 'tumble-dryer',
406
-        data: data.dryers === undefined ? [] : data.dryers,
407
-        keyExtractor: this.getKeyExtractor,
408
-      },
409
-      {
410
-        title: i18n.t('screens.proxiwash.washers'),
411
-        icon: 'washing-machine',
412
-        data: data.washers === undefined ? [] : data.washers,
413
-        keyExtractor: this.getKeyExtractor,
414
-      },
415
-    ];
416 429
   };
417 430
 
418 431
   /**
@@ -481,7 +494,6 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
481 494
 
482 495
   render() {
483 496
     const { state } = this;
484
-    const { navigation } = this.props;
485 497
     let data: LaundromatType;
486 498
     switch (state.selectedWash) {
487 499
       case 'tripodeB':
@@ -494,13 +506,12 @@ class ProxiwashScreen extends React.Component<PropsType, StateType> {
494 506
       <View style={GENERAL_STYLES.flex}>
495 507
         <View style={styles.container}>
496 508
           <WebSectionList
509
+            request={() => readData<FetchedDataType>(data.url)}
497 510
             createDataset={this.createDataset}
498
-            navigation={navigation}
499
-            fetchUrl={data.url}
500 511
             renderItem={this.getRenderItem}
501 512
             renderSectionHeader={this.getRenderSectionHeader}
502 513
             autoRefreshTime={REFRESH_TIME}
503
-            refreshOnFocus
514
+            refreshOnFocus={true}
504 515
             updateData={state.machinesWatched.length}
505 516
           />
506 517
         </View>

+ 114
- 158
src/screens/Services/Proximo/ProximoListScreen.tsx View File

@@ -17,7 +17,7 @@
17 17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18 18
  */
19 19
 
20
-import * as React from 'react';
20
+import React, { useLayoutEffect, useRef, useState } from 'react';
21 21
 import { Image, Platform, ScrollView, StyleSheet, View } from 'react-native';
22 22
 import i18n from 'i18n-js';
23 23
 import {
@@ -26,9 +26,8 @@ import {
26 26
   Subheading,
27 27
   Text,
28 28
   Title,
29
-  withTheme,
29
+  useTheme,
30 30
 } from 'react-native-paper';
31
-import { StackNavigationProp } from '@react-navigation/stack';
32 31
 import { Modalize } from 'react-native-modalize';
33 32
 import CustomModal from '../../../components/Overrides/CustomModal';
34 33
 import { stringMatchQuery } from '../../../utils/Search';
@@ -36,19 +35,29 @@ import ProximoListItem from '../../../components/Lists/Proximo/ProximoListItem';
36 35
 import MaterialHeaderButtons, {
37 36
   Item,
38 37
 } from '../../../components/Overrides/CustomHeaderButton';
39
-import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
40 38
 import type { ProximoArticleType } from './ProximoMainScreen';
41 39
 import GENERAL_STYLES from '../../../constants/Styles';
40
+import { useNavigation } from '@react-navigation/core';
41
+import Urls from '../../../constants/Urls';
42
+import WebSectionList, {
43
+  SectionListDataType,
44
+} from '../../../components/Screens/WebSectionList';
45
+import { readData } from '../../../utils/WebData';
46
+import { StackScreenProps } from '@react-navigation/stack';
47
+import {
48
+  MainRoutes,
49
+  MainStackParamsList,
50
+} from '../../../navigation/MainNavigator';
42 51
 
43 52
 function sortPrice(a: ProximoArticleType, b: ProximoArticleType): number {
44
-  return parseInt(a.price, 10) - parseInt(b.price, 10);
53
+  return a.price - b.price;
45 54
 }
46 55
 
47 56
 function sortPriceReverse(
48 57
   a: ProximoArticleType,
49 58
   b: ProximoArticleType
50 59
 ): number {
51
-  return parseInt(b.price, 10) - parseInt(a.price, 10);
60
+  return b.price - a.price;
52 61
 }
53 62
 
54 63
 function sortName(a: ProximoArticleType, b: ProximoArticleType): number {
@@ -73,23 +82,6 @@ function sortNameReverse(a: ProximoArticleType, b: ProximoArticleType): number {
73 82
 
74 83
 const LIST_ITEM_HEIGHT = 84;
75 84
 
76
-type PropsType = {
77
-  navigation: StackNavigationProp<any>;
78
-  route: {
79
-    params: {
80
-      data: { data: Array<ProximoArticleType> };
81
-      shouldFocusSearchBar: boolean;
82
-    };
83
-  };
84
-  theme: ReactNativePaper.Theme;
85
-};
86
-
87
-type StateType = {
88
-  currentSortMode: number;
89
-  modalCurrentDisplayItem: React.ReactNode;
90
-  currentSearchString: string;
91
-};
92
-
93 85
 const styles = StyleSheet.create({
94 86
   modalContainer: {
95 87
     flex: 1,
@@ -118,113 +110,72 @@ const styles = StyleSheet.create({
118 110
   },
119 111
 });
120 112
 
121
-/**
122
- * Class defining Proximo article list of a certain category.
123
- */
124
-class ProximoListScreen extends React.Component<PropsType, StateType> {
125
-  modalRef: Modalize | null;
113
+type ArticlesType = Array<ProximoArticleType>;
126 114
 
127
-  listData: Array<ProximoArticleType>;
115
+type Props = StackScreenProps<MainStackParamsList, MainRoutes.ProximoList>;
128 116
 
129
-  shouldFocusSearchBar: boolean;
117
+function ProximoListScreen(props: Props) {
118
+  const navigation = useNavigation();
119
+  const theme = useTheme();
120
+  const modalRef = useRef<Modalize>();
130 121
 
131
-  constructor(props: PropsType) {
132
-    super(props);
133
-    this.modalRef = null;
134
-    this.listData = props.route.params.data.data.sort(sortName);
135
-    this.shouldFocusSearchBar = props.route.params.shouldFocusSearchBar;
136
-    this.state = {
137
-      currentSearchString: '',
138
-      currentSortMode: 3,
139
-      modalCurrentDisplayItem: null,
140
-    };
141
-  }
122
+  const [currentSearchString, setCurrentSearchString] = useState('');
123
+  const [currentSortMode, setCurrentSortMode] = useState(2);
124
+  const [modalCurrentDisplayItem, setModalCurrentDisplayItem] = useState<
125
+    React.ReactNode | undefined
126
+  >();
142 127
 
143
-  /**
144
-   * Creates the header content
145
-   */
146
-  componentDidMount() {
147
-    const { navigation } = this.props;
128
+  const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
129
+
130
+  useLayoutEffect(() => {
148 131
     navigation.setOptions({
149
-      headerRight: this.getSortMenuButton,
150
-      headerTitle: this.getSearchBar,
132
+      headerRight: getSortMenuButton,
133
+      headerTitle: getSearchBar,
151 134
       headerBackTitleVisible: false,
152 135
       headerTitleContainerStyle:
153 136
         Platform.OS === 'ios'
154 137
           ? { marginHorizontal: 0, width: '70%' }
155 138
           : { marginHorizontal: 0, right: 50, left: 50 },
156 139
     });
157
-  }
140
+    // eslint-disable-next-line react-hooks/exhaustive-deps
141
+  }, [navigation, currentSortMode]);
158 142
 
159 143
   /**
160 144
    * Callback used when clicking on the sort menu button.
161 145
    * It will open the modal to show a sort selection
162 146
    */
163
-  onSortMenuPress = () => {
164
-    this.setState({
165
-      modalCurrentDisplayItem: this.getModalSortMenu(),
166
-    });
167
-    if (this.modalRef) {
168
-      this.modalRef.open();
147
+  const onSortMenuPress = () => {
148
+    setModalCurrentDisplayItem(getModalSortMenu());
149
+    if (modalRef.current) {
150
+      modalRef.current.open();
169 151
     }
170 152
   };
171 153
 
172 154
   /**
173
-   * Callback used when the search changes
174
-   *
175
-   * @param str The new search string
176
-   */
177
-  onSearchStringChange = (str: string) => {
178
-    this.setState({ currentSearchString: str });
179
-  };
180
-
181
-  /**
182 155
    * Callback used when clicking an article in the list.
183 156
    * It opens the modal to show detailed information about the article
184 157
    *
185 158
    * @param item The article pressed
186 159
    */
187
-  onListItemPress(item: ProximoArticleType) {
188
-    this.setState({
189
-      modalCurrentDisplayItem: this.getModalItemContent(item),
190
-    });
191
-    if (this.modalRef) {
192
-      this.modalRef.open();
160
+  const onListItemPress = (item: ProximoArticleType) => {
161
+    setModalCurrentDisplayItem(getModalItemContent(item));
162
+    if (modalRef.current) {
163
+      modalRef.current.open();
193 164
     }
194
-  }
165
+  };
195 166
 
196 167
   /**
197 168
    * Sets the current sort mode.
198 169
    *
199 170
    * @param mode The number representing the mode
200 171
    */
201
-  setSortMode(mode: string) {
202
-    const { currentSortMode } = this.state;
172
+  const setSortMode = (mode: string) => {
203 173
     const currentMode = parseInt(mode, 10);
204
-    this.setState({
205
-      currentSortMode: currentMode,
206
-    });
207
-    switch (currentMode) {
208
-      case 1:
209
-        this.listData.sort(sortPrice);
210
-        break;
211
-      case 2:
212
-        this.listData.sort(sortPriceReverse);
213
-        break;
214
-      case 3:
215
-        this.listData.sort(sortName);
216
-        break;
217
-      case 4:
218
-        this.listData.sort(sortNameReverse);
219
-        break;
220
-      default:
221
-        this.listData.sort(sortName);
222
-        break;
223
-    }
224
-    if (this.modalRef && currentMode !== currentSortMode) {
225
-      this.modalRef.close();
174
+    setCurrentSortMode(currentMode);
175
+    if (modalRef.current && currentMode !== currentSortMode) {
176
+      modalRef.current.close();
226 177
     }
227
-  }
178
+  };
228 179
 
229 180
   /**
230 181
    * Gets a color depending on the quantity available
@@ -232,8 +183,7 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
232 183
    * @param availableStock The quantity available
233 184
    * @return
234 185
    */
235
-  getStockColor(availableStock: number): string {
236
-    const { theme } = this.props;
186
+  const getStockColor = (availableStock: number): string => {
237 187
     let color: string;
238 188
     if (availableStock > 3) {
239 189
       color = theme.colors.success;
@@ -243,17 +193,17 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
243 193
       color = theme.colors.danger;
244 194
     }
245 195
     return color;
246
-  }
196
+  };
247 197
 
248 198
   /**
249 199
    * Gets the sort menu header button
250 200
    *
251 201
    * @return {*}
252 202
    */
253
-  getSortMenuButton = () => {
203
+  const getSortMenuButton = () => {
254 204
     return (
255 205
       <MaterialHeaderButtons>
256
-        <Item title="main" iconName="sort" onPress={this.onSortMenuPress} />
206
+        <Item title="main" iconName="sort" onPress={onSortMenuPress} />
257 207
       </MaterialHeaderButtons>
258 208
     );
259 209
   };
@@ -263,12 +213,13 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
263 213
    *
264 214
    * @return {*}
265 215
    */
266
-  getSearchBar = () => {
216
+  const getSearchBar = () => {
267 217
     return (
268 218
       // @ts-ignore
269 219
       <Searchbar
270 220
         placeholder={i18n.t('screens.proximo.search')}
271
-        onChangeText={this.onSearchStringChange}
221
+        onChangeText={setCurrentSearchString}
222
+        autoFocus={props.route.params.shouldFocusSearchBar}
272 223
       />
273 224
     );
274 225
   };
@@ -279,14 +230,14 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
279 230
    * @param item The article to display
280 231
    * @return {*}
281 232
    */
282
-  getModalItemContent(item: ProximoArticleType) {
233
+  const getModalItemContent = (item: ProximoArticleType) => {
283 234
     return (
284 235
       <View style={styles.modalContainer}>
285 236
         <Title>{item.name}</Title>
286 237
         <View style={styles.modalTitleContainer}>
287 238
           <Subheading
288 239
             style={{
289
-              color: this.getStockColor(parseInt(item.quantity, 10)),
240
+              color: getStockColor(item.quantity),
290 241
             }}
291 242
           >
292 243
             {`${item.quantity} ${i18n.t('screens.proximo.inStock')}`}
@@ -302,46 +253,43 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
302 253
         </ScrollView>
303 254
       </View>
304 255
     );
305
-  }
256
+  };
306 257
 
307 258
   /**
308 259
    * Gets the modal content to display a sort menu
309 260
    *
310 261
    * @return {*}
311 262
    */
312
-  getModalSortMenu() {
313
-    const { currentSortMode } = this.state;
263
+  const getModalSortMenu = () => {
314 264
     return (
315 265
       <View style={styles.modalContainer}>
316 266
         <Title style={styles.sortTitle}>
317 267
           {i18n.t('screens.proximo.sortOrder')}
318 268
         </Title>
319 269
         <RadioButton.Group
320
-          onValueChange={(value: string) => {
321
-            this.setSortMode(value);
322
-          }}
270
+          onValueChange={setSortMode}
323 271
           value={currentSortMode.toString()}
324 272
         >
325 273
           <RadioButton.Item
326 274
             label={i18n.t('screens.proximo.sortPrice')}
327
-            value={'1'}
275
+            value={'0'}
328 276
           />
329 277
           <RadioButton.Item
330 278
             label={i18n.t('screens.proximo.sortPriceReverse')}
331
-            value={'2'}
279
+            value={'1'}
332 280
           />
333 281
           <RadioButton.Item
334 282
             label={i18n.t('screens.proximo.sortName')}
335
-            value={'3'}
283
+            value={'2'}
336 284
           />
337 285
           <RadioButton.Item
338 286
             label={i18n.t('screens.proximo.sortNameReverse')}
339
-            value={'4'}
287
+            value={'3'}
340 288
           />
341 289
         </RadioButton.Group>
342 290
       </View>
343 291
     );
344
-  }
292
+  };
345 293
 
346 294
   /**
347 295
    * Gets a render item for the given article
@@ -349,13 +297,12 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
349 297
    * @param item The article to render
350 298
    * @return {*}
351 299
    */
352
-  getRenderItem = ({ item }: { item: ProximoArticleType }) => {
353
-    const { currentSearchString } = this.state;
300
+  const getRenderItem = ({ item }: { item: ProximoArticleType }) => {
354 301
     if (stringMatchQuery(item.name, currentSearchString)) {
355 302
       const onPress = () => {
356
-        this.onListItemPress(item);
303
+        onListItemPress(item);
357 304
       };
358
-      const color = this.getStockColor(parseInt(item.quantity, 10));
305
+      const color = getStockColor(item.quantity);
359 306
       return (
360 307
         <ProximoListItem
361 308
           item={item}
@@ -374,46 +321,55 @@ class ProximoListScreen extends React.Component<PropsType, StateType> {
374 321
    * @param item The article to extract the key from
375 322
    * @return {string} The extracted key
376 323
    */
377
-  keyExtractor = (item: ProximoArticleType): string => item.name + item.code;
324
+  const keyExtractor = (item: ProximoArticleType): string =>
325
+    item.name + item.code;
378 326
 
379
-  /**
380
-   * Callback used when receiving the modal ref
381
-   *
382
-   * @param ref
383
-   */
384
-  onModalRef = (ref: Modalize) => {
385
-    this.modalRef = ref;
386
-  };
327
+  const createDataset = (
328
+    data: ArticlesType | undefined
329
+  ): SectionListDataType<ProximoArticleType> => {
330
+    if (data) {
331
+      console.log(data);
332
+      console.log(props.route.params.category);
387 333
 
388
-  itemLayout = (
389
-    data: Array<ProximoArticleType> | null | undefined,
390
-    index: number
391
-  ): { length: number; offset: number; index: number } => ({
392
-    length: LIST_ITEM_HEIGHT,
393
-    offset: LIST_ITEM_HEIGHT * index,
394
-    index,
395
-  });
334
+      return [
335
+        {
336
+          title: '',
337
+          data: data
338
+            .filter(
339
+              (d) =>
340
+                props.route.params.category === -1 ||
341
+                props.route.params.category === d.category_id
342
+            )
343
+            .sort(sortModes[currentSortMode]),
344
+          keyExtractor: keyExtractor,
345
+        },
346
+      ];
347
+    } else {
348
+      return [
349
+        {
350
+          title: '',
351
+          data: [],
352
+          keyExtractor: keyExtractor,
353
+        },
354
+      ];
355
+    }
356
+  };
396 357
 
397
-  render() {
398
-    const { state } = this;
399
-    return (
400
-      <View style={GENERAL_STYLES.flex}>
401
-        <CustomModal onRef={this.onModalRef}>
402
-          {state.modalCurrentDisplayItem}
403
-        </CustomModal>
404
-        <CollapsibleFlatList
405
-          data={this.listData}
406
-          extraData={state.currentSearchString + state.currentSortMode}
407
-          keyExtractor={this.keyExtractor}
408
-          renderItem={this.getRenderItem}
409
-          // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
410
-          removeClippedSubviews
411
-          getItemLayout={this.itemLayout}
412
-          initialNumToRender={10}
413
-        />
414
-      </View>
415
-    );
416
-  }
358
+  return (
359
+    <View style={GENERAL_STYLES.flex}>
360
+      <CustomModal onRef={(ref) => (modalRef.current = ref)}>
361
+        {modalCurrentDisplayItem}
362
+      </CustomModal>
363
+      <WebSectionList
364
+        request={() => readData<ArticlesType>(Urls.proximo.articles)}
365
+        createDataset={createDataset}
366
+        refreshOnFocus={true}
367
+        renderItem={getRenderItem}
368
+        updateData={currentSearchString + currentSortMode}
369
+        itemHeight={LIST_ITEM_HEIGHT}
370
+      />
371
+    </View>
372
+  );
417 373
 }
418 374
 
419
-export default withTheme(ProximoListScreen);
375
+export default ProximoListScreen;

+ 107
- 197
src/screens/Services/Proximo/ProximoMainScreen.tsx View File

@@ -19,8 +19,7 @@
19 19
 
20 20
 import * as React from 'react';
21 21
 import i18n from 'i18n-js';
22
-import { List, withTheme } from 'react-native-paper';
23
-import { StackNavigationProp } from '@react-navigation/stack';
22
+import { Avatar, List, useTheme, withTheme } from 'react-native-paper';
24 23
 import WebSectionList from '../../../components/Screens/WebSectionList';
25 24
 import MaterialHeaderButtons, {
26 25
   Item,
@@ -28,40 +27,35 @@ import MaterialHeaderButtons, {
28 27
 import type { SectionListDataType } from '../../../components/Screens/WebSectionList';
29 28
 import { StyleSheet } from 'react-native';
30 29
 import Urls from '../../../constants/Urls';
30
+import { readData } from '../../../utils/WebData';
31
+import { useNavigation } from '@react-navigation/core';
32
+import { useLayoutEffect } from 'react';
31 33
 
32 34
 const LIST_ITEM_HEIGHT = 84;
33 35
 
34 36
 export type ProximoCategoryType = {
37
+  id: number;
35 38
   name: string;
36 39
   icon: string;
37
-  id: string;
40
+  created_at: string;
41
+  updated_at: string;
38 42
 };
39 43
 
40 44
 export type ProximoArticleType = {
45
+  id: number;
41 46
   name: string;
42 47
   description: string;
43
-  quantity: string;
44
-  price: string;
48
+  quantity: number;
49
+  price: number;
45 50
   code: string;
46
-  id: string;
47
-  type: Array<string>;
48 51
   image: string;
52
+  category_id: number;
53
+  created_at: string;
54
+  updated_at: string;
55
+  category: ProximoCategoryType;
49 56
 };
50 57
 
51
-export type ProximoMainListItemType = {
52
-  type: ProximoCategoryType;
53
-  data: Array<ProximoArticleType>;
54
-};
55
-
56
-export type ProximoDataType = {
57
-  types: Array<ProximoCategoryType>;
58
-  articles: Array<ProximoArticleType>;
59
-};
60
-
61
-type PropsType = {
62
-  navigation: StackNavigationProp<any>;
63
-  theme: ReactNativePaper.Theme;
64
-};
58
+type CategoriesType = Array<ProximoCategoryType>;
65 59
 
66 60
 const styles = StyleSheet.create({
67 61
   item: {
@@ -69,138 +63,69 @@ const styles = StyleSheet.create({
69 63
   },
70 64
 });
71 65
 
72
-/**
73
- * Class defining the main proximo screen.
74
- * This screen shows the different categories of articles offered by proximo.
75
- */
76
-class ProximoMainScreen extends React.Component<PropsType> {
77
-  /**
78
-   * Function used to sort items in the list.
79
-   * Makes the All category sticks to the top and sorts the others by name ascending
80
-   *
81
-   * @param a
82
-   * @param b
83
-   * @return {number}
84
-   */
85
-  static sortFinalData(
86
-    a: ProximoMainListItemType,
87
-    b: ProximoMainListItemType
88
-  ): number {
89
-    const str1 = a.type.name.toLowerCase();
90
-    const str2 = b.type.name.toLowerCase();
91
-
92
-    // Make 'All' category with id -1 stick to the top
93
-    if (a.type.id === '-1') {
94
-      return -1;
95
-    }
96
-    if (b.type.id === '-1') {
97
-      return 1;
98
-    }
66
+function sortFinalData(a: ProximoCategoryType, b: ProximoCategoryType): number {
67
+  const str1 = a.name.toLowerCase();
68
+  const str2 = b.name.toLowerCase();
99 69
 
100
-    // Sort others by name ascending
101
-    if (str1 < str2) {
102
-      return -1;
103
-    }
104
-    if (str1 > str2) {
105
-      return 1;
106
-    }
107
-    return 0;
70
+  // Make 'All' category with id -1 stick to the top
71
+  if (a.id === -1) {
72
+    return -1;
108 73
   }
109
-
110
-  /**
111
-   * Get an array of available articles (in stock) of the given type
112
-   *
113
-   * @param articles The list of all articles
114
-   * @param type The type of articles to find (undefined for any type)
115
-   * @return {Array} The array of available articles
116
-   */
117
-  static getAvailableArticles(
118
-    articles: Array<ProximoArticleType> | null,
119
-    type?: ProximoCategoryType
120
-  ): Array<ProximoArticleType> {
121
-    const availableArticles: Array<ProximoArticleType> = [];
122
-    if (articles != null) {
123
-      articles.forEach((article: ProximoArticleType) => {
124
-        if (
125
-          ((type != null && article.type.includes(type.id)) || type == null) &&
126
-          parseInt(article.quantity, 10) > 0
127
-        ) {
128
-          availableArticles.push(article);
129
-        }
130
-      });
131
-    }
132
-    return availableArticles;
74
+  if (b.id === -1) {
75
+    return 1;
133 76
   }
134 77
 
135
-  articles: Array<ProximoArticleType> | null;
136
-
137
-  constructor(props: PropsType) {
138
-    super(props);
139
-    this.articles = null;
78
+  // Sort others by name ascending
79
+  if (str1 < str2) {
80
+    return -1;
81
+  }
82
+  if (str1 > str2) {
83
+    return 1;
140 84
   }
85
+  return 0;
86
+}
141 87
 
142
-  /**
143
-   * Creates header button
144
-   */
145
-  componentDidMount() {
146
-    const { navigation } = this.props;
88
+/**
89
+ * Class defining the main proximo screen.
90
+ * This screen shows the different categories of articles offered by proximo.
91
+ */
92
+function ProximoMainScreen() {
93
+  const navigation = useNavigation();
94
+  const theme = useTheme();
95
+
96
+  useLayoutEffect(() => {
147 97
     navigation.setOptions({
148
-      headerRight: () => this.getHeaderButtons(),
98
+      headerRight: () => getHeaderButtons(),
149 99
     });
150
-  }
100
+    // eslint-disable-next-line react-hooks/exhaustive-deps
101
+  }, [navigation]);
151 102
 
152 103
   /**
153 104
    * Callback used when the search button is pressed.
154 105
    * This will open a new ProximoListScreen with all items displayed
155 106
    */
156
-  onPressSearchBtn = () => {
157
-    const { navigation } = this.props;
107
+  const onPressSearchBtn = () => {
158 108
     const searchScreenData = {
159 109
       shouldFocusSearchBar: true,
160
-      data: {
161
-        type: {
162
-          id: '0',
163
-          name: i18n.t('screens.proximo.all'),
164
-          icon: 'star',
165
-        },
166
-        data:
167
-          this.articles != null
168
-            ? ProximoMainScreen.getAvailableArticles(this.articles)
169
-            : [],
170
-      },
110
+      category: -1,
171 111
     };
172 112
     navigation.navigate('proximo-list', searchScreenData);
173 113
   };
174 114
 
175
-  /**
176
-   * Callback used when the about button is pressed.
177
-   * This will open the ProximoAboutScreen
178
-   */
179
-  onPressAboutBtn = () => {
180
-    const { navigation } = this.props;
181
-    navigation.navigate('proximo-about');
182
-  };
115
+  const onPressAboutBtn = () => navigation.navigate('proximo-about');
183 116
 
184
-  /**
185
-   * Gets the header buttons
186
-   * @return {*}
187
-   */
188
-  getHeaderButtons() {
117
+  const getHeaderButtons = () => {
189 118
     return (
190 119
       <MaterialHeaderButtons>
191
-        <Item
192
-          title="magnify"
193
-          iconName="magnify"
194
-          onPress={this.onPressSearchBtn}
195
-        />
120
+        <Item title="magnify" iconName="magnify" onPress={onPressSearchBtn} />
196 121
         <Item
197 122
           title="information"
198 123
           iconName="information"
199
-          onPress={this.onPressAboutBtn}
124
+          onPress={onPressAboutBtn}
200 125
         />
201 126
       </MaterialHeaderButtons>
202 127
     );
203
-  }
128
+  };
204 129
 
205 130
   /**
206 131
    * Extracts a key for the given category
@@ -208,7 +133,8 @@ class ProximoMainScreen extends React.Component<PropsType> {
208 133
    * @param item The category to extract the key from
209 134
    * @return {*} The extracted key
210 135
    */
211
-  getKeyExtractor = (item: ProximoMainListItemType): string => item.type.id;
136
+  const getKeyExtractor = (item: ProximoCategoryType): string =>
137
+    item.id.toString();
212 138
 
213 139
   /**
214 140
    * Gets the given category render item
@@ -216,33 +142,36 @@ class ProximoMainScreen extends React.Component<PropsType> {
216 142
    * @param item The category to render
217 143
    * @return {*}
218 144
    */
219
-  getRenderItem = ({ item }: { item: ProximoMainListItemType }) => {
220
-    const { navigation, theme } = this.props;
145
+  const getRenderItem = ({ item }: { item: ProximoCategoryType }) => {
221 146
     const dataToSend = {
222 147
       shouldFocusSearchBar: false,
223
-      data: item,
148
+      category: item.id,
224 149
     };
225
-    const subtitle = `${item.data.length} ${
226
-      item.data.length > 1
150
+    // TODO get article number
151
+    const article_number = 1;
152
+    const subtitle = `${article_number} ${
153
+      article_number > 1
227 154
         ? i18n.t('screens.proximo.articles')
228 155
         : i18n.t('screens.proximo.article')
229 156
     }`;
230
-    const onPress = () => {
231
-      navigation.navigate('proximo-list', dataToSend);
232
-    };
233
-    if (item.data.length > 0) {
157
+    const onPress = () => navigation.navigate('proximo-list', dataToSend);
158
+    if (article_number > 0) {
234 159
       return (
235 160
         <List.Item
236
-          title={item.type.name}
161
+          title={item.name}
237 162
           description={subtitle}
238 163
           onPress={onPress}
239
-          left={(props) => (
240
-            <List.Icon
241
-              style={props.style}
242
-              icon={item.type.icon}
243
-              color={theme.colors.primary}
244
-            />
245
-          )}
164
+          left={(props) =>
165
+            item.icon.endsWith('.png') ? (
166
+              <Avatar.Image style={props.style} source={{ uri: item.icon }} />
167
+            ) : (
168
+              <List.Icon
169
+                style={props.style}
170
+                icon={item.icon}
171
+                color={theme.colors.primary}
172
+              />
173
+            )
174
+          }
246 175
           right={(props) => (
247 176
             <List.Icon
248 177
               color={props.color}
@@ -266,65 +195,46 @@ class ProximoMainScreen extends React.Component<PropsType> {
266 195
    * @param fetchedData
267 196
    * @return {*}
268 197
    * */
269
-  createDataset = (
270
-    fetchedData: ProximoDataType | null
271
-  ): SectionListDataType<ProximoMainListItemType> => {
272
-    return [
273
-      {
274
-        title: '',
275
-        data: this.generateData(fetchedData),
276
-        keyExtractor: this.getKeyExtractor,
277
-      },
278
-    ];
279
-  };
280
-
281
-  /**
282
-   * Generate the data using types and FetchedData.
283
-   * This will group items under the same type.
284
-   *
285
-   * @param fetchedData The array of articles represented by objects
286
-   * @returns {Array} The formatted dataset
287
-   */
288
-  generateData(
289
-    fetchedData: ProximoDataType | null
290
-  ): Array<ProximoMainListItemType> {
291
-    const finalData: Array<ProximoMainListItemType> = [];
292
-    this.articles = null;
293
-    if (fetchedData != null) {
294
-      const { types } = fetchedData;
295
-      this.articles = fetchedData.articles;
296
-      finalData.push({
297
-        type: {
298
-          id: '-1',
198
+  const createDataset = (
199
+    data: CategoriesType | undefined
200
+  ): SectionListDataType<ProximoCategoryType> => {
201
+    if (data) {
202
+      const finalData: CategoriesType = [
203
+        {
204
+          id: -1,
299 205
           name: i18n.t('screens.proximo.all'),
300 206
           icon: 'star',
207
+          created_at: '',
208
+          updated_at: '',
301 209
         },
302
-        data: ProximoMainScreen.getAvailableArticles(this.articles),
303
-      });
304
-      types.forEach((type: ProximoCategoryType) => {
305
-        finalData.push({
306
-          type,
307
-          data: ProximoMainScreen.getAvailableArticles(this.articles, type),
308
-        });
309
-      });
210
+        ...data,
211
+      ];
212
+      return [
213
+        {
214
+          title: '',
215
+          data: finalData.sort(sortFinalData),
216
+          keyExtractor: getKeyExtractor,
217
+        },
218
+      ];
219
+    } else {
220
+      return [
221
+        {
222
+          title: '',
223
+          data: [],
224
+          keyExtractor: getKeyExtractor,
225
+        },
226
+      ];
310 227
     }
311
-    finalData.sort(ProximoMainScreen.sortFinalData);
312
-    return finalData;
313
-  }
228
+  };
314 229
 
315
-  render() {
316
-    const { navigation } = this.props;
317
-    return (
318
-      <WebSectionList
319
-        createDataset={this.createDataset}
320
-        navigation={navigation}
321
-        autoRefreshTime={0}
322
-        refreshOnFocus={false}
323
-        fetchUrl={Urls.proximo}
324
-        renderItem={this.getRenderItem}
325
-      />
326
-    );
327
-  }
230
+  return (
231
+    <WebSectionList
232
+      request={() => readData<CategoriesType>(Urls.proximo.categories)}
233
+      createDataset={createDataset}
234
+      refreshOnFocus={true}
235
+      renderItem={getRenderItem}
236
+    />
237
+  );
328 238
 }
329 239
 
330 240
 export default withTheme(ProximoMainScreen);

+ 11
- 9
src/screens/Services/SelfMenuScreen.tsx View File

@@ -18,7 +18,7 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import { StyleSheet, View } from 'react-native';
21
+import { SectionListData, StyleSheet, View } from 'react-native';
22 22
 import { Card, Text, withTheme } from 'react-native-paper';
23 23
 import { StackNavigationProp } from '@react-navigation/stack';
24 24
 import i18n from 'i18n-js';
@@ -26,6 +26,7 @@ import DateManager from '../../managers/DateManager';
26 26
 import WebSectionList from '../../components/Screens/WebSectionList';
27 27
 import type { SectionListDataType } from '../../components/Screens/WebSectionList';
28 28
 import Urls from '../../constants/Urls';
29
+import { readData } from '../../utils/WebData';
29 30
 
30 31
 type PropsType = {
31 32
   navigation: StackNavigationProp<any>;
@@ -108,7 +109,7 @@ class SelfMenuScreen extends React.Component<PropsType> {
108 109
    * @return {[]}
109 110
    */
110 111
   createDataset = (
111
-    fetchedData: Array<RawRuMenuType>
112
+    fetchedData: Array<RawRuMenuType> | undefined
112 113
   ): SectionListDataType<RuFoodCategoryType> => {
113 114
     let result: SectionListDataType<RuFoodCategoryType> = [];
114 115
     if (fetchedData == null || fetchedData.length === 0) {
@@ -137,7 +138,11 @@ class SelfMenuScreen extends React.Component<PropsType> {
137 138
    * @param section The section to render the header from
138 139
    * @return {*}
139 140
    */
140
-  getRenderSectionHeader = ({ section }: { section: { title: string } }) => {
141
+  getRenderSectionHeader = ({
142
+    section,
143
+  }: {
144
+    section: SectionListData<RuFoodCategoryType>;
145
+  }) => {
141 146
     return (
142 147
       <Card style={styles.headerCard}>
143 148
         <Card.Title
@@ -189,17 +194,14 @@ class SelfMenuScreen extends React.Component<PropsType> {
189 194
   getKeyExtractor = (item: RuFoodCategoryType): string => item.name;
190 195
 
191 196
   render() {
192
-    const { navigation } = this.props;
193 197
     return (
194 198
       <WebSectionList
199
+        request={() => readData<Array<RawRuMenuType>>(Urls.app.menu)}
195 200
         createDataset={this.createDataset}
196
-        navigation={navigation}
197
-        autoRefreshTime={0}
198
-        refreshOnFocus={false}
199
-        fetchUrl={Urls.app.menu}
201
+        refreshOnFocus={true}
200 202
         renderItem={this.getRenderItem}
201 203
         renderSectionHeader={this.getRenderSectionHeader}
202
-        stickyHeader
204
+        stickyHeader={true}
203 205
       />
204 206
     );
205 207
   }

+ 0
- 1
src/screens/Services/WebsiteScreen.tsx View File

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

+ 0
- 1
src/screens/Test.tsx View File

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

+ 22
- 0
src/utils/Requests.tsx View File

@@ -0,0 +1,22 @@
1
+export enum REQUEST_STATUS {
2
+  SUCCESS = 200,
3
+  BAD_INPUT = 400,
4
+  FORBIDDEN = 403,
5
+  CONNECTION_ERROR = 404,
6
+  SERVER_ERROR = 500,
7
+  UNKNOWN = 999,
8
+}
9
+
10
+export enum REQUEST_CODES {
11
+  SUCCESS = 0,
12
+  BAD_CREDENTIALS = 1,
13
+  BAD_TOKEN = 2,
14
+  NO_CONSENT = 3,
15
+  TOKEN_SAVE = 4,
16
+  TOKEN_RETRIEVE = 5,
17
+  BAD_INPUT = 400,
18
+  FORBIDDEN = 403,
19
+  CONNECTION_ERROR = 404,
20
+  SERVER_ERROR = 500,
21
+  UNKNOWN = 999,
22
+}

+ 4
- 4
src/utils/WebData.ts View File

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

+ 21
- 0
src/utils/cacheContext.ts View File

@@ -0,0 +1,21 @@
1
+import React, { useContext } from 'react';
2
+
3
+export type CacheContextType<T> = {
4
+  cache: T | undefined;
5
+  setCache: (newCache: T) => void;
6
+  resetCache: () => void;
7
+};
8
+
9
+export const CacheContext = React.createContext<CacheContextType<any>>({
10
+  cache: undefined,
11
+  setCache: () => undefined,
12
+  resetCache: () => undefined,
13
+});
14
+
15
+function getCacheContext<T>() {
16
+  return CacheContext as React.Context<CacheContextType<T>>;
17
+}
18
+
19
+export function useCache<T>() {
20
+  return useContext(getCacheContext<T>());
21
+}

+ 106
- 0
src/utils/customHooks.tsx View File

@@ -0,0 +1,106 @@
1
+import { DependencyList, useEffect, useRef, useState } from 'react';
2
+import { REQUEST_STATUS } from './Requests';
3
+
4
+export function useMountEffect(func: () => void) {
5
+  // eslint-disable-next-line react-hooks/exhaustive-deps
6
+  useEffect(func, []);
7
+}
8
+
9
+/**
10
+ * Effect that does not run on first render
11
+ *
12
+ * @param effect
13
+ * @param deps
14
+ */
15
+export function useSubsequentEffect(effect: () => void, deps?: DependencyList) {
16
+  const didMountRef = useRef(false);
17
+  useEffect(
18
+    () => {
19
+      if (didMountRef.current) {
20
+        effect();
21
+      } else {
22
+        didMountRef.current = true;
23
+      }
24
+    },
25
+    // eslint-disable-next-line react-hooks/exhaustive-deps
26
+    deps ? deps : []
27
+  );
28
+}
29
+
30
+export function useRequestLogic<T>(
31
+  request: () => Promise<T>,
32
+  cache?: T,
33
+  onCacheUpdate?: (newCache: T) => void,
34
+  startLoading?: boolean,
35
+  minRefreshTime?: number
36
+) {
37
+  const [response, setResponse] = useState<{
38
+    loading: boolean;
39
+    status: REQUEST_STATUS;
40
+    code?: number;
41
+    data: T | undefined;
42
+  }>({
43
+    loading: startLoading !== false && cache === undefined,
44
+    status: REQUEST_STATUS.SUCCESS,
45
+    code: undefined,
46
+    data: undefined,
47
+  });
48
+  const [lastRefreshDate, setLastRefreshDate] = useState<Date | undefined>(
49
+    undefined
50
+  );
51
+
52
+  const refreshData = (newRequest?: () => Promise<T>) => {
53
+    let canRefresh;
54
+    if (lastRefreshDate && minRefreshTime) {
55
+      const last = lastRefreshDate;
56
+      canRefresh = new Date().getTime() - last.getTime() > minRefreshTime;
57
+    } else {
58
+      canRefresh = true;
59
+    }
60
+    if (canRefresh) {
61
+      if (!response.loading) {
62
+        setResponse((prevState) => ({
63
+          ...prevState,
64
+          loading: true,
65
+        }));
66
+      }
67
+      setLastRefreshDate(new Date());
68
+      const r = newRequest ? newRequest : request;
69
+      r()
70
+        .then((requestResponse: T) => {
71
+          setResponse({
72
+            loading: false,
73
+            status: REQUEST_STATUS.SUCCESS,
74
+            code: undefined,
75
+            data: requestResponse,
76
+          });
77
+          if (onCacheUpdate) {
78
+            onCacheUpdate(requestResponse);
79
+          }
80
+        })
81
+        .catch(() => {
82
+          setResponse((prevState) => ({
83
+            loading: false,
84
+            status: REQUEST_STATUS.CONNECTION_ERROR,
85
+            code: 0,
86
+            data: prevState.data,
87
+          }));
88
+        });
89
+    }
90
+  };
91
+
92
+  const value: [
93
+    boolean,
94
+    REQUEST_STATUS,
95
+    number | undefined,
96
+    T | undefined,
97
+    (newRequest?: () => Promise<T>) => void
98
+  ] = [
99
+    response.loading,
100
+    response.status,
101
+    response.code,
102
+    cache ? cache : response.data,
103
+    refreshData,
104
+  ];
105
+  return value;
106
+}

+ 8
- 6
tsconfig.json View File

</