Improve Planning screen components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-05 15:04:41 +02:00
parent a3299c19f7
commit 3ce23726c2
4 changed files with 586 additions and 537 deletions

View file

@ -2,40 +2,43 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning';
import {Card, withTheme} from 'react-native-paper'; import {Card, withTheme} from 'react-native-paper';
import DateManager from "../../managers/DateManager";
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import BasicLoadingScreen from "../../components/Screens/BasicLoadingScreen";
import {apiRequest, ERROR_TYPE} from "../../utils/WebData";
import ErrorView from "../../components/Screens/ErrorView";
import CustomHTML from "../../components/Overrides/CustomHTML";
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../managers/ThemeManager"; import {getDateOnlyString, getFormattedEventTime} from '../../utils/Planning';
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView"; import DateManager from '../../managers/DateManager';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
import {apiRequest, ERROR_TYPE} from '../../utils/WebData';
import ErrorView from '../../components/Screens/ErrorView';
import CustomHTML from '../../components/Overrides/CustomHTML';
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
import type {PlanningEventType} from '../../utils/Planning';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: { data: Object, id: number, eventId: number } }, route: {params: {data: PlanningEventType, id: number, eventId: number}},
theme: CustomTheme theme: CustomThemeType,
}; };
type State = { type StateType = {
loading: boolean loading: boolean,
}; };
const CLUB_INFO_PATH = "event/info"; const EVENT_INFO_URL = 'event/info';
/** /**
* Class defining a planning event information page. * Class defining a planning event information page.
*/ */
class PlanningDisplayScreen extends React.Component<Props, State> { class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
displayData: null | PlanningEventType;
displayData: Object;
shouldFetchData: boolean; shouldFetchData: boolean;
eventId: number; eventId: number;
errorCode: number; errorCode: number;
/** /**
@ -43,11 +46,11 @@ class PlanningDisplayScreen extends React.Component<Props, State> {
* *
* @param props * @param props
*/ */
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
if (this.props.route.params.data != null) { if (props.route.params.data != null) {
this.displayData = this.props.route.params.data; this.displayData = props.route.params.data;
this.eventId = this.displayData.id; this.eventId = this.displayData.id;
this.shouldFetchData = false; this.shouldFetchData = false;
this.errorCode = 0; this.errorCode = 0;
@ -56,33 +59,22 @@ class PlanningDisplayScreen extends React.Component<Props, State> {
}; };
} else { } else {
this.displayData = null; this.displayData = null;
this.eventId = this.props.route.params.eventId; this.eventId = props.route.params.eventId;
this.shouldFetchData = true; this.shouldFetchData = true;
this.errorCode = 0; this.errorCode = 0;
this.state = { this.state = {
loading: true, loading: true,
}; };
this.fetchData(); this.fetchData();
} }
} }
/**
* Fetches data for the current event id from the API
*/
fetchData = () => {
this.setState({loading: true});
apiRequest(CLUB_INFO_PATH, 'POST', {id: this.eventId})
.then(this.onFetchSuccess)
.catch(this.onFetchError);
};
/** /**
* Hides loading and saves fetched data * Hides loading and saves fetched data
* *
* @param data Received data * @param data Received data
*/ */
onFetchSuccess = (data: Object) => { onFetchSuccess = (data: PlanningEventType) => {
this.displayData = data; this.displayData = data;
this.setState({loading: false}); this.setState({loading: false});
}; };
@ -102,41 +94,46 @@ class PlanningDisplayScreen extends React.Component<Props, State> {
* *
* @returns {*} * @returns {*}
*/ */
getContent() { getContent(): React.Node {
const {theme} = this.props;
const {displayData} = this;
if (displayData == null) return null;
let subtitle = getFormattedEventTime( let subtitle = getFormattedEventTime(
this.displayData["date_begin"], this.displayData["date_end"]); displayData.date_begin,
let dateString = getDateOnlyString(this.displayData["date_begin"]); displayData.date_end,
);
const dateString = getDateOnlyString(displayData.date_begin);
if (dateString !== null) if (dateString !== null)
subtitle += ' | ' + DateManager.getInstance().getTranslatedDate(dateString); subtitle += ` | ${DateManager.getInstance().getTranslatedDate(
dateString,
)}`;
return ( return (
<CollapsibleScrollView <CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
style={{paddingLeft: 5, paddingRight: 5}} <Card.Title title={displayData.title} subtitle={subtitle} />
hasTab={true} {displayData.logo !== null ? (
>
<Card.Title
title={this.displayData.title}
subtitle={subtitle}
/>
{this.displayData.logo !== null ?
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor={this.props.theme.colors.background} imageBackgroundColor={theme.colors.background}
style={{ style={{
width: 300, width: 300,
height: 300, height: 300,
}} }}
source={{ source={{
uri: this.displayData.logo, uri: displayData.logo,
}} }}
/></View> />
: <View/>} </View>
) : null}
{this.displayData.description !== null ? {displayData.description !== null ? (
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> <Card.Content
<CustomHTML html={this.displayData.description}/> style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<CustomHTML html={displayData.description} />
</Card.Content> </Card.Content>
: <View/>} ) : (
<View />
)}
</CollapsibleScrollView> </CollapsibleScrollView>
); );
} }
@ -146,20 +143,40 @@ class PlanningDisplayScreen extends React.Component<Props, State> {
* *
* @returns {*} * @returns {*}
*/ */
getErrorView() { getErrorView(): React.Node {
const {navigation} = this.props;
if (this.errorCode === ERROR_TYPE.BAD_INPUT) if (this.errorCode === ERROR_TYPE.BAD_INPUT)
return <ErrorView {...this.props} showRetryButton={false} message={i18n.t("screens.planning.invalidEvent")} return (
icon={"calendar-remove"}/>; <ErrorView
else navigation={navigation}
return <ErrorView {...this.props} errorCode={this.errorCode} onRefresh={this.fetchData}/>; showRetryButton={false}
message={i18n.t('screens.planning.invalidEvent')}
icon="calendar-remove"
/>
);
return (
<ErrorView
navigation={navigation}
errorCode={this.errorCode}
onRefresh={this.fetchData}
/>
);
} }
render() { /**
if (this.state.loading) * Fetches data for the current event id from the API
return <BasicLoadingScreen/>; */
else if (this.errorCode === 0) fetchData = () => {
return this.getContent(); this.setState({loading: true});
else apiRequest(EVENT_INFO_URL, 'POST', {id: this.eventId})
.then(this.onFetchSuccess)
.catch(this.onFetchError);
};
render(): React.Node {
const {loading} = this.state;
if (loading) return <BasicLoadingScreen />;
if (this.errorCode === 0) return this.getContent();
return this.getErrorView(); return this.getErrorView();
} }
} }

