forked from vergnet/application-amicale
287 lines
7.5 KiB
TypeScript
287 lines
7.5 KiB
TypeScript
/*
|
|
* 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/>.
|
|
*/
|
|
|
|
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 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 ConnectionManager from '../../../managers/ConnectionManager';
|
|
import WebSectionList from '../../../components/Screens/WebSectionList';
|
|
|
|
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<any>;
|
|
};
|
|
|
|
type StateType = {
|
|
currentlySelectedCategories: Array<number>;
|
|
currentSearchString: string;
|
|
};
|
|
|
|
type ResponseType = {
|
|
categories: Array<ClubCategoryType>;
|
|
clubs: Array<ClubType>;
|
|
};
|
|
|
|
const LIST_ITEM_HEIGHT = 96;
|
|
|
|
class ClubListScreen extends React.Component<PropsType, StateType> {
|
|
categories: Array<ClubCategoryType>;
|
|
|
|
constructor(props: PropsType) {
|
|
super(props);
|
|
this.categories = [];
|
|
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 = () => {
|
|
return (
|
|
// @ts-ignore
|
|
<Searchbar
|
|
placeholder={i18n.t('screens.proximo.search')}
|
|
onChangeText={this.onSearchStringChange}
|
|
/>
|
|
);
|
|
};
|
|
|
|
onChipSelect = (id: number) => {
|
|
this.updateFilteredData(null, id);
|
|
};
|
|
|
|
/**
|
|
* Gets the header button
|
|
* @return {*}
|
|
*/
|
|
getHeaderButtons = () => {
|
|
const onPress = () => {
|
|
const { props } = this;
|
|
props.navigation.navigate('club-about');
|
|
};
|
|
return (
|
|
<MaterialHeaderButtons>
|
|
<Item title="main" iconName="information" onPress={onPress} />
|
|
</MaterialHeaderButtons>
|
|
);
|
|
};
|
|
|
|
createDataset = (data: ResponseType | undefined) => {
|
|
if (data) {
|
|
this.categories = data?.categories;
|
|
return [{ title: '', data: data.clubs }];
|
|
} else {
|
|
return [{ title: '', data: [] }];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets the list header, with controls to change the categories filter
|
|
*
|
|
* @returns {*}
|
|
*/
|
|
getListHeader(data: ResponseType | undefined) {
|
|
const { state } = this;
|
|
if (data) {
|
|
return (
|
|
<ClubListHeader
|
|
categories={this.categories}
|
|
selectedCategories={state.currentlySelectedCategories}
|
|
onChipSelect={this.onChipSelect}
|
|
/>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 }) => {
|
|
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();
|
|
|
|
/**
|
|
* 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() {
|
|
return (
|
|
<WebSectionList
|
|
request={() =>
|
|
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
|
'clubs/list'
|
|
)
|
|
}
|
|
createDataset={this.createDataset}
|
|
keyExtractor={this.keyExtractor}
|
|
renderItem={this.getRenderItem}
|
|
renderListHeaderComponent={(data) => this.getListHeader(data)}
|
|
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
|
removeClippedSubviews={true}
|
|
itemHeight={LIST_ITEM_HEIGHT}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default ClubListScreen;
|