forked from vergnet/application-amicale
Improve Home components to match linter
This commit is contained in:
parent
34ccf9c4c9
commit
6b12b4cde2
9 changed files with 1273 additions and 1198 deletions
|
@ -2,37 +2,46 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import {View} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
};
|
||||
|
||||
class ActionsDashBoardItem extends React.Component<Props> {
|
||||
class ActionsDashBoardItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return nextProps.theme.dark !== props.theme.dark;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
title={i18n.t("screens.feedback.homeButtonTitle")}
|
||||
description={i18n.t("screens.feedback.homeButtonSubtitle")}
|
||||
left={props => <List.Icon {...props} icon={"comment-quote"}/>}
|
||||
right={props => <List.Icon {...props} icon={"chevron-right"}/>}
|
||||
onPress={() => this.props.navigation.navigate("feedback")}
|
||||
style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
title={i18n.t('screens.feedback.homeButtonTitle')}
|
||||
description={i18n.t('screens.feedback.homeButtonSubtitle')}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<List.Icon size={size} icon="comment-quote" />
|
||||
)}
|
||||
right={({size}: {size: number}): React.Node => (
|
||||
<List.Icon size={size} icon="chevron-right" />
|
||||
)}
|
||||
onPress={(): void => props.navigation.navigate('feedback')}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ActionsDashBoardItem);
|
||||
|
|
|
@ -1,91 +1,96 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, Text, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import i18n from "i18n-js";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
Text,
|
||||
TouchableRipple,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
eventNumber: number;
|
||||
clickAction: () => void,
|
||||
theme: CustomTheme,
|
||||
children?: React.Node
|
||||
}
|
||||
type PropsType = {
|
||||
eventNumber: number,
|
||||
clickAction: () => void,
|
||||
theme: CustomTheme,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: 'auto',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a dashboard item containing a preview event
|
||||
*/
|
||||
class EventDashBoardItem extends React.Component<Props> {
|
||||
class EventDashBoardItem extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.eventNumber !== this.props.eventNumber);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const colors = props.theme.colors;
|
||||
const isAvailable = props.eventNumber > 0;
|
||||
const iconColor = isAvailable ?
|
||||
colors.planningColor :
|
||||
colors.textDisabled;
|
||||
const textColor = isAvailable ?
|
||||
colors.text :
|
||||
colors.textDisabled;
|
||||
let subtitle;
|
||||
if (isAvailable) {
|
||||
subtitle =
|
||||
<Text>
|
||||
<Text style={{fontWeight: "bold"}}>{props.eventNumber}</Text>
|
||||
<Text>
|
||||
{props.eventNumber > 1
|
||||
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
|
||||
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
|
||||
</Text>
|
||||
</Text>;
|
||||
} else
|
||||
subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<TouchableRipple
|
||||
style={{flex: 1}}
|
||||
onPress={props.clickAction}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
|
||||
titleStyle={{color: textColor}}
|
||||
subtitle={subtitle}
|
||||
subtitleStyle={{color: textColor}}
|
||||
left={() =>
|
||||
<Avatar.Icon
|
||||
icon={'calendar-range'}
|
||||
color={iconColor}
|
||||
size={60}
|
||||
style={styles.avatar}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
{props.children}
|
||||
</Card.Content>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.eventNumber !== props.eventNumber
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
const isAvailable = props.eventNumber > 0;
|
||||
const iconColor = isAvailable ? colors.planningColor : colors.textDisabled;
|
||||
const textColor = isAvailable ? colors.text : colors.textDisabled;
|
||||
let subtitle;
|
||||
if (isAvailable) {
|
||||
subtitle = (
|
||||
<Text>
|
||||
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
|
||||
<Text>
|
||||
{props.eventNumber > 1
|
||||
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
|
||||
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
} else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
|
||||
titleStyle={{color: textColor}}
|
||||
subtitle={subtitle}
|
||||
subtitleStyle={{color: textColor}}
|
||||
left={(): React.Node => (
|
||||
<Avatar.Icon
|
||||
icon="calendar-range"
|
||||
color={iconColor}
|
||||
size={60}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>{props.children}</Card.Content>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: 'auto',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
});
|
||||
|
||||
export default withTheme(EventDashBoardItem);
|
||||
|
|
|
@ -2,126 +2,114 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
|
||||
import {Image, View} from "react-native";
|
||||
import Autolink from "react-native-autolink";
|
||||
import i18n from "i18n-js";
|
||||
import {Image, View} from 'react-native';
|
||||
import Autolink from 'react-native-autolink';
|
||||
import i18n from 'i18n-js';
|
||||
import ImageModal from 'react-native-image-modal';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import type {feedItem} from "../../screens/Home/HomeScreen";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {FeedItemType} from '../../screens/Home/HomeScreen';
|
||||
|
||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
item: feedItem,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
height: number,
|
||||
}
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
item: FeedItemType,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
height: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to display a feed item
|
||||
*/
|
||||
class FeedItem extends React.Component<Props> {
|
||||
class FeedItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
onPress = () => {
|
||||
const {props} = this;
|
||||
props.navigation.navigate('feed-information', {
|
||||
data: props.item,
|
||||
date: props.subtitle,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the amicale INSAT logo
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getAvatar() {
|
||||
return (
|
||||
<Image
|
||||
size={48}
|
||||
source={ICON_AMICALE}
|
||||
style={{
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {item} = props;
|
||||
const hasImage =
|
||||
item.full_picture !== '' && item.full_picture !== undefined;
|
||||
|
||||
const cardMargin = 10;
|
||||
const cardHeight = props.height - 2 * cardMargin;
|
||||
const imageSize = 250;
|
||||
const titleHeight = 80;
|
||||
const actionsHeight = 60;
|
||||
const textHeight = hasImage
|
||||
? cardHeight - titleHeight - actionsHeight - imageSize
|
||||
: cardHeight - titleHeight - actionsHeight;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
margin: cardMargin,
|
||||
height: cardHeight,
|
||||
}}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={this.onPress}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={props.title}
|
||||
subtitle={props.subtitle}
|
||||
left={(): React.Node => (
|
||||
<Image
|
||||
size={48}
|
||||
source={ICON_AMICALE}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
}}/>
|
||||
);
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
this.props.navigation.navigate(
|
||||
'feed-information',
|
||||
{
|
||||
data: this.props.item,
|
||||
date: this.props.subtitle
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const item = this.props.item;
|
||||
const hasImage = item.full_picture !== '' && item.full_picture !== undefined;
|
||||
|
||||
const cardMargin = 10;
|
||||
const cardHeight = this.props.height - 2 * cardMargin;
|
||||
const imageSize = 250;
|
||||
const titleHeight = 80;
|
||||
const actionsHeight = 60;
|
||||
const textHeight = hasImage
|
||||
? cardHeight - titleHeight - actionsHeight - imageSize
|
||||
: cardHeight - titleHeight - actionsHeight;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
margin: cardMargin,
|
||||
height: cardHeight,
|
||||
}}
|
||||
>
|
||||
<TouchableRipple
|
||||
style={{flex: 1}}
|
||||
onPress={this.onPress}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={this.props.title}
|
||||
subtitle={this.props.subtitle}
|
||||
left={this.getAvatar}
|
||||
style={{height: titleHeight}}
|
||||
/>
|
||||
{hasImage ?
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor={"#000"}
|
||||
style={{
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
}}
|
||||
source={{
|
||||
uri: item.full_picture,
|
||||
}}
|
||||
/></View> : null}
|
||||
<Card.Content>
|
||||
{item.message !== undefined ?
|
||||
<Autolink
|
||||
text={item.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
style={{height: textHeight}}
|
||||
/> : null
|
||||
}
|
||||
</Card.Content>
|
||||
<Card.Actions style={{height: actionsHeight}}>
|
||||
<Button
|
||||
onPress={this.onPress}
|
||||
icon={'plus'}
|
||||
style={{marginLeft: 'auto'}}>
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{height: titleHeight}}
|
||||
/>
|
||||
{hasImage ? (
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor="#000"
|
||||
style={{
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
}}
|
||||
source={{
|
||||
uri: item.full_picture,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
<Card.Content>
|
||||
{item.message !== undefined ? (
|
||||
<Autolink
|
||||
text={item.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
style={{height: textHeight}}
|
||||
/>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
<Card.Actions style={{height: actionsHeight}}>
|
||||
<Button
|
||||
onPress={this.onPress}
|
||||
icon="plus"
|
||||
style={{marginLeft: 'auto'}}>
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedItem;
|
||||
|
|
|
@ -1,94 +1,100 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import i18n from "i18n-js";
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
|
||||
import {getFormattedEventTime, isDescriptionEmpty} from "../../utils/Planning";
|
||||
import CustomHTML from "../Overrides/CustomHTML";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import type {event} from "../../screens/Home/HomeScreen";
|
||||
import {getFormattedEventTime, isDescriptionEmpty} from '../../utils/Planning';
|
||||
import CustomHTML from '../Overrides/CustomHTML';
|
||||
import type {EventType} from '../../screens/Home/HomeScreen';
|
||||
|
||||
type Props = {
|
||||
event?: event,
|
||||
clickAction: () => void,
|
||||
theme?: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
event?: EventType | null,
|
||||
clickAction: () => void,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
content: {
|
||||
maxHeight: 150,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
actions: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display an event preview if an event is available
|
||||
*/
|
||||
class PreviewEventDashboardItem extends React.Component<Props> {
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class PreviewEventDashboardItem extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
event: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const isEmpty = props.event == null
|
||||
? true
|
||||
: isDescriptionEmpty(props.event.description);
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {event} = props;
|
||||
const isEmpty =
|
||||
event == null ? true : isDescriptionEmpty(event.description);
|
||||
|
||||
if (props.event != null) {
|
||||
const event = props.event;
|
||||
const hasImage = event.logo !== '' && event.logo != null;
|
||||
const getImage = () => <Avatar.Image
|
||||
source={{uri: event.logo}}
|
||||
size={50}
|
||||
style={styles.avatar}/>;
|
||||
return (
|
||||
<Card
|
||||
style={styles.card}
|
||||
elevation={3}
|
||||
>
|
||||
<TouchableRipple
|
||||
style={{flex: 1}}
|
||||
onPress={props.clickAction}>
|
||||
<View>
|
||||
{hasImage ?
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
|
||||
left={getImage}
|
||||
/> :
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
|
||||
/>}
|
||||
{!isEmpty ?
|
||||
<Card.Content style={styles.content}>
|
||||
<CustomHTML html={event.description}/>
|
||||
</Card.Content> : null}
|
||||
if (event != null) {
|
||||
const hasImage = event.logo !== '' && event.logo != null;
|
||||
const getImage = (): React.Node => (
|
||||
<Avatar.Image
|
||||
source={{uri: event.logo}}
|
||||
size={50}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Card style={styles.card} elevation={3}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
|
||||
<View>
|
||||
{hasImage ? (
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
subtitle={getFormattedEventTime(
|
||||
event.date_begin,
|
||||
event.date_end,
|
||||
)}
|
||||
left={getImage}
|
||||
/>
|
||||
) : (
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
subtitle={getFormattedEventTime(
|
||||
event.date_begin,
|
||||
event.date_end,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!isEmpty ? (
|
||||
<Card.Content style={styles.content}>
|
||||
<CustomHTML html={event.description} />
|
||||
</Card.Content>
|
||||
) : null}
|
||||
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button
|
||||
icon={'chevron-right'}
|
||||
>
|
||||
{i18n.t("screens.home.dashboard.seeMore")}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
} else
|
||||
return null;
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button icon="chevron-right">
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginBottom: 10
|
||||
},
|
||||
content: {
|
||||
maxHeight: 150,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
actions: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
});
|
||||
|
||||
export default PreviewEventDashboardItem;
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomTheme} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
image: string,
|
||||
onPress: () => void,
|
||||
badgeCount: number | null,
|
||||
theme: CustomTheme,
|
||||
type PropsType = {
|
||||
image: string | null,
|
||||
onPress: () => void | null,
|
||||
badgeCount: number | null,
|
||||
theme: CustomTheme,
|
||||
};
|
||||
|
||||
const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
||||
|
@ -18,69 +18,68 @@ const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
|||
/**
|
||||
* Component used to render a small dashboard item
|
||||
*/
|
||||
class SmallDashboardItem extends React.Component<Props> {
|
||||
class SmallDashboardItem extends React.Component<PropsType> {
|
||||
itemSize: number;
|
||||
|
||||
itemSize: number;
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.itemSize = Dimensions.get('window').width / 8;
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.itemSize = Dimensions.get('window').width / 8;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.badgeCount !== this.props.badgeCount);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={this.props.onPress}
|
||||
borderless={true}
|
||||
style={{
|
||||
marginLeft: this.itemSize / 6,
|
||||
marginRight: this.itemSize / 6,
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
width: this.itemSize,
|
||||
height: this.itemSize,
|
||||
}}>
|
||||
<Image
|
||||
source={{uri: props.image}}
|
||||
style={{
|
||||
width: "80%",
|
||||
height: "80%",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
/>
|
||||
{
|
||||
props.badgeCount != null && props.badgeCount > 0 ?
|
||||
<AnimatableBadge
|
||||
animation={"zoomIn"}
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
backgroundColor: props.theme.colors.primary,
|
||||
borderColor: props.theme.colors.background,
|
||||
borderWidth: 2,
|
||||
}}>
|
||||
{props.badgeCount}
|
||||
</AnimatableBadge> : null
|
||||
}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
|
||||
);
|
||||
}
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.badgeCount !== props.badgeCount
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
borderless
|
||||
style={{
|
||||
marginLeft: this.itemSize / 6,
|
||||
marginRight: this.itemSize / 6,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
width: this.itemSize,
|
||||
height: this.itemSize,
|
||||
}}>
|
||||
<Image
|
||||
source={{uri: props.image}}
|
||||
style={{
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
{props.badgeCount != null && props.badgeCount > 0 ? (
|
||||
<AnimatableBadge
|
||||
animation="zoomIn"
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
backgroundColor: props.theme.colors.primary,
|
||||
borderColor: props.theme.colors.background,
|
||||
borderWidth: 2,
|
||||
}}>
|
||||
{props.badgeCount}
|
||||
</AnimatableBadge>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(SmallDashboardItem);
|
||||
|
|
|
@ -4,108 +4,114 @@ import * as React from 'react';
|
|||
import {Linking, View} from 'react-native';
|
||||
import {Avatar, Card, Text, withTheme} from 'react-native-paper';
|
||||
import ImageModal from 'react-native-image-modal';
|
||||
import Autolink from "react-native-autolink";
|
||||
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
|
||||
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {feedItem} from "./HomeScreen";
|
||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
||||
import Autolink from 'react-native-autolink';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import type {FeedItemType} from './HomeScreen';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
route: { params: { data: feedItem, date: string } }
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
route: {params: {data: FeedItemType, date: string}},
|
||||
};
|
||||
|
||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||
|
||||
const NAME_AMICALE = 'Amicale INSA Toulouse';
|
||||
|
||||
/**
|
||||
* Class defining a feed item page.
|
||||
*/
|
||||
class FeedItemScreen extends React.Component<Props> {
|
||||
class FeedItemScreen extends React.Component<PropsType> {
|
||||
displayData: FeedItemType;
|
||||
|
||||
displayData: feedItem;
|
||||
date: string;
|
||||
date: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.displayData = props.route.params.data;
|
||||
this.date = props.route.params.date;
|
||||
}
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.displayData = props.route.params.data;
|
||||
this.date = props.route.params.date;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.navigation.setOptions({
|
||||
headerRight: this.getHeaderButton,
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
const {props} = this;
|
||||
props.navigation.setOptions({
|
||||
headerRight: this.getHeaderButton,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the feed item out link in browser or compatible app
|
||||
*/
|
||||
onOutLinkPress = () => {
|
||||
Linking.openURL(this.displayData.permalink_url);
|
||||
};
|
||||
/**
|
||||
* Opens the feed item out link in browser or compatible app
|
||||
*/
|
||||
onOutLinkPress = () => {
|
||||
Linking.openURL(this.displayData.permalink_url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the out link header button
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getHeaderButton = () => {
|
||||
return <MaterialHeaderButtons>
|
||||
<Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/>
|
||||
</MaterialHeaderButtons>;
|
||||
};
|
||||
/**
|
||||
* Gets the out link header button
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getHeaderButton = (): React.Node => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="main"
|
||||
iconName="facebook"
|
||||
color="#2e88fe"
|
||||
onPress={this.onOutLinkPress}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the Amicale INSA avatar
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getAvatar() {
|
||||
return (
|
||||
<Avatar.Image size={48} source={ICON_AMICALE}
|
||||
style={{backgroundColor: 'transparent'}}/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null;
|
||||
return (
|
||||
<CollapsibleScrollView
|
||||
style={{margin: 5,}}
|
||||
hasTab={true}
|
||||
>
|
||||
<Card.Title
|
||||
title={NAME_AMICALE}
|
||||
subtitle={this.date}
|
||||
left={this.getAvatar}
|
||||
/>
|
||||
{hasImage ?
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor={"#000"}
|
||||
style={{
|
||||
width: 250,
|
||||
height: 250,
|
||||
}}
|
||||
source={{
|
||||
uri: this.displayData.full_picture,
|
||||
}}
|
||||
/></View> : null}
|
||||
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
||||
{this.displayData.message !== undefined ?
|
||||
<Autolink
|
||||
text={this.displayData.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
/> : null
|
||||
}
|
||||
</Card.Content>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const hasImage =
|
||||
this.displayData.full_picture !== '' &&
|
||||
this.displayData.full_picture != null;
|
||||
return (
|
||||
<CollapsibleScrollView style={{margin: 5}} hasTab>
|
||||
<Card.Title
|
||||
title={NAME_AMICALE}
|
||||
subtitle={this.date}
|
||||
left={(): React.Node => (
|
||||
<Avatar.Image
|
||||
size={48}
|
||||
source={ICON_AMICALE}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{hasImage ? (
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor="#000"
|
||||
style={{
|
||||
width: 250,
|
||||
height: 250,
|
||||
}}
|
||||
source={{
|
||||
uri: this.displayData.full_picture,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
||||
{this.displayData.message !== undefined ? (
|
||||
<Autolink
|
||||
text={this.displayData.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
/>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(FeedItemScreen);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,238 +1,237 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Linking, Platform, StyleSheet, View} from "react-native";
|
||||
import {Linking, Platform, StyleSheet, View} from 'react-native';
|
||||
import {Button, Text, withTheme} from 'react-native-paper';
|
||||
import {RNCamera} from 'react-native-camera';
|
||||
import {BarcodeMask} from '@nartc/react-native-barcode-mask';
|
||||
import URLHandler from "../../utils/URLHandler";
|
||||
import AlertDialog from "../../components/Dialogs/AlertDialog";
|
||||
import i18n from 'i18n-js';
|
||||
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
|
||||
import LoadingConfirmDialog from "../../components/Dialogs/LoadingConfirmDialog";
|
||||
import {PERMISSIONS, request, RESULTS} from 'react-native-permissions';
|
||||
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
||||
import MascotPopup from "../../components/Mascot/MascotPopup";
|
||||
import URLHandler from '../../utils/URLHandler';
|
||||
import AlertDialog from '../../components/Dialogs/AlertDialog';
|
||||
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
|
||||
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
|
||||
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
|
||||
type Props = {};
|
||||
type State = {
|
||||
hasPermission: boolean,
|
||||
scanned: boolean,
|
||||
dialogVisible: boolean,
|
||||
mascotDialogVisible: boolean,
|
||||
dialogTitle: string,
|
||||
dialogMessage: string,
|
||||
loading: boolean,
|
||||
type StateType = {
|
||||
hasPermission: boolean,
|
||||
scanned: boolean,
|
||||
dialogVisible: boolean,
|
||||
mascotDialogVisible: boolean,
|
||||
loading: boolean,
|
||||
};
|
||||
|
||||
class ScannerScreen extends React.Component<Props, State> {
|
||||
|
||||
state = {
|
||||
hasPermission: false,
|
||||
scanned: false,
|
||||
mascotDialogVisible: false,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "",
|
||||
dialogMessage: "",
|
||||
loading: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.requestPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permission to use the camera
|
||||
*/
|
||||
requestPermissions = () => {
|
||||
if (Platform.OS === 'android')
|
||||
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus)
|
||||
else
|
||||
request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus)
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the state permission status
|
||||
*
|
||||
* @param result
|
||||
*/
|
||||
updatePermissionStatus = (result) => this.setState({hasPermission: result === RESULTS.GRANTED});
|
||||
|
||||
/**
|
||||
* Opens scanned link if it is a valid app link or shows and error dialog
|
||||
*
|
||||
* @param type The barcode type
|
||||
* @param data The scanned value
|
||||
*/
|
||||
handleCodeScanned = ({type, data}) => {
|
||||
if (!URLHandler.isUrlValid(data))
|
||||
this.showErrorDialog();
|
||||
else {
|
||||
this.showOpeningDialog();
|
||||
Linking.openURL(data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a view asking user for permission to use the camera
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getPermissionScreen() {
|
||||
return <View style={{marginLeft: 10, marginRight: 10}}>
|
||||
<Text>{i18n.t("screens.scanner.permissions.error")}</Text>
|
||||
<Button
|
||||
icon="camera"
|
||||
mode="contained"
|
||||
onPress={this.requestPermissions}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
>
|
||||
{i18n.t("screens.scanner.permissions.button")}
|
||||
</Button>
|
||||
</View>
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog indicating how to use the scanner
|
||||
*/
|
||||
showHelpDialog = () => {
|
||||
this.setState({
|
||||
mascotDialogVisible: true,
|
||||
scanned: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a loading dialog
|
||||
*/
|
||||
showOpeningDialog = () => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
scanned: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a dialog indicating the user the scanned code was invalid
|
||||
*/
|
||||
showErrorDialog() {
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
scanned: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide any dialog
|
||||
*/
|
||||
onDialogDismiss = () => this.setState({
|
||||
dialogVisible: false,
|
||||
scanned: false,
|
||||
});
|
||||
|
||||
onMascotDialogDismiss = () => this.setState({
|
||||
mascotDialogVisible: false,
|
||||
scanned: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets a view with the scanner.
|
||||
* This scanner uses the back camera, can only scan qr codes and has a square mask on the center.
|
||||
* The mask is only for design purposes as a code is scanned as soon as it enters the camera view
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getScanner() {
|
||||
return (
|
||||
<RNCamera
|
||||
onBarCodeRead={this.state.scanned ? undefined : this.handleCodeScanned}
|
||||
type={RNCamera.Constants.Type.back}
|
||||
barCodeScannerSettings={{
|
||||
barCodeTypes: [RNCamera.Constants.BarCodeType.qr],
|
||||
}}
|
||||
style={StyleSheet.absoluteFill}
|
||||
captureAudio={false}
|
||||
>
|
||||
<BarcodeMask
|
||||
backgroundColor={"#000"}
|
||||
maskOpacity={0.5}
|
||||
animatedLineThickness={1}
|
||||
animationDuration={1000}
|
||||
width={250}
|
||||
height={250}
|
||||
/>
|
||||
</RNCamera>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{
|
||||
...styles.container,
|
||||
marginBottom: CustomTabBar.TAB_BAR_HEIGHT
|
||||
}}>
|
||||
{this.state.hasPermission
|
||||
? this.getScanner()
|
||||
: this.getPermissionScreen()
|
||||
}
|
||||
<Button
|
||||
icon="information"
|
||||
mode="contained"
|
||||
onPress={this.showHelpDialog}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t("screens.scanner.help.button")}
|
||||
</Button>
|
||||
<MascotPopup
|
||||
visible={this.state.mascotDialogVisible}
|
||||
title={i18n.t("screens.scanner.mascotDialog.title")}
|
||||
message={i18n.t("screens.scanner.mascotDialog.message")}
|
||||
icon={"camera-iris"}
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t("screens.scanner.mascotDialog.button"),
|
||||
icon: "check",
|
||||
onPress: this.onMascotDialogDismiss,
|
||||
}
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<AlertDialog
|
||||
visible={this.state.dialogVisible}
|
||||
onDismiss={this.onDialogDismiss}
|
||||
title={i18n.t("screens.scanner.error.title")}
|
||||
message={i18n.t("screens.scanner.error.message")}
|
||||
/>
|
||||
<LoadingConfirmDialog
|
||||
visible={this.state.loading}
|
||||
titleLoading={i18n.t("general.loading")}
|
||||
startLoading={true}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
button: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
width: '80%',
|
||||
left: '10%'
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
button: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
width: '80%',
|
||||
left: '10%',
|
||||
},
|
||||
});
|
||||
|
||||
class ScannerScreen extends React.Component<null, StateType> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
hasPermission: false,
|
||||
scanned: false,
|
||||
mascotDialogVisible: false,
|
||||
dialogVisible: false,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.requestPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a view asking user for permission to use the camera
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getPermissionScreen(): React.Node {
|
||||
return (
|
||||
<View style={{marginLeft: 10, marginRight: 10}}>
|
||||
<Text>{i18n.t('screens.scanner.permissions.error')}</Text>
|
||||
<Button
|
||||
icon="camera"
|
||||
mode="contained"
|
||||
onPress={this.requestPermissions}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
{i18n.t('screens.scanner.permissions.button')}
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a view with the scanner.
|
||||
* This scanner uses the back camera, can only scan qr codes and has a square mask on the center.
|
||||
* The mask is only for design purposes as a code is scanned as soon as it enters the camera view
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getScanner(): React.Node {
|
||||
const {state} = this;
|
||||
return (
|
||||
<RNCamera
|
||||
onBarCodeRead={state.scanned ? null : this.onCodeScanned}
|
||||
type={RNCamera.Constants.Type.back}
|
||||
barCodeScannerSettings={{
|
||||
barCodeTypes: [RNCamera.Constants.BarCodeType.qr],
|
||||
}}
|
||||
style={StyleSheet.absoluteFill}
|
||||
captureAudio={false}>
|
||||
<BarcodeMask
|
||||
backgroundColor="#000"
|
||||
maskOpacity={0.5}
|
||||
animatedLineThickness={1}
|
||||
animationDuration={1000}
|
||||
width={250}
|
||||
height={250}
|
||||
/>
|
||||
</RNCamera>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests permission to use the camera
|
||||
*/
|
||||
requestPermissions = () => {
|
||||
if (Platform.OS === 'android')
|
||||
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus);
|
||||
else request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the state permission status
|
||||
*
|
||||
* @param result
|
||||
*/
|
||||
updatePermissionStatus = (result: RESULTS) => {
|
||||
this.setState({
|
||||
hasPermission: result === RESULTS.GRANTED,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a dialog indicating the user the scanned code was invalid
|
||||
*/
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
showErrorDialog() {
|
||||
this.setState({
|
||||
dialogVisible: true,
|
||||
scanned: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog indicating how to use the scanner
|
||||
*/
|
||||
showHelpDialog = () => {
|
||||
this.setState({
|
||||
mascotDialogVisible: true,
|
||||
scanned: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a loading dialog
|
||||
*/
|
||||
showOpeningDialog = () => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
scanned: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide any dialog
|
||||
*/
|
||||
onDialogDismiss = () => {
|
||||
this.setState({
|
||||
dialogVisible: false,
|
||||
scanned: false,
|
||||
});
|
||||
};
|
||||
|
||||
onMascotDialogDismiss = () => {
|
||||
this.setState({
|
||||
mascotDialogVisible: false,
|
||||
scanned: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens scanned link if it is a valid app link or shows and error dialog
|
||||
*
|
||||
* @param type The barcode type
|
||||
* @param data The scanned value
|
||||
*/
|
||||
onCodeScanned = ({data}: {data: string}) => {
|
||||
if (!URLHandler.isUrlValid(data)) this.showErrorDialog();
|
||||
else {
|
||||
this.showOpeningDialog();
|
||||
Linking.openURL(data);
|
||||
}
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {state} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...styles.container,
|
||||
marginBottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}>
|
||||
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()}
|
||||
<Button
|
||||
icon="information"
|
||||
mode="contained"
|
||||
onPress={this.showHelpDialog}
|
||||
style={styles.button}>
|
||||
{i18n.t('screens.scanner.help.button')}
|
||||
</Button>
|
||||
<MascotPopup
|
||||
visible={state.mascotDialogVisible}
|
||||
title={i18n.t('screens.scanner.mascotDialog.title')}
|
||||
message={i18n.t('screens.scanner.mascotDialog.message')}
|
||||
icon="camera-iris"
|
||||
buttons={{
|
||||
action: null,
|
||||
cancel: {
|
||||
message: i18n.t('screens.scanner.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.onMascotDialogDismiss,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<AlertDialog
|
||||
visible={state.dialogVisible}
|
||||
onDismiss={this.onDialogDismiss}
|
||||
title={i18n.t('screens.scanner.error.title')}
|
||||
message={i18n.t('screens.scanner.error.message')}
|
||||
/>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.loading}
|
||||
titleLoading={i18n.t('general.loading')}
|
||||
startLoading
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ScannerScreen);
|
||||
|
|
123
src/utils/Home.js
Normal file
123
src/utils/Home.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
// @flow
|
||||
|
||||
import {stringToDate} from './Planning';
|
||||
import type {EventType} from '../screens/Home/HomeScreen';
|
||||
|
||||
/**
|
||||
* Gets the time limit depending on the current day:
|
||||
* 17:30 for every day of the week except for thursday 11:30
|
||||
* 00:00 on weekends
|
||||
*/
|
||||
export function getTodayEventTimeLimit(): Date {
|
||||
const now = new Date();
|
||||
if (now.getDay() === 4)
|
||||
// Thursday
|
||||
now.setHours(11, 30, 0);
|
||||
else if (now.getDay() === 6 || now.getDay() === 0)
|
||||
// Weekend
|
||||
now.setHours(0, 0, 0);
|
||||
else now.setHours(17, 30, 0);
|
||||
return now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration (in milliseconds) of an event
|
||||
*
|
||||
* @param event {EventType}
|
||||
* @return {number} The number of milliseconds
|
||||
*/
|
||||
export function getEventDuration(event: EventType): number {
|
||||
const start = stringToDate(event.date_begin);
|
||||
const end = stringToDate(event.date_end);
|
||||
let duration = 0;
|
||||
if (start != null && end != null) duration = end - start;
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets events starting after the limit
|
||||
*
|
||||
* @param events
|
||||
* @param limit
|
||||
* @return {Array<Object>}
|
||||
*/
|
||||
export function getEventsAfterLimit(
|
||||
events: Array<EventType>,
|
||||
limit: Date,
|
||||
): Array<EventType> {
|
||||
const validEvents = [];
|
||||
events.forEach((event: EventType) => {
|
||||
const startDate = stringToDate(event.date_begin);
|
||||
if (startDate != null && startDate >= limit) {
|
||||
validEvents.push(event);
|
||||
}
|
||||
});
|
||||
return validEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the event with the longest duration in the given array.
|
||||
* If all events have the same duration, return the first in the array.
|
||||
*
|
||||
* @param events
|
||||
*/
|
||||
export function getLongestEvent(events: Array<EventType>): EventType {
|
||||
let longestEvent = events[0];
|
||||
let longestTime = 0;
|
||||
events.forEach((event: EventType) => {
|
||||
const time = getEventDuration(event);
|
||||
if (time > longestTime) {
|
||||
longestTime = time;
|
||||
longestEvent = event;
|
||||
}
|
||||
});
|
||||
return longestEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets events that have not yet ended/started
|
||||
*
|
||||
* @param events
|
||||
*/
|
||||
export function getFutureEvents(events: Array<EventType>): Array<EventType> {
|
||||
const validEvents = [];
|
||||
const now = new Date();
|
||||
events.forEach((event: EventType) => {
|
||||
const startDate = stringToDate(event.date_begin);
|
||||
const endDate = stringToDate(event.date_end);
|
||||
if (startDate != null) {
|
||||
if (startDate > now) validEvents.push(event);
|
||||
else if (endDate != null) {
|
||||
if (endDate > now || endDate < startDate)
|
||||
// Display event if it ends the following day
|
||||
validEvents.push(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
return validEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the event to display in the preview
|
||||
*
|
||||
* @param events
|
||||
* @return {EventType | null}
|
||||
*/
|
||||
export function getDisplayEvent(events: Array<EventType>): EventType | null {
|
||||
let displayEvent = null;
|
||||
if (events.length > 1) {
|
||||
const eventsAfterLimit = getEventsAfterLimit(
|
||||
events,
|
||||
getTodayEventTimeLimit(),
|
||||
);
|
||||
if (eventsAfterLimit.length > 0) {
|
||||
if (eventsAfterLimit.length === 1) [displayEvent] = eventsAfterLimit;
|
||||
else displayEvent = getLongestEvent(events);
|
||||
} else {
|
||||
displayEvent = getLongestEvent(events);
|
||||
}
|
||||
} else if (events.length === 1) {
|
||||
[displayEvent] = events;
|
||||
}
|
||||
return displayEvent;
|
||||
}
|
Loading…
Reference in a new issue