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,166 +2,183 @@
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;
errorCode: number;
/** eventId: number;
* Generates data depending on whether the screen was opened from the planning or from a link
*
* @param props
*/
constructor(props) {
super(props);
if (this.props.route.params.data != null) { errorCode: number;
this.displayData = this.props.route.params.data;
this.eventId = this.displayData.id;
this.shouldFetchData = false;
this.errorCode = 0;
this.state = {
loading: false,
};
} else {
this.displayData = null;
this.eventId = this.props.route.params.eventId;
this.shouldFetchData = true;
this.errorCode = 0;
this.state = {
loading: true,
};
this.fetchData();
} /**
* Generates data depending on whether the screen was opened from the planning or from a link
*
* @param props
*/
constructor(props: PropsType) {
super(props);
if (props.route.params.data != null) {
this.displayData = props.route.params.data;
this.eventId = this.displayData.id;
this.shouldFetchData = false;
this.errorCode = 0;
this.state = {
loading: false,
};
} else {
this.displayData = null;
this.eventId = props.route.params.eventId;
this.shouldFetchData = true;
this.errorCode = 0;
this.state = {
loading: true,
};
this.fetchData();
} }
}
/** /**
* Fetches data for the current event id from the API * Hides loading and saves fetched data
*/ *
fetchData = () => { * @param data Received data
this.setState({loading: true}); */
apiRequest(CLUB_INFO_PATH, 'POST', {id: this.eventId}) onFetchSuccess = (data: PlanningEventType) => {
.then(this.onFetchSuccess) this.displayData = data;
.catch(this.onFetchError); this.setState({loading: false});
}; };
/** /**
* Hides loading and saves fetched data * Hides loading and saves the error code
* *
* @param data Received data * @param error
*/ */
onFetchSuccess = (data: Object) => { onFetchError = (error: number) => {
this.displayData = data; this.errorCode = error;
this.setState({loading: false}); this.setState({loading: false});
}; };
/** /**
* Hides loading and saves the error code * Gets content to display
* *
* @param error * @returns {*}
*/ */
onFetchError = (error: number) => { getContent(): React.Node {
this.errorCode = error; const {theme} = this.props;
this.setState({loading: false}); const {displayData} = this;
}; if (displayData == null) return null;
let subtitle = getFormattedEventTime(
displayData.date_begin,
displayData.date_end,
);
const dateString = getDateOnlyString(displayData.date_begin);
if (dateString !== null)
subtitle += ` | ${DateManager.getInstance().getTranslatedDate(
dateString,
)}`;
return (
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
<Card.Title title={displayData.title} subtitle={subtitle} />
{displayData.logo !== null ? (
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={theme.colors.background}
style={{
width: 300,
height: 300,
}}
source={{
uri: displayData.logo,
}}
/>
</View>
) : null}
/** {displayData.description !== null ? (
* Gets content to display <Card.Content
* style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
* @returns {*} <CustomHTML html={displayData.description} />
*/ </Card.Content>
getContent() { ) : (
let subtitle = getFormattedEventTime( <View />
this.displayData["date_begin"], this.displayData["date_end"]); )}
let dateString = getDateOnlyString(this.displayData["date_begin"]); </CollapsibleScrollView>
if (dateString !== null) );
subtitle += ' | ' + DateManager.getInstance().getTranslatedDate(dateString); }
return (
<CollapsibleScrollView
style={{paddingLeft: 5, paddingRight: 5}}
hasTab={true}
>
<Card.Title
title={this.displayData.title}
subtitle={subtitle}
/>
{this.displayData.logo !== null ?
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={this.props.theme.colors.background}
style={{
width: 300,
height: 300,
}}
source={{
uri: this.displayData.logo,
}}
/></View>
: <View/>}
{this.displayData.description !== null ? /**
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> * Shows an error view and use a custom message if the event does not exist
<CustomHTML html={this.displayData.description}/> *
</Card.Content> * @returns {*}
: <View/>} */
</CollapsibleScrollView> getErrorView(): React.Node {
); const {navigation} = this.props;
} if (this.errorCode === ERROR_TYPE.BAD_INPUT)
return (
<ErrorView
navigation={navigation}
showRetryButton={false}
message={i18n.t('screens.planning.invalidEvent')}
icon="calendar-remove"
/>
);
return (
<ErrorView
navigation={navigation}
errorCode={this.errorCode}
onRefresh={this.fetchData}
/>
);
}
/** /**
* Shows an error view and use a custom message if the event does not exist * Fetches data for the current event id from the API
* */
* @returns {*} fetchData = () => {
*/ this.setState({loading: true});
getErrorView() { apiRequest(EVENT_INFO_URL, 'POST', {id: this.eventId})
if (this.errorCode === ERROR_TYPE.BAD_INPUT) .then(this.onFetchSuccess)
return <ErrorView {...this.props} showRetryButton={false} message={i18n.t("screens.planning.invalidEvent")} .catch(this.onFetchError);
icon={"calendar-remove"}/>; };
else
return <ErrorView {...this.props} errorCode={this.errorCode} onRefresh={this.fetchData}/>;
}
render() { render(): React.Node {
if (this.state.loading) const {loading} = this.state;
return <BasicLoadingScreen/>; if (loading) return <BasicLoadingScreen />;
else if (this.errorCode === 0) if (this.errorCode === 0) return this.getContent();
return this.getContent(); return this.getErrorView();
else }
return this.getErrorView();
}
} }
export default withTheme(PlanningDisplayScreen); export default withTheme(PlanningDisplayScreen);

