diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 58c43556a..e3f9c8025 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -219,6 +219,20 @@ components: advancedOptions: Advanced options groupSize: "Group size:" intermediateDestination: Enter intermediate destination + CompanionsPane: + addNewCompanion: Add a new travel companion + companionAlreadyAdded: You already have a companion with email {email} + companionExplanation: > + Invite an exiting G-MAP user to be a travel companion by entering their + email. When they accept, their status will change to "verified", and you + can share your trip status and plan trips based on one another's mobility + profile. + confirmDeleteCompanion: Do you want to delete companion {email}? + currentCompanionsLabel: "Current travel companions:" + deleteCompanion: Delete {email} + noCompanions: You do not have any existing travel companions. + submitNewCompanion: Send invitation + title: Travel companions DateTimeOptions: arriveBy: Arrive by departAt: Depart at @@ -421,7 +435,6 @@ components: changeNumber: Change number invalidCode: Please enter 6 digits for the validation code. invalidPhone: Please enter a valid phone number. - pending: Pending phoneNumberSubmitted: Phone number {phoneNumber} was successfully submitted. phoneNumberVerified: Phone number {phoneNumber} was successfully verified. placeholder: Enter your phone number @@ -437,7 +450,6 @@ components: Please check the SMS messaging app on your mobile phone for a text message with a verification code, and enter the code below (code expires after 10 minutes). - verified: Verified verify: Verify verifySms: >- Please complete the verification process in order to set up SMS @@ -515,6 +527,7 @@ components: deleteSavedTrip: Delete saved trip editSavedTrip: Edit saved trip saveNewTrip: Save new trip + travelCompanions: Travel companions tripInformation: Trip information tripNotFound: Trip not found tripNotFoundDescription: Sorry, the requested trip was not found. @@ -548,6 +561,10 @@ components: usingRealtimeInfo: This trip uses real-time traffic and delay information StackedPaneDisplay: savePreferences: Save preferences + StatusBadge: + invalid: Invalid + pending: Pending + verified: Verified StopScheduleTable: block: Block departure: Departure diff --git a/i18n/es.yml b/i18n/es.yml index 2a25daa1a..a68f7a03c 100644 --- a/i18n/es.yml +++ b/i18n/es.yml @@ -429,7 +429,6 @@ components: changeNumber: Cambiar número de teléfono invalidCode: Introduzca 6 dígitos para el código de validación. invalidPhone: Por favor, introduzca un número de teléfono válido. - pending: Pendiente phoneNumberSubmitted: El número de teléfono {phoneNumber} se ha enviado correctamente. phoneNumberVerified: El número de teléfono {phoneNumber} se ha verificado correctamente. placeholder: Introduzca su número de teléfono @@ -448,7 +447,6 @@ components: teléfono móvil si hay un mensaje de texto con un código de verificación, e introduzca el código que aparece a continuación. El código caduca a los 10 minutos. - verified: Verificado verify: Verificar verifySms: >- Por favor, complete el proceso de verificación para configurar las @@ -561,6 +559,10 @@ components: usingRealtimeInfo: Este viaje utiliza información de tráfico y retrasos en tiempo real StackedPaneDisplay: savePreferences: Guardar preferencias + StatusBadge: + invalid: Inválido + pending: Pendiente + verified: Verificado StopScheduleTable: block: Bloquear departure: Salida diff --git a/i18n/fr.yml b/i18n/fr.yml index 2d3e297ee..a8283e9f1 100644 --- a/i18n/fr.yml +++ b/i18n/fr.yml @@ -232,6 +232,20 @@ components: advancedOptions: Options avancées groupSize: "Taille du groupe :" intermediateDestination: Entrez la destination intermédiaire + CompanionsPane: + addNewCompanion: Ajouter un accompagnateur + companionAlreadyAdded: Vous avez déjà un accompagnateur utilisant {email}. + companionExplanation: > + Invitez un utilisateur G-MAP existant à être votre accompagnateur en + entrant leur adresse email. Leur statut passera à "vérifié" dès leur + acceptation, et vous pourrez alors partager vos trajets et planifier des + trajets qui conviennent à leur profil de mobilité. + confirmDeleteCompanion: Voulez-vous supprimer l'accompagnateur {email} ? + currentCompanionsLabel: "Mes accompagnateurs :" + deleteCompanion: Supprimer {email} + noCompanions: Vous n'avez aucun accompagnateur. + submitNewCompanion: Envoyer l'invitation + title: Accompagnateurs DateTimeOptions: arriveBy: Arriver à departAt: Partir à @@ -442,7 +456,6 @@ components: changeNumber: Changer de numéro invalidCode: Le code de vérification doit comporter 6 chiffres. invalidPhone: Veuillez entrer un numéro de téléphone valable. - pending: Non vérifié phoneNumberSubmitted: Le numéro {phoneNumber} a bien été envoyé. phoneNumberVerified: Le numéro {phoneNumber} a bien été vérifié. placeholder: Entrez votre numéro @@ -457,7 +470,6 @@ components: verificationInstructions: > Un SMS vous a été envoyé avec un code de vérification. Veuillez taper ce code ci-dessous (le code expire après 10 minutes). - verified: Vérifié verify: Vérifier verifySms: >- Veuillez effectuer la vérification de votre numéro de téléphone afin de @@ -536,6 +548,7 @@ components: deleteSavedTrip: Supprimer ce trajet editSavedTrip: Modifier un trajet enregistré saveNewTrip: Enregistrer un nouveau trajet + travelCompanions: Accompagnateurs tripInformation: Informations sur le trajet tripNotFound: Trajet introuvable tripNotFoundDescription: Le trajet recherché est introuvable. @@ -576,6 +589,10 @@ components: retards StackedPaneDisplay: savePreferences: Enregistrer mes préférences + StatusBadge: + invalid: Non valable + pending: Non vérifié + verified: Vérifié StopScheduleTable: block: Bloc departure: Départ diff --git a/i18n/ko.yml b/i18n/ko.yml index e333e7e62..bc5bbb9d9 100644 --- a/i18n/ko.yml +++ b/i18n/ko.yml @@ -354,7 +354,6 @@ components: changeNumber: 번호 변경 invalidCode: 확인 코드 6 자리를 입력하세요. invalidPhone: 유효한 전화번호를 입력하세요. - pending: 보류 중 phoneNumberSubmitted: 전화번호 {phoneNumber} 이/가 성공적으로 제출되었습니다. phoneNumberVerified: 전화번호 {phoneNumber}가 성공적으로 확인되었습니다. placeholder: 전화번호를 입력하십시오 @@ -367,7 +366,6 @@ components: verificationCode: "확인 코드:" verificationInstructions: | 휴대폰의 SMS 메시지 앱에서 인증 코드를 확인하고 아래에 코드를 입력하세요(코드는 10분 후에 만료됩니다). - verified: 확인됨 verify: 확인 Place: deleteThisPlace: 이 장소 삭제 @@ -465,6 +463,10 @@ components: usingRealtimeInfo: 이 트립은 실시간 교통 및 지체 정보를 사용합니다 StackedPaneDisplay: savePreferences: 환경설정 저장 + StatusBadge: + invalid: Invalid + pending: 보류 중 + verified: 확인됨 StopScheduleTable: block: 블록 departure: 출발 diff --git a/i18n/ru.yml b/i18n/ru.yml index b7a2eedd4..0e1a1768b 100644 --- a/i18n/ru.yml +++ b/i18n/ru.yml @@ -390,7 +390,6 @@ components: changeNumber: Изменить номер invalidCode: "Введите проверочный код из 6\_цифр." invalidPhone: Введите действительный номер телефона. - pending: Ожидание phoneNumberSubmitted: Номер телефона{phoneNumber} был успешно отправлен. phoneNumberVerified: Номер телефона{phoneNumber} был успешно проверен. placeholder: Введите свой номер телефона @@ -403,7 +402,6 @@ components: могут взиматься дополнительные пени. verificationCode: "Код подтверждения:" verificationInstructions: "Откройте приложение для обмена SMS на телефоне и найдите текстовое сообщение с кодом подтверждения. Затем введите код ниже (срок действия кода: 10\_минут).\n" - verified: Подтверждено verify: Подтвердить Place: deleteThisPlace: Удалить это место @@ -514,6 +512,10 @@ components: режиме реального времени. StackedPaneDisplay: savePreferences: Сохранить параметры + StatusBadge: + invalid: Invalid + pending: Ожидание + verified: Подтверждено StopScheduleTable: block: Заблокировать departure: Отправление diff --git a/i18n/tl.yml b/i18n/tl.yml index d1e65084a..4d48060ef 100644 --- a/i18n/tl.yml +++ b/i18n/tl.yml @@ -395,7 +395,6 @@ components: changeNumber: Baguhin ang numero invalidCode: Maglagay ng 6 na digit para sa code sa pag-validate. invalidPhone: Maglagay ng valid na numero ng telepono. - pending: Nakabinbin phoneNumberSubmitted: Matagumpay na naisumite ang numero ng teleponong {phoneNumber}. phoneNumberVerified: Matagumpay na na-verify ang numero ng teleponong {phoneNumber} . placeholder: Ilagay ang numero ng iyong telepono @@ -412,7 +411,6 @@ components: Tingnan ang app sa SMS messaging sa iyong mobile phone para sa isang text message na may code sa pag-verify, at ilagay ang code sa ibaba (mag-e-expire ang code pagkalipas ng 10 minuto). - verified: Na-verify verify: I-verify Place: deleteThisPlace: I-delete ang lugar na ito @@ -523,6 +521,10 @@ components: pagkaantala StackedPaneDisplay: savePreferences: I-save ang mga kagustuhan + StatusBadge: + invalid: Invalid + pending: Nakabinbin + verified: Na-verify StopScheduleTable: block: I-block departure: Pag-alis diff --git a/i18n/vi.yml b/i18n/vi.yml index 719ee0898..316b37876 100644 --- a/i18n/vi.yml +++ b/i18n/vi.yml @@ -391,7 +391,6 @@ components: changeNumber: Thay đổi số điện thoại invalidCode: Vui lòng nhập 6 chữ số cho mã xác thực. invalidPhone: Xin vui lòng nhập một số điện thoại hợp lệ. - pending: Chưa xác minh phoneNumberSubmitted: Gửi thành công số điện thoại {phoneNumber}. phoneNumberVerified: Số điện thoại {phoneNumber} đã được xác minh thành công. placeholder: Nhập số điện thoại của bạn @@ -407,7 +406,6 @@ components: Vui lòng kiểm tra ứng dụng nhắn tin SMS trên điện thoại di động của bạn để thấy tin nhắn với mã xác minh và nhập mã bên dưới (mã hết hạn sau 10 phút). - verified: Đã xác minh verify: Kiểm chứng Place: deleteThisPlace: Xóa nơi này @@ -517,6 +515,10 @@ components: thực StackedPaneDisplay: savePreferences: Lưu lại những ưu tiên + StatusBadge: + invalid: Invalid + pending: Chưa xác minh + verified: Đã xác minh StopScheduleTable: block: Dãy nhà departure: Khởi hành diff --git a/i18n/zh_Hans.yml b/i18n/zh_Hans.yml index 47464fc2c..5d4b6a419 100644 --- a/i18n/zh_Hans.yml +++ b/i18n/zh_Hans.yml @@ -356,7 +356,6 @@ components: changeNumber: 更改电话号码 invalidCode: 请输入6位数的验证码. invalidPhone: 请输入一个有效的电话号码. - pending: 待定 phoneNumberSubmitted: 电话号码{phoneNumber}已成功提交。 phoneNumberVerified: 电话号码 {phoneNumber} 已成功验证。 placeholder: 输入你的电话号码 @@ -367,7 +366,6 @@ components: verificationCode: "验证码:" verificationInstructions: | 请检查您手机上的短信应用查看是否有验证码的短信并输入以下代码 (代码在10分钟后失效). - verified: 已验证 verify: 核实 Place: deleteThisPlace: 删除这个地点 @@ -464,6 +462,10 @@ components: usingRealtimeInfo: 这个行程使用实时交通和延迟信息 StackedPaneDisplay: savePreferences: 保存偏好 + StatusBadge: + invalid: Invalid + pending: 待定 + verified: 已验证 StopScheduleTable: block: 堵塞 departure: 出发 diff --git a/i18n/zh_Hant.yml b/i18n/zh_Hant.yml index e625d04c7..4fbc7d8fd 100644 --- a/i18n/zh_Hant.yml +++ b/i18n/zh_Hant.yml @@ -344,7 +344,6 @@ components: changeNumber: 變更號碼 invalidCode: 請輸入6位數的驗證碼。 invalidPhone: 請輸入有效的電話號碼。 - pending: 待處理 phoneNumberSubmitted: 已順利提交電話號碼 {phoneNumber}。 phoneNumberVerified: 已順利驗證電話號碼 {phoneNumber} 。 placeholder: 輸入您的電話號碼 @@ -356,7 +355,6 @@ components: verificationCode: 驗證碼: verificationInstructions: | 請查看您手機的簡訊應用程式是否收到含有驗證碼的簡訊,並在下方輸入該驗證碼 (驗證碼將在10分鐘後失效)。 - verified: 已驗證 verify: 驗證 Place: deleteThisPlace: 刪除此地點 @@ -453,6 +451,10 @@ components: usingRealtimeInfo: 此行程使用即時交通和延誤資訊。 StackedPaneDisplay: savePreferences: 儲存偏好 + StatusBadge: + invalid: Invalid + pending: 待處理 + verified: 已驗證 StopScheduleTable: block: 封鎖 departure: 出發 diff --git a/lib/components/user/common/add-email-form.tsx b/lib/components/user/common/add-email-form.tsx new file mode 100644 index 000000000..88e368402 --- /dev/null +++ b/lib/components/user/common/add-email-form.tsx @@ -0,0 +1,87 @@ +// @ts-expect-error No TypeScript for yup. +import * as yup from 'yup' +import { + ControlLabel, + FormControl, + FormGroup, + HelpBlock +} from 'react-bootstrap' +import { Field, Form, Formik } from 'formik' +import React, { ReactNode } from 'react' +import styled from 'styled-components' + +import { ControlStrip, phoneFieldStyle } from '../styled' +import SubmitButton from '../../util/submit-button' + +interface Props { + id: string + label: ReactNode + onSubmit: any + placeholder?: string + submitText: ReactNode +} + +// Styles +const InlineInput = styled(FormControl)` + ${phoneFieldStyle} + width: 100%; +` + +const Controls = styled.span` + display: flex; + + input { + margin-right: 10px; + } +` + +// The validation schema for email addresses +const emailValidationSchema = yup.object({ + newEmail: yup.string().email().required() +}) + +/** + * Just a form to add an email. + */ +const AddEmailForm = ({ + id, + label, + onSubmit, + placeholder, + submitText +}: Props): JSX.Element => ( + + {({ errors, isSubmitting, touched, values }) => { + const showError = + errors.newEmail && touched.newEmail && values.newEmail?.length > 0 + return ( + + {label} + +
+ + + {submitText} + + {showError && 'Invalid email'} + + ) + }} + +) + +export default AddEmailForm diff --git a/lib/components/user/existing-account-display.tsx b/lib/components/user/existing-account-display.tsx index fc87aaa9c..0f4452869 100644 --- a/lib/components/user/existing-account-display.tsx +++ b/lib/components/user/existing-account-display.tsx @@ -10,6 +10,7 @@ import PageTitle from '../util/page-title' import { EditedUser } from './types' import A11yPrefs from './a11y-prefs' import BackToTripPlanner from './back-to-trip-planner' +import CompanionsPane from './mobility-profile/companions-pane' import DeleteUser from './delete-user' import FavoritePlaceList from './places/favorite-place-list' import MobilityPane from './mobility-profile/mobility-pane' @@ -48,6 +49,12 @@ const ExistingAccountDisplay = (props: Props) => { ) }, + { + hidden: !mobilityProfileEnabled, + pane: CompanionsPane, + props, + title: + }, { pane: NotificationPrefsPane, props, diff --git a/lib/components/user/mobility-profile/companions-pane.tsx b/lib/components/user/mobility-profile/companions-pane.tsx new file mode 100644 index 000000000..d189b36eb --- /dev/null +++ b/lib/components/user/mobility-profile/companions-pane.tsx @@ -0,0 +1,199 @@ +import { ControlLabel, FormGroup } from 'react-bootstrap' +import { FormattedMessage, useIntl } from 'react-intl' +import { FormikProps } from 'formik' +import { Trash } from '@styled-icons/fa-solid/Trash' +import { User as UserIcon } from '@styled-icons/fa-solid/User' +import React, { useCallback, useState } from 'react' +import styled from 'styled-components' + +import { CompanionInfo, User } from '../types' +import { StyledIconWrapper } from '../../util/styledIcon' +import { UnstyledButton } from '../../util/unstyled-button' +import AddEmailForm from '../common/add-email-form' +import InvisibleA11yLabel from '../../util/invisible-a11y-label' +import StatusBadge from '../../util/status-badge' +import SubmitButton from '../../util/submit-button' + +const Companion = styled.li` + align-items: center; + display: flex; + justify-content: space-between; + list-style-type: none; + margin-top: 20px; + width: 100%; +` + +const CompanionList = styled.ul` + margin-bottom: 30px; +` + +const LeftGroup = styled.div` + align-items: center; + display: flex; + gap: 40px; +` + +const RightGroup = styled.div` + align-items: center; + display: flex; + gap: 20px; +` + +interface CompanionRowProps { + companionInfo: CompanionInfo + onDelete: (email: string) => void +} + +const CompanionRow = ({ + companionInfo, + onDelete +}: CompanionRowProps): JSX.Element => { + const intl = useIntl() + const { email, status } = companionInfo + const [disabled, setDisabled] = useState(false) + + const handleDelete = useCallback(async () => { + if ( + window.confirm( + intl.formatMessage( + { id: 'components.CompanionsPane.confirmDeleteCompanion' }, + { email: email } + ) + ) + ) { + setDisabled(true) + await onDelete(email) + setDisabled(false) + } + }, [email, intl, onDelete]) + + return ( + + + + + + {email} + + + + + + + + + + + + + + ) +} +/** + * Companions pane, part of mobility profile. + */ +const CompanionsPane = ({ + handleChange, + setFieldValue, + values: userData +}: FormikProps): JSX.Element => { + const { relatedUsers: companions = [] } = userData + const formId = 'add-companion-form' + const intl = useIntl() + + const updateCompanions = useCallback( + async (newCompanions) => { + setFieldValue('relatedUsers', newCompanions) + + // Register the change (can include a submission). + await handleChange({ + target: document.getElementById(formId) + }) + }, + [handleChange, setFieldValue] + ) + + const handleAddNewEmail = useCallback( + async ({ newEmail }, { resetForm }) => { + // Submit the new email if it is not already listed + if (!companions.find((comp) => comp.email === newEmail)) { + await updateCompanions([ + ...companions, + { + email: newEmail, + status: 'PENDING' + } + ]) + resetForm() + } else { + alert( + intl.formatMessage( + { id: 'components.CompanionsPane.companionAlreadyAdded' }, + { email: newEmail } + ) + ) + } + }, + [companions, intl, updateCompanions] + ) + + const handleDeleteEmail = useCallback( + async (email: string) => { + await updateCompanions(companions.filter((comp) => comp.email !== email)) + }, + [companions, updateCompanions] + ) + + return ( +
+

+ +

+ + + + + + {companions.length === 0 ? ( +

+ +

+ ) : ( + + {companions.map((companion) => ( + + ))} + + )} +
+ + } + onSubmit={handleAddNewEmail} + placeholder="friend.email@example.com" + submitText={ + + } + /> +
+ ) +} + +export default CompanionsPane diff --git a/lib/components/user/monitored-trip/saved-trip-editor.tsx b/lib/components/user/monitored-trip/saved-trip-editor.tsx index 701b7cd38..55e235878 100644 --- a/lib/components/user/monitored-trip/saved-trip-editor.tsx +++ b/lib/components/user/monitored-trip/saved-trip-editor.tsx @@ -3,6 +3,7 @@ import React, { ComponentType } from 'react' import { BackButtonContent } from '../back-link' import { MonitoredTrip } from '../types' +import { PaneAttributes } from '../stacked-panes' import { TRIPS_PATH } from '../../../util/constants' import DeleteForm from '../delete-form' import Link from '../../util/link' @@ -12,6 +13,7 @@ import StackedPanesWithSave from '../stacked-panes-with-save' import TripNotFound from './trip-not-found' interface Props { + hasMobilityProfile: boolean isCreating: boolean onCancel: () => void panes: Record @@ -30,7 +32,7 @@ const SavedTripEditor = (props: Props): JSX.Element => { const intl = useIntl() if (monitoredTrip) { - const paneSequence = [ + const paneSequence: PaneAttributes[] = [ { pane: panes.basics, props, @@ -47,6 +49,17 @@ const SavedTripEditor = (props: Props): JSX.Element => { } ] + // if mobility profile is present, then add travel companions pane + if (props.hasMobilityProfile) { + paneSequence.push({ + pane: panes.travelCompanions, + props, + title: ( + + ) + }) + } + const title = isCreating ? intl.formatMessage({ id: 'components.SavedTripEditor.saveNewTrip' }) : intl.formatMessage({ id: 'components.SavedTripEditor.editSavedTrip' }) diff --git a/lib/components/user/monitored-trip/saved-trip-screen.js b/lib/components/user/monitored-trip/saved-trip-screen.js index de7da92d3..5e66539ab 100644 --- a/lib/components/user/monitored-trip/saved-trip-screen.js +++ b/lib/components/user/monitored-trip/saved-trip-screen.js @@ -20,6 +20,7 @@ import { RETURN_TO_CURRENT_ROUTE } from '../../../util/ui' import { TRIPS_PATH } from '../../../util/constants' import AccountPage from '../account-page' import AwaitingScreen from '../awaiting-screen' +import CompanionsPane from '../mobility-profile/companions-pane' import InvisibleA11yLabel from '../../util/invisible-a11y-label' import withLoggedInUserSupport from '../with-logged-in-user-support' @@ -120,7 +121,8 @@ class SavedTripScreen extends Component { _panes = { basics: TripBasicsPane, notifications: TripNotificationsPane, - summary: TripSummaryPane + summary: TripSummaryPane, + travelCompanions: CompanionsPane } componentDidMount() { @@ -239,6 +241,7 @@ class SavedTripScreen extends Component { { 'terms', 'mobilityDevices', 'mobilityLimitations', + 'companions', 'notifications', 'places', 'finish' diff --git a/lib/components/user/phone-number-editor.tsx b/lib/components/user/phone-number-editor.tsx index 91da5f1b6..196776e3b 100644 --- a/lib/components/user/phone-number-editor.tsx +++ b/lib/components/user/phone-number-editor.tsx @@ -1,5 +1,5 @@ -import { Label as BsLabel, FormGroup } from 'react-bootstrap' import { connect } from 'react-redux' +import { FormGroup } from 'react-bootstrap' // @ts-expect-error Package does not have type declaration import { formatPhoneNumber } from 'react-phone-number-input' import { FormattedMessage, injectIntl, IntlShape } from 'react-intl' @@ -13,6 +13,7 @@ import { GREY_ON_WHITE } from '../util/colors' import { isBlank } from '../../util/ui' import { PhoneFormatConfig } from '../../util/config-types' import InvisibleA11yLabel from '../util/invisible-a11y-label' +import StatusBadge from '../util/status-badge' import { ControlStrip } from './styled' import PhoneChangeForm, { PhoneChangeSubmitHandler } from './phone-change-form' @@ -253,17 +254,11 @@ class PhoneNumberEditor extends Component { {/* Invisible parentheses for no-CSS and screen readers */} ( - {isPending ? ( - - - - ) : ( - phoneNumberVerified && ( - - - - ) - )} + )