From 7e90b80ca2e281cc063c0d8015e79c1967c087ab Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Mon, 30 Mar 2020 15:28:08 +0200 Subject: [PATCH] Improved project structure --- App.js | 12 +- __tests__/utils/PlanningEventManager.test.js | 210 +++++++++++++++ components/PreviewEventDashboardItem.js | 8 +- components/WebSectionList.js | 7 +- coverage/clover.xml | 2 +- .../lcov-report/PlanningEventManager.js.html | 25 +- coverage/lcov-report/index.html | 23 +- coverage/lcov.info | 2 +- {utils => managers}/AprilFoolsManager.js | 0 {utils => managers}/AsyncStorageManager.js | 0 {utils => managers}/DateManager.js | 0 {utils => managers}/LocaleManager.js | 0 {utils => managers}/ThemeManager.js | 0 navigation/MainTabNavigator.js | 2 +- screens/About/AboutScreen.js | 2 +- screens/About/DebugScreen.js | 2 +- screens/HomeScreen.js | 12 +- screens/Planning/PlanningDisplayScreen.js | 8 +- screens/Planning/PlanningScreen.js | 23 +- screens/Proxiwash/ProxiwashScreen.js | 14 +- screens/SelfMenuScreen.js | 4 +- screens/SettingsScreen.js | 8 +- screens/Websites/PlanexScreen.js | 4 +- utils/Notifications.js | 125 +++++++++ utils/NotificationsManager.js | 131 ---------- utils/Planning.js | 240 +++++++++++++++++ utils/PlanningEventManager.js | 243 ------------------ utils/WebData.js | 19 ++ utils/WebDataManager.js | 34 --- utils/__test__/PlanningEventManager.test.js | 210 --------------- 30 files changed, 672 insertions(+), 698 deletions(-) create mode 100644 __tests__/utils/PlanningEventManager.test.js rename {utils => managers}/AprilFoolsManager.js (100%) rename {utils => managers}/AsyncStorageManager.js (100%) rename {utils => managers}/DateManager.js (100%) rename {utils => managers}/LocaleManager.js (100%) rename {utils => managers}/ThemeManager.js (100%) create mode 100644 utils/Notifications.js delete mode 100644 utils/NotificationsManager.js create mode 100644 utils/Planning.js delete mode 100644 utils/PlanningEventManager.js create mode 100644 utils/WebData.js delete mode 100644 utils/WebDataManager.js delete mode 100644 utils/__test__/PlanningEventManager.test.js diff --git a/App.js b/App.js index 585b137..a134ba6 100644 --- a/App.js +++ b/App.js @@ -2,17 +2,17 @@ import * as React from 'react'; import {Platform, StatusBar} from 'react-native'; -import LocaleManager from './utils/LocaleManager'; -import AsyncStorageManager from "./utils/AsyncStorageManager"; +import LocaleManager from './managers/LocaleManager'; +import AsyncStorageManager from "./managers/AsyncStorageManager"; import CustomIntroSlider from "./components/CustomIntroSlider"; import {SplashScreen} from 'expo'; -import ThemeManager from './utils/ThemeManager'; +import ThemeManager from './managers/ThemeManager'; import {NavigationContainer} from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack'; import DrawerNavigator from './navigation/DrawerNavigator'; -import NotificationsManager from "./utils/NotificationsManager"; +import {initExpoToken} from "./utils/Notifications"; import {Provider as PaperProvider} from 'react-native-paper'; -import AprilFoolsManager from "./utils/AprilFoolsManager"; +import AprilFoolsManager from "./managers/AprilFoolsManager"; import Update from "./constants/Update"; type Props = {}; @@ -90,7 +90,7 @@ export default class App extends React.Component { // Wait for custom fonts to be loaded before showing the app await AsyncStorageManager.getInstance().loadPreferences(); ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme); - await NotificationsManager.initExpoToken(); + await initExpoToken(); this.onLoadFinished(); } diff --git a/__tests__/utils/PlanningEventManager.test.js b/__tests__/utils/PlanningEventManager.test.js new file mode 100644 index 0000000..2970924 --- /dev/null +++ b/__tests__/utils/PlanningEventManager.test.js @@ -0,0 +1,210 @@ +import React from 'react'; +import * as Planning from "../../utils/Planning"; + +test('isDescriptionEmpty', () => { + expect(Planning.isDescriptionEmpty("")).toBeTrue(); + expect(Planning.isDescriptionEmpty(" ")).toBeTrue(); + // noinspection CheckTagEmptyBody + expect(Planning.isDescriptionEmpty("

")).toBeTrue(); + expect(Planning.isDescriptionEmpty("

")).toBeTrue(); + expect(Planning.isDescriptionEmpty("


")).toBeTrue(); + expect(Planning.isDescriptionEmpty("



")).toBeTrue(); + expect(Planning.isDescriptionEmpty("




")).toBeTrue(); + expect(Planning.isDescriptionEmpty("


")).toBeTrue(); + expect(Planning.isDescriptionEmpty(null)).toBeTrue(); + expect(Planning.isDescriptionEmpty(undefined)).toBeTrue(); + expect(Planning.isDescriptionEmpty("coucou")).toBeFalse(); + expect(Planning.isDescriptionEmpty("

coucou

