Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions lib/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { secureFetch } from '../util/middleware'
import { isNewUser } from '../util/user'

// Middleware API paths.
const API_MONITORTRIP_PATH = '/api/secure/monitoredtrip'
const API_MONITORED_TRIP_PATH = '/api/secure/monitoredtrip'
const API_OTPUSER_PATH = '/api/secure/user'
const API_OTPUSER_VERIFYSMS_PATH = '/verify_sms'
const API_OTPUSER_VERIFY_SMS_SUBPATH = '/verify_sms'

const setCurrentUser = createAction('SET_CURRENT_USER')
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST')
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')
export const clearItineraryExistence = createAction('CLEAR_ITINERARY_EXISTENCE')
const setitineraryExistence = createAction('SET_ITINERARY_EXISTENCE')

function createNewUser (auth0User) {
return {
Expand Down Expand Up @@ -160,7 +162,7 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) {
export function fetchUserMonitoredTrips () {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`
const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}`

const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')
if (status === 'success') {
Expand All @@ -182,10 +184,10 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSucces

// Determine URL and method to use.
if (isNew) {
requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`
requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}`
method = 'POST'
} else {
requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`
requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}/${id}`
method = 'PUT'
}

Expand Down Expand Up @@ -217,7 +219,7 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSucces
export function deleteUserMonitoredTrip (tripId) {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${tripId}`
const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}/${tripId}`

const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'DELETE')
if (status === 'success') {
Expand All @@ -243,7 +245,7 @@ export function requestPhoneVerificationSms (newPhoneNumber) {
// TODO: Should throttling be handled in the middleware?
if (number !== newPhoneNumber || (now - timestamp) >= 60000) {
const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(state)
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${encodeURIComponent(newPhoneNumber)}`
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFY_SMS_SUBPATH}/${encodeURIComponent(newPhoneNumber)}`

const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')

Expand Down Expand Up @@ -273,7 +275,7 @@ export function requestPhoneVerificationSms (newPhoneNumber) {
export function verifyPhoneNumber (code) {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${code}`
const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFY_SMS_SUBPATH}/${code}`

const { data, status } = await secureFetch(requestUrl, accessToken, apiKey, 'POST')

Expand All @@ -293,3 +295,26 @@ export function verifyPhoneNumber (code) {
}
}
}

/**
* Check itinerary existence for the given monitored trip.
*/
export function checkItineraryExistence (trip) {
return async function (dispatch, getState) {
const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
const requestUrl = `${apiBaseUrl}${API_MONITORED_TRIP_PATH}/checkitinerary`

// Empty state before performing the checks.
dispatch(clearItineraryExistence())

const { data, status } = await secureFetch(requestUrl, accessToken, apiKey, 'POST', {
body: JSON.stringify(trip)
})

if (status === 'success' && data) {
dispatch(setitineraryExistence(data))
} else {
alert('Error checking whether your selected trip is possible.')
}
}
}
2 changes: 1 addition & 1 deletion lib/components/narrative/narrative-itineraries.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class NarrativeItineraries extends Component {
static propTypes = {
itineraries: PropTypes.array,
itineraryClass: PropTypes.func,
pending: PropTypes.number,
pending: PropTypes.bool,
activeItinerary: PropTypes.number,
setActiveItinerary: PropTypes.func,
setActiveLeg: PropTypes.func,
Expand Down
191 changes: 128 additions & 63 deletions lib/components/user/trip-basics-pane.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { Field } from 'formik'
import React from 'react'
import React, { Component } from 'react'
import {
ControlLabel,
FormControl,
FormGroup,
HelpBlock
Glyphicon,
HelpBlock,
ProgressBar
} from 'react-bootstrap'
import { connect } from 'react-redux'
import styled from 'styled-components'

import { ALL_DAYS } from '../../util/monitored-trip'
import * as userActions from '../../actions/user'
import TripSummary from './trip-summary'

// Styles.
const StyledLabel = styled.label`
const TripDayLabel = styled.label`
border: 1px solid #ccc;
border-left: none;
box-sizing: border-box;
display: inline-block;
font-weight: inherit;
float: left;
height: 3em;
max-width: 150px;
min-width: 14.28%;
text-align: center;
& > span {
& > span:first-of-type {
display: block;
width: 100%;
}
Expand All @@ -31,73 +36,133 @@ const StyledLabel = styled.label`
`

const allDays = [
{ name: 'monday', text: 'Mon.' },
{ name: 'tuesday', text: 'Tue.' },
{ name: 'wednesday', text: 'Wed.' },
{ name: 'thursday', text: 'Thu.' },
{ name: 'friday', text: 'Fri.' },
{ name: 'saturday', text: 'Sat.' },
{ name: 'sunday', text: 'Sun.' }
{ name: 'monday', text: 'Mon.', fullText: 'Mondays' },
{ name: 'tuesday', text: 'Tue.', fullText: 'Tuesdays' },
{ name: 'wednesday', text: 'Wed.', fullText: 'Wednesdays' },
{ name: 'thursday', text: 'Thu.', fullText: 'Thursdays' },
{ name: 'friday', text: 'Fri.', fullText: 'Fridays' },
{ name: 'saturday', text: 'Sat.', fullText: 'Saturdays' },
{ name: 'sunday', text: 'Sun.', fullText: 'Sundays' }
]

/**
* This component shows summary information for a trip
* and lets the user edit the trip name and day.
*/
const TripBasicsPane = ({ errors, touched, values: monitoredTrip }) => {
const { itinerary } = monitoredTrip

if (!itinerary) {
return <div>No itinerary to display.</div>
} else {
// Show an error indicaton when monitoredTrip.tripName is not blank (from the form's validation schema)
// and that tripName is not already used.
let tripNameValidationState = null
if (touched.tripName) {
tripNameValidationState = errors.tripName ? 'error' : null
class TripBasicsPane extends Component {
componentDidMount () {
// Check itinerary availability (existence) for all days.
const { checkItineraryExistence, values: monitoredTrip } = this.props
checkItineraryExistence(monitoredTrip)
}

componentDidUpdate (prevProps) {
const { isCreating, itineraryExistence, setFieldValue } = this.props

if (itineraryExistence !== prevProps.itineraryExistence) {
// For new trips only,
// update the Formik state to uncheck days for which the itinerary is not available.
if (isCreating && itineraryExistence) {
allDays.forEach(({ name }) => {
if (!itineraryExistence[name].valid) {
setFieldValue(name, false)
}
})
}
}
}

// Show a combined error indicaton when no day is selected.
let monitoredDaysValidationState = null
ALL_DAYS.forEach(day => {
if (touched[day]) {
if (!monitoredDaysValidationState) {
monitoredDaysValidationState = errors[day] ? 'error' : null
}
componentWillUnmount () {
this.props.clearItineraryExistence()
}

render () {
const { errors, isCreating, itineraryExistence, touched, values: monitoredTrip } = this.props
const { itinerary } = monitoredTrip

if (!itinerary) {
return <div>No itinerary to display.</div>
} else {
// Show an error indicaton when monitoredTrip.tripName is not blank (from the form's validation schema)
// and that tripName is not already used.
let tripNameValidationState = null
if (touched.tripName) {
tripNameValidationState = errors.tripName ? 'error' : null
}
})

return (
<div>
<ControlLabel>Selected itinerary:</ControlLabel>
<TripSummary monitoredTrip={monitoredTrip} />

<FormGroup validationState={tripNameValidationState}>
<ControlLabel>Please provide a name for this trip:</ControlLabel>
{/* onBlur, onChange, and value are passed automatically. */}
<Field as={FormControl} name='tripName' />
<FormControl.Feedback />
{errors.tripName && <HelpBlock>{errors.tripName}</HelpBlock>}
</FormGroup>

<FormGroup validationState={monitoredDaysValidationState}>
<ControlLabel>What days to you take this trip?</ControlLabel>
<div>
{allDays.map(({ name, text }, index) => (
<StyledLabel className={monitoredTrip[name] ? 'bg-primary' : ''} key={index}>
<span>{text}</span>
<Field
name={name}
type='checkbox'
/>
</StyledLabel>
))}
</div>
{monitoredDaysValidationState && <HelpBlock>Please select at least one day to monitor.</HelpBlock>}
</FormGroup>
</div>
)

// Show a combined error indicaton when no day is selected.
let monitoredDaysValidationState = null
allDays.forEach(({ name }) => {
if (touched[name]) {
if (!monitoredDaysValidationState) {
monitoredDaysValidationState = errors[name] ? 'error' : null
}
}
})

return (
<div>
<ControlLabel>Selected itinerary:</ControlLabel>
<TripSummary monitoredTrip={monitoredTrip} />

<FormGroup validationState={tripNameValidationState}>
<ControlLabel>Please provide a name for this trip:</ControlLabel>
{/* onBlur, onChange, and value are passed automatically. */}
<Field as={FormControl} name='tripName' />
<FormControl.Feedback />
{errors.tripName && <HelpBlock>{errors.tripName}</HelpBlock>}
</FormGroup>

<FormGroup validationState={monitoredDaysValidationState}>
<ControlLabel>What days do you take this trip?</ControlLabel>
<div>
{allDays.map(({ name, fullText, text }, index) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit redundant to have allDays and ALL_DAYS, which effectively contain the same info.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 846892a.

const isDayDisabled = itineraryExistence && !itineraryExistence[name].valid
const boxClass = isDayDisabled ? 'alert-danger' : (monitoredTrip[name] ? 'bg-primary' : '')
const notAvailableText = isDayDisabled ? `Trip not available on ${fullText}` : null

return (
<TripDayLabel className={boxClass} key={index} title={notAvailableText}>
<span>{text}</span>
{ // Let users save an existing trip, even though it may not be available on some days.
// TODO: improve checking trip availability.
isDayDisabled && isCreating
? <Glyphicon aria-label={notAvailableText} glyph='ban-circle' />
: <Field name={name} type='checkbox' />
}
</TripDayLabel>
)
})}
<div style={{clear: 'both'}} />
</div>
<HelpBlock>
{itineraryExistence
? (
<>Your trip is available on the days of the week as indicated above.</>
) : (
<ProgressBar active label='Checking itinerary existence for each day of the week...' now={100} />
)
}
</HelpBlock>
{monitoredDaysValidationState && <HelpBlock>Please select at least one day to monitor.</HelpBlock>}
</FormGroup>
</div>
)
}
}
}

export default TripBasicsPane
// Connect to redux store
const mapStateToProps = (state, ownProps) => {
const { itineraryExistence } = state.user
return {
itineraryExistence
}
}

const mapDispatchToProps = {
checkItineraryExistence: userActions.checkItineraryExistence,
clearItineraryExistence: userActions.clearItineraryExistence
}

export default connect(mapStateToProps, mapDispatchToProps)(TripBasicsPane)
13 changes: 13 additions & 0 deletions lib/reducers/create-user-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import update from 'immutability-helper'
function createUserReducer () {
const initialState = {
accessToken: null,
itineraryExistence: null,
lastPhoneSmsRequest: {
number: null,
status: null,
Expand Down Expand Up @@ -41,6 +42,18 @@ function createUserReducer () {
})
}

case 'SET_ITINERARY_EXISTENCE': {
return update(state, {
itineraryExistence: { $set: action.payload }
})
}

case 'CLEAR_ITINERARY_EXISTENCE': {
return update(state, {
itineraryExistence: { $set: null }
})
}

default:
return state
}
Expand Down