123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- /*
- * Copyright (c) 2019 - 2020 Arnaud Vergnet.
- *
- * This file is part of Campus INSAT.
- *
- * Campus INSAT is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Campus INSAT is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
- */
-
- // @flow
-
- import * as React from 'react';
- import {Platform} from 'react-native';
- import {Searchbar} from 'react-native-paper';
- 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 ClubCategoryType = {
- id: number,
- name: string,
- };
-
- export type ClubType = {
- id: number,
- name: string,
- description: string,
- logo: string,
- email: string | null,
- category: Array<number | null>,
- responsibles: Array<string>,
- };
-
- type PropsType = {
- navigation: StackNavigationProp,
- };
-
- type StateType = {
- currentlySelectedCategories: Array<number>,
- currentSearchString: string,
- };
-
- const LIST_ITEM_HEIGHT = 96;
-
- class ClubListScreen extends React.Component<PropsType, StateType> {
- categories: Array<ClubCategoryType>;
-
- constructor() {
- super();
- this.state = {
- currentlySelectedCategories: [],
- currentSearchString: '',
- };
- }
-
- /**
- * 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},
- });
- }
-
- /**
- * 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 (
- <Searchbar
- placeholder={i18n.t('screens.proximo.search')}
- onChangeText={this.onSearchStringChange}
- />
- );
- };
-
- 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 (
- <MaterialHeaderButtons>
- <Item title="main" iconName="information" onPress={onPress} />
- </MaterialHeaderButtons>
- );
- };
-
- getScreen = (
- data: Array<{
- categories: Array<ClubCategoryType>,
- clubs: Array<ClubType>,
- } | null>,
- ): React.Node => {
- let categoryList = [];
- let clubList = [];
- if (data[0] != null) {
- categoryList = data[0].categories;
- clubList = data[0].clubs;
- }
- this.categories = categoryList;
- return (
- <CollapsibleFlatList
- data={clubList}
- keyExtractor={this.keyExtractor}
- renderItem={this.getRenderItem}
- ListHeaderComponent={this.getListHeader()}
- // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
- removeClippedSubviews
- getItemLayout={this.itemLayout}
- />
- );
- };
-
- /**
- * Gets the list header, with controls to change the categories filter
- *
- * @returns {*}
- */
- getListHeader(): React.Node {
- const {state} = this;
- return (
- <ClubListHeader
- categories={this.categories}
- selectedCategories={state.currentlySelectedCategories}
- onChipSelect={this.onChipSelect}
- />
- );
- }
-
- /**
- * 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);
- };
- if (this.shouldRenderItem(item)) {
- return (
- <ClubListItem
- categoryTranslator={this.getCategoryOfId}
- item={item}
- onPress={onPress}
- height={LIST_ITEM_HEIGHT}
- />
- );
- }
- return null;
- };
-
- 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,
- });
- }
-
- /**
- * 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;
- }
-
- render(): React.Node {
- const {props} = this;
- return (
- <AuthenticatedScreen
- navigation={props.navigation}
- requests={[
- {
- link: 'clubs/list',
- params: {},
- mandatory: true,
- },
- ]}
- renderFunction={this.getScreen}
- />
- );
- }
- }
-
- export default ClubListScreen;
|