Improved project structure

This commit is contained in:
Arnaud Vergnet 2020-03-30 15:28:08 +02:00
parent fac9d8208e
commit 7e90b80ca2
30 changed files with 672 additions and 698 deletions

12
App.js
View file

@ -2,17 +2,17 @@
import * as React from 'react'; import * as React from 'react';
import {Platform, StatusBar} from 'react-native'; import {Platform, StatusBar} from 'react-native';
import LocaleManager from './utils/LocaleManager'; import LocaleManager from './managers/LocaleManager';
import AsyncStorageManager from "./utils/AsyncStorageManager"; import AsyncStorageManager from "./managers/AsyncStorageManager";
import CustomIntroSlider from "./components/CustomIntroSlider"; import CustomIntroSlider from "./components/CustomIntroSlider";
import {SplashScreen} from 'expo'; import {SplashScreen} from 'expo';
import ThemeManager from './utils/ThemeManager'; import ThemeManager from './managers/ThemeManager';
import {NavigationContainer} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack'; import {createStackNavigator} from '@react-navigation/stack';
import DrawerNavigator from './navigation/DrawerNavigator'; import DrawerNavigator from './navigation/DrawerNavigator';
import NotificationsManager from "./utils/NotificationsManager"; import {initExpoToken} from "./utils/Notifications";
import {Provider as PaperProvider} from 'react-native-paper'; import {Provider as PaperProvider} from 'react-native-paper';
import AprilFoolsManager from "./utils/AprilFoolsManager"; import AprilFoolsManager from "./managers/AprilFoolsManager";
import Update from "./constants/Update"; import Update from "./constants/Update";
type Props = {}; type Props = {};
@ -90,7 +90,7 @@ export default class App extends React.Component<Props, State> {
// Wait for custom fonts to be loaded before showing the app // Wait for custom fonts to be loaded before showing the app
await AsyncStorageManager.getInstance().loadPreferences(); await AsyncStorageManager.getInstance().loadPreferences();
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme); ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
await NotificationsManager.initExpoToken(); await initExpoToken();
this.onLoadFinished(); this.onLoadFinished();
} }

View file

@ -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("<p></p>")).toBeTrue();
expect(Planning.isDescriptionEmpty("<p> </p>")).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br></p>")).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br></p><p><br></p>")).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br><br><br></p>")).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br>")).toBeTrue();
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
expect(Planning.isDescriptionEmpty("coucou")).toBeFalse();
expect(Planning.isDescriptionEmpty("<p>coucou</p>")).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');
});

View file