View file

@ -2,259 +2,282 @@
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 type {eventObject} from "../../utils/Planning";
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/Overrides/CustomAgenda"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import {readData} from '../../utils/WebData';
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import type {PlanningEventType} from '../../utils/Planning';
import MascotPopup from "../../components/Mascot/MascotPopup"; import {
import AsyncStorageManager from "../../managers/AsyncStorageManager"; generateEventAgenda,
getCurrentDateString,
getDateOnlyString,
getFormattedEventTime,
} from '../../utils/Planning';
import CustomAgenda from '../../components/Overrides/CustomAgenda';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
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',
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'], 'Mars',
today: 'Aujourd\'hui' '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'],
today: "Aujourd'hui",
}; };
type PropsType = {
type Props = { navigation: StackNavigationProp,
navigation: StackNavigationProp,
}
type State = {
refreshing: boolean,
agendaItems: Object,
calendarShowing: boolean,
}; };
const FETCH_URL = "https://www.amicale-insat.fr/api/event/list"; type StateType = {
refreshing: boolean,
agendaItems: {[key: string]: Array<PlanningEventType>},
calendarShowing: boolean,
};
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());
refreshing: false,
agendaItems: {}, constructor(props: PropsType) {
calendarShowing: false, super(props);
if (i18n.currentLocale().startsWith('fr')) {
LocaleConfig.defaultLocale = 'fr';
}
this.state = {
refreshing: false,
agendaItems: {},
calendarShowing: false,
}; };
}
currentDate = getDateOnlyString(getCurrentDateString()); /**
* Captures focus and blur events to hook on android back button
*/
componentDidMount() {
const {navigation} = this.props;
this.onRefresh();
navigation.addListener('focus', () => {
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
navigation.addListener('blur', () => {
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
}
constructor(props: any) { /**
super(props); * Overrides default android back button behaviour to close the calendar if it was open.
if (i18n.currentLocale().startsWith("fr")) { *
LocaleConfig.defaultLocale = 'fr'; * @return {boolean}
} */
onBackButtonPressAndroid = (): boolean => {
const {calendarShowing} = this.state;
if (calendarShowing && this.agendaRef != null) {
this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
return true;
} }
return false;
};
/** /**
* Captures focus and blur events to hook on android back button * Refreshes data and shows an animation while doing it
*/ */
componentDidMount() { onRefresh = () => {
this.onRefresh(); let canRefresh;
this.props.navigation.addListener( if (this.lastRefresh !== undefined)
'focus', canRefresh =
() => (new Date().getTime() - this.lastRefresh.getTime()) / 1000 >
BackHandler.addEventListener( this.minTimeBetweenRefresh;
'hardwareBackPress', else canRefresh = true;
this.onBackButtonPressAndroid
) if (canRefresh) {
); this.setState({refreshing: true});
this.props.navigation.addListener( readData(FETCH_URL)
'blur', .then((fetchedData: Array<PlanningEventType>) => {
() => this.setState({
BackHandler.removeEventListener( refreshing: false,
'hardwareBackPress', agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN),
this.onBackButtonPressAndroid });
) this.lastRefresh = new Date();
); })
.catch(() => {
this.setState({
refreshing: false,
});
});
} }
};
/** /**
* Overrides default android back button behaviour to close the calendar if it was open. * Callback used when receiving the agenda ref
* *
* @return {boolean} * @param ref
*/ */
onBackButtonPressAndroid = () => { onAgendaRef = (ref: Agenda) => {
if (this.state.calendarShowing) { this.agendaRef = ref;
this.agendaRef.chooseDay(this.agendaRef.state.selectedDay); };
return true;
} else { /**
return false; * Callback used when a button is pressed to toggle the calendar
} *
* @param isCalendarOpened True is the calendar is already open, false otherwise
*/
onCalendarToggled = (isCalendarOpened: boolean) => {
this.setState({calendarShowing: isCalendarOpened});
};
/**
* Gets an event render item
*
* @param item The current event to render
* @return {*}
*/
getRenderItem = (item: PlanningEventType): React.Node => {
const {navigation} = this.props;
const onPress = () => {
navigation.navigate('planning-information', {
data: item,
});
}; };
if (item.logo !== null) {
/** return (
* Function used to check if a row has changed <View>
* <Divider />
* @param r1 <List.Item
* @param r2 title={item.title}
* @return {boolean} description={getFormattedEventTime(item.date_begin, item.date_end)}
*/ left={(): React.Node => (
rowHasChanged(r1: Object, r2: Object) { <Avatar.Image
return false; source={{uri: item.logo}}
// if (r1 !== undefined && r2 !== undefined) style={{backgroundColor: 'transparent'}}
// return r1.title !== r2.title; />
// else return !(r1 === undefined && r2 === undefined); )}
onPress={onPress}
/>
</View>
);
} }
return (
<View>
<Divider />
<List.Item
title={item.title}
description={getFormattedEventTime(item.date_begin, item.date_end)}
onPress={onPress}
/>
</View>
);
};
/** /**
* Refreshes data and shows an animation while doing it * Gets an empty render item for an empty date
*/ *
onRefresh = () => { * @return {*}
let canRefresh; */
if (this.lastRefresh !== undefined) getRenderEmptyDate = (): React.Node => <Divider />;
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) / 1000 > this.minTimeBetweenRefresh;
else
canRefresh = true;
if (canRefresh) { render(): React.Node {
this.setState({refreshing: true}); const {state, props} = this;
readData(FETCH_URL) return (
.then((fetchedData) => { <View style={{flex: 1}}>
this.setState({ <CustomAgenda
refreshing: false, // eslint-disable-next-line react/jsx-props-no-spreading
agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN) {...props}
}); // the list of items that have to be displayed in agenda. If you want to render item as empty date
this.lastRefresh = new Date(); // 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
.catch(() => { items={state.agendaItems}
this.setState({ // initially selected day
refreshing: false, selected={this.currentDate}
}); // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
}); minDate={this.currentDate}
} // Max amount of months allowed to scroll to the past. Default = 50
}; pastScrollRange={1}
// Max amount of months allowed to scroll to the future. Default = 50
/** futureScrollRange={AGENDA_MONTH_SPAN}
* Callback used when receiving the agenda ref // If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make sure to also set the refreshing prop correctly.
* onRefresh={this.onRefresh}
* @param ref // callback that fires when the calendar is opened or closed
*/ onCalendarToggled={this.onCalendarToggled}
onAgendaRef = (ref: Object) => { // Set this true while waiting for new data from a refresh
this.agendaRef = ref; refreshing={state.refreshing}
} renderItem={this.getRenderItem}
renderEmptyDate={this.getRenderEmptyDate}
/** // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
* Callback used when a button is pressed to toggle the calendar firstDay={1}
* // ref to this agenda in order to handle back button event
* @param isCalendarOpened True is the calendar is already open, false otherwise onRef={this.onAgendaRef}
*/ />
onCalendarToggled = (isCalendarOpened: boolean) => { <MascotPopup
this.setState({calendarShowing: isCalendarOpened}); prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key}
} title={i18n.t('screens.planning.mascotDialog.title')}
message={i18n.t('screens.planning.mascotDialog.message')}
/** icon="party-popper"
* Gets an event render item buttons={{
* action: null,
* @param item The current event to render cancel: {
* @return {*} message: i18n.t('screens.planning.mascotDialog.button'),
*/ icon: 'check',
getRenderItem = (item: eventObject) => { },
const onPress = this.props.navigation.navigate.bind(this, 'planning-information', {data: item}); }}
if (item.logo !== null) { emotion={MASCOT_STYLE.HAPPY}
return ( />
<View> </View>
<Divider/> );
<List.Item }
title={item.title}
description={getFormattedEventTime(item["date_begin"], item["date_end"])}
left={() => <Avatar.Image
source={{uri: item.logo}}
style={{backgroundColor: 'transparent'}}
/>}
onPress={onPress}
/>
</View>
);
} else {
return (
<View>
<Divider/>
<List.Item
title={item.title}
description={getFormattedEventTime(item["date_begin"], item["date_end"])}
onPress={onPress}
/>
</View>
);
}
}
/**
* Gets an empty render item for an empty date
*
* @return {*}
*/
getRenderEmptyDate = () => <Divider/>;
render() {
return (
<View style={{flex: 1}}>
<CustomAgenda
{...this.props}
// 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
// considered that the date in question is not yet loaded
items={this.state.agendaItems}
// initially selected day
selected={this.currentDate}
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={this.currentDate}
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={1}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={AGENDA_MONTH_SPAN}
// If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make sure to also set the refreshing prop correctly.
onRefresh={this.onRefresh}
// callback that fires when the calendar is opened or closed
onCalendarToggled={this.onCalendarToggled}
// Set this true while waiting for new data from a refresh
refreshing={this.state.refreshing}
renderItem={this.getRenderItem}
renderEmptyDate={this.getRenderEmptyDate}
rowHasChanged={this.rowHasChanged}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1}
// ref to this agenda in order to handle back button event
onRef={this.onAgendaRef}
/>
<MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.eventsShowBanner.key}
title={i18n.t("screens.planning.mascotDialog.title")}
message={i18n.t("screens.planning.mascotDialog.message")}
icon={"party-popper"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.planning.mascotDialog.button"),
icon: "check",
}
}}
emotion={MASCOT_STYLE.HAPPY}
/>
</View>
);
}
} }
export default PlanningScreen; export default PlanningScreen;

