Browse Source

Added search feature

Arnaud Vergnet 4 years ago
parent
commit
8f1fc3f1bd
2 changed files with 199 additions and 43 deletions
  1. 62
    0
      components/Lists/ClubListItem.js
  2. 137
    43
      screens/Amicale/ClubListScreen.js

+ 62
- 0
components/Lists/ClubListItem.js View File

@@ -0,0 +1,62 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {Avatar, List, withTheme} from 'react-native-paper';
5
+import {View} from "react-native";
6
+
7
+type Props = {
8
+    onPress: Function,
9
+    categoryTranslator: Function,
10
+    chipRender: Function,
11
+    item: Object,
12
+}
13
+
14
+class ClubListItem extends React.PureComponent<Props> {
15
+
16
+    colors: Object;
17
+    hasManagers: boolean;
18
+
19
+    constructor(props) {
20
+        super(props);
21
+        this.colors = props.theme.colors;
22
+        this.hasManagers = props.item.responsibles.length > 0;
23
+    }
24
+
25
+    getCategoriesRender(categories: Array<number | null>) {
26
+        let final = [];
27
+        for (let i = 0; i < categories.length; i++) {
28
+            if (categories[i] !== null)
29
+                final.push(this.props.chipRender(this.props.categoryTranslator(categories[i])));
30
+        }
31
+        return <View style={{flexDirection: 'row'}}>{final}</View>;
32
+    }
33
+
34
+    render() {
35
+        const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
36
+        return (
37
+            <List.Item
38
+                title={this.props.item.name}
39
+                description={categoriesRender}
40
+                onPress={this.props.onPress}
41
+                left={(props) => <Avatar.Image
42
+                    {...props}
43
+                    style={{backgroundColor: 'transparent'}}
44
+                    size={64}
45
+                    source={{uri: this.props.item.logo}}/>}
46
+                right={(props) => <Avatar.Icon
47
+                    {...props}
48
+                    style={{
49
+                        marginTop: 'auto',
50
+                        marginBottom: 'auto',
51
+                        backgroundColor: 'transparent',
52
+                    }}
53
+                    size={48}
54
+                    icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
55
+                    color={this.hasManagers ? this.colors.success : this.colors.primary}
56
+                />}
57
+            />
58
+        );
59
+    }
60
+}
61
+
62
+export default withTheme(ClubListItem);

+ 137
- 43
screens/Amicale/ClubListScreen.js View File

@@ -1,26 +1,33 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import {View} from "react-native";
5
-import {Avatar, Chip, List, withTheme} from 'react-native-paper';
4
+import {FlatList, Platform, View} from "react-native";
5
+import {Chip, Searchbar, withTheme} from 'react-native-paper';
6 6
 import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
7
-import PureFlatList from "../../components/Lists/PureFlatList";
7
+import i18n from "i18n-js";
8
+import ClubListItem from "../../components/Lists/ClubListItem";
8 9
 
9 10
 type Props = {
10 11
     navigation: Object,
11 12
     theme: Object,
12 13
 }
13 14
 
14
-type State = {}
15
+type State = {
16
+    currentlySelectedCategories: Array<string>,
17
+    currentSearchString: string,
18
+}
15 19
 
