From 93d12b27f8c19020caa6a2d422977e5597dadf12 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Mon, 3 Aug 2020 21:53:53 +0200 Subject: [PATCH] Improve Clubs components to match linter --- src/components/Lists/Clubs/ClubListHeader.js | 152 +++--- src/components/Lists/Clubs/ClubListItem.js | 153 +++--- src/screens/Amicale/Clubs/ClubAboutScreen.js | 80 +-- .../Amicale/Clubs/ClubDisplayScreen.js | 464 +++++++++--------- src/screens/Amicale/Clubs/ClubListScreen.js | 452 +++++++++-------- src/utils/Search.js | 31 +- 6 files changed, 705 insertions(+), 627 deletions(-) diff --git a/src/components/Lists/Clubs/ClubListHeader.js b/src/components/Lists/Clubs/ClubListHeader.js index f82ef82..d245d78 100644 --- a/src/components/Lists/Clubs/ClubListHeader.js +++ b/src/components/Lists/Clubs/ClubListHeader.js @@ -2,82 +2,90 @@ import * as React from 'react'; import {Card, Chip, List, Text} from 'react-native-paper'; -import {StyleSheet, View} from "react-native"; +import {StyleSheet, View} from 'react-native'; import i18n from 'i18n-js'; -import AnimatedAccordion from "../../Animations/AnimatedAccordion"; -import {isItemInCategoryFilter} from "../../../utils/Search"; -import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen"; +import AnimatedAccordion from '../../Animations/AnimatedAccordion'; +import {isItemInCategoryFilter} from '../../../utils/Search'; +import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen'; -type Props = { - categories: Array, - onChipSelect: (id: number) => void, - selectedCategories: Array, -} - -class ClubListHeader extends React.Component { - - shouldComponentUpdate(nextProps: Props) { - return nextProps.selectedCategories.length !== this.props.selectedCategories.length; - } - - getChipRender = (category: category, key: string) => { - const onPress = () => this.props.onChipSelect(category.id); - return - {category.name} - ; - }; - - - getCategoriesRender() { - let final = []; - for (let i = 0; i < this.props.categories.length; i++) { - final.push(this.getChipRender(this.props.categories[i], this.props.categories[i].id.toString())); - } - return final; - } - - render() { - return ( - - } - opened={true} - > - {i18n.t("screens.clubs.categoriesFilterMessage")} - - {this.getCategoriesRender()} - - - - ); - } -} +type PropsType = { + categories: Array, + onChipSelect: (id: number) => void, + selectedCategories: Array, +}; const styles = StyleSheet.create({ - card: { - margin: 5 - }, - text: { - paddingLeft: 0, - marginTop: 5, - marginBottom: 10, - marginLeft: 'auto', - marginRight: 'auto', - }, - chipContainer: { - justifyContent: 'space-around', - flexDirection: 'row', - flexWrap: 'wrap', - paddingLeft: 0, - marginBottom: 5, - }, + card: { + margin: 5, + }, + text: { + paddingLeft: 0, + marginTop: 5, + marginBottom: 10, + marginLeft: 'auto', + marginRight: 'auto', + }, + chipContainer: { + justifyContent: 'space-around', + flexDirection: 'row', + flexWrap: 'wrap', + paddingLeft: 0, + marginBottom: 5, + }, }); +class ClubListHeader extends React.Component { + shouldComponentUpdate(nextProps: PropsType): boolean { + const {props} = this; + return ( + nextProps.selectedCategories.length !== props.selectedCategories.length + ); + } + + getChipRender = (category: ClubCategoryType, key: string): React.Node => { + const {props} = this; + const onPress = (): void => props.onChipSelect(category.id); + return ( + + {category.name} + + ); + }; + + getCategoriesRender(): React.Node { + const {props} = this; + const final = []; + props.categories.forEach((cat: ClubCategoryType) => { + final.push(this.getChipRender(cat, cat.id.toString())); + }); + return final; + } + + render(): React.Node { + return ( + + ( + + )} + opened> + + {i18n.t('screens.clubs.categoriesFilterMessage')} + + {this.getCategoriesRender()} + + + ); + } +} + export default ClubListHeader; diff --git a/src/components/Lists/Clubs/ClubListItem.js b/src/components/Lists/Clubs/ClubListItem.js index efc6e56..ce59d42 100644 --- a/src/components/Lists/Clubs/ClubListItem.js +++ b/src/components/Lists/Clubs/ClubListItem.js @@ -2,84 +2,93 @@ import * as React from 'react'; import {Avatar, Chip, List, withTheme} from 'react-native-paper'; -import {View} from "react-native"; -import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen"; -import type {CustomTheme} from "../../../managers/ThemeManager"; +import {View} from 'react-native'; +import type { + ClubCategoryType, + ClubType, +} from '../../../screens/Amicale/Clubs/ClubListScreen'; +import type {CustomTheme} from '../../../managers/ThemeManager'; -type Props = { - onPress: () => void, - categoryTranslator: (id: number) => category, - item: club, - height: number, - theme: CustomTheme, -} +type PropsType = { + onPress: () => void, + categoryTranslator: (id: number) => ClubCategoryType, + item: ClubType, + height: number, + theme: CustomTheme, +}; -class ClubListItem extends React.Component { +class ClubListItem extends React.Component { + hasManagers: boolean; - hasManagers: boolean; + constructor(props: PropsType) { + super(props); + this.hasManagers = props.item.responsibles.length > 0; + } - constructor(props) { - super(props); - this.hasManagers = props.item.responsibles.length > 0; - } + shouldComponentUpdate(): boolean { + return false; + } - shouldComponentUpdate() { - return false; - } - - getCategoriesRender(categories: Array) { - let final = []; - for (let i = 0; i < categories.length; i++) { - if (categories[i] !== null) { - const category: category = this.props.categoryTranslator(categories[i]); - final.push( - - {category.name} - - ); - } - } - return {final}; - } - - render() { - const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category); - const colors = this.props.theme.colors; - return ( - } - right={(props) => } - style={{ - height: this.props.height, - justifyContent: 'center', - }} - /> + getCategoriesRender(categories: Array): React.Node { + const {props} = this; + const final = []; + categories.forEach((cat: number | null) => { + if (cat != null) { + const category: ClubCategoryType = props.categoryTranslator(cat); + final.push( + + {category.name} + , ); - } + } + }); + return {final}; + } + + render(): React.Node { + const {props} = this; + const categoriesRender = (): React.Node => + this.getCategoriesRender(props.item.category); + const {colors} = props.theme; + return ( + ( + + )} + right={(): React.Node => ( + + )} + style={{ + height: props.height, + justifyContent: 'center', + }} + /> + ); + } } export default withTheme(ClubListItem); diff --git a/src/screens/Amicale/Clubs/ClubAboutScreen.js b/src/screens/Amicale/Clubs/ClubAboutScreen.js index 825f220..dfb96a1 100644 --- a/src/screens/Amicale/Clubs/ClubAboutScreen.js +++ b/src/screens/Amicale/Clubs/ClubAboutScreen.js @@ -4,49 +4,49 @@ import * as React from 'react'; import {Image, View} from 'react-native'; import {Card, List, Text, withTheme} from 'react-native-paper'; import i18n from 'i18n-js'; -import Autolink from "react-native-autolink"; -import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; - -type Props = {}; +import Autolink from 'react-native-autolink'; +import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; +import AMICALE_ICON from '../../../../assets/amicale.png'; const CONTACT_LINK = 'clubs@amicale-insat.fr'; -class ClubAboutScreen extends React.Component { - - render() { - return ( - - - - - {i18n.t("screens.clubs.about.text")} - - } - /> - - {i18n.t("screens.clubs.about.message")} - - - - - ); - } +// eslint-disable-next-line react/prefer-stateless-function +class ClubAboutScreen extends React.Component { + render(): React.Node { + return ( + + + + + {i18n.t('screens.clubs.about.text')} + + ( + + )} + /> + + {i18n.t('screens.clubs.about.message')} + + + + + ); + } } export default withTheme(ClubAboutScreen); diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.js b/src/screens/Amicale/Clubs/ClubDisplayScreen.js index 773c2b0..598597d 100644 --- a/src/screens/Amicale/Clubs/ClubDisplayScreen.js +++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.js @@ -2,252 +2,276 @@ import * as React from 'react'; import {Linking, View} from 'react-native'; -import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper'; +import { + Avatar, + Button, + Card, + Chip, + Paragraph, + withTheme, +} from 'react-native-paper'; import ImageModal from 'react-native-image-modal'; -import i18n from "i18n-js"; -import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; -import CustomHTML from "../../../components/Overrides/CustomHTML"; -import CustomTabBar from "../../../components/Tabbar/CustomTabBar"; -import type {category, club} from "./ClubListScreen"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import {StackNavigationProp} from "@react-navigation/stack"; -import {ERROR_TYPE} from "../../../utils/WebData"; -import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; +import i18n from 'i18n-js'; +import {StackNavigationProp} from '@react-navigation/stack'; +import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; +import CustomHTML from '../../../components/Overrides/CustomHTML'; +import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; +import type {ClubCategoryType, ClubType} from './ClubListScreen'; +import type {CustomTheme} from '../../../managers/ThemeManager'; +import {ERROR_TYPE} from '../../../utils/WebData'; +import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; +import type {ApiGenericDataType} from '../../../utils/WebData'; -type Props = { - navigation: StackNavigationProp, - route: { - params?: { - data?: club, - categories?: Array, - clubId?: number, - }, ... +type PropsType = { + navigation: StackNavigationProp, + route: { + params?: { + data?: ClubType, + categories?: Array, + clubId?: number, }, - theme: CustomTheme + ... + }, + theme: CustomTheme, }; -type State = { - imageModalVisible: boolean, -}; - -const AMICALE_MAIL = "clubs@amicale-insat.fr"; +const AMICALE_MAIL = 'clubs@amicale-insat.fr'; /** * Class defining a club event information page. * If called with data and categories navigation parameters, will use those to display the data. * If called with clubId parameter, will fetch the information on the server */ -class ClubDisplayScreen extends React.Component { +class ClubDisplayScreen extends React.Component { + displayData: ClubType | null; - displayData: club | null; - categories: Array | null; - clubId: number; + categories: Array | null; - shouldFetchData: boolean; + clubId: number; - state = { - imageModalVisible: false, - }; + shouldFetchData: boolean; - constructor(props) { - super(props); - if (this.props.route.params != null) { - if (this.props.route.params.data != null && this.props.route.params.categories != null) { - this.displayData = this.props.route.params.data; - this.categories = this.props.route.params.categories; - this.clubId = this.props.route.params.data.id; - this.shouldFetchData = false; - } else if (this.props.route.params.clubId != null) { - this.displayData = null; - this.categories = null; - this.clubId = this.props.route.params.clubId; - this.shouldFetchData = true; - } - } + constructor(props: PropsType) { + super(props); + if (props.route.params != null) { + if ( + props.route.params.data != null && + props.route.params.categories != null + ) { + this.displayData = props.route.params.data; + this.categories = props.route.params.categories; + this.clubId = props.route.params.data.id; + this.shouldFetchData = false; + } else if (props.route.params.clubId != null) { + this.displayData = null; + this.categories = null; + this.clubId = props.route.params.clubId; + this.shouldFetchData = true; + } } + } - /** - * Gets the name of the category with the given ID - * - * @param id The category's ID - * @returns {string|*} - */ - getCategoryName(id: number) { - if (this.categories !== null) { - for (let i = 0; i < this.categories.length; i++) { - if (id === this.categories[i].id) - return this.categories[i].name; - } - } - return ""; + /** + * Gets the name of the category with the given ID + * + * @param id The category's ID + * @returns {string|*} + */ + getCategoryName(id: number): string { + let categoryName = ''; + if (this.categories !== null) { + this.categories.forEach((item: ClubCategoryType) => { + if (id === item.id) categoryName = item.name; + }); } + return categoryName; + } - /** - * Gets the view for rendering categories - * - * @param categories The categories to display (max 2) - * @returns {null|*} - */ - getCategoriesRender(categories: [number, number]) { - if (this.categories === null) - return null; + /** + * Gets the view for rendering categories + * + * @param categories The categories to display (max 2) + * @returns {null|*} + */ + getCategoriesRender(categories: Array): React.Node { + if (this.categories == null) return null; - let final = []; - for (let i = 0; i < categories.length; i++) { - let cat = categories[i]; - if (cat !== null) { - final.push( - - {this.getCategoryName(cat)} - - ); - } - } - return {final}; - } - - /** - * Gets the view for rendering club managers if any - * - * @param managers The list of manager names - * @param email The club contact email - * @returns {*} - */ - getManagersRender(managers: Array, email: string | null) { - let managersListView = []; - for (let i = 0; i < managers.length; i++) { - managersListView.push({managers[i]}) - } - const hasManagers = managers.length > 0; - return ( - - } - /> - - {managersListView} - {this.getEmailButton(email, hasManagers)} - - + const final = []; + categories.forEach((cat: number | null) => { + if (cat != null) { + final.push( + + {this.getCategoryName(cat)} + , ); + } + }); + return {final}; + } + + /** + * Gets the view for rendering club managers if any + * + * @param managers The list of manager names + * @param email The club contact email + * @returns {*} + */ + getManagersRender(managers: Array, email: string | null): React.Node { + const {props} = this; + const managersListView = []; + managers.forEach((item: string) => { + managersListView.push({item}); + }); + const hasManagers = managers.length > 0; + return ( + + ( + + )} + /> + + {managersListView} + {ClubDisplayScreen.getEmailButton(email, hasManagers)} + + + ); + } + + /** + * Gets the email button to contact the club, or the amicale if the club does not have any managers + * + * @param email The club contact email + * @param hasManagers True if the club has managers + * @returns {*} + */ + static getEmailButton( + email: string | null, + hasManagers: boolean, + ): React.Node { + const destinationEmail = + email != null && hasManagers ? email : AMICALE_MAIL; + const text = + email != null && hasManagers + ? i18n.t('screens.clubs.clubContact') + : i18n.t('screens.clubs.amicaleContact'); + return ( + + + + ); + } + + getScreen = (response: Array): React.Node => { + const {props} = this; + let data: ClubType | null = null; + if (response[0] != null) { + [data] = response; + this.updateHeaderTitle(data); } + if (data != null) { + return ( + + {this.getCategoriesRender(data.category)} + {data.logo !== null ? ( + + + + ) : ( + + )} - /** - * Gets the email button to contact the club, or the amicale if the club does not have any managers - * - * @param email The club contact email - * @param hasManagers True if the club has managers - * @returns {*} - */ - getEmailButton(email: string | null, hasManagers: boolean) { - const destinationEmail = email != null && hasManagers - ? email - : AMICALE_MAIL; - const text = email != null && hasManagers - ? i18n.t("screens.clubs.clubContact") - : i18n.t("screens.clubs.amicaleContact"); - return ( - - - - ); + {data.description !== null ? ( + // Surround description with div to allow text styling if the description is not html + + + + ) : ( + + )} + {this.getManagersRender(data.responsibles, data.email)} + + ); } + return null; + }; - /** - * Updates the header title to match the given club - * - * @param data The club data - */ - updateHeaderTitle(data: club) { - this.props.navigation.setOptions({title: data.name}) - } + /** + * Updates the header title to match the given club + * + * @param data The club data + */ + updateHeaderTitle(data: ClubType) { + const {props} = this; + props.navigation.setOptions({title: data.name}); + } - getScreen = (response: Array<{ [key: string]: any } | null>) => { - let data: club | null = null; - if (response[0] != null) { - data = response[0]; - this.updateHeaderTitle(data); - } - if (data != null) { - return ( - - {this.getCategoriesRender(data.category)} - {data.logo !== null ? - - - - : } - - {data.description !== null ? - // Surround description with div to allow text styling if the description is not html - - - - : } - {this.getManagersRender(data.responsibles, data.email)} - - ); - } else - return null; - }; - - render() { - if (this.shouldFetchData) - return ; - else - return this.getScreen([this.displayData]); - } + render(): React.Node { + const {props} = this; + if (this.shouldFetchData) + return ( + + ); + return this.getScreen([this.displayData]); + } } export default withTheme(ClubDisplayScreen); diff --git a/src/screens/Amicale/Clubs/ClubListScreen.js b/src/screens/Amicale/Clubs/ClubListScreen.js index 31364cb..a859a98 100644 --- a/src/screens/Amicale/Clubs/ClubListScreen.js +++ b/src/screens/Amicale/Clubs/ClubListScreen.js @@ -1,237 +1,271 @@ // @flow import * as React from 'react'; -import {Platform} from "react-native"; +import {Platform} from 'react-native'; import {Searchbar} from 'react-native-paper'; -import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; -import i18n from "i18n-js"; -import ClubListItem from "../../../components/Lists/Clubs/ClubListItem"; -import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search"; -import ClubListHeader from "../../../components/Lists/Clubs/ClubListHeader"; -import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; +import i18n from 'i18n-js'; +import {StackNavigationProp} from '@react-navigation/stack'; +import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; +import ClubListItem from '../../../components/Lists/Clubs/ClubListItem'; +import {isItemInCategoryFilter, stringMatchQuery} from '../../../utils/Search'; +import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader'; +import MaterialHeaderButtons, { + Item, +} from '../../../components/Overrides/CustomHeaderButton'; +import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList'; -export type category = { - id: number, - name: string, +export type ClubCategoryType = { + id: number, + name: string, }; -export type club = { - id: number, - name: string, - description: string, - logo: string, - email: string | null, - category: [number, number], - responsibles: Array, +export type ClubType = { + id: number, + name: string, + description: string, + logo: string, + email: string | null, + category: Array, + responsibles: Array, }; -type Props = { - navigation: StackNavigationProp, - theme: CustomTheme, -} +type PropsType = { + navigation: StackNavigationProp, +}; -type State = { - currentlySelectedCategories: Array, - currentSearchString: string, -} +type StateType = { + currentlySelectedCategories: Array, + currentSearchString: string, +}; const LIST_ITEM_HEIGHT = 96; -class ClubListScreen extends React.Component { +class ClubListScreen extends React.Component { + categories: Array; - state = { - currentlySelectedCategories: [], - currentSearchString: '', + constructor() { + super(); + this.state = { + currentlySelectedCategories: [], + currentSearchString: '', }; + } - categories: Array; + /** + * Creates the header content + */ + componentDidMount() { + const {props} = this; + props.navigation.setOptions({ + headerTitle: this.getSearchBar, + headerRight: this.getHeaderButtons, + headerBackTitleVisible: false, + headerTitleContainerStyle: + Platform.OS === 'ios' + ? {marginHorizontal: 0, width: '70%'} + : {marginHorizontal: 0, right: 50, left: 50}, + }); + } - /** - * Creates the header content - */ - componentDidMount() { - this.props.navigation.setOptions({ - headerTitle: this.getSearchBar, - headerRight: this.getHeaderButtons, - headerBackTitleVisible: false, - headerTitleContainerStyle: Platform.OS === 'ios' ? - {marginHorizontal: 0, width: '70%'} : - {marginHorizontal: 0, right: 50, left: 50}, - }); + /** + * Callback used when clicking an article in the list. + * It opens the modal to show detailed information about the article + * + * @param item The article pressed + */ + onListItemPress(item: ClubType) { + const {props} = this; + props.navigation.navigate('club-information', { + data: item, + categories: this.categories, + }); + } + + /** + * Callback used when the search changes + * + * @param str The new search string + */ + onSearchStringChange = (str: string) => { + this.updateFilteredData(str, null); + }; + + /** + * Gets the header search bar + * + * @return {*} + */ + getSearchBar = (): React.Node => { + return ( + + ); + }; + + onChipSelect = (id: number) => { + this.updateFilteredData(null, id); + }; + + /** + * Gets the header button + * @return {*} + */ + getHeaderButtons = (): React.Node => { + const onPress = () => { + const {props} = this; + props.navigation.navigate('club-about'); + }; + return ( + + + + ); + }; + + getScreen = ( + data: Array<{ + categories: Array, + clubs: Array, + } | null>, + ): React.Node => { + let categoryList = []; + let clubList = []; + if (data[0] != null) { + categoryList = data[0].categories; + clubList = data[0].clubs; } + this.categories = categoryList; + return ( + + ); + }; - /** - * Gets the header search bar - * - * @return {*} - */ - getSearchBar = () => { - return ( - - ); + /** + * Gets the list header, with controls to change the categories filter + * + * @returns {*} + */ + getListHeader(): React.Node { + const {state} = this; + return ( + + ); + } + + /** + * Gets the category object of the given ID + * + * @param id The ID of the category to find + * @returns {*} + */ + getCategoryOfId = (id: number): ClubCategoryType | null => { + let cat = null; + this.categories.forEach((item: ClubCategoryType) => { + if (id === item.id) cat = item; + }); + return cat; + }; + + getRenderItem = ({item}: {item: ClubType}): React.Node => { + const onPress = () => { + this.onListItemPress(item); }; - - /** - * Gets the header button - * @return {*} - */ - getHeaderButtons = () => { - const onPress = () => this.props.navigation.navigate("club-about"); - return - - ; - }; - - /** - * Callback used when the search changes - * - * @param str The new search string - */ - onSearchStringChange = (str: string) => { - this.updateFilteredData(str, null); - }; - - keyExtractor = (item: club) => item.id.toString(); - - itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); - - getScreen = (data: Array<{ categories: Array, clubs: Array } | null>) => { - let categoryList = []; - let clubList = []; - if (data[0] != null) { - categoryList = data[0].categories; - clubList = data[0].clubs; - } - this.categories = categoryList; - return ( - - ) - }; - - onChipSelect = (id: number) => this.updateFilteredData(null, id); - - /** - * Updates the search string and category filter, saving them to the State. - * - * If the given category is already in the filter, it removes it. - * Otherwise it adds it to the filter. - * - * @param filterStr The new filter string to use - * @param categoryId The category to add/remove from the filter - */ - updateFilteredData(filterStr: string | null, categoryId: number | null) { - let newCategoriesState = [...this.state.currentlySelectedCategories]; - let newStrState = this.state.currentSearchString; - if (filterStr !== null) - newStrState = filterStr; - if (categoryId !== null) { - let index = newCategoriesState.indexOf(categoryId); - if (index === -1) - newCategoriesState.push(categoryId); - else - newCategoriesState.splice(index, 1); - } - if (filterStr !== null || categoryId !== null) - this.setState({ - currentSearchString: newStrState, - currentlySelectedCategories: newCategoriesState, - }) + if (this.shouldRenderItem(item)) { + return ( + + ); } + return null; + }; - /** - * Gets the list header, with controls to change the categories filter - * - * @returns {*} - */ - getListHeader() { - return ; + keyExtractor = (item: ClubType): string => item.id.toString(); + + itemLayout = ( + data: {...}, + index: number, + ): {length: number, offset: number, index: number} => ({ + length: LIST_ITEM_HEIGHT, + offset: LIST_ITEM_HEIGHT * index, + index, + }); + + /** + * Updates the search string and category filter, saving them to the State. + * + * If the given category is already in the filter, it removes it. + * Otherwise it adds it to the filter. + * + * @param filterStr The new filter string to use + * @param categoryId The category to add/remove from the filter + */ + updateFilteredData(filterStr: string | null, categoryId: number | null) { + const {state} = this; + const newCategoriesState = [...state.currentlySelectedCategories]; + let newStrState = state.currentSearchString; + if (filterStr !== null) newStrState = filterStr; + if (categoryId !== null) { + const index = newCategoriesState.indexOf(categoryId); + if (index === -1) newCategoriesState.push(categoryId); + else newCategoriesState.splice(index, 1); } + if (filterStr !== null || categoryId !== null) + this.setState({ + currentSearchString: newStrState, + currentlySelectedCategories: newCategoriesState, + }); + } - /** - * Gets the category object of the given ID - * - * @param id The ID of the category to find - * @returns {*} - */ - getCategoryOfId = (id: number) => { - for (let i = 0; i < this.categories.length; i++) { - if (id === this.categories[i].id) - return this.categories[i]; - } - }; + /** + * Checks if the given item should be rendered according to current name and category filters + * + * @param item The club to check + * @returns {boolean} + */ + shouldRenderItem(item: ClubType): boolean { + const {state} = this; + let shouldRender = + state.currentlySelectedCategories.length === 0 || + isItemInCategoryFilter(state.currentlySelectedCategories, item.category); + if (shouldRender) + shouldRender = stringMatchQuery(item.name, state.currentSearchString); + return shouldRender; + } - /** - * Checks if the given item should be rendered according to current name and category filters - * - * @param item The club to check - * @returns {boolean} - */ - shouldRenderItem(item: club) { - let shouldRender = this.state.currentlySelectedCategories.length === 0 - || isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category); - if (shouldRender) - shouldRender = stringMatchQuery(item.name, this.state.currentSearchString); - return shouldRender; - } - - getRenderItem = ({item}: { item: club }) => { - const onPress = this.onListItemPress.bind(this, item); - if (this.shouldRenderItem(item)) { - return ( - - ); - } else - return null; - }; - - /** - * Callback used when clicking an article in the list. - * It opens the modal to show detailed information about the article - * - * @param item The article pressed - */ - onListItemPress(item: club) { - this.props.navigation.navigate("club-information", {data: item, categories: this.categories}); - } - - render() { - return ( - - ); - } + render(): React.Node { + const {props} = this; + return ( + + ); + } } export default ClubListScreen; diff --git a/src/utils/Search.js b/src/utils/Search.js index edbc68e..146e46c 100644 --- a/src/utils/Search.js +++ b/src/utils/Search.js @@ -1,6 +1,5 @@ // @flow - /** * Sanitizes the given string to improve search performance. * @@ -10,11 +9,12 @@ * @return {string} The sanitized string */ export function sanitizeString(str: string): string { - return str.toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/ /g, "") - .replace(/_/g, ""); + return str + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/ /g, '') + .replace(/_/g, ''); } /** @@ -24,8 +24,8 @@ export function sanitizeString(str: string): string { * @param query The query string used to find a match * @returns {boolean} */ -export function stringMatchQuery(str: string, query: string) { - return sanitizeString(str).includes(sanitizeString(query)); +export function stringMatchQuery(str: string, query: string): boolean { + return sanitizeString(str).includes(sanitizeString(query)); } /** @@ -35,10 +35,13 @@ export function stringMatchQuery(str: string, query: string) { * @param categories The item's categories tuple * @returns {boolean} True if at least one entry is in both arrays */ -export function isItemInCategoryFilter(filter: Array, categories: [number, number]) { - for (const category of categories) { - if (filter.indexOf(category) !== -1) - return true; - } - return false; +export function isItemInCategoryFilter( + filter: Array, + categories: Array, +): boolean { + let itemFound = false; + categories.forEach((cat: number | null) => { + if (cat != null && filter.indexOf(cat) !== -1) itemFound = true; + }); + return itemFound; }