View file

@ -1,74 +1,20 @@
// @flow // @flow
export type eventObject = { export type PlanningEventType = {
id: number, id: number,
title: string, title: string,
logo: string, logo: string,
date_begin: string, date_begin: string,
date_end: string, date_end: string,
description: string, description: string,
club: string, club: string,
category_id: number, category_id: number,
url: string, url: string,
}; };
// 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}$/;
/**
* 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()), false);
}
/**
* 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;
}
/**
* Gets only the time 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} Time in format HH:MM or null if given string is invalid
*/
export function getTimeOnlyString(dateString: string): string | null {
if (isEventDateStringFormatValid(dateString))
return dateString.split(" ")[1];
else
return null;
}
/** /**
* Checks if the given date string is in the format * Checks if the given date string is in the format
* YYYY-MM-DD HH:MM * YYYY-MM-DD HH:MM
@ -77,9 +23,11 @@ export function getTimeOnlyString(dateString: string): string | null {
* @return {boolean} * @return {boolean}
*/ */
export function isEventDateStringFormatValid(dateString: ?string): boolean { export function isEventDateStringFormatValid(dateString: ?string): boolean {
return dateString !== undefined return (
&& dateString !== null dateString !== undefined &&
&& dateRegExp.test(dateString); dateString !== null &&
dateRegExp.test(dateString)
);
} }
/** /**
@ -90,26 +38,20 @@ export function isEventDateStringFormatValid(dateString: ?string): boolean {
* @return {Date|null} The date object or null if the given string is invalid * @return {Date|null} The date object or null if the given string is invalid
*/ */
export function stringToDate(dateString: string): Date | null { export function stringToDate(dateString: string): Date | null {
let date = new Date(); let date = new Date();
if (isEventDateStringFormatValid(dateString)) { if (isEventDateStringFormatValid(dateString)) {
let stringArray = dateString.split(' '); const stringArray = dateString.split(' ');
let dateArray = stringArray[0].split('-'); const dateArray = stringArray[0].split('-');
let timeArray = stringArray[1].split(':'); const timeArray = stringArray[1].split(':');
date.setFullYear( date.setFullYear(
parseInt(dateArray[0]), parseInt(dateArray[0], 10),
parseInt(dateArray[1]) - 1, // Month range from 0 to 11 parseInt(dateArray[1], 10) - 1, // Month range from 0 to 11
parseInt(dateArray[2]) parseInt(dateArray[2], 10),
); );
date.setHours( date.setHours(parseInt(timeArray[0], 10), parseInt(timeArray[1], 10), 0, 0);
parseInt(timeArray[0]), } else date = null;
parseInt(timeArray[1]),
0,
0,
);
} else
date = null;
return date; return date;
} }
/** /**
@ -121,13 +63,61 @@ export function stringToDate(dateString: string): Date | null {
* @return {string} The converted string * @return {string} The converted string
*/ */
export function dateToString(date: Date, isUTC: boolean): string { export function dateToString(date: Date, isUTC: boolean): string {
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! const month = String(date.getMonth() + 1).padStart(2, '0'); // January is 0!
const year = date.getFullYear(); const year = date.getFullYear();
const h = isUTC ? date.getUTCHours() : date.getHours(); const h = isUTC ? date.getUTCHours() : date.getHours();
const hours = String(h).padStart(2, '0'); const hours = String(h).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0');
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes; return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/**
* 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()), false);
}
/**
* 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 {
const date1 = stringToDate(event1Date);
const date2 = stringToDate(event2Date);
if (date1 !== null && date2 !== null) return date1 < date2;
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];
return null;
}
/**
* Gets only the time 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} Time in format HH:MM or null if given string is invalid
*/
export function getTimeOnlyString(dateString: string): string | null {
if (isEventDateStringFormatValid(dateString)) return dateString.split(' ')[1];
return null;
} }
/** /**
@ -144,26 +134,34 @@ export function dateToString(date: Date, isUTC: boolean): string {
* @return {string} Formatted string or "/ - /" on error * @return {string} Formatted string or "/ - /" on error
*/ */
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(
formattedStr += '23:59'; startDate.getMinutes(),
else ).padStart(2, '0')} - `;
formattedStr += String(endDate.getHours()).padStart(2, '0') + ':' if (
+ String(endDate.getMinutes()).padStart(2, '0'); endDate.getFullYear() > startDate.getFullYear() ||
} else if (startDate !== null) endDate.getMonth() > startDate.getMonth() ||
formattedStr = endDate.getDate() > startDate.getDate()
String(startDate.getHours()).padStart(2, '0') + ':' )
+ String(startDate.getMinutes()).padStart(2, '0'); 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 return formattedStr;
} }
/** /**
@ -176,13 +174,19 @@ export function getFormattedEventTime(start: string, end: string): string {
* @return {boolean} * @return {boolean}
*/ */
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>')
return true; .join('')
.split('<br>')
.join('')
.trim() === ''
);
}
return true;
} }
/** /**
@ -193,17 +197,43 @@ 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,
end.setMonth(end.getMonth() + numberOfMonths); ): {[key: string]: Array<PlanningEventType>} {
let daysOfYear = {}; const end = new Date(Date.now());
for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) { end.setMonth(end.getMonth() + numberOfMonths);
const dateString = getDateOnlyString( const daysOfYear = {};
dateToString(new Date(d), false)); for (let d = new Date(Date.now()); d <= end; d.setDate(d.getDate() + 1)) {
if (dateString !== null) const dateString = getDateOnlyString(dateToString(new Date(d), false));
daysOfYear[dateString] = [] if (dateString !== null) daysOfYear[dateString] = [];
}
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;
}
} }
return daysOfYear; }
} }
/** /**
@ -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,
const dateString = getDateOnlyString(eventList[i].date_begin); ): {[key: string]: Array<PlanningEventType>} {
if (dateString !== null) { const agendaItems = generateEmptyCalendar(numberOfMonths);
const eventArray = agendaItems[dateString]; for (let i = 0; i < eventList.length; i += 1) {
if (eventArray !== undefined) const dateString = getDateOnlyString(eventList[i].date_begin);
pushEventInOrder(eventArray, eventList[i]); if (dateString != null) {
} const eventArray = agendaItems[dateString];
if (eventArray != null) 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;
}
}
} }
}
return agendaItems;
} }

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
fetch(url) return new Promise((resolve: (response: any) => void, reject: () => void) => {
.then(async (response: Response): Promise<{...}> => response.json()) fetch(url)
.then((data: {...}): void => resolve(data)) // eslint-disable-next-line flowtype/no-weak-types
.catch((): void => reject()); .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());
});
} }