Improve Clubs components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-03 21:53:53 +02:00
parent 33d98b024b
commit 93d12b27f8
6 changed files with 705 additions and 627 deletions

View file

@ -2,67 +2,21 @@
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<category>,
type PropsType = {
categories: Array<ClubCategoryType>,
onChipSelect: (id: number) => void,
selectedCategories: Array<number>,
}
class ClubListHeader extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return nextProps.selectedCategories.length !== this.props.selectedCategories.length;
}
getChipRender = (category: category, key: string) => {
const onPress = () => this.props.onChipSelect(category.id);
return <Chip
selected={isItemInCategoryFilter(this.props.selectedCategories, [category.id])}
mode={'outlined'}
onPress={onPress}
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
key={key}
>
{category.name}
</Chip>;
};
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 (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t("screens.clubs.categories")}
left={props => <List.Icon {...props} icon="star"/>}
opened={true}
>
<Text style={styles.text}>{i18n.t("screens.clubs.categoriesFilterMessage")}</Text>
<View style={styles.chipContainer}>
{this.getCategoriesRender()}
</View>
</AnimatedAccordion>
</Card>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 5
margin: 5,
},
text: {
paddingLeft: 0,
@ -80,4 +34,58 @@ const styles = StyleSheet.create({
},
});
class ClubListHeader extends React.Component<PropsType> {
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 (
<Chip
selected={isItemInCategoryFilter(props.selectedCategories, [
category.id,
null,
])}
mode="outlined"
onPress={onPress}
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
key={key}>
{category.name}
</Chip>
);
};
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 (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="star" />
)}
opened>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
</AnimatedAccordion>
</Card>
);
}
}
export default ClubListHeader;

View file