16 20
 class ClubListScreen extends React.Component<Props, State> {
17 21
 
18
-    state = {};
22
+    state = {
23
+        currentlySelectedCategories: [],
24
+        currentSearchString: '',
25
+    };
19 26
 
20 27
     colors: Object;
21 28
 
22 29
     getRenderItem: Function;
23
-
30
+    originalData: Array<Object>;
24 31
     categories: Array<Object>;
25 32
 
26 33
     constructor(props) {
@@ -28,6 +35,53 @@ class ClubListScreen extends React.Component<Props, State> {
28 35
         this.colors = props.theme.colors;
29 36
     }
30 37
 
38
+    /**
39
+     * Creates the header content
40
+     */
41
+    componentDidMount() {
42
+        const title = this.getSearchBar.bind(this);
43
+        this.props.navigation.setOptions({
44
+            headerTitle: title,
45
+            headerBackTitleVisible: false,
46
+            headerTitleContainerStyle: Platform.OS === 'ios' ?
47
+                {marginHorizontal: 0, width: '70%'} :
48
+                {marginHorizontal: 0, right: 50, left: 50},
49
+        });
50
+    }
51
+
52
+    /**
53
+     * Gets the header search bar
54
+     *
55
+     * @return {*}
56
+     */
57
+    getSearchBar() {
58
+        return (
59
+            <Searchbar
60
+                placeholder={i18n.t('proximoScreen.search')}
61
+                onChangeText={this.onSearchStringChange}
62
+            />
63
+        );
64
+    }
65
+
66
+    /**
67
+     * Callback used when the search changes
68
+     *
69
+     * @param str The new search string
70
+     */
71
+    onSearchStringChange = (str: string) => {
72
+        this.updateFilteredData(this.sanitizeString(str), null);
73
+    };
74
+
75
+    /**
76
+     * Sanitizes the given string to improve search performance
77
+     *
78
+     * @param str The string to sanitize
79
+     * @return {string} The sanitized string
80
+     */
81
+    sanitizeString(str: string): string {
82
+        return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
83
+    }
84
+
31 85
     keyExtractor = (item: Object) => {
32 86
         return item.name + item.logo;
33 87
     };
@@ -35,59 +89,99 @@ class ClubListScreen extends React.Component<Props, State> {
35 89
     getScreen = (data: Object) => {
36 90
         this.categories = data.categories;
37 91
         return (
38
-            <PureFlatList
92
+            <FlatList
39 93
                 data={data.clubs}
40 94
                 keyExtractor={this.keyExtractor}
41 95
                 renderItem={this.getRenderItem}
42
-                updateData={0}
96
+                ListHeaderComponent={this.getListHeader()}
43 97
             />
44 98
         )
45 99
     };
46 100
 
47
-    getCategoryName(id: number) {
48
-        for (let i = 0; i < this.categories.length; i++) {
49
-            if (id === this.categories[i].id)
50
-                return this.categories[i].name;
101
+    onChipSelect(id: string) {
102
+        this.updateFilteredData(null, id);
103
+    }
104
+
105
+    updateFilteredData(filterStr: string | null, categoryId: string | null) {
106
+        let newCategoriesState = [...this.state.currentlySelectedCategories];
107
+        let newStrState = this.state.currentSearchString;
108
+        if (filterStr !== null)
109
+            newStrState = filterStr;
110
+        if (categoryId !== null) {
111
+            let index = newCategoriesState.indexOf(categoryId);
112
+            if (index === -1)
113
+                newCategoriesState.push(categoryId);
114
+            else
115
+                newCategoriesState.splice(index);
51 116
         }
52
-        return "";
117
+        if (filterStr !== null || categoryId !== null)
118
+            this.setState({
119
+                currentSearchString: newStrState,
120
+                currentlySelectedCategories: newCategoriesState,
121
+            })
53 122
     }
54 123
 
55
-    getCategoriesRender(categories: Array<number|null>) {
124
+    isItemInCategoryFilter(categories: Array<string>) {
125
+        for (const category of categories) {
126
+            if (this.state.currentlySelectedCategories.indexOf(category) !== -1)
127
+                return true;
128
+        }
129
+        return false;
130
+    }
131
+
132
+    getChipRender = (category: Object) => {
133
+        const onPress = this.onChipSelect.bind(this, category.id);
134
+        return <Chip
135
+            selected={this.isItemInCategoryFilter([category.id])}
136
+            mode={'outlined'}
137
+            onPress={onPress}
138
+            style={{marginRight: 5, marginBottom: 5}}
139
+        >
140
+            {category.name}
141
+        </Chip>;
142
+    };
143
+
144
+    getListHeader() {
56 145
         let final = [];
57
-        for (let i = 0; i < categories.length; i++) {
58
-            if (categories[i] !== null)
59
-                final.push(<Chip style={{marginRight: 5}}>{this.getCategoryName(categories[i])}</Chip>);
146
+        for (let i = 0; i < this.categories.length; i++) {
147
+            final.push(this.getChipRender(this.categories[i]));
60 148
         }
61
-        return <View style={{flexDirection: 'row'}}>{final}</View>;
149
+        return <View style={{
150
+            justifyContent: 'space-around',
151
+            flexDirection: 'row',
152
+            flexWrap: 'wrap',
153
+            margin: 10,
154
+        }}>{final}</View>;
155
+    }
156
+
157
+    getCategoryOfId = (id: number) => {
158
+        for (let i = 0; i < this.categories.length; i++) {
159
+            if (id === this.categories[i].id)
160
+                return this.categories[i];
161
+        }
162
+    };
163
+
164
+    shouldRenderItem(item) {
165
+        let shouldRender = this.state.currentlySelectedCategories.length === 0
166
+            || this.isItemInCategoryFilter(item.category);
167
+        if (shouldRender)
168
+            shouldRender = this.sanitizeString(item.name).includes(this.state.currentSearchString);
169
+        return shouldRender;
62 170
     }
63 171
 
64 172
     getRenderItem = ({item}: Object) => {
65 173
         const onPress = this.onListItemPress.bind(this, item);
66
-        const categoriesRender = this.getCategoriesRender.bind(this, item.category);
67
-        const hasManagers = item.responsibles.length > 0;
68
-        return (
69
-            <List.Item
70
-                title={item.name}
71
-                description={categoriesRender}
72
-                onPress={onPress}
73
-                left={(props) => <Avatar.Image
74
-                    {...props}
75
-                    style={{backgroundColor: 'transparent'}}
76
-                    size={64}
77
-                    source={{uri: item.logo}}/>}
78
-                right={(props) => <Avatar.Icon
79
-                    {...props}
80
-                    style={{
81
-                        marginTop: 'auto',
82
-                        marginBottom: 'auto',
83
-                        backgroundColor: 'transparent',
84
-                    }}
85
-                    size={48}
86
-                    icon={hasManagers ? "check-circle-outline" : "alert-circle-outline"}
87
-                    color={hasManagers ? this.colors.success : this.colors.primary}
88
-                />}
89
-            />
90
-        );
174
+        if (this.shouldRenderItem(item)) {
175
+            return (
176
+                <ClubListItem
177
+                    categoryTranslator={this.getCategoryOfId}
178
+                    chipRender={this.getChipRender}
179
+                    item={item}
180
+                    onPress={onPress}
181
+                />
182
+            );
183
+        } else
184
+            return null;
91 185
     };
92 186
 
93 187
     /**

Loading…
Cancel
Save