Improve equipment booking components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-04 10:57:19 +02:00
parent 70365136ac
commit 11b5f2ac71
5 changed files with 871 additions and 863 deletions

View file

@ -2,111 +2,112 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, List, withTheme} from 'react-native-paper'; import {Avatar, List, withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager"; import i18n from 'i18n-js';
import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import i18n from "i18n-js"; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import { import {
getFirstEquipmentAvailability, getFirstEquipmentAvailability,
getRelativeDateString, getRelativeDateString,
isEquipmentAvailable isEquipmentAvailable,
} from "../../../utils/EquipmentBooking"; } from '../../../utils/EquipmentBooking';
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
userDeviceRentDates: [string, string], userDeviceRentDates: [string, string],
item: Device, item: DeviceType,
height: number, height: number,
theme: CustomTheme, theme: CustomTheme,
} };
class EquipmentListItem extends React.Component<Props> { class EquipmentListItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {userDeviceRentDates} = this.props;
return nextProps.userDeviceRentDates !== userDeviceRentDates;
}
shouldComponentUpdate(nextProps: Props): boolean { render(): React.Node {
return nextProps.userDeviceRentDates !== this.props.userDeviceRentDates; const {item, userDeviceRentDates, navigation, height, theme} = this.props;
} const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
render() { let onPress;
const colors = this.props.theme.colors; if (isRented)
const item = this.props.item; onPress = () => {
const userDeviceRentDates = this.props.userDeviceRentDates; navigation.navigate('equipment-confirm', {
const isRented = userDeviceRentDates != null; item,
const isAvailable = isEquipmentAvailable(item); dates: userDeviceRentDates,
const firstAvailability = getFirstEquipmentAvailability(item); });
};
else
onPress = () => {
navigation.navigate('equipment-rent', {item});
};
let onPress; let description;
if (isRented) if (isRented) {
onPress = () => this.props.navigation.navigate("equipment-confirm", { const start = new Date(userDeviceRentDates[0]);
item: item, const end = new Date(userDeviceRentDates[1]);
dates: userDeviceRentDates if (start.getTime() !== end.getTime())
}); description = i18n.t('screens.equipment.bookingPeriod', {
else begin: getRelativeDateString(start),
onPress = () => this.props.navigation.navigate("equipment-rent", {item: item}); end: getRelativeDateString(end),
});
else
description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start),
});
} else if (isAvailable)
description = i18n.t('screens.equipment.bail', {cost: item.caution});
else
description = i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
});
let description; let icon;
if (isRented) { if (isRented) icon = 'bookmark-check';
const start = new Date(userDeviceRentDates[0]); else if (isAvailable) icon = 'check-circle-outline';
const end = new Date(userDeviceRentDates[1]); else icon = 'update';
if (start.getTime() !== end.getTime())
description = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end)
});
else
description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start)
});
} else if (isAvailable)
description = i18n.t('screens.equipment.bail', {cost: item.caution});
else
description = i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)});
let icon; let color;
if (isRented) if (isRented) color = theme.colors.warning;
icon = "bookmark-check"; else if (isAvailable) color = theme.colors.success;
else if (isAvailable) else color = theme.colors.primary;
icon = "check-circle-outline";
else
icon = "update";
let color; return (
if (isRented) <List.Item
color = colors.warning; title={item.name}
else if (isAvailable) description={description}
color = colors.success; onPress={onPress}
else left={({size}: {size: number}): React.Node => (
color = colors.primary; <Avatar.Icon
size={size}
return ( style={{
<List.Item backgroundColor: 'transparent',
title={item.name} }}
description={description} icon={icon}
onPress={onPress} color={color}
left={(props) => <Avatar.Icon />
{...props} )}
style={{ right={(): React.Node => (
backgroundColor: 'transparent', <Avatar.Icon
}} style={{
icon={icon} marginTop: 'auto',
color={color} marginBottom: 'auto',
/>} backgroundColor: 'transparent',
right={(props) => <Avatar.Icon }}
{...props} size={48}
style={{ icon="chevron-right"
marginTop: 'auto', />
marginBottom: 'auto', )}
backgroundColor: 'transparent', style={{
}} height,
size={48} justifyContent: 'center',
icon={"chevron-right"} }}
/>} />
style={{ );
height: this.props.height, }
justifyContent: 'center',
}}
/>
);
}
} }
export default withTheme(EquipmentListItem); export default withTheme(EquipmentListItem);

View file

