From 6b12b4cde2aba23f3f5bb40c60d3b4fd31648764 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Mon, 3 Aug 2020 18:36:52 +0200 Subject: [PATCH] Improve Home components to match linter --- src/components/Home/ActionsDashboardItem.js | 63 +- src/components/Home/EventDashboardItem.js | 161 +-- src/components/Home/FeedItem.js | 204 ++-- .../Home/PreviewEventDashboardItem.js | 162 +-- src/components/Home/SmallDashboardItem.js | 135 ++- src/screens/Home/FeedItemScreen.js | 180 ++-- src/screens/Home/HomeScreen.js | 998 ++++++++---------- src/screens/Home/ScannerScreen.js | 445 ++++---- src/utils/Home.js | 123 +++ 9 files changed, 1273 insertions(+), 1198 deletions(-) create mode 100644 src/utils/Home.js diff --git a/src/components/Home/ActionsDashboardItem.js b/src/components/Home/ActionsDashboardItem.js index 5b02df3..662c459 100644 --- a/src/components/Home/ActionsDashboardItem.js +++ b/src/components/Home/ActionsDashboardItem.js @@ -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 { +class ActionsDashBoardItem extends React.Component { + 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 ( - - } - right={props => } - onPress={() => this.props.navigation.navigate("feedback")} - style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}} - /> - - - ); - } + render(): React.Node { + const {props} = this; + return ( + + ( + + )} + right={({size}: {size: number}): React.Node => ( + + )} + onPress={(): void => props.navigation.navigate('feedback')} + style={{ + paddingTop: 0, + paddingBottom: 0, + marginLeft: 10, + marginRight: 10, + }} + /> + + ); + } } export default withTheme(ActionsDashBoardItem); diff --git a/src/components/Home/EventDashboardItem.js b/src/components/Home/EventDashboardItem.js index d66b8ee..f9f06b7 100644 --- a/src/components/Home/EventDashboardItem.js +++ b/src/components/Home/EventDashboardItem.js @@ -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 { +class EventDashBoardItem extends React.Component { + 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 = - - {props.eventNumber} - - {props.eventNumber > 1 - ? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural') - : i18n.t('screens.home.dashboard.todayEventsSubtitle')} - - ; - } else - subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA'); - return ( - - - - - } - /> - - {props.children} - - - - - ); - } + 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 = ( + + {props.eventNumber} + + {props.eventNumber > 1 + ? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural') + : i18n.t('screens.home.dashboard.todayEventsSubtitle')} + + + ); + } else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA'); + return ( + + + + ( + + )} + /> + {props.children} + + + + ); + } } -const styles = StyleSheet.create({ - card: { - width: 'auto', - marginLeft: 10, - marginRight: 10, - marginTop: 10, - overflow: 'hidden', - }, - avatar: { - backgroundColor: 'transparent' - } -}); - export default withTheme(EventDashBoardItem); diff --git a/src/components/Home/FeedItem.js b/src/components/Home/FeedItem.js index d887766..b28a2d3 100644 --- a/src/components/Home/FeedItem.js +++ b/src/components/Home/FeedItem.js @@ -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 { +class FeedItem extends React.Component { + 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 ( - + + + ( + - ); - } - - 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 ( - - - - - {hasImage ? - - : null} - - {item.message !== undefined ? - : null - } - - - - - - - - ); - } + }} + /> + )} + style={{height: titleHeight}} + /> + {hasImage ? ( + + + + ) : null} + + {item.message !== undefined ? ( + + ) : null} + + + + + + + + ); + } } export default FeedItem; diff --git a/src/components/Home/PreviewEventDashboardItem.js b/src/components/Home/PreviewEventDashboardItem.js index a8eb0c8..c9b9e4b 100644 --- a/src/components/Home/PreviewEventDashboardItem.js +++ b/src/components/Home/PreviewEventDashboardItem.js @@ -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 { +// eslint-disable-next-line react/prefer-stateless-function +class PreviewEventDashboardItem extends React.Component { + 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 = () => ; - return ( - - - - {hasImage ? - : - } - {!isEmpty ? - - - : null} + if (event != null) { + const hasImage = event.logo !== '' && event.logo != null; + const getImage = (): React.Node => ( + + ); + return ( + + + + {hasImage ? ( + + ) : ( + + )} + {!isEmpty ? ( + + + + ) : null} - - - - - - - ); - } else - return null; + + + + + + + ); } + 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; diff --git a/src/components/Home/SmallDashboardItem.js b/src/components/Home/SmallDashboardItem.js index efc70fd..5f2e5d6 100644 --- a/src/components/Home/SmallDashboardItem.js +++ b/src/components/Home/SmallDashboardItem.js @@ -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 { +class SmallDashboardItem extends React.Component { + 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 ( - - - - { - props.badgeCount != null && props.badgeCount > 0 ? - - {props.badgeCount} - : null - } - - - - ); - } + 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 ( + + + + {props.badgeCount != null && props.badgeCount > 0 ? ( + + {props.badgeCount} + + ) : null} + + + ); + } } export default withTheme(SmallDashboardItem); diff --git a/src/screens/Home/FeedItemScreen.js b/src/screens/Home/FeedItemScreen.js index 00eeef9..260af1f 100644 --- a/src/screens/Home/FeedItemScreen.js +++ b/src/screens/Home/FeedItemScreen.js @@ -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 { +class FeedItemScreen extends React.Component { + 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 - - ; - }; + /** + * Gets the out link header button + * + * @returns {*} + */ + getHeaderButton = (): React.Node => { + return ( + + + + ); + }; - /** - * Gets the Amicale INSA avatar - * - * @returns {*} - */ - getAvatar() { - return ( - - ); - } - - render() { - const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null; - return ( - - - {hasImage ? - - : null} - - {this.displayData.message !== undefined ? - : null - } - - - ); - } + render(): React.Node { + const hasImage = + this.displayData.full_picture !== '' && + this.displayData.full_picture != null; + return ( + + ( + + )} + /> + {hasImage ? ( + + + + ) : null} + + {this.displayData.message !== undefined ? ( + + ) : null} + + + ); + } } export default withTheme(FeedItemScreen); diff --git a/src/screens/Home/HomeScreen.js b/src/screens/Home/HomeScreen.js index c8321df..07082b2 100644 --- a/src/screens/Home/HomeScreen.js +++ b/src/screens/Home/HomeScreen.js @@ -2,580 +2,520 @@ import * as React from 'react'; import {FlatList} from 'react-native'; -import i18n from "i18n-js"; -import DashboardItem from "../../components/Home/EventDashboardItem"; -import WebSectionList from "../../components/Screens/WebSectionList"; +import i18n from 'i18n-js'; import {ActivityIndicator, Headline, withTheme} from 'react-native-paper'; -import FeedItem from "../../components/Home/FeedItem"; -import SmallDashboardItem from "../../components/Home/SmallDashboardItem"; -import PreviewEventDashboardItem from "../../components/Home/PreviewEventDashboardItem"; -import {stringToDate} from "../../utils/Planning"; -import ActionsDashBoardItem from "../../components/Home/ActionsDashboardItem"; import {CommonActions} from '@react-navigation/native'; -import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; -import AnimatedFAB from "../../components/Animations/AnimatedFAB"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../managers/ThemeManager"; -import * as Animatable from "react-native-animatable"; -import {View} from "react-native-animatable"; -import ConnectionManager from "../../managers/ConnectionManager"; -import LogoutDialog from "../../components/Amicale/LogoutDialog"; -import AsyncStorageManager from "../../managers/AsyncStorageManager"; -import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; -import MascotPopup from "../../components/Mascot/MascotPopup"; -import DashboardManager from "../../managers/DashboardManager"; -import type {ServiceItem} from "../../managers/ServicesManager"; -import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; +import {StackNavigationProp} from '@react-navigation/stack'; +import * as Animatable from 'react-native-animatable'; +import {View} from 'react-native-animatable'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import DashboardItem from '../../components/Home/EventDashboardItem'; +import WebSectionList from '../../components/Screens/WebSectionList'; +import FeedItem from '../../components/Home/FeedItem'; +import SmallDashboardItem from '../../components/Home/SmallDashboardItem'; +import PreviewEventDashboardItem from '../../components/Home/PreviewEventDashboardItem'; +import ActionsDashBoardItem from '../../components/Home/ActionsDashboardItem'; +import MaterialHeaderButtons, { + Item, +} from '../../components/Overrides/CustomHeaderButton'; +import AnimatedFAB from '../../components/Animations/AnimatedFAB'; +import type {CustomTheme} from '../../managers/ThemeManager'; +import ConnectionManager from '../../managers/ConnectionManager'; +import LogoutDialog from '../../components/Amicale/LogoutDialog'; +import AsyncStorageManager from '../../managers/AsyncStorageManager'; +import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; +import MascotPopup from '../../components/Mascot/MascotPopup'; +import DashboardManager from '../../managers/DashboardManager'; +import type {ServiceItem} from '../../managers/ServicesManager'; +import {getDisplayEvent, getFutureEvents} from '../../utils/Home'; // import DATA from "../dashboard_data.json"; - const NAME_AMICALE = 'Amicale INSA Toulouse'; -const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json"; +const DATA_URL = + 'https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json'; const FEED_ITEM_HEIGHT = 500; -const SECTIONS_ID = [ - 'dashboard', - 'news_feed' -]; +const SECTIONS_ID = ['dashboard', 'news_feed']; const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds -type rawDashboard = { - news_feed: { - data: Array, - }, - dashboard: fullDashboard, -} - -export type feedItem = { - full_picture: string, - message: string, - permalink_url: string, - created_time: number, - id: string, +export type FeedItemType = { + full_picture: string, + message: string, + permalink_url: string, + created_time: number, + id: string, }; -export type fullDashboard = { - today_menu: Array<{ [key: string]: any }>, - proximo_articles: number, - available_dryers: number, - available_washers: number, - today_events: Array<{ [key: string]: any }>, - available_tutorials: number, -} +export type EventType = { + id: number, + title: string, + logo: string | null, + date_begin: string, + date_end: string, + description: string, + club: string, + category_id: number, + url: string, +}; -export type event = { - id: number, - title: string, - logo: string | null, - date_begin: string, - date_end: string, - description: string, - club: string, - category_id: number, - url: string, -} +export type FullDashboardType = { + today_menu: Array<{[key: string]: {...}}>, + proximo_articles: number, + available_dryers: number, + available_washers: number, + today_events: Array, + available_tutorials: number, +}; -type Props = { - navigation: StackNavigationProp, - route: { params: any, ... }, - theme: CustomTheme, -} +type RawDashboardType = { + news_feed: { + data: Array, + }, + dashboard: FullDashboardType, +}; -type State = { - dialogVisible: boolean, -} +type PropsType = { + navigation: StackNavigationProp, + route: {params: {nextScreen: string, data: {...}}}, + theme: CustomTheme, +}; + +type StateType = { + dialogVisible: boolean, +}; /** * Class defining the app's home screen */ -class HomeScreen extends React.Component { +class HomeScreen extends React.Component { + isLoggedIn: boolean | null; - isLoggedIn: boolean | null; + fabRef: {current: null | AnimatedFAB}; - fabRef: { current: null | AnimatedFAB }; - currentNewFeed: Array; - currentDashboard: fullDashboard | null; + currentNewFeed: Array; - dashboardManager: DashboardManager; + currentDashboard: FullDashboardType | null; - constructor(props) { - super(props); - this.fabRef = React.createRef(); - this.dashboardManager = new DashboardManager(this.props.navigation); - this.currentNewFeed = []; - this.currentDashboard = null; - this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); - this.props.navigation.setOptions({ - headerRight: this.getHeaderButton, - }); - this.state = { - dialogVisible: false, - } - } + dashboardManager: DashboardManager; - /** - * 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) { - let date = new Date(dateString * 1000); - return date.toLocaleString(); - } - - componentDidMount() { - this.props.navigation.addListener('focus', this.onScreenFocus); - // Handle link open when home is focused - this.props.navigation.addListener('state', this.handleNavigationParams); - } - - /** - * Updates login state and navigation parameters on screen focus - */ - onScreenFocus = () => { - if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) { - this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); - this.props.navigation.setOptions({ - headerRight: this.getHeaderButton, - }); - } - // handle link open when home is not focused or created - this.handleNavigationParams(); + constructor(props: PropsType) { + super(props); + this.fabRef = React.createRef(); + this.dashboardManager = new DashboardManager(props.navigation); + this.currentNewFeed = []; + this.currentDashboard = null; + this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); + props.navigation.setOptions({ + headerRight: this.getHeaderButton, + }); + this.state = { + dialogVisible: false, }; + } - /** - * Navigates to the a new screen if navigation parameters specify one - */ - handleNavigationParams = () => { - if (this.props.route.params != null) { - if (this.props.route.params.nextScreen != null) { - this.props.navigation.navigate(this.props.route.params.nextScreen, this.props.route.params.data); - // reset params to prevent infinite loop - this.props.navigation.dispatch(CommonActions.setParams({nextScreen: null})); - } - } - }; + componentDidMount() { + const {props} = this; + props.navigation.addListener('focus', this.onScreenFocus); + // Handle link open when home is focused + props.navigation.addListener('state', this.handleNavigationParams); + } - /** - * Gets header buttons based on login state - * - * @returns {*} - */ - getHeaderButton = () => { - let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"}); - let logIcon = "login"; - let logColor = this.props.theme.colors.primary; - if (this.isLoggedIn) { - onPressLog = () => this.showDisconnectDialog(); - logIcon = "logout"; - logColor = this.props.theme.colors.text; - } + /** + * 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(); + } - const onPressSettings = () => this.props.navigation.navigate("settings"); - return - - - ; - }; + /** + * Updates login state and navigation parameters on screen focus + */ + onScreenFocus = () => { + const {props} = this; + if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) { + this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); + props.navigation.setOptions({ + headerRight: this.getHeaderButton, + }); + } + // handle link open when home is not focused or created + this.handleNavigationParams(); + }; - showDisconnectDialog = () => this.setState({dialogVisible: true}); - - hideDisconnectDialog = () => this.setState({dialogVisible: false}); - - openScanner = () => this.props.navigation.navigate("scanner"); - - /** - * Creates the dataset to be used in the FlatList - * - * @param fetchedData - * @param isLoading - * @return {*} - */ - createDataset = (fetchedData: rawDashboard | null, isLoading: boolean) => { - // fetchedData = DATA; - if (fetchedData != null) { - if (fetchedData.news_feed != null) - this.currentNewFeed = fetchedData.news_feed.data; - if (fetchedData.dashboard != null) - this.currentDashboard = fetchedData.dashboard; - } - if (this.currentNewFeed.length > 0) - return [ - { - title: i18n.t("screens.home.feedTitle"), - data: this.currentNewFeed, - id: SECTIONS_ID[1] - } - ]; - else - return [ - { - title: isLoading ? i18n.t("screens.home.feedLoading") : i18n.t("screens.home.feedError"), - data: [], - id: SECTIONS_ID[1] - } - ]; - }; - - /** - * 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 - */ - getTodayEventTimeLimit() { - let 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 header buttons based on login state + * + * @returns {*} + */ + getHeaderButton = (): React.Node => { + const {props} = this; + let onPressLog = (): void => + props.navigation.navigate('login', {nextScreen: 'profile'}); + let logIcon = 'login'; + let logColor = props.theme.colors.primary; + if (this.isLoggedIn) { + onPressLog = (): void => this.showDisconnectDialog(); + logIcon = 'logout'; + logColor = props.theme.colors.text; } - /** - * Gets the duration (in milliseconds) of an event - * - * @param event {event} - * @return {number} The number of milliseconds - */ - getEventDuration(event: event): number { - let start = stringToDate(event.date_begin); - let end = stringToDate(event.date_end); - let duration = 0; - if (start != null && end != null) - duration = end - start; - return duration; - } + const onPressSettings = (): void => props.navigation.navigate('settings'); + return ( + + + + + ); + }; - /** - * Gets events starting after the limit - * - * @param events - * @param limit - * @return {Array} - */ - getEventsAfterLimit(events: Array, limit: Date): Array { - let validEvents = []; - for (let event of events) { - let startDate = stringToDate(event.date_begin); - if (startDate != null && startDate >= limit) { - validEvents.push(event); - } - } - return validEvents; - } + /** + * Gets the event dashboard render item. + * If a preview is available, it will be rendered inside + * + * @param content + * @return {*} + */ + getDashboardEvent(content: Array): React.Node { + const futureEvents = getFutureEvents(content); + const displayEvent = getDisplayEvent(futureEvents); + // const clickPreviewAction = () => + // this.props.navigation.navigate('students', { + // screen: 'planning-information', + // params: {data: displayEvent} + // }); + return ( + + + + ); + } - /** - * 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 - */ - getLongestEvent(events: Array): event { - let longestEvent = events[0]; - let longestTime = 0; - for (let event of events) { - let time = this.getEventDuration(event); - if (time > longestTime) { - longestTime = time; - longestEvent = event; - } - } - return longestEvent; - } + /** + * Gets a dashboard item with action buttons + * + * @returns {*} + */ + getDashboardActions(): React.Node { + const {props} = this; + return ( + + ); + } - /** - * Gets events that have not yet ended/started - * - * @param events - */ - getFutureEvents(events: Array): Array { - let validEvents = []; - let now = new Date(); - for (let event of events) { - let startDate = stringToDate(event.date_begin); - let 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 a dashboard item with a row of shortcut buttons. + * + * @param content + * @return {*} + */ + getDashboardRow(content: Array): React.Node { + return ( + // $FlowFixMe + + ); + } - /** - * Gets the event to display in the preview - * - * @param events - * @return {Object} - */ - getDisplayEvent(events: Array): event | null { - let displayEvent = null; - if (events.length > 1) { - let eventsAfterLimit = this.getEventsAfterLimit(events, this.getTodayEventTimeLimit()); - if (eventsAfterLimit.length > 0) { - if (eventsAfterLimit.length === 1) - displayEvent = eventsAfterLimit[0]; - else - displayEvent = this.getLongestEvent(events); - } else { - displayEvent = this.getLongestEvent(events); - } - } else if (events.length === 1) { - displayEvent = events[0]; - } - return displayEvent; - } + /** + * Gets a dashboard shortcut item + * + * @param item + * @returns {*} + */ + getDashboardRowRenderItem = ({ + item, + }: { + item: ServiceItem | null, + }): React.Node => { + if (item != null) + return ( + + ); + return ; + }; - onEventContainerClick = () => this.props.navigation.navigate('planning'); + /** + * Gets a render item for the given feed object + * + * @param item The feed item to display + * @return {*} + */ + getFeedItem(item: FeedItemType): React.Node { + const {props} = this; + return ( + + ); + } - /** - * Gets the event dashboard render item. - * If a preview is available, it will be rendered inside - * - * @param content - * @return {*} - */ - getDashboardEvent(content: Array) { - let futureEvents = this.getFutureEvents(content); - let displayEvent = this.getDisplayEvent(futureEvents); - // const clickPreviewAction = () => - // this.props.navigation.navigate('students', { - // screen: 'planning-information', - // params: {data: displayEvent} - // }); - return ( - - - + /** + * Gets a FlatList render item + * + * @param item The item to display + * @param section The current section + * @return {*} + */ + getRenderItem = ({item}: {item: FeedItemType}): React.Node => + this.getFeedItem(item); + + getRenderSectionHeader = ( + data: { + section: { + data: Array<{...}>, + title: string, + }, + }, + isLoading: boolean, + ): React.Node => { + const {props} = this; + if (data.section.data.length > 0) + return ( + + {data.section.title} + + ); + return ( + + + {data.section.title} + + {isLoading ? ( + + ) : ( + + )} + + ); + }; + + getListHeader = (fetchedData: RawDashboardType): React.Node => { + let dashboard = null; + if (fetchedData != null) dashboard = fetchedData.dashboard; + return ( + + {this.getDashboardActions()} + {this.getDashboardRow(this.dashboardManager.getCurrentDashboard())} + {this.getDashboardEvent( + dashboard == null ? [] : dashboard.today_events, + )} + + ); + }; + + /** + * Navigates to the a new screen if navigation parameters specify one + */ + handleNavigationParams = () => { + const {props} = this; + if (props.route.params != null) { + if (props.route.params.nextScreen != null) { + props.navigation.navigate( + props.route.params.nextScreen, + props.route.params.data, ); + // reset params to prevent infinite loop + props.navigation.dispatch(CommonActions.setParams({nextScreen: null})); + } } + }; - /** - * Gets a dashboard item with action buttons - * - * @returns {*} - */ - getDashboardActions() { - return ; + showDisconnectDialog = (): void => this.setState({dialogVisible: true}); + + hideDisconnectDialog = (): void => this.setState({dialogVisible: false}); + + openScanner = () => { + const {props} = this; + props.navigation.navigate('scanner'); + }; + + /** + * Creates the dataset to be used in the FlatList + * + * @param fetchedData + * @param isLoading + * @return {*} + */ + createDataset = ( + fetchedData: RawDashboardType | null, + isLoading: boolean, + ): Array<{ + title: string, + data: [] | Array, + id: string, + }> => { + // fetchedData = DATA; + if (fetchedData != null) { + if (fetchedData.news_feed != null) + this.currentNewFeed = fetchedData.news_feed.data; + if (fetchedData.dashboard != null) + this.currentDashboard = fetchedData.dashboard; } + if (this.currentNewFeed.length > 0) + return [ + { + title: i18n.t('screens.home.feedTitle'), + data: this.currentNewFeed, + id: SECTIONS_ID[1], + }, + ]; + return [ + { + title: isLoading + ? i18n.t('screens.home.feedLoading') + : i18n.t('screens.home.feedError'), + data: [], + id: SECTIONS_ID[1], + }, + ]; + }; - /** - * Gets a dashboard item with a row of shortcut buttons. - * - * @param content - * @return {*} - */ - getDashboardRow(content: Array) { - return ( - //$FlowFixMe - ); - } + onEventContainerClick = () => { + const {props} = this; + props.navigation.navigate('planning'); + }; - /** - * Gets a dashboard shortcut item - * - * @param item - * @returns {*} - */ - dashboardRowRenderItem = ({item}: { item: ServiceItem }) => { - return ( - - ); - }; + onScroll = (event: SyntheticEvent) => { + if (this.fabRef.current != null) this.fabRef.current.onScroll(event); + }; - /** - * Gets a render item for the given feed object - * - * @param item The feed item to display - * @return {*} - */ - getFeedItem(item: feedItem) { - return ( - - ); - } + /** + * Callback when pressing the login button on the banner. + * This hides the banner and takes the user to the login page. + */ + onLogin = () => { + const {props} = this; + props.navigation.navigate('login', { + nextScreen: 'profile', + }); + }; - /** - * Gets a FlatList render item - * - * @param item The item to display - * @param section The current section - * @return {*} - */ - getRenderItem = ({item}: { item: feedItem, }) => this.getFeedItem(item); - - onScroll = (event: SyntheticEvent) => { - if (this.fabRef.current != null) - this.fabRef.current.onScroll(event); - }; - - renderSectionHeader = (data: { section: { [key: string]: any } }, isLoading: boolean) => { - if (data.section.data.length > 0) - return ( - - {data.section.title} - - ) - else - return ( - - - {data.section.title} - - {isLoading - ? - : } - - - ); - } - - getListHeader = (fetchedData: rawDashboard) => { - let dashboard = null; - if (fetchedData != null) { - dashboard = fetchedData.dashboard; - } - - return ( - - {this.getDashboardActions()} - {this.getDashboardRow(this.dashboardManager.getCurrentDashboard())} - {this.getDashboardEvent( - dashboard == null - ? [] - : dashboard.today_events - )} - - ); - } - - /** - * Callback when pressing the login button on the banner. - * This hides the banner and takes the user to the login page. - */ - onLogin = () => this.props.navigation.navigate("login", {nextScreen: "profile"}); - - render() { - return ( - - - - - {!this.isLoggedIn - ? : null} - - - - ); - } + render(): React.Node { + const {props, state} = this; + return ( + + + + + {!this.isLoggedIn ? ( + + ) : null} + + + + ); + } } export default withTheme(HomeScreen); diff --git a/src/screens/Home/ScannerScreen.js b/src/screens/Home/ScannerScreen.js index 30e370b..79103e6 100644 --- a/src/screens/Home/ScannerScreen.js +++ b/src/screens/Home/ScannerScreen.js @@ -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 { - - 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 - {i18n.t("screens.scanner.permissions.error")} - - - } - - /** - * 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 ( - - - - ); - } - - render() { - return ( - - {this.state.hasPermission - ? this.getScanner() - : this.getPermissionScreen() - } - - - - - - ); - } -} - 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 { + 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 ( + + {i18n.t('screens.scanner.permissions.error')} + + + ); + } + + /** + * 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 ( + + + + ); + } + + /** + * 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 ( + + {state.hasPermission ? this.getScanner() : this.getPermissionScreen()} + + + + + + ); + } +} + export default withTheme(ScannerScreen); diff --git a/src/utils/Home.js b/src/utils/Home.js new file mode 100644 index 0000000..e717003 --- /dev/null +++ b/src/utils/Home.js @@ -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} + */ +export function getEventsAfterLimit( + events: Array, + limit: Date, +): Array { + 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 { + 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): Array { + 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 | 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; +}