View file

@ -2,91 +2,120 @@
import * as React from 'react'; 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 {Agenda, LocaleConfig} from 'react-native-calendars';
import {readData} from "../../utils/WebData"; import {Avatar, Divider, List} from 'react-native-paper';
import type {eventObject} from "../../utils/Planning"; import {StackNavigationProp} from '@react-navigation/stack';
import {readData} from '../../utils/WebData';
import type {PlanningEventType} from '../../utils/Planning';
import { import {
generateEventAgenda, generateEventAgenda,
getCurrentDateString, getCurrentDateString,
getDateOnlyString, getDateOnlyString,
getFormattedEventTime, getFormattedEventTime,
} from '../../utils/Planning'; } from '../../utils/Planning';
import {Avatar, Divider, List} from 'react-native-paper'; import CustomAgenda from '../../components/Overrides/CustomAgenda';
import CustomAgenda from "../../components/Overrides/CustomAgenda"; import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import {StackNavigationProp} from "@react-navigation/stack"; import MascotPopup from '../../components/Mascot/MascotPopup';
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import AsyncStorageManager from '../../managers/AsyncStorageManager';
import MascotPopup from "../../components/Mascot/MascotPopup";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
LocaleConfig.locales['fr'] = { LocaleConfig.locales.fr = {
monthNames: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'], monthNames: [
monthNamesShort: ['Janv.', 'Févr.', 'Mars', 'Avril', 'Mai', 'Juin', 'Juil.', 'Août', 'Sept.', 'Oct.', 'Nov.', 'Déc.'], 'Janvier',
dayNames: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'], 'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre',
],
monthNamesShort: [
'Janv.',
'Févr.',
'Mars',
'Avril',
'Mai',
'Juin',
'Juil.',
'Août',
'Sept.',
'Oct.',
'Nov.',
'Déc.',
],
dayNames: [
'Dimanche',
'Lundi',
'Mardi',
'Mercredi',
'Jeudi',
'Vendredi',
'Samedi',
],
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'], dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
today: 'Aujourd\'hui' today: "Aujourd'hui",
}; };
type PropsType = {
type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
} };
type State = { type StateType = {
refreshing: boolean, refreshing: boolean,
agendaItems: Object, agendaItems: {[key: string]: Array<PlanningEventType>},
calendarShowing: boolean, calendarShowing: boolean,
}; };
const FETCH_URL = "https://www.amicale-insat.fr/api/event/list"; const FETCH_URL = 'https://www.amicale-insat.fr/api/event/list';
const AGENDA_MONTH_SPAN = 3; const AGENDA_MONTH_SPAN = 3;
/** /**
* Class defining the app's planning screen * Class defining the app's planning screen
*/ */
class PlanningScreen extends React.Component<Props, State> { class PlanningScreen extends React.Component<PropsType, StateType> {
agendaRef: null | Agenda;
agendaRef: Object;
lastRefresh: Date; lastRefresh: Date;
minTimeBetweenRefresh = 60; minTimeBetweenRefresh = 60;
state = { currentDate = getDateOnlyString(getCurrentDateString());
constructor(props: PropsType) {
super(props);
if (i18n.currentLocale().startsWith('fr')) {
LocaleConfig.defaultLocale = 'fr';
}
this.state = {
refreshing: false, refreshing: false,
agendaItems: {}, agendaItems: {},
calendarShowing: false, calendarShowing: false,
}; };
currentDate = getDateOnlyString(getCurrentDateString());
constructor(props: any) {
super(props);
if (i18n.currentLocale().startsWith("fr")) {
LocaleConfig.defaultLocale = 'fr';
}
} }
/** /**
* Captures focus and blur events to hook on android back button * Captures focus and blur events to hook on android back button
*/ */
componentDidMount() { componentDidMount() {
const {navigation} = this.props;
this.onRefresh(); this.onRefresh();
this.props.navigation.addListener( navigation.addListener('focus', () => {
'focus',
() =>
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid this.onBackButtonPressAndroid,
)
); );
this.props.navigation.addListener( });
'blur', navigation.addListener('blur', () => {
() =>
BackHandler.removeEventListener( BackHandler.removeEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid this.onBackButtonPressAndroid,
)
); );
});
} }
/** /**
@ -94,46 +123,33 @@ class PlanningScreen extends React.Component<Props, State> {
* *
* @return {boolean} * @return {boolean}
*/ */
onBackButtonPressAndroid = () => { onBackButtonPressAndroid = (): boolean => {
if (this.state.calendarShowing) { const {calendarShowing} = this.state;
if (calendarShowing && this.agendaRef != null) {
this.agendaRef.chooseDay(this.agendaRef.state.selectedDay); this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
return true; return true;
} else {
return false;
} }
return false;
}; };
/**
* Function used to check if a row has changed
*
* @param r1
* @param r2
* @return {boolean}
*/
rowHasChanged(r1: Object, r2: Object) {
return false;
// if (r1 !== undefined && r2 !== undefined)
// return r1.title !== r2.title;
// else return !(r1 === undefined && r2 === undefined);
}
/** /**
* Refreshes data and shows an animation while doing it * Refreshes data and shows an animation while doing it
*/ */
onRefresh = () => { onRefresh = () => {
let canRefresh; let canRefresh;
if (this.lastRefresh !== undefined) if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh; canRefresh =
else (new Date().getTime() - this.lastRefresh.getTime()) / 1000 >
canRefresh = true; this.minTimeBetweenRefresh;
else canRefresh = true;
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
readData(FETCH_URL) readData(FETCH_URL)
.then((fetchedData) => { .then((fetchedData: Array<PlanningEventType>) => {
this.setState({ this.setState({
refreshing: false, refreshing: false,
agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN) agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN),
}); });
this.lastRefresh = new Date(); this.lastRefresh = new Date();
}) })
@ -150,9 +166,9 @@ class PlanningScreen extends React.Component<Props, State> {
* *
* @param ref * @param ref
*/ */
onAgendaRef = (ref: Object) => { onAgendaRef = (ref: Agenda) => {
this.agendaRef = ref; this.agendaRef = ref;
} };
/** /**
* Callback used when a button is pressed to toggle the calendar * Callback used when a button is pressed to toggle the calendar
@ -161,7 +177,7 @@ class PlanningScreen extends React.Component<Props, State> {
*/ */
onCalendarToggled = (isCalendarOpened: boolean) => { onCalendarToggled = (isCalendarOpened: boolean) => {
this.setState({calendarShowing: isCalendarOpened}); this.setState({calendarShowing: isCalendarOpened});
} };
/** /**
* Gets an event render item * Gets an event render item
@ -169,53 +185,61 @@ class PlanningScreen extends React.Component<Props, State> {
* @param item The current event to render * @param item The current event to render
* @return {*} * @return {*}
*/ */
getRenderItem = (item: eventObject) => { getRenderItem = (item: PlanningEventType): React.Node => {
const onPress = this.props.navigation.navigate.bind(this, 'planning-information', {data: item}); const {navigation} = this.props;
const onPress = () => {
navigation.navigate('planning-information', {
data: item,
});
};
if (item.logo !== null) { if (item.logo !== null) {
return ( return (
<View> <View>
<Divider/> <Divider />
<List.Item <List.Item
title={item.title} title={item.title}
description={getFormattedEventTime(item["date_begin"], item["date_end"])} description={getFormattedEventTime(item.date_begin, item.date_end)}
left={() => <Avatar.Image left={(): React.Node => (
<Avatar.Image
source={{uri: item.logo}} source={{uri: item.logo}}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
/>} />
)}
onPress={onPress} onPress={onPress}
/> />
</View> </View>
); );
} else { }
return ( return (
<View> <View>
<Divider/> <Divider />
<List.Item <List.Item
title={item.title} title={item.title}
description={getFormattedEventTime(item["date_begin"], item["date_end"])} description={getFormattedEventTime(item.date_begin, item.date_end)}
onPress={onPress} onPress={onPress}
/> />
</View> </View>
); );
} };
}
/** /**
* Gets an empty render item for an empty date * Gets an empty render item for an empty date
* *
* @return {*} * @return {*}
*/ */
getRenderEmptyDate = () => <Divider/>; getRenderEmptyDate = (): React.Node => <Divider />;
render() { render(): React.Node {
const {state, props} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<CustomAgenda <CustomAgenda
{...this.props} // eslint-disable-next-line react/jsx-props-no-spreading
{...props}
// the list of items that have to be displayed in agenda. If you want to render item as empty date // the list of items that have to be displayed in agenda. If you want to render item as empty date
// the value of date key kas to be an empty array []. If there exists no value for date key it is // the value of date key kas to be an empty array []. If there exists no value for date key it is
// considered that the date in question is not yet loaded // considered that the date in question is not yet loaded
items={this.state.agendaItems} items={state.agendaItems}
// initially selected day // initially selected day
selected={this.currentDate} selected={this.currentDate}
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
@ -229,10 +253,9 @@ class PlanningScreen extends React.Component<Props, State> {
// callback that fires when the calendar is opened or closed // callback that fires when the calendar is opened or closed
onCalendarToggled={this.onCalendarToggled} onCalendarToggled={this.onCalendarToggled}
// Set this true while waiting for new data from a refresh // Set this true while waiting for new data from a refresh
refreshing={this.state.refreshing} refreshing={state.refreshing}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
renderEmptyDate={this.getRenderEmptyDate} renderEmptyDate={this.getRenderEmptyDate}
rowHasChanged={this.rowHasChanged}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday. // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1} firstDay={1}
// ref to this agenda in order to handle back button event // ref to this agenda in order to handle back button event
@ -240,15 +263,15 @@ class PlanningScreen extends React.Component<Props, State> {
/> />
<MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key} prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key}
title={i18n.t("screens.planning.mascotDialog.title")} title={i18n.t('screens.planning.mascotDialog.title')}
message={i18n.t("screens.planning.mascotDialog.message")} message={i18n.t('screens.planning.mascotDialog.message')}
icon={"party-popper"} icon="party-popper"
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t("screens.planning.mascotDialog.button"), message: i18n.t('screens.planning.mascotDialog.button'),
icon: "check", icon: 'check',
} },
}} }}
emotion={MASCOT_STYLE.HAPPY} emotion={MASCOT_STYLE.HAPPY}
/> />