@ -1,105 +1,102 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Caption, Card, Headline, Paragraph, withTheme} from 'react-native-paper'; import {
import {StackNavigationProp} from "@react-navigation/stack"; Button,
import type {CustomTheme} from "../../../managers/ThemeManager"; Caption,
import type {Device} from "./EquipmentListScreen"; Card,
import {View} from "react-native"; Headline,
import i18n from "i18n-js"; Paragraph,
import {getRelativeDateString} from "../../../utils/EquipmentBooking"; withTheme,
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; } from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomTheme} from '../../../managers/ThemeManager';
import type {DeviceType} from './EquipmentListScreen';
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, route: {
route: { params?: {
params?: { item?: DeviceType,
item?: Device, dates: [string, string],
dates: [string, string]
},
}, },
theme: CustomTheme, },
} theme: CustomTheme,
};
class EquipmentConfirmScreen extends React.Component<PropsType> {
item: DeviceType | null;
class EquipmentConfirmScreen extends React.Component<Props> { dates: [string, string] | null;
item: Device | null; constructor(props: PropsType) {
dates: [string, string] | null; super(props);
if (props.route.params != null) {
constructor(props: Props) { if (props.route.params.item != null) this.item = props.route.params.item;
super(props); else this.item = null;
if (this.props.route.params != null) { if (props.route.params.dates != null)
if (this.props.route.params.item != null) this.dates = props.route.params.dates;
this.item = this.props.route.params.item; else this.dates = null;
else
this.item = null;
if (this.props.route.params.dates != null)
this.dates = this.props.route.params.dates;
else
this.dates = null;
}
} }
}
render() { render(): React.Node {
const item = this.item; const {item, dates, props} = this;
const dates = this.dates; if (item != null && dates != null) {
if (item != null && dates != null) { const start = new Date(dates[0]);
const start = new Date(dates[0]); const end = new Date(dates[1]);
const end = new Date(dates[1]); let buttonText;
return ( if (start == null) buttonText = i18n.t('screens.equipment.booking');
<CollapsibleScrollView> else if (end != null && start.getTime() !== end.getTime())
<Card style={{margin: 5}}> buttonText = i18n.t('screens.equipment.bookingPeriod', {
<Card.Content> begin: getRelativeDateString(start),
<View style={{flex: 1}}> end: getRelativeDateString(end),
<View style={{ });
marginLeft: "auto", else
marginRight: "auto", buttonText = i18n.t('screens.equipment.bookingDay', {
flexDirection: "row", date: getRelativeDateString(start),
flexWrap: "wrap", });
}}> return (
<Headline style={{textAlign: "center"}}> <CollapsibleScrollView>
{item.name} <Card style={{margin: 5}}>
</Headline> <Card.Content>
<Caption style={{ <View style={{flex: 1}}>
textAlign: "center", <View
lineHeight: 35, style={{
marginLeft: 10, marginLeft: 'auto',
}}> marginRight: 'auto',
({i18n.t('screens.equipment.bail', {cost: item.caution})}) flexDirection: 'row',
</Caption> flexWrap: 'wrap',
</View> }}>
</View> <Headline style={{textAlign: 'center'}}>{item.name}</Headline>
<Button <Caption
icon={"check-circle-outline"} style={{
color={this.props.theme.colors.success} textAlign: 'center',
mode="text" lineHeight: 35,
> marginLeft: 10,
{ }}>
start == null ({i18n.t('screens.equipment.bail', {cost: item.caution})})
? i18n.t('screens.equipment.booking') </Caption>
: end != null && start.getTime() !== end.getTime() </View>
? i18n.t('screens.equipment.bookingPeriod', { </View>
begin: getRelativeDateString(start), <Button
end: getRelativeDateString(end) icon="check-circle-outline"
}) color={props.theme.colors.success}
: i18n.t('screens.equipment.bookingDay', { mode="text">
date: getRelativeDateString(start) {buttonText}
}) </Button>
} <Paragraph style={{textAlign: 'center'}}>
</Button> {i18n.t('screens.equipment.bookingConfirmedMessage')}
<Paragraph style={{textAlign: "center"}}> </Paragraph>
{i18n.t("screens.equipment.bookingConfirmedMessage")} </Card.Content>
</Paragraph> </Card>
</Card.Content> </CollapsibleScrollView>
</Card> );
</CollapsibleScrollView>
);
} else
return null;
} }
return null;
}
} }
export default withTheme(EquipmentConfirmScreen); export default withTheme(EquipmentConfirmScreen);

View file

