forked from vergnet/application-amicale
Improve Clubs components to match linter
This commit is contained in:
parent
33d98b024b
commit
93d12b27f8
6 changed files with 705 additions and 627 deletions
|
@ -2,82 +2,90 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Card, Chip, List, Text} from 'react-native-paper';
|
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 i18n from 'i18n-js';
|
||||||
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
|
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||||
import {isItemInCategoryFilter} from "../../../utils/Search";
|
import {isItemInCategoryFilter} from '../../../utils/Search';
|
||||||
import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
|
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
categories: Array<category>,
|
categories: Array<ClubCategoryType>,
|
||||||
onChipSelect: (id: number) => void,
|
onChipSelect: (id: number) => void,
|
||||||
selectedCategories: Array<number>,
|
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({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
margin: 5
|
margin: 5,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
},
|
},
|
||||||
chipContainer: {
|
chipContainer: {
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
export default ClubListHeader;
|
||||||
|
|
|
@ -2,84 +2,93 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
|
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen";
|
import type {
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
ClubCategoryType,
|
||||||
|
ClubType,
|
||||||
|
} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||||
|
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
categoryTranslator: (id: number) => category,
|
categoryTranslator: (id: number) => ClubCategoryType,
|
||||||
item: club,
|
item: ClubType,
|
||||||
height: number,
|
height: number,
|
||||||
theme: CustomTheme,
|
theme: CustomTheme,
|
||||||
}
|
};
|
||||||
|
|
||||||
class ClubListItem extends React.Component<Props> {
|
class ClubListItem extends React.Component<PropsType> {
|
||||||
|
hasManagers: boolean;
|
||||||
|
|
||||||
hasManagers: boolean;
|
constructor(props: PropsType) {
|
||||||
|
super(props);
|
||||||
|
this.hasManagers = props.item.responsibles.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
shouldComponentUpdate(): boolean {
|
||||||
super(props);
|
return false;
|
||||||
this.hasManagers = props.item.responsibles.length > 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
getCategoriesRender(categories: Array<number | null>): React.Node {
|
||||||
return false;
|
const {props} = this;
|
||||||
}
|
const final = [];
|
||||||
|
categories.forEach((cat: number | null) => {
|
||||||
getCategoriesRender(categories: Array<number | null>) {
|
if (cat != null) {
|
||||||
let final = [];
|
const category: ClubCategoryType = props.categoryTranslator(cat);
|
||||||
for (let i = 0; i < categories.length; i++) {
|
final.push(
|
||||||
if (categories[i] !== null) {
|
<Chip
|
||||||
const category: category = this.props.categoryTranslator(categories[i]);
|
style={{marginRight: 5, marginBottom: 5}}
|
||||||
final.push(
|
key={`${props.item.id}:${category.id}`}>
|
||||||
<Chip
|
{category.name}
|
||||||
style={{marginRight: 5, marginBottom: 5}}
|
</Chip>,
|
||||||
key={this.props.item.id + ':' + category.id}
|
|
||||||
>
|
|
||||||
{category.name}
|
|
||||||
</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;
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
title={this.props.item.name}
|
|
||||||
description={categoriesRender}
|
|
||||||
onPress={this.props.onPress}
|
|
||||||
left={(props) => <Avatar.Image
|
|
||||||
{...props}
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
marginLeft: 10,
|
|
||||||
marginRight: 10,
|
|
||||||
}}
|
|
||||||
size={64}
|
|
||||||
source={{uri: this.props.item.logo}}/>}
|
|
||||||
right={(props) => <Avatar.Icon
|
|
||||||
{...props}
|
|
||||||
style={{
|
|
||||||
marginTop: 'auto',
|
|
||||||
marginBottom: 'auto',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
}}
|
|
||||||
size={48}
|
|
||||||
icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
|
|
||||||
color={this.hasManagers ? colors.success : colors.primary}
|
|
||||||
/>}
|
|
||||||
style={{
|
|
||||||
height: this.props.height,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return <View style={{flexDirection: 'row'}}>{final}</View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
|
const categoriesRender = (): React.Node =>
|
||||||
|
this.getCategoriesRender(props.item.category);
|
||||||
|
const {colors} = props.theme;
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
title={props.item.name}
|
||||||
|
description={categoriesRender}
|
||||||
|
onPress={props.onPress}
|
||||||
|
left={(): React.Node => (
|
||||||
|
<Avatar.Image
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
marginLeft: 10,
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
size={64}
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
color={this.hasManagers ? colors.success : colors.primary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
height: props.height,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(ClubListItem);
|
export default withTheme(ClubListItem);
|
||||||
|
|
|
@ -4,49 +4,49 @@ import * as React from 'react';
|
||||||
import {Image, View} from 'react-native';
|
import {Image, View} from 'react-native';
|
||||||
import {Card, List, Text, withTheme} from 'react-native-paper';
|
import {Card, List, Text, withTheme} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import Autolink from "react-native-autolink";
|
import Autolink from 'react-native-autolink';
|
||||||
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
import AMICALE_ICON from '../../../../assets/amicale.png';
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
const CONTACT_LINK = 'clubs@amicale-insat.fr';
|
const CONTACT_LINK = 'clubs@amicale-insat.fr';
|
||||||
|
|
||||||
class ClubAboutScreen extends React.Component<Props> {
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
|
class ClubAboutScreen extends React.Component<null> {
|
||||||
render() {
|
render(): React.Node {
|
||||||
return (
|
return (
|
||||||
<CollapsibleScrollView style={{padding: 5}}>
|
<CollapsibleScrollView style={{padding: 5}}>
|
||||||
<View style={{
|
<View
|
||||||
width: '100%',
|
style={{
|
||||||
height: 100,
|
width: '100%',
|
||||||
marginTop: 20,
|
height: 100,
|
||||||
marginBottom: 20,
|
marginTop: 20,
|
||||||
justifyContent: 'center',
|
marginBottom: 20,
|
||||||
alignItems: 'center'
|
justifyContent: 'center',
|
||||||
}}>
|
alignItems: 'center',
|
||||||
<Image
|
}}>
|
||||||
source={require('../../../../assets/amicale.png')}
|
<Image
|
||||||
style={{flex: 1, resizeMode: "contain"}}
|
source={AMICALE_ICON}
|
||||||
resizeMode="contain"/>
|
style={{flex: 1, resizeMode: 'contain'}}
|
||||||
</View>
|
resizeMode="contain"
|
||||||
<Text>{i18n.t("screens.clubs.about.text")}</Text>
|
/>
|
||||||
<Card style={{margin: 5}}>
|
</View>
|
||||||
<Card.Title
|
<Text>{i18n.t('screens.clubs.about.text')}</Text>
|
||||||
title={i18n.t("screens.clubs.about.title")}
|
<Card style={{margin: 5}}>
|
||||||
subtitle={i18n.t("screens.clubs.about.subtitle")}
|
<Card.Title
|
||||||
left={props => <List.Icon {...props} icon={'information'}/>}
|
title={i18n.t('screens.clubs.about.title')}
|
||||||
/>
|
subtitle={i18n.t('screens.clubs.about.subtitle')}
|
||||||
<Card.Content>
|
left={({size}: {size: number}): React.Node => (
|
||||||
<Text>{i18n.t("screens.clubs.about.message")}</Text>
|
<List.Icon size={size} icon="information" />
|
||||||
<Autolink
|
)}
|
||||||
text={CONTACT_LINK}
|
/>
|
||||||
component={Text}
|
<Card.Content>
|
||||||
/>
|
<Text>{i18n.t('screens.clubs.about.message')}</Text>
|
||||||
</Card.Content>
|
<Autolink text={CONTACT_LINK} component={Text} />
|
||||||
</Card>
|
</Card.Content>
|
||||||
</CollapsibleScrollView>
|
</Card>
|
||||||
);
|
</CollapsibleScrollView>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(ClubAboutScreen);
|
export default withTheme(ClubAboutScreen);
|
||||||
|
|
|
@ -2,252 +2,276 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Linking, View} from 'react-native';
|
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 ImageModal from 'react-native-image-modal';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import CustomHTML from "../../../components/Overrides/CustomHTML";
|
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
|
||||||
import CustomTabBar from "../../../components/Tabbar/CustomTabBar";
|
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||||
import type {category, club} from "./ClubListScreen";
|
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {ClubCategoryType, ClubType} from './ClubListScreen';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import type {CustomTheme} from '../../../managers/ThemeManager';
|
||||||
import {ERROR_TYPE} from "../../../utils/WebData";
|
import {ERROR_TYPE} from '../../../utils/WebData';
|
||||||
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
import type {ApiGenericDataType} from '../../../utils/WebData';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
route: {
|
route: {
|
||||||
params?: {
|
params?: {
|
||||||
data?: club,
|
data?: ClubType,
|
||||||
categories?: Array<category>,
|
categories?: Array<ClubCategoryType>,
|
||||||
clubId?: number,
|
clubId?: number,
|
||||||
}, ...
|
|
||||||
},
|
},
|
||||||
theme: CustomTheme
|
...
|
||||||
|
},
|
||||||
|
theme: CustomTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
const AMICALE_MAIL = 'clubs@amicale-insat.fr';
|
||||||
imageModalVisible: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AMICALE_MAIL = "clubs@amicale-insat.fr";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class defining a club event information page.
|
* 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 data and categories navigation parameters, will use those to display the data.
|
||||||
* If called with clubId parameter, will fetch the information on the server
|
* 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;
|
||||||
|
|
||||||
displayData: club | null;
|
categories: Array<ClubCategoryType> | null;
|
||||||
categories: Array<category> | null;
|
|
||||||
clubId: number;
|
|
||||||
|
|
||||||
shouldFetchData: boolean;
|
clubId: number;
|
||||||
|
|
||||||
state = {
|
shouldFetchData: boolean;
|
||||||
imageModalVisible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
if (this.props.route.params != null) {
|
if (props.route.params != null) {
|
||||||
if (this.props.route.params.data != null && this.props.route.params.categories != null) {
|
if (
|
||||||
this.displayData = this.props.route.params.data;
|
props.route.params.data != null &&
|
||||||
this.categories = this.props.route.params.categories;
|
props.route.params.categories != null
|
||||||
this.clubId = this.props.route.params.data.id;
|
) {
|
||||||
this.shouldFetchData = false;
|
this.displayData = props.route.params.data;
|
||||||
} else if (this.props.route.params.clubId != null) {
|
this.categories = props.route.params.categories;
|
||||||
this.displayData = null;
|
this.clubId = props.route.params.data.id;
|
||||||
this.categories = null;
|
this.shouldFetchData = false;
|
||||||
this.clubId = this.props.route.params.clubId;
|
} else if (props.route.params.clubId != null) {
|
||||||
this.shouldFetchData = true;
|
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
|
* Gets the name of the category with the given ID
|
||||||
*
|
*
|
||||||
* @param id The category's ID
|
* @param id The category's ID
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
*/
|
*/
|
||||||
getCategoryName(id: number) {
|
getCategoryName(id: number): string {
|
||||||
if (this.categories !== null) {
|
let categoryName = '';
|
||||||
for (let i = 0; i < this.categories.length; i++) {
|
if (this.categories !== null) {
|
||||||
if (id === this.categories[i].id)
|
this.categories.forEach((item: ClubCategoryType) => {
|
||||||
return this.categories[i].name;
|
if (id === item.id) categoryName = item.name;
|
||||||
}
|
});
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
return categoryName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the view for rendering categories
|
* Gets the view for rendering categories
|
||||||
*
|
*
|
||||||
* @param categories The categories to display (max 2)
|
* @param categories The categories to display (max 2)
|
||||||
* @returns {null|*}
|
* @returns {null|*}
|
||||||
*/
|
*/
|
||||||
getCategoriesRender(categories: [number, number]) {
|
getCategoriesRender(categories: Array<number | null>): React.Node {
|
||||||
if (this.categories === null)
|
if (this.categories == null) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
let final = [];
|
const final = [];
|
||||||
for (let i = 0; i < categories.length; i++) {
|
categories.forEach((cat: number | null) => {
|
||||||
let cat = categories[i];
|
if (cat != null) {
|
||||||
if (cat !== null) {
|
final.push(
|
||||||
final.push(
|
<Chip style={{marginRight: 5}} key={cat}>
|
||||||
<Chip
|
{this.getCategoryName(cat)}
|
||||||
style={{marginRight: 5}}
|
</Chip>,
|
||||||
key={i.toString()}>
|
|
||||||
{this.getCategoryName(cat)}
|
|
||||||
</Chip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<string>, email: string | null) {
|
|
||||||
let managersListView = [];
|
|
||||||
for (let i = 0; i < managers.length; i++) {
|
|
||||||
managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
|
|
||||||
}
|
|
||||||
const hasManagers = managers.length > 0;
|
|
||||||
return (
|
|
||||||
<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}
|
|
||||||
style={{backgroundColor: 'transparent'}}
|
|
||||||
color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
|
|
||||||
icon="account-tie"/>}
|
|
||||||
/>
|
|
||||||
<Card.Content>
|
|
||||||
{managersListView}
|
|
||||||
{this.getEmailButton(email, hasManagers)}
|
|
||||||
</Card.Content>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<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.Title
|
||||||
|
title={i18n.t('screens.clubs.managers')}
|
||||||
|
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
|
||||||
|
? props.theme.colors.success
|
||||||
|
: props.theme.colors.primary
|
||||||
|
}
|
||||||
|
icon="account-tie"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
{managersListView}
|
||||||
|
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Card.Actions>
|
||||||
|
<Button
|
||||||
|
icon="email"
|
||||||
|
mode="contained"
|
||||||
|
onPress={() => {
|
||||||
|
Linking.openURL(`mailto:${destinationEmail}`);
|
||||||
|
}}
|
||||||
|
style={{marginLeft: 'auto'}}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => {
|
||||||
|
const {props} = this;
|
||||||
|
let data: ClubType | null = null;
|
||||||
|
if (response[0] != null) {
|
||||||
|
[data] = response;
|
||||||
|
this.updateHeaderTitle(data);
|
||||||
}
|
}
|
||||||
|
if (data != null) {
|
||||||
|
return (
|
||||||
|
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
|
||||||
|
{this.getCategoriesRender(data.category)}
|
||||||
|
{data.logo !== null ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
marginTop: 10,
|
||||||
|
marginBottom: 10,
|
||||||
|
}}>
|
||||||
|
<ImageModal
|
||||||
|
resizeMode="contain"
|
||||||
|
imageBackgroundColor={props.theme.colors.background}
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
}}
|
||||||
|
source={{
|
||||||
|
uri: data.logo,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View />
|
||||||
|
)}
|
||||||
|
|
||||||
/**
|
{data.description !== null ? (
|
||||||
* Gets the email button to contact the club, or the amicale if the club does not have any managers
|
// Surround description with div to allow text styling if the description is not html
|
||||||
*
|
<Card.Content>
|
||||||
* @param email The club contact email
|
<CustomHTML html={data.description} />
|
||||||
* @param hasManagers True if the club has managers
|
</Card.Content>
|
||||||
* @returns {*}
|
) : (
|
||||||
*/
|
<View />
|
||||||
getEmailButton(email: string | null, hasManagers: boolean) {
|
)}
|
||||||
const destinationEmail = email != null && hasManagers
|
{this.getManagersRender(data.responsibles, data.email)}
|
||||||
? email
|
</CollapsibleScrollView>
|
||||||
: 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'}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
</Card.Actions>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the header title to match the given club
|
* Updates the header title to match the given club
|
||||||
*
|
*
|
||||||
* @param data The club data
|
* @param data The club data
|
||||||
*/
|
*/
|
||||||
updateHeaderTitle(data: club) {
|
updateHeaderTitle(data: ClubType) {
|
||||||
this.props.navigation.setOptions({title: data.name})
|
const {props} = this;
|
||||||
}
|
props.navigation.setOptions({title: data.name});
|
||||||
|
}
|
||||||
|
|
||||||
getScreen = (response: Array<{ [key: string]: any } | null>) => {
|
render(): React.Node {
|
||||||
let data: club | null = null;
|
const {props} = this;
|
||||||
if (response[0] != null) {
|
if (this.shouldFetchData)
|
||||||
data = response[0];
|
return (
|
||||||
this.updateHeaderTitle(data);
|
<AuthenticatedScreen
|
||||||
}
|
navigation={props.navigation}
|
||||||
if (data != null) {
|
requests={[
|
||||||
return (
|
{
|
||||||
<CollapsibleScrollView
|
link: 'clubs/info',
|
||||||
style={{paddingLeft: 5, paddingRight: 5}}
|
params: {id: this.clubId},
|
||||||
hasTab={true}
|
mandatory: true,
|
||||||
>
|
},
|
||||||
{this.getCategoriesRender(data.category)}
|
]}
|
||||||
{data.logo !== null ?
|
renderFunction={this.getScreen}
|
||||||
<View style={{
|
errorViewOverride={[
|
||||||
marginLeft: 'auto',
|
{
|
||||||
marginRight: 'auto',
|
errorCode: ERROR_TYPE.BAD_INPUT,
|
||||||
marginTop: 10,
|
message: i18n.t('screens.clubs.invalidClub'),
|
||||||
marginBottom: 10,
|
icon: 'account-question',
|
||||||
}}>
|
showRetryButton: false,
|
||||||
<ImageModal
|
},
|
||||||
resizeMode="contain"
|
]}
|
||||||
imageBackgroundColor={this.props.theme.colors.background}
|
/>
|
||||||
style={{
|
);
|
||||||
width: 300,
|
return this.getScreen([this.displayData]);
|
||||||
height: 300,
|
}
|
||||||
}}
|
|
||||||
source={{
|
|
||||||
uri: data.logo,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
: <View/>}
|
|
||||||
|
|
||||||
{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/>}
|
|
||||||
{this.getManagersRender(data.responsibles, data.email)}
|
|
||||||
</CollapsibleScrollView>
|
|
||||||
);
|
|
||||||
} else
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.shouldFetchData)
|
|
||||||
return <AuthenticatedScreen
|
|
||||||
{...this.props}
|
|
||||||
requests={[
|
|
||||||
{
|
|
||||||
link: 'clubs/info',
|
|
||||||
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
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>;
|
|
||||||
else
|
|
||||||
return this.getScreen([this.displayData]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(ClubDisplayScreen);
|
export default withTheme(ClubDisplayScreen);
|
||||||
|
|
|
@ -1,237 +1,271 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Platform} from "react-native";
|
import {Platform} from 'react-native';
|
||||||
import {Searchbar} from 'react-native-paper';
|
import {Searchbar} from 'react-native-paper';
|
||||||
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
|
import i18n from 'i18n-js';
|
||||||
import i18n from "i18n-js";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import ClubListItem from "../../../components/Lists/Clubs/ClubListItem";
|
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
|
||||||
import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search";
|
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
|
||||||
import ClubListHeader from "../../../components/Lists/Clubs/ClubListHeader";
|
import {isItemInCategoryFilter, stringMatchQuery} from '../../../utils/Search';
|
||||||
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
|
import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import MaterialHeaderButtons, {
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
Item,
|
||||||
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList";
|
} from '../../../components/Overrides/CustomHeaderButton';
|
||||||
|
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
|
||||||
export type category = {
|
export type ClubCategoryType = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type club = {
|
export type ClubType = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
logo: string,
|
logo: string,
|
||||||
email: string | null,
|
email: string | null,
|
||||||
category: [number, number],
|
category: Array<number | null>,
|
||||||
responsibles: Array<string>,
|
responsibles: Array<string>,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
currentlySelectedCategories: Array<number>,
|
currentlySelectedCategories: Array<number>,
|
||||||
currentSearchString: string,
|
currentSearchString: string,
|
||||||
}
|
};
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 96;
|
const LIST_ITEM_HEIGHT = 96;
|
||||||
|
|
||||||
class ClubListScreen extends React.Component<Props, State> {
|
class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
|
categories: Array<ClubCategoryType>;
|
||||||
|
|
||||||
state = {
|
constructor() {
|
||||||
currentlySelectedCategories: [],
|
super();
|
||||||
currentSearchString: '',
|
this.state = {
|
||||||
|
currentlySelectedCategories: [],
|
||||||
|
currentSearchString: '',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
categories: Array<category>;
|
/**
|
||||||
|
* 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
|
* Callback used when clicking an article in the list.
|
||||||
*/
|
* It opens the modal to show detailed information about the article
|
||||||
componentDidMount() {
|
*
|
||||||
this.props.navigation.setOptions({
|
* @param item The article pressed
|
||||||
headerTitle: this.getSearchBar,
|
*/
|
||||||
headerRight: this.getHeaderButtons,
|
onListItemPress(item: ClubType) {
|
||||||
headerBackTitleVisible: false,
|
const {props} = this;
|
||||||
headerTitleContainerStyle: Platform.OS === 'ios' ?
|
props.navigation.navigate('club-information', {
|
||||||
{marginHorizontal: 0, width: '70%'} :
|
data: item,
|
||||||
{marginHorizontal: 0, right: 50, left: 50},
|
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 header search bar
|
* Gets the list header, with controls to change the categories filter
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getSearchBar = () => {
|
getListHeader(): React.Node {
|
||||||
return (
|
const {state} = this;
|
||||||
<Searchbar
|
return (
|
||||||
placeholder={i18n.t('screens.proximo.search')}
|
<ClubListHeader
|
||||||
onChangeText={this.onSearchStringChange}
|
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 (
|
||||||
* Gets the header button
|
<ClubListItem
|
||||||
* @return {*}
|
categoryTranslator={this.getCategoryOfId}
|
||||||
*/
|
item={item}
|
||||||
getHeaderButtons = () => {
|
onPress={onPress}
|
||||||
const onPress = () => this.props.navigation.navigate("club-about");
|
height={LIST_ITEM_HEIGHT}
|
||||||
return <MaterialHeaderButtons>
|
/>
|
||||||
<Item title="main" iconName="information" onPress={onPress}/>
|
);
|
||||||
</MaterialHeaderButtons>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<category>, clubs: Array<club> } | null>) => {
|
|
||||||
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={true}
|
|
||||||
getItemLayout={this.itemLayout}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
keyExtractor = (item: ClubType): string => item.id.toString();
|
||||||
* Gets the list header, with controls to change the categories filter
|
|
||||||
*
|
itemLayout = (
|
||||||
* @returns {*}
|
data: {...},
|
||||||
*/
|
index: number,
|
||||||
getListHeader() {
|
): {length: number, offset: number, index: number} => ({
|
||||||
return <ClubListHeader
|
length: LIST_ITEM_HEIGHT,
|
||||||
categories={this.categories}
|
offset: LIST_ITEM_HEIGHT * index,
|
||||||
selectedCategories={this.state.currentlySelectedCategories}
|
index,
|
||||||
onChipSelect={this.onChipSelect}
|
});
|
||||||
/>;
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Checks if the given item should be rendered according to current name and category filters
|
||||||
*
|
*
|
||||||
* @param id The ID of the category to find
|
* @param item The club to check
|
||||||
* @returns {*}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
getCategoryOfId = (id: number) => {
|
shouldRenderItem(item: ClubType): boolean {
|
||||||
for (let i = 0; i < this.categories.length; i++) {
|
const {state} = this;
|
||||||
if (id === this.categories[i].id)
|
let shouldRender =
|
||||||
return this.categories[i];
|
state.currentlySelectedCategories.length === 0 ||
|
||||||
}
|
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
|
||||||
};
|
if (shouldRender)
|
||||||
|
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
|
||||||
|
return shouldRender;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
render(): React.Node {
|
||||||
* Checks if the given item should be rendered according to current name and category filters
|
const {props} = this;
|
||||||
*
|
return (
|
||||||
* @param item The club to check
|
<AuthenticatedScreen
|
||||||
* @returns {boolean}
|
navigation={props.navigation}
|
||||||
*/
|
requests={[
|
||||||
shouldRenderItem(item: club) {
|
{
|
||||||
let shouldRender = this.state.currentlySelectedCategories.length === 0
|
link: 'clubs/list',
|
||||||
|| isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category);
|
params: {},
|
||||||
if (shouldRender)
|
mandatory: true,
|
||||||
shouldRender = stringMatchQuery(item.name, this.state.currentSearchString);
|
},
|
||||||
return shouldRender;
|
]}
|
||||||
}
|
renderFunction={this.getScreen}
|
||||||
|
/>
|
||||||
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() {
|
|
||||||
return (
|
|
||||||
<AuthenticatedScreen
|
|
||||||
{...this.props}
|
|
||||||
requests={[
|
|
||||||
{
|
|
||||||
link: 'clubs/list',
|
|
||||||
params: {},
|
|
||||||
mandatory: true,
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
renderFunction={this.getScreen}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClubListScreen;
|
export default ClubListScreen;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes the given string to improve search performance.
|
* Sanitizes the given string to improve search performance.
|
||||||
*
|
*
|
||||||
|
@ -10,11 +9,12 @@
|
||||||
* @return {string} The sanitized string
|
* @return {string} The sanitized string
|
||||||
*/
|
*/
|
||||||
export function sanitizeString(str: string): string {
|
export function sanitizeString(str: string): string {
|
||||||
return str.toLowerCase()
|
return str
|
||||||
.normalize("NFD")
|
.toLowerCase()
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
.normalize('NFD')
|
||||||
.replace(/ /g, "")
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
.replace(/_/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
|
* @param query The query string used to find a match
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function stringMatchQuery(str: string, query: string) {
|
export function stringMatchQuery(str: string, query: string): boolean {
|
||||||
return sanitizeString(str).includes(sanitizeString(query));
|
return sanitizeString(str).includes(sanitizeString(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,10 +35,13 @@ export function stringMatchQuery(str: string, query: string) {
|
||||||
* @param categories The item's categories tuple
|
* @param categories The item's categories tuple
|
||||||
* @returns {boolean} True if at least one entry is in both arrays
|
* @returns {boolean} True if at least one entry is in both arrays
|
||||||
*/
|
*/
|
||||||
export function isItemInCategoryFilter(filter: Array<number>, categories: [number, number]) {
|
export function isItemInCategoryFilter(
|
||||||
for (const category of categories) {
|
filter: Array<number>,
|
||||||
if (filter.indexOf(category) !== -1)
|
categories: Array<number | null>,
|
||||||
return true;
|
): boolean {
|
||||||
}
|
let itemFound = false;
|
||||||
return false;
|
categories.forEach((cat: number | null) => {
|
||||||
|
if (cat != null && filter.indexOf(cat) !== -1) itemFound = true;
|
||||||
|
});
|
||||||
|
return itemFound;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue