Update list components to use TypeScript

This commit is contained in:
Arnaud Vergnet 2020-09-22 15:06:38 +02:00
parent acc4f8cdcc
commit e4adcd0057
21 changed files with 648 additions and 705 deletions

View file

@ -17,19 +17,16 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Animated, Dimensions} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import {Animated, Dimensions, ViewStyle} from 'react-native';
import ImageListItem from './ImageListItem';
import CardListItem from './CardListItem';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
dataset: Array<ServiceItemType>,
isHorizontal?: boolean,
contentContainerStyle?: ViewStyle | null,
dataset: Array<ServiceItemType>;
isHorizontal?: boolean;
contentContainerStyle?: ViewStyle;
};
export default class CardList extends React.Component<PropsType> {
@ -45,12 +42,12 @@ export default class CardList extends React.Component<PropsType> {
constructor(props: PropsType) {
super(props);
this.windowWidth = Dimensions.get('window').width;
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items, and a part of the 4th => user knows he can scroll
}
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
getRenderItem = ({item}: {item: ServiceItemType}) => {
const {props} = this;
if (props.isHorizontal)
if (props.isHorizontal) {
return (
<ImageListItem
item={item}
@ -58,12 +55,13 @@ export default class CardList extends React.Component<PropsType> {
width={this.horizontalItemSize}
/>
);
}
return <CardListItem item={item} key={item.title} />;
};
keyExtractor = (item: ServiceItemType): string => item.key;
render(): React.Node {
render() {
const {props} = this;
let containerStyle = {};
if (props.isHorizontal) {
@ -84,7 +82,7 @@ export default class CardList extends React.Component<PropsType> {
}
pagingEnabled={props.isHorizontal}
snapToInterval={
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : undefined
}
/>
);

View file

@ -17,24 +17,16 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType,
item: ServiceItemType;
};
export default class CardListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
function CardListItem(props: PropsType) {
const {item} = props;
const source =
typeof item.image === 'number' ? item.image : {uri: item.image};
@ -58,4 +50,5 @@ export default class CardListItem extends React.Component<PropsType> {
</Card>
);
}
}
export default React.memo(CardListItem, () => true);

View file

@ -1,73 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType,
width: number,
};
export default class ImageListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
const {item} = props;
const source =
typeof item.image === 'number' ? item.image : {uri: item.image};
return (
<TouchableRipple
style={{
width: props.width,
height: props.width + 40,
margin: 5,
}}
onPress={item.onPress}>
<View>
<Image
style={{
width: props.width - 20,
height: props.width - 20,
marginLeft: 'auto',
marginRight: 'auto',
}}
source={source}
/>
<Text
style={{
marginTop: 5,
marginLeft: 'auto',
marginRight: 'auto',
textAlign: 'center',
}}>
{item.title}
</Text>
</View>
</TouchableRipple>
);
}
}

View file

@ -0,0 +1,66 @@
/*
* 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 {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType;
width: number;
};
function ImageListItem(props: PropsType) {
const {item} = props;
const source =
typeof item.image === 'number' ? item.image : {uri: item.image};
return (
<TouchableRipple
style={{
width: props.width,
height: props.width + 40,
margin: 5,
}}
onPress={item.onPress}>
<View>
<Image
style={{
width: props.width - 20,
height: props.width - 20,
marginLeft: 'auto',
marginRight: 'auto',
}}
source={source}
/>
<Text
style={{
marginTop: 5,
marginLeft: 'auto',
marginRight: 'auto',
textAlign: 'center',
}}>
{item.title}
</Text>
</View>
</TouchableRipple>
);
}
export default React.memo(ImageListItem, () => true);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Card, Chip, List, Text} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
@ -26,12 +24,11 @@ import i18n from 'i18n-js';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import {isItemInCategoryFilter} from '../../../utils/Search';
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
categories: Array<ClubCategoryType>,
onChipSelect: (id: number) => void,
selectedCategories: Array<number>,
categories: Array<ClubCategoryType>;
onChipSelect: (id: number) => void;
selectedCategories: Array<number>;
};
const styles = StyleSheet.create({
@ -54,16 +51,8 @@ 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;
function ClubListHeader(props: PropsType) {
const getChipRender = (category: ClubCategoryType, key: string) => {
const onPress = (): void => props.onChipSelect(category.id);
return (
<Chip
@ -80,32 +69,39 @@ class ClubListHeader extends React.Component<PropsType> {
);
};
getCategoriesRender(): React.Node {
const {props} = this;
const final = [];
const getCategoriesRender = () => {
const final: Array<React.ReactNode> = [];
props.categories.forEach((cat: ClubCategoryType) => {
final.push(this.getChipRender(cat, cat.id.toString()));
final.push(getChipRender(cat, cat.id.toString()));
});
return final;
}
};
render(): React.Node {
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={(props: ListIconPropsType): React.Node => (
<List.Icon color={props.color} style={props.style} icon="star" />
left={(iconProps) => (
<List.Icon
color={iconProps.color}
style={iconProps.style}
icon="star"
/>
)}
opened>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
<View style={styles.chipContainer}>{getCategoriesRender()}</View>
</AnimatedAccordion>
</Card>
);
}
}
export default ClubListHeader;
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return (
prevProps.selectedCategories.length === nextProps.selectedCategories.length
);
};
export default React.memo(ClubListHeader, areEqual);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
import {View} from 'react-native';
@ -26,14 +24,13 @@ import type {
ClubCategoryType,
ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
onPress: () => void,
categoryTranslator: (id: number) => ClubCategoryType,
item: ClubType,
height: number,
theme: CustomThemeType,
onPress: () => void;
categoryTranslator: (id: number) => ClubCategoryType;
item: ClubType;
height: number;
theme: ReactNativePaper.Theme;
};
class ClubListItem extends React.Component<PropsType> {
@ -48,9 +45,9 @@ class ClubListItem extends React.Component<PropsType> {
return false;
}
getCategoriesRender(categories: Array<number | null>): React.Node {
getCategoriesRender(categories: Array<number | null>) {
const {props} = this;
const final = [];
const final: Array<React.ReactNode> = [];
categories.forEach((cat: number | null) => {
if (cat != null) {
const category: ClubCategoryType = props.categoryTranslator(cat);
@ -66,9 +63,9 @@ class ClubListItem extends React.Component<PropsType> {
return <View style={{flexDirection: 'row'}}>{final}</View>;
}
render(): React.Node {
render() {
const {props} = this;
const categoriesRender = (): React.Node =>
const categoriesRender = () =>
this.getCategoriesRender(props.item.category);
const {colors} = props.theme;
return (
@ -76,7 +73,7 @@ class ClubListItem extends React.Component<PropsType> {
title={props.item.name}
description={categoriesRender}
onPress={props.onPress}
left={(): React.Node => (
left={() => (
<Avatar.Image
style={{
backgroundColor: 'transparent',
@ -87,7 +84,7 @@ class ClubListItem extends React.Component<PropsType> {
source={{uri: props.item.logo}}
/>
)}
right={(): React.Node => (
right={() => (
<Avatar.Icon
style={{
marginTop: 'auto',

View file

@ -1,108 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {withTheme} from 'react-native-paper';
import {FlatList, Image, View} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardEditItem from './DashboardEditItem';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import type {
ServiceCategoryType,
ServiceItemType,
} from '../../../managers/ServicesManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
item: ServiceCategoryType,
activeDashboard: Array<string>,
onPress: (service: ServiceItemType) => void,
theme: CustomThemeType,
};
const LIST_ITEM_HEIGHT = 64;
class DashboardEditAccordion extends React.Component<PropsType> {
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
const {props} = this;
return (
<DashboardEditItem
height={LIST_ITEM_HEIGHT}
item={item}
isActive={props.activeDashboard.includes(item.key)}
onPress={() => {
props.onPress(item);
}}
/>
);
};
getItemLayout = (
data: ?Array<ServiceItemType>,
index: number,
): {length: number, offset: number, index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
render(): React.Node {
const {props} = this;
const {item} = props;
return (
<View>
<AnimatedAccordion
title={item.title}
left={(): React.Node =>
typeof item.image === 'number' ? (
<Image
source={item.image}
style={{
width: 40,
height: 40,
}}
/>
) : (
<MaterialCommunityIcons
// $FlowFixMe
name={item.image}
color={props.theme.colors.primary}
size={40}
/>
)
}>
{/* $FlowFixMe */}
<FlatList
data={item.content}
extraData={props.activeDashboard.toString()}
renderItem={this.getRenderItem}
listKey={item.key}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
getItemLayout={this.getItemLayout}
removeClippedSubviews
/>
</AnimatedAccordion>
</View>
);
}
}
export default withTheme(DashboardEditAccordion);

View file

@ -0,0 +1,100 @@
/*
* 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 {useTheme} from 'react-native-paper';
import {FlatList, Image, View} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardEditItem from './DashboardEditItem';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import type {
ServiceCategoryType,
ServiceItemType,
} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceCategoryType;
activeDashboard: Array<string>;
onPress: (service: ServiceItemType) => void;
};
const LIST_ITEM_HEIGHT = 64;
function DashboardEditAccordion(props: PropsType) {
const theme = useTheme();
const getRenderItem = ({item}: {item: ServiceItemType}) => {
return (
<DashboardEditItem
height={LIST_ITEM_HEIGHT}
item={item}
isActive={props.activeDashboard.includes(item.key)}
onPress={() => {
props.onPress(item);
}}
/>
);
};
const getItemLayout = (
data: Array<ServiceItemType> | null | undefined,
index: number,
): {length: number; offset: number; index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
const {item} = props;
return (
<View>
<AnimatedAccordion
title={item.title}
left={() =>
typeof item.image === 'number' ? (
<Image
source={item.image}
style={{
width: 40,
height: 40,
}}
/>
) : (
<MaterialCommunityIcons
name={item.image}
color={theme.colors.primary}
size={40}
/>
)
}>
<FlatList
data={item.content}
extraData={props.activeDashboard.toString()}
renderItem={getRenderItem}
listKey={item.key}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
getItemLayout={getItemLayout}
removeClippedSubviews
/>
</AnimatedAccordion>
</View>
);
}
export default DashboardEditAccordion;

View file

@ -1,81 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Image} from 'react-native';
import {List, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ServiceItemType} from '../../../managers/ServicesManager';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
item: ServiceItemType,
isActive: boolean,
height: number,
onPress: () => void,
theme: CustomThemeType,
};
class DashboardEditItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {isActive} = this.props;
return nextProps.isActive !== isActive;
}
render(): React.Node {
const {item, onPress, height, isActive, theme} = this.props;
return (
<List.Item
title={item.title}
description={item.subtitle}
onPress={isActive ? null : onPress}
left={(): React.Node => (
<Image
source={{uri: item.image}}
style={{
width: 40,
height: 40,
}}
/>
)}
right={(props: ListIconPropsType): React.Node =>
isActive ? (
<List.Icon
style={props.style}
icon="check"
color={theme.colors.success}
/>
) : null
}
style={{
height,
justifyContent: 'center',
paddingLeft: 30,
backgroundColor: isActive
? theme.colors.proxiwashFinishedColor
: 'transparent',
}}
/>
);
}
}
export default withTheme(DashboardEditItem);

View file

@ -0,0 +1,76 @@
/*
* 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 {Image} from 'react-native';
import {List, useTheme} from 'react-native-paper';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType;
isActive: boolean;
height: number;
onPress: () => void;
};
function DashboardEditItem(props: PropsType) {
const theme = useTheme();
const {item, onPress, height, isActive} = props;
return (
<List.Item
title={item.title}
description={item.subtitle}
onPress={isActive ? undefined : onPress}
left={() => (
<Image
source={
typeof item.image === 'string' ? {uri: item.image} : item.image
}
style={{
width: 40,
height: 40,
}}
/>
)}
right={(iconProps) =>
isActive ? (
<List.Icon
style={iconProps.style}
icon="check"
color={theme.colors.success}
/>
) : null
}
style={{
height,
justifyContent: 'center',
paddingLeft: 30,
backgroundColor: isActive
? theme.colors.proxiwashFinishedColor
: 'transparent',
}}
/>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.isActive !== prevProps.isActive;
};
export default React.memo(DashboardEditItem, areEqual);

View file

@ -1,77 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
image: string,
isActive: boolean,
onPress: () => void,
theme: CustomThemeType,
};
/**
* Component used to render a small dashboard item
*/
class DashboardEditPreviewItem extends React.Component<PropsType> {
itemSize: number;
constructor(props: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
render(): React.Node {
const {props} = this;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: props.isActive
? props.theme.colors.textDisabled
: 'transparent',
borderRadius: 5,
}}>
<View
style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: '100%',
height: '100%',
}}
/>
</View>
</TouchableRipple>
);
}
}
export default withTheme(DashboardEditPreviewItem);

View file

@ -0,0 +1,66 @@
/*
* 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 {TouchableRipple, useTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native';
type PropsType = {
image: string;
isActive: boolean;
onPress: () => void;
};
/**
* Component used to render a small dashboard item
*/
function DashboardEditPreviewItem(props: PropsType) {
const theme = useTheme();
const itemSize = Dimensions.get('window').width / 8;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: props.isActive
? theme.colors.textDisabled
: 'transparent',
borderRadius: 5,
}}>
<View
style={{
width: itemSize,
height: itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: '100%',
height: '100%',
}}
/>
</View>
</TouchableRipple>
);
}
export default DashboardEditPreviewItem;

View file

@ -1,132 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, List, withTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import {
getFirstEquipmentAvailability,
getRelativeDateString,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
type PropsType = {
navigation: StackNavigationProp,
userDeviceRentDates: [string, string],
item: DeviceType,
height: number,
theme: CustomThemeType,
};
class EquipmentListItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {userDeviceRentDates} = this.props;
return nextProps.userDeviceRentDates !== userDeviceRentDates;
}
render(): React.Node {
const {item, userDeviceRentDates, navigation, height, theme} = this.props;
const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
let onPress;
if (isRented)
onPress = () => {
navigation.navigate('equipment-confirm', {
item,
dates: userDeviceRentDates,
});
};
else
onPress = () => {
navigation.navigate('equipment-rent', {item});
};
let description;
if (isRented) {
const start = new Date(userDeviceRentDates[0]);
const end = new Date(userDeviceRentDates[1]);
if (start.getTime() !== end.getTime())
description = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end),
});
else
description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start),
});
} else if (isAvailable)
description = i18n.t('screens.equipment.bail', {cost: item.caution});
else
description = i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
});
let icon;
if (isRented) icon = 'bookmark-check';
else if (isAvailable) icon = 'check-circle-outline';
else icon = 'update';
let color;
if (isRented) color = theme.colors.warning;
else if (isAvailable) color = theme.colors.success;
else color = theme.colors.primary;
return (
<List.Item
title={item.name}
description={description}
onPress={onPress}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{
backgroundColor: 'transparent',
}}
icon={icon}
color={color}
/>
)}
right={(): React.Node => (
<Avatar.Icon
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon="chevron-right"
/>
)}
style={{
height,
justifyContent: 'center',
}}
/>
);
}
}
export default withTheme(EquipmentListItem);