@ -1,193 +1,197 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import {Button, withTheme} from 'react-native-paper'; import {Button, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import i18n from 'i18n-js';
import type {CustomTheme} from "../../../managers/ThemeManager"; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import i18n from "i18n-js"; import type {ClubType} from '../Clubs/ClubListScreen';
import type {club} from "../Clubs/ClubListScreen"; import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem"; import MascotPopup from '../../../components/Mascot/MascotPopup';
import MascotPopup from "../../../components/Mascot/MascotPopup"; import {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
import {MASCOT_STYLE} from "../../../components/Mascot/Mascot"; import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import AsyncStorageManager from "../../../managers/AsyncStorageManager"; import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; import type {ApiGenericDataType} from '../../../utils/WebData';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme,
}
type State = {
mascotDialogVisible: boolean,
}
export type Device = {
id: number,
name: string,
caution: number,
booked_at: Array<{ begin: string, end: string }>,
}; };
export type RentedDevice = { type StateType = {
device_id: number, mascotDialogVisible: boolean,
device_name: string, };
begin: string,
end: string, export type DeviceType = {
} id: number,
name: string,
caution: number,
booked_at: Array<{begin: string, end: string}>,
};
export type RentedDeviceType = {
device_id: number,
device_name: string,
begin: string,
end: string,
};
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
class EquipmentListScreen extends React.Component<Props, State> { class EquipmentListScreen extends React.Component<PropsType, StateType> {
data: Array<DeviceType>;
state = { userRents: Array<RentedDeviceType>;
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key),
}
data: Array<Device>; authRef: {current: null | AuthenticatedScreen};
userRents: Array<RentedDevice>;
authRef: { current: null | AuthenticatedScreen }; canRefresh: boolean;
canRefresh: boolean;
constructor(props: Props) { constructor(props: PropsType) {
super(props); super(props);
this.canRefresh = false; this.state = {
this.authRef = React.createRef(); mascotDialogVisible: AsyncStorageManager.getBool(
this.props.navigation.addListener('focus', this.onScreenFocus); AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
} ),
onScreenFocus = () => {
if (this.canRefresh && this.authRef.current != null)
this.authRef.current.reload();
this.canRefresh = true;
}; };
this.canRefresh = false;
this.authRef = React.createRef();
props.navigation.addListener('focus', this.onScreenFocus);
}
getRenderItem = ({item}: { item: Device }) => { onScreenFocus = () => {
return ( if (this.canRefresh && this.authRef.current != null)
<EquipmentListItem this.authRef.current.reload();
navigation={this.props.navigation} this.canRefresh = true;
item={item} };
userDeviceRentDates={this.getUserDeviceRentDates(item)}
height={LIST_ITEM_HEIGHT}/>
);
};
getUserDeviceRentDates(item: Device) { getRenderItem = ({item}: {item: DeviceType}): React.Node => {
let dates = null; const {navigation} = this.props;
for (let i = 0; i < this.userRents.length; i++) { return (
let device = this.userRents[i]; <EquipmentListItem
if (item.id === device.device_id) { navigation={navigation}
dates = [device.begin, device.end]; item={item}
break; userDeviceRentDates={this.getUserDeviceRentDates(item)}
} height={LIST_ITEM_HEIGHT}
} />
return dates; );
};
getUserDeviceRentDates(item: DeviceType): [number, number] | null {
let dates = null;
this.userRents.forEach((device: RentedDeviceType) => {
if (item.id === device.device_id) {
dates = [device.begin, device.end];
}
});
return dates;
}
/**
* Gets the list header, with explains this screen's purpose
*
* @returns {*}
*/
getListHeader(): React.Node {
return (
<View
style={{
width: '100%',
marginTop: 10,
marginBottom: 10,
}}>
<Button
mode="contained"
icon="help-circle"
onPress={this.showMascotDialog}
style={{
marginRight: 'auto',
marginLeft: 'auto',
}}>
{i18n.t('screens.equipment.mascotDialog.title')}
</Button>
</View>
);
}
keyExtractor = (item: ClubType): string => item.id.toString();
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
if (data[0] != null) {
const fetchedData = data[0];
if (fetchedData != null) this.data = fetchedData.devices;
} }
if (data[1] != null) {
/** const fetchedData = data[1];
* Gets the list header, with explains this screen's purpose if (fetchedData != null) this.userRents = fetchedData.locations;
*
* @returns {*}
*/
getListHeader() {
return (
<View style={{
width: "100%",
marginTop: 10,
marginBottom: 10,
}}>
<Button
mode={"contained"}
icon={"help-circle"}
onPress={this.showMascotDialog}
style={{
marginRight: "auto",
marginLeft: "auto",
}}>
{i18n.t("screens.equipment.mascotDialog.title")}
</Button>
</View>
);
} }
return (
<CollapsibleFlatList
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
data={this.data}
/>
);
};
keyExtractor = (item: club) => item.id.toString(); showMascotDialog = () => {
this.setState({mascotDialogVisible: true});
};
/** hideMascotDialog = () => {
* Gets the main screen component with the fetched data AsyncStorageManager.set(
* AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
* @param data The data fetched from the server false,
* @returns {*} );
*/ this.setState({mascotDialogVisible: false});
getScreen = (data: Array<{ [key: string]: any } | null>) => { };
if (data[0] != null) {
const fetchedData = data[0];
if (fetchedData != null)
this.data = fetchedData["devices"];
}
if (data[1] != null) {
const fetchedData = data[1];
if (fetchedData != null)
this.userRents = fetchedData["locations"];
}
return (
<CollapsibleFlatList
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
data={this.data}
/>
)
};
showMascotDialog = () => { render(): React.Node {
this.setState({mascotDialogVisible: true}) const {props, state} = this;
}; return (
<View style={{flex: 1}}>
hideMascotDialog = () => { <AuthenticatedScreen
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key, false); navigation={props.navigation}
this.setState({mascotDialogVisible: false}) ref={this.authRef}
}; requests={[
{
render() { link: 'location/all',
return ( params: {},
<View style={{flex: 1}}> mandatory: true,
<AuthenticatedScreen },
{...this.props} {
ref={this.authRef} link: 'location/my',
requests={[ params: {},
{ mandatory: false,
link: 'location/all', },
params: {}, ]}
mandatory: true, renderFunction={this.getScreen}
}, />
{ <MascotPopup
link: 'location/my', visible={state.mascotDialogVisible}
params: {}, title={i18n.t('screens.equipment.mascotDialog.title')}
mandatory: false, message={i18n.t('screens.equipment.mascotDialog.message')}
} icon="vote"
]} buttons={{
renderFunction={this.getScreen} action: null,
/> cancel: {
<MascotPopup message: i18n.t('screens.equipment.mascotDialog.button'),
visible={this.state.mascotDialogVisible} icon: 'check',
title={i18n.t("screens.equipment.mascotDialog.title")} onPress: this.hideMascotDialog,
message={i18n.t("screens.equipment.mascotDialog.message")} },
icon={"vote"} }}
buttons={{ emotion={MASCOT_STYLE.WINK}
action: null, />
cancel: { </View>
message: i18n.t("screens.equipment.mascotDialog.button"), );
icon: "check", }
onPress: this.hideMascotDialog,
}
}}
emotion={MASCOT_STYLE.WINK}
/>
</View>
);
}
} }
export default withTheme(EquipmentListScreen); export default withTheme(EquipmentListScreen);

