Skip to content

Commit a6b1710

Browse files
Merge pull request #174 from opentripplanner/add-save-trips
Add save trips
2 parents 5007601 + 982a877 commit a6b1710

21 files changed

+961
-51
lines changed

Diff for: lib/actions/user.js

+96-18
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { createAction } from 'redux-actions'
22

3-
import { addUser, fetchUser, updateUser } from '../util/middleware'
3+
import {
4+
addTrip,
5+
addUser,
6+
deleteTrip,
7+
fetchUser,
8+
getTrips,
9+
updateTrip,
10+
updateUser
11+
} from '../util/middleware'
412
import { isNewUser } from '../util/user'
513

614
const setCurrentUser = createAction('SET_CURRENT_USER')
15+
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
716
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')
817

9-
function getStateForNewUser (auth0User) {
18+
function createNewUser (auth0User) {
1019
return {
1120
auth0UserId: auth0User.sub,
1221
email: auth0User.email,
@@ -18,19 +27,36 @@ function getStateForNewUser (auth0User) {
1827
}
1928
}
2029

30+
/**
31+
* Fetches the saved/monitored trips for a user.
32+
* We use the accessToken to fetch the data regardless of
33+
* whether the process to populate state.user is completed or not.
34+
*/
35+
export function fetchUserMonitoredTrips (accessToken) {
36+
return async function (dispatch, getState) {
37+
const { otp } = getState()
38+
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
39+
40+
if (otpMiddleware) {
41+
const { data: trips, status: fetchStatus } = await getTrips(otpMiddleware, accessToken)
42+
if (fetchStatus === 'success') {
43+
dispatch(setCurrentUserMonitoredTrips(trips))
44+
}
45+
}
46+
}
47+
}
48+
2149
/**
2250
* Fetches user preferences to state.user, or set initial values under state.user if no user has been loaded.
2351
*/
2452
export function fetchOrInitializeUser (auth) {
2553
return async function (dispatch, getState) {
2654
const { otp } = getState()
27-
const { accessToken, user } = auth
55+
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
2856

29-
try {
30-
const result = await fetchUser(
31-
otp.config.persistence.otp_middleware,
32-
accessToken
33-
)
57+
if (otpMiddleware) {
58+
const { accessToken, user: authUser } = auth
59+
const { data: user, status: fetchUserStatus } = await fetchUser(otpMiddleware, accessToken)
3460

3561
// Beware! On AWS API gateway, if a user is not found in the middleware
3662
// (e.g. they just created their Auth0 password but have not completed the account setup form yet),
@@ -53,19 +79,15 @@ export function fetchOrInitializeUser (auth) {
5379
// }
5480
// TODO: Improve AWS response.
5581

56-
const resultData = result.data
57-
const isNewAccount = result.status === 'error' || (resultData && resultData.result === 'ERR')
58-
82+
const isNewAccount = fetchUserStatus === 'error' || (user && user.result === 'ERR')
5983
if (!isNewAccount) {
60-
// TODO: Move next line somewhere else.
61-
if (resultData.savedLocations === null) resultData.savedLocations = []
62-
dispatch(setCurrentUser({ accessToken, user: resultData }))
84+
// Load user's monitored trips before setting the user state.
85+
await dispatch(fetchUserMonitoredTrips(accessToken))
86+
87+
dispatch(setCurrentUser({ accessToken, user }))
6388
} else {
64-
dispatch(setCurrentUser({ accessToken, user: getStateForNewUser(user) }))
89+
dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) }))
6590
}
66-
} catch (error) {
67-
// TODO: improve error handling.
68-
alert(`An error was encountered:\n${error}`)
6991
}
7092
}
7193
}
@@ -91,15 +113,71 @@ export function createOrUpdateUser (userData) {
91113

92114
// TODO: improve the UI feedback messages for this.
93115
if (result.status === 'success' && result.data) {
116+
alert('Your preferences have been saved.')
117+
94118
// Update application state with the user entry as saved
95119
// (as returned) by the middleware.
96120
const userData = result.data
97121
dispatch(setCurrentUser({ accessToken, user: userData }))
122+
} else {
123+
alert(`An error was encountered:\n${JSON.stringify(result)}`)
124+
}
125+
}
126+
}
127+
}
98128

