Skip to content

Instantly share code, notes, and snippets.

@jennmueng
Created June 11, 2020 07:15
Show Gist options
  • Save jennmueng/92059201cd427140b8f4e649435143c2 to your computer and use it in GitHub Desktop.
Save jennmueng/92059201cd427140b8f4e649435143c2 to your computer and use it in GitHub Desktop.
// @flow
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaskedView from '@react-native-community/masked-view';
import Svg, { Path } from 'react-native-svg';
import moment from 'moment-timezone';
import styled from 'styled-components';
import { BlurView } from '@react-native-community/blur';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { withNavigation, type NavigationScreenProp } from 'react-navigation';
import type { PlannerEntity, Hotel } from 'tour-flowtypes';
import CalloutNeighborCard from './CalloutNeighborCard';
import DummyAction from '../planner/dummyEntities/DummyAction';
import DummyActivity from '../planner/dummyEntities/DummyActivity';
import DummyFreeTime from '../planner/dummyEntities/DummyFreeTime';
import DurationSection from '../planner/durationSection';
import FlightCard from '../planner/entities/FlightCard';
import ScaleFeedback from '../../../../components/feedback/scaleFeedback';
import TimeSeparator from '../planner/timeSeparator';
import openCalloutAction from '../../actions/map/set.openCallout';
import closeCalloutAction from '../../actions/map/set.closeCallout';
import selectEntityNeighbors from '../../selectors/selectEntityNeighbors';
import getDurationString from '../../../../utilities/strings/getDurationString';
import getEntityTime from '../../functions/helpers/getEntityTime';
import getNameFromEntity from '../../functions/helpers/getNameFromEntity';
import getTextColorFromEntity from '../../functions/helpers/getTextColorFromEntity';
import DeviceProps from '../../../../config/DeviceProps';
import ENTITY_TYPES from '../../constants/ENTITY_TYPES';
import MEASUREMENTS from '../../constants/measurements';
import TRAVEL_TYPES from '../../constants/TRAVEL_TYPES';
import TourStyles from '../../../../config/TourStyles';
const TRIANGLE_HEIGHT = 18;
class MapCalloutDisplay extends Component<
{
calloutActivated: boolean,
entity: ?PlannerEntity,
openCallout: Function,
closeCallout: Function,
/* eslint-disable react/no-unused-prop-types */
// These props are copied over to state in componentWillReceiveProps and not used directly.
hotel: ?Hotel,
previousPreviousEntity: ?PlannerEntity,
previousEntity: ?PlannerEntity,
nextEntity: ?PlannerEntity,
nextNextEntity: ?PlannerEntity,
dayDate: string,
zoomLevel: number,
/* eslint-enable react/no-unused-prop-types */
navigation: NavigationScreenProp
},
{
showCallout: boolean,
entity: ?PlannerEntity,
hotel: ?Hotel,
dayDate: string,
previousPreviousEntity: ?PlannerEntity,
previousEntity: ?PlannerEntity,
nextEntity: ?PlannerEntity,
nextNextEntity: ?PlannerEntity,
zoomLevel: number,
height: number
}
> {
state = {
showCallout: false,
entity: null,
hotel: null,
dayDate: '',
previousPreviousEntity: null,
previousEntity: null,
nextEntity: null,
nextNextEntity: null,
zoomLevel: 9,
height: 0
};
componentWillReceiveProps(newProps) {
if (!this.props.calloutActivated && newProps.calloutActivated) {
// Store the props used in rendering the callout so it still displays while animating out.
this.setState({
showCallout: true,
entity: newProps.entity,
dayDate: newProps.dayDate,
hotel: newProps.hotel,
previousPreviousEntity: newProps.previousPreviousEntity,
previousEntity: newProps.previousEntity,
nextEntity: newProps.nextEntity,
nextNextEntity: newProps.nextNextEntity,
zoomLevel: newProps.zoomLevel
});
}
if (
this.props.calloutActivated &&
newProps.calloutActivated &&
this.props.entity &&
newProps.entity &&
this.props.entity.id !== newProps.entity.id
) {
this.animateOut();
setTimeout(() => {
if (this) {
this.setState(
{
entity: newProps.entity,
dayDate: newProps.dayDate,
hotel: newProps.hotel,
previousPreviousEntity: newProps.previousPreviousEntity,
previousEntity: newProps.previousEntity,
nextEntity: newProps.nextEntity,
nextNextEntity: newProps.nextNextEntity,
zoomLevel: newProps.zoomLevel
},
() => {
this.animateIn();
}
);
}
}, 180);
}
}
componentDidUpdate(prevProps) {
if (!prevProps.calloutActivated && this.props.calloutActivated) {
this.animateIn();
} else if (prevProps.calloutActivated && !this.props.calloutActivated) {
this.closeCallout();
}
}
openEntity = () => {
const { entity } = this.state;
if (entity && entity.entityType === ENTITY_TYPES.ACTIVITY) {
this.props.closeCallout();
// this.props.navigation.navigate(() => {
// })
}
};
openHotel = () => {
const { hotel } = this.state;
if (hotel) {
this.props.closeCallout();
// this.props.navigation.navigate(() => {
// })
}
};
closeCallout = () => {
this.animateOut();
setTimeout(() => {
if (this) {
this.setState({
showCallout: false,
entity: null,
hotel: null,
previousPreviousEntity: null,
previousEntity: null,
nextEntity: null,
nextNextEntity: null,
zoomLevel: 9,
dayDate: ''
});
}
}, 180);
};
setCardHeight = (height: number) => {
this.setState(prevState => {
if (height !== prevState.height) {
return { height };
}
return null;
});
};
animateIn = () => {
if (this.calloutContainer) {
const halfHeight = (this.state.height + TRIANGLE_HEIGHT) / 1.5;
this.calloutContainer.transition(
{
opacity: 0,
scale: 0,
translateY: halfHeight
},
{
opacity: 1,
scale: 1,
translateY: 0
},
180,
'ease-out-cubic'
);
}
};
animateOut = () => {
if (this.calloutContainer) {
const halfHeight = (this.state.height + TRIANGLE_HEIGHT) / 1.5;
this.calloutContainer.transition(
{
opacity: 1,
scale: 1,
translateY: 0
},
{
opacity: 0,
scale: 0,
translateY: halfHeight
},
180,
'ease-in-cubic'
);
}
};
onPressPreviousNeighbor = () => {
const { navigation, openCallout } = this.props;
const { previousPreviousEntity, previousEntity, zoomLevel } = this.state;
if (previousEntity && previousPreviousEntity && previousEntity.entityType === ENTITY_TYPES.TRAVEL) {
// open flight card if the previous entity is a flight.
if (previousEntity.entity.travelType === TRAVEL_TYPES.FLIGHT) {
return navigation.navigate({
routeName: 'EditFlight',
params: {
packId: previousEntity.entity.travelData.packId,
flightId: previousEntity.entity.travelData.flightId
}
});
}
openCallout({
entityId: previousPreviousEntity.id,
zoomLevel,
focusMap: true
});
} else if (previousEntity) {
openCallout({
entityId: previousEntity.id,
zoomLevel,
focusMap: true
});
}
};
onPressNextNeighbor = () => {
const { navigation, openCallout } = this.props;
const { nextEntity, nextNextEntity, zoomLevel } = this.state;
if (nextEntity && nextNextEntity && nextEntity.entityType === ENTITY_TYPES.TRAVEL) {
if (nextEntity.entity.travelType === TRAVEL_TYPES.FLIGHT) {
// open flight card if the previous entity is a flight.
return navigation.navigate({
routeName: 'EditFlight',
params: {
packId: nextEntity.entity.travelData.packId,
flightId: nextEntity.entity.travelData.flightId
}
});
}
openCallout({
entityId: nextNextEntity.id,
zoomLevel,
focusMap: true
});
} else if (nextEntity) {
openCallout({
entityId: nextEntity.id,
zoomLevel,
focusMap: true
});
}
};
calloutContainer: Animatable.AnimatableComponent;
renderPreviousNeighbor = () => {
const { previousPreviousEntity, previousEntity, entity, dayDate } = this.state;
if (previousEntity && previousPreviousEntity && previousEntity.entityType === ENTITY_TYPES.TRAVEL) {
if (previousEntity.entity.travelType === TRAVEL_TYPES.FLIGHT) {
const { nextEntity, nextNextEntity } = this.state;
if (
!(
nextEntity &&
nextNextEntity &&
nextEntity.entityType === ENTITY_TYPES.TRAVEL &&
nextEntity.entity.travelType === TRAVEL_TYPES.FLIGHT
)
) {
// We only render a flight card if the next entity is not a flight.
// In the case that both are flights they would only have the small entity card.
return (
<UpperNeighborCardContainer>
<FlightCardContainer>
<FlightCard
packId={previousEntity.entity.travelData.packId}
flightId={previousEntity.entity.travelData.flightId}
dayDate={dayDate}
/>
</FlightCardContainer>
</UpperNeighborCardContainer>
);
}
}
return (
<UpperNeighborCardContainer>
<CalloutNeighborCard
travelType={previousEntity.entity.travelType}
onPress={this.onPressPreviousNeighbor}
text={
<>
<TimeBold>
{getDurationString(
// $FlowFixMe
getEntityTime(previousEntity, 'from'),
// $FlowFixMe
getEntityTime(previousEntity, 'to')
)}
</TimeBold>
{' from '}
<EntityTextBold color={getTextColorFromEntity(previousPreviousEntity)}>
{getNameFromEntity(previousPreviousEntity)}
</EntityTextBold>
</>
}
/>
</UpperNeighborCardContainer>
);
}
if (previousEntity && entity) {
const previousEntityTo = getEntityTime(previousEntity, 'to');
const thisFrom = getEntityTime(entity, 'from');
/* We only render neighboring entities if they're on the same day, or within 3 hours of eachother. */
if (
previousEntityTo.format('YYYY-MM-DD') === dayDate ||
moment.duration(thisFrom.diff(previousEntityTo)).asHours() <= 3
)
return (
<UpperNeighborCardContainer>
<CalloutNeighborCard
onPress={this.onPressPreviousNeighbor}
text={
<>
{'Coming from '}
<EntityTextBold color={getTextColorFromEntity(previousEntity)}>
{getNameFromEntity(previousEntity)}
</EntityTextBold>
</>
}
/>
</UpperNeighborCardContainer>
);
}
return null;
};
renderNextNeighbor = () => {
const { nextEntity, nextNextEntity, entity, dayDate } = this.state;
if (nextEntity && nextNextEntity && nextEntity.entityType === ENTITY_TYPES.TRAVEL) {
if (nextEntity.entity.travelType === TRAVEL_TYPES.FLIGHT) {
const { previousEntity, previousPreviousEntity } = this.state;
if (
!(
previousEntity &&
previousPreviousEntity &&
previousEntity.entityType === ENTITY_TYPES.TRAVEL &&
previousEntity.entity.travelType === TRAVEL_TYPES.FLIGHT
)
) {
// We only render a flight card if the previous entity is not a flight.
// In the case that both are flights they would only have the small entity card.
return (
<LowerNeighborCardContainer>
<FlightCardContainer>
<FlightCard
packId={nextEntity.entity.travelData.packId}
flightId={nextEntity.entity.travelData.flightId}
dayDate={dayDate}
/>
</FlightCardContainer>
</LowerNeighborCardContainer>
);
}
}
return (
<LowerNeighborCardContainer>
<CalloutNeighborCard
travelType={nextEntity.entity.travelType}
onPress={this.onPressNextNeighbor}
text={
<>
<TimeBold>
{getDurationString(
// $FlowFixMe
getEntityTime(nextEntity, 'from'),
// $FlowFixMe
getEntityTime(nextEntity, 'to')
)}
</TimeBold>
{' to '}
<EntityTextBold color={getTextColorFromEntity(nextNextEntity)}>
{getNameFromEntity(nextNextEntity)}
</EntityTextBold>
</>
}
/>
</LowerNeighborCardContainer>
);
}
/* We only render neighboring entities if they're on the same day, or within 3 hours of eachother. */
if (nextEntity && entity) {
const nextEntityFrom = getEntityTime(nextEntity, 'from');
const thisTo = getEntityTime(entity, 'to');
/* We only render neighboring entities if they're on the same day, or within 3 hours of eachother. */
if (
nextEntityFrom.format('YYYY-MM-DD') === dayDate ||
moment.duration(nextEntityFrom.diff(thisTo)).asHours() <= 3
) {
return (
<LowerNeighborCardContainer>
<CalloutNeighborCard
onPress={this.onPressNextNeighbor}
text={
<>
{'Next is '}
<EntityTextBold color={getTextColorFromEntity(nextEntity)}>
{getNameFromEntity(nextEntity)}
</EntityTextBold>
</>
}
/>
</LowerNeighborCardContainer>
);
}
}
return null;
};
render() {
const { showCallout, entity, hotel, height, dayDate, zoomLevel } = this.state;
const canOpenEntity = !!(entity && entity.entityType !== ENTITY_TYPES.FREE_TIME);
return showCallout ? (
<Container pointerEvents="box-none">
<Positioner
style={{
transform: [
{
translateY: MEASUREMENTS.CALLOUT_CENTER_OFFSET
}
]
}}
>
<CalloutContainer ref={c => (this.calloutContainer = c)} useNativeDriver>
<MaskedView
style={{
width: '100%',
height: height + TRIANGLE_HEIGHT
}}
maskElement={
<DisplayContainer>
<MaskCard height={height} />
{zoomLevel > 2.5 && (
<Svg width={40} height={TRIANGLE_HEIGHT}>
<Path
d="M22.7799 16.8615C21.2295 18.3795 18.7705 18.3795 17.2201 16.8615L0 0L40 4.28009e-06L22.7799 16.8615Z"
fill="#000"
/>
</Svg>
)}
</DisplayContainer>
}
>
<BackgroundBlurView blurAmount={9} blurType="light" />
<BackgroundFill />
<ContentContainer
onLayout={({
nativeEvent: {
layout: { height: layoutHeight }
}
}) => {
this.setCardHeight(layoutHeight);
}}
>
{this.renderPreviousNeighbor()}
{entity && (
<EntityContainer>
<TimeSeparator
time={getEntityTime(entity, 'from')}
dayDate={dayDate}
adjustable={false}
separatorHasPopup={false}
reduceLineWidth
loc="from"
/>
<RowContainer>
<DurationSection
from={getEntityTime(entity, 'from')}
to={getEntityTime(entity, 'to')}
reduceWidth
isFixed={false}
/>
{entity.entityType === ENTITY_TYPES.ACTIVITY && (
<ScaleFeedback
style={styles.scaleFeedback}
onPress={this.openEntity}
enabled={canOpenEntity}
useOpacity
>
<DummyActivity activity={entity.entity} hasShadow={false} />
</ScaleFeedback>
)}
{entity.entityType === ENTITY_TYPES.FREE_TIME && (
<DummyFreeTime text="Oh! Free time on the map? Niiiice." transparent />
)}
{entity.entityType === ENTITY_TYPES.ACTION && hotel && (
<ScaleFeedback
style={styles.scaleFeedback}
onPress={this.openHotel}
enabled={canOpenEntity}
useOpacity
>
<DummyAction
hotel={hotel}
actionType={entity.entity.actionType}
hasShadow={false}
/>
</ScaleFeedback>
)}
</RowContainer>
<TimeSeparator
time={getEntityTime(entity, 'to')}
dayDate={dayDate}
adjustable={false}
separatorHasPopup={false}
reduceLineWidth
loc="to"
/>
</EntityContainer>
)}
{!entity && hotel && (
<RowContainer>
<ScaleFeedback style={styles.scaleFeedback} onPress={this.openHotel} useOpacity>
<DummyAction hotel={hotel} actionType="hotel-stay" hasShadow={false} />
</ScaleFeedback>
</RowContainer>
)}
{this.renderNextNeighbor()}
</ContentContainer>
</MaskedView>
</CalloutContainer>
</Positioner>
</Container>
) : null;
}
}
const selectCalloutEntityNeighbors = createSelector(
[state => state, state => state.mapCallouts.entity],
(state, entity) => {
if (entity) {
return selectEntityNeighbors(state, {
id: entity.id,
_getPreviousPreviousEntity: true,
_getNextNextEntity: true
});
}
return {
previousPreviousEntity: null,
previousEntity: null,
nextEntity: null,
nextNextEntity: null
};
}
);
const selectDayDate = createSelector(
[state => state.trip.dayPlans, state => state.trip.selectedDayIndex],
(dayPlans, selectedDayIndex) => {
if (!dayPlans) {
return '';
}
const dayPlan = dayPlans[selectedDayIndex];
if (!dayPlan) {
return '';
}
return dayPlan.day.dayDate;
}
);
const selectHotel = createSelector(
[state => state.mapCallouts.hotelId, state => state.trip.hotels],
(hotelId: string, hotels: { [hotelId: string]: Hotel }) => {
if (hotelId) {
return hotels[hotelId];
}
return null;
}
);
const mapStateToProps = state => ({
...selectCalloutEntityNeighbors(state),
calloutActivated: state.mapCallouts.calloutActivated,
entity: state.mapCallouts.entity,
hotel: selectHotel(state),
dayDate: selectDayDate(state),
zoomLevel: state.mapCallouts.zoomLevel
});
const actionCreators = {
openCallout: openCalloutAction,
closeCallout: closeCalloutAction
};
export default connect(
mapStateToProps,
actionCreators
)(withNavigation(MapCalloutDisplay));
const Container = styled.View`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
padding-bottom: ${MEASUREMENTS.CONTENT_LOWER_ANCHOR_HEIGHT};
`;
const Positioner = styled.View`
width: 100%;
align-items: center;
justify-content: flex-end;
`;
const CalloutContainer = Animatable.createAnimatableComponent(styled.View`
width: ${DeviceProps.width - 40};
height: auto;
position: absolute;
shadow-radius: 14;
shadow-color: #000;
shadow-offset: 0px 14px;
shadow-opacity: 0.07;
`);
const DisplayContainer = styled.View`
width: 100%;
height: auto;
align-items: center;
background-color: transparent;
`;
const MaskCard = styled.View`
width: 100%;
height: ${({ height }) => height};
border-radius: ${TourStyles.values.screenViewBorderRadius};
background-color: #000;
`;
const BackgroundBlurView = styled(BlurView)`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
const BackgroundFill = styled.View`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: ${TourStyles.colors.background1};
opacity: 0.55;
`;
const ContentContainer = styled.View`
width: 100%;
height: auto;
flex-direction: column;
padding: ${TourStyles.values.cardInnerPadding}px;
`;
const EntityContainer = styled.View`
width: 100%;
height: auto;
`;
const RowContainer = styled.View`
width: 100%;
height: ${MEASUREMENTS.ACTIVITY_CARD_HEIGHT_PER_HOUR};
flex-direction: row;
`;
const TimeBold = styled.Text`
color: ${TourStyles.colors.text1};
font-weight: 700;
`;
const EntityTextBold = styled.Text`
color: ${({ color }) => color};
font-weight: 700;
`;
const UpperNeighborCardContainer = styled.View`
width: 100%;
margin-bottom: 6px;
`;
const LowerNeighborCardContainer = styled.View`
width: 100%;
margin-top: 6px;
`;
const FlightCardContainer = styled.View`
height: auto;
width: 100%;
`;
const styles = StyleSheet.create({
scaleFeedback: {
height: 'auto',
flex: 1
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment