Skip to content

New saved place UI #1388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
1 change: 1 addition & 0 deletions i18n/en-US.yml
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@ actions:
itineraryExistenceCheckFailed: Error checking whether your selected trip is possible.
mustAcceptTermsToSavePlace: Please accept the Terms of Use (under My Account) to save locations.
mustBeLoggedInToSavePlace: Please log in to save locations.
placeDeleted: Your place has been deleted.
placeRemembered: The settings for this place have been saved.
preferencesSaved: Your preferences have been saved.
smsInvalidCode: The code you entered is invalid. Please try again.
2 changes: 1 addition & 1 deletion lib/actions/user.js
Original file line number Diff line number Diff line change
@@ -790,7 +790,7 @@ export function deleteLoggedInUserPlace(placeIndex, intl) {
const { loggedInUser } = getState().user
loggedInUser.savedLocations.splice(placeIndex, 1)

dispatch(createOrUpdateUser(loggedInUser, intl))
return dispatch(createOrUpdateUser(loggedInUser, intl))
}
}

8 changes: 6 additions & 2 deletions lib/components/map/connected-endpoints-overlay.tsx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import {
import { getActiveSearch, getShowUserSettings } from '../../util/state'
import { setLocation } from '../../actions/map'
import { setViewedStop } from '../../actions/ui'
import { toastOnPlaceSaved } from '../util/toasts'
import { toastOnPlaceChanged } from '../util/toasts'

type Props = ComponentProps<typeof EndpointsOverlay> & {
forgetPlace: (place: string, intl: IntlShape) => void
@@ -40,7 +40,11 @@ const ConnectedEndpointsOverlay = ({
async (placeTypeLocation) => {
const result = await rememberPlace(placeTypeLocation, intl)
if (result === UserActionResult.SUCCESS) {
toastOnPlaceSaved(convertToPlace(placeTypeLocation.location), intl)
toastOnPlaceChanged(
convertToPlace(placeTypeLocation.location),
intl,
'Remembered'
)
}
},
[rememberPlace, intl]
2 changes: 1 addition & 1 deletion lib/components/mobile/mobile.css
Original file line number Diff line number Diff line change
@@ -250,7 +250,7 @@
}

@media (max-width: 768px){
.panel-default {
.saved-trip-panel .panel-default {
width: 90%;
}
}
2 changes: 1 addition & 1 deletion lib/components/user/delete-form.tsx
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ interface DeleteFormProps {
tripId: string
}

const DeleteFormButton = styled(Button)`
export const DeleteFormButton = styled(Button)`
background-color: white;
border-color: ${RED_ON_WHITE};
color: ${RED_ON_WHITE};
2 changes: 1 addition & 1 deletion lib/components/user/monitored-trip/saved-trip-list.tsx
Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@ class TripListItem extends Component<ItemProps, ItemState> {
})
const { LegIcon } = this.context
return (
<Panel>
<Panel className="saved-trip-panel">
<TripPanelHeading>
<TripPanelTitle>
<Panel.Title>
16 changes: 6 additions & 10 deletions lib/components/user/places/favorite-place-list.js
Original file line number Diff line number Diff line change
@@ -12,9 +12,10 @@ import {
getPlaceMainText
} from '../../../util/user'
import { isBlank } from '../../../util/ui'
import { Plus } from '@styled-icons/fa-solid/Plus'
import { UnpaddedList } from '../../form/styled'

import { StyledFavoritePlace as FavoritePlace } from './styled'
import { StyledFavoritePlace as FavoritePlace, NewPlaceButton } from './styled'

/**
* Renders an editable list user's favorite locations, and lets the user add a new one.
@@ -54,15 +55,10 @@ const FavoritePlaceList = ({
))}
</UnpaddedList>

<FavoritePlace
icon="plus"
mainText={
<FormattedMessage id="components.FavoritePlaceList.addAnotherPlace" />
}
path={`${basePath}/new`}
tag="div"
title=""
/>
<NewPlaceButton className="btn btn-primary" to={`${basePath}/new`}>
<Plus size={10} />
<FormattedMessage id="components.FavoritePlaceList.addAnotherPlace" />
</NewPlaceButton>
</div>
)
}
28 changes: 26 additions & 2 deletions lib/components/user/places/favorite-place-screen.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { injectIntl, FormattedMessage } from 'react-intl'
import { connect } from 'react-redux'
import styled from 'styled-components'
import * as yup from 'yup'
import { Trash } from '@styled-icons/fa-solid/Trash'

import AccountPage from '../account-page'
import * as userActions from '../../../actions/user'
@@ -25,7 +26,9 @@ import { isHomeOrWork, PLACE_TYPES } from '../../../util/user'
import withLoggedInUserSupport from '../with-logged-in-user-support'
import { InlineLoading } from '../../narrative/loading'
import PageTitle from '../../util/page-title'
import { toastOnPlaceSaved } from '../../util/toasts'
import { toastOnPlaceChanged } from '../../util/toasts'
import { DeleteFormButton } from '../delete-form'
import { IconWithText } from '../../util/styledIcon'

import PlaceEditor from './place-editor'

@@ -73,13 +76,23 @@ class FavoritePlaceScreen extends Component {
const { intl, placeIndex, saveUserPlace } = this.props
const result = await saveUserPlace(placeToSave, placeIndex, intl)
if (result === userActions.UserActionResult.SUCCESS) {
toastOnPlaceSaved(placeToSave, intl)
toastOnPlaceChanged(placeToSave, intl, 'Remembered')
}

// Return to previous location when done.
navigateBack()
}

_handleDelete = async placeToSave => {
const { deleteLoggedInUserPlace, intl, placeIndex } = this.props
const result = await deleteLoggedInUserPlace(placeIndex, intl)
if (result === userActions.UserActionResult.SUCCESS) {
toastOnPlaceChanged(placeToSave, intl, 'Deleted')
}
// Return to previous location when done.
navigateBack()
}

/**
* Based on the URL, returns an existing place or a new place for editing,
* or null if the requested place is not found.
@@ -191,6 +204,16 @@ class FavoritePlaceScreen extends Component {
onClick: navigateBack,
text: <FormattedMessage id='common.forms.back' />
}}
extraButton={!isNewPlace && {content: (
<DeleteFormButton onClick={() => this._handleDelete(place)}>
{pendingSave ? (
<InlineLoading />
) : (
<IconWithText Icon={Trash}>
<FormattedMessage id="components.Place.deleteThisPlace" />
</IconWithText>)}
</DeleteFormButton>)
}}
okayButton={place && {
text: pendingSave ? <InlineLoading /> : <FormattedMessage id='common.forms.save' />,
type: 'submit'
@@ -220,6 +243,7 @@ const mapStateToProps = (state, ownProps) => {
}

const mapDispatchToProps = {
deleteLoggedInUserPlace: userActions.deleteLoggedInUserPlace,
saveUserPlace: userActions.saveUserPlace
}

80 changes: 30 additions & 50 deletions lib/components/user/places/place.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Button } from 'react-bootstrap'
import { Search } from '@styled-icons/fa-solid/Search'
import { TrashAlt } from '@styled-icons/fa-solid/TrashAlt'
import { Button, Panel } from 'react-bootstrap'
import { Edit } from '@styled-icons/fa-solid/Edit'
import { useIntl } from 'react-intl'
import React, { HTMLAttributes, ReactNode, useContext } from 'react'
import styled, { css } from 'styled-components'
@@ -34,34 +33,25 @@ interface Props extends HTMLAttributes<HTMLLIElement> {
/** The title for the main button */
title?: string
}
/*
interface ConfigContext extends Context {
SvgIcon: ComponentType<{ iconName?: string }>
}
*/
const Container = styled.li`
align-items: stretch;
display: flex;
list-style: none;
`

// Definitions below are for customizable subcomponents referenced in
// styled.js to define multiple flavors of the Place component,
// without creating circular references between that file and this file.

const placeButtonCss = css`
background: none;
flex: 1 0 0;
overflow: hidden;
const placeCss = css`
text-align: left;
text-overflow: ellipsis;
width: 100%;
`

export const PlaceButton = styled(Button)`
${placeButtonCss}
${placeCss}
`

export const PlaceLink = styled(Link)`
${placeButtonCss}
export const PlaceContainer = styled(Panel.Body)`
${placeCss}
`

export const PlaceDetail = styled.span``
@@ -72,7 +62,10 @@ export const PlaceName = styled.span``

export const PlaceText = styled.span``

export const IconWrapper = styled(StyledIconWrapper)``
export const IconWrapper = styled(StyledIconWrapper)`
justify-self: center;
grid-column: 1;
`

export const ActionButton = styled(Button)`
background: none;
@@ -81,6 +74,15 @@ export const ActionButton = styled(Button)`

export const ActionButtonPlaceholder = styled.span``

const SavedPlacePanel = styled(Panel)`
margin-bottom: 10px;

.panel-body:before,
.panel-body:after {
display: none !important;
}
`

/**
* Renders a stylable clickable button for editing/selecting a user's favorite place,
* and buttons for viewing and deleting the place if corresponding handlers are provided.
@@ -93,25 +95,16 @@ const Place = ({
largeIcon,
mainText,
onClick,
onDelete,
onView,
path,
tag = 'li',
title = `${mainText}${detailText && ` (${detailText})`}`
}: Props): JSX.Element => {
const intl = useIntl()
// @ts-expect-error TODO: Add types to ComponentContext
const { SvgIcon } = useContext(ComponentContext)
const viewStopLabel = intl.formatMessage({ id: 'components.Place.viewStop' })
const deletePlaceLabel = intl.formatMessage({
id: 'components.Place.deleteThisPlace'
})
const iconSize = largeIcon ? '2x' : undefined

const placeContent = (
<>
{largeIcon && (
<IconWrapper size="2x">
<IconWrapper size="1.5x">
<SvgIcon iconName={icon} />
</IconWrapper>
)}
@@ -141,30 +134,17 @@ const Place = ({
{onClick ? (
<PlaceButton onClick={onClick}>{placeContent}</PlaceButton>
) : (
<PlaceLink className="btn btn-default" to={path}>
{placeContent}
</PlaceLink>
<SavedPlacePanel style={{ marginBottom: '10px' }}>
<PlaceContainer>
{placeContent}
<Link aria-label={actionText} title={actionText} to={path}>
<Edit height={18} />
</Link>
</PlaceContainer>
</SavedPlacePanel>
)}

{/* Action buttons. If none, render a placeholder. */}
{!onView && !onDelete && <ActionButtonPlaceholder />}
{onView && (
// This button is only used for viewing stops.
<ActionButton onClick={onView} title={viewStopLabel}>
<IconWrapper size={iconSize}>
<Search />
</IconWrapper>
<InvisibleA11yLabel>{viewStopLabel}</InvisibleA11yLabel>
</ActionButton>
)}
{onDelete && (
<ActionButton onClick={onDelete} title={deletePlaceLabel}>
<IconWrapper size={iconSize}>
<TrashAlt />
</IconWrapper>
<InvisibleA11yLabel>{deletePlaceLabel}</InvisibleA11yLabel>
</ActionButton>
)}
</Container>
)
}
Loading

Unchanged files with check annotations Beta

import { Field, Form, Formik } from 'formik'
import React, {Component} from 'react'

Check failure on line 2 in lib/components/admin/editable-section.js

GitHub Actions / test-build-release

Replace `Component` with `·Component·`
import {
Button,
import { createAction } from 'redux-actions'
if (typeof (fetch) === 'undefined') require('isomorphic-fetch')

Check failure on line 2 in lib/actions/zipcar.js

GitHub Actions / test-build-release

Replace `(fetch)` with `fetch`
export const receivedZipcarLocationsError = createAction('ZIPCAR_LOCATIONS_ERROR')

Check failure on line 4 in lib/actions/zipcar.js

GitHub Actions / test-build-release

Replace `'ZIPCAR_LOCATIONS_ERROR'` with `⏎··'ZIPCAR_LOCATIONS_ERROR'⏎`
export const receivedZipcarLocationsResponse = createAction('ZIPCAR_LOCATIONS_RESPONSE')

Check failure on line 5 in lib/actions/zipcar.js

GitHub Actions / test-build-release

Replace `'ZIPCAR_LOCATIONS_RESPONSE'` with `⏎··'ZIPCAR_LOCATIONS_RESPONSE'⏎`
export const requestZipcarLocationsResponse = createAction('ZIPCAR_LOCATIONS_REQUEST')

Check failure on line 6 in lib/actions/zipcar.js

GitHub Actions / test-build-release

Replace `'ZIPCAR_LOCATIONS_REQUEST'` with `⏎··'ZIPCAR_LOCATIONS_REQUEST'⏎`
export function zipcarLocationsQuery (url) {

Check failure on line 8 in lib/actions/zipcar.js

GitHub Actions / test-build-release

Delete `·`
return async function (dispatch, getState) {
dispatch(requestZipcarLocationsResponse())
let json
import { replace, push } from 'connected-react-router'

Check failure on line 1 in lib/actions/auth.js

GitHub Actions / test-build-release

Member 'push' of the import declaration should be sorted alphabetically
import { setPathBeforeSignIn } from '../actions/user'
* @param {Error} err
* @param {AccessTokenRequestOptions} options
*/
export function showAccessTokenError (err, options) {

Check failure on line 11 in lib/actions/auth.js

GitHub Actions / test-build-release

Delete `·`
return function (dispatch, getState) {
// TODO: improve this.
console.error('Failed to retrieve access token: ', err)
* when signing-in fails for some reason.
* @param {Error} err
*/
export function showLoginError (err) {

Check failure on line 23 in lib/actions/auth.js

GitHub Actions / test-build-release

Delete `·`
return function (dispatch, getState) {
// TODO: improve this.
if (err) dispatch(push('/oops'))
* @param {Object} appState The state stored when calling useAuth0().loginWithRedirect
* or when instantiating a component that uses withAuhenticationRequired.
*/
export function processSignIn (appState) {

Check failure on line 36 in lib/actions/auth.js

GitHub Actions / test-build-release

Delete `·`
return function (dispatch, getState) {
if (appState && appState.returnTo) {
// Remove URL parameters that were added by auth0-react