View file

@ -0,0 +1,136 @@
/*
* 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 {Avatar, List, useTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import {
getFirstEquipmentAvailability,
getRelativeDateString,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
type PropsType = {
navigation: StackNavigationProp<any>;
userDeviceRentDates: [string, string];
item: DeviceType;
height: number;
};
function EquipmentListItem(props: PropsType) {
const theme = useTheme();
const {item, userDeviceRentDates, navigation, height} = props;
const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
let onPress;
if (isRented) {
onPress = () => {
navigation.navigate('equipment-confirm', {
item,
dates: userDeviceRentDates,
});
};
} else {
onPress = () => {
navigation.navigate('equipment-rent', {item});
};
}
let description;
if (isRented) {
const start = new Date(userDeviceRentDates[0]);
const end = new Date(userDeviceRentDates[1]);
if (start.getTime() !== end.getTime()) {
description = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end),
});
} else {
description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start),
});
}
} else if (isAvailable) {
description = i18n.t('screens.equipment.bail', {cost: item.caution});
} else {
description = i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
});
}
let icon: string;
if (isRented) {
icon = 'bookmark-check';
} else if (isAvailable) {
icon = 'check-circle-outline';
} else {
icon = 'update';
}
let color: string;
if (isRented) {
color = theme.colors.warning;
} else if (isAvailable) {
color = theme.colors.success;
} else {
color = theme.colors.primary;
}
return (
<List.Item
title={item.name}
description={description}
onPress={onPress}
left={() => (
<Avatar.Icon
style={{
backgroundColor: 'transparent',
}}
icon={icon}
color={color}
/>
)}
right={() => (
<Avatar.Icon
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon="chevron-right"
/>
)}
style={{
height,
justifyContent: 'center',
}}
/>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.userDeviceRentDates !== prevProps.userDeviceRentDates;
};
export default React.memo(EquipmentListItem, areEqual);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {List, withTheme} from 'react-native-paper';
import {FlatList, View} from 'react-native';
@ -29,17 +27,15 @@ import type {
PlanexGroupType,
PlanexGroupCategoryType,
} from '../../../screens/Planex/GroupSelectionScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
item: PlanexGroupCategoryType,
favorites: Array<PlanexGroupType>,
onGroupPress: (PlanexGroupType) => void,
onFavoritePress: (PlanexGroupType) => void,
currentSearchString: string,
height: number,
theme: CustomThemeType,
item: PlanexGroupCategoryType;
favorites: Array<PlanexGroupType>;
onGroupPress: (data: PlanexGroupType) => void;
onFavoritePress: (data: PlanexGroupType) => void;
currentSearchString: string;
height: number;
theme: ReactNativePaper.Theme;
};
const LIST_ITEM_HEIGHT = 64;
@ -55,7 +51,7 @@ class GroupListAccordion extends React.Component<PropsType> {
);
}
getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => {
getRenderItem = ({item}: {item: PlanexGroupType}) => {
const {props} = this;
const onPress = () => {
props.onGroupPress(item);
@ -77,18 +73,19 @@ class GroupListAccordion extends React.Component<PropsType> {
getData(): Array<PlanexGroupType> {
const {props} = this;
const originalData = props.item.content;
const displayData = [];
const displayData: Array<PlanexGroupType> = [];
originalData.forEach((data: PlanexGroupType) => {
if (stringMatchQuery(data.name, props.currentSearchString))
if (stringMatchQuery(data.name, props.currentSearchString)) {
displayData.push(data);
}
});
return displayData;
}
itemLayout = (
data: ?Array<PlanexGroupType>,
data: Array<PlanexGroupType> | null | undefined,
index: number,
): {length: number, offset: number, index: number} => ({
): {length: number; offset: number; index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
@ -96,7 +93,7 @@ class GroupListAccordion extends React.Component<PropsType> {
keyExtractor = (item: PlanexGroupType): string => item.id.toString();
render(): React.Node {
render() {
const {props} = this;
const {item} = this.props;
return (
@ -107,7 +104,7 @@ class GroupListAccordion extends React.Component<PropsType> {
height: props.height,
justifyContent: 'center',
}}
left={(iconProps: ListIconPropsType): React.Node =>
left={(iconProps) =>
item.id === 0 ? (
<List.Icon
style={iconProps.style}

View file

@ -17,23 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {List, TouchableRipple, withTheme} from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
theme: CustomThemeType,
onPress: () => void,
onStarPress: () => void,
item: PlanexGroupType,
favorites: Array<PlanexGroupType>,
height: number,
theme: ReactNativePaper.Theme;
onPress: () => void;
onStarPress: () => void;
item: PlanexGroupType;
favorites: Array<PlanexGroupType>;
height: number;
};
const REPLACE_REGEX = /_/g;
@ -41,10 +37,11 @@ const REPLACE_REGEX = /_/g;
class GroupListItem extends React.Component<PropsType> {
isFav: boolean;
starRef: null | Animatable.View;
starRef: {current: null | Animatable.View};
constructor(props: PropsType) {
super(props);
this.starRef = React.createRef<Animatable.View>();
this.isFav = this.isGroupInFavorites(props.favorites);
}
@ -52,7 +49,9 @@ class GroupListItem extends React.Component<PropsType> {
const {favorites} = this.props;
const favChanged = favorites.length !== nextProps.favorites.length;
let newFavState = this.isFav;
if (favChanged) newFavState = this.isGroupInFavorites(nextProps.favorites);
if (favChanged) {
newFavState = this.isGroupInFavorites(nextProps.favorites);
}
const shouldUpdate = this.isFav !== newFavState;
this.isFav = newFavState;
return shouldUpdate;
@ -61,9 +60,12 @@ class GroupListItem extends React.Component<PropsType> {
onStarPress = () => {
const {props} = this;
const ref = this.starRef;
if (ref != null) {
if (this.isFav) ref.rubberBand();
else ref.swing();
if (ref.current) {
if (this.isFav) {
ref.current.rubberBand();
} else {
ref.current.swing();
}
}
props.onStarPress();
};
@ -71,31 +73,29 @@ class GroupListItem extends React.Component<PropsType> {
isGroupInFavorites(favorites: Array<PlanexGroupType>): boolean {
const {item} = this.props;
for (let i = 0; i < favorites.length; i += 1) {
if (favorites[i].id === item.id) return true;
if (favorites[i].id === item.id) {
return true;
}
}
return false;
}
render(): React.Node {
render() {
const {props} = this;
const {colors} = props.theme;
return (
<List.Item
title={props.item.name.replace(REPLACE_REGEX, ' ')}
onPress={props.onPress}
left={(iconProps: ListIconPropsType): React.Node => (
left={(iconProps) => (
<List.Icon
color={iconProps.color}
style={iconProps.style}
icon="chevron-right"
/>
)}
right={(iconProps: ListIconPropsType): React.Node => (
<Animatable.View
ref={(ref: Animatable.View) => {
this.starRef = ref;
}}
useNativeDriver>
right={(iconProps) => (
<Animatable.View ref={this.starRef} useNativeDriver>
<TouchableRipple
onPress={this.onStarPress}
style={{

View file

@ -1,68 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen';
type PropsType = {
onPress: () => void,
color: string,
item: ProximoArticleType,
height: number,
};
class ProximoListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<List.Item
title={props.item.name}
description={`${props.item.quantity} ${i18n.t(
'screens.proximo.inStock',
)}`}
descriptionStyle={{color: props.color}}
onPress={props.onPress}
left={(): React.Node => (
<Avatar.Image
style={{backgroundColor: 'transparent'}}
size={64}
source={{uri: props.item.image}}
/>
)}
right={(): React.Node => (
<Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
)}
style={{
height: props.height,
justifyContent: 'center',
}}
/>
);
}
}
export default withTheme(ProximoListItem);

View file

@ -0,0 +1,59 @@
/*
* 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 {Avatar, List, Text} from 'react-native-paper';
import i18n from 'i18n-js';
import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen';
type PropsType = {
onPress: () => void;
color: string;
item: ProximoArticleType;
height: number;
};
function ProximoListItem(props: PropsType) {
return (
<List.Item
title={props.item.name}
description={`${props.item.quantity} ${i18n.t(
'screens.proximo.inStock',
)}`}
descriptionStyle={{color: props.color}}
onPress={props.onPress}
left={() => (
<Avatar.Image
style={{backgroundColor: 'transparent'}}
size={64}
source={{uri: props.item.image}}
/>
)}
right={() => (
<Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
)}
style={{
height: props.height,
justifyContent: 'center',
}}
/>
);
}
export default React.memo(ProximoListItem, () => true);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {
Avatar,
@ -34,20 +32,19 @@ import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
import AprilFoolsManager from '../../../managers/AprilFoolsManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
type PropsType = {
item: ProxiwashMachineType,
theme: CustomThemeType,
item: ProxiwashMachineType;
theme: ReactNativePaper.Theme;
onPress: (
title: string,
item: ProxiwashMachineType,
isDryer: boolean,
) => void,
isWatched: boolean,
isDryer: boolean,
height: number,
) => void;
isWatched: boolean;
isDryer: boolean;
height: number;
};
const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon);
@ -89,10 +86,11 @@ class ProxiwashListItem extends React.Component<PropsType> {
let displayNumber = props.item.number;
const displayMaxWeight = props.item.maxWeight;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
parseInt(props.item.number, 10),
);
}
this.title = props.isDryer
? i18n.t('screens.proxiwash.dryer')
@ -159,10 +157,10 @@ class ProxiwashListItem extends React.Component<PropsType> {
colors.proxiwashUnknownColor;
}
render(): React.Node {
render() {
const {props} = this;
const {colors} = props.theme;
const machineState = props.item.state;
const machineState = parseInt(props.item.state, 10);
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
const description = isRunning
@ -171,10 +169,13 @@ class ProxiwashListItem extends React.Component<PropsType> {
const stateIcon = ProxiwashConstants.stateIcons[machineState];
const stateString = this.stateStrings[machineState];
let progress;
if (isRunning && props.item.donePercent !== '')
if (isRunning && props.item.donePercent !== '') {
progress = parseFloat(props.item.donePercent) / 100;
else if (isRunning) progress = 0;
else progress = 1;
} else if (isRunning) {
progress = 0;
} else {
progress = 1;
}
const icon = props.isWatched ? (
<AnimatedIcon
@ -224,8 +225,8 @@ class ProxiwashListItem extends React.Component<PropsType> {
justifyContent: 'center',
}}
onPress={this.onListItemPress}
left={(): React.Node => icon}
right={(): React.Node => (
left={() => icon}
right={() => (
<View style={{flexDirection: 'row'}}>
<View style={{justifyContent: 'center'}}>
<Text

View file

@ -17,19 +17,16 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
theme: CustomThemeType,
title: string,
isDryer: boolean,
nbAvailable: number,
theme: ReactNativePaper.Theme;
title: string;
isDryer: boolean;
nbAvailable: number;
};
const styles = StyleSheet.create({
@ -61,7 +58,7 @@ class ProxiwashListItem extends React.Component<PropsType> {
);
}
render(): React.Node {
render() {
const {props} = this;
const subtitle = `${props.nbAvailable} ${
props.nbAvailable <= 1

View file

@ -94,7 +94,7 @@ export type ServiceItemType = {
key: string;
title: string;
subtitle: string;
image: string;
image: string | number;
onPress: () => void;
badgeFunction?: (dashboard: FullDashboardType) => number;
};