Skip to content

Commit

Permalink
Merge pull request #1090 from opentripplanner/mobility-profile
Browse files Browse the repository at this point in the history
Mobility Profile
  • Loading branch information
binh-dam-ibigroup authored Dec 11, 2023
2 parents 1258a03 + 0366dc9 commit 9a5b19a
Show file tree
Hide file tree
Showing 20 changed files with 924 additions and 437 deletions.
34 changes: 34 additions & 0 deletions i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,40 @@ components:
timeWalking: "{time} walking"
MobileOptions:
header: Set Search Options
MobilityProfile:
DevicesPane:
devices:
cane: Cane
crutches: Crutches
electric wheelchair: Electric wheelchair
manual walker: Manual walker
manual wheelchair: Manual/traditional wheelchair
mobility scooter: Mobility scooter
none: No assistive device
service animal: Service animal
stroller: Stroller
wheeled walker: Wheeled walker
white cane: White cane
prompt: Do you regularly use a mobility assistive device? (Check all that apply)
LimitationsPane:
mobilityPrompt: >-
Do you have any mobility limitations that cause you to walk more slowly
or more carefully than other people?
visionLimitations:
legally-blind: Legally blind
low-vision: Low-vision
none: None
visionPrompt: Do you have any vision limitations?
MobilityPane:
button: Edit your mobility profile
header: Mobility Profile
mobilityDevices: "Mobility devices: "
mobilityLimitations: "Mobility limitations: "
visionLimitations: "Vision limitations: "
intro: >-
Please answer a few questions to customize the trip planning experience to
your needs and preferences.
title: Define Your Mobility Profile
NarrativeItinerariesHeader:
changeSortDir: Change sort direction
howToFindResults: To view results, see the Itineraries Found heading below.
Expand Down
36 changes: 36 additions & 0 deletions i18n/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,42 @@ components:
timeWalking: "{time} de marche"
MobileOptions:
header: Options de recherche
MobilityProfile:
DevicesPane:
devices:
cane: Canne
crutches: Béquilles
electric wheelchair: Fauteuil roulant électrique
manual walker: Déambulateur manuel
manual wheelchair: Fauteuil roulant manuel
mobility scooter: Scooter électrique
none: Aucun
service animal: Animal de service
stroller: Poussette
wheeled walker: Déambulateur à roues
white cane: Canne blanche
prompt: >-
Utilisez-vous habituellement l'aide d'un appareil pour vous déplacer ?
(Cochez tous les cas qui vous concernent)
LimitationsPane:
mobilityPrompt: >-
Avez-vous des handicaps moteurs qui vous font marcher plus lentement ou
plus prudemment que d'autres personnes ?
visionLimitations:
legally-blind: Non-voyant
low-vision: Vision basse
none: Aucune
visionPrompt: Avez-vous des handicaps visuels ?
MobilityPane:
button: Modifier votre profil mobilité
header: Profil mobilité
mobilityDevices: "Appareils d'aide : "
mobilityLimitations: "Handicaps moteurs : "
visionLimitations: "Handicaps visuels : "
intro: >-
Veuillez répondre a quelques questions pour personaliser vos recherches de
trajets selon vos besoins et préférences.
title: Spécifiez votre profil de mobilité
NarrativeItinerariesHeader:
changeSortDir: Changer l'ordre de tri
howToFindResults: Pour afficher les résultats, utilisez l'en-tête Trajets trouvés plus bas.
Expand Down
18 changes: 18 additions & 0 deletions i18n/i18n-exceptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@
"components.OTP2ErrorRenderer.inputFields.*": [
"FROM",
"TO"
],
"components.MobilityProfile.DevicesPane.devices.*": [
"cane",
"crutches",
"electric wheelchair",
"manual walker",
"manual wheelchair",
"mobility scooter",
"none",
"service animal",
"stroller",
"wheeled walker",
"white cane"
],
"components.MobilityProfile.LimitationsPane.visionLimitations.*": [
"none",
"low-vision",
"legally-blind"
]
}
}
140 changes: 140 additions & 0 deletions lib/components/user/common/dropdown-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Field } from 'formik'
import { FormControl } from 'react-bootstrap'
import { IntlShape, useIntl } from 'react-intl'
import React, { ChangeEventHandler, ComponentType, ReactNode } from 'react'

interface SelectProps {
Control?: ComponentType
children: ReactNode
defaultValue?: string | number | boolean
label?: ReactNode
name: string
onChange?: ChangeEventHandler
}

/**
* A label followed by a dropdown control.
*/
export const Select = ({
Control = FormControl,
children,
defaultValue,
label,
name,
onChange
}: SelectProps): JSX.Element => (
// <Field> is kept outside of <label> to accommodate layout in table/grid cells.
<>
{label && <label htmlFor={name}>{label}</label>}
<Field
as={Control}
componentClass="select"
defaultValue={defaultValue}
id={name}
name={name}
onChange={onChange}
>
{children}
</Field>
</>
)

interface OptionsPropsBase<T> {
defaultValue?: T
hideDefaultIndication?: boolean
}