View file

@ -1,441 +1,441 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper';
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {Device} from "./EquipmentListScreen";
import {BackHandler, View} from "react-native";
import * as Animatable from "react-native-animatable";
import i18n from "i18n-js";
import {CalendarList} from "react-native-calendars";
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
import { import {
generateMarkedDates, Button,
getFirstEquipmentAvailability, Caption,
getISODate, Card,
getRelativeDateString, Headline,
getValidRange, Subheading,
isEquipmentAvailable withTheme,
} from "../../../utils/EquipmentBooking"; } from 'react-native-paper';
import ConnectionManager from "../../../managers/ConnectionManager"; import {StackNavigationProp} from '@react-navigation/stack';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; import {BackHandler, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js';
import {CalendarList} from 'react-native-calendars';
import type {DeviceType} from './EquipmentListScreen';
import type {CustomTheme} from '../../../managers/ThemeManager';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import {
generateMarkedDates,
getFirstEquipmentAvailability,
getISODate,
getRelativeDateString,
getValidRange,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
import ConnectionManager from '../../../managers/ConnectionManager';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
item?: Device, item?: DeviceType,
},
}, },
theme: CustomTheme, },
} theme: CustomTheme,
};
type State = { export type MarkedDatesObjectType = {
dialogVisible: boolean, [key: string]: {startingDay: boolean, endingDay: boolean, color: string},
errorDialogVisible: boolean, };
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
currentError: number,
}
class EquipmentRentScreen extends React.Component<Props, State> { type StateType = {
dialogVisible: boolean,
errorDialogVisible: boolean,
markedDates: MarkedDatesObjectType,
currentError: number,
};
state = { class EquipmentRentScreen extends React.Component<PropsType, StateType> {
dialogVisible: false, item: DeviceType | null;
errorDialogVisible: false,
markedDates: {},
currentError: 0,
}
item: Device | null; bookedDates: Array<string>;
bookedDates: Array<string>;
bookRef: { current: null | Animatable.View } bookRef: {current: null | Animatable.View};
canBookEquipment: boolean;
lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } } canBookEquipment: boolean;
constructor(props: Props) { lockedDates: {
super(props); [key: string]: {startingDay: boolean, endingDay: boolean, color: string},
this.resetSelection(); };
this.bookRef = React.createRef();
this.canBookEquipment = false;
this.bookedDates = [];
if (this.props.route.params != null) {
if (this.props.route.params.item != null)
this.item = this.props.route.params.item;
else
this.item = null;
}
const item = this.item;
if (item != null) {
this.lockedDates = {};
for (let i = 0; i < item.booked_at.length; i++) {
const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null);
this.lockedDates = {
...this.lockedDates,
...generateMarkedDates(
false,
this.props.theme,
range
)
};
}
}
} constructor(props: PropsType) {
super(props);
/** this.state = {
* Captures focus and blur events to hook on android back button dialogVisible: false,
*/ errorDialogVisible: false,
componentDidMount() { markedDates: {},
this.props.navigation.addListener( currentError: 0,
'focus',
() =>
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
this.props.navigation.addListener(
'blur',
() =>
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
}
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
onBackButtonPressAndroid = () => {
if (this.bookedDates.length > 0) {
this.resetSelection();
this.updateMarkedSelection();
return true;
} else
return false;
}; };
this.resetSelection();
/** this.bookRef = React.createRef();
* Selects a new date on the calendar. this.canBookEquipment = false;
* If both start and end dates are already selected, unselect all. this.bookedDates = [];
* if (props.route.params != null) {
* @param day The day selected if (props.route.params.item != null) this.item = props.route.params.item;
*/ else this.item = null;
selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
const selected = new Date(day.dateString);
const start = this.getBookStartDate();
if (!(this.lockedDates.hasOwnProperty(day.dateString))) {
if (start === null) {
this.updateSelectionRange(selected, selected);
this.enableBooking();
} else if (start.getTime() === selected.getTime()) {
this.resetSelection();
} else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected);
this.enableBooking();
} else
this.resetSelection();
this.updateMarkedSelection();
}
} }
const {item} = this;
updateSelectionRange(start: Date, end: Date) { if (item != null) {
this.bookedDates = getValidRange(start, end, this.item); this.lockedDates = {};
item.booked_at.forEach((date: {begin: string, end: string}) => {
const range = getValidRange(
new Date(date.begin),
new Date(date.end),
null,
);
this.lockedDates = {
...this.lockedDates,
...generateMarkedDates(false, props.theme, range),
};
});
} }
}
updateMarkedSelection() { /**
this.setState({ * Captures focus and blur events to hook on android back button
markedDates: generateMarkedDates( */
true, componentDidMount() {
this.props.theme, const {navigation} = this.props;
this.bookedDates navigation.addListener('focus', () => {
), BackHandler.addEventListener(
}); 'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
navigation.addListener('blur', () => {
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
}
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
onBackButtonPressAndroid = (): boolean => {
if (this.bookedDates.length > 0) {
this.resetSelection();
this.updateMarkedSelection();
return true;
} }
return false;
};
enableBooking() { onDialogDismiss = () => {
if (!this.canBookEquipment) { this.setState({dialogVisible: false});
this.showBookButton(); };
this.canBookEquipment = true;
} onErrorDialogDismiss = () => {
this.setState({errorDialogVisible: false});
};
/**
* Sends the selected data to the server and waits for a response.
* If the request is a success, navigate to the recap screen.
* If it is an error, display the error to the user.
*
* @returns {Promise<void>}
*/
onDialogAccept = (): Promise<void> => {
return new Promise((resolve: () => void) => {
const {item, props} = this;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
if (item != null && start != null && end != null) {
ConnectionManager.getInstance()
.authenticatedRequest('location/booking', {
device: item.id,
begin: getISODate(start),
end: getISODate(end),
})
.then(() => {
this.onDialogDismiss();
props.navigation.replace('equipment-confirm', {
item: this.item,
dates: [getISODate(start), getISODate(end)],
});
resolve();
})
.catch((error: number) => {
this.onDialogDismiss();
this.showErrorDialog(error);
resolve();
});
} else {
this.onDialogDismiss();
resolve();
}
});
};
getBookStartDate(): Date | null {
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
}
getBookEndDate(): Date | null {
const {length} = this.bookedDates;
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
}
/**
* Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all.
*
* @param day The day selected
*/
selectNewDate = (day: {
dateString: string,
day: number,
month: number,
timestamp: number,
year: number,
}) => {
const selected = new Date(day.dateString);
const start = this.getBookStartDate();
if (!this.lockedDates[day.dateString] != null) {
if (start === null) {
this.updateSelectionRange(selected, selected);
this.enableBooking();
} else if (start.getTime() === selected.getTime()) {
this.resetSelection();
} else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected);
this.enableBooking();
} else this.resetSelection();
this.updateMarkedSelection();
} }
};
resetSelection() { showErrorDialog = (error: number) => {
if (this.canBookEquipment) this.setState({
this.hideBookButton(); errorDialogVisible: true,
this.canBookEquipment = false; currentError: error,
this.bookedDates = []; });
};
showDialog = () => {
this.setState({dialogVisible: true});
};
/**
* Shows the book button by plying a fade animation
*/
showBookButton() {
if (this.bookRef.current != null) {
this.bookRef.current.fadeInUp(500);
} }
}
/** /**
* Shows the book button by plying a fade animation * Hides the book button by plying a fade animation
*/ */
showBookButton() { hideBookButton() {
if (this.bookRef.current != null) { if (this.bookRef.current != null) {
this.bookRef.current.fadeInUp(500); this.bookRef.current.fadeOutDown(500);
}
} }
}
/** enableBooking() {
* Hides the book button by plying a fade animation if (!this.canBookEquipment) {
*/ this.showBookButton();
hideBookButton() { this.canBookEquipment = true;
if (this.bookRef.current != null) {
this.bookRef.current.fadeOutDown(500);
}
} }
}
showDialog = () => { resetSelection() {
this.setState({dialogVisible: true}); if (this.canBookEquipment) this.hideBookButton();
} this.canBookEquipment = false;
this.bookedDates = [];
}
showErrorDialog = (error: number) => { updateSelectionRange(start: Date, end: Date) {
this.setState({ this.bookedDates = getValidRange(start, end, this.item);
errorDialogVisible: true, }
currentError: error,
});
}
onDialogDismiss = () => { updateMarkedSelection() {
this.setState({dialogVisible: false}); const {theme} = this.props;
} this.setState({
markedDates: generateMarkedDates(true, theme, this.bookedDates),
});
}
onErrorDialogDismiss = () => { render(): React.Node {
this.setState({errorDialogVisible: false}); const {item, props, state} = this;
} const start = this.getBookStartDate();
const end = this.getBookEndDate();
/** let subHeadingText;
* Sends the selected data to the server and waits for a response. if (start == null) subHeadingText = i18n.t('screens.equipment.booking');
* If the request is a success, navigate to the recap screen. else if (end != null && start.getTime() !== end.getTime())
* If it is an error, display the error to the user. subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
* begin: getRelativeDateString(start),
* @returns {Promise<R>} end: getRelativeDateString(end),
*/ });
onDialogAccept = () => { else
return new Promise((resolve) => { i18n.t('screens.equipment.bookingDay', {
const item = this.item; date: getRelativeDateString(start),
const start = this.getBookStartDate(); });
const end = this.getBookEndDate(); if (item != null) {
if (item != null && start != null && end != null) { const isAvailable = isEquipmentAvailable(item);
console.log({ const firstAvailability = getFirstEquipmentAvailability(item);
"device": item.id, return (
"begin": getISODate(start), <View style={{flex: 1}}>
"end": getISODate(end), <CollapsibleScrollView>
}) <Card style={{margin: 5}}>
ConnectionManager.getInstance().authenticatedRequest( <Card.Content>
"location/booking",
{
"device": item.id,
"begin": getISODate(start),
"end": getISODate(end),
})
.then(() => {
this.onDialogDismiss();
this.props.navigation.replace("equipment-confirm", {
item: this.item,
dates: [getISODate(start), getISODate(end)]
});
resolve();
})
.catch((error: number) => {
this.onDialogDismiss();
this.showErrorDialog(error);
resolve();
});
} else {
this.onDialogDismiss();
resolve();
}
});
}
getBookStartDate() {
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
}
getBookEndDate() {
const length = this.bookedDates.length;
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
}
render() {
const item = this.item;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
if (item != null) {
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<CollapsibleScrollView> <View
<Card style={{margin: 5}}> style={{
<Card.Content> marginLeft: 'auto',
<View style={{flex: 1}}> marginRight: 'auto',
<View style={{ flexDirection: 'row',
marginLeft: "auto", flexWrap: 'wrap',
marginRight: "auto", }}>
flexDirection: "row", <Headline style={{textAlign: 'center'}}>
flexWrap: "wrap", {item.name}
}}> </Headline>
<Headline style={{textAlign: "center"}}> <Caption
{item.name} style={{
</Headline> textAlign: 'center',
<Caption style={{ lineHeight: 35,
textAlign: "center", marginLeft: 10,
lineHeight: 35, }}>
marginLeft: 10, ({i18n.t('screens.equipment.bail', {cost: item.caution})})
}}> </Caption>
({i18n.t('screens.equipment.bail', {cost: item.caution})}) </View>
</Caption>
</View>
</View>
<Button
icon={isAvailable ? "check-circle-outline" : "update"}
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
mode="text"
>
{i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)})}
</Button>
<Subheading style={{
textAlign: "center",
marginBottom: 10,
minHeight: 50
}}>
{
start == null
? i18n.t('screens.equipment.booking')
: end != null && start.getTime() !== end.getTime()
? i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end)
})
: i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start)
})
}
</Subheading>
</Card.Content>
</Card>
<CalendarList
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={new Date()}
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={0}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3}
// Enable horizontal scrolling, default = false
horizontal={true}
// Enable paging on horizontal, default = false
pagingEnabled={true}
// Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays={true}
// Hide month navigation arrows.
hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType={'period'}
markedDates={{...this.lockedDates, ...this.state.markedDates}}
theme={{
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
calendarBackground: this.props.theme.colors.background,
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: this.props.theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: this.props.theme.colors.text,
dayTextColor: this.props.theme.colors.text,
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
dotColor: this.props.theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: this.props.theme.colors.primary,
monthTextColor: this.props.theme.colors.text,
indicatorColor: this.props.theme.colors.primary,
textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace',
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
'stylesheet.day.period': {
base: {
overflow: 'hidden',
height: 34,
width: 34,
alignItems: 'center',
}
}
}}
style={{marginBottom: 50}}
/>
</CollapsibleScrollView>
<LoadingConfirmDialog
visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')}
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
message={i18n.t('screens.equipment.dialogMessage')}
/>
<ErrorDialog
visible={this.state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={this.state.currentError}
/>
<Animatable.View
ref={this.bookRef}
style={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
flex: 1,
transform: [
{translateY: 100},
]
}}>
<Button
icon="bookmark-check"
mode="contained"
onPress={this.showDialog}
style={{
width: "80%",
flex: 1,
marginLeft: "auto",
marginRight: "auto",
marginBottom: 20,
borderRadius: 10
}}
>
{i18n.t('screens.equipment.bookButton')}
</Button>
</Animatable.View>
</View> </View>
) <Button
} else icon={isAvailable ? 'check-circle-outline' : 'update'}
return <View/>; color={
} isAvailable
? props.theme.colors.success
: props.theme.colors.primary
}
mode="text">
{i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
})}
</Button>
<Subheading
style={{
textAlign: 'center',
marginBottom: 10,
minHeight: 50,
}}>
{subHeadingText}
</Subheading>
</Card.Content>
</Card>
<CalendarList
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={new Date()}
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={0}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3}
// Enable horizontal scrolling, default = false
horizontal
// Enable paging on horizontal, default = false
pagingEnabled
// Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays
// Hide month navigation arrows.
hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType="period"
markedDates={{...this.lockedDates, ...state.markedDates}}
theme={{
backgroundColor: props.theme.colors.agendaBackgroundColor,
calendarBackground: props.theme.colors.background,
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: props.theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: props.theme.colors.text,
dayTextColor: props.theme.colors.text,
textDisabledColor: props.theme.colors.agendaDayTextColor,
dotColor: props.theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: props.theme.colors.primary,
monthTextColor: props.theme.colors.text,
indicatorColor: props.theme.colors.primary,
textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace',
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
'stylesheet.day.period': {
base: {
overflow: 'hidden',
height: 34,
width: 34,
alignItems: 'center',
},
},
}}
style={{marginBottom: 50}}
/>
</CollapsibleScrollView>
<LoadingConfirmDialog
visible={state.dialogVisible}
onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')}
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
message={i18n.t('screens.equipment.dialogMessage')}
/>
<ErrorDialog
visible={state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={state.currentError}
/>
<Animatable.View
ref={this.bookRef}
style={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
flex: 1,
transform: [{translateY: 100}],
}}>
<Button
icon="bookmark-check"
mode="contained"
onPress={this.showDialog}
style={{
width: '80%',
flex: 1,
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20,
borderRadius: 10,
}}>
{i18n.t('screens.equipment.bookButton')}
</Button>
</Animatable.View>
</View>
);
}
return null;
}
} }
export default withTheme(EquipmentRentScreen); export default withTheme(EquipmentRentScreen);