@ -5,7 +5,7 @@ import {StyleSheet, View} from "react-native";
import HTML from "react-native-render-html"; import HTML from "react-native-render-html";
import i18n from "i18n-js"; import i18n from "i18n-js";
import {Avatar, Button, Card, withTheme} from 'react-native-paper'; 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 * Component used to display an event preview if an event is available
@ -17,7 +17,7 @@ function PreviewEventDashboardItem(props) {
const {colors} = props.theme; const {colors} = props.theme;
const isEmpty = props.event === undefined const isEmpty = props.event === undefined
? true ? true
: PlanningEventManager.isDescriptionEmpty(props.event['description']); : isDescriptionEmpty(props.event['description']);
if (props.event !== undefined && props.event !== null) { if (props.event !== undefined && props.event !== null) {
const hasImage = props.event['logo'] !== '' && props.event['logo'] !== null; const hasImage = props.event['logo'] !== '' && props.event['logo'] !== null;
@ -34,12 +34,12 @@ function PreviewEventDashboardItem(props) {
{hasImage ? {hasImage ?
<Card.Title <Card.Title
title={props.event['title']} title={props.event['title']}
subtitle={PlanningEventManager.getFormattedEventTime(props.event['date_begin'], props.event['date_end'])} subtitle={getFormattedEventTime(props.event['date_begin'], props.event['date_end'])}
left={getImage} left={getImage}
/> : /> :
<Card.Title <Card.Title
title={props.event['title']} title={props.event['title']}
subtitle={PlanningEventManager.getFormattedEventTime(props.event['date_begin'], props.event['date_end'])} subtitle={getFormattedEventTime(props.event['date_begin'], props.event['date_end'])}
/>} />}
{!isEmpty ? {!isEmpty ?
<Card.Content style={styles.content}> <Card.Content style={styles.content}>

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import WebDataManager from "../utils/WebDataManager"; import {readData} from "../utils/WebData";
import i18n from "i18n-js"; import i18n from "i18n-js";
import {Snackbar} from 'react-native-paper'; import {Snackbar} from 'react-native-paper';
import {RefreshControl, SectionList, View} from "react-native"; import {RefreshControl, SectionList, View} from "react-native";
@ -42,8 +42,6 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
updateData: 0, updateData: 0,
}; };
webDataManager: WebDataManager;
refreshInterval: IntervalID; refreshInterval: IntervalID;
lastRefresh: Date; lastRefresh: Date;
@ -79,7 +77,6 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
* Allows to detect when the screen is focused * Allows to detect when the screen is focused
*/ */
componentDidMount() { componentDidMount() {
this.webDataManager = new WebDataManager(this.props.fetchUrl);
const onScreenFocus = this.onScreenFocus.bind(this); const onScreenFocus = this.onScreenFocus.bind(this);
const onScreenBlur = this.onScreenBlur.bind(this); const onScreenBlur = this.onScreenBlur.bind(this);
this.props.navigation.addListener('focus', onScreenFocus); this.props.navigation.addListener('focus', onScreenFocus);
@ -144,7 +141,7 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
canRefresh = true; canRefresh = true;
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
this.webDataManager.readData() readData(this.props.fetchUrl)
.then(this.onFetchSuccess) .then(this.onFetchSuccess)
.catch(this.onFetchError); .catch(this.onFetchError);
} }

View file

@ -2,7 +2,7 @@
<coverage generated="1584889501493" clover="3.2.0"> <coverage generated="1584889501493" clover="3.2.0">
<project timestamp="1584889501493" name="All files"> <project timestamp="1584889501493" name="All files">
<metrics statements="65" coveredstatements="65" conditionals="39" coveredconditionals="36" methods="11" coveredmethods="11" elements="115" coveredelements="112" complexity="0" loc="65" ncloc="65" packages="1" files="1" classes="1"/> <metrics statements="65" coveredstatements="65" conditionals="39" coveredconditionals="36" methods="11" coveredmethods="11" elements="115" coveredelements="112" complexity="0" loc="65" ncloc="65" packages="1" files="1" classes="1"/>
<file name="PlanningEventManager.js" path="/home/keplyx/expo-projects/application-amicale/utils/PlanningEventManager.js"> <file name="Planning.js" path="/home/keplyx/expo-projects/application-amicale/utils/Planning.js">
<metrics statements="65" coveredstatements="65" conditionals="39" coveredconditionals="36" methods="11" coveredmethods="11"/> <metrics statements="65" coveredstatements="65" conditionals="39" coveredconditionals="36" methods="11" coveredmethods="11"/>
<line num="18" count="1" type="stmt"/> <line num="18" count="1" type="stmt"/>
<line num="27" count="1" type="stmt"/> <line num="27" count="1" type="stmt"/>

View file

@ -3,7 +3,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Code coverage report for PlanningEventManager.js</title> <title>Code coverage report for Planning.js</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" /> <link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" /> <link rel="stylesheet" href="base.css" />
@ -19,7 +19,7 @@
<body> <body>
<div class='wrapper'> <div class='wrapper'>
<div class='pad1'> <div class='pad1'>
<h1><a href="index.html">All files</a> PlanningEventManager.js</h1> <h1><a href="index.html">All files</a> Planning.js</h1>
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
@ -806,4 +806,3 @@ export default class PlanningEventManager {
<script src="block-navigation.js"></script> <script src="block-navigation.js"></script>
</body> </body>
</html> </html>

View file

@ -73,7 +73,7 @@
</tr> </tr>
</thead> </thead>
<tbody><tr> <tbody><tr>
<td class="file high" data-value="PlanningEventManager.js"><a href="PlanningEventManager.js.html">PlanningEventManager.js</a></td> <td class="file high" data-value="Planning.js"><a href="PlanningEventManager.js.html">Planning.js</a></td>
<td data-value="100" class="pic high"> <td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div> <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td> </td>
@ -108,4 +108,3 @@
<script src="block-navigation.js"></script> <script src="block-navigation.js"></script>
</body> </body>
</html> </html>

View file

@ -1,5 +1,5 @@
TN: TN:
SF:utils/PlanningEventManager.js SF:utils/Planning.js
FN:26,(anonymous_0) FN:26,(anonymous_0)
FN:37,(anonymous_1) FN:37,(anonymous_1)
FN:53,(anonymous_2) FN:53,(anonymous_2)

View file

@ -12,7 +12,7 @@ import ProximoListScreen from "../screens/Proximo/ProximoListScreen";
import ProximoAboutScreen from "../screens/Proximo/ProximoAboutScreen"; import ProximoAboutScreen from "../screens/Proximo/ProximoAboutScreen";
import PlanexScreen from '../screens/Websites/PlanexScreen'; import PlanexScreen from '../screens/Websites/PlanexScreen';
import {MaterialCommunityIcons} from "@expo/vector-icons"; import {MaterialCommunityIcons} from "@expo/vector-icons";
import AsyncStorageManager from "../utils/AsyncStorageManager"; import AsyncStorageManager from "../managers/AsyncStorageManager";
import HeaderButton from "../components/HeaderButton"; import HeaderButton from "../components/HeaderButton";
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import i18n from "i18n-js"; import i18n from "i18n-js";

View file

@ -4,7 +4,7 @@ import * as React from 'react';
import {FlatList, Linking, Platform, View} from 'react-native'; import {FlatList, Linking, Platform, View} from 'react-native';
import i18n from "i18n-js"; import i18n from "i18n-js";
import appJson from '../../app'; import appJson from '../../app';
import AsyncStorageManager from "../../utils/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import CustomModal from "../../components/CustomModal"; import CustomModal from "../../components/CustomModal";
import {Avatar, Button, Card, List, Text, Title, withTheme} from 'react-native-paper'; import {Avatar, Button, Card, List, Text, Title, withTheme} from 'react-native-paper';

View file

@ -2,7 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import {ScrollView, View} from "react-native"; import {ScrollView, View} from "react-native";
import AsyncStorageManager from "../../utils/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import CustomModal from "../../components/CustomModal"; import CustomModal from "../../components/CustomModal";
import {Button, Card, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper'; import {Button, Card, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper';

View file

@ -10,7 +10,7 @@ import {Text, withTheme} from 'react-native-paper';
import FeedItem from "../components/FeedItem"; import FeedItem from "../components/FeedItem";
import SquareDashboardItem from "../components/SquareDashboardItem"; import SquareDashboardItem from "../components/SquareDashboardItem";
import PreviewEventDashboardItem from "../components/PreviewEventDashboardItem"; import PreviewEventDashboardItem from "../components/PreviewEventDashboardItem";
import PlanningEventManager from "../utils/PlanningEventManager"; import {stringToDate} from "../utils/Planning";
// import DATA from "../dashboard_data.json"; // import DATA from "../dashboard_data.json";
@ -201,8 +201,8 @@ class HomeScreen extends React.Component<Props> {
* @return {number} The number of milliseconds * @return {number} The number of milliseconds
*/ */
getEventDuration(event: Object): number { getEventDuration(event: Object): number {
let start = PlanningEventManager.stringToDate(event['date_begin']); let start = stringToDate(event['date_begin']);
let end = PlanningEventManager.stringToDate(event['date_end']); let end = stringToDate(event['date_end']);
let duration = 0; let duration = 0;
if (start !== undefined && start !== null && end !== undefined && end !== null) if (start !== undefined && start !== null && end !== undefined && end !== null)
duration = end - start; duration = end - start;
@ -219,7 +219,7 @@ class HomeScreen extends React.Component<Props> {
getEventsAfterLimit(events: Object, limit: Date): Array<Object> { getEventsAfterLimit(events: Object, limit: Date): Array<Object> {
let validEvents = []; let validEvents = [];
for (let event of events) { 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) { if (startDate !== undefined && startDate !== null && startDate >= limit) {
validEvents.push(event); validEvents.push(event);
} }
@ -255,8 +255,8 @@ class HomeScreen extends React.Component<Props> {
let validEvents = []; let validEvents = [];
let now = new Date(); let now = new Date();
for (let event of events) { for (let event of events) {
let startDate = PlanningEventManager.stringToDate(event['date_begin']); let startDate = stringToDate(event['date_begin']);
let endDate = PlanningEventManager.stringToDate(event['date_end']); let endDate = stringToDate(event['date_end']);
if (startDate !== undefined && startDate !== null) { if (startDate !== undefined && startDate !== null) {
if (startDate > now) if (startDate > now)
validEvents.push(event); validEvents.push(event);

View file

@ -4,9 +4,9 @@ import * as React from 'react';
import {Image, ScrollView, View} from 'react-native'; import {Image, ScrollView, View} from 'react-native';
import HTML from "react-native-render-html"; import HTML from "react-native-render-html";
import {Linking} from "expo"; import {Linking} from "expo";
import PlanningEventManager from '../../utils/PlanningEventManager'; import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning';
import {Card, withTheme} from 'react-native-paper'; import {Card, withTheme} from 'react-native-paper';
import DateManager from "../../utils/DateManager"; import DateManager from "../../managers/DateManager";
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -33,9 +33,9 @@ class PlanningDisplayScreen extends React.Component<Props> {
render() { render() {
// console.log("rendering planningDisplayScreen"); // console.log("rendering planningDisplayScreen");
let subtitle = PlanningEventManager.getFormattedEventTime( let subtitle = getFormattedEventTime(
this.displayData["date_begin"], this.displayData["date_end"]); 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) if (dateString !== null)
subtitle += ' | ' + DateManager.getInstance().getTranslatedDate(dateString); subtitle += ' | ' + DateManager.getInstance().getTranslatedDate(dateString);
return ( return (

View file

@ -4,9 +4,14 @@ import * as React from 'react';
import {BackHandler, View} from 'react-native'; import {BackHandler, View} from 'react-native';
import i18n from "i18n-js"; import i18n from "i18n-js";
import {LocaleConfig} from 'react-native-calendars'; import {LocaleConfig} from 'react-native-calendars';
import WebDataManager from "../../utils/WebDataManager"; import {readData} from "../../utils/WebData";
import type {eventObject} from "../../utils/PlanningEventManager"; import type {eventObject} from "../../utils/Planning";
import PlanningEventManager from '../../utils/PlanningEventManager'; import {
generateEventAgenda,
getCurrentDateString,
getDateOnlyString,
getFormattedEventTime,
} from '../../utils/Planning';
import {Avatar, Divider, List} from 'react-native-paper'; import {Avatar, Divider, List} from 'react-native-paper';
import CustomAgenda from "../../components/CustomAgenda"; import CustomAgenda from "../../components/CustomAgenda";
@ -38,7 +43,6 @@ const AGENDA_MONTH_SPAN = 3;
export default class PlanningScreen extends React.Component<Props, State> { export default class PlanningScreen extends React.Component<Props, State> {
agendaRef: Object; agendaRef: Object;
webDataManager: WebDataManager;
lastRefresh: Date; lastRefresh: Date;
minTimeBetweenRefresh = 60; minTimeBetweenRefresh = 60;
@ -59,11 +63,10 @@ export default class PlanningScreen extends React.Component<Props, State> {
onAgendaRef: Function; onAgendaRef: Function;
onCalendarToggled: Function; onCalendarToggled: Function;
onBackButtonPressAndroid: Function; onBackButtonPressAndroid: Function;
currentDate = PlanningEventManager.getDateOnlyString(PlanningEventManager.getCurrentDateString()); currentDate = getDateOnlyString(getCurrentDateString());
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.webDataManager = new WebDataManager(FETCH_URL);
if (i18n.currentLocale().startsWith("fr")) { if (i18n.currentLocale().startsWith("fr")) {
LocaleConfig.defaultLocale = 'fr'; LocaleConfig.defaultLocale = 'fr';
} }
@ -141,11 +144,11 @@ export default class PlanningScreen extends React.Component<Props, State> {
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
this.webDataManager.readData() readData(FETCH_URL)
.then((fetchedData) => { .then((fetchedData) => {
this.setState({ this.setState({
refreshing: false, refreshing: false,
agendaItems: PlanningEventManager.generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN) agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN)
}); });
this.lastRefresh = new Date(); this.lastRefresh = new Date();
}) })
@ -189,7 +192,7 @@ export default class PlanningScreen extends React.Component<Props, State> {
<Divider/> <Divider/>
<List.Item <List.Item
title={item.title} title={item.title}
description={PlanningEventManager.getFormattedEventTime(item["date_begin"], item["date_end"])} description={getFormattedEventTime(item["date_begin"], item["date_end"])}
left={() => <Avatar.Image left={() => <Avatar.Image
source={{uri: item.logo}} source={{uri: item.logo}}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
@ -204,7 +207,7 @@ export default class PlanningScreen extends React.Component<Props, State> {
<Divider/> <Divider/>
<List.Item <List.Item
title={item.title} title={item.title}
description={PlanningEventManager.getFormattedEventTime(item["date_begin"], item["date_end"])} description={getFormattedEventTime(item["date_begin"], item["date_end"])}
onPress={onPress} onPress={onPress}
/> />
</View> </View>

View file

@ -4,15 +4,15 @@ import * as React from 'react';
import {Alert, Platform, View} from 'react-native'; import {Alert, Platform, View} from 'react-native';
import i18n from "i18n-js"; import i18n from "i18n-js";
import WebSectionList from "../../components/WebSectionList"; import WebSectionList from "../../components/WebSectionList";
import NotificationsManager from "../../utils/NotificationsManager"; import * as Notifications from "../../utils/Notifications";
import AsyncStorageManager from "../../utils/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import * as Expo from "expo"; import * as Expo from "expo";
import {Avatar, Banner, Button, Card, Text, withTheme} from 'react-native-paper'; import {Avatar, Banner, Button, Card, Text, withTheme} from 'react-native-paper';
import HeaderButton from "../../components/HeaderButton"; import HeaderButton from "../../components/HeaderButton";
import ProxiwashListItem from "../../components/ProxiwashListItem"; import ProxiwashListItem from "../../components/ProxiwashListItem";
import ProxiwashConstants from "../../constants/ProxiwashConstants"; import ProxiwashConstants from "../../constants/ProxiwashConstants";
import CustomModal from "../../components/CustomModal"; 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"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
@ -118,12 +118,12 @@ class ProxiwashScreen extends React.Component<Props, State> {
}); });
if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') { if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
// Get latest watchlist from server // Get latest watchlist from server
NotificationsManager.getMachineNotificationWatchlist((fetchedList) => { Notifications.getMachineNotificationWatchlist((fetchedList) => {
this.setState({machinesWatched: fetchedList}) this.setState({machinesWatched: fetchedList})
}); });
// Get updated watchlist after received notification // Get updated watchlist after received notification
Expo.Notifications.addListener(() => { Expo.Notifications.addListener(() => {
NotificationsManager.getMachineNotificationWatchlist((fetchedList) => { Notifications.getMachineNotificationWatchlist((fetchedList) => {
this.setState({machinesWatched: fetchedList}) this.setState({machinesWatched: fetchedList})
}); });
}); });
@ -175,7 +175,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
setupNotifications(machineId: string) { setupNotifications(machineId: string) {
if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') { if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
if (!this.isMachineWatched(machineId)) { if (!this.isMachineWatched(machineId)) {
NotificationsManager.setupMachineNotification(machineId, true); Notifications.setupMachineNotification(machineId, true);
this.saveNotificationToState(machineId); this.saveNotificationToState(machineId);
} else } else
this.disableNotification(machineId); this.disableNotification(machineId);
@ -205,7 +205,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
if (data.length > 0) { if (data.length > 0) {
let arrayIndex = data.indexOf(machineId); let arrayIndex = data.indexOf(machineId);
if (arrayIndex !== -1) { if (arrayIndex !== -1) {
NotificationsManager.setupMachineNotification(machineId, false); Notifications.setupMachineNotification(machineId, false);
this.removeNotificationFroState(arrayIndex); this.removeNotificationFroState(arrayIndex);
} }
} }

View file

@ -2,10 +2,10 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import DateManager from "../utils/DateManager"; import DateManager from "../managers/DateManager";
import WebSectionList from "../components/WebSectionList"; import WebSectionList from "../components/WebSectionList";
import {Card, Text, withTheme} from 'react-native-paper'; 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"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/menu/menu_data.json";

View file

@ -2,10 +2,10 @@
import * as React from 'react'; import * as React from 'react';
import {ScrollView} from "react-native"; import {ScrollView} from "react-native";
import ThemeManager from '../utils/ThemeManager'; import ThemeManager from '../managers/ThemeManager';
import i18n from "i18n-js"; import i18n from "i18n-js";
import AsyncStorageManager from "../utils/AsyncStorageManager"; import AsyncStorageManager from "../managers/AsyncStorageManager";
import NotificationsManager from "../utils/NotificationsManager"; import {setMachineReminderNotificationTime} from "../utils/Notifications";
import {Card, List, Switch, ToggleButton} from 'react-native-paper'; import {Card, List, Switch, ToggleButton} from 'react-native-paper';
import {Appearance} from "react-native-appearance"; import {Appearance} from "react-native-appearance";
@ -60,7 +60,7 @@ export default class SettingsScreen extends React.Component<Props, State> {
let intVal = 0; let intVal = 0;
if (value !== 'never') if (value !== 'never')
intVal = parseInt(value); intVal = parseInt(value);
NotificationsManager.setMachineReminderNotificationTime(intVal); setMachineReminderNotificationTime(intVal);
} }
} }

View file

@ -1,12 +1,12 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import ThemeManager from "../../utils/ThemeManager"; import ThemeManager from "../../managers/ThemeManager";
import WebViewScreen from "../../components/WebViewScreen"; import WebViewScreen from "../../components/WebViewScreen";
import {Avatar, Banner} from "react-native-paper"; import {Avatar, Banner} from "react-native-paper";
import i18n from "i18n-js"; import i18n from "i18n-js";
import {View} from "react-native"; import {View} from "react-native";
import AsyncStorageManager from "../../utils/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
type Props = { type Props = {
navigation: Object, navigation: Object,

125
utils/Notifications.js Normal file
View file

@ -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<void>}
*/
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
});
}
}

View file

@ -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<void>}
*/
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
});
}
}
}

240
utils/Planning.js Normal file
View file

@ -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.<br>
* 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.
* <br>
* An empty description is composed only of whitespace, <b>br</b> or <b>p</b> tags
*
*
* @param description The text to check
* @return {boolean}
*/
export function isDescriptionEmpty(description: ?string): boolean {
if (description !== undefined && description !== null) {
return description
.split('<p>').join('') // Equivalent to a replace all
.split('</p>').join('')
.split('<br>').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<eventObject>, 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<eventObject>, 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;
}
}
}
}

View file

@ -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.<br>
* 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.
* <br>
* An empty description is composed only of whitespace, <b>br</b> or <b>p</b> tags
*
*
* @param description The text to check
* @return {boolean}
*/
static isDescriptionEmpty(description: ?string): boolean {
if (description !== undefined && description !== null) {
return description
.split('<p>').join('') // Equivalent to a replace all
.split('</p>').join('')
.split('<br>').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<eventObject>, 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<eventObject>, 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;
}
}
}
}
}

19
utils/WebData.js Normal file
View file

@ -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<Object>}
*/
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;
}

View file

@ -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<Object>}
*/
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;
}
}

View file

@ -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("<p></p>")).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty("<p> </p>")).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty("<p><br></p>")).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty("<p><br></p><p><br></p>")).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty("<p><br><br><br></p>")).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty("<p><br>")).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty(null)).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty(undefined)).toBeTrue();
expect(PlanningEventManager.isDescriptionEmpty("coucou")).toBeFalse();
expect(PlanningEventManager.isDescriptionEmpty("<p>coucou</p>")).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');
});