View file

@ -1,6 +1,6 @@
// @flow // @flow
export type eventObject = { export type PlanningEventType = {
id: number, id: number,
title: string, title: string,
logo: string, logo: string,
@ -15,6 +15,63 @@ export type eventObject = {
// Regex used to check date string validity // Regex used to check date string validity
const dateRegExp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/; const dateRegExp = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/;
/**
* 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)) {
const stringArray = dateString.split(' ');
const dateArray = stringArray[0].split('-');
const timeArray = stringArray[1].split(':');
date.setFullYear(
parseInt(dateArray[0], 10),
parseInt(dateArray[1], 10) - 1, // Month range from 0 to 11
parseInt(dateArray[2], 10),
);
date.setHours(parseInt(timeArray[0], 10), parseInt(timeArray[1], 10), 0, 0);
} else date = null;
return date;
}
/**
* Converts a date object to a string in the format
* YYYY-MM-DD HH-MM
*
* @param date The date object to convert
* @param isUTC Whether to treat the date as UTC
* @return {string} The converted string
*/
export function dateToString(date: Date, isUTC: boolean): 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 h = isUTC ? date.getUTCHours() : date.getHours();
const hours = String(h).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/** /**
* Gets the current day string representation in the format * Gets the current day string representation in the format
* YYYY-MM-DD * YYYY-MM-DD
@ -33,11 +90,9 @@ export function getCurrentDateString(): string {
* @return {boolean} * @return {boolean}
*/ */
export function isEventBefore(event1Date: string, event2Date: string): boolean { export function isEventBefore(event1Date: string, event2Date: string): boolean {
let date1 = stringToDate(event1Date); const date1 = stringToDate(event1Date);
let date2 = stringToDate(event2Date); const date2 = stringToDate(event2Date);
if (date1 !== null && date2 !== null) if (date1 !== null && date2 !== null) return date1 < date2;
return date1 < date2;
else
return false; return false;
} }
@ -49,9 +104,7 @@ export function isEventBefore(event1Date: string, event2Date: string): boolean {
* @return {string|null} Date in format YYYY:MM:DD or null if given string is invalid * @return {string|null} Date in format YYYY:MM:DD or null if given string is invalid
*/ */
export function getDateOnlyString(dateString: string): string | null { export function getDateOnlyString(dateString: string): string | null {
if (isEventDateStringFormatValid(dateString)) if (isEventDateStringFormatValid(dateString)) return dateString.split(' ')[0];
return dateString.split(" ")[0];
else
return null; return null;
} }
@ -63,73 +116,10 @@ export function getDateOnlyString(dateString: string): string | null {
* @return {string|null} Time in format HH:MM or null if given string is invalid * @return {string|null} Time in format HH:MM or null if given string is invalid
*/ */
export function getTimeOnlyString(dateString: string): string | null { export function getTimeOnlyString(dateString: string): string | null {
if (isEventDateStringFormatValid(dateString)) if (isEventDateStringFormatValid(dateString)) return dateString.split(' ')[1];
return dateString.split(" ")[1];
else
return null; 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
*
* @param date The date object to convert
* @param isUTC Whether to treat the date as UTC
* @return {string} The converted string
*/
export function dateToString(date: Date, isUTC: boolean): 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 h = isUTC ? date.getUTCHours() : date.getHours();
const hours = String(h).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: * Returns a string corresponding to the event start and end times in the following format:
* *
@ -145,25 +135,33 @@ export function dateToString(date: Date, isUTC: boolean): string {
*/ */
export function getFormattedEventTime(start: string, end: string): string { export function getFormattedEventTime(start: string, end: string): string {
let formattedStr = '/ - /'; let formattedStr = '/ - /';
let startDate = stringToDate(start); const startDate = stringToDate(start);
let endDate = stringToDate(end); const endDate = stringToDate(end);
if (startDate !== null && endDate !== null && startDate.getTime() !== endDate.getTime()) { if (
formattedStr = String(startDate.getHours()).padStart(2, '0') + ':' startDate !== null &&
+ String(startDate.getMinutes()).padStart(2, '0') + ' - '; endDate !== null &&
if (endDate.getFullYear() > startDate.getFullYear() startDate.getTime() !== endDate.getTime()
|| endDate.getMonth() > startDate.getMonth() ) {
|| endDate.getDate() > startDate.getDate()) 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'; formattedStr += '23:59';
else else
formattedStr += String(endDate.getHours()).padStart(2, '0') + ':' formattedStr += `${String(endDate.getHours()).padStart(2, '0')}:${String(
+ String(endDate.getMinutes()).padStart(2, '0'); endDate.getMinutes(),
).padStart(2, '0')}`;
} else if (startDate !== null) } else if (startDate !== null)
formattedStr = formattedStr = `${String(startDate.getHours()).padStart(2, '0')}:${String(
String(startDate.getHours()).padStart(2, '0') + ':' startDate.getMinutes(),
+ String(startDate.getMinutes()).padStart(2, '0'); ).padStart(2, '0')}`;
return formattedStr return formattedStr;
} }
/** /**
@ -177,11 +175,17 @@ export function getFormattedEventTime(start: string, end: string): string {
*/ */
export function isDescriptionEmpty(description: ?string): boolean { export function isDescriptionEmpty(description: ?string): boolean {
if (description !== undefined && description !== null) { if (description !== undefined && description !== null) {
return description return (
.split('<p>').join('') // Equivalent to a replace all description
.split('</p>').join('') .split('<p>')
.split('<br>').join('').trim() === ''; .join('') // Equivalent to a replace all
} else .split('</p>')
.join('')
.split('<br>')
.join('')
.trim() === ''
);
}
return true; return true;
} }
@ -193,19 +197,45 @@ export function isDescriptionEmpty(description: ?string): boolean {
* @param numberOfMonths The number of months to create, starting from the current date * @param numberOfMonths The number of months to create, starting from the current date
* @return {Object} * @return {Object}
*/ */
export function generateEmptyCalendar(numberOfMonths: number): Object { export function generateEmptyCalendar(
let end = new Date(Date.now()); numberOfMonths: number,
): {[key: string]: Array<PlanningEventType>} {
const end = new Date(Date.now());
end.setMonth(end.getMonth() + numberOfMonths); end.setMonth(end.getMonth() + numberOfMonths);
let daysOfYear = {}; const daysOfYear = {};
for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) { for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) {
const dateString = getDateOnlyString( const dateString = getDateOnlyString(dateToString(new Date(d), false));
dateToString(new Date(d), false)); if (dateString !== null) daysOfYear[dateString] = [];
if (dateString !== null)
daysOfYear[dateString] = []
} }
return daysOfYear; return daysOfYear;
} }
/**
* 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<PlanningEventType>,
event: PlanningEventType,
) {
if (eventArray.length === 0) eventArray.push(event);
else {
for (let i = 0; i < eventArray.length; i += 1) {
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;
}
}
}
}
/** /**
* Generates an object with an array of eventObject at each key. * Generates an object with an array of eventObject at each key.
* Each key is a date string in the format * Each key is a date string in the format
@ -217,40 +247,17 @@ export function generateEmptyCalendar(numberOfMonths: number): Object {
* @param numberOfMonths The number of months to create the agenda for * @param numberOfMonths The number of months to create the agenda for
* @return {Object} * @return {Object}
*/ */
export function generateEventAgenda(eventList: Array<eventObject>, numberOfMonths: number): Object { export function generateEventAgenda(
let agendaItems = generateEmptyCalendar(numberOfMonths); eventList: Array<PlanningEventType>,
for (let i = 0; i < eventList.length; i++) { numberOfMonths: number,
): {[key: string]: Array<PlanningEventType>} {
const agendaItems = generateEmptyCalendar(numberOfMonths);
for (let i = 0; i < eventList.length; i += 1) {
const dateString = getDateOnlyString(eventList[i].date_begin); const dateString = getDateOnlyString(eventList[i].date_begin);
if (dateString !== null) { if (dateString != null) {
const eventArray = agendaItems[dateString]; const eventArray = agendaItems[dateString];
if (eventArray !== undefined) if (eventArray != null) pushEventInOrder(eventArray, eventList[i]);
pushEventInOrder(eventArray, eventList[i]);
} }
} }
return agendaItems; 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

@ -100,15 +100,17 @@ export async function apiRequest(
* If no data was found, returns an empty object * If no data was found, returns an empty object
* *
* @param url The urls to fetch data from * @param url The urls to fetch data from
* @return Promise<{...}> * @return Promise<any>
*/ */
export async function readData(url: string): Promise<{...}> { // eslint-disable-next-line flowtype/no-weak-types
return new Promise( export async function readData(url: string): Promise<any> {
(resolve: (response: {...}) => void, reject: () => void) => { // eslint-disable-next-line flowtype/no-weak-types
return new Promise((resolve: (response: any) => void, reject: () => void) => {
fetch(url) fetch(url)
.then(async (response: Response): Promise<{...}> => response.json()) // eslint-disable-next-line flowtype/no-weak-types
.then((data: {...}): void => resolve(data)) .then(async (response: Response): Promise<any> => response.json())
// eslint-disable-next-line flowtype/no-weak-types
.then((data: any): void => resolve(data))
.catch((): void => reject()); .catch((): void => reject());
}, });
);
} }