View file

@ -1,19 +1,20 @@
// @flow // @flow
import type {Device} from "../screens/Amicale/Equipment/EquipmentListScreen"; import i18n from 'i18n-js';
import i18n from "i18n-js"; import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen';
import DateManager from "../managers/DateManager"; import DateManager from '../managers/DateManager';
import type {CustomTheme} from "../managers/ThemeManager"; import type {CustomTheme} from '../managers/ThemeManager';
import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen';
/** /**
* Gets the current day at midnight * Gets the current day at midnight
* *
* @returns {Date} * @returns {Date}
*/ */
export function getCurrentDay() { export function getCurrentDay(): Date {
let today = new Date(Date.now()); const today = new Date(Date.now());
today.setUTCHours(0, 0, 0, 0); today.setUTCHours(0, 0, 0, 0);
return today; return today;
} }
/** /**
@ -22,8 +23,8 @@ export function getCurrentDay() {
* @param date The date to recover the ISO format from * @param date The date to recover the ISO format from
* @returns {*} * @returns {*}
*/ */
export function getISODate(date: Date) { export function getISODate(date: Date): string {
return date.toISOString().split("T")[0]; return date.toISOString().split('T')[0];
} }
/** /**
@ -32,18 +33,16 @@ export function getISODate(date: Date) {
* @param item * @param item
* @returns {boolean} * @returns {boolean}
*/ */
export function isEquipmentAvailable(item: Device) { export function isEquipmentAvailable(item: DeviceType): boolean {
let isAvailable = true; let isAvailable = true;
const today = getCurrentDay(); const today = getCurrentDay();
const dates = item.booked_at; const dates = item.booked_at;
for (let i = 0; i < dates.length; i++) { dates.forEach((date: {begin: string, end: string}) => {
const start = new Date(dates[i].begin); const start = new Date(date.begin);
const end = new Date(dates[i].end); const end = new Date(date.end);
isAvailable = today < start || today > end; if (!(today < start || today > end)) isAvailable = false;
if (!isAvailable) });
break; return isAvailable;
}
return isAvailable;
} }
/** /**
@ -52,17 +51,16 @@ export function isEquipmentAvailable(item: Device) {
* @param item * @param item
* @returns {Date} * @returns {Date}
*/ */
export function getFirstEquipmentAvailability(item: Device) { export function getFirstEquipmentAvailability(item: DeviceType): Date {
let firstAvailability = getCurrentDay(); let firstAvailability = getCurrentDay();
const dates = item.booked_at; const dates = item.booked_at;
for (let i = 0; i < dates.length; i++) { dates.forEach((date: {begin: string, end: string}) => {
const start = new Date(dates[i].begin); const start = new Date(date.begin);
let end = new Date(dates[i].end); const end = new Date(date.end);
end.setDate(end.getDate() + 1); end.setDate(end.getDate() + 1);
if (firstAvailability >= start) if (firstAvailability >= start) firstAvailability = end;
firstAvailability = end; });
} return firstAvailability;
return firstAvailability;
} }
/** /**
@ -70,31 +68,31 @@ export function getFirstEquipmentAvailability(item: Device) {
* *
* @param date The date to translate * @param date The date to translate
*/ */
export function getRelativeDateString(date: Date) { export function getRelativeDateString(date: Date): string {
const today = getCurrentDay(); const today = getCurrentDay();
const yearDelta = date.getUTCFullYear() - today.getUTCFullYear(); const yearDelta = date.getUTCFullYear() - today.getUTCFullYear();
const monthDelta = date.getUTCMonth() - today.getUTCMonth(); const monthDelta = date.getUTCMonth() - today.getUTCMonth();
const dayDelta = date.getUTCDate() - today.getUTCDate(); const dayDelta = date.getUTCDate() - today.getUTCDate();
let translatedString = i18n.t('screens.equipment.today'); let translatedString = i18n.t('screens.equipment.today');
if (yearDelta > 0) if (yearDelta > 0)
translatedString = i18n.t('screens.equipment.otherYear', { translatedString = i18n.t('screens.equipment.otherYear', {
date: date.getDate(), date: date.getDate(),
month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()],
year: date.getFullYear() year: date.getFullYear(),
}); });
else if (monthDelta > 0) else if (monthDelta > 0)
translatedString = i18n.t('screens.equipment.otherMonth', { translatedString = i18n.t('screens.equipment.otherMonth', {
date: date.getDate(), date: date.getDate(),
month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()],
}); });
else if (dayDelta > 1) else if (dayDelta > 1)
translatedString = i18n.t('screens.equipment.thisMonth', { translatedString = i18n.t('screens.equipment.thisMonth', {
date: date.getDate(), date: date.getDate(),
}); });
else if (dayDelta === 1) else if (dayDelta === 1)
translatedString = i18n.t('screens.equipment.tomorrow'); translatedString = i18n.t('screens.equipment.tomorrow');
return translatedString; return translatedString;
} }
/** /**
@ -111,41 +109,45 @@ export function getRelativeDateString(date: Date) {
* @param item Item containing booked dates to look for * @param item Item containing booked dates to look for
* @returns {[string]} * @returns {[string]}
*/ */
export function getValidRange(start: Date, end: Date, item: Device | null) { export function getValidRange(
let direction = start <= end ? 1 : -1; start: Date,
let limit = new Date(end); end: Date,
limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end item: DeviceType | null,
if (item != null) { ): Array<string> {
if (direction === 1) { const direction = start <= end ? 1 : -1;
for (let i = 0; i < item.booked_at.length; i++) { let limit = new Date(end);
const bookLimit = new Date(item.booked_at[i].begin); limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end
if (start < bookLimit && limit > bookLimit) { if (item != null) {
limit = bookLimit; if (direction === 1) {
break; for (let i = 0; i < item.booked_at.length; i += 1) {
} const bookLimit = new Date(item.booked_at[i].begin);
} if (start < bookLimit && limit > bookLimit) {
} else { limit = bookLimit;
for (let i = item.booked_at.length - 1; i >= 0; i--) { break;
const bookLimit = new Date(item.booked_at[i].end);
if (start > bookLimit && limit < bookLimit) {
limit = bookLimit;
break;
}
}
} }
}
} else {
for (let i = item.booked_at.length - 1; i >= 0; i -= 1) {
const bookLimit = new Date(item.booked_at[i].end);
if (start > bookLimit && limit < bookLimit) {
limit = bookLimit;
break;
}
}
} }
}
const validRange = [];
let validRange = []; const date = new Date(start);
let date = new Date(start); while (
while ((direction === 1 && date < limit) || (direction === -1 && date > limit)) { (direction === 1 && date < limit) ||
if (direction === 1) (direction === -1 && date > limit)
validRange.push(getISODate(date)); ) {
else if (direction === 1) validRange.push(getISODate(date));
validRange.unshift(getISODate(date)); else validRange.unshift(getISODate(date));
date.setDate(date.getDate() + direction); date.setDate(date.getDate() + direction);
} }
return validRange; return validRange;
} }
/** /**
@ -157,20 +159,24 @@ export function getValidRange(start: Date, end: Date, item: Device | null) {
* @param range The range to mark dates for * @param range The range to mark dates for
* @returns {{}} * @returns {{}}
*/ */
export function generateMarkedDates(isSelection: boolean, theme: CustomTheme, range: Array<string>) { export function generateMarkedDates(
let markedDates = {} isSelection: boolean,
for (let i = 0; i < range.length; i++) { theme: CustomTheme,
const isStart = i === 0; range: Array<string>,
const isEnd = i === range.length - 1; ): MarkedDatesObjectType {
markedDates[range[i]] = { const markedDates = {};
startingDay: isStart, for (let i = 0; i < range.length; i += 1) {
endingDay: isEnd, const isStart = i === 0;
color: isSelection const isEnd = i === range.length - 1;
? isStart || isEnd let color;
? theme.colors.primary if (isSelection && (isStart || isEnd)) color = theme.colors.primary;
: theme.colors.danger else if (isSelection) color = theme.colors.danger;
: theme.colors.textDisabled else color = theme.colors.textDisabled;
}; markedDates[range[i]] = {
} startingDay: isStart,
return markedDates; endingDay: isEnd,
color,
};
}
return markedDates;
} }