@ -2,79 +2,88 @@
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 = {
type PropsType = {
onPress: () => void,
categoryTranslator: (id: number) => category,
item: club,
categoryTranslator: (id: number) => ClubCategoryType,
item: ClubType,
height: number,
theme: CustomTheme,
}
class ClubListItem extends React.Component<Props> {
};
class ClubListItem extends React.Component<PropsType> {
hasManagers: boolean;
constructor(props) {
constructor(props: PropsType) {
super(props);
this.hasManagers = props.item.responsibles.length > 0;
}
shouldComponentUpdate() {
shouldComponentUpdate(): boolean {
return false;
}
getCategoriesRender(categories: Array<number | null>) {
let final = [];
for (let i = 0; i < categories.length; i++) {
if (categories[i] !== null) {
const category: category = this.props.categoryTranslator(categories[i]);
getCategoriesRender(categories: Array<number | null>): React.Node {
const {props} = this;
const final = [];
categories.forEach((cat: number | null) => {
if (cat != null) {
const category: ClubCategoryType = props.categoryTranslator(cat);
final.push(
<Chip
style={{marginRight: 5, marginBottom: 5}}
key={this.props.item.id + ':' + category.id}
>
key={`${props.item.id}:${category.id}`}>
{category.name}
</Chip>
</Chip>,
);
}
}
});
return <View style={{flexDirection: 'row'}}>{final}</View>;
}
render() {
const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
const colors = this.props.theme.colors;
render(): React.Node {
const {props} = this;
const categoriesRender = (): React.Node =>
this.getCategoriesRender(props.item.category);
const {colors} = props.theme;
return (
<List.Item
title={this.props.item.name}
title={props.item.name}
description={categoriesRender}
onPress={this.props.onPress}
left={(props) => <Avatar.Image
{...props}
onPress={props.onPress}
left={(): React.Node => (
<Avatar.Image
style={{
backgroundColor: 'transparent',
marginLeft: 10,
marginRight: 10,
}}
size={64}
source={{uri: this.props.item.logo}}/>}
right={(props) => <Avatar.Icon
{...props}
source={{uri: props.item.logo}}
/>
)}
right={(): React.Node => (
<Avatar.Icon
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
icon={
this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
}
color={this.hasManagers ? colors.success : colors.primary}
/>}
/>
)}
style={{
height: this.props.height,
height: props.height,
justifyContent: 'center',
}}
/>

View file

@ -4,44 +4,44 @@ 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<Props> {
render() {
// eslint-disable-next-line react/prefer-stateless-function
class ClubAboutScreen extends React.Component<null> {
render(): React.Node {
return (
<CollapsibleScrollView style={{padding: 5}}>
<View style={{
<View
style={{
width: '100%',
height: 100,
marginTop: 20,
marginBottom: 20,
justifyContent: 'center',
alignItems: 'center'
alignItems: 'center',
}}>
<Image
source={require('../../../../assets/amicale.png')}
style={{flex: 1, resizeMode: "contain"}}
resizeMode="contain"/>
source={AMICALE_ICON}
style={{flex: 1, resizeMode: 'contain'}}
resizeMode="contain"
/>
</View>
<Text>{i18n.t("screens.clubs.about.text")}</Text>
<Text>{i18n.t('screens.clubs.about.text')}</Text>
<Card style={{margin: 5}}>
<Card.Title
title={i18n.t("screens.clubs.about.title")}
subtitle={i18n.t("screens.clubs.about.subtitle")}
left={props => <List.Icon {...props} icon={'information'}/>}
title={i18n.t('screens.clubs.about.title')}
subtitle={i18n.t('screens.clubs.about.subtitle')}
left={({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="information" />
)}
/>
<Card.Content>
<Text>{i18n.t("screens.clubs.about.message")}</Text>
<Autolink
text={CONTACT_LINK}
component={Text}
/>
<Text>{i18n.t('screens.clubs.about.message')}</Text>
<Autolink text={CONTACT_LINK} component={Text} />
</Card.Content>
</Card>
</CollapsibleScrollView>

View file

@ -2,65 +2,70 @@
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 = {
type PropsType = {
navigation: StackNavigationProp,
route: {
params?: {
data?: club,
categories?: Array<category>,
data?: ClubType,
categories?: Array<ClubCategoryType>,
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<Props, State> {
class ClubDisplayScreen extends React.Component<PropsType> {
displayData: ClubType | null;
categories: Array<ClubCategoryType> | null;
displayData: club | null;
categories: Array<category> | null;
clubId: number;
shouldFetchData: boolean;
state = {
imageModalVisible: false,
};
constructor(props) {
constructor(props: PropsType) {
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;
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 (this.props.route.params.clubId != null) {
} else if (props.route.params.clubId != null) {
this.displayData = null;
this.categories = null;
this.clubId = this.props.route.params.clubId;
this.clubId = props.route.params.clubId;
this.shouldFetchData = true;
}
}
@ -72,14 +77,14 @@ class ClubDisplayScreen extends React.Component<Props, State> {
* @param id The category's ID
* @returns {string|*}
*/
getCategoryName(id: number) {
getCategoryName(id: number): string {
let categoryName = '';
if (this.categories !== null) {
for (let i = 0; i < this.categories.length; i++) {
if (id === this.categories[i].id)
return this.categories[i].name;
this.categories.forEach((item: ClubCategoryType) => {
if (id === item.id) categoryName = item.name;
});
}
}
return "";
return categoryName;
}
/**
@ -88,23 +93,19 @@ class ClubDisplayScreen extends React.Component<Props, State> {
* @param categories The categories to display (max 2)
* @returns {null|*}
*/
getCategoriesRender(categories: [number, number]) {
if (this.categories === null)
return null;
getCategoriesRender(categories: Array<number | null>): 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) {
const final = [];
categories.forEach((cat: number | null) => {
if (cat != null) {
final.push(
<Chip
style={{marginRight: 5}}
key={i.toString()}>
<Chip style={{marginRight: 5}} key={cat}>
{this.getCategoryName(cat)}
</Chip>
</Chip>,
);
}
}
});
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
}
@ -115,26 +116,39 @@ class ClubDisplayScreen extends React.Component<Props, State> {
* @param email The club contact email
* @returns {*}
*/
getManagersRender(managers: Array<string>, email: string | null) {
let managersListView = [];
for (let i = 0; i < managers.length; i++) {
managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
}
getManagersRender(managers: Array<string>, email: string | null): React.Node {
const {props} = this;
const managersListView = [];
managers.forEach((item: string) => {
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
});
const hasManagers = managers.length > 0;
return (
<Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card
style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.clubs.managers')}
subtitle={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
left={(props) => <Avatar.Icon
{...props}
subtitle={
hasManagers
? i18n.t('screens.clubs.managersSubtitle')
: i18n.t('screens.clubs.managersUnavailable')
}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{backgroundColor: 'transparent'}}
color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
icon="account-tie"/>}
color={
hasManagers
? props.theme.colors.success
: props.theme.colors.primary
}
icon="account-tie"
/>
)}
/>
<Card.Content>
{managersListView}
{this.getEmailButton(email, hasManagers)}
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
</Card.Content>
</Card>
);
@ -147,51 +161,45 @@ class ClubDisplayScreen extends React.Component<Props, State> {
* @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");
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 (
<Card.Actions>
<Button
icon="email"
mode="contained"
onPress={() => Linking.openURL('mailto:' + destinationEmail)}
style={{marginLeft: 'auto'}}
>
onPress={() => {
Linking.openURL(`mailto:${destinationEmail}`);
}}
style={{marginLeft: 'auto'}}>
{text}
</Button>
</Card.Actions>
);
}
/**
* Updates the header title to match the given club
*
* @param data The club data
*/
updateHeaderTitle(data: club) {
this.props.navigation.setOptions({title: data.name})
}
getScreen = (response: Array<{ [key: string]: any } | null>) => {
let data: club | null = null;
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => {
const {props} = this;
let data: ClubType | null = null;
if (response[0] != null) {
data = response[0];
[data] = response;
this.updateHeaderTitle(data);
}
if (data != null) {
return (
<CollapsibleScrollView
style={{paddingLeft: 5, paddingRight: 5}}
hasTab={true}
>
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
{this.getCategoriesRender(data.category)}
{data.logo !== null ?
<View style={{
{data.logo !== null ? (
<View
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
@ -199,7 +207,7 @@ class ClubDisplayScreen extends React.Component<Props, State> {
}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={this.props.theme.colors.background}
imageBackgroundColor={props.theme.colors.background}
style={{
width: 300,
height: 300,
@ -209,43 +217,59 @@ class ClubDisplayScreen extends React.Component<Props, State> {
}}
/>
</View>
: <View/>}
) : (
<View />
)}
{data.description !== null ?
{data.description !== null ? (
// Surround description with div to allow text styling if the description is not html
<Card.Content>
<CustomHTML html={data.description} />
</Card.Content>
: <View/>}
) : (
<View />
)}
{this.getManagersRender(data.responsibles, data.email)}
</CollapsibleScrollView>
);
} else
}
return null;
};
render() {
/**
* 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});
}
render(): React.Node {
const {props} = this;
if (this.shouldFetchData)
return <AuthenticatedScreen
{...this.props}
return (
<AuthenticatedScreen
navigation={props.navigation}
requests={[
{
link: 'clubs/info',
params: {'id': this.clubId},
mandatory: true
}
params: {id: this.clubId},
mandatory: true,
},
]}
renderFunction={this.getScreen}
errorViewOverride={[
{
errorCode: ERROR_TYPE.BAD_INPUT,
message: i18n.t("screens.clubs.invalidClub"),
icon: "account-question",
showRetryButton: false
}
message: i18n.t('screens.clubs.invalidClub'),
icon: 'account-question',
showRetryButton: false,
},
]}
/>;
else
/>
);
return this.getScreen([this.displayData]);
}
}

View file

@ -1,92 +1,85 @@
// @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 = {
export type ClubCategoryType = {
id: number,
name: string,
};
export type club = {
export type ClubType = {
id: number,
name: string,
description: string,
logo: string,
email: string | null,
category: [number, number],
category: Array<number | null>,
responsibles: Array<string>,
};
type Props = {
type PropsType = {
navigation: StackNavigationProp,
theme: CustomTheme,
}
};
type State = {
type StateType = {
currentlySelectedCategories: Array<number>,
currentSearchString: string,
}
};
const LIST_ITEM_HEIGHT = 96;
class ClubListScreen extends React.Component<Props, State> {
class ClubListScreen extends React.Component<PropsType, StateType> {
categories: Array<ClubCategoryType>;
state = {
constructor() {
super();
this.state = {
currentlySelectedCategories: [],
currentSearchString: '',
};
categories: Array<category>;
}
/**
* Creates the header content
*/
componentDidMount() {
this.props.navigation.setOptions({
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},
headerTitleContainerStyle:
Platform.OS === 'ios'
? {marginHorizontal: 0, width: '70%'}
: {marginHorizontal: 0, right: 50, left: 50},
});
}
/**
* Gets the header search bar
* Callback used when clicking an article in the list.
* It opens the modal to show detailed information about the article
*
* @return {*}
* @param item The article pressed
*/
getSearchBar = () => {
return (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
};
/**
* Gets the header button
* @return {*}
*/
getHeaderButtons = () => {
const onPress = () => this.props.navigation.navigate("club-about");
return <MaterialHeaderButtons>
<Item title="main" iconName="information" onPress={onPress}/>
</MaterialHeaderButtons>;
};
onListItemPress(item: ClubType) {
const {props} = this;
props.navigation.navigate('club-information', {
data: item,
categories: this.categories,
});
}
/**
* Callback used when the search changes
@ -97,11 +90,46 @@ class ClubListScreen extends React.Component<Props, State> {
this.updateFilteredData(str, null);
};
keyExtractor = (item: club) => item.id.toString();
/**
* Gets the header search bar
*
* @return {*}
*/
getSearchBar = (): React.Node => {
return (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
};
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
onChipSelect = (id: number) => {
this.updateFilteredData(null, id);
};
getScreen = (data: Array<{ categories: Array<category>, clubs: Array<club> } | null>) => {
/**
* 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) {
@ -116,13 +144,69 @@ class ClubListScreen extends React.Component<Props, State> {
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true}
removeClippedSubviews
getItemLayout={this.itemLayout}
/>
)
);
};
onChipSelect = (id: number) => this.updateFilteredData(null, id);
/**
* 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.
@ -134,99 +218,49 @@ class ClubListScreen extends React.Component<Props, State> {
* @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;
const {state} = this;
const newCategoriesState = [...state.currentlySelectedCategories];
let newStrState = 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);
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 list header, with controls to change the categories filter
*
* @returns {*}
*/
getListHeader() {
return <ClubListHeader
categories={this.categories}
selectedCategories={this.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) => {
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: club) {
let shouldRender = this.state.currentlySelectedCategories.length === 0
|| isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category);
shouldRenderItem(item: ClubType): boolean {
const {state} = this;
let shouldRender =
state.currentlySelectedCategories.length === 0 ||
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
if (shouldRender)
shouldRender = stringMatchQuery(item.name, this.state.currentSearchString);
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
return shouldRender;
}
getRenderItem = ({item}: { item: club }) => {
const onPress = this.onListItemPress.bind(this, item);
if (this.shouldRenderItem(item)) {
return (
<ClubListItem
categoryTranslator={this.getCategoryOfId}
item={item}
onPress={onPress}
height={LIST_ITEM_HEIGHT}
/>
);
} 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() {
render(): React.Node {
const {props} = this;
return (
<AuthenticatedScreen
{...this.props}
navigation={props.navigation}
requests={[
{
link: 'clubs/list',
params: {},
mandatory: true,
}
},
]}
renderFunction={this.getScreen}
/>

View file

@ -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,7 +24,7 @@ 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) {
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<number>, categories: [number, number]) {
for (const category of categories) {
if (filter.indexOf(category) !== -1)
return true;
}
return false;
export function isItemInCategoryFilter(
filter: Array<number>,
categories: Array<number | null>,
): boolean {
let itemFound = false;
categories.forEach((cat: number | null) => {
if (cat != null && filter.indexOf(cat) !== -1) itemFound = true;
});
return itemFound;
}