129+
/**
130+
* Updates a logged-in user's monitored trip,
131+
* then, if that was successful, refreshes the redux monitoredTrips
132+
* with the updated trip.
133+
*/
134+
export function createOrUpdateUserMonitoredTrip (tripData, isNew) {
135+
return async function (dispatch, getState) {
136+
const { otp, user } = getState()
137+
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
138+
139+
if (otpMiddleware) {
140+
const { accessToken } = user
141+
142+
let result
143+
if (isNew) {
144+
result = await addTrip(otpMiddleware, accessToken, tripData)
145+
} else {
146+
result = await updateTrip(otpMiddleware, accessToken, tripData)
147+
}
148+
149+
// TODO: improve the UI feedback messages for this.
150+
if (result.status === 'success' && result.data) {
99151
alert('Your preferences have been saved.')
152+
153+
// Reload user's monitored trips after add/update.
154+
await dispatch(fetchUserMonitoredTrips(accessToken))
100155
} else {
101156
alert(`An error was encountered:\n${JSON.stringify(result)}`)
102157
}
103158
}
104159
}
105160
}
161+
162+
/**
163+
* Deletes a logged-in user's monitored trip,
164+
* then, if that was successful, refreshes the redux monitoredTrips state.
165+
*/
166+
export function deleteUserMonitoredTrip (id) {
167+
return async function (dispatch, getState) {
168+
const { otp, user } = getState()
169+
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
170+
171+
if (otpMiddleware) {
172+
const { accessToken } = user
173+
const deleteResult = await deleteTrip(otpMiddleware, accessToken, id)
174+
175+
if (deleteResult.status === 'success') {
176+
// Reload user's monitored trips after deletion.
177+
await dispatch(fetchUserMonitoredTrips(accessToken))
178+
} else {
179+
alert(`An error was encountered:\n${JSON.stringify(deleteResult)}`)
180+
}
181+
}
182+
}
183+
}

Diff for: lib/components/app/responsive-webapp.js

+20
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { AUTH0_AUDIENCE, AUTH0_SCOPE, URL_ROOT } from '../../util/constants'
2020
import { getActiveItinerary, getTitle } from '../../util/state'
2121
import AfterSignInScreen from '../user/after-signin-screen'
2222
import BeforeSignInScreen from '../user/before-signin-screen'
23+
import SavedTripList from '../user/saved-trip-list'
24+
import SavedTripScreen from '../user/saved-trip-screen'
2325
import UserAccountScreen from '../user/user-account-screen'
2426
import withLoggedInUserSupport from '../user/with-logged-in-user-support'
2527

@@ -233,6 +235,24 @@ class RouterWrapperWithAuth0 extends Component {
233235
return <UserAccountScreen {...props} />
234236
}}
235237
/>
238+
<Route
239+
path={'/savetrip'}
240+
component={(routerProps) => {
241+
const props = this._combineProps(routerProps)
242+
return <SavedTripScreen isCreating {...props} />
243+
}}
244+
/>
245+
<Route
246+
path={'/savedtrips/:id'}
247+
component={(routerProps) => {
248+
const props = this._combineProps(routerProps)
249+
return <SavedTripScreen {...props} />
250+
}}
251+
/>
252+
<Route
253+
path={'/savedtrips'}
254+
component={SavedTripList}
255+
/>
236256
<Route
237257
// This route is called immediately after login by Auth0
238258
// and by the onRedirectCallback function from /lib/util/auth.js.

Diff for: lib/components/narrative/default/default-itinerary.js

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ItinerarySummary from './itinerary-summary'
66
import ItineraryDetails from './itinerary-details'
77
import TripDetails from '../connected-trip-details'
88
import TripTools from '../trip-tools'
9+
import LinkButton from '../../user/link-button'
910

1011
const { formatDuration, formatTime } = coreUtils.time
1112

