Browse Source

Improve Clubs components to match linter

Arnaud Vergnet 1 year ago
parent
commit
93d12b27f8

+ 77
- 69
src/components/Lists/Clubs/ClubListHeader.js View File

@@ -2,82 +2,90 @@
2 2
 
3 3
 import * as React from 'react';
4 4
 import {Card, Chip, List, Text} from 'react-native-paper';
5
-import {StyleSheet, View} from "react-native";
5
+import {StyleSheet, View} from 'react-native';
6 6
 import i18n from 'i18n-js';
7
-import AnimatedAccordion from "../../Animations/AnimatedAccordion";
8
-import {isItemInCategoryFilter} from "../../../utils/Search";
9
-import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
7
+import AnimatedAccordion from '../../Animations/AnimatedAccordion';
8
+import {isItemInCategoryFilter} from '../../../utils/Search';
9
+import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
10 10
 
11
-type Props = {
12
-    categories: Array<category>,
13
-    onChipSelect: (id: number) => void,
14
-    selectedCategories: Array<number>,
15
-}
16
-
17
-class ClubListHeader extends React.Component<Props> {
11
+type PropsType = {
12
+  categories: Array<ClubCategoryType>,
13
+  onChipSelect: (id: number) => void,
14
+  selectedCategories: Array<number>,
15
+};
18 16
 
19
-    shouldComponentUpdate(nextProps: Props) {
20
-        return nextProps.selectedCategories.length !== this.props.selectedCategories.length;
21
-    }
17
+const styles = StyleSheet.create({
18
+  card: {
19
+    margin: 5,
20
+  },
21
+  text: {
22
+    paddingLeft: 0,
23
+    marginTop: 5,
24
+    marginBottom: 10,
25
+    marginLeft: 'auto',
26
+    marginRight: 'auto',
27
+  },
28
+  chipContainer: {
29
+    justifyContent: 'space-around',
30
+    flexDirection: 'row',
31
+    flexWrap: 'wrap',
32
+    paddingLeft: 0,
33
+    marginBottom: 5,
34
+  },
35
+});
22 36
 
23
-    getChipRender = (category: category, key: string) => {
24
-        const onPress = () => this.props.onChipSelect(category.id);
25
-        return <Chip
26
-            selected={isItemInCategoryFilter(this.props.selectedCategories, [category.id])}
27
-            mode={'outlined'}
28
-            onPress={onPress}
29
-            style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
30
-            key={key}
31
-        >
32
-            {category.name}
33
-        </Chip>;
34
-    };
37
+class ClubListHeader extends React.Component<PropsType> {
38
+  shouldComponentUpdate(nextProps: PropsType): boolean {
39
+    const {props} = this;
40
+    return (
41
+      nextProps.selectedCategories.length !== props.selectedCategories.length
42
+    );
43
+  }
35 44
 
45
+  getChipRender = (category: ClubCategoryType, key: string): React.Node => {
46
+    const {props} = this;
47
+    const onPress = (): void => props.onChipSelect(category.id);
48
+    return (
49
+      <Chip
50
+        selected={isItemInCategoryFilter(props.selectedCategories, [
51
+          category.id,
52
+          null,
53
+        ])}
54
+        mode="outlined"
55
+        onPress={onPress}
56
+        style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
57
+        key={key}>
58
+        {category.name}
59
+      </Chip>
60
+    );
61
+  };
36 62
 
37
-    getCategoriesRender() {
38
-        let final = [];
39
-        for (let i = 0; i < this.props.categories.length; i++) {
40
-            final.push(this.getChipRender(this.props.categories[i], this.props.categories[i].id.toString()));
41
-        }
42
-        return final;
43
-    }
63
+  getCategoriesRender(): React.Node {
64
+    const {props} = this;
65
+    const final = [];
66
+    props.categories.forEach((cat: ClubCategoryType) => {
67
+      final.push(this.getChipRender(cat, cat.id.toString()));
68
+    });
69
+    return final;
70
+  }
44 71
 
45
-    render() {
46
-        return (
47
-            <Card style={styles.card}>
48
-                <AnimatedAccordion
49
-                    title={i18n.t("screens.clubs.categories")}
50
-                    left={props => <List.Icon {...props} icon="star"/>}
51
-                    opened={true}
52
-                >
53
-                    <Text style={styles.text}>{i18n.t("screens.clubs.categoriesFilterMessage")}</Text>
54
-                    <View style={styles.chipContainer}>
55
-                        {this.getCategoriesRender()}
56
-                    </View>
57
-                </AnimatedAccordion>
58
-            </Card>
59
-        );
60
-    }
72
+  render(): React.Node {
73
+    return (
74
+      <Card style={styles.card}>
75
+        <AnimatedAccordion
76
+          title={i18n.t('screens.clubs.categories')}
77
+          left={({size}: {size: number}): React.Node => (
78
+            <List.Icon size={size} icon="star" />
79
+          )}
80
+          opened>
81
+          <Text style={styles.text}>
82
+            {i18n.t('screens.clubs.categoriesFilterMessage')}
83
+          </Text>
84
+          <View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
85
+        </AnimatedAccordion>
86
+      </Card>
87
+    );
88
+  }
61 89
 }
62 90
 
63
-const styles = StyleSheet.create({
64
-    card: {
65
-        margin: 5
66
-    },
67
-    text: {
68
-        paddingLeft: 0,
69
-        marginTop: 5,
70
-        marginBottom: 10,
71
-        marginLeft: 'auto',
72
-        marginRight: 'auto',
73
-    },
74
-    chipContainer: {
75
-        justifyContent: 'space-around',
76
-        flexDirection: 'row',
77
-        flexWrap: 'wrap',
78
-        paddingLeft: 0,
79
-        marginBottom: 5,
80
-    },
81
-});
82
-
83 91
 export default ClubListHeader;

+ 80
- 71
src/components/Lists/Clubs/ClubListItem.js View File

@@ -2,84 +2,93 @@
2 2
 
3 3
 import * as React from 'react';
4 4
 import {Avatar, Chip, List, withTheme} from 'react-native-paper';
5
-import {View} from "react-native";
6
-import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen";
7
-import type {CustomTheme} from "../../../managers/ThemeManager";
5
+import {View} from 'react-native';
6
+import type {
7
+  ClubCategoryType,
8
+  ClubType,
9
+} from '../../../screens/Amicale/Clubs/ClubListScreen';
10
+import type {CustomTheme} from '../../../managers/ThemeManager';
8 11
 
9
-type Props = {
10
-    onPress: () => void,
11
-    categoryTranslator: (id: number) => category,
12
-    item: club,
13
-    height: number,
14
-    theme: CustomTheme,
15
-}
12
+type PropsType = {
13
+  onPress: () => void,
14
+  categoryTranslator: (id: number) => ClubCategoryType,
15
+  item: ClubType,
16
+  height: number,
17
+  theme: CustomTheme,
18
+};
16 19
 
17
-class ClubListItem extends React.Component<Props> {
20
+class ClubListItem extends React.Component<PropsType> {
21
+  hasManagers: boolean;
18 22
 
19
-    hasManagers: boolean;
23
+  constructor(props: PropsType) {
24
+    super(props);
25
+    this.hasManagers = props.item.responsibles.length > 0;
26
+  }
20 27
 
21
-    constructor(props) {
22
-        super(props);
23
-        this.hasManagers = props.item.responsibles.length > 0;
24
-    }
28
+  shouldComponentUpdate(): boolean {
29
+    return false;
30
+  }
25 31
 
26
-    shouldComponentUpdate() {
27
-        return false;
28
-    }
32
+  getCategoriesRender(categories: Array<number | null>): React.Node {
33
+    const {props} = this;
34
+    const final = [];
35
+    categories.forEach((cat: number | null) => {
36
+      if (cat != null) {
37
+        const category: ClubCategoryType = props.categoryTranslator(cat);
38
+        final.push(
39
+          <Chip
40
+            style={{marginRight: 5, marginBottom: 5}}
41
+            key={`${props.item.id}:${category.id}`}>
42
+            {category.name}
43
+          </Chip>,
44
+        );
45
+      }
46
+    });
47
+    return <View style={{flexDirection: 'row'}}>{final}</View>;
48
+  }
29 49
 
30
-    getCategoriesRender(categories: Array<number | null>) {
31
-        let final = [];
32
-        for (let i = 0; i < categories.length; i++) {
33
-            if (categories[i] !== null) {
34
-                const category: category = this.props.categoryTranslator(categories[i]);
35
-                final.push(
36
-                    <Chip
37
-                        style={{marginRight: 5, marginBottom: 5}}
38
-                        key={this.props.item.id + ':' + category.id}
39
-                    >
40
-                        {category.name}
41
-                    </Chip>
42
-                );
50
+  render(): React.Node {
51
+    const {props} = this;
52
+    const categoriesRender = (): React.Node =>
53
+      this.getCategoriesRender(props.item.category);
54
+    const {colors} = props.theme;
55
+    return (
56
+      <List.Item
57
+        title={props.item.name}
58
+        description={categoriesRender}
59
+        onPress={props.onPress}
60
+        left={(): React.Node => (
61
+          <Avatar.Image
62
+            style={{
63
+              backgroundColor: 'transparent',
64
+              marginLeft: 10,
65
+              marginRight: 10,
66
+            }}
67
+            size={64}
68
+            source={{uri: props.item.logo}}
69
+          />
70
+        )}
71
+        right={(): React.Node => (
72
+          <Avatar.Icon
73
+            style={{
74
+              marginTop: 'auto',
75
+              marginBottom: 'auto',
76
+              backgroundColor: 'transparent',
77
+            }}
78
+            size={48}
79
+            icon={
80
+              this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
43 81
             }
44
-        }
45
-        return <View style={{flexDirection: 'row'}}>{final}</View>;
46
-    }
47
-
48
-    render() {
49
-        const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
50
-        const colors = this.props.theme.colors;
51
-        return (
52
-            <List.Item
53
-                title={this.props.item.name}
54
-                description={categoriesRender}
55
-                onPress={this.props.onPress}
56
-                left={(props) => <Avatar.Image
57
-                    {...props}
58
-                    style={{
59
-                        backgroundColor: 'transparent',
60
-                        marginLeft: 10,
61
-                        marginRight: 10,
62
-                    }}
63
-                    size={64}
64
-                    source={{uri: this.props.item.logo}}/>}
65
-                right={(props) => <Avatar.Icon
66
-                    {...props}
67
-                    style={{
68
-                        marginTop: 'auto',
69
-                        marginBottom: 'auto',
70
-                        backgroundColor: 'transparent',
71
-                    }}
72
-                    size={48}
73
-                    icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
74
-                    color={this.hasManagers ? colors.success : colors.primary}
75
-                />}
76
-                style={{
77
-                    height: this.props.height,
78
-                    justifyContent: 'center',
79
-                }}
80
-            />
81
-        );
82
-    }
82
+            color={this.hasManagers ? colors.success : colors.primary}
83
+          />
84
+        )}
85
+        style={{
86
+          height: props.height,
87
+          justifyContent: 'center',
88
+        }}
89
+      />
90
+    );
91
+  }
83 92
 }
84 93
 
85 94
 export default withTheme(ClubListItem);

+ 40
- 40
src/screens/Amicale/Clubs/ClubAboutScreen.js View File

@@ -4,49 +4,49 @@ import * as React from 'react';
4 4
 import {Image, View} from 'react-native';
5 5
 import {Card, List, Text, withTheme} from 'react-native-paper';
6 6
 import i18n from 'i18n-js';
7
-import Autolink from "react-native-autolink";
8
-import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
9
-
10
-type Props = {};
7
+import Autolink from 'react-native-autolink';
8
+import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
9
+import AMICALE_ICON from '../../../../assets/amicale.png';
11 10
 
12 11
 const CONTACT_LINK = 'clubs@amicale-insat.fr';
13 12
 
14
-class ClubAboutScreen extends React.Component<Props> {
15
-
16
-    render() {
17
-        return (
18
-            <CollapsibleScrollView style={{padding: 5}}>
19
-                <View style={{
20
-                    width: '100%',
21
-                    height: 100,
22
-                    marginTop: 20,
23
-                    marginBottom: 20,
24
-                    justifyContent: 'center',
25
-                    alignItems: 'center'
26
-                }}>
27
-                    <Image
28
-                        source={require('../../../../assets/amicale.png')}
29
-                        style={{flex: 1, resizeMode: "contain"}}
30
-                        resizeMode="contain"/>
31
-                </View>
32
-                <Text>{i18n.t("screens.clubs.about.text")}</Text>
33
-                <Card style={{margin: 5}}>
34
-                    <Card.Title
35
-                        title={i18n.t("screens.clubs.about.title")}
36
-                        subtitle={i18n.t("screens.clubs.about.subtitle")}
37
-                        left={props => <List.Icon {...props} icon={'information'}/>}
38
-                    />
39
-                    <Card.Content>
40
-                        <Text>{i18n.t("screens.clubs.about.message")}</Text>
41
-                        <Autolink
42
-                            text={CONTACT_LINK}
43
-                            component={Text}
44
-                        />
45
-                    </Card.Content>
46
-                </Card>
47
-            </CollapsibleScrollView>
48
-        );
49
-    }
13
+// eslint-disable-next-line react/prefer-stateless-function
14
+class ClubAboutScreen extends React.Component<null> {
15
+  render(): React.Node {
16
+    return (
17
+      <CollapsibleScrollView style={{padding: 5}}>
18
+        <View
19
+          style={{
20
+            width: '100%',
21
+            height: 100,
22
+            marginTop: 20,
23
+            marginBottom: 20,
24
+            justifyContent: 'center',
25
+            alignItems: 'center',
26
+          }}>
27
+          <Image
28
+            source={AMICALE_ICON}
29
+            style={{flex: 1, resizeMode: 'contain'}}
30
+            resizeMode="contain"
31
+          />
32
+        </View>
33
+        <Text>{i18n.t('screens.clubs.about.text')}</Text>
34
+        <Card style={{margin: 5}}>
35
+          <Card.Title
36
+            title={i18n.t('screens.clubs.about.title')}
37
+            subtitle={i18n.t('screens.clubs.about.subtitle')}
38
+            left={({size}: {size: number}): React.Node => (
39
+              <List.Icon size={size} icon="information" />
40
+            )}
41
+          />
42
+          <Card.Content>
43
+            <Text>{i18n.t('screens.clubs.about.message')}</Text>
44
+            <Autolink text={CONTACT_LINK} component={Text} />
45
+          </Card.Content>
46
+        </Card>
47
+      </CollapsibleScrollView>
48
+    );
49
+  }
50 50
 }
51 51
 
52 52
 export default withTheme(ClubAboutScreen);

+ 249
- 225
src/screens/Amicale/Clubs/ClubDisplayScreen.js View File

@@ -2,252 +2,276 @@
2 2
 
3 3
 import * as React from 'react';
4 4
 import {Linking, View} from 'react-native';
5
-import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper';
5
+import {
6
+  Avatar,
7
+  Button,
8
+  Card,
9
+  Chip,
10
+  Paragraph,
11
+  withTheme,
12
+} from 'react-native-paper';
6 13
 import ImageModal from 'react-native-image-modal';
7
-import i18n from "i18n-js";
8
-import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
9
-import CustomHTML from "../../../components/Overrides/CustomHTML";
10
-import CustomTabBar from "../../../components/Tabbar/CustomTabBar";
11
-import type {category, club} from "./ClubListScreen";
12
-import type {CustomTheme} from "../../../managers/ThemeManager";
13
-import {StackNavigationProp} from "@react-navigation/stack";
14
-import {ERROR_TYPE} from "../../../utils/WebData";
15
-import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
16
-
17
-type Props = {
18
-    navigation: StackNavigationProp,
19
-    route: {
20
-        params?: {
21
-            data?: club,
22
-            categories?: Array<category>,
23
-            clubId?: number,
24
-        }, ...
25
-    },
26
-    theme: CustomTheme
27
-};
14
+import i18n from 'i18n-js';
15
+import {StackNavigationProp} from '@react-navigation/stack';
16
+import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
17
+import CustomHTML from '../../../components/Overrides/CustomHTML';
18
+import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
19
+import type {ClubCategoryType, ClubType} from './ClubListScreen';
20
+import type {CustomTheme} from '../../../managers/ThemeManager';
21
+import {ERROR_TYPE} from '../../../utils/WebData';
22
+import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
23
+import type {ApiGenericDataType} from '../../../utils/WebData';
28 24
 
29
-type State = {
30
-    imageModalVisible: boolean,
25
+type PropsType = {
26
+  navigation: StackNavigationProp,
27
+  route: {
28
+    params?: {
29
+      data?: ClubType,
30
+      categories?: Array<ClubCategoryType>,
31
+      clubId?: number,
32
+    },
33
+    ...
34
+  },
35
+  theme: CustomTheme,
31 36
 };
32 37
 
33
-const AMICALE_MAIL = "clubs@amicale-insat.fr";
38
+const AMICALE_MAIL = 'clubs@amicale-insat.fr';
34 39
 
35 40
 /**
36 41
  * Class defining a club event information page.
37 42
  * If called with data and categories navigation parameters, will use those to display the data.
38 43
  * If called with clubId parameter, will fetch the information on the server
39 44
  */
40
-class ClubDisplayScreen extends React.Component<Props, State> {
41
-
42
-    displayData: club | null;
43
-    categories: Array<category> | null;
44
-    clubId: number;
45
-
46
-    shouldFetchData: boolean;
47
-
48
-    state = {
49
-        imageModalVisible: false,
50
-    };
51
-
52
-    constructor(props) {
53
-        super(props);
54
-        if (this.props.route.params != null) {
55
-            if (this.props.route.params.data != null && this.props.route.params.categories != null) {
56
-                this.displayData = this.props.route.params.data;
57
-                this.categories = this.props.route.params.categories;
58
-                this.clubId = this.props.route.params.data.id;
59
-                this.shouldFetchData = false;
60
-            } else if (this.props.route.params.clubId != null) {
61
-                this.displayData = null;
62
-                this.categories = null;
63
-                this.clubId = this.props.route.params.clubId;
64
-                this.shouldFetchData = true;
65
-            }
66
-        }
67
-    }
45
+class ClubDisplayScreen extends React.Component<PropsType> {
46
+  displayData: ClubType | null;
68 47
 
69
-    /**
70
-     * Gets the name of the category with the given ID
71
-     *
72
-     * @param id The category's ID
73
-     * @returns {string|*}
74
-     */
75
-    getCategoryName(id: number) {
76
-        if (this.categories !== null) {
77
-            for (let i = 0; i < this.categories.length; i++) {
78
-                if (id === this.categories[i].id)
79
-                    return this.categories[i].name;
80
-            }
81
-        }
82
-        return "";
83
-    }
48
+  categories: Array<ClubCategoryType> | null;
49
+
50
+  clubId: number;
51
+
52
+  shouldFetchData: boolean;
84 53
 
85
-    /**
86
-     * Gets the view for rendering categories
87
-     *
88
-     * @param categories The categories to display (max 2)
89
-     * @returns {null|*}
90
-     */
91
-    getCategoriesRender(categories: [number, number]) {
92
-        if (this.categories === null)
93
-            return null;
94
-
95
-        let final = [];
96
-        for (let i = 0; i < categories.length; i++) {
97
-            let cat = categories[i];
98
-            if (cat !== null) {
99
-                final.push(
100
-                    <Chip
101
-                        style={{marginRight: 5}}
102
-                        key={i.toString()}>
103
-                        {this.getCategoryName(cat)}
104
-                    </Chip>
105
-                );
106
-            }
107
-        }
108
-        return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
54
+  constructor(props: PropsType) {
55
+    super(props);
56
+    if (props.route.params != null) {
57
+      if (
58
+        props.route.params.data != null &&
59
+        props.route.params.categories != null
60
+      ) {
61
+        this.displayData = props.route.params.data;
62
+        this.categories = props.route.params.categories;
63
+        this.clubId = props.route.params.data.id;
64
+        this.shouldFetchData = false;
65
+      } else if (props.route.params.clubId != null) {
66
+        this.displayData = null;
67
+        this.categories = null;
68
+        this.clubId = props.route.params.clubId;
69
+        this.shouldFetchData = true;
70
+      }
109 71
     }
72
+  }
110 73
 
111
-    /**
112
-     * Gets the view for rendering club managers if any
113
-     *
114
-     * @param managers The list of manager names
115
-     * @param email The club contact email
116
-     * @returns {*}
117
-     */
118
-    getManagersRender(managers: Array<string>, email: string | null) {
119
-        let managersListView = [];
120
-        for (let i = 0; i < managers.length; i++) {
121
-            managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
122
-        }
123
-        const hasManagers = managers.length > 0;
124
-        return (
125
-            <Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
126
-                <Card.Title
127
-                    title={i18n.t('screens.clubs.managers')}
128
-                    subtitle={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
129
-                    left={(props) => <Avatar.Icon
130
-                        {...props}
131
-                        style={{backgroundColor: 'transparent'}}
132
-                        color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
133
-                        icon="account-tie"/>}
134
-                />
135
-                <Card.Content>
136
-                    {managersListView}
137
-                    {this.getEmailButton(email, hasManagers)}
138
-                </Card.Content>
139
-            </Card>
140
-        );
74
+  /**
75
+   * Gets the name of the category with the given ID
76
+   *
77
+   * @param id The category's ID
78
+   * @returns {string|*}
79
+   */
80
+  getCategoryName(id: number): string {
81
+    let categoryName = '';
82
+    if (this.categories !== null) {
83
+      this.categories.forEach((item: ClubCategoryType) => {
84
+        if (id === item.id) categoryName = item.name;
85
+      });
141 86
     }
87
+    return categoryName;
88
+  }
89
+
90
+  /**
91
+   * Gets the view for rendering categories
92
+   *
93
+   * @param categories The categories to display (max 2)
94
+   * @returns {null|*}
95
+   */
96
+  getCategoriesRender(categories: Array<number | null>): React.Node {
97
+    if (this.categories == null) return null;
142 98
 
143
-    /**
144
-     * Gets the email button to contact the club, or the amicale if the club does not have any managers
145
-     *
146
-     * @param email The club contact email
147
-     * @param hasManagers True if the club has managers
148
-     * @returns {*}
149
-     */
150
-    getEmailButton(email: string | null, hasManagers: boolean) {
151
-        const destinationEmail = email != null && hasManagers
152
-            ? email
153
-            : AMICALE_MAIL;
154
-        const text = email != null && hasManagers
155
-            ? i18n.t("screens.clubs.clubContact")
156
-            : i18n.t("screens.clubs.amicaleContact");
157
-        return (
158
-            <Card.Actions>
159
-                <Button
160
-                    icon="email"
161
-                    mode="contained"
162
-                    onPress={() => Linking.openURL('mailto:' + destinationEmail)}
163
-                    style={{marginLeft: 'auto'}}
164
-                >
165
-                    {text}
166
-                </Button>
167
-            </Card.Actions>
99
+    const final = [];
100
+    categories.forEach((cat: number | null) => {
101
+      if (cat != null) {
102
+        final.push(
103
+          <Chip style={{marginRight: 5}} key={cat}>
104
+            {this.getCategoryName(cat)}
105
+          </Chip>,
168 106
         );
169
-    }
107
+      }
108
+    });
109
+    return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
110
+  }
111
+
112
+  /**
113
+   * Gets the view for rendering club managers if any
114
+   *
115
+   * @param managers The list of manager names
116
+   * @param email The club contact email
117
+   * @returns {*}
118
+   */
119
+  getManagersRender(managers: Array<string>, email: string | null): React.Node {
120
+    const {props} = this;
121
+    const managersListView = [];
122
+    managers.forEach((item: string) => {
123
+      managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
124
+    });
125
+    const hasManagers = managers.length > 0;
126
+    return (
127
+      <Card
128
+        style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
129
+        <Card.Title
130
+          title={i18n.t('screens.clubs.managers')}
131
+          subtitle={
132
+            hasManagers
133
+              ? i18n.t('screens.clubs.managersSubtitle')
134
+              : i18n.t('screens.clubs.managersUnavailable')
135
+          }
136
+          left={({size}: {size: number}): React.Node => (
137
+            <Avatar.Icon
138
+              size={size}
139
+              style={{backgroundColor: 'transparent'}}
140
+              color={
141
+                hasManagers
142
+                  ? props.theme.colors.success
143
+                  : props.theme.colors.primary
144
+              }
145
+              icon="account-tie"
146
+            />
147
+          )}
148
+        />
149
+        <Card.Content>
150
+          {managersListView}
151
+          {ClubDisplayScreen.getEmailButton(email, hasManagers)}
152
+        </Card.Content>
153
+      </Card>
154
+    );
155
+  }
170 156
 
171
-    /**
172
-     * Updates the header title to match the given club
173
-     *
174
-     * @param data The club data
175
-     */
176
-    updateHeaderTitle(data: club) {
177
-        this.props.navigation.setOptions({title: data.name})
157
+  /**
158
+   * Gets the email button to contact the club, or the amicale if the club does not have any managers
159
+   *
160
+   * @param email The club contact email
161
+   * @param hasManagers True if the club has managers
162
+   * @returns {*}
163
+   */
164
+  static getEmailButton(
165
+    email: string | null,
166
+    hasManagers: boolean,
167
+  ): React.Node {
168
+    const destinationEmail =
169
+      email != null && hasManagers ? email : AMICALE_MAIL;
170
+    const text =
171
+      email != null && hasManagers
172
+        ? i18n.t('screens.clubs.clubContact')
173
+        : i18n.t('screens.clubs.amicaleContact');
174
+    return (
175
+      <Card.Actions>
176
+        <Button
177
+          icon="email"
178
+          mode="contained"
179
+          onPress={() => {
180
+            Linking.openURL(`mailto:${destinationEmail}`);
181
+          }}
182
+          style={{marginLeft: 'auto'}}>
183
+          {text}
184
+        </Button>
185
+      </Card.Actions>
186
+    );
187
+  }
188
+
189
+  getScreen = (response: Array<ApiGenericDataType | null>): React.Node => {
190
+    const {props} = this;
191
+    let data: ClubType | null = null;
192
+    if (response[0] != null) {
193
+      [data] = response;
194
+      this.updateHeaderTitle(data);
178 195
     }
196
+    if (data != null) {
197
+      return (
198
+        <CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
199
+          {this.getCategoriesRender(data.category)}
200
+          {data.logo !== null ? (
201
+            <View
202
+              style={{
203
+                marginLeft: 'auto',
204
+                marginRight: 'auto',
205
+                marginTop: 10,
206
+                marginBottom: 10,
207
+              }}>
208
+              <ImageModal
209
+                resizeMode="contain"
210
+                imageBackgroundColor={props.theme.colors.background}
211
+                style={{
212
+                  width: 300,
213
+                  height: 300,
214
+                }}
215
+                source={{
216
+                  uri: data.logo,
217
+                }}
218
+              />
219
+            </View>
220
+          ) : (
221
+            <View />
222
+          )}
179 223
 
180
-    getScreen = (response: Array<{ [key: string]: any } | null>) => {
181
-        let data: club | null = null;
182
-        if (response[0] != null) {
183
-            data = response[0];
184
-            this.updateHeaderTitle(data);
185
-        }
186
-        if (data != null) {
187
-            return (
188
-                <CollapsibleScrollView
189
-                    style={{paddingLeft: 5, paddingRight: 5}}
190
-                    hasTab={true}
191
-                >
192
-                    {this.getCategoriesRender(data.category)}
193
-                    {data.logo !== null ?
194
-                        <View style={{
195
-                            marginLeft: 'auto',
196
-                            marginRight: 'auto',
197
-                            marginTop: 10,
198
-                            marginBottom: 10,
199
-                        }}>
200
-                            <ImageModal
201
-                                resizeMode="contain"
202
-                                imageBackgroundColor={this.props.theme.colors.background}
203
-                                style={{
204
-                                    width: 300,
205
-                                    height: 300,
206
-                                }}
207
-                                source={{
208
-                                    uri: data.logo,
209
-                                }}
210
-                            />
211
-                        </View>
212
-                        : <View/>}
213
-
214
-                    {data.description !== null ?
215
-                        // Surround description with div to allow text styling if the description is not html
216
-                        <Card.Content>
217
-                            <CustomHTML html={data.description}/>
218
-                        </Card.Content>
219
-                        : <View/>}
220
-                    {this.getManagersRender(data.responsibles, data.email)}
221
-                </CollapsibleScrollView>
222
-            );
223
-        } else
224
-            return null;
225
-    };
226
-
227
-    render() {
228
-        if (this.shouldFetchData)
229
-            return <AuthenticatedScreen
230
-                {...this.props}
231
-                requests={[
232
-                    {
233
-                        link: 'clubs/info',
234
-                        params: {'id': this.clubId},
235
-                        mandatory: true
236
-                    }
237
-                ]}
238
-                renderFunction={this.getScreen}
239
-                errorViewOverride={[
240
-                    {
241
-                        errorCode: ERROR_TYPE.BAD_INPUT,
242
-                        message: i18n.t("screens.clubs.invalidClub"),
243
-                        icon: "account-question",
244
-                        showRetryButton: false
245
-                    }
246
-                ]}
247
-            />;
248
-        else
249
-            return this.getScreen([this.displayData]);
224
+          {data.description !== null ? (
225
+            // Surround description with div to allow text styling if the description is not html
226
+            <Card.Content>
227
+              <CustomHTML html={data.description} />
228
+            </Card.Content>
229
+          ) : (
230
+            <View />
231
+          )}
232
+          {this.getManagersRender(data.responsibles, data.email)}
233
+        </CollapsibleScrollView>
234
+      );
250 235
     }
236
+    return null;
237
+  };
238
+
239
+  /**
240
+   * Updates the header title to match the given club
241
+   *
242
+   * @param data The club data
243
+   */
244
+  updateHeaderTitle(data: ClubType) {
245
+    const {props} = this;
246
+    props.navigation.setOptions({title: data.name});
247
+  }
248
+
249
+  render(): React.Node {
250
+    const {props} = this;
251
+    if (this.shouldFetchData)
252
+      return (
253
+        <AuthenticatedScreen
254
+          navigation={props.navigation}
255
+          requests={[
256
+            {
257
+              link: 'clubs/info',
258
+              params: {id: this.clubId},
259
+              mandatory: true,
260
+            },
261
+          ]}
262
+          renderFunction={this.getScreen}
263
+          errorViewOverride={[
264
+            {
265
+              errorCode: ERROR_TYPE.BAD_INPUT,
266
+              message: i18n.t('screens.clubs.invalidClub'),
267
+              icon: 'account-question',
268
+              showRetryButton: false,
269
+            },
270
+          ]}
271
+        />
272
+      );
273
+    return this.getScreen([this.displayData]);
274
+  }
251 275
 }
252 276
 
253 277
 export default withTheme(ClubDisplayScreen);

+ 238
- 204
src/screens/Amicale/Clubs/ClubListScreen.js View File

@@ -1,237 +1,271 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import {Platform} from "react-native";
4
+import {Platform} from 'react-native';
5 5
 import {Searchbar} from 'react-native-paper';
6
-import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
7
-import i18n from "i18n-js";
8
-import ClubListItem from "../../../components/Lists/Clubs/ClubListItem";
9
-import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search";
10
-import ClubListHeader from "../../../components/Lists/Clubs/ClubListHeader";
11
-import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
12
-import {StackNavigationProp} from "@react-navigation/stack";
13
-import type {CustomTheme} from "../../../managers/ThemeManager";
14
-import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList";
15
-
16
-export type category = {
17
-    id: number,
18
-    name: string,
6
+import i18n from 'i18n-js';
7
+import {StackNavigationProp} from '@react-navigation/stack';
8
+import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
9
+import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
10
+import {isItemInCategoryFilter, stringMatchQuery} from '../../../utils/Search';
11
+import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
12
+import MaterialHeaderButtons, {
13
+  Item,
14
+} from '../../../components/Overrides/CustomHeaderButton';
15
+import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
16
+
17
+export type ClubCategoryType = {
18
+  id: number,
19
+  name: string,
19 20
 };
20 21
 
21
-export type club = {
22
-    id: number,
23
-    name: string,
24
-    description: string,
25
-    logo: string,
26
-    email: string | null,
27
-    category: [number, number],
28
-    responsibles: Array<string>,
22
+export type ClubType = {
23
+  id: number,
24
+  name: string,
25
+  description: string,
26
+  logo: string,
27
+  email: string | null,
28
+  category: Array<number | null>,
29
+  responsibles: Array<string>,
29 30
 };
30 31
 
31
-type Props = {
32
-    navigation: StackNavigationProp,
33
-    theme: CustomTheme,
34
-}
32
+type PropsType = {
33
+  navigation: StackNavigationProp,
34
+};
35 35
 
36
-type State = {
37
-    currentlySelectedCategories: Array<number>,
38
-    currentSearchString: string,
39
-}
36
+type StateType = {
37
+  currentlySelectedCategories: Array<number>,
38
+  currentSearchString: string,
39
+};
40 40
 
41 41
 const LIST_ITEM_HEIGHT = 96;
42 42
 
43
-class ClubListScreen extends React.Component<Props, State> {
43
+class ClubListScreen extends React.Component<PropsType, StateType> {
44
+  categories: Array<ClubCategoryType>;
44 45
 
45
-    state = {
46
-        currentlySelectedCategories: [],
47
-        currentSearchString: '',
46
+  constructor() {
47
+    super();
48
+    this.state = {
49
+      currentlySelectedCategories: [],
50
+      currentSearchString: '',
48 51
     };
52
+  }
49 53
 
50
-    categories: Array<category>;
51
-
52
-    /**
53
-     * Creates the header content
54
-     */
55
-    componentDidMount() {
56
-        this.props.navigation.setOptions({
57
-            headerTitle: this.getSearchBar,
58
-            headerRight: this.getHeaderButtons,
59
-            headerBackTitleVisible: false,
60
-            headerTitleContainerStyle: Platform.OS === 'ios' ?
61
-                {marginHorizontal: 0, width: '70%'} :
62
-                {marginHorizontal: 0, right: 50, left: 50},
63
-        });
64
-    }
54
+  /**
55
+   * Creates the header content
56
+   */
57
+  componentDidMount() {
58
+    const {props} = this;
59
+    props.navigation.setOptions({
60
+      headerTitle: this.getSearchBar,
61
+      headerRight: this.getHeaderButtons,
62
+      headerBackTitleVisible: false,
63
+      headerTitleContainerStyle:
64
+        Platform.OS === 'ios'
65
+          ? {marginHorizontal: 0, width: '70%'}
66
+          : {marginHorizontal: 0, right: 50, left: 50},
67
+    });
68
+  }
65 69
 
66
-    /**
67
-     * Gets the header search bar
68
-     *
69
-     * @return {*}
70
-     */
71
-    getSearchBar = () => {
72
-        return (
73
-            <Searchbar
74
-                placeholder={i18n.t('screens.proximo.search')}
75
-                onChangeText={this.onSearchStringChange}
76
-            />
77
-        );
78
-    };
70
+  /**
71
+   * Callback used when clicking an article in the list.
72
+   * It opens the modal to show detailed information about the article
73
+   *
74
+   * @param item The article pressed
75
+   */
76
+  onListItemPress(item: ClubType) {
77
+    const {props} = this;
78
+    props.navigation.navigate('club-information', {
79
+      data: item,
80
+      categories: this.categories,
81
+    });
82
+  }
79 83
 
80
-    /**
81
-     * Gets the header button
82
-     * @return {*}
83
-     */
84
-    getHeaderButtons = () => {
85
-        const onPress = () => this.props.navigation.navigate("club-about");
86
-        return <MaterialHeaderButtons>
87
-            <Item title="main" iconName="information" onPress={onPress}/>
88
-        </MaterialHeaderButtons>;
89
-    };
84
+  /**
85
+   * Callback used when the search changes
86
+   *
87
+   * @param str The new search string
88
+   */
89
+  onSearchStringChange = (str: string) => {
90
+    this.updateFilteredData(str, null);
91
+  };
90 92
 
91
-    /**
92
-     * Callback used when the search changes
93
-     *
94
-     * @param str The new search string
95
-     */
96
-    onSearchStringChange = (str: string) => {
97
-        this.updateFilteredData(str, null);
98
-    };
93
+  /**
94
+   * Gets the header search bar
95
+   *
96
+   * @return {*}
97
+   */
98
+  getSearchBar = (): React.Node => {
99
+    return (
100
+      <Searchbar
101
+        placeholder={i18n.t('screens.proximo.search')}
102
+        onChangeText={this.onSearchStringChange}
103
+      />
104
+    );
105
+  };
99 106
 
100
-    keyExtractor = (item: club) => item.id.toString();
101
-
102
-    itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
103
-
104
-    getScreen = (data: Array<{ categories: Array<category>, clubs: Array<club> } | null>) => {
105
-        let categoryList = [];
106
-        let clubList = [];
107
-        if (data[0] != null) {
108
-            categoryList = data[0].categories;
109
-            clubList = data[0].clubs;
110
-        }
111
-        this.categories = categoryList;
112
-        return (
113
-            <CollapsibleFlatList
114
-                data={clubList}
115
-                keyExtractor={this.keyExtractor}
116
-                renderItem={this.getRenderItem}
117
-                ListHeaderComponent={this.getListHeader()}
118
-                // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
119
-                removeClippedSubviews={true}
120
-                getItemLayout={this.itemLayout}
121
-            />
122
-        )
107
+  onChipSelect = (id: number) => {
108
+    this.updateFilteredData(null, id);
109
+  };
110
+
111
+  /**
112
+   * Gets the header button
113
+   * @return {*}
114
+   */
115
+  getHeaderButtons = (): React.Node => {
116
+    const onPress = () => {
117
+      const {props} = this;
118
+      props.navigation.navigate('club-about');
123 119
     };
120
+    return (
121
+      <MaterialHeaderButtons>
122
+        <Item title="main" iconName="information" onPress={onPress} />
123
+      </MaterialHeaderButtons>
124
+    );
125
+  };
124 126
 
125
-    onChipSelect = (id: number) => this.updateFilteredData(null, id);
126
-
127
-    /**
128
-     * Updates the search string and category filter, saving them to the State.
129
-     *
130
-     * If the given category is already in the filter, it removes it.
131
-     * Otherwise it adds it to the filter.
132
-     *
133
-     * @param filterStr The new filter string to use
134
-     * @param categoryId The category to add/remove from the filter
135
-     */
136
-    updateFilteredData(filterStr: string | null, categoryId: number | null) {
137
-        let newCategoriesState = [...this.state.currentlySelectedCategories];
138
-        let newStrState = this.state.currentSearchString;
139
-        if (filterStr !== null)
140
-            newStrState = filterStr;
141
-        if (categoryId !== null) {
142
-            let index = newCategoriesState.indexOf(categoryId);
143
-            if (index === -1)
144
-                newCategoriesState.push(categoryId);
145
-            else
146
-                newCategoriesState.splice(index, 1);
147
-        }
148
-        if (filterStr !== null || categoryId !== null)
149
-            this.setState({
150
-                currentSearchString: newStrState,
151
-                currentlySelectedCategories: newCategoriesState,
152
-            })
127
+  getScreen = (
128
+    data: Array<{
129
+      categories: Array<ClubCategoryType>,
130
+      clubs: Array<ClubType>,
131
+    } | null>,
132
+  ): React.Node => {
133
+    let categoryList = [];
134
+    let clubList = [];
135
+    if (data[0] != null) {
136
+      categoryList = data[0].categories;
137
+      clubList = data[0].clubs;
153 138
     }
139
+    this.categories = categoryList;
140
+    return (
141
+      <CollapsibleFlatList
142
+        data={clubList}
143
+        keyExtractor={this.keyExtractor}
144
+        renderItem={this.getRenderItem}
145
+        ListHeaderComponent={this.getListHeader()}
146
+        // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
147
+        removeClippedSubviews
148
+        getItemLayout={this.itemLayout}
149
+      />
150
+    );
151
+  };
154 152
 
155
-    /**
156
-     * Gets the list header, with controls to change the categories filter
157
-     *
158
-     * @returns {*}
159
-     */
160
-    getListHeader() {
161
-        return <ClubListHeader
162
-            categories={this.categories}
163
-            selectedCategories={this.state.currentlySelectedCategories}
164
-            onChipSelect={this.onChipSelect}
165
-        />;
166
-    }
153
+  /**
154
+   * Gets the list header, with controls to change the categories filter
155
+   *
156
+   * @returns {*}
157
+   */
158
+  getListHeader(): React.Node {
159
+    const {state} = this;
160
+    return (
161
+      <ClubListHeader
162
+        categories={this.categories}
163
+        selectedCategories={state.currentlySelectedCategories}
164
+        onChipSelect={this.onChipSelect}
165
+      />
166
+    );
167
+  }
167 168
 
168
-    /**
169
-     * Gets the category object of the given ID
170
-     *
171
-     * @param id The ID of the category to find
172
-     * @returns {*}
173
-     */
174
-    getCategoryOfId = (id: number) => {
175
-        for (let i = 0; i < this.categories.length; i++) {
176
-            if (id === this.categories[i].id)
177
-                return this.categories[i];
178
-        }
179
-    };
169
+  /**
170
+   * Gets the category object of the given ID
171
+   *
172
+   * @param id The ID of the category to find
173
+   * @returns {*}
174
+   */
175
+  getCategoryOfId = (id: number): ClubCategoryType | null => {
176
+    let cat = null;
177
+    this.categories.forEach((item: ClubCategoryType) => {
178
+      if (id === item.id) cat = item;
179
+    });
180
+    return cat;
181
+  };
180 182
 
181
-    /**
182
-     * Checks if the given item should be rendered according to current name and category filters
183
-     *
184
-     * @param item The club to check
185
-     * @returns {boolean}
186
-     */
187
-    shouldRenderItem(item: club) {
188
-        let shouldRender = this.state.currentlySelectedCategories.length === 0
189
-            || isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category);
190
-        if (shouldRender)
191
-            shouldRender = stringMatchQuery(item.name, this.state.currentSearchString);
192
-        return shouldRender;
183
+  getRenderItem = ({item}: {item: ClubType}): React.Node => {
184
+    const onPress = () => {
185
+      this.onListItemPress(item);
186
+    };
187
+    if (this.shouldRenderItem(item)) {
188
+      return (
189
+        <ClubListItem
190
+          categoryTranslator={this.getCategoryOfId}
191
+          item={item}
192
+          onPress={onPress}
193
+          height={LIST_ITEM_HEIGHT}
194
+        />
195
+      );
193 196
     }
197
+    return null;
198
+  };
194 199
 
195
-    getRenderItem = ({item}: { item: club }) => {
196
-        const onPress = this.onListItemPress.bind(this, item);
197
-        if (this.shouldRenderItem(item)) {
198
-            return (
199
-                <ClubListItem
200
-                    categoryTranslator={this.getCategoryOfId}
201
-                    item={item}
202
-                    onPress={onPress}
203
-                    height={LIST_ITEM_HEIGHT}
204
-                />
205
-            );
206
-        } else
207
-            return null;
208
-    };
200
+  keyExtractor = (item: ClubType): string => item.id.toString();
209 201
 
210
-    /**
211
-     * Callback used when clicking an article in the list.
212
-     * It opens the modal to show detailed information about the article
213
-     *
214
-     * @param item The article pressed
215
-     */
216
-    onListItemPress(item: club) {
217
-        this.props.navigation.navigate("club-information", {data: item, categories: this.categories});
218
-    }
202
+  itemLayout = (
203
+    data: {...},
204
+    index: number,
205
+  ): {length: number, offset: number, index: number} => ({
206
+    length: LIST_ITEM_HEIGHT,
207
+    offset: LIST_ITEM_HEIGHT * index,
208
+    index,
209
+  });
219 210
 
220
-    render() {
221
-        return (
222
-            <AuthenticatedScreen
223
-                {...this.props}
224
-                requests={[
225
-                    {
226
-                        link: 'clubs/list',
227
-                        params: {},
228
-                        mandatory: true,
229
-                    }
230
-                ]}
231
-                renderFunction={this.getScreen}
232
-            />
233
-        );
211
+  /**
212
+   * Updates the search string and category filter, saving them to the State.
213
+   *
214
+   * If the given category is already in the filter, it removes it.
215
+   * Otherwise it adds it to the filter.
216
+   *
217
+   * @param filterStr The new filter string to use
218
+   * @param categoryId The category to add/remove from the filter
219
+   */
220
+  updateFilteredData(filterStr: string | null, categoryId: number | null) {
221
+    const {state} = this;
222
+    const newCategoriesState = [...state.currentlySelectedCategories];
223
+    let newStrState = state.currentSearchString;
224
+    if (filterStr !== null) newStrState = filterStr;
225
+    if (categoryId !== null) {
226
+      const index = newCategoriesState.indexOf(categoryId);
227
+      if (index === -1) newCategoriesState.push(categoryId);
228
+      else newCategoriesState.splice(index, 1);
234 229
     }
230
+    if (filterStr !== null || categoryId !== null)
231
+      this.setState({
232
+        currentSearchString: newStrState,
233
+        currentlySelectedCategories: newCategoriesState,
234
+      });
235
+  }
236
+
237
+  /**
238
+   * Checks if the given item should be rendered according to current name and category filters
239
+   *
240
+   * @param item The club to check
241
+   * @returns {boolean}
242
+   */
243
+  shouldRenderItem(item: ClubType): boolean {
244
+    const {state} = this;
245
+    let shouldRender =
246
+      state.currentlySelectedCategories.length === 0 ||
247
+      isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
248
+    if (shouldRender)
249
+      shouldRender = stringMatchQuery(item.name, state.currentSearchString);
250
+    return shouldRender;
251
+  }
252
+
253
+  render(): React.Node {
254
+    const {props} = this;
255
+    return (
256
+      <AuthenticatedScreen
257
+        navigation={props.navigation}
258
+        requests={[
259
+          {
260
+            link: 'clubs/list',
261
+            params: {},
262
+            mandatory: true,
263
+          },
264
+        ]}
265
+        renderFunction={this.getScreen}
266
+      />
267
+    );
268
+  }
235 269
 }
236 270
 
237 271
 export default ClubListScreen;

+ 17
- 14
src/utils/Search.js View File

@@ -1,6 +1,5 @@
1 1
 // @flow
2 2
 
3
-
4 3
 /**
5 4
  * Sanitizes the given string to improve search performance.
6 5
  *
@@ -10,11 +9,12 @@
10 9
  * @return {string} The sanitized string
11 10
  */
12 11
 export function sanitizeString(str: string): string {
13
-    return str.toLowerCase()
14
-        .normalize("NFD")
15
-        .replace(/[\u0300-\u036f]/g, "")
16
-        .replace(/ /g, "")
17
-        .replace(/_/g, "");
12
+  return str
13
+    .toLowerCase()
14
+    .normalize('NFD')
15
+    .replace(/[\u0300-\u036f]/g, '')
16
+    .replace(/ /g, '')
17
+    .replace(/_/g, '');
18 18
 }
19 19
 
20 20
 /**
@@ -24,8 +24,8 @@ export function sanitizeString(str: string): string {
24 24
  * @param query The query string used to find a match
25 25
  * @returns {boolean}
26 26
  */
27
-export function stringMatchQuery(str: string, query: string) {
28
-    return sanitizeString(str).includes(sanitizeString(query));
27
+export function stringMatchQuery(str: string, query: string): boolean {
28
+  return sanitizeString(str).includes(sanitizeString(query));
29 29
 }
30 30
 
31 31
 /**
@@ -35,10 +35,13 @@ export function stringMatchQuery(str: string, query: string) {
35 35
  * @param categories The item's categories tuple
36 36
  * @returns {boolean} True if at least one entry is in both arrays
37 37
  */
38
-export function isItemInCategoryFilter(filter: Array<number>, categories: [number, number]) {
39
-    for (const category of categories) {
40
-        if (filter.indexOf(category) !== -1)
41
-            return true;
42
-    }
43
-    return false;
38
+export function isItemInCategoryFilter(
39
+  filter: Array<number>,
40
+  categories: Array<number | null>,
41
+): boolean {
42
+  let itemFound = false;
43
+  categories.forEach((cat: number | null) => {
44
+    if (cat != null && filter.indexOf(cat) !== -1) itemFound = true;
45
+  });
46
+  return itemFound;
44 47
 }

Loading…
Cancel
Save