Compare commits
No commits in common. "e4530ded185af97a63a220b7481dbedd48b34d6e" and "8ac19f36def017880bf536075fd2ac0c86fb6fb1" have entirely different histories.
e4530ded18
...
8ac19f36de
17 changed files with 702 additions and 680 deletions
|
|
@ -17,37 +17,42 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View, ViewStyle} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {ListIconPropsType} from '../../constants/PaperStyles';
|
||||
|
||||
type PropsType = {
|
||||
theme: ReactNativePaper.Theme;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
style: ViewStyle;
|
||||
left?: (props: {
|
||||
color: string;
|
||||
style?: {
|
||||
marginRight: number;
|
||||
marginVertical?: number;
|
||||
};
|
||||
}) => React.ReactNode;
|
||||
opened?: boolean;
|
||||
unmountWhenCollapsed?: boolean;
|
||||
children?: React.ReactNode;
|
||||
theme: CustomThemeType,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
left?: () => React.Node,
|
||||
opened?: boolean,
|
||||
unmountWhenCollapsed?: boolean,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
expanded: boolean;
|
||||
expanded: boolean,
|
||||
};
|
||||
|
||||
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
|
||||
|
||||
class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
||||
chevronRef: {current: null | (typeof AnimatedListIcon & List.Icon)};
|
||||
static defaultProps = {
|
||||
subtitle: '',
|
||||
left: null,
|
||||
opened: null,
|
||||
unmountWhenCollapsed: false,
|
||||
children: null,
|
||||
};
|
||||
|
||||
chevronRef: {current: null | AnimatedListIcon};
|
||||
|
||||
chevronIcon: string;
|
||||
|
||||
|
|
@ -57,9 +62,6 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
|||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.chevronIcon = '';
|
||||
this.animStart = '';
|
||||
this.animEnd = '';
|
||||
this.state = {
|
||||
expanded: props.opened != null ? props.opened : false,
|
||||
};
|
||||
|
|
@ -69,9 +71,8 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
|||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {state, props} = this;
|
||||
if (nextProps.opened != null && nextProps.opened !== props.opened) {
|
||||
if (nextProps.opened != null && nextProps.opened !== props.opened)
|
||||
state.expanded = nextProps.opened;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -100,17 +101,17 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<View>
|
||||
<List.Item
|
||||
title={props.title}
|
||||
description={props.subtitle}
|
||||
subtitle={props.subtitle}
|
||||
titleStyle={state.expanded ? {color: colors.primary} : null}
|
||||
onPress={this.toggleAccordion}
|
||||
right={(iconProps) => (
|
||||
right={(iconProps: ListIconPropsType): React.Node => (
|
||||
<AnimatedListIcon
|
||||
ref={this.chevronRef}
|
||||
style={iconProps.style}
|
||||
|
|
@ -17,30 +17,29 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {OnScrollType} from '../../utils/AutoHideHandler';
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
onPress: (action: string, data?: string) => void;
|
||||
seekAttention: boolean;
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
onPress: (action: string, data?: string) => void,
|
||||
seekAttention: boolean,
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
currentMode: string;
|
||||
currentMode: string,
|
||||
};
|
||||
|
||||
const DISPLAY_MODES = {
|
||||
|
|
@ -79,14 +78,14 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
||||
ref: {current: null | (Animatable.View & View)};
|
||||
ref: {current: null | Animatable.View};
|
||||
|
||||
hideHandler: AutoHideHandler;
|
||||
|
||||
displayModeIcons: {[key: string]: string};
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
currentMode: DISPLAY_MODES.WEEK,
|
||||
};
|
||||
|
|
@ -109,17 +108,13 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
|
||||
onHideChange = (shouldHide: boolean) => {
|
||||
const ref = this.ref;
|
||||
if (ref && ref.current && ref.current.fadeOutDown && ref.current.fadeInUp) {
|
||||
if (shouldHide) {
|
||||
ref.current.fadeOutDown(500);
|
||||
} else {
|
||||
ref.current.fadeInUp(500);
|
||||
}
|
||||
if (this.ref.current != null) {
|
||||
if (shouldHide) this.ref.current.fadeOutDown(500);
|
||||
else this.ref.current.fadeInUp(500);
|
||||
}
|
||||
};
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
onScroll = (event: OnScrollType) => {
|
||||
this.hideHandler.onScroll(event);
|
||||
};
|
||||
|
||||
|
|
@ -144,7 +139,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
|||
props.onPress('changeView', newMode);
|
||||
};
|
||||
|
||||
render() {
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
const buttonColor = props.theme.colors.primary;
|
||||
return (
|
||||
|
|
@ -17,23 +17,22 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {FAB} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
|
||||
type PropsType = {
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
icon: string,
|
||||
onPress: () => void,
|
||||
};
|
||||
|
||||
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
|
|
@ -43,50 +42,41 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
export default class AnimatedFAB extends React.Component<PropsType> {
|
||||
ref: {current: null | (Animatable.View & View)};
|
||||
ref: {current: null | Animatable.View};
|
||||
|
||||
hideHandler: AutoHideHandler;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
this.ref = React.createRef();
|
||||
this.hideHandler = new AutoHideHandler(false);
|
||||
this.hideHandler.addListener(this.onHideChange);
|
||||
}
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
this.hideHandler.onScroll(event);
|
||||
};
|
||||
|
||||
onHideChange = (shouldHide: boolean) => {
|
||||
const ref = this.ref;
|
||||
if (
|
||||
ref &&
|
||||
ref.current &&
|
||||
ref.current.bounceOutDown &&
|
||||
ref.current.bounceInUp
|
||||
) {
|
||||
if (shouldHide) {
|
||||
ref.current.bounceOutDown(1000);
|
||||
} else {
|
||||
ref.current.bounceInUp(1000);
|
||||
}
|
||||
if (this.ref.current != null) {
|
||||
if (shouldHide) this.ref.current.bounceOutDown(1000);
|
||||
else this.ref.current.bounceInUp(1000);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Animatable.View ref={this.ref} useNativeDriver={true}>
|
||||
<FAB
|
||||
icon={props.icon}
|
||||
onPress={props.onPress}
|
||||
style={{
|
||||
...styles.fab,
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<AnimatedFab
|
||||
ref={this.ref}
|
||||
useNativeDriver
|
||||
icon={props.icon}
|
||||
onPress={props.onPress}
|
||||
style={{
|
||||
...styles.fab,
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
75
src/components/Home/ActionsDashboardItem.js
Normal file
75
src/components/Home/ActionsDashboardItem.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 {List, withTheme} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {ListIconPropsType} from '../../constants/PaperStyles';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class ActionsDashBoardItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return nextProps.theme.dark !== props.theme.dark;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
title={i18n.t('screens.feedback.homeButtonTitle')}
|
||||
description={i18n.t('screens.feedback.homeButtonSubtitle')}
|
||||
left={(props: ListIconPropsType): React.Node => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
style={props.style}
|
||||
icon="comment-quote"
|
||||
/>
|
||||
)}
|
||||
right={(props: ListIconPropsType): React.Node => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
style={props.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
onPress={(): void => navigation.navigate('feedback')}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ActionsDashBoardItem);
|
||||
|
|
@ -1,59 +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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {List} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
|
||||
function ActionsDashBoardItem() {
|
||||
const navigation = useNavigation();
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
title={i18n.t('screens.feedback.homeButtonTitle')}
|
||||
description={i18n.t('screens.feedback.homeButtonSubtitle')}
|
||||
left={(props) => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
style={props.style}
|
||||
icon="comment-quote"
|
||||
/>
|
||||
)}
|
||||
right={(props) => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
style={props.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
onPress={(): void => navigation.navigate('feedback')}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionsDashBoardItem;
|
||||
116
src/components/Home/EventDashboardItem.js
Normal file
116
src/components/Home/EventDashboardItem.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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,
|
||||
Card,
|
||||
Text,
|
||||
TouchableRipple,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {CardTitleIconPropsType} from '../../constants/PaperStyles';
|
||||
|
||||
type PropsType = {
|
||||
eventNumber: number,
|
||||
clickAction: () => void,
|
||||
theme: CustomThemeType,
|
||||
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<PropsType> {
|
||||
static defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
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={(iconProps: CardTitleIconPropsType): React.Node => (
|
||||
<Avatar.Icon
|
||||
icon="calendar-range"
|
||||
color={iconColor}
|
||||
size={iconProps.size}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>{props.children}</Card.Content>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(EventDashBoardItem);
|
||||
|
|
@ -1,104 +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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
Text,
|
||||
TouchableRipple,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type PropsType = {
|
||||
eventNumber: number;
|
||||
clickAction: () => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
function EventDashBoardItem(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const isAvailable = props.eventNumber > 0;
|
||||
const iconColor = isAvailable
|
||||
? theme.colors.planningColor
|
||||
: theme.colors.textDisabled;
|
||||
const textColor = isAvailable ? theme.colors.text : theme.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={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
icon="calendar-range"
|
||||
color={iconColor}
|
||||
size={iconProps.size}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>{props.children}</Card.Content>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
|
||||
return nextProps.eventNumber !== prevProps.eventNumber;
|
||||
};
|
||||
|
||||
export default React.memo(EventDashBoardItem, areEqual);
|
||||
139
src/components/Home/FeedItem.js
Normal file
139
src/components/Home/FeedItem.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 {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 {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {FeedItemType} from '../../screens/Home/HomeScreen';
|
||||
import NewsSourcesConstants from '../../constants/NewsSourcesConstants';
|
||||
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
|
||||
import ImageGalleryButton from '../Media/ImageGalleryButton';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
item: FeedItemType,
|
||||
height: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to display a feed item
|
||||
*/
|
||||
class FeedItem extends React.Component<PropsType> {
|
||||
/**
|
||||
* Converts a dateString using Unix Timestamp to a formatted date
|
||||
*
|
||||
* @param dateString {string} The Unix Timestamp representation of a date
|
||||
* @return {string} The formatted output date
|
||||
*/
|
||||
static getFormattedDate(dateString: number): string {
|
||||
const date = new Date(dateString * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
const {item, navigation} = this.props;
|
||||
navigation.navigate('feed-information', {
|
||||
data: item,
|
||||
date: FeedItem.getFormattedDate(item.time),
|
||||
});
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {item, height, navigation} = this.props;
|
||||
const image = item.image !== '' && item.image != null ? item.image : null;
|
||||
const pageSource: NewsSourceType = NewsSourcesConstants[item.page_id];
|
||||
const cardMargin = 10;
|
||||
const cardHeight = height - 2 * cardMargin;
|
||||
const imageSize = 250;
|
||||
const titleHeight = 80;
|
||||
const actionsHeight = 60;
|
||||
const textHeight =
|
||||
image != null
|
||||
? 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={pageSource.name}
|
||||
subtitle={FeedItem.getFormattedDate(item.time)}
|
||||
left={(): React.Node => (
|
||||
<Image
|
||||
size={48}
|
||||
source={pageSource.icon}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{height: titleHeight}}
|
||||
/>
|
||||
{image != null ? (
|
||||
<ImageGalleryButton
|
||||
navigation={navigation}
|
||||
images={[{url: image}]}
|
||||
style={{
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
) : 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,128 +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/>.
|
||||
*/
|
||||
|
||||
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 type {FeedItemType} from '../../screens/Home/HomeScreen';
|
||||
import NewsSourcesConstants, {
|
||||
AvailablePages,
|
||||
} from '../../constants/NewsSourcesConstants';
|
||||
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
|
||||
import ImageGalleryButton from '../Media/ImageGalleryButton';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
|
||||
type PropsType = {
|
||||
item: FeedItemType;
|
||||
height: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a dateString using Unix Timestamp to a formatted date
|
||||
*
|
||||
* @param dateString {string} The Unix Timestamp representation of a date
|
||||
* @return {string} The formatted output date
|
||||
*/
|
||||
function getFormattedDate(dateString: number): string {
|
||||
const date = new Date(dateString * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component used to display a feed item
|
||||
*/
|
||||
function FeedItem(props: PropsType) {
|
||||
const navigation = useNavigation();
|
||||
const onPress = () => {
|
||||
navigation.navigate('feed-information', {
|
||||
data: item,
|
||||
date: getFormattedDate(props.item.time),
|
||||
});
|
||||
};
|
||||
|
||||
const {item, height} = props;
|
||||
const image = item.image !== '' && item.image != null ? item.image : null;
|
||||
const pageSource: NewsSourceType =
|
||||
NewsSourcesConstants[item.page_id as AvailablePages];
|
||||
const cardMargin = 10;
|
||||
const cardHeight = height - 2 * cardMargin;
|
||||
const imageSize = 250;
|
||||
const titleHeight = 80;
|
||||
const actionsHeight = 60;
|
||||
const textHeight =
|
||||
image != null
|
||||
? cardHeight - titleHeight - actionsHeight - imageSize
|
||||
: cardHeight - titleHeight - actionsHeight;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
margin: cardMargin,
|
||||
height: cardHeight,
|
||||
}}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={onPress}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={pageSource.name}
|
||||
subtitle={getFormattedDate(item.time)}
|
||||
left={() => (
|
||||
<Image
|
||||
source={pageSource.icon}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
style={{height: titleHeight}}
|
||||
/>
|
||||
{image != null ? (
|
||||
<ImageGalleryButton
|
||||
images={[{url: image}]}
|
||||
style={{
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Card.Content>
|
||||
{item.message !== undefined ? (
|
||||
<Autolink<typeof Text>
|
||||
text={item.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
style={{height: textHeight}}
|
||||
/>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
<Card.Actions style={{height: actionsHeight}}>
|
||||
<Button onPress={onPress} icon="plus" style={{marginLeft: 'auto'}}>
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(FeedItem, () => true);
|
||||
113
src/components/Home/PreviewEventDashboardItem.js
Normal file
113
src/components/Home/PreviewEventDashboardItem.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
|
||||
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
|
||||
import CustomHTML from '../Overrides/CustomHTML';
|
||||
import type {PlanningEventType} from '../../utils/Planning';
|
||||
|
||||
type PropsType = {
|
||||
event?: PlanningEventType | 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
|
||||
*/
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class PreviewEventDashboardItem extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
event: null,
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {event} = props;
|
||||
const isEmpty =
|
||||
event == null ? true : isDescriptionEmpty(event.description);
|
||||
|
||||
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={getTimeOnlyString(event.date_begin)}
|
||||
left={getImage}
|
||||
/>
|
||||
) : (
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
subtitle={getTimeOnlyString(event.date_begin)}
|
||||
/>
|
||||
)}
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default PreviewEventDashboardItem;
|
||||
|
|
@ -1,93 +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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
|
||||
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
|
||||
import CustomHTML from '../Overrides/CustomHTML';
|
||||
import type {PlanningEventType} from '../../utils/Planning';
|
||||
|
||||
type PropsType = {
|
||||
event?: PlanningEventType | 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
|
||||
*/
|
||||
function PreviewEventDashboardItem(props: PropsType) {
|
||||
const {event} = props;
|
||||
const isEmpty = event == null ? true : isDescriptionEmpty(event.description);
|
||||
|
||||
if (event != null) {
|
||||
const logo = event.logo;
|
||||
const getImage = logo
|
||||
? () => (
|
||||
<Avatar.Image source={{uri: logo}} size={50} style={styles.avatar} />
|
||||
)
|
||||
: () => null;
|
||||
return (
|
||||
<Card style={styles.card} elevation={3}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
subtitle={getTimeOnlyString(event.date_begin)}
|
||||
left={getImage}
|
||||
/>
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default PreviewEventDashboardItem;
|
||||
106
src/components/Home/SmallDashboardItem.js
Normal file
106
src/components/Home/SmallDashboardItem.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 {Badge, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
image: string | null,
|
||||
onPress: () => void | null,
|
||||
badgeCount: number | null,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to render a small dashboard item
|
||||
*/
|
||||
class SmallDashboardItem extends React.Component<PropsType> {
|
||||
itemSize: number;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.itemSize = Dimensions.get('window').width / 8;
|
||||
}
|
||||
|
||||
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 ? (
|
||||
<Animatable.View
|
||||
animation="zoomIn"
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}>
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: props.theme.colors.primary,
|
||||
borderColor: props.theme.colors.background,
|
||||
borderWidth: 2,
|
||||
}}>
|
||||
{props.badgeCount}
|
||||
</Badge>
|
||||
</Animatable.View>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(SmallDashboardItem);
|
||||
|
|
@ -1,94 +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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Badge, TouchableRipple, useTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
type PropsType = {
|
||||
image: string | null;
|
||||
onPress: () => void | null;
|
||||
badgeCount: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to render a small dashboard item
|
||||
*/
|
||||
function SmallDashboardItem(props: PropsType) {
|
||||
const itemSize = Dimensions.get('window').width / 8;
|
||||
const theme = useTheme();
|
||||
const {image} = props;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
borderless
|
||||
style={{
|
||||
marginLeft: itemSize / 6,
|
||||
marginRight: itemSize / 6,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
width: itemSize,
|
||||
height: itemSize,
|
||||
}}>
|
||||
{image ? (
|
||||
<Image
|
||||
source={{uri: image}}
|
||||
style={{
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{props.badgeCount != null && props.badgeCount > 0 ? (
|
||||
<Animatable.View
|
||||
animation="zoomIn"
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}>
|
||||
<Badge
|
||||
visible={true}
|
||||
style={{
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderColor: theme.colors.background,
|
||||
borderWidth: 2,
|
||||
}}>
|
||||
{props.badgeCount}
|
||||
</Badge>
|
||||
</Animatable.View>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
|
||||
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
|
||||
return nextProps.badgeCount !== prevProps.badgeCount;
|
||||
};
|
||||
|
||||
export default React.memo(SmallDashboardItem, areEqual);
|
||||
|
|
@ -39,9 +39,7 @@ const MaterialHeaderButton = (props: HeaderButtonProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
const MaterialHeaderButtons = (
|
||||
props: HeaderButtonsProps & {children?: React.ReactNode},
|
||||
) => {
|
||||
const MaterialHeaderButtons = (props: HeaderButtonsProps) => {
|
||||
return (
|
||||
<HeaderButtons {...props} HeaderButtonComponent={MaterialHeaderButton} />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,15 +17,12 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import {Snackbar} from 'react-native-paper';
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
RefreshControl,
|
||||
SectionListData,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {RefreshControl, View} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
|
|
@ -35,43 +32,42 @@ import withCollapsible from '../../utils/withCollapsible';
|
|||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import {ERROR_TYPE, readData} from '../../utils/WebData';
|
||||
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
|
||||
import type {ApiGenericDataType} from '../../utils/WebData';
|
||||
|
||||
export type SectionListDataType<ItemT> = Array<{
|
||||
title: string;
|
||||
data: Array<ItemT>;
|
||||
keyExtractor?: (data: ItemT) => string;
|
||||
export type SectionListDataType<T> = Array<{
|
||||
title: string,
|
||||
data: Array<T>,
|
||||
keyExtractor?: (T) => string,
|
||||
}>;
|
||||
|
||||
type PropsType<ItemT, RawData> = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
fetchUrl: string;
|
||||
autoRefreshTime: number;
|
||||
refreshOnFocus: boolean;
|
||||
renderItem: (data: {item: ItemT}) => React.ReactNode;
|
||||
type PropsType<T> = {
|
||||
navigation: StackNavigationProp,
|
||||
fetchUrl: string,
|
||||
autoRefreshTime: number,
|
||||
refreshOnFocus: boolean,
|
||||
renderItem: (data: {item: T}) => React.Node,
|
||||
createDataset: (
|
||||
data: RawData | null,
|
||||
data: ApiGenericDataType | null,
|
||||
isLoading?: boolean,
|
||||
) => SectionListDataType<ItemT>;
|
||||
onScroll: (event: NativeSyntheticEvent<EventTarget>) => void;
|
||||
collapsibleStack: Collapsible;
|
||||
) => SectionListDataType<T>,
|
||||
onScroll: (event: SyntheticEvent<EventTarget>) => void,
|
||||
collapsibleStack: Collapsible,
|
||||
|
||||
showError?: boolean;
|
||||
itemHeight?: number | null;
|
||||
updateData?: number;
|
||||
renderListHeaderComponent?: (
|
||||
data: RawData | null,
|
||||
) => React.ComponentType<any> | React.ReactElement | null;
|
||||
showError?: boolean,
|
||||
itemHeight?: number | null,
|
||||
updateData?: number,
|
||||
renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
|
||||
renderSectionHeader?: (
|
||||
data: {section: SectionListData<ItemT>},
|
||||
data: {section: {title: string}},
|
||||
isLoading?: boolean,
|
||||
) => React.ReactElement | null;
|
||||
stickyHeader?: boolean;
|
||||
) => React.Node,
|
||||
stickyHeader?: boolean,
|
||||
};
|
||||
|
||||
type StateType<RawData> = {
|
||||
refreshing: boolean;
|
||||
fetchedData: RawData | null;
|
||||
snackbarVisible: boolean;
|
||||
type StateType = {
|
||||
refreshing: boolean,
|
||||
fetchedData: ApiGenericDataType | null,
|
||||
snackbarVisible: boolean,
|
||||
};
|
||||
|
||||
const MIN_REFRESH_TIME = 5 * 1000;
|
||||
|
|
@ -82,25 +78,22 @@ const MIN_REFRESH_TIME = 5 * 1000;
|
|||
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
|
||||
* To force the component to update, change the value of updateData.
|
||||
*/
|
||||
class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
||||
PropsType<ItemT, RawData>,
|
||||
StateType<RawData>
|
||||
> {
|
||||
class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
|
||||
static defaultProps = {
|
||||
showError: true,
|
||||
itemHeight: null,
|
||||
updateData: 0,
|
||||
renderListHeaderComponent: () => null,
|
||||
renderSectionHeader: () => null,
|
||||
renderListHeaderComponent: (): React.Node => null,
|
||||
renderSectionHeader: (): React.Node => null,
|
||||
stickyHeader: false,
|
||||
};
|
||||
|
||||
refreshInterval: NodeJS.Timeout | undefined;
|
||||
refreshInterval: IntervalID;
|
||||
|
||||
lastRefresh: Date | undefined;
|
||||
lastRefresh: Date | null;
|
||||
|
||||
constructor(props: PropsType<ItemT, RawData>) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
refreshing: false,
|
||||
fetchedData: null,
|
||||
|
|
@ -116,7 +109,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
const {navigation} = this.props;
|
||||
navigation.addListener('focus', this.onScreenFocus);
|
||||
navigation.addListener('blur', this.onScreenBlur);
|
||||
this.lastRefresh = undefined;
|
||||
this.lastRefresh = null;
|
||||
this.onRefresh();
|
||||
}
|
||||
|
||||
|
|
@ -128,18 +121,15 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
if (props.refreshOnFocus && this.lastRefresh) {
|
||||
setTimeout(this.onRefresh, 200);
|
||||
}
|
||||
if (props.autoRefreshTime > 0) {
|
||||
if (props.autoRefreshTime > 0)
|
||||
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any interval on un-focus
|
||||
*/
|
||||
onScreenBlur = () => {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
clearInterval(this.refreshInterval);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -148,7 +138,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
*
|
||||
* @param fetchedData The newly fetched data
|
||||
*/
|
||||
onFetchSuccess = (fetchedData: RawData) => {
|
||||
onFetchSuccess = (fetchedData: ApiGenericDataType) => {
|
||||
this.setState({
|
||||
fetchedData,
|
||||
refreshing: false,
|
||||
|
|
@ -177,9 +167,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
if (this.lastRefresh != null) {
|
||||
const last = this.lastRefresh;
|
||||
canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
|
||||
} else {
|
||||
canRefresh = true;
|
||||
}
|
||||
} else canRefresh = true;
|
||||
if (canRefresh) {
|
||||
this.setState({refreshing: true});
|
||||
readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
|
||||
|
|
@ -201,18 +189,19 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
};
|
||||
|
||||
getItemLayout = (
|
||||
height: number,
|
||||
data: Array<SectionListData<ItemT>> | null,
|
||||
data: T,
|
||||
index: number,
|
||||
): {length: number; offset: number; index: number} => {
|
||||
): {length: number, offset: number, index: number} | null => {
|
||||
const {itemHeight} = this.props;
|
||||
if (itemHeight == null) return null;
|
||||
return {
|
||||
length: height,
|
||||
offset: height * index,
|
||||
length: itemHeight,
|
||||
offset: itemHeight * index,
|
||||
index,
|
||||
};
|
||||
};
|
||||
|
||||
getRenderSectionHeader = (data: {section: SectionListData<ItemT>}) => {
|
||||
getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
|
||||
const {renderSectionHeader} = this.props;
|
||||
const {refreshing} = this.state;
|
||||
if (renderSectionHeader != null) {
|
||||
|
|
@ -225,7 +214,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
return null;
|
||||
};
|
||||
|
||||
getRenderItem = (data: {item: ItemT}) => {
|
||||
getRenderItem = (data: {item: T}): React.Node => {
|
||||
const {renderItem} = this.props;
|
||||
return (
|
||||
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
|
||||
|
|
@ -234,23 +223,19 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
);
|
||||
};
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<EventTarget>) => {
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
const {onScroll} = this.props;
|
||||
if (onScroll != null) {
|
||||
onScroll(event);
|
||||
}
|
||||
if (onScroll != null) onScroll(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
const {itemHeight} = props;
|
||||
let dataset: SectionListDataType<ItemT> = [];
|
||||
let dataset = [];
|
||||
if (
|
||||
state.fetchedData != null ||
|
||||
(state.fetchedData == null && !props.showError)
|
||||
) {
|
||||
)
|
||||
dataset = props.createDataset(state.fetchedData, state.refreshing);
|
||||
}
|
||||
|
||||
const {containerPaddingTop} = props.collapsibleStack;
|
||||
return (
|
||||
|
|
@ -285,11 +270,7 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
/>
|
||||
)
|
||||
}
|
||||
getItemLayout={
|
||||
itemHeight
|
||||
? (data, index) => this.getItemLayout(itemHeight, data, index)
|
||||
: undefined
|
||||
}
|
||||
getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
|
||||
onScroll={this.onScroll}
|
||||
hasTab
|
||||
/>
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import WebView from 'react-native-webview';
|
||||
import {
|
||||
|
|
@ -25,17 +27,12 @@ import {
|
|||
OverflowMenu,
|
||||
} from 'react-navigation-header-buttons';
|
||||
import i18n from 'i18n-js';
|
||||
import {
|
||||
Animated,
|
||||
BackHandler,
|
||||
Linking,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
} from 'react-native';
|
||||
import {Animated, BackHandler, Linking} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
|
|
@ -43,15 +40,15 @@ import ErrorView from './ErrorView';
|
|||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
url: string;
|
||||
collapsibleStack: Collapsible;
|
||||
onMessage: (event: {nativeEvent: {data: string}}) => void;
|
||||
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
customJS?: string;
|
||||
customPaddingFunction?: null | ((padding: number) => string);
|
||||
showAdvancedControls?: boolean;
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
url: string,
|
||||
collapsibleStack: Collapsible,
|
||||
onMessage: (event: {nativeEvent: {data: string}}) => void,
|
||||
onScroll: (event: SyntheticEvent<EventTarget>) => void,
|
||||
customJS?: string,
|
||||
customPaddingFunction?: null | ((padding: number) => string),
|
||||
showAdvancedControls?: boolean,
|
||||
};
|
||||
|
||||
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
|
||||
|
|
@ -70,8 +67,8 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
|
||||
canGoBack: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
this.webviewRef = React.createRef();
|
||||
this.canGoBack = false;
|
||||
}
|
||||
|
|
@ -118,7 +115,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getBasicButton = () => {
|
||||
getBasicButton = (): React.Node => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
|
|
@ -141,7 +138,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getAdvancedButtons = () => {
|
||||
getAdvancedButtons = (): React.Node => {
|
||||
const {props} = this;
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
|
|
@ -182,7 +179,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
|
||||
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />;
|
||||
|
||||
/**
|
||||
* Gets the javascript needed to generate a padding on top of the page
|
||||
|
|
@ -204,21 +201,15 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
* Callback to use when refresh button is clicked. Reloads the webview.
|
||||
*/
|
||||
onRefreshClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.reload();
|
||||
}
|
||||
if (this.webviewRef.current != null) this.webviewRef.current.reload();
|
||||
};
|
||||
|
||||
onGoBackClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.goBack();
|
||||
}
|
||||
if (this.webviewRef.current != null) this.webviewRef.current.goBack();
|
||||
};
|
||||
|
||||
onGoForwardClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.goForward();
|
||||
}
|
||||
if (this.webviewRef.current != null) this.webviewRef.current.goForward();
|
||||
};
|
||||
|
||||
onOpenClicked = () => {
|
||||
|
|
@ -226,11 +217,9 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
Linking.openURL(url);
|
||||
};
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
const {onScroll} = this.props;
|
||||
if (onScroll) {
|
||||
onScroll(event);
|
||||
}
|
||||
if (onScroll) onScroll(event);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -239,12 +228,11 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
* @param script The script to inject
|
||||
*/
|
||||
injectJavaScript = (script: string) => {
|
||||
if (this.webviewRef.current != null) {
|
||||
if (this.webviewRef.current != null)
|
||||
this.webviewRef.current.injectJavaScript(script);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
|
||||
return (
|
||||
|
|
@ -255,7 +243,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
injectedJavaScript={props.customJS}
|
||||
javaScriptEnabled
|
||||
renderLoading={this.getRenderLoading}
|
||||
renderError={() => (
|
||||
renderError={(): React.Node => (
|
||||
<ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefreshClicked}
|
||||
|
|
@ -269,7 +257,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
|
||||
}}
|
||||
// Animations
|
||||
onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
|
||||
onScroll={onScrollWithListener(this.onScroll)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -27,8 +27,6 @@ export type NewsSourceType = {
|
|||
name: string;
|
||||
};
|
||||
|
||||
export type AvailablePages = 'amicale.deseleves' | 'campus.insat';
|
||||
|
||||
export default {
|
||||
'amicale.deseleves': {
|
||||
icon: ICON_AMICALE,
|
||||
|
|
|
|||
Loading…
Reference in a new issue