Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ClubListScreen.tsx 7.5KB

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