")).toBeFalse(); +}); + +test('isEventDateStringFormatValid', () => { + expect(Planning.isEventDateStringFormatValid("2020-03-21 09:00")).toBeTrue(); + expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16")).toBeTrue(); + + expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("garbage")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid("")).toBeFalse(); + expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse(); + expect(Planning.isEventDateStringFormatValid(null)).toBeFalse(); +}); + +test('stringToDate', () => { + let testDate = new Date(); + expect(Planning.stringToDate(undefined)).toBeNull(); + expect(Planning.stringToDate("")).toBeNull(); + expect(Planning.stringToDate("garbage")).toBeNull(); + expect(Planning.stringToDate("2020-03-21")).toBeNull(); + expect(Planning.stringToDate("09:00:00")).toBeNull(); + expect(Planning.stringToDate("2020-03-21 09:g0")).toBeNull(); + expect(Planning.stringToDate("2020-03-21 09:g0:")).toBeNull(); + testDate.setFullYear(2020, 2, 21); + testDate.setHours(9, 0, 0, 0); + expect(Planning.stringToDate("2020-03-21 09:00")).toEqual(testDate); + testDate.setFullYear(2020, 0, 31); + testDate.setHours(18, 30, 0, 0); + expect(Planning.stringToDate("2020-01-31 18:30")).toEqual(testDate); + testDate.setFullYear(2020, 50, 50); + testDate.setHours(65, 65, 0, 0); + expect(Planning.stringToDate("2020-51-50 65:65")).toEqual(testDate); +}); + +test('getFormattedEventTime', () => { + expect(Planning.getFormattedEventTime(null, null)) + .toBe('/ - /'); + expect(Planning.getFormattedEventTime(undefined, undefined)) + .toBe('/ - /'); + expect(Planning.getFormattedEventTime("20:30", "23:00")) + .toBe('/ - /'); + expect(Planning.getFormattedEventTime("2020-03-30", "2020-03-31")) + .toBe('/ - /'); + + + expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00")) + .toBe('09:00'); + expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00")) + .toBe('09:00 - 23:59'); + expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00")) + .toBe('20:30 - 23:00'); +}); + +test('getDateOnlyString', () => { + expect(Planning.getDateOnlyString("2020-03-21 09:00")).toBe("2020-03-21"); + expect(Planning.getDateOnlyString("2021-12-15 09:00")).toBe("2021-12-15"); + expect(Planning.getDateOnlyString("2021-12-o5 09:00")).toBeNull(); + expect(Planning.getDateOnlyString("2021-12-15 09:")).toBeNull(); + expect(Planning.getDateOnlyString("2021-12-15")).toBeNull(); + expect(Planning.getDateOnlyString("garbage")).toBeNull(); +}); + +test('isEventBefore', () => { + expect(Planning.isEventBefore( + "2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue(); + expect(Planning.isEventBefore( + "2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue(); + expect(Planning.isEventBefore( + "2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue(); + expect(Planning.isEventBefore( + "2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue(); + expect(Planning.isEventBefore( + "2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue(); + + expect(Planning.isEventBefore( + "2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse(); + expect(Planning.isEventBefore( + "2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse(); + expect(Planning.isEventBefore( + "2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse(); + expect(Planning.isEventBefore( + "2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse(); + expect(Planning.isEventBefore( + "2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse(); + expect(Planning.isEventBefore( + "2020-03-30 10:15", "2020-03-21 10:15")).toBeFalse(); + + expect(Planning.isEventBefore( + "garbage", "2020-03-21 10:15")).toBeFalse(); + expect(Planning.isEventBefore( + undefined, undefined)).toBeFalse(); +}); + +test('dateToString', () => { + let testDate = new Date(); + testDate.setFullYear(2020, 2, 21); + testDate.setHours(9, 0, 0, 0); + expect(Planning.dateToString(testDate)).toBe("2020-03-21 09:00"); + testDate.setFullYear(2021, 0, 12); + testDate.setHours(9, 10, 0, 0); + expect(Planning.dateToString(testDate)).toBe("2021-01-12 09:10"); + testDate.setFullYear(2022, 11, 31); + testDate.setHours(9, 10, 15, 0); + expect(Planning.dateToString(testDate)).toBe("2022-12-31 09:10"); +}); + +test('generateEmptyCalendar', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => + new Date('2020-01-14T00:00:00.000Z').getTime() + ); + let calendar = Planning.generateEmptyCalendar(1); + expect(calendar).toHaveProperty("2020-01-14"); + expect(calendar).toHaveProperty("2020-01-20"); + expect(calendar).toHaveProperty("2020-02-10"); + expect(Object.keys(calendar).length).toBe(32); + calendar = Planning.generateEmptyCalendar(3); + expect(calendar).toHaveProperty("2020-01-14"); + expect(calendar).toHaveProperty("2020-01-20"); + expect(calendar).toHaveProperty("2020-02-10"); + expect(calendar).toHaveProperty("2020-02-14"); + expect(calendar).toHaveProperty("2020-03-20"); + expect(calendar).toHaveProperty("2020-04-12"); + expect(Object.keys(calendar).length).toBe(92); +}); + +test('pushEventInOrder', () => { + let eventArray = []; + let event1 = {date_begin: "2020-01-14 09:15"}; + Planning.pushEventInOrder(eventArray, event1); + expect(eventArray.length).toBe(1); + expect(eventArray[0]).toBe(event1); + + let event2 = {date_begin: "2020-01-14 10:15"}; + Planning.pushEventInOrder(eventArray, event2); + expect(eventArray.length).toBe(2); + expect(eventArray[0]).toBe(event1); + expect(eventArray[1]).toBe(event2); + + let event3 = {date_begin: "2020-01-14 10:15", title: "garbage"}; + Planning.pushEventInOrder(eventArray, event3); + expect(eventArray.length).toBe(3); + expect(eventArray[0]).toBe(event1); + expect(eventArray[1]).toBe(event2); + expect(eventArray[2]).toBe(event3); + + let event4 = {date_begin: "2020-01-13 09:00"}; + Planning.pushEventInOrder(eventArray, event4); + expect(eventArray.length).toBe(4); + expect(eventArray[0]).toBe(event4); + expect(eventArray[1]).toBe(event1); + expect(eventArray[2]).toBe(event2); + expect(eventArray[3]).toBe(event3); +}); + +test('generateEventAgenda', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => + new Date('2020-01-14T00:00:00.000Z').getTime() + ); + let eventList = [ + {date_begin: "2020-01-14 09:15"}, + {date_begin: "2020-02-01 09:15"}, + {date_begin: "2020-01-15 09:15"}, + {date_begin: "2020-02-01 09:30"}, + {date_begin: "2020-02-01 08:30"}, + ]; + const calendar = Planning.generateEventAgenda(eventList, 2); + expect(calendar["2020-01-14"].length).toBe(1); + expect(calendar["2020-01-14"][0]).toBe(eventList[0]); + expect(calendar["2020-01-15"].length).toBe(1); + expect(calendar["2020-01-15"][0]).toBe(eventList[2]); + expect(calendar["2020-02-01"].length).toBe(3); + expect(calendar["2020-02-01"][0]).toBe(eventList[4]); + expect(calendar["2020-02-01"][1]).toBe(eventList[1]); + expect(calendar["2020-02-01"][2]).toBe(eventList[3]); +}); + +test('getCurrentDateString', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => { + let date = new Date(); + date.setFullYear(2020, 0, 14); + date.setHours(15, 30, 54, 65); + return date.getTime(); + }); + expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30'); +}); diff --git a/components/PreviewEventDashboardItem.js b/components/PreviewEventDashboardItem.js index c4aafbd..b459255 100644 --- a/components/PreviewEventDashboardItem.js +++ b/components/PreviewEventDashboardItem.js @@ -5,7 +5,7 @@ import {StyleSheet, View} from "react-native"; import HTML from "react-native-render-html"; import i18n from "i18n-js"; import {Avatar, Button, Card, withTheme} from 'react-native-paper'; -import PlanningEventManager from "../utils/PlanningEventManager"; +import {getFormattedEventTime, isDescriptionEmpty} from "../utils/Planning"; /** * Component used to display an event preview if an event is available @@ -17,7 +17,7 @@ function PreviewEventDashboardItem(props) { const {colors} = props.theme; const isEmpty = props.event === undefined ? true - : PlanningEventManager.isDescriptionEmpty(props.event['description']); + : isDescriptionEmpty(props.event['description']); if (props.event !== undefined && props.event !== null) { const hasImage = props.event['logo'] !== '' && props.event['logo'] !== null; @@ -34,12 +34,12 @@ function PreviewEventDashboardItem(props) { {hasImage ? : } {!isEmpty ? diff --git a/components/WebSectionList.js b/components/WebSectionList.js index c09dbca..3d0bdd2 100644 --- a/components/WebSectionList.js +++ b/components/WebSectionList.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import WebDataManager from "../utils/WebDataManager"; +import {readData} from "../utils/WebData"; import i18n from "i18n-js"; import {Snackbar} from 'react-native-paper'; import {RefreshControl, SectionList, View} from "react-native"; @@ -42,8 +42,6 @@ export default class WebSectionList extends React.PureComponent { updateData: 0, }; - webDataManager: WebDataManager; - refreshInterval: IntervalID; lastRefresh: Date; @@ -79,7 +77,6 @@ export default class WebSectionList extends React.PureComponent { * Allows to detect when the screen is focused */ componentDidMount() { - this.webDataManager = new WebDataManager(this.props.fetchUrl); const onScreenFocus = this.onScreenFocus.bind(this); const onScreenBlur = this.onScreenBlur.bind(this); this.props.navigation.addListener('focus', onScreenFocus); @@ -144,7 +141,7 @@ export default class WebSectionList extends React.PureComponent { canRefresh = true; if (canRefresh) { this.setState({refreshing: true}); - this.webDataManager.readData() + readData(this.props.fetchUrl) .then(this.onFetchSuccess) .catch(this.onFetchError); } diff --git a/coverage/clover.xml b/coverage/clover.xml index 8e579e1..76c61dd 100644 --- a/coverage/clover.xml +++ b/coverage/clover.xml @@ -2,7 +2,7 @@ - + diff --git a/coverage/lcov-report/PlanningEventManager.js.html b/coverage/lcov-report/PlanningEventManager.js.html index bef4141..365e21e 100644 --- a/coverage/lcov-report/PlanningEventManager.js.html +++ b/coverage/lcov-report/PlanningEventManager.js.html @@ -3,7 +3,7 @@ - Code coverage report for PlanningEventManager.js + Code coverage report for Planning.js @@ -15,41 +15,41 @@ } - +
-

All files PlanningEventManager.js

+

All files Planning.js

- +
100% Statements 68/68
- - + +
92.31% Branches 36/39
- - + +
100% Functions 11/11
- - + +
100% Lines 65/65
- - + +

Press n or j to go to the next uncovered block, b, p or k for the previous block. @@ -806,4 +806,3 @@ export default class PlanningEventManager { - \ No newline at end of file diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html index e19d1b9..463220c 100644 --- a/coverage/lcov-report/index.html +++ b/coverage/lcov-report/index.html @@ -15,41 +15,41 @@ } - +

All files

- +
100% Statements 68/68
- - + +
92.31% Branches 36/39
- - + +
100% Functions 11/11
- - + +
100% Lines 65/65
- - + +

Press n or j to go to the next uncovered block, b, p or k for the previous block. @@ -73,7 +73,7 @@ - PlanningEventManager.js + Planning.js

@@ -108,4 +108,3 @@ - \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info index d6659e6..3c94fad 100644 --- a/coverage/lcov.info +++ b/coverage/lcov.info @@ -1,5 +1,5 @@ TN: -SF:utils/PlanningEventManager.js +SF:utils/Planning.js FN:26,(anonymous_0) FN:37,(anonymous_1) FN:53,(anonymous_2) diff --git a/utils/AprilFoolsManager.js b/managers/AprilFoolsManager.js similarity index 100% rename from utils/AprilFoolsManager.js rename to managers/AprilFoolsManager.js diff --git a/utils/AsyncStorageManager.js b/managers/AsyncStorageManager.js similarity index 100% rename from utils/AsyncStorageManager.js rename to managers/AsyncStorageManager.js diff --git a/utils/DateManager.js b/managers/DateManager.js similarity index 100% rename from utils/DateManager.js rename to managers/DateManager.js diff --git a/utils/LocaleManager.js b/managers/LocaleManager.js similarity index 100% rename from utils/LocaleManager.js rename to managers/LocaleManager.js diff --git a/utils/ThemeManager.js b/managers/ThemeManager.js similarity index 100% rename from utils/ThemeManager.js rename to managers/ThemeManager.js diff --git a/navigation/MainTabNavigator.js b/navigation/MainTabNavigator.js index 3392976..b75bc35 100644 --- a/navigation/MainTabNavigator.js +++ b/navigation/MainTabNavigator.js @@ -12,7 +12,7 @@ import ProximoListScreen from "../screens/Proximo/ProximoListScreen"; import ProximoAboutScreen from "../screens/Proximo/ProximoAboutScreen"; import PlanexScreen from '../screens/Websites/PlanexScreen'; import {MaterialCommunityIcons} from "@expo/vector-icons"; -import AsyncStorageManager from "../utils/AsyncStorageManager"; +import AsyncStorageManager from "../managers/AsyncStorageManager"; import HeaderButton from "../components/HeaderButton"; import {withTheme} from 'react-native-paper'; import i18n from "i18n-js"; diff --git a/screens/About/AboutScreen.js b/screens/About/AboutScreen.js index 022d8f4..615f872 100644 --- a/screens/About/AboutScreen.js +++ b/screens/About/AboutScreen.js @@ -4,7 +4,7 @@ import * as React from 'react'; import {FlatList, Linking, Platform, View} from 'react-native'; import i18n from "i18n-js"; import appJson from '../../app'; -import AsyncStorageManager from "../../utils/AsyncStorageManager"; +import AsyncStorageManager from "../../managers/AsyncStorageManager"; import CustomModal from "../../components/CustomModal"; import {Avatar, Button, Card, List, Text, Title, withTheme} from 'react-native-paper'; diff --git a/screens/About/DebugScreen.js b/screens/About/DebugScreen.js index 5cbf141..c13cea8 100644 --- a/screens/About/DebugScreen.js +++ b/screens/About/DebugScreen.js @@ -2,7 +2,7 @@ import * as React from 'react'; import {ScrollView, View} from "react-native"; -import AsyncStorageManager from "../../utils/AsyncStorageManager"; +import AsyncStorageManager from "../../managers/AsyncStorageManager"; import CustomModal from "../../components/CustomModal"; import {Button, Card, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper'; diff --git a/screens/HomeScreen.js b/screens/HomeScreen.js index 4dc65b3..7fdae18 100644 --- a/screens/HomeScreen.js +++ b/screens/HomeScreen.js @@ -10,7 +10,7 @@ import {Text, withTheme} from 'react-native-paper'; import FeedItem from "../components/FeedItem"; import SquareDashboardItem from "../components/SquareDashboardItem"; import PreviewEventDashboardItem from "../components/PreviewEventDashboardItem"; -import PlanningEventManager from "../utils/PlanningEventManager"; +import {stringToDate} from "../utils/Planning"; // import DATA from "../dashboard_data.json"; @@ -201,8 +201,8 @@ class HomeScreen extends React.Component { * @return {number} The number of milliseconds */ getEventDuration(event: Object): number { - let start = PlanningEventManager.stringToDate(event['date_begin']); - let end = PlanningEventManager.stringToDate(event['date_end']); + let start = stringToDate(event['date_begin']); + let end = stringToDate(event['date_end']); let duration = 0; if (start !== undefined && start !== null && end !== undefined && end !== null) duration = end - start; @@ -219,7 +219,7 @@ class HomeScreen extends React.Component { getEventsAfterLimit(events: Object, limit: Date): Array { let validEvents = []; for (let event of events) { - let startDate = PlanningEventManager.stringToDate(event['date_begin']); + let startDate = stringToDate(event['date_begin']); if (startDate !== undefined && startDate !== null && startDate >= limit) { validEvents.push(event); } @@ -255,8 +255,8 @@ class HomeScreen extends React.Component { let validEvents = []; let now = new Date(); for (let event of events) { - let startDate = PlanningEventManager.stringToDate(event['date_begin']); - let endDate = PlanningEventManager.stringToDate(event['date_end']); + let startDate = stringToDate(event['date_begin']); + let endDate = stringToDate(event['date_end']); if (startDate !== undefined && startDate !== null) { if (startDate > now) validEvents.push(event); diff --git a/screens/Planning/PlanningDisplayScreen.js b/screens/Planning/PlanningDisplayScreen.js index 6535f99..7d0db4f 100644 --- a/screens/Planning/PlanningDisplayScreen.js +++ b/screens/Planning/PlanningDisplayScreen.js @@ -4,9 +4,9 @@ import * as React from 'react'; import {Image, ScrollView, View} from 'react-native'; import HTML from "react-native-render-html"; import {Linking} from "expo"; -import PlanningEventManager from '../../utils/PlanningEventManager'; +import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning'; import {Card, withTheme} from 'react-native-paper'; -import DateManager from "../../utils/DateManager"; +import DateManager from "../../managers/DateManager"; type Props = { navigation: Object, @@ -33,9 +33,9 @@ class PlanningDisplayScreen extends React.Component { render() { // console.log("rendering planningDisplayScreen"); - let subtitle = PlanningEventManager.getFormattedEventTime( + let subtitle = getFormattedEventTime( this.displayData["date_begin"], this.displayData["date_end"]); - let dateString = PlanningEventManager.getDateOnlyString(this.displayData["date_begin"]); + let dateString = getDateOnlyString(this.displayData["date_begin"]); if (dateString !== null) subtitle += ' | ' + DateManager.getInstance().getTranslatedDate(dateString); return ( diff --git a/screens/Planning/PlanningScreen.js b/screens/Planning/PlanningScreen.js index 54cf7f4..4e8805f 100644 --- a/screens/Planning/PlanningScreen.js +++ b/screens/Planning/PlanningScreen.js @@ -4,9 +4,14 @@ import * as React from 'react'; import {BackHandler, View} from 'react-native'; import i18n from "i18n-js"; import {LocaleConfig} from 'react-native-calendars'; -import WebDataManager from "../../utils/WebDataManager"; -import type {eventObject} from "../../utils/PlanningEventManager"; -import PlanningEventManager from '../../utils/PlanningEventManager'; +import {readData} from "../../utils/WebData"; +import type {eventObject} from "../../utils/Planning"; +import { + generateEventAgenda, + getCurrentDateString, + getDateOnlyString, + getFormattedEventTime, +} from '../../utils/Planning'; import {Avatar, Divider, List} from 'react-native-paper'; import CustomAgenda from "../../components/CustomAgenda"; @@ -38,7 +43,6 @@ const AGENDA_MONTH_SPAN = 3; export default class PlanningScreen extends React.Component { agendaRef: Object; - webDataManager: WebDataManager; lastRefresh: Date; minTimeBetweenRefresh = 60; @@ -59,11 +63,10 @@ export default class PlanningScreen extends React.Component { onAgendaRef: Function; onCalendarToggled: Function; onBackButtonPressAndroid: Function; - currentDate = PlanningEventManager.getDateOnlyString(PlanningEventManager.getCurrentDateString()); + currentDate = getDateOnlyString(getCurrentDateString()); constructor(props: any) { super(props); - this.webDataManager = new WebDataManager(FETCH_URL); if (i18n.currentLocale().startsWith("fr")) { LocaleConfig.defaultLocale = 'fr'; } @@ -141,11 +144,11 @@ export default class PlanningScreen extends React.Component { if (canRefresh) { this.setState({refreshing: true}); - this.webDataManager.readData() + readData(FETCH_URL) .then((fetchedData) => { this.setState({ refreshing: false, - agendaItems: PlanningEventManager.generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN) + agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN) }); this.lastRefresh = new Date(); }) @@ -189,7 +192,7 @@ export default class PlanningScreen extends React.Component { { diff --git a/screens/Proxiwash/ProxiwashScreen.js b/screens/Proxiwash/ProxiwashScreen.js index e9a00a1..a8b0d40 100644 --- a/screens/Proxiwash/ProxiwashScreen.js +++ b/screens/Proxiwash/ProxiwashScreen.js @@ -4,15 +4,15 @@ import * as React from 'react'; import {Alert, Platform, View} from 'react-native'; import i18n from "i18n-js"; import WebSectionList from "../../components/WebSectionList"; -import NotificationsManager from "../../utils/NotificationsManager"; -import AsyncStorageManager from "../../utils/AsyncStorageManager"; +import * as Notifications from "../../utils/Notifications"; +import AsyncStorageManager from "../../managers/AsyncStorageManager"; import * as Expo from "expo"; import {Avatar, Banner, Button, Card, Text, withTheme} from 'react-native-paper'; import HeaderButton from "../../components/HeaderButton"; import ProxiwashListItem from "../../components/ProxiwashListItem"; import ProxiwashConstants from "../../constants/ProxiwashConstants"; import CustomModal from "../../components/CustomModal"; -import AprilFoolsManager from "../../utils/AprilFoolsManager"; +import AprilFoolsManager from "../../managers/AprilFoolsManager"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json"; @@ -118,12 +118,12 @@ class ProxiwashScreen extends React.Component { }); if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') { // Get latest watchlist from server - NotificationsManager.getMachineNotificationWatchlist((fetchedList) => { + Notifications.getMachineNotificationWatchlist((fetchedList) => { this.setState({machinesWatched: fetchedList}) }); // Get updated watchlist after received notification Expo.Notifications.addListener(() => { - NotificationsManager.getMachineNotificationWatchlist((fetchedList) => { + Notifications.getMachineNotificationWatchlist((fetchedList) => { this.setState({machinesWatched: fetchedList}) }); }); @@ -175,7 +175,7 @@ class ProxiwashScreen extends React.Component { setupNotifications(machineId: string) { if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') { if (!this.isMachineWatched(machineId)) { - NotificationsManager.setupMachineNotification(machineId, true); + Notifications.setupMachineNotification(machineId, true); this.saveNotificationToState(machineId); } else this.disableNotification(machineId); @@ -205,7 +205,7 @@ class ProxiwashScreen extends React.Component { if (data.length > 0) { let arrayIndex = data.indexOf(machineId); if (arrayIndex !== -1) { - NotificationsManager.setupMachineNotification(machineId, false); + Notifications.setupMachineNotification(machineId, false); this.removeNotificationFroState(arrayIndex); } } diff --git a/screens/SelfMenuScreen.js b/screens/SelfMenuScreen.js index 7e09920..b2d8f8d 100644 --- a/screens/SelfMenuScreen.js +++ b/screens/SelfMenuScreen.js @@ -2,10 +2,10 @@ import * as React from 'react'; import {View} from 'react-native'; -import DateManager from "../utils/DateManager"; +import DateManager from "../managers/DateManager"; import WebSectionList from "../components/WebSectionList"; import {Card, Text, withTheme} from 'react-native-paper'; -import AprilFoolsManager from "../utils/AprilFoolsManager"; +import AprilFoolsManager from "../managers/AprilFoolsManager"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json"; diff --git a/screens/SettingsScreen.js b/screens/SettingsScreen.js index a275e21..eb45f20 100644 --- a/screens/SettingsScreen.js +++ b/screens/SettingsScreen.js @@ -2,10 +2,10 @@ import * as React from 'react'; import {ScrollView} from "react-native"; -import ThemeManager from '../utils/ThemeManager'; +import ThemeManager from '../managers/ThemeManager'; import i18n from "i18n-js"; -import AsyncStorageManager from "../utils/AsyncStorageManager"; -import NotificationsManager from "../utils/NotificationsManager"; +import AsyncStorageManager from "../managers/AsyncStorageManager"; +import {setMachineReminderNotificationTime} from "../utils/Notifications"; import {Card, List, Switch, ToggleButton} from 'react-native-paper'; import {Appearance} from "react-native-appearance"; @@ -60,7 +60,7 @@ export default class SettingsScreen extends React.Component { let intVal = 0; if (value !== 'never') intVal = parseInt(value); - NotificationsManager.setMachineReminderNotificationTime(intVal); + setMachineReminderNotificationTime(intVal); } } diff --git a/screens/Websites/PlanexScreen.js b/screens/Websites/PlanexScreen.js index 13bb50a..fa33e8d 100644 --- a/screens/Websites/PlanexScreen.js +++ b/screens/Websites/PlanexScreen.js @@ -1,12 +1,12 @@ // @flow import * as React from 'react'; -import ThemeManager from "../../utils/ThemeManager"; +import ThemeManager from "../../managers/ThemeManager"; import WebViewScreen from "../../components/WebViewScreen"; import {Avatar, Banner} from "react-native-paper"; import i18n from "i18n-js"; import {View} from "react-native"; -import AsyncStorageManager from "../../utils/AsyncStorageManager"; +import AsyncStorageManager from "../../managers/AsyncStorageManager"; type Props = { navigation: Object, diff --git a/utils/Notifications.js b/utils/Notifications.js new file mode 100644 index 0000000..b179d3b --- /dev/null +++ b/utils/Notifications.js @@ -0,0 +1,125 @@ +// @flow + +import * as Permissions from 'expo-permissions'; +import {Notifications} from 'expo'; +import AsyncStorageManager from "../managers/AsyncStorageManager"; +import LocaleManager from "../managers/LocaleManager"; +import passwords from "../passwords"; + +const EXPO_TOKEN_SERVER = 'https://etud.insa-toulouse.fr/~amicale_app/expo_notifications/save_token.php'; + +/** + * Async function asking permission to send notifications to the user + * + * @returns {Promise} + */ +export async function askPermissions() { + const {status: existingStatus} = await Permissions.getAsync(Permissions.NOTIFICATIONS); + let finalStatus = existingStatus; + if (existingStatus !== 'granted') { + const {status} = await Permissions.askAsync(Permissions.NOTIFICATIONS); + finalStatus = status; + } + return finalStatus === 'granted'; +} + +/** + * Save expo token to allow sending notifications to this device. + * This token is unique for each device and won't change. + * It only needs to be fetched once, then it will be saved in storage. + * + * @return {Promise} + */ +export async function initExpoToken() { + let token = AsyncStorageManager.getInstance().preferences.expoToken.current; + if (token === '') { + try { + await askPermissions(); + let expoToken = await Notifications.getExpoPushTokenAsync(); + // Save token for instant use later on + AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.expoToken.key, expoToken); + } catch (e) { + console.log(e); + } + } +} + +/** + * Gets the machines watched from the server + * + * @param callback Function to execute with the fetched data + */ +export function getMachineNotificationWatchlist(callback: Function) { + let token = AsyncStorageManager.getInstance().preferences.expoToken.current; + if (token !== '') { + let data = { + function: 'get_machine_watchlist', + password: passwords.expoNotifications, + token: token, + }; + fetch(EXPO_TOKEN_SERVER, { + method: 'POST', + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(data) // <-- Post parameters + }).then((response) => response.json()) + .then((responseJson) => { + callback(responseJson); + }); + } +} + +/** + * Asks the server to enable/disable notifications for the specified machine + * + * @param machineID The machine ID + * @param isEnabled True to enable notifications, false to disable + */ +export function setupMachineNotification(machineID: string, isEnabled: boolean) { + let token = AsyncStorageManager.getInstance().preferences.expoToken.current; + if (token !== '') { + let data = { + function: 'setup_machine_notification', + password: passwords.expoNotifications, + locale: LocaleManager.getCurrentLocale(), + token: token, + machine_id: machineID, + enabled: isEnabled + }; + fetch(EXPO_TOKEN_SERVER, { + method: 'POST', + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(data) // <-- Post parameters + }); + } +} + +/** + * Sends the selected reminder time for notifications to the server + * + * @param time The reminder time to use + */ +export function setMachineReminderNotificationTime(time: number) { + let token = AsyncStorageManager.getInstance().preferences.expoToken.current; + if (token !== '') { + let data = { + function: 'set_machine_reminder', + password: passwords.expoNotifications, + token: token, + time: time, + }; + fetch(EXPO_TOKEN_SERVER, { + method: 'POST', + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(data) // <-- Post parameters + }); + } +} diff --git a/utils/NotificationsManager.js b/utils/NotificationsManager.js deleted file mode 100644 index bca9594..0000000 --- a/utils/NotificationsManager.js +++ /dev/null @@ -1,131 +0,0 @@ -// @flow - -import * as Permissions from 'expo-permissions'; -import {Notifications} from 'expo'; -import AsyncStorageManager from "./AsyncStorageManager"; -import LocaleManager from "./LocaleManager"; -import passwords from "../passwords"; - -const EXPO_TOKEN_SERVER = 'https://etud.insa-toulouse.fr/~amicale_app/expo_notifications/save_token.php'; - -/** - * Static class used to manage notifications sent to the user - */ -export default class NotificationsManager { - - /** - * Async function asking permission to send notifications to the user - * - * @returns {Promise} - */ - static async askPermissions() { - const {status: existingStatus} = await Permissions.getAsync(Permissions.NOTIFICATIONS); - let finalStatus = existingStatus; - if (existingStatus !== 'granted') { - const {status} = await Permissions.askAsync(Permissions.NOTIFICATIONS); - finalStatus = status; - } - return finalStatus === 'granted'; - } - - /** - * Save expo token to allow sending notifications to this device. - * This token is unique for each device and won't change. - * It only needs to be fetched once, then it will be saved in storage. - * - * @return {Promise} - */ - static async initExpoToken() { - let token = AsyncStorageManager.getInstance().preferences.expoToken.current; - if (token === '') { - try { - await NotificationsManager.askPermissions(); - let expoToken = await Notifications.getExpoPushTokenAsync(); - // Save token for instant use later on - AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.expoToken.key, expoToken); - } catch(e) { - console.log(e); - } - } - } - - /** - * Gets the machines watched from the server - * - * @param callback Function to execute with the fetched data - */ - static getMachineNotificationWatchlist(callback: Function) { - let token = AsyncStorageManager.getInstance().preferences.expoToken.current; - if (token !== '') { - let data = { - function: 'get_machine_watchlist', - password: passwords.expoNotifications, - token: token, - }; - fetch(EXPO_TOKEN_SERVER, { - method: 'POST', - headers: new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify(data) // <-- Post parameters - }).then((response) => response.json()) - .then((responseJson) => { - callback(responseJson); - }); - } - } - - /** - * Asks the server to enable/disable notifications for the specified machine - * - * @param machineID The machine ID - * @param isEnabled True to enable notifications, false to disable - */ - static setupMachineNotification(machineID: string, isEnabled: boolean) { - let token = AsyncStorageManager.getInstance().preferences.expoToken.current; - if (token !== '') { - let data = { - function: 'setup_machine_notification', - password: passwords.expoNotifications, - locale: LocaleManager.getCurrentLocale(), - token: token, - machine_id: machineID, - enabled: isEnabled - }; - fetch(EXPO_TOKEN_SERVER, { - method: 'POST', - headers: new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify(data) // <-- Post parameters - }); - } - } - - /** - * Sends the selected reminder time for notifications to the server - * - * @param time The reminder time to use - */ - static setMachineReminderNotificationTime(time: number) { - let token = AsyncStorageManager.getInstance().preferences.expoToken.current; - if (token !== '') { - let data = { - function: 'set_machine_reminder', - password: passwords.expoNotifications, - token: token, - time: time, - }; - fetch(EXPO_TOKEN_SERVER, { - method: 'POST', - headers: new Headers({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify(data) // <-- Post parameters - }); - } - } -} diff --git a/utils/Planning.js b/utils/Planning.js new file mode 100644 index 0000000..c2e244e --- /dev/null +++ b/utils/Planning.js @@ -0,0 +1,240 @@ +// @flow + +export type eventObject = { + id: number, + title: string, + logo: string, + date_begin: string, + date_end: string, + description: string, + club: string, + category_id: number, + url: string, +}; + +// Regex used to check date string validity +const dateRegExp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/; + +/** + * Gets the current day string representation in the format + * YYYY-MM-DD + * + * @return {string} The string representation + */ +export function getCurrentDateString(): string { + return dateToString(new Date(Date.now())); +} + +/** + * Checks if the given date is before the other. + * + * @param event1Date Event 1 date in format YYYY-MM-DD HH:MM + * @param event2Date Event 2 date in format YYYY-MM-DD HH:MM + * @return {boolean} + */ +export function isEventBefore(event1Date: string, event2Date: string): boolean { + let date1 = stringToDate(event1Date); + let date2 = stringToDate(event2Date); + if (date1 !== null && date2 !== null) + return date1 < date2; + else + return false; +} + +/** + * Gets only the date part of the given event date string in the format + * YYYY-MM-DD HH:MM + * + * @param dateString The string to get the date from + * @return {string|null} Date in format YYYY:MM:DD or null if given string is invalid + */ +export function getDateOnlyString(dateString: string): string | null { + if (isEventDateStringFormatValid(dateString)) + return dateString.split(" ")[0]; + else + return null; +} + +/** + * Checks if the given date string is in the format + * YYYY-MM-DD HH:MM + * + * @param dateString The string to check + * @return {boolean} + */ +export function isEventDateStringFormatValid(dateString: ?string): boolean { + return dateString !== undefined + && dateString !== null + && dateRegExp.test(dateString); +} + +/** + * Converts the given date string to a date object.
+ * Accepted format: YYYY-MM-DD HH:MM + * + * @param dateString The string to convert + * @return {Date|null} The date object or null if the given string is invalid + */ +export function stringToDate(dateString: string): Date | null { + let date = new Date(); + if (isEventDateStringFormatValid(dateString)) { + let stringArray = dateString.split(' '); + let dateArray = stringArray[0].split('-'); + let timeArray = stringArray[1].split(':'); + date.setFullYear( + parseInt(dateArray[0]), + parseInt(dateArray[1]) - 1, // Month range from 0 to 11 + parseInt(dateArray[2]) + ); + date.setHours( + parseInt(timeArray[0]), + parseInt(timeArray[1]), + 0, + 0, + ); + } else + date = null; + + return date; +} + +/** + * Converts a date object to a string in the format + * YYYY-MM-DD HH-MM-SS + * + * @param date The date object to convert + * @return {string} The converted string + */ +export function dateToString(date: Date): string { + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes; +} + +/** + * Returns a string corresponding to the event start and end times in the following format: + * + * HH:MM - HH:MM + * + * If the end date is not specified or is equal to start time, only start time will be shown. + * + * If the end date is not on the same day, 23:59 will be shown as end time + * + * @param start Start time in YYYY-MM-DD HH:MM:SS format + * @param end End time in YYYY-MM-DD HH:MM:SS format + * @return {string} Formatted string or "/ - /" on error + */ +export function getFormattedEventTime(start: string, end: string): string { + let formattedStr = '/ - /'; + let startDate = stringToDate(start); + let endDate = stringToDate(end); + + if (startDate !== null && endDate !== null && startDate.getTime() !== endDate.getTime()) { + formattedStr = String(startDate.getHours()).padStart(2, '0') + ':' + + String(startDate.getMinutes()).padStart(2, '0') + ' - '; + if (endDate.getFullYear() > startDate.getFullYear() + || endDate.getMonth() > startDate.getMonth() + || endDate.getDate() > startDate.getDate()) + formattedStr += '23:59'; + else + formattedStr += String(endDate.getHours()).padStart(2, '0') + ':' + + String(endDate.getMinutes()).padStart(2, '0'); + } else if (startDate !== null) + formattedStr = + String(startDate.getHours()).padStart(2, '0') + ':' + + String(startDate.getMinutes()).padStart(2, '0'); + + return formattedStr +} + +/** + * Checks if the given description can be considered empty. + *
+ * An empty description is composed only of whitespace, br or p tags + * + * + * @param description The text to check + * @return {boolean} + */ +export function isDescriptionEmpty(description: ?string): boolean { + if (description !== undefined && description !== null) { + return description + .split('

').join('') // Equivalent to a replace all + .split('

').join('') + .split('
').join('').trim() === ''; + } else + return true; +} + +/** + * Generates an object with an empty array for each key. + * Each key is a date string in the format + * YYYY-MM-DD + * + * @param numberOfMonths The number of months to create, starting from the current date + * @return {Object} + */ +export function generateEmptyCalendar(numberOfMonths: number): Object { + let end = new Date(Date.now()); + end.setMonth(end.getMonth() + numberOfMonths); + let daysOfYear = {}; + for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) { + const dateString = getDateOnlyString( + dateToString(new Date(d))); + if (dateString !== null) + daysOfYear[dateString] = [] + } + return daysOfYear; +} + +/** + * Generates an object with an array of eventObject at each key. + * Each key is a date string in the format + * YYYY-MM-DD. + * + * If no event is available at the given key, the array will be empty + * + * @param eventList The list of events to map to the agenda + * @param numberOfMonths The number of months to create the agenda for + * @return {Object} + */ +export function generateEventAgenda(eventList: Array, numberOfMonths: number): Object { + let agendaItems = generateEmptyCalendar(numberOfMonths); + for (let i = 0; i < eventList.length; i++) { + const dateString = getDateOnlyString(eventList[i].date_begin); + if (dateString !== null) { + const eventArray = agendaItems[dateString]; + if (eventArray !== undefined) + this.pushEventInOrder(eventArray, eventList[i]); + } + + } + return agendaItems; +} + +/** + * Adds events to the given array depending on their starting date. + * + * Events starting before are added at the front. + * + * @param eventArray The array to hold sorted events + * @param event The event to add to the array + */ +export function pushEventInOrder(eventArray: Array, event: eventObject): Object { + if (eventArray.length === 0) + eventArray.push(event); + else { + for (let i = 0; i < eventArray.length; i++) { + if (isEventBefore(event.date_begin, eventArray[i].date_begin)) { + eventArray.splice(i, 0, event); + break; + } else if (i === eventArray.length - 1) { + eventArray.push(event); + break; + } + } + } +} diff --git a/utils/PlanningEventManager.js b/utils/PlanningEventManager.js deleted file mode 100644 index b32d3af..0000000 --- a/utils/PlanningEventManager.js +++ /dev/null @@ -1,243 +0,0 @@ -// @flow - -export type eventObject = { - id: number, - title: string, - logo: string, - date_begin: string, - date_end: string, - description: string, - club: string, - category_id: number, - url: string, -}; - -export default class PlanningEventManager { - - // Regex used to check date string validity - static dateRegExp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/; - - /** - * Gets the current day string representation in the format - * YYYY-MM-DD - * - * @return {string} The string representation - */ - static getCurrentDateString(): string { - return PlanningEventManager.dateToString(new Date(Date.now())); - } - - /** - * Checks if the given date is before the other. - * - * @param event1Date Event 1 date in format YYYY-MM-DD HH:MM - * @param event2Date Event 2 date in format YYYY-MM-DD HH:MM - * @return {boolean} - */ - static isEventBefore(event1Date: string, event2Date: string): boolean { - let date1 = PlanningEventManager.stringToDate(event1Date); - let date2 = PlanningEventManager.stringToDate(event2Date); - if (date1 !== null && date2 !== null) - return date1 < date2; - else - return false; - } - - /** - * Gets only the date part of the given event date string in the format - * YYYY-MM-DD HH:MM - * - * @param dateString The string to get the date from - * @return {string|null} Date in format YYYY:MM:DD or null if given string is invalid - */ - static getDateOnlyString(dateString: string): string | null { - if (PlanningEventManager.isEventDateStringFormatValid(dateString)) - return dateString.split(" ")[0]; - else - return null; - } - - /** - * Checks if the given date string is in the format - * YYYY-MM-DD HH:MM - * - * @param dateString The string to check - * @return {boolean} - */ - static isEventDateStringFormatValid(dateString: ?string): boolean { - return dateString !== undefined - && dateString !== null - && PlanningEventManager.dateRegExp.test(dateString); - } - - /** - * Converts the given date string to a date object.
- * Accepted format: YYYY-MM-DD HH:MM - * - * @param dateString The string to convert - * @return {Date|null} The date object or null if the given string is invalid - */ - static stringToDate(dateString: string): Date | null { - let date = new Date(); - if (PlanningEventManager.isEventDateStringFormatValid(dateString)) { - let stringArray = dateString.split(' '); - let dateArray = stringArray[0].split('-'); - let timeArray = stringArray[1].split(':'); - date.setFullYear( - parseInt(dateArray[0]), - parseInt(dateArray[1]) - 1, // Month range from 0 to 11 - parseInt(dateArray[2]) - ); - date.setHours( - parseInt(timeArray[0]), - parseInt(timeArray[1]), - 0, - 0, - ); - } else - date = null; - - return date; - } - - /** - * Converts a date object to a string in the format - * YYYY-MM-DD HH-MM-SS - * - * @param date The date object to convert - * @return {string} The converted string - */ - static dateToString(date: Date): string { - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! - const year = date.getFullYear(); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes; - } - - /** - * Returns a string corresponding to the event start and end times in the following format: - * - * HH:MM - HH:MM - * - * If the end date is not specified or is equal to start time, only start time will be shown. - * - * If the end date is not on the same day, 23:59 will be shown as end time - * - * @param start Start time in YYYY-MM-DD HH:MM:SS format - * @param end End time in YYYY-MM-DD HH:MM:SS format - * @return {string} Formatted string or "/ - /" on error - */ - static getFormattedEventTime(start: string, end: string): string { - let formattedStr = '/ - /'; - let startDate = PlanningEventManager.stringToDate(start); - let endDate = PlanningEventManager.stringToDate(end); - - if (startDate !== null && endDate !== null && startDate.getTime() !== endDate.getTime()) { - formattedStr = String(startDate.getHours()).padStart(2, '0') + ':' - + String(startDate.getMinutes()).padStart(2, '0') + ' - '; - if (endDate.getFullYear() > startDate.getFullYear() - || endDate.getMonth() > startDate.getMonth() - || endDate.getDate() > startDate.getDate()) - formattedStr += '23:59'; - else - formattedStr += String(endDate.getHours()).padStart(2, '0') + ':' - + String(endDate.getMinutes()).padStart(2, '0'); - } else if (startDate !== null) - formattedStr = - String(startDate.getHours()).padStart(2, '0') + ':' - + String(startDate.getMinutes()).padStart(2, '0'); - - return formattedStr - } - - /** - * Checks if the given description can be considered empty. - *
- * An empty description is composed only of whitespace, br or p tags - * - * - * @param description The text to check - * @return {boolean} - */ - static isDescriptionEmpty(description: ?string): boolean { - if (description !== undefined && description !== null) { - return description - .split('

').join('') // Equivalent to a replace all - .split('

').join('') - .split('
').join('').trim() === ''; - } else - return true; - } - - /** - * Generates an object with an empty array for each key. - * Each key is a date string in the format - * YYYY-MM-DD - * - * @param numberOfMonths The number of months to create, starting from the current date - * @return {Object} - */ - static generateEmptyCalendar(numberOfMonths: number): Object { - let end = new Date(Date.now()); - end.setMonth(end.getMonth() + numberOfMonths); - let daysOfYear = {}; - for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) { - const dateString = PlanningEventManager.getDateOnlyString( - PlanningEventManager.dateToString(new Date(d))); - if (dateString !== null) - daysOfYear[dateString] = [] - } - return daysOfYear; - } - - /** - * Generates an object with an array of eventObject at each key. - * Each key is a date string in the format - * YYYY-MM-DD. - * - * If no event is available at the given key, the array will be empty - * - * @param eventList The list of events to map to the agenda - * @param numberOfMonths The number of months to create the agenda for - * @return {Object} - */ - static generateEventAgenda(eventList: Array, numberOfMonths: number): Object { - let agendaItems = PlanningEventManager.generateEmptyCalendar(numberOfMonths); - for (let i = 0; i < eventList.length; i++) { - const dateString = PlanningEventManager.getDateOnlyString(eventList[i].date_begin); - if (dateString !== null) { - const eventArray = agendaItems[dateString]; - if (eventArray !== undefined) - this.pushEventInOrder(eventArray, eventList[i]); - } - - } - return agendaItems; - } - - /** - * Adds events to the given array depending on their starting date. - * - * Events starting before are added at the front. - * - * @param eventArray The array to hold sorted events - * @param event The event to add to the array - */ - static pushEventInOrder(eventArray: Array, event: eventObject): Object { - if (eventArray.length === 0) - eventArray.push(event); - else { - for (let i = 0; i < eventArray.length; i++) { - if (PlanningEventManager.isEventBefore(event.date_begin, eventArray[i].date_begin)) { - eventArray.splice(i, 0, event); - break; - } else if (i === eventArray.length - 1) { - eventArray.push(event); - break; - } - } - } - } -} diff --git a/utils/WebData.js b/utils/WebData.js new file mode 100644 index 0000000..5e6235d --- /dev/null +++ b/utils/WebData.js @@ -0,0 +1,19 @@ +// @flow + +/** + * Read data from FETCH_URL and return it. + * If no data was found, returns an empty object + * + * @param url The urls to fetch data from + * @return {Promise} + */ +export async function readData(url: string) { + let fetchedData: Object = {}; + try { + let response = await fetch(url); + fetchedData = await response.json(); + } catch (error) { + throw new Error('Could not read FetchedData from server'); + } + return fetchedData; +} diff --git a/utils/WebDataManager.js b/utils/WebDataManager.js deleted file mode 100644 index 8db8e2a..0000000 --- a/utils/WebDataManager.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow - -/** - * Class used to get json data from the web - */ -export default class WebDataManager { - - FETCH_URL: string; - lastDataFetched: Object = {}; - - - constructor(url: string) { - this.FETCH_URL = url; - } - - /** - * Read data from FETCH_URL and return it. - * If no data was found, returns an empty object - * - * @return {Promise} - */ - async readData() { - let fetchedData: Object = {}; - try { - let response = await fetch(this.FETCH_URL); - fetchedData = await response.json(); - } catch (error) { - throw new Error('Could not read FetchedData from server'); - } - this.lastDataFetched = fetchedData; - return fetchedData; - } - -} diff --git a/utils/__test__/PlanningEventManager.test.js b/utils/__test__/PlanningEventManager.test.js deleted file mode 100644 index 0ec7ed2..0000000 --- a/utils/__test__/PlanningEventManager.test.js +++ /dev/null @@ -1,210 +0,0 @@ -import React from 'react'; -import PlanningEventManager from "../PlanningEventManager"; - -test('isDescriptionEmpty', () => { - expect(PlanningEventManager.isDescriptionEmpty("")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty(" ")).toBeTrue(); - // noinspection CheckTagEmptyBody - expect(PlanningEventManager.isDescriptionEmpty("

")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty("

")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty("


")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty("



")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty("




")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty("


")).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty(null)).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty(undefined)).toBeTrue(); - expect(PlanningEventManager.isDescriptionEmpty("coucou")).toBeFalse(); - expect(PlanningEventManager.isDescriptionEmpty("

coucou

")).toBeFalse(); -}); - -test('isEventDateStringFormatValid', () => { - expect(PlanningEventManager.isEventDateStringFormatValid("2020-03-21 09:00")).toBeTrue(); - expect(PlanningEventManager.isEventDateStringFormatValid("3214-64-12 01:16")).toBeTrue(); - - expect(PlanningEventManager.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("2020-03-21")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("garbage")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid("")).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid(undefined)).toBeFalse(); - expect(PlanningEventManager.isEventDateStringFormatValid(null)).toBeFalse(); -}); - -test('stringToDate', () => { - let testDate = new Date(); - expect(PlanningEventManager.stringToDate(undefined)).toBeNull(); - expect(PlanningEventManager.stringToDate("")).toBeNull(); - expect(PlanningEventManager.stringToDate("garbage")).toBeNull(); - expect(PlanningEventManager.stringToDate("2020-03-21")).toBeNull(); - expect(PlanningEventManager.stringToDate("09:00:00")).toBeNull(); - expect(PlanningEventManager.stringToDate("2020-03-21 09:g0")).toBeNull(); - expect(PlanningEventManager.stringToDate("2020-03-21 09:g0:")).toBeNull(); - testDate.setFullYear(2020, 2, 21); - testDate.setHours(9, 0, 0, 0); - expect(PlanningEventManager.stringToDate("2020-03-21 09:00")).toEqual(testDate); - testDate.setFullYear(2020, 0, 31); - testDate.setHours(18, 30, 0, 0); - expect(PlanningEventManager.stringToDate("2020-01-31 18:30")).toEqual(testDate); - testDate.setFullYear(2020, 50, 50); - testDate.setHours(65, 65, 0, 0); - expect(PlanningEventManager.stringToDate("2020-51-50 65:65")).toEqual(testDate); -}); - -test('getFormattedEventTime', () => { - expect(PlanningEventManager.getFormattedEventTime(null, null)) - .toBe('/ - /'); - expect(PlanningEventManager.getFormattedEventTime(undefined, undefined)) - .toBe('/ - /'); - expect(PlanningEventManager.getFormattedEventTime("20:30", "23:00")) - .toBe('/ - /'); - expect(PlanningEventManager.getFormattedEventTime("2020-03-30", "2020-03-31")) - .toBe('/ - /'); - - - expect(PlanningEventManager.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00")) - .toBe('09:00'); - expect(PlanningEventManager.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00")) - .toBe('09:00 - 23:59'); - expect(PlanningEventManager.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00")) - .toBe('20:30 - 23:00'); -}); - -test('getDateOnlyString', () => { - expect(PlanningEventManager.getDateOnlyString("2020-03-21 09:00")).toBe("2020-03-21"); - expect(PlanningEventManager.getDateOnlyString("2021-12-15 09:00")).toBe("2021-12-15"); - expect(PlanningEventManager.getDateOnlyString("2021-12-o5 09:00")).toBeNull(); - expect(PlanningEventManager.getDateOnlyString("2021-12-15 09:")).toBeNull(); - expect(PlanningEventManager.getDateOnlyString("2021-12-15")).toBeNull(); - expect(PlanningEventManager.getDateOnlyString("garbage")).toBeNull(); -}); - -test('isEventBefore', () => { - expect(PlanningEventManager.isEventBefore( - "2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue(); - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue(); - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue(); - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue(); - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue(); - - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse(); - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse(); - expect(PlanningEventManager.isEventBefore( - "2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse(); - expect(PlanningEventManager.isEventBefore( - "2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse(); - expect(PlanningEventManager.isEventBefore( - "2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse(); - expect(PlanningEventManager.isEventBefore( - "2020-03-30 10:15", "2020-03-21 10:15")).toBeFalse(); - - expect(PlanningEventManager.isEventBefore( - "garbage", "2020-03-21 10:15")).toBeFalse(); - expect(PlanningEventManager.isEventBefore( - undefined, undefined)).toBeFalse(); -}); - -test('dateToString', () => { - let testDate = new Date(); - testDate.setFullYear(2020, 2, 21); - testDate.setHours(9, 0, 0, 0); - expect(PlanningEventManager.dateToString(testDate)).toBe("2020-03-21 09:00"); - testDate.setFullYear(2021, 0, 12); - testDate.setHours(9, 10, 0, 0); - expect(PlanningEventManager.dateToString(testDate)).toBe("2021-01-12 09:10"); - testDate.setFullYear(2022, 11, 31); - testDate.setHours(9, 10, 15, 0); - expect(PlanningEventManager.dateToString(testDate)).toBe("2022-12-31 09:10"); -}); - -test('generateEmptyCalendar', () => { - jest.spyOn(Date, 'now') - .mockImplementation(() => - new Date('2020-01-14T00:00:00.000Z').getTime() - ); - let calendar = PlanningEventManager.generateEmptyCalendar(1); - expect(calendar).toHaveProperty("2020-01-14"); - expect(calendar).toHaveProperty("2020-01-20"); - expect(calendar).toHaveProperty("2020-02-10"); - expect(Object.keys(calendar).length).toBe(32); - calendar = PlanningEventManager.generateEmptyCalendar(3); - expect(calendar).toHaveProperty("2020-01-14"); - expect(calendar).toHaveProperty("2020-01-20"); - expect(calendar).toHaveProperty("2020-02-10"); - expect(calendar).toHaveProperty("2020-02-14"); - expect(calendar).toHaveProperty("2020-03-20"); - expect(calendar).toHaveProperty("2020-04-12"); - expect(Object.keys(calendar).length).toBe(92); -}); - -test('pushEventInOrder', () => { - let eventArray = []; - let event1 = {date_begin: "2020-01-14 09:15"}; - PlanningEventManager.pushEventInOrder(eventArray, event1); - expect(eventArray.length).toBe(1); - expect(eventArray[0]).toBe(event1); - - let event2 = {date_begin: "2020-01-14 10:15"}; - PlanningEventManager.pushEventInOrder(eventArray, event2); - expect(eventArray.length).toBe(2); - expect(eventArray[0]).toBe(event1); - expect(eventArray[1]).toBe(event2); - - let event3 = {date_begin: "2020-01-14 10:15", title: "garbage"}; - PlanningEventManager.pushEventInOrder(eventArray, event3); - expect(eventArray.length).toBe(3); - expect(eventArray[0]).toBe(event1); - expect(eventArray[1]).toBe(event2); - expect(eventArray[2]).toBe(event3); - - let event4 = {date_begin: "2020-01-13 09:00"}; - PlanningEventManager.pushEventInOrder(eventArray, event4); - expect(eventArray.length).toBe(4); - expect(eventArray[0]).toBe(event4); - expect(eventArray[1]).toBe(event1); - expect(eventArray[2]).toBe(event2); - expect(eventArray[3]).toBe(event3); -}); - -test('generateEventAgenda', () => { - jest.spyOn(Date, 'now') - .mockImplementation(() => - new Date('2020-01-14T00:00:00.000Z').getTime() - ); - let eventList = [ - {date_begin: "2020-01-14 09:15"}, - {date_begin: "2020-02-01 09:15"}, - {date_begin: "2020-01-15 09:15"}, - {date_begin: "2020-02-01 09:30"}, - {date_begin: "2020-02-01 08:30"}, - ]; - const calendar = PlanningEventManager.generateEventAgenda(eventList, 2); - expect(calendar["2020-01-14"].length).toBe(1); - expect(calendar["2020-01-14"][0]).toBe(eventList[0]); - expect(calendar["2020-01-15"].length).toBe(1); - expect(calendar["2020-01-15"][0]).toBe(eventList[2]); - expect(calendar["2020-02-01"].length).toBe(3); - expect(calendar["2020-02-01"][0]).toBe(eventList[4]); - expect(calendar["2020-02-01"][1]).toBe(eventList[1]); - expect(calendar["2020-02-01"][2]).toBe(eventList[3]); -}); - -test('getCurrentDateString', () => { - jest.spyOn(Date, 'now') - .mockImplementation(() => { - let date = new Date(); - date.setFullYear(2020, 0, 14); - date.setHours(15, 30, 54, 65); - return date.getTime(); - }); - expect(PlanningEventManager.getCurrentDateString()).toBe('2020-01-14 15:30'); -});