diff --git a/__tests__/components/viewers/__snapshots__/nearby-view.js.snap b/__tests__/components/viewers/__snapshots__/nearby-view.js.snap index 3d4a9501a..ac5bc3bee 100644 --- a/__tests__/components/viewers/__snapshots__/nearby-view.js.snap +++ b/__tests__/components/viewers/__snapshots__/nearby-view.js.snap @@ -46,7 +46,7 @@ exports[`components > viewers > nearby view renders nothing on a blank page 1`] className="nearby-view base-color-bg" >
viewers > nearby view renders nothing on a blank page 1`] } >
    viewers > nearby view renders proper scooter dates 1`] = ` className="nearby-view base-color-bg" >
    viewers > nearby view renders proper scooter dates 1`] = ` } >
      viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = `

      viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = `

      viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = `

      viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
      viewers > nearby view renders proper scooter dates 1`] = ` >

      + Roosevelt Station - Bay 2 @@ -10036,7 +10603,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` >

      viewers > nearby view renders proper scooter dates 1`] = `

      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
        viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
      • viewers > nearby view renders proper scooter dates 1`] = ` title="45" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Loyal Heights Greenwood" > - Loyal Heights Greenwood + + components.NearbyView.headsign +

          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

        1. viewers > nearby view renders proper scooter dates 1`] = ` title="62" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Downtown Seattle Fremont" > - Downtown Seattle Fremont + + components.NearbyView.headsign +

            viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

          1. viewers > nearby view renders proper scooter dates 1`] = ` title="79" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Roosevelt Station Sand Point" > - Roosevelt Station Sand Point + + components.NearbyView.headsign +

              viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

              viewers > nearby view renders proper scooter dates 1`] = ` >

              + Roosevelt @@ -16883,7 +17843,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` >

              viewers > nearby view renders proper scooter dates 1`] = `

              viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
              • viewers > nearby view renders proper scooter dates 1`] = ` title="1 Line" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Angle Lake" > - Angle Lake + + components.NearbyView.headsign +

                  viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                1. viewers > nearby view renders proper scooter dates 1`] = ` title="1 Line" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Northgate" > - Northgate + + components.NearbyView.headsign +

                    viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                  1. viewers > nearby view renders proper scooter dates 1`] = ` title="1 Line" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Capitol Hill" > - Capitol Hill + + components.NearbyView.headsign +

                      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                      viewers > nearby view renders proper scooter dates 1`] = `

                      viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

                      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                      viewers > nearby view renders proper scooter dates 1`] = `

                      viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

                      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                      viewers > nearby view renders proper scooter dates 1`] = `

                      viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                      viewers > nearby view renders proper scooter dates 1`] = ` >

                      - - Roosevelt Station - Bay 1 - -

                      -
                      - - -

                      - - components.NearbyView.distanceAway - -

                      -
                      -
                      -
                      -
                      - -
                      - - + + Roosevelt Station - Bay 1 + +

                      + + + +

                      + + components.NearbyView.distanceAway + +

                      +
                      +
                      +
                      +
                      + +
                      + + viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                        viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
                      • viewers > nearby view renders proper scooter dates 1`] = ` title="45" > viewers > nearby view renders proper scooter dates 1`] = ` } title="University District Roosevelt Station" > - University District Roosevelt Station + + components.NearbyView.headsign +

                          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                        1. viewers > nearby view renders proper scooter dates 1`] = ` title="62" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Sand Point East Green Lake" > - Sand Point East Green Lake + + components.NearbyView.headsign +

                            viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                          1. viewers > nearby view renders proper scooter dates 1`] = ` title="79" > viewers > nearby view renders proper scooter dates 1`] = ` } title="University District Sand Point" > - University District Sand Point + + components.NearbyView.headsign +

                              viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                            1. viewers > nearby view renders proper scooter dates 1`] = ` title="988" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Madrona Special" > - Madrona Special + + components.NearbyView.headsign +

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = `

                                viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = `

                                viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = `

                                viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = `

                                viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = `

                                viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                viewers > nearby view renders proper scooter dates 1`] = ` >

                                + Roosevelt Station Bay 5 - Bay 5 @@ -35949,7 +37982,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` >

                                viewers > nearby view renders proper scooter dates 1`] = `

                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                  viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
                                • viewers > nearby view renders proper scooter dates 1`] = ` title="67" > viewers > nearby view renders proper scooter dates 1`] = ` } title="University District Roosevelt" > - University District Roosevelt + + components.NearbyView.headsign +

                                    viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                  1. viewers > nearby view renders proper scooter dates 1`] = ` title="73" > viewers > nearby view renders proper scooter dates 1`] = ` } title="University District Maple Leaf" > - University District Maple Leaf + + components.NearbyView.headsign +

                                      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                    1. viewers > nearby view renders proper scooter dates 1`] = ` title="984" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Downtown Seattle Special" > - Downtown Seattle Special + + components.NearbyView.headsign +

                                        viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                        viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                        viewers > nearby view renders proper scooter dates 1`] = `

                                        viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                        viewers > nearby view renders proper scooter dates 1`] = ` >

                                        + Roosevelt @@ -43780,7 +46381,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` >

                                        viewers > nearby view renders proper scooter dates 1`] = `

                                        viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                          viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
                                        • viewers > nearby view renders proper scooter dates 1`] = ` title="1 Line" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Northgate" > - Northgate + + components.NearbyView.headsign +

                                            viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                            viewers > nearby view renders proper scooter dates 1`] = ` >

                                            + Roosevelt Station - Bay 3 @@ -51332,7 +54586,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` >

                                            viewers > nearby view renders proper scooter dates 1`] = `

                                            viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                              viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
                                            • viewers > nearby view renders proper scooter dates 1`] = ` title="522" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Bothell" > - Bothell + + components.NearbyView.headsign +

                                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                              1. viewers > nearby view renders proper scooter dates 1`] = ` title="67" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Northgate Station Roosevelt Station" > - Northgate Station Roosevelt Station + + components.NearbyView.headsign +

                                                  viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                                1. viewers > nearby view renders proper scooter dates 1`] = ` title="522" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Woodinville" > - Woodinville + + components.NearbyView.headsign +

                                                    viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                                  1. viewers > nearby view renders proper scooter dates 1`] = ` title="73" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Jackson Park Maple Leaf" > - Jackson Park Maple Leaf + + components.NearbyView.headsign +

                                                      viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                                    1. viewers > nearby view renders proper scooter dates 1`] = ` title="322" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Kenmore P&R Roosevelt Station" > - Kenmore P&R Roosevelt Station + + components.NearbyView.headsign +

                                                        viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                                      1. viewers > nearby view renders proper scooter dates 1`] = ` title="322" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Kenmore P&R" > - Kenmore P&R + + components.NearbyView.headsign +

                                                          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                                          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` >

                                                          viewers > nearby view renders proper scooter dates 1`] = `

                                                          viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                                          viewers > nearby view renders proper scooter dates 1`] = ` >

                                                          + NE 65th St & 14th Ave NE @@ -62499,7 +66374,7 @@ exports[`components > viewers > nearby view renders proper scooter dates 1`] = ` >

                                                          viewers > nearby view renders proper scooter dates 1`] = `

                                                          viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` size="0.9em" > viewers > nearby view renders proper scooter dates 1`] = ` >
                                                            viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >
                                                          • viewers > nearby view renders proper scooter dates 1`] = ` title="45" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Loyal Heights Greenwood" > - Loyal Heights Greenwood + + components.NearbyView.headsign +

                                                              viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                                            1. viewers > nearby view renders proper scooter dates 1`] = ` title="62" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Downtown Seattle Fremont" > - Downtown Seattle Fremont + + components.NearbyView.headsign +

                                                                viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` roundedTop={false} >

                                                              1. viewers > nearby view renders proper scooter dates 1`] = ` title="79" > viewers > nearby view renders proper scooter dates 1`] = ` } title="Roosevelt Station Sand Point" > - Roosevelt Station Sand Point + + components.NearbyView.headsign +

                                                                  viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.realtime" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` iconViewBox="0 0 448 512" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > nearby view renders proper scooter dates 1`] = ` title="components.StopTimeCell.scheduled" > viewers > nearby view renders proper scooter dates 1`] = ` > viewers > stop viewer should render with initial stop id a > viewers > stop viewer should render with initial stop id a

                                                          { }) // Web security is disabled to allow requests to the mock OTP server browser = await puppeteer.launch({ - args: ['--disable-web-security'] + args: ['--disable-web-security', '--no-sandbox'] }) }) diff --git a/craco.config.js b/craco.config.js index 283dc3350..f156dcfe1 100644 --- a/craco.config.js +++ b/craco.config.js @@ -48,6 +48,8 @@ module.exports = { findBackwardsCompatibleEnvVar('JS_CONFIG') backwardsCompatibleEnv.HTML_FILE = findBackwardsCompatibleEnvVar('HTML_FILE') + backwardsCompatibleEnv.PLAN_QUERY_RESOURCE_URI = + findBackwardsCompatibleEnvVar('PLAN_QUERY_RESOURCE_URI') backwardsCompatibleEnv.CUSTOM_CSS = findBackwardsCompatibleEnvVar('CUSTOM_CSS') @@ -65,6 +67,13 @@ module.exports = { } addBeforeLoader(webpackConfig, loaderByName('file-loader'), yamlLoader) + // Support YAML + const graphqlLoader = { + loader: ['raw-loader'], + test: /\.graphql$/ + } + addBeforeLoader(webpackConfig, loaderByName('file-loader'), graphqlLoader) + // Support webfonts (for font awesome) const webfontLoader = { loader: ['url-loader'], @@ -82,7 +91,7 @@ module.exports = { loader.exclude = /node_modules/ }) - // Gather the CSS, HTML, YAML, and JS override files. + // Gather the CSS, HTML, YAML, GraphQL, and JS override files. const CUSTOM_CSS = (process.env && process.env.CUSTOM_CSS) || backwardsCompatibleEnv.CUSTOM_CSS || @@ -91,6 +100,22 @@ module.exports = { (process.env && process.env.HTML_FILE) || backwardsCompatibleEnv.HTML_FILE || 'lib/index.tpl.html' + // resolve the custom GraphQL file. If it is present, copy the file to a + // temporary folder within this project so that it can be bundled and loaded at runtime. + let customPlanGraphQLFile = './planQuery.graphql' + const PLAN_QUERY_RESOURCE_URI = + (process.env && process.env.PLAN_QUERY_RESOURCE_URI) || + backwardsCompatibleEnv.PLAN_QUERY_RESOURCE_URI || + 'node_modules/@opentripplanner/core-utils/src/planQuery.graphql' + if (PLAN_QUERY_RESOURCE_URI) { + const splitPath = PLAN_QUERY_RESOURCE_URI.split(path.sep) + customPlanGraphQLFile = `../tmp/${splitPath[splitPath.length - 1]}` + // copy location is relative to root, while js file for app is relative to lib + fs.copySync( + PLAN_QUERY_RESOURCE_URI, + `./tmp/${splitPath[splitPath.length - 1]}` + ) + } const YAML_CONFIG = (process.env && process.env.YAML_CONFIG) || backwardsCompatibleEnv.YAML_CONFIG || @@ -143,6 +168,7 @@ module.exports = { new webpack.DefinePlugin({ CSS: JSON.stringify(CUSTOM_CSS), JS_CONFIG: JSON.stringify(customJsFile), + PLAN_QUERY_RESOURCE: JSON.stringify(customPlanGraphQLFile), // Optionally override the default config files with some other // files. YAML_CONFIG: JSON.stringify(YAML_CONFIG) diff --git a/example-config.yml b/example-config.yml index c24e1265d..79a242954 100644 --- a/example-config.yml +++ b/example-config.yml @@ -112,6 +112,11 @@ persistence: # iconUrl: '' # href: '' +### These settings are only used for the field trip features. +dateTime: + timeFormat: h:mm a + dateFormat: MM/dd/yyyy + map: initLat: 45.52 initLon: -122.682 @@ -411,6 +416,8 @@ itinerary: displayA11yError: false # Whether to display itinerary info in the side of the preview or next to the departure times showInlineItinerarySummary: false + # Whether to sync the sort type with the depart/arrive time in the date/time modal + syncSortWithDepartArrive: true # The sort option to use by default # Available sort options: 'BEST', 'DURATION', 'ARRIVALTIME', 'WALKTIME', 'COST', 'DEPARTURETIME' # defaultSort: "BEST" # Default @@ -426,6 +433,11 @@ itinerary: # - 'DEPARTURETIME' # In the batch itinerary UI, this setting will always show both departure/arrival times alwaysShowBothTimes: false +advancedSettingsPanel: + # Show button in advanced panel that allows users to save and return + saveAndReturnButton: true +# Prevent users from selecting a single day for saving trips. +disableSingleItineraryDays: false # The transitOperators key is a list of transit operators that can be used to # order transit agencies when sorting by route. Also, this can optionally diff --git a/i18n/en-US.yml b/i18n/en-US.yml index 08a39b393..be563db0e 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -109,13 +109,11 @@ common: submitting: Submitting… "yes": "Yes" itineraryDescriptions: - calories: "{calories, number} Cal" fareUnknown: No fare information noItineraryToDisplay: No itinerary to display. relativeCo2: | {co2} {isMore, select, true {more} other {less} } CO₂ than driving alone timeStartEnd: "{start} – {end}" - transfers: "{transfers, plural, =0 {} one {# transfer} other {# transfers}}" linkOpensNewWindow: (Opens new window) modes: bicycle_rent: Bikeshare @@ -154,7 +152,6 @@ common: enterStartLocation: Enter start location or {mapAction} on map… tap: tap time: - departureArrivalTimes: "{startTime, time, short}—{endTime, time, short}" duration: aFewSeconds: a few seconds nDays: "{days, plural, =1 {one day} other {# days}}" @@ -196,8 +193,15 @@ components: BatchRoutingPanel: shortTitle: Plan Trip BatchSearchScreen: + advancedHeader: Advanced Preferences header: Plan Your Trip + modeOptions: Mode Options modeSelectorLabel: Select a travel mode + moreOptions: More options + saveAndReturn: Save and return + saved: Saved + submodeSelectorLabel: Select travel modes and submodes + tripOptions: Trip Options BatchSettings: destination: destination invalidModeSelection: >- @@ -276,7 +280,6 @@ components: ariaLabel: Form navigation ItinerarySummary: itineraryDetails: Itinerary details - minMaxFare: "{minTotalFare} - {maxTotalFare}" LocationSearch: enterLocation: Enter location setDestination: Set Destination @@ -371,6 +374,7 @@ components: distanceAway: "{localizedDistanceString} away" error: An error occurred loading nearby amenities. header: Nearby View + headsign: "{destination}" nearbyListIntro: List of {count} nearby entities. nothingNearby: There are no places nearby. spacesAvailable: "{spacesAvailable} empty spaces available" @@ -539,6 +543,7 @@ components: SavedTripScreen: itineraryLoaded: Itinerary loaded itineraryLoading: Loading itinerary + selectAtLeastOneDay: Please select at least one day to monitor. tooManyTrips: > You already have reached the maximum of five saved trips. Please remove unused trips from your saved trips, and try again. @@ -640,6 +645,8 @@ components: oneHour: 1 hour realtimeAlertFlagged: There is a realtime alert flagged on my journey timeBefore: "{time} before" + TripPreviewLayout: + previewTrip: Preview Trip TripStatus: alerts: "{alerts, plural, one {# alert!} other {# alerts!}}" deleteTrip: Delete Trip diff --git a/i18n/fr.yml b/i18n/fr.yml index 7cb0daaab..20afa65fb 100644 --- a/i18n/fr.yml +++ b/i18n/fr.yml @@ -118,15 +118,11 @@ common: submitting: Envoi en cours… "yes": Oui itineraryDescriptions: - calories: "{calories, number} kcal" fareUnknown: Tarif inconnu noItineraryToDisplay: Aucun trajet à afficher. relativeCo2: | {co2} de CO₂ en {isMore, select, true {plus} other {moins} } qu'en voiture timeStartEnd: "{start} – {end}" - transfers: >- - {transfers, plural, =0 {} one {# correspondance} other {# - correspondances}} linkOpensNewWindow: (Ouvre une nouvelle fenêtre) modes: bicycle_rent: En vélo en libre-service @@ -165,7 +161,6 @@ common: enterStartLocation: Entrez votre point de départ ou {mapAction} sur la carte… tap: appuyez time: - departureArrivalTimes: "{startTime, time, short}—{endTime, time, short}" duration: aFewSeconds: quelques secondes nDays: "{days, plural, =1 {un jour} other {# jours}}" @@ -209,8 +204,15 @@ components: BatchRoutingPanel: shortTitle: Planifier un trajet BatchSearchScreen: + advancedHeader: Préférences avancées header: Votre trajet + modeOptions: Choix du mode modeSelectorLabel: Sélectionnez un mode de déplacement + moreOptions: Plus d'options + saveAndReturn: Enregistrer et fermer + saved: Enregistré + submodeSelectorLabel: Sélectionnez vos modes et sous-modes de déplacement + tripOptions: Options concernant le trajet BatchSettings: destination: destination invalidModeSelection: >- @@ -279,7 +281,6 @@ components: ariaLabel: Navigation du formulaire ItinerarySummary: itineraryDetails: Détails du trajet - minMaxFare: "{minTotalFare} - {maxTotalFare}" LocationSearch: enterLocation: Entrez le lieu setDestination: Destination @@ -376,6 +377,7 @@ components: distanceAway: à {localizedDistanceString} error: Erreur lors du chargement des services à proximité. header: À proximité + headsign: "{destination}" nearbyListIntro: Liste de {count} entités à proximité. nothingNearby: Aucun lieu à proximité. spacesAvailable: "{spacesAvailable} emplacements libres disponibles" @@ -549,6 +551,7 @@ components: SavedTripScreen: itineraryLoaded: Trajet chargé itineraryLoading: Chargement du trajet + selectAtLeastOneDay: Veuillez choisir au moins un jour pour le suivi. tooManyTrips: > Vous avez déjà atteint le nombre maximum de 5 trajets enregistrés. Veuillez supprimer les trajets enregistrés qui sont inutilisés, puis @@ -660,6 +663,8 @@ components: oneHour: 1 heure realtimeAlertFlagged: Une alerte en temps réel affecte mon trajet timeBefore: "{time} avant" + TripPreviewLayout: + previewTrip: Aperçu du trajet TripStatus: alerts: "{alerts, plural, =0 {# alerte !} one {# alerte !} other {# alertes !}}" deleteTrip: Supprimer le trajet diff --git a/lib/actions/apiV2.js b/lib/actions/apiV2.js index 2a38c1061..46c857616 100644 --- a/lib/actions/apiV2.js +++ b/lib/actions/apiV2.js @@ -499,15 +499,13 @@ export const fetchNearby = (position, radius) => { export const findStopTimesForStop = (params) => function (dispatch, getState) { dispatch(fetchingStopTimesForStop(params)) - const { date, stopId } = params + const { date, onlyRequestForOperators, stopId } = params const timeZone = getState().otp.config.homeTimezone // Create a service date timestamp from 3:30am local. const serviceDay = getServiceStart(date, timeZone).getTime() / 1000 - return dispatch( - createGraphQLQueryAction( - `query StopTimes( + const fullStopTimesQuery = `query StopTimes( $serviceDay: Long! $stopId: String! ) { @@ -567,7 +565,48 @@ export const findStopTimesForStop = (params) => } } } - }`, + }` + + const shorterStopTimesQueryForOperators = `query StopTimes( + $stopId: String! + ) { + stop(id: $stopId) { + gtfsId + code + routes { + id: gtfsId + agency { + gtfsId + name + } + patterns { + id + headsign + } + } + stoptimesForPatterns(numberOfDepartures: 100, omitNonPickups: true, omitCanceled: false) { + pattern { + desc: name + headsign + id: code + route { + agency { + gtfsId + } + gtfsId + } + } + } + } + }` + + const query = onlyRequestForOperators + ? shorterStopTimesQueryForOperators + : fullStopTimesQuery + + return dispatch( + createGraphQLQueryAction( + query, { serviceDay, stopId @@ -924,6 +963,7 @@ export function routingQuery(searchId = null, updateSearchInReducer) { return function (dispatch, getState) { const state = getState() const { config, currentQuery, modeSettingDefinitions } = state.otp + const { planQuery } = config.api const { loggedInUser } = state.user const persistenceMode = getPersistenceMode(config.persistence) const activeItinerary = @@ -999,7 +1039,6 @@ export function routingQuery(searchId = null, updateSearchInReducer) { }, date, from: currentQuery.from, - mobilityProfile: loggedInUser?.mobilityProfile?.mobilityMode, modes: modes || activeModes, modeSettings, time, @@ -1009,6 +1048,9 @@ export function routingQuery(searchId = null, updateSearchInReducer) { ...currentQuery, numItineraries: numItineraries || getDefaultNumItineraries(config) } + if (config.mobilityProfile) { + baseQuery.mobilityProfile = loggedInUser?.mobilityProfile?.mobilityMode + } // Generate combinations if the modes for query are not specified in the query // FIXME: BICYCLE_RENT does not appear in this list unless TRANSIT is also enabled. // This is likely due to the fact that BICYCLE_RENT is treated as a transit submode. @@ -1032,7 +1074,7 @@ export function routingQuery(searchId = null, updateSearchInReducer) { const query = generateOtp2Query(combo) dispatch( createGraphQLQueryAction( - query.query, + planQuery || query.query, query.variables, (response) => { const dispatchedRoutingResponse = routingResponse(response) @@ -1111,7 +1153,11 @@ export function routingQuery(searchId = null, updateSearchInReducer) { } } }) - ?.map(convertGraphQLResponseToLegacy) + ?.map((leg) => ({ + ...convertGraphQLResponseToLegacy(leg), + route: leg.transitLeg ? leg.route : undefined + })), + otp2QueryParams: query.variables }) ) diff --git a/lib/actions/field-trip.js b/lib/actions/field-trip.js index 316978475..0b667dbf1 100644 --- a/lib/actions/field-trip.js +++ b/lib/actions/field-trip.js @@ -6,7 +6,7 @@ import { getRoutingParams, planParamsToQueryAsync } from '@opentripplanner/core-utils/lib/query' -import { isTransit } from '@opentripplanner/core-utils/lib/itinerary' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT @@ -395,36 +395,34 @@ function makeSaveFieldTripItinerariesData(request, outbound, state) { } const gtfsTripsForItinerary = [] - itinerary.legs - .filter((leg) => isTransit(leg.mode)) - .forEach((leg) => { - let routeName = leg.routeShortName ? `(${leg.routeShortName}) ` : '' - routeName = `${routeName}${leg.routeLongName}` - const gtfsTrip = { - agencyAndId: leg.tripId, - // 'arrive' must be expressed in the agency's timezone - arrive: formatInTz(new Date(leg.endTime), FIELD_TRIP_TIME_FORMAT, { - timeZone: homeTimezone - }), - capacity: getFieldTripGroupCapacityForMode( - fieldTripModuleConfig, - leg.mode - ), - // 'depart' must be expressed in the agency's timezone - depart: formatInTz(new Date(leg.startTime), FIELD_TRIP_TIME_FORMAT, { - timeZone: homeTimezone - }), - fromStopIndex: leg.from.stopIndex, - fromStopName: leg.from.name, - headsign: leg.headsign, - routeName, - toStopIndex: leg.to.stopIndex, - toStopName: leg.to.name, - tripHash: tripHashLookup[leg.tripId] - } - if (leg.tripBlockId) gtfsTrip.blockId = leg.tripBlockId - gtfsTripsForItinerary.push(gtfsTrip) - }) + itinerary.legs.filter(isTransitLeg).forEach((leg) => { + let routeName = leg.routeShortName ? `(${leg.routeShortName}) ` : '' + routeName = `${routeName}${leg.routeLongName}` + const gtfsTrip = { + agencyAndId: leg.tripId, + // 'arrive' must be expressed in the agency's timezone + arrive: formatInTz(new Date(leg.endTime), FIELD_TRIP_TIME_FORMAT, { + timeZone: homeTimezone + }), + capacity: getFieldTripGroupCapacityForMode( + fieldTripModuleConfig, + leg.mode + ), + // 'depart' must be expressed in the agency's timezone + depart: formatInTz(new Date(leg.startTime), FIELD_TRIP_TIME_FORMAT, { + timeZone: homeTimezone + }), + fromStopIndex: leg.from.stopIndex, + fromStopName: leg.from.name, + headsign: leg.headsign, + routeName, + toStopIndex: leg.to.stopIndex, + toStopName: leg.to.name, + tripHash: tripHashLookup[leg.tripId] + } + if (leg.tripBlockId) gtfsTrip.blockId = leg.tripBlockId + gtfsTripsForItinerary.push(gtfsTrip) + }) data.itins.push(itineraryDataToSave) data.gtfsTrips.push(gtfsTripsForItinerary) @@ -616,7 +614,6 @@ function getMissingTripHashesForActiveItineraries() { }) ) } - return true } /** @@ -712,52 +709,50 @@ function checkValidityAndCapacity(state, request) { // check each individual trip to see if there aren't any trips in this // itinerary that are already in use by another field trip - itinerary.legs - .filter((leg) => isTransit(leg.mode)) - .forEach((leg) => { - const tripId = leg?.trip?.gtfsId - - // this variable is used to track how many other field trips are using a - // particular trip - let capacityInUse = 0 - - // iterate over trips that are already being used by other field trips - // NOTE: In the use case of re-planning trips, there is currently no way - // to discern whether a tripInUse belongs to the current direction of - // the field trip being planned. Therefore, this will result in the - // re-planning of trips avoiding it's own previously planned trips - // that it currently has saved - travelDateTripsInUse.forEach((tripInUse) => { - if (!tripsOverlap(leg, tripHashLookup, tripId, tripInUse)) return - - // ranges overlap! Add number of passengers on this other field trip - // to total capacity in use - capacityInUse += tripInUse.passengers - }) + itinerary.legs.filter(isTransitLeg).forEach((leg) => { + const tripId = leg?.trip?.gtfsId + + // this variable is used to track how many other field trips are using a + // particular trip + let capacityInUse = 0 + + // iterate over trips that are already being used by other field trips + // NOTE: In the use case of re-planning trips, there is currently no way + // to discern whether a tripInUse belongs to the current direction of + // the field trip being planned. Therefore, this will result in the + // re-planning of trips avoiding it's own previously planned trips + // that it currently has saved + travelDateTripsInUse.forEach((tripInUse) => { + if (!tripsOverlap(leg, tripHashLookup, tripId, tripInUse)) return + + // ranges overlap! Add number of passengers on this other field trip + // to total capacity in use + capacityInUse += tripInUse.passengers + }) - // check if the remaining capacity on this trip is enough to allow more - // field trip passengers on board - const legModeCapacity = getFieldTripGroupCapacityForMode( - fieldTripModuleConfig, - leg.mode - ) - let remainingTripCapacity = legModeCapacity - capacityInUse - if (remainingTripCapacity < minimumAllowableRemainingCapacity) { - // This trip is already too "full" to allow any addition field trips - // on board. Ban this trip in future searches and don't use this - // itinerary in final results (set trip and itinerary capacity to 0). - remainingTripCapacity = 0 - } + // check if the remaining capacity on this trip is enough to allow more + // field trip passengers on board + const legModeCapacity = getFieldTripGroupCapacityForMode( + fieldTripModuleConfig, + leg.mode + ) + let remainingTripCapacity = legModeCapacity - capacityInUse + if (remainingTripCapacity < minimumAllowableRemainingCapacity) { + // This trip is already too "full" to allow any addition field trips + // on board. Ban this trip in future searches and don't use this + // itinerary in final results (set trip and itinerary capacity to 0). + remainingTripCapacity = 0 + } - // always ban trips found in itineraries so that subsequent searches - // don't encounter them. - // TODO: a more advanced way of doing things might be to ban trip - // sequences to not find the same exact sequence, but also - // individual trips that are too full. - tripsToBanInSubsequentSearches.push(tripId) + // always ban trips found in itineraries so that subsequent searches + // don't encounter them. + // TODO: a more advanced way of doing things might be to ban trip + // sequences to not find the same exact sequence, but also + // individual trips that are too full. + tripsToBanInSubsequentSearches.push(tripId) - itineraryCapacity = Math.min(itineraryCapacity, remainingTripCapacity) - }) + itineraryCapacity = Math.min(itineraryCapacity, remainingTripCapacity) + }) if (itineraryCapacity > 0) { // itinerary has capacity, add to list and update remaining group size. diff --git a/lib/actions/user.js b/lib/actions/user.js index dad68bece..837d7341e 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -6,6 +6,7 @@ import isEqual from 'lodash.isequal' import qs from 'qs' import toast from 'react-hot-toast' +import { applyRouteModeOverrides } from '../util/itinerary' import { convertToPlace, getPersistenceMode, @@ -129,6 +130,11 @@ export function fetchMonitoredTrips() { 'GET' ) if (status === 'success') { + const { routeModeOverrides } = getState().otp.config + trips.data.forEach((trip) => { + applyRouteModeOverrides(trip.itinerary, routeModeOverrides) + }) + dispatch(setCurrentUserMonitoredTrips(trips.data)) } } @@ -169,6 +175,20 @@ function convertRequestToSearch(config) { } } +/** + * Determines whether two GraphQL sets of variables are for the same trip request/search. + * + * Modes are excluded from the comparison because the UI triggers multiple queries + * with the same GraphQL variables but with different combinations of modes. + * Modes exclusion also means that if someone makes the same search with different mode settings + * (e.g. excludes/adds transit modes), that search will also be combined with the previous ones. + */ +function areRequestsSameExceptModes(qp1, qp2) { + const { modes: modes1, ...otherParams1 } = qp1 + const { modes: modes2, ...otherParams2 } = qp2 + return isEqual(otherParams1, otherParams2) +} + /** * Removes duplicate requests so that only one request is displayed per "batch". */ @@ -176,7 +196,12 @@ function removeDuplicateRequests(filtered, tripRequest) { // Compare one trip request to the next one. if (filtered.length === 0) { filtered.push(tripRequest) - } else if (!isEqual(filtered[filtered.length - 1].query, tripRequest.query)) { + } else if ( + !areRequestsSameExceptModes( + filtered[filtered.length - 1].query, + tripRequest.query + ) + ) { filtered.push(tripRequest) } else { filtered[filtered.length - 1].query.modes.push(...tripRequest.query.modes) diff --git a/lib/components/app/batch-routing-panel.tsx b/lib/components/app/batch-routing-panel.tsx index c1321091e..5c6a69c73 100644 --- a/lib/components/app/batch-routing-panel.tsx +++ b/lib/components/app/batch-routing-panel.tsx @@ -1,9 +1,17 @@ import { connect } from 'react-redux' +import { CSSTransition, TransitionGroup } from 'react-transition-group' import { FormattedMessage, injectIntl, IntlShape } from 'react-intl' import React, { Component, FormEvent } from 'react' +import { + advancedPanelClassName, + mainPanelClassName, + transitionDuration, + TransitionStyles +} from '../form/styled' import { getActiveSearch, getShowUserSettings } from '../../util/state' import { getPersistenceMode } from '../../util/user' +import AdvancedSettingsPanel from '../form/advanced-settings-panel' import BatchSettings from '../form/batch-settings' import InvisibleA11yLabel from '../util/invisible-a11y-label' import LocationField from '../form/connected-location-field' @@ -15,6 +23,7 @@ import ViewerContainer from '../viewers/viewer-container' interface Props { activeSearch: any intl: IntlShape + mainPanelContent: number mobile?: boolean showUserSettings: boolean } @@ -24,7 +33,43 @@ interface Props { */ class BatchRoutingPanel extends Component { state = { - planTripClicked: false + closeAdvancedSettingsWithDelay: false, + planTripClicked: false, + showAdvancedModeSettings: false + } + + _advancedSettingRef = React.createRef() + _mainPanelContentRef = React.createRef() + _itinerariesAndUserRef = React.createRef() + + componentDidUpdate(prevProps: Readonly): void { + // Close the advanced mode settings if we navigate to another page + if ( + prevProps.mainPanelContent === null && + this.props.mainPanelContent !== null && + this.state.showAdvancedModeSettings + ) { + this.setState({ + showAdvancedModeSettings: false + }) + } + } + + openAdvancedSettings = () => { + this.setState({ + closeAdvancedSettingsWithDelay: false, + showAdvancedModeSettings: true + }) + } + + closeAdvancedSettings = () => { + this.setState({ showAdvancedModeSettings: false }) + } + + setCloseAdvancedSettingsWithDelay = () => { + this.setState({ + closeAdvancedSettingsWithDelay: true + }) } handleSubmit = (e: FormEvent) => e.preventDefault() @@ -44,6 +89,11 @@ class BatchRoutingPanel extends Component { id: 'common.searchForms.click' }) + /* If there is a save button in advanced preferences, add a transition delay to allow + the saved state to be displayed to users */ + const transitionDelay = this.state.closeAdvancedSettingsWithDelay ? 300 : 0 + const transitionDurationWithDelay = transitionDuration + transitionDelay + return ( { height: '100%' }} > - -

                                                          - -

                                                          -
                                                          -
                                                          - - + {!this.state.showAdvancedModeSettings && ( + +

                                                          + +

                                                          +
                                                          + )} + + + {this.state.showAdvancedModeSettings && ( + + + )} - isRequired - locationType="from" - selfValidate={planTripClicked} - showClearButton={!mobile} - /> - +
                                                          + + + +
                                                          + +
                                                          +
                                                          + +
                                                          + )} - isRequired - locationType="to" - selfValidate={planTripClicked} - showClearButton={!mobile} - /> -
                                                          - -
                                                          -
                                                          - - - {!activeSearch && showUserSettings && ( - - )} -
                                                          - -
                                                          + + + + {!this.state.showAdvancedModeSettings && ( + +
                                                          + {!activeSearch && showUserSettings && ( + + )} +
                                                          + +
                                                          +
                                                          +
                                                          + )} +
                                                          +
                                                          ) } @@ -115,8 +222,11 @@ const mapStateToProps = (state: any) => { getShowUserSettings(state) && (state.user.loggedInUser?.hasConsentedToTerms || getPersistenceMode(state.otp.config.persistence).isLocalStorage) + const { mainPanelContent } = state.otp.ui + return { activeSearch: getActiveSearch(state), + mainPanelContent, showUserSettings } } diff --git a/lib/components/app/print-layout.tsx b/lib/components/app/print-layout.tsx index d0365f015..7d8b2fd4e 100644 --- a/lib/components/app/print-layout.tsx +++ b/lib/components/app/print-layout.tsx @@ -1,65 +1,29 @@ -import { Button } from 'react-bootstrap' import { connect } from 'react-redux' import { FormattedMessage, injectIntl, IntlShape } from 'react-intl' import { Itinerary } from '@opentripplanner/types' -import { Map } from '@styled-icons/fa-solid/Map' -import { Print } from '@styled-icons/fa-solid/Print' -import { Times } from '@styled-icons/fa-solid/Times' -// @ts-expect-error not typescripted yet -import PrintableItinerary from '@opentripplanner/printable-itinerary' import React, { Component } from 'react' import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' -import { - addPrintViewClassToRootHtml, - clearClassFromRootHtml -} from '../../util/print' -import { ComponentContext } from '../../util/contexts' +import { AppReduxState } from '../../util/state-types' import { getActiveItinerary, getActiveSearch } from '../../util/state' -import { IconWithText } from '../util/styledIcon' import { summarizeQuery } from '../form/user-settings-i18n' +import { User } from '../user/types' import DefaultMap from '../map/default-map' -import PageTitle from '../util/page-title' -import SpanWithSpace from '../util/span-with-space' -import TripDetails from '../narrative/connected-trip-details' + +import TripPreviewLayoutBase from './trip-preview-layout-base' type Props = { // TODO: Typescript activeSearch type activeSearch: any - // TODO: Typescript config type - config: any - currentQuery: any intl: IntlShape itinerary: Itinerary location?: { search?: string } parseUrlQueryString: (params?: any, source?: string) => any - // TODO: Typescript user type - user: any -} - -type State = { - mapVisible?: boolean + user: User } -class PrintLayout extends Component { - static contextType = ComponentContext - - constructor(props: Props) { - super(props) - this.state = { - mapVisible: true - } - } - - _toggleMap = () => { - this.setState({ mapVisible: !this.state.mapVisible }) - } - - _print = () => { - window.print() - } - +class PrintLayout extends Component { _close = () => { window.location.replace(String(window.location).replace('print/', '')) } @@ -67,98 +31,45 @@ class PrintLayout extends Component { componentDidMount() { const { itinerary, location, parseUrlQueryString } = this.props - // Add print-view class to html tag to ensure that iOS scroll fix only applies - // to non-print views. - addPrintViewClassToRootHtml() // Parse the URL query parameters, if present if (!itinerary && location && location.search) { parseUrlQueryString() } - - // TODO: use currentQuery to pan/zoom to the correct part of the map - } - - componentWillUnmount() { - clearClassFromRootHtml() } render() { - const { activeSearch, config, intl, itinerary, user } = this.props - const { LegIcon } = this.context + const { activeSearch, intl, itinerary, user } = this.props const printVerb = intl.formatMessage({ id: 'common.forms.print' }) return ( -
                                                          - - {/* The header bar, including the Toggle Map and Print buttons */} -
                                                          -
                                                          - - - - - - - -
                                                          - -
                                                          - - {/* The map, if visible */} - {this.state.mapVisible && ( + } + itinerary={itinerary} + mapElement={
                                                          {/* FIXME: Improve reframing/setting map bounds when itinerary is received. */}
                                                          - )} - - {/* The main itinerary body */} - {itinerary && ( - <> - - - - )} -
                                                          + } + onClose={this._close} + subTitle={ + activeSearch && + summarizeQuery(activeSearch.query, intl, user.savedLocations) + } + title={printVerb} + /> ) } } // connect to the redux store -// TODO: Typescript state -const mapStateToProps = (state: any) => { +const mapStateToProps = (state: AppReduxState) => { const activeSearch = getActiveSearch(state) const { localUser, loggedInUser } = state.user const user = loggedInUser || localUser return { activeSearch, - config: state.otp.config, - currentQuery: state.otp.currentQuery, itinerary: getActiveItinerary(state) as Itinerary, user } diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index dc7d26f72..537e13bc3 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -33,6 +33,7 @@ import BeforeSignInScreen from '../user/before-signin-screen' import Map from '../map/map' import MobileMain from '../mobile/main' import printRoutes from '../../util/webapp-print-routes' +import tripPreviewRoutes from '../../util/webapp-trip-preview-routes' import webAppRoutes from '../../util/webapp-routes' import withLoggedInUserSupport from '../user/with-logged-in-user-support' import withMap from '../map/with-map' @@ -43,7 +44,7 @@ import SessionTimeout from './session-timeout' const { isMobile } = coreUtils.ui -const routes = [...webAppRoutes, ...printRoutes] +const routes = [...webAppRoutes, ...printRoutes, ...tripPreviewRoutes] class ResponsiveWebapp extends Component { static propTypes = { diff --git a/lib/components/app/trip-preview-layout-base.tsx b/lib/components/app/trip-preview-layout-base.tsx new file mode 100644 index 000000000..14fbf080b --- /dev/null +++ b/lib/components/app/trip-preview-layout-base.tsx @@ -0,0 +1,138 @@ +import { Button } from 'react-bootstrap' +import { connect } from 'react-redux' +import { FormattedMessage } from 'react-intl' +import { Itinerary } from '@opentripplanner/types' +import { Map } from '@styled-icons/fa-solid/Map' +import { Print } from '@styled-icons/fa-solid/Print' +import { Times } from '@styled-icons/fa-solid/Times' +// @ts-expect-error not typescripted yet +import PrintableItinerary from '@opentripplanner/printable-itinerary' +import React, { Component, ReactNode } from 'react' + +import { + addPrintViewClassToRootHtml, + clearClassFromRootHtml +} from '../../util/print' +import { AppConfig } from '../../util/config-types' +import { AppReduxState } from '../../util/state-types' +import { ComponentContext } from '../../util/contexts' +import { IconWithText } from '../util/styledIcon' +import PageTitle from '../util/page-title' +import SpanWithSpace from '../util/span-with-space' +import TripDetails from '../narrative/connected-trip-details' + +type Props = { + config: AppConfig + header?: ReactNode + itinerary?: Itinerary + mapElement?: ReactNode + onClose?: () => void + subTitle?: string + title: string +} + +type State = { + mapVisible?: boolean +} + +class TripPreviewLayoutBase extends Component { + static contextType = ComponentContext + + constructor(props: Props) { + super(props) + this.state = { + mapVisible: true + } + } + + _toggleMap = () => { + this.setState({ mapVisible: !this.state.mapVisible }) + } + + _print = () => { + window.print() + } + + componentDidUpdate() { + // Add print-view class to html tag to ensure that iOS scroll fix only applies + // to non-print views. + addPrintViewClassToRootHtml() + } + + componentWillUnmount() { + clearClassFromRootHtml() + } + + render() { + const { + config, + header, + itinerary, + mapElement, + onClose, + subTitle = '', + title + } = this.props + const { LegIcon } = this.context + + return ( +
                                                          + + {/* The header bar, including the Toggle Map and Print buttons */} +
                                                          +
                                                          + + + + + + + {onClose && ( + + )} +
                                                          + {header} +
                                                          + + {/* The map, if visible */} + {this.state.mapVisible && mapElement} + + {/* The main itinerary body */} + {itinerary && ( + <> + + + + )} +
                                                          + ) + } +} + +// connect to the redux store + +const mapStateToProps = (state: AppReduxState) => ({ + config: state.otp.config +}) + +export default connect(mapStateToProps)(TripPreviewLayoutBase) diff --git a/lib/components/app/trip-preview-layout.tsx b/lib/components/app/trip-preview-layout.tsx new file mode 100644 index 000000000..f00ff4d4c --- /dev/null +++ b/lib/components/app/trip-preview-layout.tsx @@ -0,0 +1,73 @@ +import { connect } from 'react-redux' +import { RouteComponentProps } from 'react-router' +import { useIntl } from 'react-intl' +import { withAuthenticationRequired } from '@auth0/auth0-react' +import React from 'react' +import styled from 'styled-components' + +import { AppReduxState } from '../../util/state-types' +import { MonitoredTrip } from '../user/types' +import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui' +import SimpleMap from '../map/simple-map' +import withLoggedInUserSupport from '../user/with-logged-in-user-support' + +import TripPreviewLayoutBase from './trip-preview-layout-base' + +type Props = { + monitoredTrip?: MonitoredTrip +} + +const MapContainer = styled.div` + height: 100%; + width: 100%; + + .map { + height: 100%; + width: 100%; + } +` + +const TripPreviewLayout = ({ monitoredTrip }: Props) => { + const intl = useIntl() + const previewTripText = intl.formatMessage({ + id: 'components.TripPreviewLayout.previewTrip' + }) + const itinerary = + monitoredTrip?.journeyState?.matchingItinerary || monitoredTrip?.itinerary + + return ( + + + + } + subTitle={monitoredTrip?.tripName} + title={previewTripText} + /> + ) +} + +// connect to the redux store + +const mapStateToProps = ( + state: AppReduxState, + ownProps: Props & RouteComponentProps<{ id: string }> +) => { + const { loggedInUserMonitoredTrips: trips } = state.user + const tripId = ownProps.match.params.id + + return { + monitoredTrip: trips?.find((trip) => trip.id === tripId) + } +} + +export default withLoggedInUserSupport( + withAuthenticationRequired( + connect(mapStateToProps)(TripPreviewLayout), + RETURN_TO_CURRENT_ROUTE + ), + true +) diff --git a/lib/components/form/advanced-settings-button.tsx b/lib/components/form/advanced-settings-button.tsx new file mode 100644 index 000000000..0449dc85d --- /dev/null +++ b/lib/components/form/advanced-settings-button.tsx @@ -0,0 +1,29 @@ +import { ArrowRight } from '@styled-icons/fa-solid' +import { FormattedMessage } from 'react-intl' + +import { grey } from '../util/colors' +import React from 'react' +import styled from 'styled-components' + +interface Props { + onClick: () => void +} + +const StyledTransparentButton = styled.button` + align-items: center; + background: transparent; + border: none; + color: ${grey[800]}; + display: flex; + gap: 7px; + margin-bottom: 5px; +` + +const AdvancedSettingsButton = ({ onClick }: Props): JSX.Element => ( + + + + +) + +export default AdvancedSettingsButton diff --git a/lib/components/form/advanced-settings-panel.tsx b/lib/components/form/advanced-settings-panel.tsx new file mode 100644 index 000000000..ea085d017 --- /dev/null +++ b/lib/components/form/advanced-settings-panel.tsx @@ -0,0 +1,285 @@ +import { + addSettingsToButton, + AdvancedModeSubsettingsContainer, + ModeSettingRenderer, + populateSettingWithValue +} from '@opentripplanner/trip-form' +import { ArrowLeft } from '@styled-icons/fa-solid/ArrowLeft' +import { Check } from '@styled-icons/boxicons-regular' +import { connect } from 'react-redux' +import { decodeQueryParams, DelimitedArrayParam } from 'serialize-query-params' +import { FormattedMessage, useIntl } from 'react-intl' +import { invisibleCss } from '@opentripplanner/trip-form/lib/MetroModeSelector' +import { + ModeButtonDefinition, + ModeSetting, + ModeSettingValues +} from '@opentripplanner/types' +import React, { RefObject, useCallback, useContext, useState } from 'react' +import styled from 'styled-components' + +import * as formActions from '../../actions/form' +import { AppReduxState } from '../../util/state-types' +import { blue, getBaseColor } from '../util/colors' +import { ComponentContext } from '../../util/contexts' +import { generateModeSettingValues } from '../../util/api' + +import { + addCustomSettingLabels, + addModeButtonIcon, + onSettingsUpdate, + pipe, + populateSettingWithIcon, + setModeButton +} from './util' +import { setModeButtonEnabled } from './batch-settings' +import { styledCheckboxCss } from './styled' +import DateTimeModal from './date-time-modal' + +const PanelOverlay = styled.div` + height: 100%; + left: 0; + overflow-y: auto; + padding: 1.5em; + position: absolute; + top: 0; + width: 100%; + z-index: 100; +` + +const GlobalSettingsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 13px; + margin-bottom: 2em; + + ${styledCheckboxCss} +` + +const CloseButton = styled.button` + background: transparent; + border: none; +` + +const HeaderContainer = styled.div` + align-items: center; + display: flex; + gap: 10px; + height: 30px; +` + +const Subheader = styled.h2` + ${invisibleCss} +` + +const ReturnToTripPlanButton = styled.button` + align-items: center; + background-color: var(--main-base-color, ${blue[900]}); + border: 0; + color: white; + display: flex; + font-weight: 700; + gap: 5px; + height: 51px; + justify-content: center; + margin-top: 2em; + width: 100%; + + svg { + margin-bottom: 7px; + } +` + +const DtSelectorContainer = styled.div` + margin: 2em 0; + + .date-time-modal { + padding: 0; + + .main-panel { + margin: 0; + + button { + padding: 6px 0; + } + + .date-time-selector { + margin: 15px 0; + } + } + } +` + +const AdvancedSettingsPanel = ({ + closeAdvancedSettings, + enabledModeButtons, + innerRef, + modeButtonOptions, + modeSettingDefinitions, + modeSettingValues, + saveAndReturnButton, + setCloseAdvancedSettingsWithDelay, + setQueryParam +}: { + closeAdvancedSettings: () => void + enabledModeButtons: string[] + innerRef: RefObject + modeButtonOptions: ModeButtonDefinition[] + modeSettingDefinitions: ModeSetting[] + modeSettingValues: ModeSettingValues + saveAndReturnButton?: boolean + setCloseAdvancedSettingsWithDelay: () => void + setQueryParam: (evt: any) => void +}): JSX.Element => { + const [closingBySave, setClosingBySave] = useState(false) + const baseColor = getBaseColor() + const accentColor = baseColor || blue[900] + + const intl = useIntl() + const closeButtonText = intl.formatMessage({ + id: 'components.BatchSearchScreen.saveAndReturn' + }) + const headerText = intl.formatMessage({ + id: 'components.BatchSearchScreen.advancedHeader' + }) + + // @ts-expect-error Context not typed + const { ModeIcon } = useContext(ComponentContext) + + const processSettings = (settings: ModeSetting[]) => + settings.map( + pipe( + populateSettingWithIcon(ModeIcon), + populateSettingWithValue(modeSettingValues), + addCustomSettingLabels(intl) + ) + ) + + const globalSettings = modeSettingDefinitions.filter((x) => !x.applicableMode) + const processedGlobalSettings = processSettings(globalSettings) + + const globalSettingsComponents = processedGlobalSettings.map( + (setting: ModeSetting) => ( + + ) + ) + + const processedModeSettings = processSettings(modeSettingDefinitions) + const processedModeButtons = modeButtonOptions.map( + pipe( + addModeButtonIcon(ModeIcon), + addSettingsToButton(processedModeSettings), + setModeButtonEnabled(enabledModeButtons) + ) + ) + + const onSaveAndReturnClick = useCallback(async () => { + await setCloseAdvancedSettingsWithDelay() + setClosingBySave(true) + closeAdvancedSettings() + }, [closeAdvancedSettings, setCloseAdvancedSettingsWithDelay]) + + return ( + + + { + closeAdvancedSettings() + }} + title={closeButtonText} + > + + +

                                                          {headerText}

                                                          +
                                                          + + + + {processedGlobalSettings.length > 0 && ( + <> + + + + + {globalSettingsComponents} + + + )} + + + + + {saveAndReturnButton && ( + + {closingBySave ? ( + <> + + + + ) : ( + + )} + + )} +
                                                          + ) +} + +const queryParamConfig = { modeButtons: DelimitedArrayParam } + +const mapStateToProps = (state: AppReduxState) => { + const urlSearchParams = new URLSearchParams(state.router.location.search) + const { modes } = state.otp.config + const modeSettingValues = generateModeSettingValues( + urlSearchParams, + state.otp.modeSettingDefinitions || [], + modes?.initialState?.modeSettingValues || {} + ) + const saveAndReturnButton = + state.otp.config?.advancedSettingsPanel?.saveAndReturnButton + return { + currentQuery: state.otp.currentQuery, + // TODO: Duplicated in apiv2.js + enabledModeButtons: + decodeQueryParams(queryParamConfig, { + modeButtons: urlSearchParams.get('modeButtons') + })?.modeButtons?.filter((mb): mb is string => mb !== null) || + modes?.initialState?.enabledModeButtons || + [], + modeButtonOptions: modes?.modeButtons || [], + modeSettingDefinitions: state.otp?.modeSettingDefinitions || [], + modeSettingValues, + saveAndReturnButton + } +} + +const mapDispatchToProps = { + setQueryParam: formActions.setQueryParam, + updateQueryTimeIfLeavingNow: formActions.updateQueryTimeIfLeavingNow +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AdvancedSettingsPanel) diff --git a/lib/components/form/batch-settings.tsx b/lib/components/form/batch-settings.tsx index 63e748973..23bd13a6c 100644 --- a/lib/components/form/batch-settings.tsx +++ b/lib/components/form/batch-settings.tsx @@ -1,19 +1,7 @@ -import { - addSettingsToButton, - MetroModeSelector, - populateSettingWithValue -} from '@opentripplanner/trip-form' import { connect } from 'react-redux' -import { - decodeQueryParams, - DelimitedArrayParam, - encodeQueryParams -} from 'use-query-params' -import { - ModeButtonDefinition, - ModeSetting, - ModeSettingValues -} from '@opentripplanner/types' +import { decodeQueryParams } from 'use-query-params' +import { MetroModeSelector } from '@opentripplanner/trip-form' +import { ModeButtonDefinition } from '@opentripplanner/types' import { Search } from '@styled-icons/fa-solid/Search' import { SyncAlt } from '@styled-icons/fa-solid/SyncAlt' import { useIntl } from 'react-intl' @@ -22,22 +10,26 @@ import React, { useCallback, useContext, useState } from 'react' import * as apiActions from '../../actions/api' import * as formActions from '../../actions/form' import { ComponentContext } from '../../util/contexts' -import { generateModeSettingValues } from '../../util/api' import { getActiveSearch, hasValidLocation } from '../../util/state' import { getBaseColor, getDarkenedBaseColor } from '../util/colors' -import { getFormattedMode } from '../../util/i18n' -import { RoutingQueryCallResult } from '../../actions/api-constants' import { StyledIconWrapper } from '../util/styledIcon' +import { + addModeButtonIcon, + alertUserTripPlan, + modesQueryParamConfig, + onSettingsUpdate, + pipe, + setModeButton +} from './util' import { MainSettingsRow, ModeSelectorContainer, PlanTripButton } from './batch-styled' +import AdvancedSettingsButton from './advanced-settings-button' import DateTimeButton from './date-time-button' -const queryParamConfig = { modeButtons: DelimitedArrayParam } - // TYPESCRIPT TODO: better types type Props = { activeSearch: any @@ -45,18 +37,11 @@ type Props = { enabledModeButtons: string[] fillModeIcons?: boolean modeButtonOptions: ModeButtonDefinition[] - modeSettingDefinitions: ModeSetting[] - modeSettingValues: ModeSettingValues onPlanTripClick: () => void + openAdvancedSettings: () => void routingQuery: any setQueryParam: (evt: any) => void spacedOutModeSelector?: boolean - updateQueryTimeIfLeavingNow: () => void -} - -// This method is used to daisy-chain a series of functions together on a given value -function pipe(...fns: Array<(arg: T) => T>) { - return (value: T) => fns.reduce((acc, fn) => fn(acc), value) } export function setModeButtonEnabled(enabledKeys: string[]) { @@ -77,174 +62,37 @@ function BatchSettings({ enabledModeButtons, fillModeIcons, modeButtonOptions, - modeSettingDefinitions, - modeSettingValues, onPlanTripClick, + openAdvancedSettings, routingQuery, setQueryParam, - spacedOutModeSelector, - updateQueryTimeIfLeavingNow + spacedOutModeSelector }: Props) { const intl = useIntl() // Whether the date/time selector is open const [dateTimeOpen, setDateTimeOpen] = useState(false) - // Whether the mode selector has a popup open - const [modeSelectorPopup, setModeSelectorPopup] = useState(false) - // @ts-expect-error Context not typed const { ModeIcon } = useContext(ComponentContext) - const addModeButtonIcon = useCallback( - (def: ModeButtonDefinition) => ({ - ...def, - Icon: function ModeButtonIcon() { - return - } - }), - [ModeIcon] - ) - - const populateSettingWithIcon = useCallback( - (msd: ModeSetting) => ({ - ...msd, - icon: - }), - [ModeIcon] - ) - - const addCustomSettingLabels = useCallback( - (msd: ModeSetting) => { - let modeLabel - // If we're using route mode overrides, make sure we're using the custom mode name - if (msd.type === 'SUBMODE') { - modeLabel = msd.overrideMode || msd.addTransportMode.mode - return { - ...msd, - label: getFormattedMode(modeLabel, intl) - } - } - return msd - }, - [intl] - ) - - const processedModeSettings = modeSettingDefinitions.map( - pipe( - populateSettingWithIcon, - populateSettingWithValue(modeSettingValues), - addCustomSettingLabels - ) - ) - const processedModeButtons = modeButtonOptions.map( - pipe( - addModeButtonIcon, - addSettingsToButton(processedModeSettings), - setModeButtonEnabled(enabledModeButtons) - ) + pipe(addModeButtonIcon(ModeIcon), setModeButtonEnabled(enabledModeButtons)) ) const _planTrip = useCallback(() => { - // Check for any validation issues in query. - const issues = [] - if (!hasValidLocation(currentQuery, 'from')) { - issues.push(intl.formatMessage({ id: 'components.BatchSettings.origin' })) - } - if (!hasValidLocation(currentQuery, 'to')) { - issues.push( - intl.formatMessage({ id: 'components.BatchSettings.destination' }) - ) - } - onPlanTripClick && onPlanTripClick() - if (issues.length > 0) { - // TODO: replace with less obtrusive validation. - window.alert( - intl.formatMessage( - { id: 'components.BatchSettings.validationMessage' }, - { issues: intl.formatList(issues, { type: 'conjunction' }) } - ) - ) - return - } - - // Plan trip. - updateQueryTimeIfLeavingNow() - const routingQueryResult = routingQuery() - - // If mode combination is not valid (i.e. produced no query), alert the user. - if (routingQueryResult === RoutingQueryCallResult.INVALID_MODE_SELECTION) { - window.alert( - intl.formatMessage({ - id: 'components.BatchSettings.invalidModeSelection' - }) - ) - } - }, [ - currentQuery, - intl, - onPlanTripClick, - routingQuery, - updateQueryTimeIfLeavingNow - ]) - - /** - * Stores parameters in both the Redux `currentQuery` and URL - * @param params Params to store - */ - const _onSettingsUpdate = useCallback( - (params: any) => { - setQueryParam({ queryParamData: params, ...params }) - }, - [setQueryParam] - ) - - const _toggleModeButton = useCallback( - (buttonId: string, newState: boolean) => { - let newButtons - if (newState) { - newButtons = [...enabledModeButtons, buttonId] - } else { - newButtons = enabledModeButtons.filter((c) => c !== buttonId) - } - - // encodeQueryParams serializes the mode buttons for the URL - // to get nice looking URL params and consistency - _onSettingsUpdate( - encodeQueryParams(queryParamConfig, { modeButtons: newButtons }) - ) - }, - [enabledModeButtons, _onSettingsUpdate] - ) - - /** - * Check whether the mode selector is showing a popup. - */ - const checkModeSelectorPopup = useCallback(() => { - const modeSelectorPopup = document.querySelector( - '.metro-mode-selector div[role="dialog"]' - ) - setModeSelectorPopup(!!modeSelectorPopup) - }, [setModeSelectorPopup]) + alertUserTripPlan(intl, currentQuery, onPlanTripClick, routingQuery) + }, [currentQuery, intl, onPlanTripClick, routingQuery]) const baseColor = getBaseColor() const accentColor = getDarkenedBaseColor() return ( - - - + + + + { const urlSearchParams = new URLSearchParams(state.router.location.search) - const modeSettingValues = generateModeSettingValues( - urlSearchParams, - state.otp?.modeSettingDefinitions || [], - state.otp.config.modes?.initialState?.modeSettingValues - ) + const { modes } = state.otp.config return { activeSearch: getActiveSearch(state), currentQuery: state.otp.currentQuery, // TODO: Duplicated in apiv2.js enabledModeButtons: - decodeQueryParams(queryParamConfig, { + decodeQueryParams(modesQueryParamConfig, { modeButtons: urlSearchParams.get('modeButtons') })?.modeButtons || - state.otp.config?.modes?.initialState?.enabledModeButtons || + modes?.initialState?.enabledModeButtons || {}, fillModeIcons: state.otp.config.itinerary?.fillModeIcons, - modeButtonOptions: state.otp.config?.modes?.modeButtons || [], - modeSettingDefinitions: state.otp?.modeSettingDefinitions || [], - modeSettingValues, - spacedOutModeSelector: state.otp?.config?.modes?.spacedOut + modeButtonOptions: modes?.modeButtons || [], + spacedOutModeSelector: modes?.spacedOut } } const mapDispatchToProps = { routingQuery: apiActions.routingQuery, - setQueryParam: formActions.setQueryParam, - updateQueryTimeIfLeavingNow: formActions.updateQueryTimeIfLeavingNow + setQueryParam: formActions.setQueryParam } export default connect(mapStateToProps, mapDispatchToProps)(BatchSettings) diff --git a/lib/components/form/batch-styled.ts b/lib/components/form/batch-styled.ts index 2283f3b9e..4fe5d9da5 100644 --- a/lib/components/form/batch-styled.ts +++ b/lib/components/form/batch-styled.ts @@ -65,6 +65,8 @@ export const ModeSelectorContainer = styled.div<{ squashed?: boolean }>` align-items: flex-start; display: flex; float: right; + justify-content: space-between; + width: 100%; ${PlanTripButton} { border-bottom-left-radius: ${(props) => (props.squashed ? 0 : 'invalid')}; diff --git a/lib/components/form/date-time-modal.js b/lib/components/form/date-time-modal.js deleted file mode 100644 index 0936ebbe9..000000000 --- a/lib/components/form/date-time-modal.js +++ /dev/null @@ -1,76 +0,0 @@ -// TODO: TypeScript with props. -/* eslint-disable react/prop-types */ -import { connect } from 'react-redux' -import coreUtils from '@opentripplanner/core-utils' -import PropTypes from 'prop-types' -import React, { Component } from 'react' - -import { setQueryParam } from '../../actions/form' - -import { StyledDateTimeSelector } from './styled' - -class DateTimeModal extends Component { - static propTypes = { - setQueryParam: PropTypes.func - } - - render() { - const { - config, - date, - dateFormatLegacy, - departArrive, - setQueryParam, - time, - timeFormatLegacy - } = this.props - const { homeTimezone, isTouchScreenOnDesktop } = config - const touchClassName = isTouchScreenOnDesktop - ? 'with-desktop-touchscreen' - : '' - - return ( -
                                                          -
                                                          - `. - // These props are not relevant in modern browsers, - // where `` already - // formats the time|date according to the OS settings. - // eslint-disable-next-line react/jsx-sort-props - timeFormatLegacy={timeFormatLegacy} - timeZone={homeTimezone} - /> -
                                                          -
                                                          - ) - } -} - -const mapStateToProps = (state) => { - const { date, departArrive, time } = state.otp.currentQuery - const config = state.otp.config - return { - config, - date, - // This prop is for legacy browsers (see render method above). - dateFormatLegacy: coreUtils.time.getDateFormat(config), - departArrive, - time, - // This prop is for legacy browsers (see render method above). - timeFormatLegacy: coreUtils.time.getTimeFormat(config) - } -} - -const mapDispatchToProps = { - setQueryParam -} - -export default connect(mapStateToProps, mapDispatchToProps)(DateTimeModal) diff --git a/lib/components/form/date-time-modal.tsx b/lib/components/form/date-time-modal.tsx new file mode 100644 index 000000000..e9f31407b --- /dev/null +++ b/lib/components/form/date-time-modal.tsx @@ -0,0 +1,116 @@ +import { connect } from 'react-redux' +import coreUtils from '@opentripplanner/core-utils' +import React, { useCallback } from 'react' + +import * as formActions from '../../actions/form' +import * as narrativeActions from '../../actions/narrative' +import { AppConfig } from '../../util/config-types' +import { AppReduxState, FilterType, SortType } from '../../util/state-types' + +import { StyledDateTimeSelector } from './styled' + +type Props = { + config: AppConfig + date: string + dateFormatLegacy?: string + departArrive: DepartArriveValue + setQueryParam: (params: any) => void + sort: SortType + time: string + timeFormatLegacy?: string + updateItineraryFilter: (payload: FilterType) => void +} + +type DepartArriveValue = 'NOW' | 'DEPART' | 'ARRIVE' + +const DepartArriveTypeMap: Record< + DepartArriveValue, + FilterType['sort']['type'] +> = { + ARRIVE: 'ARRIVALTIME', + DEPART: 'DEPARTURETIME', + NOW: 'DURATION' +} + +function DateTimeModal({ + config, + date, + dateFormatLegacy, + departArrive, + setQueryParam, + sort, + time, + timeFormatLegacy, + updateItineraryFilter +}: Props) { + const { homeTimezone, isTouchScreenOnDesktop } = config + const touchClassName = isTouchScreenOnDesktop + ? 'with-desktop-touchscreen' + : '' + + const syncSortWithDepartArrive = config?.itinerary?.syncSortWithDepartArrive + // Note the side effect that this will resort the results of a previous query + // if the user changes the depart/arrive setting before the query is run. + const setQueryParamMiddleware = useCallback( + (params: any) => { + if (syncSortWithDepartArrive) { + updateItineraryFilter({ + sort: { + ...sort, + type: DepartArriveTypeMap[params.departArrive as DepartArriveValue] + } + }) + } + setQueryParam(params) + }, + [setQueryParam, updateItineraryFilter, sort, syncSortWithDepartArrive] + ) + return ( +
                                                          +
                                                          + `. + // These props are not relevant in modern browsers, + // where `` already + // formats the time|date according to the OS settings. + // eslint-disable-next-line react/jsx-sort-props + timeFormatLegacy={timeFormatLegacy} + timeZone={homeTimezone} + /> +
                                                          +
                                                          + ) +} + +const mapStateToProps = (state: AppReduxState) => { + const { date, departArrive, time } = state.otp.currentQuery + const config = state.otp.config + const { sort } = state.otp.filter + return { + config, + date, + // This prop is for legacy browsers (see render method above). + // @ts-expect-error Mismatched config types + dateFormatLegacy: coreUtils.time.getDateFormat(config), + departArrive, + sort, + time, + // This prop is for legacy browsers (see render method above). + // @ts-expect-error Mismatched config types + timeFormatLegacy: coreUtils.time.getTimeFormat(config) + } +} + +const mapDispatchToProps = { + setQueryParam: formActions.setQueryParam, + updateItineraryFilter: narrativeActions.updateItineraryFilter +} + +export default connect(mapStateToProps, mapDispatchToProps)(DateTimeModal) diff --git a/lib/components/form/form.css b/lib/components/form/form.css index 7ef399d67..9f694e155 100644 --- a/lib/components/form/form.css +++ b/lib/components/form/form.css @@ -93,14 +93,14 @@ position: absolute; top: 22px; right: 32px; - z-index: 100000; + z-index: 99; } .otp .switch-button-container-mobile { position: absolute; top: 32px; right: 45px; - z-index: 100000; + z-index: 99; } /* Settings Selector Panel */ diff --git a/lib/components/form/styled.ts b/lib/components/form/styled.ts index 06e9449f7..5033db42f 100644 --- a/lib/components/form/styled.ts +++ b/lib/components/form/styled.ts @@ -5,6 +5,7 @@ import { Styled as TripFormClasses } from '@opentripplanner/trip-form' import { Input, MenuItemLi } from '@opentripplanner/location-field/lib/styled' +import { prefersReducedMotion } from '../util/prefersReducedMotion' import LocationField from '@opentripplanner/location-field' import styled, { css } from 'styled-components' @@ -230,3 +231,77 @@ export const StyledLocationField = styled(LocationField)` } } ` + +export const advancedPanelClassName = 'advanced-panel' +export const mainPanelClassName = 'main-panel' +export const transitionDuration = prefersReducedMotion ? 0 : 175 + +const wipeOffset = 7 + +const transitionMixin = css` + transition: all ${transitionDuration}ms ease-in-out; +` + +const wipeOutMixin = (offset: number) => css` + opacity: 0; + transform: translateX(${offset}px); +` +const wipeInMixin = css` + opacity: 1; +` + +export const TransitionStyles = styled.div<{ transitionDelay: number }>` + display: contents; + .${advancedPanelClassName}-enter { + ${wipeOutMixin(wipeOffset)} + } + .${advancedPanelClassName}-enter-done { + ${wipeInMixin} + ${transitionMixin} + } + + .${advancedPanelClassName}-exit { + ${wipeInMixin} + } + + .${advancedPanelClassName}-exit-active { + ${wipeOutMixin(wipeOffset)} + ${transitionMixin} + transition-delay: ${(props) => props.transitionDelay}ms; + } + + .${mainPanelClassName}-enter { + ${wipeOutMixin(-wipeOffset)} + } + .${mainPanelClassName}-enter-done { + ${wipeInMixin} + ${transitionMixin} + } + + .${mainPanelClassName}-exit { + ${wipeInMixin} + } + + .${mainPanelClassName}-exit-active { + ${wipeOutMixin(-wipeOffset)} + ${transitionMixin} + } +` +export const styledCheckboxCss = css` + div { + align-items: center; + justify-content: space-between; + + label { + margin-bottom: 0; + } + input[type='checkbox'] { + margin-top: 0; + order: 2; + + &:focus-visible + label { + outline: 1px solid blue; + } + } + } +` diff --git a/lib/components/form/util.tsx b/lib/components/form/util.tsx new file mode 100644 index 000000000..85dd3107d --- /dev/null +++ b/lib/components/form/util.tsx @@ -0,0 +1,115 @@ +import { DelimitedArrayParam, encodeQueryParams } from 'serialize-query-params' +import { IntlShape } from 'react-intl' +import { ModeButtonDefinition, ModeSetting } from '@opentripplanner/types' +import React from 'react' + +import { getFormattedMode } from '../../util/i18n' +import { hasValidLocation } from '../../util/state' +import { QueryParamChangeHandler } from '../util/types' +import { RoutingQueryCallResult } from '../../actions/api-constants' +import { updateQueryTimeIfLeavingNow } from '../../actions/form' + +// This method is used to daisy-chain a series of functions together on a given value +export function pipe(...fns: Array<(arg: T) => T>) { + return (value: T) => fns.reduce((acc, fn) => fn(acc), value) +} + +export const modesQueryParamConfig = { modeButtons: DelimitedArrayParam } + +export const populateSettingWithIcon = + (ModeIcon: React.ComponentType<{ mode?: string; width?: number }>) => + // eslint-disable-next-line react/display-name + (modeSetting: ModeSetting): ModeSetting => ({ + ...modeSetting, + icon: + }) + +export const addModeButtonIcon = + (ModeIcon: React.ComponentType<{ mode?: string; width?: number }>) => + (def: ModeButtonDefinition): ModeButtonDefinition => ({ + ...def, + Icon: function ModeButtonIcon() { + return + } + }) + +export const addCustomSettingLabels = + (intl: IntlShape) => + (modeSetting: ModeSetting): ModeSetting => { + // If we're using route mode overrides, make sure we're using the custom mode name + if (modeSetting.type === 'SUBMODE') { + const modeLabel = + modeSetting.overrideMode || modeSetting.addTransportMode.mode + return { + ...modeSetting, + label: getFormattedMode(modeLabel, intl) + } + } + return modeSetting + } + +/** + * Stores parameters in both the Redux `currentQuery` and URL + * @param params Params to store + */ +export const onSettingsUpdate = + (setQueryParam: QueryParamChangeHandler) => + (params: any): void => { + setQueryParam({ queryParamData: params, ...params }) + } + +export const setModeButton = + (enabledModeButtons: string[], updateHandler: (params: any) => void) => + (buttonId: string, newState: boolean): void => { + const newButtons = newState + ? [...enabledModeButtons, buttonId] + : enabledModeButtons.filter((c) => c !== buttonId) + + // encodeQueryParams serializes the mode buttons for the URL + // to get nice looking URL params and consistency + updateHandler( + encodeQueryParams(modesQueryParamConfig, { modeButtons: newButtons }) + ) + } + +export const alertUserTripPlan = ( + intl: IntlShape, + currentQuery: any, + onPlanTripClick: () => void, + routingQuery: () => any +): void => { + // Check for any validation issues in query. + const issues: string[] = [] + if (!hasValidLocation(currentQuery, 'from')) { + issues.push(intl.formatMessage({ id: 'components.BatchSettings.origin' })) + } + if (!hasValidLocation(currentQuery, 'to')) { + issues.push( + intl.formatMessage({ id: 'components.BatchSettings.destination' }) + ) + } + onPlanTripClick() + if (issues.length > 0) { + // TODO: replace with less obtrusive validation. + window.alert( + intl.formatMessage( + { id: 'components.BatchSettings.validationMessage' }, + { issues: intl.formatList(issues, { type: 'conjunction' }) } + ) + ) + return + } + + // Plan trip. + updateQueryTimeIfLeavingNow() + const routingQueryResult = routingQuery() + + // If mode combination is not valid (i.e. produced no query), alert the user. + if (routingQueryResult === RoutingQueryCallResult.INVALID_MODE_SELECTION) { + window.alert( + intl.formatMessage({ + id: 'components.BatchSettings.invalidModeSelection' + }) + ) + } +} diff --git a/lib/components/map/connected-transitive-overlay.tsx b/lib/components/map/connected-transitive-overlay.tsx index 71af754b8..7ed9d628c 100644 --- a/lib/components/map/connected-transitive-overlay.tsx +++ b/lib/components/map/connected-transitive-overlay.tsx @@ -2,19 +2,14 @@ import { connect } from 'react-redux' import { injectIntl, IntlShape } from 'react-intl' import TransitiveCanvasOverlay from '@opentripplanner/transitive-overlay' +import { AppReduxState } from '../../util/state-types' import { getActiveLeg, getTransitiveData } from '../../util/state' +import { TransitiveConfig } from '../../util/config-types' -type Props = { - intl?: IntlShape - labeledModes?: string[] - styles?: { - labels: Record - segmentLabels: Record - } -} +type Props = TransitiveConfig & IntlShape // connect to the redux store -const mapStateToProps = (state: Record, ownProps: Props) => { +const mapStateToProps = (state: AppReduxState, ownProps: Props) => { const { labeledModes, styles } = state.otp.config.map.transitive || {} const { viewedRoute } = state.otp.ui diff --git a/lib/components/map/default-map.tsx b/lib/components/map/default-map.tsx index 6c50d1899..b2e7b899f 100644 --- a/lib/components/map/default-map.tsx +++ b/lib/components/map/default-map.tsx @@ -3,6 +3,7 @@ // @ts-nocheck import { connect } from 'react-redux' import { GeolocateControl, NavigationControl } from 'react-map-gl' +import { getCurrentDate } from '@opentripplanner/core-utils/lib/time' import { injectIntl } from 'react-intl' import BaseMap from '@opentripplanner/base-map' import generateOTP2TileLayers from '@opentripplanner/otp2-tile-overlay' @@ -13,6 +14,7 @@ import { assembleBasePath, bikeRentalQuery, carRentalQuery, + findStopTimesForStop, vehicleRentalQuery } from '../../actions/api' import { ComponentContext } from '../../util/contexts' @@ -22,6 +24,7 @@ import { MainPanelContent } from '../../actions/ui-constants' import { setLocation, setMapPopupLocationAndGeocode } from '../../actions/map' import { setViewedStop } from '../../actions/ui' import { updateOverlayVisibility } from '../../actions/config' +import TransitOperatorIcons from '../util/connected-transit-operator-icons' import ElevationPointMarker from './elevation-point-marker' import EndpointsOverlay from './connected-endpoints-overlay' @@ -153,6 +156,17 @@ class DefaultMap extends Component { } } + // Generate operator logos to pass through OTP tile layer to map-popup + getEntityPrefix = (entity) => { + const stopId = entity.gtfsId + this.props.findStopTimesForStop({ + date: getCurrentDate(), + onlyRequestForOperators: true, + stopId + }) + return + } + /** * Checks whether the modes have changed between old and new queries and * whether to update the map overlays accordingly (e.g., to show rental vehicle @@ -267,7 +281,8 @@ class DefaultMap extends Component { setLocation, setViewedStop, vehicleRentalQuery, - vehicleRentalStations + vehicleRentalStations, + viewedRouteStops } = this.props const { getCustomMapOverlays, getTransitiveRouteLabel, ModeIcon } = this.context @@ -405,8 +420,9 @@ class DefaultMap extends Component { vectorTilesEndpoint, setLocation, setViewedStop, - null, - config.companies + viewedRouteStops, + config.companies, + this.getEntityPrefix ) default: return null @@ -428,6 +444,26 @@ class DefaultMap extends Component { const mapStateToProps = (state) => { const activeSearch = getActiveSearch(state) + const viewedRoute = state.otp?.ui?.viewedRoute?.routeId + const nearbyViewerActive = + state.otp.ui.mainPanelContent === MainPanelContent.NEARBY_VIEW + + const viewedRoutePatterns = Object.entries( + state.otp?.transitIndex?.routes?.[viewedRoute]?.patterns || {} + ) + const viewedRouteStops = + viewedRoute && !nearbyViewerActive + ? // Ensure we don't have duplicates + Array.from( + new Set( + // Generate a list of every stop id the pattern stops at + viewedRoutePatterns.reduce((acc, cur) => { + // Convert pattern object to list of the pattern's stops + return [...cur?.[1]?.stops.map((s) => s.id), ...acc] + }, []) + ) + ) + : null return { bikeRentalStations: state.otp.overlay.bikeRental.stations, @@ -439,13 +475,15 @@ const mapStateToProps = (state) => { state.otp.ui.mainPanelContent === MainPanelContent.NEARBY_VIEW, pending: activeSearch ? Boolean(activeSearch.pending) : false, query: state.otp.currentQuery, - vehicleRentalStations: state.otp.overlay.vehicleRental.stations + vehicleRentalStations: state.otp.overlay.vehicleRental.stations, + viewedRouteStops } } const mapDispatchToProps = { bikeRentalQuery, carRentalQuery, + findStopTimesForStop, getCurrentPosition, setLocation, setMapPopupLocationAndGeocode, diff --git a/lib/components/map/point-popup.tsx b/lib/components/map/point-popup.tsx index d7121b34b..071901434 100644 --- a/lib/components/map/point-popup.tsx +++ b/lib/components/map/point-popup.tsx @@ -82,7 +82,7 @@ function MapPopup({ // Override inline style supplied by react-map-gl to accommodate long "plan a trip" translations. style={{ maxWidth: '260px', width: '260px' }} > - + {typeof popupName === 'string' && popupName.split(',').length > 3 diff --git a/lib/components/map/simple-map.tsx b/lib/components/map/simple-map.tsx new file mode 100644 index 000000000..f9312a9a4 --- /dev/null +++ b/lib/components/map/simple-map.tsx @@ -0,0 +1,76 @@ +import { connect } from 'react-redux' +import { GeolocateControl, NavigationControl } from 'react-map-gl' +import { Itinerary } from '@opentripplanner/types' +import { useIntl } from 'react-intl' +import BaseMap from '@opentripplanner/base-map' +import EndpointsOverlay from '@opentripplanner/endpoints-overlay' +import React, { useContext } from 'react' +import TransitiveOverlay, { + itineraryToTransitive +} from '@opentripplanner/transitive-overlay' + +import { AppConfig } from '../../util/config-types' +import { AppReduxState } from '../../util/state-types' +import { ComponentContext } from '../../util/contexts' + +interface Props { + config: AppConfig + itinerary?: Itinerary +} + +/** Renders an optional itinerary with a given config. */ +const SimpleMap = ({ config, itinerary }: Props): JSX.Element => { + const intl = useIntl() + // @ts-expect-error ComponentContext not typed yet. + const { getTransitiveRouteLabel } = useContext(ComponentContext) + const { + baseLayers, + initLat = 0, + initLon = 0, + initZoom, + maxZoom, + navigationControlPosition = 'bottom-right', + transitive + } = config.map || {} + const baseLayerUrls = baseLayers?.map((bl) => bl.url) + const { disableFlexArc } = transitive || {} + const { legs = [] } = itinerary || {} + + return ( + 1 ? baseLayerUrls : baseLayerUrls?.[0] + } + center={[initLat, initLon]} + mapLibreProps={{ reuseMaps: true }} + maxZoom={maxZoom} + zoom={initZoom} + > + + + {itinerary && ( + + )} + + + + ) +} + +// connect to the redux store + +const mapStateToProps = (state: AppReduxState) => ({ + config: state.otp.config +}) + +export default connect(mapStateToProps)(SimpleMap) diff --git a/lib/components/mobile/batch-search-screen.tsx b/lib/components/mobile/batch-search-screen.tsx index 930112a88..c24b4dacb 100644 --- a/lib/components/mobile/batch-search-screen.tsx +++ b/lib/components/mobile/batch-search-screen.tsx @@ -1,9 +1,18 @@ import { connect } from 'react-redux' +import { CSSTransition, TransitionGroup } from 'react-transition-group' import { injectIntl, IntlShape } from 'react-intl' import React, { Component } from 'react' +import styled from 'styled-components' import * as uiActions from '../../actions/ui' +import { + advancedPanelClassName, + mainPanelClassName, + transitionDuration, + TransitionStyles +} from '../form/styled' import { MobileScreens } from '../../actions/ui-constants' +import AdvancedSettingsPanel from '../form/advanced-settings-panel' import BatchSettings from '../form/batch-settings' import DefaultMap from '../map/default-map' import LocationField from '../form/connected-location-field' @@ -14,6 +23,25 @@ import MobileNavigationBar from './navigation-bar' const { SET_FROM_LOCATION, SET_TO_LOCATION } = MobileScreens +const MobileSearchSettings = styled.div<{ + advancedPanelOpen: boolean + transitionDelay: number + transitionDuration: number +}>` + background: white; + box-shadow: 3px 0px 12px #00000052; + height: ${(props) => + props.advancedPanelOpen ? 'calc(100% - 50px)' : '230px'}; + left: 0; + position: fixed; + right: 0; + top: 50px; + transition: ${(props) => `all ${props.transitionDuration}ms ease`}; + transition-delay: ${(props) => props.transitionDelay}ms; + /* Must appear under the 'hamburger' dropdown which has z-index of 1000. */ + z-index: 999; +` + interface Props { intl: IntlShape map: React.ReactElement @@ -22,20 +50,45 @@ interface Props { class BatchSearchScreen extends Component { state = { - planTripClicked: false + closeAdvancedSettingsWithDelay: false, + planTripClicked: false, + showAdvancedModeSettings: false } _fromFieldClicked = () => this.props.setMobileScreen(SET_FROM_LOCATION) _toFieldClicked = () => this.props.setMobileScreen(SET_TO_LOCATION) + _mainPanelContentRef = React.createRef() + _advancedSettingRef = React.createRef() + handlePlanTripClick = () => { this.setState({ planTripClicked: true }) } + openAdvancedSettings = () => { + this.setState({ + closeAdvancedSettingsWithDelay: false, + showAdvancedModeSettings: true + }) + } + + closeAdvancedSettings = () => { + this.setState({ showAdvancedModeSettings: false }) + } + + setCloseAdvancedSettingsWithDelay = () => { + this.setState({ + closeAdvancedSettingsWithDelay: true + }) + } + render() { const { intl } = this.props - const { planTripClicked } = this.state + const { planTripClicked, showAdvancedModeSettings } = this.state + + const transitionDelay = this.state.closeAdvancedSettingsWithDelay ? 300 : 0 + const transitionDurationWithDelay = transitionDuration + transitionDelay return ( { })} />
                                                          -
                                                          - - -
                                                          - -
                                                          - -
                                                          + + + + {/* Unfortunately we can't use a ternary operator here because it is cancelling out the CSSTransition animations. */} + {!showAdvancedModeSettings && ( + +
                                                          + + +
                                                          + +
                                                          + +
                                                          +
                                                          + )} + {showAdvancedModeSettings && ( + + + + )} +
                                                          +
                                                          +
                                                          diff --git a/lib/components/mobile/mobile.css b/lib/components/mobile/mobile.css index b0b6fb3db..b1197cfbd 100644 --- a/lib/components/mobile/mobile.css +++ b/lib/components/mobile/mobile.css @@ -121,20 +121,9 @@ /* Batch routing search screen */ -.otp.mobile .batch-search-settings { - position: fixed; - top: 50px; - left: 0; - right: 0; - height: 216px; - /* Must appear under the 'hamburger' dropdown which has z-index of 1000. */ - z-index: 999; - box-shadow: 3px 0px 12px #00000052; -} - .otp.mobile .batch-search-map { position: fixed; - top: 266px; + top: 282px; left: 0; right: 0; bottom: 0; diff --git a/lib/components/narrative/default/default-itinerary.js b/lib/components/narrative/default/default-itinerary.js index 89219244f..ba67d24da 100644 --- a/lib/components/narrative/default/default-itinerary.js +++ b/lib/components/narrative/default/default-itinerary.js @@ -36,7 +36,7 @@ const { isAdvanceBookingRequired, isCoordinationRequired, isFlex, - isTransit + isTransitLeg } = coreUtils.itinerary // Styled components @@ -164,7 +164,7 @@ const ITINERARY_ATTRIBUTES = [ order: 3, render: (itinerary, options) => { const leg = clone(itinerary.legs[0]) - if (isTransit(leg.mode)) { + if (isTransitLeg(leg)) { leg.mode = 'WALK' } diff --git a/lib/components/narrative/default/itinerary-description.tsx b/lib/components/narrative/default/itinerary-description.tsx index d26e348f5..9e2f1327a 100644 --- a/lib/components/narrative/default/itinerary-description.tsx +++ b/lib/components/narrative/default/itinerary-description.tsx @@ -9,7 +9,7 @@ import FormattedMode from '../../util/formatted-mode' const { isRideshareLeg } = coreUtils.itinerary -const { isBicycle, isMicromobility, isTransit } = coreUtils.itinerary +const { isBicycle, isMicromobility, isTransitLeg } = coreUtils.itinerary type Props = { combineTransitModes?: boolean @@ -32,10 +32,7 @@ export function getMainItineraryModes({ let transitMode itinerary.legs.forEach((leg, i) => { const { duration, mode, rentedBike, rentedVehicle } = leg - if ( - (leg.transitLeg || isTransit(mode)) && - duration > primaryTransitDuration - ) { + if (isTransitLeg(leg) && duration > primaryTransitDuration) { primaryTransitDuration = duration transitMode = getFormattedMode( combineTransitModes ? 'transit' : mode.toLowerCase(), @@ -68,7 +65,7 @@ export function ItineraryDescription({ itinerary }: Props): JSX.Element { let transitMode itinerary.legs.forEach((leg) => { const { duration, mode, rentedBike, rentedVehicle } = leg - if (isTransit(mode) && duration > primaryTransitDuration) { + if (isTransitLeg(leg) && duration > primaryTransitDuration) { primaryTransitDuration = duration // If custom TransitModes have been defined for the given mode/leg, attempt to use them, diff --git a/lib/components/narrative/default/itinerary-summary.js b/lib/components/narrative/default/itinerary-summary.js index 5ff8dcc8f..5d327407b 100644 --- a/lib/components/narrative/default/itinerary-summary.js +++ b/lib/components/narrative/default/itinerary-summary.js @@ -1,7 +1,10 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import { connect } from 'react-redux' -import { getCompanyFromLeg } from '@opentripplanner/core-utils/lib/itinerary' +import { + getCompanyFromLeg, + isTransitLeg +} from '@opentripplanner/core-utils/lib/itinerary' import PropTypes from 'prop-types' import React, { Component } from 'react' import styled from 'styled-components' @@ -93,16 +96,16 @@ export default class ItinerarySummary extends Component { if ( i > 0 && i < itinerary.legs.length - 1 && - !leg.transitLeg && - itinerary.legs[i - 1].transitLeg && - itinerary.legs[i + 1].transitLeg + !isTransitLeg(leg) && + isTransitLeg(itinerary.legs[i - 1]) && + isTransitLeg(itinerary.legs[i + 1]) ) { return null } // Add the mode icon let title = leg.mode - if (leg.transitLeg) { + if (isTransitLeg(leg)) { title = leg.routeShortName ? `${leg.routeShortName}${ leg.routeLongName ? ` - ${leg.routeLongName}` : '' diff --git a/lib/components/narrative/line-itin/LegIconWithA11y.tsx b/lib/components/narrative/line-itin/LegIconWithA11y.tsx index ab5e8dacc..283fe7ace 100644 --- a/lib/components/narrative/line-itin/LegIconWithA11y.tsx +++ b/lib/components/narrative/line-itin/LegIconWithA11y.tsx @@ -1,22 +1,24 @@ -import { getFormattedMode } from '../../../util/i18n' -import coreUtils from '@opentripplanner/core-utils' -import InvisibleA11yLabel from '../../util/invisible-a11y-label' - -import React, { useContext } from 'react' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' +import { Leg } from '@opentripplanner/types' +import { useIntl } from 'react-intl' import { ComponentContext } from '../../../util/contexts' +import { getFormattedMode } from '../../../util/i18n' +import React, { useContext } from 'react' -import { useIntl } from 'react-intl' +import InvisibleA11yLabel from '../../util/invisible-a11y-label' -const { isTransit } = coreUtils.itinerary +type Props = { + leg: Leg +} -const LegIconWithA11y = (props: any) => { +const LegIconWithA11y = (props: Props): JSX.Element => { // @ts-expect-error No type on ComponentContext const { LegIcon } = useContext(ComponentContext) const intl = useIntl() const { leg } = props const { mode } = leg - const ariaLabel = isTransit(mode) ? getFormattedMode(mode, intl) : null + const ariaLabel = isTransitLeg(leg) ? getFormattedMode(mode, intl) : null return ( <> diff --git a/lib/components/narrative/line-itin/itin-summary.tsx b/lib/components/narrative/line-itin/itin-summary.tsx deleted file mode 100644 index 482aa9a72..000000000 --- a/lib/components/narrative/line-itin/itin-summary.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { connect } from 'react-redux' -import { FareProductSelector, Itinerary, Leg } from '@opentripplanner/types' -import { FormattedMessage, FormattedNumber } from 'react-intl' -import coreUtils from '@opentripplanner/core-utils' -import React, { Component } from 'react' -import styled from 'styled-components' - -import { AppReduxState } from '../../../util/state-types' -import { ComponentContext } from '../../../util/contexts' -import { getFare } from '../../../util/itinerary' -import { GREY_ON_WHITE } from '../../util/colors' -import FormattedDuration from '../../util/formatted-duration' - -// TODO: make this a prop -const defaultRouteColor = '#008' - -const Container = styled.div` - display: ${() => (coreUtils.ui.isMobile() ? 'table' : 'none')}; - height: 60px; - margin-bottom: 15px; - padding-right: 5px; - width: 100%; -` - -const Detail = styled.div` - color: ${GREY_ON_WHITE}; - font-size: 13px; -` - -const Details = styled.div` - display: table-cell; - vertical-align: top; -` - -const Header = styled.div` - font-size: 18px; - font-weight: bold; - margin-top: -3px; -` - -const LegIconContainer = styled.div` - height: 30px; - width: 30px; -` - -const NonTransitSpacer = styled.div` - height: 30px; - overflow: hidden; -` - -const RoutePreview = styled.div` - display: inline-block; - margin-left: 8px; - vertical-align: top; -` - -const Routes = styled.div` - display: table-cell; - text-align: right; -` - -const ShortName = styled.div<{ leg: Leg }>` - background-color: ${(props) => getRouteColorForBadge(props.leg)}; - border-radius: 15px; - border: 2px solid white; - box-shadow: 0 0 0.5em #000; - color: white; - font-size: 15px; - font-weight: 500; - height: 30px; - margin-top: 6px; - overflow: hidden; - padding-top: 4px; - text-align: center; - text-overflow: ellipsis; - white-space: nowrap; - width: 30px; -` - -type Props = { - currency?: string - defaultFareType?: FareProductSelector - itinerary: Itinerary - onClick: () => void -} - -export class ItinerarySummary extends Component { - static contextType = ComponentContext - - _onSummaryClicked = (): void => { - if (typeof this.props.onClick === 'function') this.props.onClick() - } - - render(): JSX.Element { - const { defaultFareType, itinerary } = this.props - const { LegIcon } = this.context - - const { fareCurrency, maxTNCFare, minTNCFare, transitFare } = getFare( - itinerary, - defaultFareType - ) - - const minTotalFare = minTNCFare * 100 + (transitFare || 0) - const maxTotalFare = maxTNCFare * 100 + (transitFare || 0) - - const { endTime, startTime } = itinerary - - const { caloriesBurned } = - coreUtils.itinerary.calculatePhysicalActivity(itinerary) - - const minTotalFareFormatted = minTotalFare > 0 && ( - - ) - - return ( - -
                                                          - {/* Travel time in hrs/mins */} -
                                                          - -
                                                          - - {/* Duration as time range */} - - - - - {/* Fare / Calories */} - - {minTotalFare > 0 && ( - - {minTotalFare === maxTotalFare ? ( - minTotalFareFormatted - ) : ( - - ), - minTotalFare: minTotalFareFormatted - }} - /> - )} - - - )} - - - - {/* Number of transfers, if applicable */} - - - -
                                                          - - {itinerary.legs - .filter((leg: Leg) => { - return !(leg.mode === 'WALK' && itinerary.transitTime > 0) - }) - .map((leg: Leg, k: number) => { - return ( - - - - - {coreUtils.itinerary.isTransit(leg.mode) ? ( - {getRouteNameForBadge(leg)} - ) : ( - - )} - - ) - })} - -
                                                          - ) - } -} - -// Helper functions - -// Leg is any for now until types package is updated with correct Leg type -function getRouteLongName(leg: any) { - return leg.routes && leg.routes.length > 0 - ? leg.routes[0].longName - : leg.routeLongName -} - -function getRouteNameForBadge(leg: any) { - const shortName = - leg.routes && leg.routes.length > 0 - ? leg.routes[0].shortName - : leg.routeShortName - - const longName = getRouteLongName(leg) - - // check for max - if (longName && longName.toLowerCase().startsWith('max')) return null - - // check for streetcar - if (longName && longName.startsWith('Portland Streetcar')) - return longName.split('-')[1].trim().split(' ')[0] - - return shortName || longName -} - -function getRouteColorForBadge(leg: Leg): string { - return leg.routeColor ? '#' + leg.routeColor : defaultRouteColor -} - -const mapStateToProps = (state: AppReduxState) => { - return { - defaultFareType: state.otp.config.itinerary?.defaultFareType - } -} - -export default connect(mapStateToProps)(ItinerarySummary) diff --git a/lib/components/narrative/line-itin/realtime-time-column.tsx b/lib/components/narrative/line-itin/realtime-time-column.tsx index 2e14834a8..303a700d9 100644 --- a/lib/components/narrative/line-itin/realtime-time-column.tsx +++ b/lib/components/narrative/line-itin/realtime-time-column.tsx @@ -1,5 +1,5 @@ import { FormattedTime } from 'react-intl' -import { isTransit } from '@opentripplanner/core-utils/lib/itinerary' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import { Leg } from '@opentripplanner/types' import React, { ReactElement } from 'react' import styled from 'styled-components' @@ -33,11 +33,10 @@ function RealtimeTimeColumn({ isDestination, leg }: Props): ReactElement { } const timeMillis = isDestination ? leg.endTime : leg.startTime - const isTransitLeg = isTransit(leg.mode) - const isRealtimeTransitLeg = isTransitLeg && leg.realTime + const isRealtimeTransitLeg = isTransitLeg(leg) && leg.realTime // For non-transit legs show only the scheduled time. - if (!isTransitLeg) { + if (!isTransitLeg(leg)) { return (
                                                          diff --git a/lib/components/narrative/metro/default-route-renderer.tsx b/lib/components/narrative/metro/default-route-renderer.tsx index c16d100ad..728ae9036 100644 --- a/lib/components/narrative/metro/default-route-renderer.tsx +++ b/lib/components/narrative/metro/default-route-renderer.tsx @@ -37,10 +37,16 @@ const DefaultRouteRenderer = ({ leg, style }: RouteRendererProps): JSX.Element => { - const routeTitle = leg.routeShortName || leg.routeLongName + const routeTitle = + typeof leg.route === 'object' + ? leg.route.shortName || leg.route.longName + : leg.routeShortName || leg.routeLongName return ( x.value === sort.type)?.text - const handleSortClick = useCallback( - (value) => { - onSortChange(value) - }, - [onSortChange] - ) - return (
                                                          handleSortClick(sortOption.value)} + onClick={() => onSortChange(sortOption.value)} role="option" > {sortOption.text} diff --git a/lib/components/narrative/narrative-itineraries.js b/lib/components/narrative/narrative-itineraries.js index 7d9faded6..e86007221 100644 --- a/lib/components/narrative/narrative-itineraries.js +++ b/lib/components/narrative/narrative-itineraries.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux' import { differenceInDays } from 'date-fns' import { FormattedMessage, injectIntl } from 'react-intl' -import { isFlex, isTransit } from '@opentripplanner/core-utils/lib/itinerary' +import { isFlex, isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import clone from 'clone' import coreUtils from '@opentripplanner/core-utils' import memoize from 'lodash.memoize' @@ -423,9 +423,7 @@ class NarrativeItineraries extends Component { }) // Identify whether an itinerary uses transit & sort into appropriate bucket - const transitLegs = cur.legs.filter( - (leg) => isTransit(leg.mode) | leg.transitLeg - ) + const transitLegs = cur.legs.filter(isTransitLeg) const modeContainer = transitLegs.length > 0 ? modeItinMap.multi : modeItinMap.single diff --git a/lib/components/user/monitored-trip/saved-trip-list.tsx b/lib/components/user/monitored-trip/saved-trip-list.tsx index 0df5ff909..03e33f80f 100644 --- a/lib/components/user/monitored-trip/saved-trip-list.tsx +++ b/lib/components/user/monitored-trip/saved-trip-list.tsx @@ -1,13 +1,15 @@ import { connect } from 'react-redux' +import { Edit, Map } from '@styled-icons/fa-solid' import { FormattedMessage, injectIntl, IntlShape, useIntl } from 'react-intl' import { Panel } from 'react-bootstrap' import { TriangleExclamation } from '@styled-icons/fa-solid/TriangleExclamation' import { withAuthenticationRequired } from '@auth0/auth0-react' import React, { Component } from 'react' +import styled from 'styled-components' import * as userActions from '../../../actions/user' import { AppReduxState } from '../../../util/state-types' -import { Edit } from '@styled-icons/fa-solid' +import { ComponentContext } from '../../../util/contexts' import { IconWithText } from '../../util/styledIcon' import { MonitoredTrip } from '../types' import { @@ -19,23 +21,17 @@ import { TripPanelTitle } from '../styled' import { RETURN_TO_CURRENT_ROUTE } from '../../../util/ui' -import { TRIPS_PATH } from '../../../util/constants' +import { TRIP_PREVIEW_PATH, TRIPS_PATH } from '../../../util/constants' import AccountPage from '../account-page' import AwaitingScreen from '../awaiting-screen' import BackToTripPlanner from '../back-to-trip-planner' -import PageTitle from '../../util/page-title' - -import styled from 'styled-components' - -import withLoggedInUserSupport from '../with-logged-in-user-support' - -import getRenderData from './trip-status-rendering-strategies' import InvisibleA11yLabel from '../../util/invisible-a11y-label' - -import { ComponentContext } from '../../../util/contexts' import Link from '../../util/link' import MetroItineraryRoutes from '../../narrative/metro/metro-itinerary-routes' +import PageTitle from '../../util/page-title' +import withLoggedInUserSupport from '../with-logged-in-user-support' +import getRenderData from './trip-status-rendering-strategies' import TripSummaryPane from './trip-summary-pane' interface ItemOwnProps { @@ -111,6 +107,12 @@ class TripListItem extends Component { const from = legs[0].from const to = legs[legs.length - 1].to const editTripPath = `${TRIPS_PATH}/${trip.id}` + const editTripText = intl.formatMessage({ + id: 'components.SavedTripEditor.editSavedTrip' + }) + const previewTripText = intl.formatMessage({ + id: 'components.TripPreviewLayout.previewTrip' + }) const { LegIcon } = this.context return ( @@ -120,15 +122,15 @@ class TripListItem extends Component { {trip.tripName} + + {previewTripText} + + - - - + {editTripText} diff --git a/lib/components/user/monitored-trip/saved-trip-screen.js b/lib/components/user/monitored-trip/saved-trip-screen.js index 49728d7d8..ccb3d15f2 100644 --- a/lib/components/user/monitored-trip/saved-trip-screen.js +++ b/lib/components/user/monitored-trip/saved-trip-screen.js @@ -11,8 +11,11 @@ import * as formActions from '../../../actions/form' import * as uiActions from '../../../actions/ui' import * as userActions from '../../../actions/user' import { arrayToDayFields } from '../../../util/monitored-trip' +import { + copyAndRemoveRouteModeOverrides, + getItineraryDefaultMonitoredDays +} from '../../../util/itinerary' import { getActiveItineraries, getActiveSearch } from '../../../util/state' -import { getItineraryDefaultMonitoredDays } from '../../../util/itinerary' import { RETURN_TO_CURRENT_ROUTE } from '../../../util/ui' import { TRIPS_PATH } from '../../../util/constants' import AccountPage from '../account-page' @@ -61,14 +64,16 @@ class SavedTripScreen extends Component { itinerary, homeTimezone ) + const { otp2QueryParams, ...otherItineraryProps } = itinerary return { ...arrayToDayFields(monitoredDays), arrivalVarianceMinutesThreshold: 5, departureVarianceMinutesThreshold: 5, excludeFederalHolidays: true, isActive: true, - itinerary, + itinerary: copyAndRemoveRouteModeOverrides(otherItineraryProps), leadTimeInMinutes: 30, + otp2QueryParams, // when creating a monitored trip, the query params will be changed on the // backend so that the modes parameter will reflect the modes seen in the // itinerary @@ -88,8 +93,13 @@ class SavedTripScreen extends Component { */ _updateMonitoredTrip = (monitoredTrip) => { const { createOrUpdateUserMonitoredTrip, intl, isCreating } = this.props + const tripToSave = { + ...monitoredTrip, + itinerary: copyAndRemoveRouteModeOverrides(monitoredTrip.itinerary) + } + createOrUpdateUserMonitoredTrip( - monitoredTrip, + tripToSave, isCreating, undefined, undefined, @@ -151,8 +161,14 @@ class SavedTripScreen extends Component { } render() { - const { isCreating, itinerary, loggedInUser, monitoredTrips, pending } = - this.props + const { + disableSingleItineraryDays, + isCreating, + itinerary, + loggedInUser, + monitoredTrips, + pending + } = this.props const isAwaiting = !monitoredTrips || (isCreating && pending) let screenContents @@ -178,7 +194,32 @@ class SavedTripScreen extends Component { // Text constant is used to allow format.js command line tool to keep track of // which IDs are in the code. .notOneOf(otherTripNames, 'trip-name-already-used') - const validationSchema = yup.object(clonedSchemaShape) + const validationSchema = yup + .object(clonedSchemaShape) + // If disableSingleItineraryDays is true, test to see if at least one day checkbox is checked + .test('dayofweek', 'Please select one day', function (obj) { + if ( + obj.monday || + obj.tuesday || + obj.wednesday || + obj.thursday || + obj.friday || + obj.saturday || + obj.sunday || + !disableSingleItineraryDays + ) { + return true + } + + /* Hack: because the selected days values are not grouped, we need to assign this error to one of the + checkboxes so that form validates correctly. Monday makes sure the focus is on the first checkbox. */ + + return new yup.ValidationError( + 'Please select at least one day to monitor', + obj.monday, + 'monday' + ) + }) screenContents = ( { const pending = activeSearch ? Boolean(activeSearch.pending) : false const itineraries = getActiveItineraries(state) || [] const tripId = ownProps.match.params.id + const { disableSingleItineraryDays } = state.otp.config return { - activeSearchId: state.otp.activeSearchId, + disableSingleItineraryDays, homeTimezone: state.otp.config.homeTimezone, isCreating: tripId === 'new', itinerary: itineraries[activeItinerary], diff --git a/lib/components/user/monitored-trip/trip-basics-pane.tsx b/lib/components/user/monitored-trip/trip-basics-pane.tsx index 9d98eedd1..dafafcd00 100644 --- a/lib/components/user/monitored-trip/trip-basics-pane.tsx +++ b/lib/components/user/monitored-trip/trip-basics-pane.tsx @@ -9,7 +9,7 @@ import { Radio } from 'react-bootstrap' import { Field, FormikProps } from 'formik' -import { FormattedMessage, injectIntl } from 'react-intl' +import { FormattedMessage, injectIntl, useIntl } from 'react-intl' import { Prompt } from 'react-router' // @ts-expect-error FormikErrorFocus does not support TypeScript yet. import FormikErrorFocus from 'formik-error-focus' @@ -46,6 +46,7 @@ type TripBasicsProps = WrappedComponentProps & intl: IntlShape ) => void clearItineraryExistence: () => void + disableSingleItineraryDays?: boolean isCreating: boolean itineraryExistence?: ItineraryExistence } @@ -132,6 +133,97 @@ function isDisabled(day: string, itineraryExistence?: ItineraryExistence) { return itineraryExistence && !itineraryExistence[day]?.valid } +const RenderAvailableDays = ({ + errorCheckingTrip, + errorSelectingDays, + finalItineraryExistence, + isCreating, + monitoredTrip +}: { + errorCheckingTrip: boolean + errorSelectingDays?: 'error' | null + finalItineraryExistence?: ItineraryExistence + isCreating: boolean + monitoredTrip: MonitoredTrip +}) => { + const intl = useIntl() + const baseColor = getBaseColor() + return ( + <> + {errorCheckingTrip && ( + <> + {/* FIXME: Temporary solution until itinerary existence check is fixed. */} +
                                                          + + + )} + + {ALL_DAYS.map((day) => { + const isDayDisabled = isDisabled(day, finalItineraryExistence) + const labelClass = isDayDisabled ? 'disabled-day' : '' + const notAvailableText = isDayDisabled + ? intl.formatMessage( + { + id: 'components.TripBasicsPane.tripNotAvailableOnDay' + }, + { + repeatedDay: getFormattedDayOfWeekPlural(day, intl) + } + ) + : '' + + return ( + + + + + {notAvailableText} + + ) + })} + + + {finalItineraryExistence ? ( + + ) : ( + + } + now={100} + /> + )} + + + {errorSelectingDays && ( + + )} + + + ) +} + /** * This component shows summary information for a trip * and lets the user edit the trip name and day. @@ -220,6 +312,7 @@ class TripBasicsPane extends Component { const { canceled, dirty, + disableSingleItineraryDays, errors, intl, isCreating, @@ -257,6 +350,9 @@ class TripBasicsPane extends Component { const errorCheckingTrip = ALL_DAYS.every((day) => isDisabled(day, finalItineraryExistence) ) + /* Hack: because the selected days checkboxes are not grouped, we need to assign this error to one of the + checkboxes so that the FormikErrorFocus works. */ + const selectOneDayError = errorStates.monday return (
                                                          {/* TODO: This component does not block navigation on reload or using the back button. @@ -286,104 +382,53 @@ class TripBasicsPane extends Component { )} - - - - - - - - {errorCheckingTrip && ( + {disableSingleItineraryDays ? ( + + + + + + + ) : ( + + + + + + + + {!isOneTime && ( <> - {/* FIXME: Temporary solution until itinerary existence check is fixed. */} -
                                                          - + )} -
                                                          - {!isOneTime && ( - <> - - {ALL_DAYS.map((day) => { - const isDayDisabled = isDisabled( - day, - finalItineraryExistence - ) - const labelClass = isDayDisabled ? 'disabled-day' : '' - const notAvailableText = isDayDisabled - ? intl.formatMessage( - { - id: 'components.TripBasicsPane.tripNotAvailableOnDay' - }, - { - repeatedDay: getFormattedDayOfWeekPlural(day, intl) - } - ) - : '' - - const baseColor = getBaseColor() - return ( - - - - - - {notAvailableText} - - - ) - })} - - - {finalItineraryExistence ? ( - - ) : ( - - } - now={100} - /> - )} - - - )} - - - - - {/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */} - -
                                                          + + + + + )} + + {/* Scroll to the trip name/days fields if submitting and there is an error on these fields. */} +
                                                          ) } @@ -394,7 +439,9 @@ class TripBasicsPane extends Component { const mapStateToProps = (state: AppReduxState) => { const { itineraryExistence } = state.user + const { disableSingleItineraryDays } = state.otp.config return { + disableSingleItineraryDays, itineraryExistence } } diff --git a/lib/components/user/monitored-trip/trip-notifications-pane.tsx b/lib/components/user/monitored-trip/trip-notifications-pane.tsx index 197275a4a..d02bbf52c 100644 --- a/lib/components/user/monitored-trip/trip-notifications-pane.tsx +++ b/lib/components/user/monitored-trip/trip-notifications-pane.tsx @@ -2,7 +2,7 @@ import { Alert, FormControl } from 'react-bootstrap' import { ExclamationTriangle } from '@styled-icons/fa-solid/ExclamationTriangle' import { FormattedList, FormattedMessage } from 'react-intl' import { FormikProps } from 'formik' -import { Leg } from '@opentripplanner/types' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import React, { Component, FormEvent } from 'react' import styled from 'styled-components' @@ -74,9 +74,7 @@ class TripNotificationsPane extends Component { values.arrivalVarianceMinutesThreshold, values.departureVarianceMinutesThreshold ) - const hasTransit = values.itinerary?.legs?.some( - (leg: Leg) => leg.transitLeg - ) + const hasTransit = values.itinerary?.legs?.some(isTransitLeg) let notificationSettingsContent if (areNotificationsDisabled) { diff --git a/lib/components/user/styled.ts b/lib/components/user/styled.ts index e1cf190dd..7139c93d0 100644 --- a/lib/components/user/styled.ts +++ b/lib/components/user/styled.ts @@ -52,7 +52,11 @@ export const TripHeader = styled.h3` export const TripPanelTitle = styled.div` align-items: center; display: flex; - justify-content: space-between; + gap: 10px; + + & > div:first-child { + flex-grow: 1; + } ` export const TripPanelHeading = styled(Panel.Heading)` diff --git a/lib/components/user/types.ts b/lib/components/user/types.ts index 01ffaa6eb..404c4fb49 100644 --- a/lib/components/user/types.ts +++ b/lib/components/user/types.ts @@ -64,6 +64,10 @@ export interface ItineraryExistenceDay { export type ItineraryExistence = Record +export interface JourneyState { + matchingItinerary?: Itinerary +} + export type MonitoredTrip = Record & { arrivalVarianceMinutesThreshold: number departureVarianceMinutesThreshold: number @@ -72,8 +76,10 @@ export type MonitoredTrip = Record & { isActive: boolean itinerary: Itinerary itineraryExistence?: ItineraryExistence + journeyState?: JourneyState leadTimeInMinutes: number - queryParams: string + otp2QueryParams: Record + queryParams: Record tripName: string userId: string } diff --git a/lib/components/util/connected-transit-operator-icons.tsx b/lib/components/util/connected-transit-operator-icons.tsx new file mode 100644 index 000000000..0475e54e4 --- /dev/null +++ b/lib/components/util/connected-transit-operator-icons.tsx @@ -0,0 +1,38 @@ +import { connect } from 'react-redux' +import { TransitOperator } from '@opentripplanner/types' +import React from 'react' + +import { AppReduxState } from '../../util/state-types' +import { FETCH_STATUS } from '../../util/constants' + +import { StopData } from './types' +import TransitOperatorLogos from './transit-operator-icons' + +interface Props { + stopData?: StopData + transitOperators: TransitOperator[] +} + +function TransitOperatorIcons({ stopData, transitOperators }: Props) { + const loading = stopData?.fetchStatus === FETCH_STATUS.FETCHING + return ( + + ) +} + +const mapStateToProps = ( + state: AppReduxState, + ownProps: Props & { stopId: string } +) => { + const stops = state.otp.transitIndex.stops + return { + stopData: stops?.[ownProps.stopId], + transitOperators: state.otp.config.transitOperators || [] + } +} + +export default connect(mapStateToProps)(TransitOperatorIcons) diff --git a/lib/components/util/formatted-validation-error.js b/lib/components/util/formatted-validation-error.js index 2bf08ecb2..9b836207f 100644 --- a/lib/components/util/formatted-validation-error.js +++ b/lib/components/util/formatted-validation-error.js @@ -28,6 +28,10 @@ export default function FormattedValidationError({ type }) { return ( ) + case 'select-at-least-one-day': + return ( + + ) default: return null } diff --git a/lib/components/util/operator-logo.tsx b/lib/components/util/operator-logo.tsx index 420e37ce7..f88f6d51a 100644 --- a/lib/components/util/operator-logo.tsx +++ b/lib/components/util/operator-logo.tsx @@ -2,20 +2,32 @@ import { TransitOperator } from '@opentripplanner/types' import React from 'react' import styled from 'styled-components' -const OperatorImg = styled.img` +const OperatorImg = styled.img<{ marginRight?: number; maxHeight?: number }>` &:not(:last-of-type) { margin-right: 0.5ch; } width: 25px; ` +const StyledOperatorImg = styled(OperatorImg)` + margin-right: 0.5ch; + max-height: 1em; + // Make sure icons stay square + max-width: 1em; +` + type Props = { alt?: string operator?: TransitOperator + styled?: boolean } -const OperatorLogo = ({ alt, operator }: Props): JSX.Element | null => { +const OperatorLogo = ({ alt, operator, styled }: Props): JSX.Element | null => { if (!operator?.logo) return null + if (styled) { + return + } + return } diff --git a/lib/components/util/prefersReducedMotion.tsx b/lib/components/util/prefersReducedMotion.tsx new file mode 100644 index 000000000..69e13b7d4 --- /dev/null +++ b/lib/components/util/prefersReducedMotion.tsx @@ -0,0 +1,9 @@ +/** + * Identifies whether the user's machine has "reduced motion" enabled + * in their local settings. If reduced motion is on, the app should + * show as few animations & transitions as possible. + * @returns boolean reflecting whether user prefers reduced motion + */ +export const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' +).matches diff --git a/lib/components/util/transit-operator-icons.tsx b/lib/components/util/transit-operator-icons.tsx new file mode 100644 index 000000000..7f40fe11c --- /dev/null +++ b/lib/components/util/transit-operator-icons.tsx @@ -0,0 +1,83 @@ +import { MapPin } from '@styled-icons/fa-solid' +import { useIntl } from 'react-intl' +import React from 'react' +import Skeleton from 'react-loading-skeleton' +import type { TransitOperator } from '@opentripplanner/types' + +import InvisibleA11yLabel from './invisible-a11y-label' +import OperatorLogo from './operator-logo' +import type { StopData } from './types' + +const Operator = ({ operator }: { operator?: TransitOperator }) => { + const intl = useIntl() + + if (!operator) { + return null + } else { + const operatorLogoAriaLabel = intl.formatMessage( + { + id: 'components.StopViewer.operatorLogoAriaLabel' + }, + { + operatorName: operator.name + } + ) + return operator.logo ? ( + // Span with agency classname allows optional contrast/customization in user + // config for logos with poor contrast. Class name is hyphenated agency name + // e.g. "sound-transit" + + + + ) : ( + // If operator exists but logo is missing, + // we still need to announce the operator name to screen readers. + <> + + {operatorLogoAriaLabel} + + ) + } +} + +const TransitOperatorLogos = ({ + loading = false, + stopData, + transitOperators +}: { + loading?: boolean + stopData?: StopData + transitOperators?: TransitOperator[] +}): JSX.Element => { + const agencies = + (stopData && + stopData.stoptimesForPatterns?.reduce>((prev, cur) => { + // @ts-expect-error The agency type is not yet compatible with OTP2 + const agencyGtfsId = cur.pattern.route.agency?.gtfsId + return agencyGtfsId ? prev.add(agencyGtfsId) : prev + }, new Set())) || + new Set() + + return ( + <> + {loading ? ( + + ) : ( + transitOperators + ?.filter((to) => Array.from(agencies).includes(to.agencyId)) + // Second pass to remove duplicates based on name + .filter( + (to, index, arr) => + index === arr.findIndex((t) => t?.name === to?.name) + ) + .map((to) => ) + )} + + ) +} + +export default TransitOperatorLogos diff --git a/lib/components/util/types.ts b/lib/components/util/types.ts index 9b21030fa..83bcd7930 100644 --- a/lib/components/util/types.ts +++ b/lib/components/util/types.ts @@ -108,3 +108,5 @@ export type ZoomToPlaceHandler = ( place?: { lat: number; lon: number }, zoom?: number ) => void + +export type QueryParamChangeHandler = (event: any) => void diff --git a/lib/components/viewers/nearby/stop-card-header.tsx b/lib/components/viewers/nearby/stop-card-header.tsx index 045616e60..d161ba7d6 100644 --- a/lib/components/viewers/nearby/stop-card-header.tsx +++ b/lib/components/viewers/nearby/stop-card-header.tsx @@ -1,6 +1,5 @@ import { connect } from 'react-redux' import { FormattedMessage, useIntl } from 'react-intl' -import { MapPin } from '@styled-icons/fa-solid' import { Search } from '@styled-icons/fa-solid/Search' import { TransitOperator } from '@opentripplanner/types' import React, { ComponentType } from 'react' @@ -10,8 +9,8 @@ import { Icon, IconWithText } from '../../util/styledIcon' import { StopData } from '../../util/types' import InvisibleA11yLabel from '../../util/invisible-a11y-label' import Link from '../../util/link' -import OperatorLogo from '../../util/operator-logo' import Strong from '../../util/strong-text' +import TransitOperatorLogos from '../../util/transit-operator-icons' import { CardBody, CardHeader, CardTitle } from './styled' import DistanceDisplay from './distance-display' @@ -28,41 +27,6 @@ type Props = { transitOperators?: TransitOperator[] } -const Operator = ({ operator }: { operator?: TransitOperator }) => { - const intl = useIntl() - if (!operator) { - return null - } else { - const operatorLogoAriaLabel = intl.formatMessage( - { - id: 'components.StopViewer.operatorLogoAriaLabel' - }, - { - operatorName: operator.name - } - ) - return operator.logo ? ( - // Span with agency classname allows optional contrast/customization in user - // config for logos with poor contrast. Class name is hyphenated agency name - // e.g. "sound-transit" - - - - ) : ( - // If operator exists but logo is missing, - // we still need to announce the operator name to screen readers. - <> - - {operatorLogoAriaLabel} - - ) - } -} - const StopCardHeader = ({ actionIcon, actionParams, @@ -75,12 +39,7 @@ const StopCardHeader = ({ transitOperators }: Props): JSX.Element => { const intl = useIntl() - const agencies = - stopData.stoptimesForPatterns?.reduce>((prev, cur) => { - // @ts-expect-error The agency type is not yet compatible with OTP2 - const agencyGtfsId = cur.pattern.route.agency?.gtfsId - return agencyGtfsId ? prev.add(agencyGtfsId) : prev - }, new Set()) || new Set() + const zoomButtonText = onZoomClick ? intl.formatMessage({ id: 'components.StopViewer.zoomToStop' @@ -92,16 +51,10 @@ const StopCardHeader = ({ {/* @ts-expect-error The 'as' prop in styled-components is not listed for TypeScript. */} - {transitOperators - ?.filter((to) => Array.from(agencies).includes(to.agencyId)) - // Second pass to remove duplicates based on name - .filter( - (to, index, arr) => - index === arr.findIndex((t) => t?.name === to?.name) - ) - .map((to) => ( - - ))} + {stopData.name} diff --git a/lib/components/viewers/nearby/styled.tsx b/lib/components/viewers/nearby/styled.tsx index 1ffd1dc76..d088ebbfe 100644 --- a/lib/components/viewers/nearby/styled.tsx +++ b/lib/components/viewers/nearby/styled.tsx @@ -48,7 +48,6 @@ export const CardTitle = styled.p` display: flex; font-size: 22px; font-weight: 600; - gap: 0.5ch; grid-column: 1; margin: 0; /* Prevent svg and images to be taller than the text. */ diff --git a/lib/components/viewers/pattern-row.tsx b/lib/components/viewers/pattern-row.tsx index 73bc22006..4f87a8c7a 100644 --- a/lib/components/viewers/pattern-row.tsx +++ b/lib/components/viewers/pattern-row.tsx @@ -1,5 +1,6 @@ import { Calendar } from '@styled-icons/fa-regular' import { format, utcToZonedTime } from 'date-fns-tz' +import { FormattedMessage } from 'react-intl' import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route' import { isSameDay } from 'date-fns' import React, { useContext } from 'react' @@ -108,8 +109,15 @@ const PatternRow = ({ {pattern.route.longName} )} - {extractHeadsignFromPattern(pattern) || - (pattern.route.longName !== routeName && pattern.route.longName)} +
                                                          {/* next departure preview (only shows up to 3 entries) */} diff --git a/lib/components/viewers/viewers.css b/lib/components/viewers/viewers.css index 31e260402..1ea4637a9 100644 --- a/lib/components/viewers/viewers.css +++ b/lib/components/viewers/viewers.css @@ -60,12 +60,18 @@ } .otp .trip-viewer .header-text, -.otp .route-viewer .header-text { - display: contents; +.otp .route-viewer .header-text, +.otp .advanced-settings .header-text { font-weight: 700; font-size: 24px; margin: 0; } + +.otp .trip-viewer .header-text, +.otp .route-viewer .header-text { + display: contents; +} + .otp .route-viewer .header-text.route-expanded { display: flex; align-items: center; diff --git a/lib/main.js b/lib/main.js index 780584e27..33e36c552 100644 --- a/lib/main.js +++ b/lib/main.js @@ -34,6 +34,18 @@ import(CSS) // eslint-disable-next-line no-undef const otpConfig = require(YAML_CONFIG) +// Loads a JavaScript file which is set in the webpack section of the craco.config.js file. +// This setting is defined from a custom environment setting passed into webpack or +// defaults to ./config.js +// defined in webpack config: +// The JS_CONFIG variable is passed to this file by webpack's `DefinePlugin` that replaces the variable +// with its content at compile time (like C's `#define` preprocessor directive). +// eslint-disable-next-line no-undef +const jsConfig = require(JS_CONFIG).configure(otpConfig) + +// Plug the plan query into the config (if available) +otpConfig.api.planQuery = jsConfig.planQuery + const history = createHashHistory() const middleware = [ diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index 0bc899936..fa0f082ef 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -6,7 +6,7 @@ import deepmerge from 'deepmerge' import objectPath from 'object-path' import update from 'immutability-helper' -import { checkForRouteModeOverride } from '../util/config' +import { applyRouteModeOverrides } from '../util/itinerary' import { FETCH_STATUS, PERSIST_TO_LOCAL_STORAGE, @@ -311,18 +311,7 @@ function createOtpReducer(config) { response.requestId = requestId response.plan.itineraries = response.plan?.itineraries?.map( (itinerary) => { - itinerary.legs = itinerary.legs.map((leg) => { - if (leg.routeId) { - leg.mode = checkForRouteModeOverride( - { - id: leg.routeId, - mode: leg.mode - }, - state.config?.routeModeOverrides - ) - } - return leg - }) + applyRouteModeOverrides(itinerary, state.config.routeModeOverrides) return itinerary } ) diff --git a/lib/util/config-types.ts b/lib/util/config-types.ts index a11c51d50..cd29d83f7 100644 --- a/lib/util/config-types.ts +++ b/lib/util/config-types.ts @@ -15,6 +15,7 @@ import { TransitOperator, VehicleRentalMapOverlaySymbol } from '@opentripplanner/types' +import { ControlPosition } from 'react-map-gl' import { GeocoderConfig as GeocoderConfigOtpUI } from '@opentripplanner/geocoder' /** Accessibility threshold settings */ @@ -222,6 +223,15 @@ export type SupportedOverlays = | Otp1StopsOverlayConfig | MapTileLayerConfig +export interface TransitiveConfig { + disableFlexArc?: boolean + labeledModes?: string[] + styles?: { + labels: Record + segmentLabels: Record + } +} + export interface MapConfig { autoFlyOnTripFormUpdate?: boolean baseLayers?: BaseLayerConfig[] @@ -229,8 +239,9 @@ export interface MapConfig { initLon?: number initZoom?: number maxZoom?: number - navigationControlPosition?: string + navigationControlPosition?: ControlPosition overlays?: SupportedOverlays[] + transitive?: TransitiveConfig views?: MapViewConfig[] } @@ -291,6 +302,7 @@ export interface ItineraryConfig { showPlanFirstLastButtons?: boolean showRouteFares?: boolean sortModes?: ItinerarySortOption[] + syncSortWithDepartArrive?: boolean weights?: ItineraryCostWeights } @@ -338,6 +350,10 @@ export interface TransitOperatorConfig extends TransitOperator { routeIcons?: boolean } +export interface AdvancedSettingsPanelConfig { + saveAndReturnButton?: boolean +} + /** Route Viewer config */ export interface RouteViewerConfig { /** Whether to hide the route linear shape inside a flex zone of that route. */ @@ -358,9 +374,15 @@ export interface StopScheduleViewerConfig { showBlockIds?: boolean } +export interface DateTimeConfig { + dateFormat: string + timeFormat: string +} + /** The main application configuration object */ export interface AppConfig { accessibilityScore?: AccessibilityScoreConfig + advancedSettingsPanel?: AdvancedSettingsPanelConfig api: ApiConfig // Optional on declaration, populated with defaults in reducer if not configured. autoPlan?: boolean | AutoPlanConfig @@ -370,6 +392,8 @@ export interface AppConfig { bugsnag?: BugsnagConfig co2?: CO2Config companies?: Company[] + dateTime?: DateTimeConfig + disableSingleItineraryDays?: boolean elevationProfile?: boolean extraMenuItems?: AppMenuItemConfig[] geocoder: GeocoderConfig diff --git a/lib/util/constants.js b/lib/util/constants.js index 07f0abf3e..0c4303772 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -23,6 +23,7 @@ export const CREATE_ACCOUNT_PLACES_PATH = `${CREATE_ACCOUNT_PATH}/places` export const CREATE_TRIP_PATH = `${TRIPS_PATH}/new` export const TERMS_OF_SERVICE_PATH = '/terms-of-service' export const TERMS_OF_STORAGE_PATH = '/terms-of-storage' +export const TRIP_PREVIEW_PATH = '/previewtrip' // Contains ignored actions when determining timeout, // such as actions triggered by a timer. diff --git a/lib/util/itinerary.tsx b/lib/util/itinerary.tsx index 6cebee5b3..76f96ec0e 100644 --- a/lib/util/itinerary.tsx +++ b/lib/util/itinerary.tsx @@ -1,4 +1,5 @@ import { differenceInMinutes } from 'date-fns' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import { Itinerary, Leg, Place } from '@opentripplanner/types' import { toDate, utcToZonedTime } from 'date-fns-tz' import coreUtils from '@opentripplanner/core-utils' @@ -6,6 +7,7 @@ import hash from 'object-hash' import memoize from 'lodash.memoize' import { AppConfig, CO2Config } from './config-types' +import { checkForRouteModeOverride } from './config' import { WEEKDAYS, WEEKEND_DAYS } from './monitored-trip' export interface ItineraryStartTime { @@ -81,7 +83,7 @@ export function getMinutesUntilItineraryStart(itinerary: Itinerary): number { * Gets the first transit leg of the given itinerary, or null if none found. */ function getFirstTransitLeg(itinerary: Itinerary) { - return itinerary?.legs?.find((leg) => leg.transitLeg) + return itinerary?.legs?.find(isTransitLeg) } /** @@ -373,7 +375,7 @@ export function getTotalFare( ) { hasBikeshare = true } - if (coreUtils.itinerary.isTransit(leg.mode) && transitFare == null) { + if (isTransitLeg(leg) && transitFare == null) { transitFareNotProvided = true } }) @@ -449,3 +451,41 @@ export function addSortingCosts( totalFare } } + +interface LegWithOriginalMode extends Leg { + originalMode?: string +} + +/** Applies route mode overrides to an itinerary. */ +export function applyRouteModeOverrides( + itinerary: Itinerary, + routeModeOverrides: Record +): void { + itinerary.legs.forEach((leg: LegWithOriginalMode) => { + // Use OTP2 leg route first, fallback on legacy leg routeId. + const routeId = typeof leg.route === 'object' ? leg.route.id : leg.routeId + if (routeId) { + leg.originalMode = leg.mode + leg.mode = checkForRouteModeOverride( + { + id: routeId, + mode: leg.mode + }, + routeModeOverrides + ) + } + }) +} + +/** Remove mode overrides from an itinerary */ +export function copyAndRemoveRouteModeOverrides( + itinerary: Itinerary +): Itinerary { + return { + ...itinerary, + legs: itinerary.legs.map((leg: LegWithOriginalMode) => ({ + ...leg, + mode: leg.originalMode || leg.mode + })) + } +} diff --git a/lib/util/state-types.ts b/lib/util/state-types.ts index 3daba23c6..4c5c70a3b 100644 --- a/lib/util/state-types.ts +++ b/lib/util/state-types.ts @@ -5,6 +5,7 @@ import { MonitoredTrip, User } from '../components/user/types' +import { ModeSetting } from '@opentripplanner/types' import { AppConfig } from './config-types' @@ -12,12 +13,10 @@ export interface OtpState { // TODO: Add other OTP states activeSearchId?: string config: AppConfig - filter: { - sort: { - type: string - } - } + currentQuery: any + filter: FilterType location: any + modeSettingDefinitions: ModeSetting[] overlay: any serviceTimeRange?: { end: number @@ -28,6 +27,15 @@ export interface OtpState { ui: any // TODO } +export interface SortType { + direction: string + type: string +} + +export interface FilterType { + sort: SortType +} + export interface UserState { itineraryExistence?: ItineraryExistence localUser?: any diff --git a/lib/util/viewer.js b/lib/util/viewer.js index faa994560..d407b356c 100644 --- a/lib/util/viewer.js +++ b/lib/util/viewer.js @@ -4,6 +4,7 @@ import { getMostReadableTextColor } from '@opentripplanner/core-utils/lib/route' import tinycolor from 'tinycolor2' import { DARK_TEXT_GREY } from '../components/util/colors' +import { isTransitLeg } from '@opentripplanner/core-utils/lib/itinerary' import { checkForRouteModeOverride } from './config' import { getOperatorAndRoute } from './state' @@ -265,7 +266,7 @@ export function getTripStatus( * has realtime info */ export function firstTransitLegIsRealtime(itinerary) { - const firstTransitLeg = itinerary.legs.find((leg) => !!leg.transitLeg) + const firstTransitLeg = itinerary.legs.find((leg) => !!isTransitLeg(leg)) return firstTransitLeg?.realTime } diff --git a/lib/util/webapp-trip-preview-routes.js b/lib/util/webapp-trip-preview-routes.js new file mode 100644 index 000000000..004fd1767 --- /dev/null +++ b/lib/util/webapp-trip-preview-routes.js @@ -0,0 +1,18 @@ +import TripPreviewLayout from '../components/app/trip-preview-layout' + +/** + * Contains mapping of the component(s) to display for the trip preview URL. + * + * Note: This file is separate from webapp-routes to isolate the import of trip preview components + * (YML file from @opentripplanner/trip-details). + * that cause build errors during the a11y test. + */ +const routes = [ + { + a11yIgnore: true, + component: TripPreviewLayout, + path: '/previewtrip/:id' + } +] + +export default routes diff --git a/package.json b/package.json index a94fba861..707a514ca 100644 --- a/package.json +++ b/package.json @@ -42,34 +42,35 @@ "@bugsnag/js": "^7.17.0", "@bugsnag/plugin-react": "^7.17.0", "@floating-ui/react": "^0.19.2", - "@opentripplanner/base-map": "^3.2.2", - "@opentripplanner/building-blocks": "^1.2.3", - "@opentripplanner/core-utils": "11.4.3-mobility-profile", - "@opentripplanner/endpoints-overlay": "^2.1.4", - "@opentripplanner/from-to-location-picker": "^2.1.14", + "@opentripplanner/base-map": "4.0.0", + "@opentripplanner/building-blocks": "2.0.0", + "@opentripplanner/core-utils": "12.0.1", + "@opentripplanner/endpoints-overlay": "3.0.1", + "@opentripplanner/from-to-location-picker": "3.0.0", "@opentripplanner/geocoder": "^3.0.2", "@opentripplanner/humanize-distance": "^1.2.0", - "@opentripplanner/icons": "^2.0.13", - "@opentripplanner/itinerary-body": "^5.3.7", - "@opentripplanner/location-field": "^2.0.24", + "@opentripplanner/icons": "3.0.0", + "@opentripplanner/itinerary-body": "6.0.1", + "@opentripplanner/location-field": "3.0.0", "@opentripplanner/location-icon": "^1.4.1", - "@opentripplanner/map-popup": "^4.0.2", - "@opentripplanner/otp2-tile-overlay": "^1.0.17", - "@opentripplanner/park-and-ride-overlay": "^2.0.9", - "@opentripplanner/printable-itinerary": "^2.0.23", - "@opentripplanner/route-viewer-overlay": "^2.0.17", - "@opentripplanner/stop-viewer-overlay": "^2.0.10", - "@opentripplanner/stops-overlay": "^5.3.3", - "@opentripplanner/transit-vehicle-overlay": "^4.0.13", - "@opentripplanner/transitive-overlay": "3.0.22", - "@opentripplanner/trip-details": "^5.0.15", - "@opentripplanner/trip-form": "^3.6.4", - "@opentripplanner/trip-viewer-overlay": "^2.0.10", - "@opentripplanner/vehicle-rental-overlay": "^2.1.9", + "@opentripplanner/map-popup": "5.1.0", + "@opentripplanner/otp2-tile-overlay": "2.1.0", + "@opentripplanner/park-and-ride-overlay": "3.0.0", + "@opentripplanner/printable-itinerary": "3.0.0", + "@opentripplanner/route-viewer-overlay": "3.0.0", + "@opentripplanner/stop-viewer-overlay": "3.0.0", + "@opentripplanner/stops-overlay": "6.0.0", + "@opentripplanner/transit-vehicle-overlay": "5.0.0", + "@opentripplanner/transitive-overlay": "4.0.0", + "@opentripplanner/trip-details": "6.0.0", + "@opentripplanner/trip-form": "4.0.0", + "@opentripplanner/trip-viewer-overlay": "3.0.0", + "@opentripplanner/vehicle-rental-overlay": "3.0.0", "@styled-icons/fa-regular": "^10.34.0", "@styled-icons/fa-solid": "^10.34.0", "@turf/centroid": "^6.5.0", "@turf/helpers": "^6.5.0", + "@types/react-transition-group": "^4.4.10", "blob-stream": "^0.1.3", "bootstrap": "^3.3.7", "bowser": "^1.9.3", @@ -118,6 +119,7 @@ "react-router-dom": "^5.3.4", "react-select": "^3.1.0", "react-sliding-pane": "^7.0.0", + "react-transition-group": "^4.4.5", "redux": "^4.0.4", "redux-actions": "^1.2.1", "redux-logger": "^2.7.4", @@ -193,6 +195,7 @@ "pinst": "^2.1.6", "prettier": "^2.3.2", "puppeteer": "^10.2.0", + "raw-loader": "^4.0.2", "react-refresh": "^0.10.0", "react-scripts": "^4.0.3", "redux-mock-store": "^1.5.3", diff --git a/percy/percy.test.js b/percy/percy.test.js index c4ac7142e..37433032d 100644 --- a/percy/percy.test.js +++ b/percy/percy.test.js @@ -71,7 +71,7 @@ beforeAll(async () => { // Web security is disabled to allow requests to the mock OTP server browser = await puppeteer.launch({ - args: ['--disable-web-security'] + args: ['--disable-web-security', '--no-sandbox'] //, headless: false }) } catch (error) { @@ -133,16 +133,19 @@ async function executeTest(page, isMobile, isCallTaker) { await page.keyboard.press('Escape') await page.waitForTimeout(200) - // Check submode selector (this will have no effect on mock query) - await page.hover('label[title="Transit"]') + // Open advanced settings and wait for animation + await page.click('#open-advanced-settings-button') await page.waitForTimeout(500) + + // Check submode selector (this will have no effect on mock query) await page.click('#id-query-param-tram') // Enable accessible routing (this will have no effect on mock query) - await page.hover('label[title="Transit"]') - await page.waitForTimeout(500) await page.click('#id-query-param-wheelchair') + // Close advanced settings + await page.click('#close-advanced-settings-button') + await page.waitForTimeout(500) // Delete both origin and destination await page.click('.from-form-control') @@ -189,14 +192,12 @@ async function executeTest(page, isMobile, isCallTaker) { await page.waitForTimeout(1000) // wait extra time for all results to load if (!isMobile) { - await page.hover('label[title="Transit"]') - await page.waitForTimeout(200) - await percySnapshotWithWait( - page, - 'Metro Transit-Walk Itinerary Desktop with Mode Selector Expanded' - ) - // Hover something else to unhover the mode selector. - await page.hover('#plan-trip') + await page.click('#open-advanced-settings-button') + await page.waitForTimeout(500) + await percySnapshotWithWait(page, 'Metro Advanced Settings Open') + // Close advanced settings + await page.click('#close-advanced-settings-button') + await page.waitForTimeout(500) } else { await percySnapshotWithWait(page, 'Metro Transit-Walk Itinerary Mobile') } diff --git a/yarn.lock b/yarn.lock index 86208d8b8..fc46f8d41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2383,12 +2383,12 @@ dependencies: "@octokit/openapi-types" "^10.0.0" -"@opentripplanner/base-map@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@opentripplanner/base-map/-/base-map-3.2.0.tgz#db4410319d9614077ec925d739165a998c4a2485" - integrity sha512-d/yTKEnXqrw9pXhSvCERT+wLFa077Xr4wEFu4pYB+WYoZFflNxuTuAXXjm268HS/d0kjNndkjSMkxaKk6AjsvA== +"@opentripplanner/base-map@4.0.0", "@opentripplanner/base-map@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/base-map/-/base-map-4.0.0.tgz#56ffa1d833673282cc3a0b7a17f388fc5dbd31e3" + integrity sha512-pWTKXxnzUQk43woPMc40uYfGIcGqHV8GoCvRwrIu2pqNw7QAV4rxjZfca0pm5hnbbJ+G83sRzYboILEbEUwMcw== dependencies: - "@opentripplanner/map-popup" "^3.1.0" + "@opentripplanner/building-blocks" "^1.2.2" mapbox-gl "npm:empty-npm-package@1.0.0" maplibre-gl "^2.1.9" react-map-gl "^7.0.15" @@ -2403,19 +2403,24 @@ maplibre-gl "^2.1.9" react-map-gl "^7.0.15" -"@opentripplanner/building-blocks@^1.2.2", "@opentripplanner/building-blocks@^1.2.3": +"@opentripplanner/building-blocks@2.0.0", "@opentripplanner/building-blocks@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/building-blocks/-/building-blocks-2.0.0.tgz#8282c01dff7db5c7e809f6ea91cb52df559a2f9d" + integrity sha512-N07rDaZL8fp552eI9/0j1udKjc0uOpvO0Wv1P19Ge0a4roques463MJgWJ026fbopRCi3uwbc/gYTlh4/ske9A== + +"@opentripplanner/building-blocks@^1.0.3", "@opentripplanner/building-blocks@^1.2.2": version "1.2.3" resolved "https://registry.yarnpkg.com/@opentripplanner/building-blocks/-/building-blocks-1.2.3.tgz#404e8f9038867d66d55f51adf8855b1326c51ed5" integrity sha512-I0AxiZrTZu+e7+av4u0tHW2ijqpxH0AkLHrhf75BHf1Ep2FOGxaul/v+8UT18mNYiM5eHNstOX3XiXaDjtCUaw== -"@opentripplanner/core-utils@11.4.3-mobility-profile": - version "11.4.3-mobility-profile" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-11.4.3-mobility-profile.tgz#b1d5675c6c1f85a3454234b720059efa03ad6ed2" - integrity sha512-+6IjMwXCUZHNmTLnZZ+u23Awtm2v4DcRVmkyxy8m8Dd5GxB9tMxXBhvvwxuzD2KV6Wf4tk4j+HqiAf8gW0Dfbg== +"@opentripplanner/core-utils@12.0.1": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-12.0.1.tgz#2bafb78133393213b4943c76fec5d46436c0fb6d" + integrity sha512-QUTxEcpiOnbqaoiu6RQngTLlQHjSHO4PCMJqR9IRiaei08FnlTx2jgpvIaRla6u7tRNr12YCzptc37+a10ryww== dependencies: "@conveyal/lonlat" "^1.4.1" "@mapbox/polyline" "^1.1.0" - "@opentripplanner/geocoder" "^3.0.0" + "@opentripplanner/geocoder" "^3.0.2" "@styled-icons/foundation" "^10.34.0" "@turf/along" "^6.0.1" chroma-js "^2.4.2" @@ -2426,14 +2431,14 @@ lodash.isequal "^4.5.0" qs "^6.9.1" -"@opentripplanner/core-utils@^11.4.0": - version "11.4.2" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-11.4.2.tgz#cc6034fb80ccda44e50f7f0a1e80a7bad8387f84" - integrity sha512-EVYVN73Cgf9IC+uya49843MFJnVkmv0nHAjsQwmPGSx/w5fY49X4fSpDprL7Bn+MTzk58U2udDsn6OzKmV0JdA== +"@opentripplanner/core-utils@^11.4.4": + version "11.4.5" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-11.4.5.tgz#f568f5f60f153f0bd74fe47ed3134851067b0064" + integrity sha512-uYaVqZXZoRRVks05KdGMTIeGEC7ItKJvexZqKsEbZMjnnMphHRndv4aSDXM19iL7ynoau7JXjYfYEny1HDp7ig== dependencies: "@conveyal/lonlat" "^1.4.1" "@mapbox/polyline" "^1.1.0" - "@opentripplanner/geocoder" "^3.0.0" + "@opentripplanner/geocoder" "^3.0.2" "@styled-icons/foundation" "^10.34.0" "@turf/along" "^6.0.1" chroma-js "^2.4.2" @@ -2444,10 +2449,10 @@ lodash.isequal "^4.5.0" qs "^6.9.1" -"@opentripplanner/core-utils@^11.4.4": - version "11.4.5" - resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-11.4.5.tgz#f568f5f60f153f0bd74fe47ed3134851067b0064" - integrity sha512-uYaVqZXZoRRVks05KdGMTIeGEC7ItKJvexZqKsEbZMjnnMphHRndv4aSDXM19iL7ynoau7JXjYfYEny1HDp7ig== +"@opentripplanner/core-utils@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/core-utils/-/core-utils-12.0.0.tgz#cc40af92620b207f4dce817d08f99def0cdaea7a" + integrity sha512-udLF8XU+k7gxZ+yyyw7ASz6/4D540zYIv8a9GbUL61TF8HmgGhcMk3XOgBnm5jdOukuaNNpOFE4J3oJc5QsSBQ== dependencies: "@conveyal/lonlat" "^1.4.1" "@mapbox/polyline" "^1.1.0" @@ -2462,22 +2467,22 @@ lodash.isequal "^4.5.0" qs "^6.9.1" -"@opentripplanner/endpoints-overlay@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@opentripplanner/endpoints-overlay/-/endpoints-overlay-2.1.4.tgz#f81088bce83236344dfa4a51b2efe00092a5c87a" - integrity sha512-VLRZArhoRQ38aafc/w986Uv1lnq/WLJOgBqnpvuUbLhLR/qHU9h5X3wg3jgwf2GA0BIn03Z99VJbCkGfkyd0LA== +"@opentripplanner/endpoints-overlay@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/endpoints-overlay/-/endpoints-overlay-3.0.1.tgz#b6b8e2f08ae41fbaad475fc0f0fe3e72d7d36463" + integrity sha512-X3T0GM8U+VU/mOOSNUgj6fVcjAKMeciKFYnQNbKiNgNeDHa5JltwvtXsM4x3wCLP2xAF6jH/HTWJmYmsfLPlAw== dependencies: - "@opentripplanner/base-map" "^3.2.2" - "@opentripplanner/building-blocks" "^1.2.2" - "@opentripplanner/core-utils" "^11.4.4" + "@opentripplanner/base-map" "^4.0.0" + "@opentripplanner/building-blocks" "^2.0.0" + "@opentripplanner/core-utils" "^12.0.0" "@opentripplanner/location-icon" "^1.4.1" "@styled-icons/fa-solid" "^10.34.0" flat "^5.0.2" -"@opentripplanner/from-to-location-picker@^2.1.12", "@opentripplanner/from-to-location-picker@^2.1.13": - version "2.1.13" - resolved "https://registry.yarnpkg.com/@opentripplanner/from-to-location-picker/-/from-to-location-picker-2.1.13.tgz#d13acd582929175c676cd4303a6cdc6e1c289d99" - integrity sha512-6/7+wYQuuQhnGvxkDQcvoACdmuwUL1BlPqBIUFwyBpkdJ1VQGZiUSAAZTxXdY1Fv/p5mKR1vRsvZgtSPhcxgcg== +"@opentripplanner/from-to-location-picker@3.0.0", "@opentripplanner/from-to-location-picker@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/from-to-location-picker/-/from-to-location-picker-3.0.0.tgz#048a596c2f854825e0058e03dac67dcad7eb5864" + integrity sha512-jRXaY9jKg+PXUL7Z2SkHRyO88xG1t7iG3U449LPiCm/6flxsY+Wlxg+nyMsAP5gQMjOU0wsGLdH83lGgrpSF4A== dependencies: "@opentripplanner/location-icon" "^1.4.1" flat "^5.0.2" @@ -2490,17 +2495,6 @@ "@opentripplanner/location-icon" "^1.4.1" flat "^5.0.2" -"@opentripplanner/geocoder@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@opentripplanner/geocoder/-/geocoder-3.0.1.tgz#834960bc52f515e1223346a8002fb847674d33bc" - integrity sha512-+LHTqY8pHmPE39IjVev5T5baa+BohEyvsLwVwFB2bYWzM+m/RgAJ188uBcDzXKdqk5y3dZR9ZODYVMtrvIiKzQ== - dependencies: - "@conveyal/geocoder-arcgis-geojson" "^0.0.3" - "@conveyal/lonlat" "^1.4.1" - "@leeoniya/ufuzzy" "^1.0.14" - isomorphic-mapzen-search "^1.6.1" - lodash.memoize "^4.1.2" - "@opentripplanner/geocoder@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@opentripplanner/geocoder/-/geocoder-3.0.2.tgz#2c7618947d1d9b082bd39d037327c9cf23282782" @@ -2517,7 +2511,15 @@ resolved "https://registry.yarnpkg.com/@opentripplanner/humanize-distance/-/humanize-distance-1.2.0.tgz#71cf5d5d1b756adef15300edbba0995ccd4b35ee" integrity sha512-x0QRXMDhypFeazZ6r6vzrdU8vhiV56nZ/WX6zUbxpgp6T9Oclw0gwR2Zdw6DZiiFpSYVNeVNxVzZwsu6NRGjcA== -"@opentripplanner/icons@^2.0.12", "@opentripplanner/icons@^2.0.13": +"@opentripplanner/icons@3.0.0", "@opentripplanner/icons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/icons/-/icons-3.0.0.tgz#f7293fd4dd2625eace3a4c82ecd573d6000d85d3" + integrity sha512-naSCdCsPwSyEiP7Vf6oN6dpgwpFIkeQFXfTJG7lp1Dg9emLTAYzRx/f+45e9Bh0zP0aA4DsN4VgHBQllyu82qQ== + dependencies: + "@opentripplanner/core-utils" "^11.4.4" + prop-types "^15.7.2" + +"@opentripplanner/icons@^2.0.12": version "2.0.13" resolved "https://registry.yarnpkg.com/@opentripplanner/icons/-/icons-2.0.13.tgz#45c4c16d8f208cff73811941f2def0fa23f87780" integrity sha512-1oEPCmFuyS88bJZ2U9eFlEw2kQ0ZZW+wOI1dggr0omJDD6L+nVNQJ6TUtosNHYL1S35Jpx4aSQEG3iwwlXOHMg== @@ -2525,7 +2527,25 @@ "@opentripplanner/core-utils" "^11.4.4" prop-types "^15.7.2" -"@opentripplanner/itinerary-body@^5.3.6", "@opentripplanner/itinerary-body@^5.3.7": +"@opentripplanner/itinerary-body@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/itinerary-body/-/itinerary-body-6.0.1.tgz#74139536b34083af5b324fb94e69be267ea6bbeb" + integrity sha512-6Z+ZEW28MwtteOwZZUjkPkTnYQ0Aq1lMpfKMZW7F+OD6hfKhFBUx4NLMExTffXHswXQ3faYaHScOHxpalX73UQ== + dependencies: + "@opentripplanner/core-utils" "^12.0.0" + "@opentripplanner/humanize-distance" "^1.2.0" + "@opentripplanner/icons" "^3.0.0" + "@opentripplanner/location-icon" "^1.4.1" + "@styled-icons/fa-solid" "^10.34.0" + "@styled-icons/foundation" "^10.34.0" + date-fns "^2.28.0" + date-fns-tz "^1.2.2" + flat "^5.0.2" + react-animate-height "^3.0.4" + react-resize-detector "^4.2.1" + string-similarity "^4.0.4" + +"@opentripplanner/itinerary-body@^5.3.6": version "5.3.7" resolved "https://registry.yarnpkg.com/@opentripplanner/itinerary-body/-/itinerary-body-5.3.7.tgz#e32437f804defc19aef23685e45c0a363ac8bc31" integrity sha512-ZnbxI78WsY6/Ynl3QY026JqimGG+gqXcN8ZEXY/BPMrNhSul27HgFFT2Flo37Fnc5gN2+jE8LsR1yxq05H3kvQ== @@ -2543,10 +2563,10 @@ react-resize-detector "^4.2.1" string-similarity "^4.0.4" -"@opentripplanner/location-field@^2.0.24": - version "2.0.24" - resolved "https://registry.yarnpkg.com/@opentripplanner/location-field/-/location-field-2.0.24.tgz#32e7109142bd754d28bd28ebbdf68d4e5ef4c812" - integrity sha512-fOAyanDnLLHC39kHG6kMSY6i09n4l0KSVQACFoosGZgUcJmz5CUCMl0/x3RszIwh3g2wqxKh6fagh4V56YEpfQ== +"@opentripplanner/location-field@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/location-field/-/location-field-3.0.0.tgz#a6f8743290cf088bcd589cf8e6b0d07a651b704a" + integrity sha512-wPb9l5pvSeocZ45K1E3Zeb4hDjJtP9tw/S0MQ3HXYEQSf+700n4CWXVXYReYJXJYriOzge5/2whjUPGTlJckWw== dependencies: "@conveyal/geocoder-arcgis-geojson" "^0.0.3" "@opentripplanner/core-utils" "^11.4.4" @@ -2564,16 +2584,18 @@ "@styled-icons/fa-regular" "^10.34.0" "@styled-icons/fa-solid" "^10.34.0" -"@opentripplanner/map-popup@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@opentripplanner/map-popup/-/map-popup-3.1.1.tgz#54f081162d328cc4bb0e89562f9ea200e29e01a1" - integrity sha512-yWBIPuYGw7biaRNIpglQm5+opZ+D5QQgXHLhKnYaCR0eNijjl9cx34lGXdyKPXt26S6MiyJZXL81uc6w6CnQ3A== +"@opentripplanner/map-popup@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/map-popup/-/map-popup-5.1.0.tgz#cf6374bf7b69af69c026ec414a84719078c56e9e" + integrity sha512-EShoMyFZa7Zb2ZZrJhEsJfuCAvs2jfQe5QstU+AEk5Jm1zc8LzU6PsXmizQ/RMVi6zIYLhlBoZ3u458tTA3VQA== dependencies: - "@opentripplanner/core-utils" "^11.4.0" - "@opentripplanner/from-to-location-picker" "^2.1.12" + "@opentripplanner/base-map" "^4.0.0" + "@opentripplanner/building-blocks" "^2.0.0" + "@opentripplanner/core-utils" "^12.0.0" + "@opentripplanner/from-to-location-picker" "^3.0.0" flat "^5.0.2" -"@opentripplanner/map-popup@^4.0.0", "@opentripplanner/map-popup@^4.0.2": +"@opentripplanner/map-popup@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@opentripplanner/map-popup/-/map-popup-4.0.2.tgz#367ab7ce69d16d319988bb48d8f6d2db762623c7" integrity sha512-RlHv9GE3Bk3++PwBcaPcALr6rZ+2AxY6Uj6W71AnLqz+wbeQO5rM3eEP99r0Sg1K3pAY0hXljBkVwKiUWhwxbQ== @@ -2584,33 +2606,44 @@ "@opentripplanner/from-to-location-picker" "^2.1.14" flat "^5.0.2" -"@opentripplanner/otp2-tile-overlay@^1.0.17": - version "1.0.17" - resolved "https://registry.yarnpkg.com/@opentripplanner/otp2-tile-overlay/-/otp2-tile-overlay-1.0.17.tgz#0e63bcb956778bbab6bd42d282aa9f5416881b74" - integrity sha512-mgMHprVVOXdzgU0D/50be57TeuEp1RP+b7xH/3Xt+rj0mF1PY+5GgLMuEul+1WqbzFwAefrmi9KRMs3MYJArrg== +"@opentripplanner/map-popup@^v3.2.0-alpha.1": + version "3.2.0-alpha.1" + resolved "https://registry.yarnpkg.com/@opentripplanner/map-popup/-/map-popup-3.2.0-alpha.1.tgz#dcad38c103500f7c5ad3c632398204849ed5885e" + integrity sha512-Z0RsyC7wkYU/aOLYQFsJI5tBhzooEE/sQZROX2WODkDWAv4Qfj95ppS8pvNkpoZ0N4fioFcj5aM2VGXVMSy0EA== dependencies: - "@opentripplanner/map-popup" "^4.0.0" + "@opentripplanner/base-map" "^3.2.2" + "@opentripplanner/building-blocks" "^1.2.2" + "@opentripplanner/core-utils" "^11.4.4" + "@opentripplanner/from-to-location-picker" "^2.1.14" + flat "^5.0.2" -"@opentripplanner/park-and-ride-overlay@^2.0.9": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@opentripplanner/park-and-ride-overlay/-/park-and-ride-overlay-2.0.9.tgz#0efe2bf8a7595b56c4da6396e89db5f04e4b3ec8" - integrity sha512-ekf6kcCgMVTzXDMY3Ed8qclaL3YY2/1BrArdpRY8DxciGWmE1HKOW90Vf1aP18aLrwcW9kpvv1Kdbl60tY6mCQ== +"@opentripplanner/otp2-tile-overlay@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/otp2-tile-overlay/-/otp2-tile-overlay-2.1.0.tgz#745cb6c80dbde767a0b5ac7b0b866193a18ec984" + integrity sha512-gkKS5OT/Ayc/987vcdSkFcGSH/YyvEBN9bZFWBHKRN5nbRykBRZu2GNFVfN5ITLoshrFw+YasIk9omfTKVJtRg== dependencies: - "@opentripplanner/base-map" "^3.2.0" - "@opentripplanner/from-to-location-picker" "^2.1.13" + "@opentripplanner/map-popup" "^v3.2.0-alpha.1" -"@opentripplanner/printable-itinerary@^2.0.23": - version "2.0.23" - resolved "https://registry.yarnpkg.com/@opentripplanner/printable-itinerary/-/printable-itinerary-2.0.23.tgz#31f11e0e22bb9f223c4c1d8fa97c6eea18496cfa" - integrity sha512-Cb5SX7ts0rX0P1XSStjqbTITP5ueWlDe1b7U4KMaRNqEOaK2tELEyb/qZGo9kMpXTT/17jmEQZsKHQj43C5Eyg== +"@opentripplanner/park-and-ride-overlay@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/park-and-ride-overlay/-/park-and-ride-overlay-3.0.0.tgz#7139d5358bc8e5f68f1574e9225a3bcfb57993be" + integrity sha512-MLalzHhXSZ1yAwW9cyMVoUUQPMGktTTPFbhgEB/Ft4fX/TJAIH1sQHsKoaeGE0+48Kxg4M49zPoeNujr9RIeGA== + dependencies: + "@opentripplanner/base-map" "^3.2.2" + "@opentripplanner/from-to-location-picker" "^2.1.14" + +"@opentripplanner/printable-itinerary@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/printable-itinerary/-/printable-itinerary-3.0.0.tgz#7f4438b18cbd767ede74b2a3b7e3ad6ab1ec936b" + integrity sha512-/P7HtzXPu0yP6ZnfHl1DQFTVgEx+YplLxs43jd7rnXc0PUvsVyAsmp4oI3TDG8tStMD9pKsVLOCBNLUacoXo1Q== dependencies: "@opentripplanner/core-utils" "^11.4.4" "@opentripplanner/itinerary-body" "^5.3.6" -"@opentripplanner/route-viewer-overlay@^2.0.17": - version "2.0.17" - resolved "https://registry.yarnpkg.com/@opentripplanner/route-viewer-overlay/-/route-viewer-overlay-2.0.17.tgz#f34686fd965cf39650a10f64df599e7aca468415" - integrity sha512-3UTTLxHhaMg4iKP4oJlobvUCbvC/TjCW6ss8PxxC3UurwiMeSFNVkaWGLElPc9YoKg0QqKxrIY7zq0WClIPa6g== +"@opentripplanner/route-viewer-overlay@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/route-viewer-overlay/-/route-viewer-overlay-3.0.0.tgz#d307af1fb734f113627a82f816bf4683a7d9a860" + integrity sha512-Kbgs+gkzOKNEoXG2ImoUBekET5olMc8sGo9ltb7tgTg8ZuBcq3woMID7b3+n3DDdvqdHLExg5T68gkip1Eu/QQ== dependencies: "@mapbox/polyline" "^1.1.0" "@opentripplanner/base-map" "^3.2.2" @@ -2628,38 +2661,38 @@ glob-promise "^4.2.2" js-yaml "^4.1.0" -"@opentripplanner/stop-viewer-overlay@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@opentripplanner/stop-viewer-overlay/-/stop-viewer-overlay-2.0.10.tgz#0749fe7ffb28dac7a6925ed7c663e2fdd5156f3b" - integrity sha512-rFmaqQ7uJ+ZE80O6fveiNxlEVSJW5PwFSMh1B9pN0HaVTB1U27+yKbIMMuP7GtWve31mPy+PaWYXDW3hMlsi8A== +"@opentripplanner/stop-viewer-overlay@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/stop-viewer-overlay/-/stop-viewer-overlay-3.0.0.tgz#69dc8af797ab9a202954137b6ea68bcaa45cc4ff" + integrity sha512-1OnLhP9QcOnAe1qwSrjLXsFsMtqx7tBullcl/CgT5kYA/aXLKNiQ7zGeMrfbALOXhU51v74gtmMEAwW1EB1pcw== dependencies: "@opentripplanner/base-map" "^3.2.2" "@opentripplanner/core-utils" "^11.4.4" -"@opentripplanner/stops-overlay@^5.3.3": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@opentripplanner/stops-overlay/-/stops-overlay-5.3.3.tgz#711dd9316de93ac6c959dc3191d31884dc0a9589" - integrity sha512-GYADErZLIG3KPJ4OiigWkyHaGrWJ/wiCenH0RTbQ5J0XbYrenUIlWBsI5kpQNZPGBXocvF77sOTOUM8gex1AUA== +"@opentripplanner/stops-overlay@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/stops-overlay/-/stops-overlay-6.0.0.tgz#97186a2ce82284e2632967a4666294ad04d092db" + integrity sha512-zGpCc2PWpHDn1VGu9zdLfRIfde9sNAMN6ElHkLQq/67ylrQ7R1jIzi9K1O2X/ZPAEtfH5VOYa/y/A1drNlN3gw== dependencies: "@opentripplanner/base-map" "^3.2.2" "@opentripplanner/from-to-location-picker" "^2.1.14" "@opentripplanner/map-popup" "^4.0.0" flat "^5.0.2" -"@opentripplanner/transit-vehicle-overlay@^4.0.13": - version "4.0.13" - resolved "https://registry.yarnpkg.com/@opentripplanner/transit-vehicle-overlay/-/transit-vehicle-overlay-4.0.13.tgz#6f66c7c6a27d2473353b7c8c14acc8c540fb01ad" - integrity sha512-Sh3c3+q2dIhFZP5uyhPboyLSsTv8unVsPYKjFWVJaK19z6wN8KqFVmSqWysrcEnooRJd+D4+4erQiUbabomDLQ== +"@opentripplanner/transit-vehicle-overlay@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/transit-vehicle-overlay/-/transit-vehicle-overlay-5.0.0.tgz#1a3ea04abce38d65a21a9eaad8c5e0ac19a41100" + integrity sha512-N0vM89xozWTbbb63pBeZEZIbdlbgnP2K0qNJZ0vk87clJU0T7RxBtq/sdftBQ6CzB86AjXCCxfitCZH9HOD7/A== dependencies: "@opentripplanner/base-map" "^3.2.2" "@opentripplanner/core-utils" "^11.4.4" "@opentripplanner/icons" "^2.0.12" flat "^5.0.2" -"@opentripplanner/transitive-overlay@3.0.22": - version "3.0.22" - resolved "https://registry.yarnpkg.com/@opentripplanner/transitive-overlay/-/transitive-overlay-3.0.22.tgz#631096b12e08671e5da05ffe1c0a0e01331750a4" - integrity sha512-Ix3+2qz1+iSbeLnMfd4tU+0AUU1LDjq4y8cTzfKHayqHe0pzHpYY9Ib2zrXbvog7Mav/Jozn2ycL27R4UgzQaA== +"@opentripplanner/transitive-overlay@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/transitive-overlay/-/transitive-overlay-4.0.0.tgz#1a0c41dd4a22704b74da105ef67c0ac63624c335" + integrity sha512-kFKRbKGeIKNDx2t21HaPXsOOkG+qlnHgXN8lbTZRxpfy+EIV3ZeoH72/ymWuiHdLnxUORHBAjhacQh+2fryc4A== dependencies: "@mapbox/polyline" "^1.1.1" "@opentripplanner/base-map" "^3.2.2" @@ -2673,22 +2706,23 @@ "@turf/midpoint" "^6.5.0" lodash.isequal "^4.5.0" -"@opentripplanner/trip-details@^5.0.15": - version "5.0.15" - resolved "https://registry.yarnpkg.com/@opentripplanner/trip-details/-/trip-details-5.0.15.tgz#73cfd7427aed49af53fec4ded9de7d17b0ed5377" - integrity sha512-1OfCEju90PXGH9DVy2dbBk8Jz8/8zSJ35/OCgodenGT3FyokQPoJsQhPjr6MPIIYMTyAdUSDT9C372thA+rU2Q== +"@opentripplanner/trip-details@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/trip-details/-/trip-details-6.0.0.tgz#60958ca14910f865125d20db8ec2eeba6d2c7362" + integrity sha512-BzUAsSpuSWzVMRW6vhiZblrg0Sq7Tnar17jjgvTvIhzv0yUUyrW5jUxlGIPPk11UUJ5WrPZmkfQasF9z8xk5BQ== dependencies: "@opentripplanner/core-utils" "^11.4.4" "@styled-icons/fa-solid" "^10.34.0" flat "^5.0.2" react-animate-height "^3.0.4" -"@opentripplanner/trip-form@^3.6.4": - version "3.6.4" - resolved "https://registry.yarnpkg.com/@opentripplanner/trip-form/-/trip-form-3.6.4.tgz#e741bb190dbea845c826d0021618daf568cc3df7" - integrity sha512-wnoJyI8jR3DbtTmg//FWWy+yhd6Yej843XxSnnGCuBhZjUnvC9aYffKa/FI1W9s8Xw+tJ7DsZ9wADiSOWEjmwA== +"@opentripplanner/trip-form@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/trip-form/-/trip-form-4.0.0.tgz#27b36a0504e46cfda1cdf50c971b5cea03de8ae2" + integrity sha512-Cg8SlAVN8M+qoWpz8jAkwuqllIPgrL2PVewTPuDPsIQ8i5B7xo5KKE3TPo7cQUM+jE6WEshpvv0FIdMF+NAlNg== dependencies: "@floating-ui/react" "^0.19.2" + "@opentripplanner/building-blocks" "^1.0.3" "@opentripplanner/core-utils" "^11.4.4" "@styled-icons/bootstrap" "^10.34.0" "@styled-icons/boxicons-regular" "^10.38.0" @@ -2696,13 +2730,14 @@ "@styled-icons/fa-solid" "^10.37.0" date-fns "^2.28.0" flat "^5.0.2" + react-animate-height "^3.0.4" react-indiana-drag-scroll "^2.0.1" react-inlinesvg "^2.3.0" -"@opentripplanner/trip-viewer-overlay@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@opentripplanner/trip-viewer-overlay/-/trip-viewer-overlay-2.0.10.tgz#2c0809b2d54da4d57d0a0683a4739e29cb13a326" - integrity sha512-7M9l7fF8shtD/566bci+zEkPncf/L+ZWIYAl5gnIgrBxwLagN/+E2zkoDebYamGFGb236FXpvTS30i1BJzhcPA== +"@opentripplanner/trip-viewer-overlay@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/trip-viewer-overlay/-/trip-viewer-overlay-3.0.0.tgz#a56263863aba87a3608d4dbed0cf839333cf99d0" + integrity sha512-64qNLUxkL6ow8T1FghdKDp13KH96djjYAo6hHTAeZYHZuu7gxffRD1GmrDJVeyk0+DixjKdiNhAZ9mOCtAgK/w== dependencies: "@mapbox/polyline" "^1.1.0" "@opentripplanner/base-map" "^3.2.2" @@ -2713,10 +2748,10 @@ resolved "https://registry.yarnpkg.com/@opentripplanner/types/-/types-6.5.2.tgz#1373d738479568d880a3b13670b0ec53a1a75bd5" integrity sha512-2qDcKOrsLoXdwjRAdi4xcdDUsZGTnwBM+vfEf8TTuuWSnA+WYav3ldlMB4sugxIdLaVKXlOfe3F5lCEh9jAHWA== -"@opentripplanner/vehicle-rental-overlay@^2.1.9": - version "2.1.9" - resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-2.1.9.tgz#c373e1400874a00f473be0f029b28e0944652c88" - integrity sha512-VYWqnuk5j1yHF/zH5NEqDIVjsSbIsIgiCK6SaYtQHOBwszWauIubpqviTBASAcY72JfKE36AQfGjPuYILd9oTw== +"@opentripplanner/vehicle-rental-overlay@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@opentripplanner/vehicle-rental-overlay/-/vehicle-rental-overlay-3.0.0.tgz#68e2a6c117fa3af485f375685c24a557762c6bb6" + integrity sha512-Yt6l6fEX+5O1jJ4z2NpbA0rib2tARxKdE3nTUFaBORBSq/NFfa2vZMk3EBXIrtzC3/GRabqluJU8sDaMGfkvvw== dependencies: "@opentripplanner/base-map" "^3.2.2" "@opentripplanner/core-utils" "^11.4.4" @@ -3695,6 +3730,13 @@ "@types/history" "^4.7.11" "@types/react" "*" +"@types/react-transition-group@^4.4.10": + version "4.4.11" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" + integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@17": version "17.0.38" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" @@ -15124,6 +15166,14 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rc@^1.0.1, rc@^1.1.6, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -15544,6 +15594,16 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@<17.0.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"