@@ -31,6 +32,7 @@ export default class DefaultItinerary extends NarrativeItinerary {
3132
<span className='title'>Itinerary {index + 1}</span>{' '}
3233
<span className='duration pull-right'>{formatDuration(itinerary.duration)}</span>{' '}
3334
<span className='arrivalTime'>{formatTime(itinerary.startTime)}{formatTime(itinerary.endTime)}</span>
35+
<span className='pull-right'><LinkButton to='/savetrip'>Save</LinkButton></span>{' '}
3436
<ItinerarySummary itinerary={itinerary} LegIcon={LegIcon} />
3537
</button>
3638
{(active || expanded) &&

Diff for: lib/components/narrative/line-itin/line-itinerary.js

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ItineraryBody from './connected-itinerary-body'
66
import ItinerarySummary from './itin-summary'
77
import NarrativeItinerary from '../narrative-itinerary'
88
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'
9+
import LinkButton from '../../user/link-button'
910

1011
const { getLegModeLabel, getTimeZoneOffset, isTransit } = coreUtils.itinerary
1112

@@ -74,6 +75,9 @@ export default class LineItinerary extends NarrativeItinerary {
7475
onClick={onClick}
7576
timeOptions={timeOptions}
7677
/>
78+
79+
<span className='pull-right'><LinkButton to='/savetrip'>Save this option</LinkButton></span>
80+
7781
{showRealtimeAnnotation && <SimpleRealtimeAnnotation />}
7882
{active || expanded
7983
? <ItineraryBody

Diff for: lib/components/user/existing-account-display.js

+33-24
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
1-
import React from 'react'
1+
import React, { Component } from 'react'
22

3+
import LinkButton from './link-button'
34
import StackedPaneDisplay from './stacked-pane-display'
45

56
/**
67
* This component handles the existing account display.
78
*/
8-
const ExistingAccountDisplay = ({ onCancel, onComplete, panes }) => {
9-
const paneSequence = [
10-
{
11-
pane: panes.terms,
12-
props: { disableCheckTerms: true },
13-
title: 'Terms'
14-
},
15-
{
16-
pane: panes.notifications,
17-
title: 'Notifications'
18-
},
19-
{
20-
pane: panes.locations,
21-
title: 'My locations'
22-
}
23-
]
9+
class ExistingAccountDisplay extends Component {
10+
render () {
11+
const { onCancel, onComplete, panes } = this.props
12+
const paneSequence = [
13+
{
14+
pane: () => <p><LinkButton to='/savedtrips'>Edit my trips</LinkButton></p>,
15+
title: 'My trips'
16+
},
17+
{
18+
pane: panes.terms,
19+
props: { disableCheckTerms: true },
20+
title: 'Terms'
21+
},
22+
{
23+
pane: panes.notifications,
24+
title: 'Notifications'
25+
},
26+
{
27+
pane: panes.locations,
28+
title: 'My locations'
29+
}
30+
]
2431

25-
return (
26-
<StackedPaneDisplay
27-
onCancel={onCancel}
28-
onComplete={onComplete}
29-
paneSequence={paneSequence}
30-
/>
31-
)
32+
return (
33+
<StackedPaneDisplay
34+
onCancel={onCancel}
35+
onComplete={onComplete}
36+
paneSequence={paneSequence}
37+
title='My Account'
38+
/>
39+
)
40+
}
3241
}
3342

3443
export default ExistingAccountDisplay

Diff for: lib/components/user/favorite-locations-pane.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import clone from 'lodash/cloneDeep'
12
import memoize from 'lodash.memoize'
23
import PropTypes from 'prop-types'
34
import React, { Component } from 'react'
@@ -53,11 +54,11 @@ class FavoriteLocationsPane extends Component {
5354
const value = e.target.value || ''
5455
if (value.trim().length > 0) {
5556
const { userData, onUserDataChange } = this.props
56-
const { savedLocations } = userData
57-
58-
// Create a new array for savedLocations.
59-
const newLocations = [].concat(savedLocations)
57+
// FIXME: remove assigning [] when null.
58+
const { savedLocations = [] } = userData
6059

60+
// Create a copy of savedLocations and add the new location to the copied array.
61+
const newLocations = clone(savedLocations)
6162
newLocations.push({
6263
address: value.trim(),
6364
icon: 'map-marker',
@@ -75,7 +76,8 @@ class FavoriteLocationsPane extends Component {
7576
_handleAddressChange = memoize(
7677
location => e => {
7778
const { userData, onUserDataChange } = this.props
78-
const { savedLocations } = userData
79+
// FIXME: remove assigning [] when null.
80+
const { savedLocations = [] } = userData
7981
const value = e.target.value
8082
const isValueEmpty = !value || value === ''
8183
const nonEmptyLocation = isValueEmpty ? null : location
@@ -108,7 +110,8 @@ class FavoriteLocationsPane extends Component {
108110

109111
render () {
110112
const { userData } = this.props
111-
const { savedLocations } = userData
113+
// FIXME: remove assigning [] when null.
114+
const { savedLocations = [] } = userData
112115

113116
// Build an 'effective' list of locations for display,
114117
// where at least one 'home' and one 'work', are always present even if blank.

Diff for: lib/components/user/link-button.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import PropTypes from 'prop-types'
2+
import React, { Component } from 'react'
3+
import { Button } from 'react-bootstrap'
4+
import { connect } from 'react-redux'
5+
6+
import * as uiActions from '../../actions/ui'
7+
8+
/**
9+
* This button provides basic redirecting functionality.
10+
* FIXME: Replace this component with Link (react-router-dom) or LinkContainer (react-router-bootstrap).
11+
*/
12+
class LinkButton extends Component {
13+
static propTypes = {
14+
/** The destination url when clicking the button. */
15+
to: PropTypes.string.isRequired
16+
}
17+
18+
_handleClick = () => {
19+
this.props.routeTo(this.props.to)
20+
}
21+
22+
render () {
23+
return <Button onClick={this._handleClick}>{this.props.children}</Button>
24+
}
25+
}
26+
27+
// connect to the redux store
28+
29+
const mapStateToProps = (state, ownProps) => {
30+
return {}
31+
}
32+
33+
const mapDispatchToProps = {
34+
routeTo: uiActions.routeTo
35+
}
36+
37+
export default connect(mapStateToProps, mapDispatchToProps)(LinkButton)

Diff for: lib/components/user/new-account-wizard.js

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const NewAccountWizard = ({ onComplete, panes, userData }) => {
4646

4747
return (
4848
<SequentialPaneDisplay
49+
initialPaneId='terms'
4950
onComplete={onComplete}
5051
paneSequence={paneSequence}
5152
/>

0 commit comments

Comments
 (0)