interface OptionsProps<T extends string | number> extends OptionsPropsBase<T> {
options: { text: string; value: T }[]
}

export function Options<T extends string | number>({
defaultValue,
hideDefaultIndication,
options
}: OptionsProps<T>): JSX.Element {
// <FormattedMessage> can't be used inside <option>.
const intl = useIntl()
return (
<>
{options.map(({ text, value }, i) => (
<option key={i} value={value}>
{!hideDefaultIndication && value === defaultValue
? intl.formatMessage(
{ id: 'common.forms.defaultValue' },
{ value: text }
)
: text}
</option>
))}
</>
)
}

const basicYesNoOptions = [
{
id: 'yes',
value: 'true'
},
{
id: 'no',
value: 'false'
}
]

/**
* Produces a yes/no list of options with the specified
* default value (true for yes, false for no).
*/
export function YesNoOptions({
defaultValue,
hideDefaultIndication
}: OptionsPropsBase<boolean>): JSX.Element {
// <FormattedMessage> can't be used inside <option>.
const intl = useIntl()
const options = basicYesNoOptions.map(({ id, value }) => ({
text: intl.formatMessage({ id: `common.forms.${id}` }),
value
}))
return (
<Options
defaultValue={(defaultValue || false).toString()}
hideDefaultIndication={hideDefaultIndication}
options={options}
/>
)
}

interface DurationOptionsProps extends OptionsPropsBase<number> {
decoratorFunc?: (text: string, intl: IntlShape) => string
minuteOptions: number[]
}

/**
* Produces a list of duration options with the specified default value.
*/
export function DurationOptions({
decoratorFunc,
defaultValue,
minuteOptions
}: DurationOptionsProps): JSX.Element {
// <FormattedMessage> can't be used inside <option>.
const intl = useIntl()
const localizedMinutes = minuteOptions.map((minutes) => ({
text:
minutes === 60
? intl.formatMessage({ id: 'components.TripNotificationsPane.oneHour' })
: intl.formatMessage(
{ id: 'common.time.tripDurationFormat' },
{ hours: 0, minutes, seconds: 0 }
),
value: minutes
}))
const options = decoratorFunc
? localizedMinutes.map(({ text, value }) => ({
text: decoratorFunc(text, intl),
value
}))
: localizedMinutes
return <Options defaultValue={defaultValue} options={options} />
}
25 changes: 21 additions & 4 deletions lib/components/user/existing-account-display.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { connect } from 'react-redux'
import { FormattedMessage, useIntl } from 'react-intl'
import { FormikProps } from 'formik'
import React from 'react'
import React, { FormEventHandler } from 'react'

import { AppReduxState } from '../../util/state-types'
import { TransitModeConfig } from '../../util/config-types'
import PageTitle from '../util/page-title'

import { EditedUser } from './types'
import { PhoneCodeRequestHandler } from './phone-number-editor'
import { PhoneVerificationSubmitHandler } from './phone-verification-form'
import A11yPrefs from './a11y-prefs'
import BackToTripPlanner from './back-to-trip-planner'
import DeleteUser from './delete-user'
import FavoritePlaceList from './places/favorite-place-list'
import MobilityPane from './mobility-profile/mobility-pane'
import NotificationPrefsPane from './notification-prefs-pane'
import StackedPanes from './stacked-panes'
import TermsOfUsePane from './terms-of-use-pane'

interface Props extends FormikProps<EditedUser> {
mobilityProfileEnabled: boolean
onDelete: FormEventHandler
onRequestPhoneVerificationCode: PhoneCodeRequestHandler
onSendPhoneVerificationCode: PhoneVerificationSubmitHandler
wheelchairEnabled: boolean
}

Expand All @@ -29,7 +36,7 @@ const ExistingAccountDisplay = (props: Props) => {
// We forward the props to each pane so that their individual controls
// can be wired to be managed by Formik.

const { wheelchairEnabled } = props
const { mobilityProfileEnabled, wheelchairEnabled } = props
const intl = useIntl()

const panes = [
Expand All @@ -38,6 +45,14 @@ const ExistingAccountDisplay = (props: Props) => {
props,
title: <FormattedMessage id="components.ExistingAccountDisplay.places" />
},
{
hidden: !mobilityProfileEnabled,
pane: MobilityPane,
props,
title: (
<FormattedMessage id="components.MobilityProfile.MobilityPane.header" />
)
},
{
pane: NotificationPrefsPane,
props,
Expand Down Expand Up @@ -85,11 +100,13 @@ const ExistingAccountDisplay = (props: Props) => {
}

const mapStateToProps = (state: AppReduxState) => {
const { accessModes } = state.otp.config.modes
const wheelchairEnabled = accessModes?.some(
const { mobilityProfile: mobilityProfileEnabled = false, modes } =
state.otp.config
const wheelchairEnabled = modes.accessModes?.some(
(mode: TransitModeConfig) => mode.showWheelchairSetting
)
return {
mobilityProfileEnabled,
wheelchairEnabled
}
}
Expand Down
Loading

0 comments on commit 9a5b19a

Please sign in to comment.