From 92cc1b386fbf5cdb41c471eebcdb853957ad3e15 Mon Sep 17 00:00:00 2001 From: Cristen Jones Date: Tue, 10 Dec 2024 17:17:24 -0500 Subject: [PATCH] feat(TripPlanner): transit leg and itinerary locations (#2251) --------- Co-authored-by: Josh Larson --- assets/tailwind.config.js | 4 +- .../trip_planner/itinerary_detail.ex | 38 +++--- lib/dotcom_web/components/trip_planner/leg.ex | 101 --------------- .../components/trip_planner/place.ex | 73 +++++++++++ .../components/trip_planner/transit_leg.ex | 122 ++++++++++++++++++ .../components/trip_planner/walking_leg.ex | 47 +++---- mix.exs | 2 +- mix.lock | 2 +- 8 files changed, 240 insertions(+), 149 deletions(-) delete mode 100644 lib/dotcom_web/components/trip_planner/leg.ex create mode 100644 lib/dotcom_web/components/trip_planner/place.ex create mode 100644 lib/dotcom_web/components/trip_planner/transit_leg.ex diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index e12c5e97f2..a2e619f98e 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -38,7 +38,9 @@ module.exports = { DEFAULT: "#494f5c", dark: "#1c1e23", light: "#788093", - lighter: "#b0b5c0" + lighter: "#b0b5c0", + lightest: "#e9eaed", + "bordered-background": "#f2f3f5" }, "brand-primary": { DEFAULT: "#165c96", diff --git a/lib/dotcom_web/components/trip_planner/itinerary_detail.ex b/lib/dotcom_web/components/trip_planner/itinerary_detail.ex index 4496053c3b..1f226f3124 100644 --- a/lib/dotcom_web/components/trip_planner/itinerary_detail.ex +++ b/lib/dotcom_web/components/trip_planner/itinerary_detail.ex @@ -6,10 +6,11 @@ defmodule DotcomWeb.Components.TripPlanner.ItineraryDetail do use DotcomWeb, :component - import DotcomWeb.Components.TripPlanner.Leg, only: [leg: 1] + import DotcomWeb.Components.TripPlanner.Place + import DotcomWeb.Components.TripPlanner.TransitLeg, only: [transit_leg: 1] import DotcomWeb.Components.TripPlanner.WalkingLeg, only: [walking_leg: 1] - alias Dotcom.TripPlan.PersonalDetail + alias Dotcom.TripPlan.{PersonalDetail, TransitDetail} def itinerary_detail( %{ @@ -78,35 +79,26 @@ defmodule DotcomWeb.Components.TripPlanner.ItineraryDetail do defp specific_itinerary_detail(assigns) do assigns = - assign( - assigns, - :all_routes, - assigns.itinerary.legs - |> Enum.reject(&match?(%PersonalDetail{}, &1.mode)) - |> Enum.map(& &1.mode.route) - ) + assigns + |> assign(:start_place, List.first(assigns.itinerary.legs).from) + |> assign(:start_time, List.first(assigns.itinerary.legs).start) + |> assign(:end_place, List.last(assigns.itinerary.legs).to) + |> assign(:end_time, List.last(assigns.itinerary.legs).stop) ~H"""
-
- Depart at {Timex.format!(@itinerary.start, "%-I:%M%p", :strftime)} - <.route_symbol :for={route <- @all_routes} route={route} class="ml-2" /> -
-
+ <.place place={@start_place} time={@start_time} /> +
<%= if match?(%PersonalDetail{}, leg.mode) do %> <.walking_leg leg={leg} /> <% else %> - <.leg - start_time={leg.start} - end_time={leg.stop} - from={leg.from} - to={leg.to} - mode={leg.mode} - realtime={leg.realtime} - realtime_state={leg.realtime_state} - /> + <.transit_leg leg={leg} /> <% end %>
+ <.place place={@end_place} time={@end_time} />
""" end diff --git a/lib/dotcom_web/components/trip_planner/leg.ex b/lib/dotcom_web/components/trip_planner/leg.ex deleted file mode 100644 index 3d8dd12cbf..0000000000 --- a/lib/dotcom_web/components/trip_planner/leg.ex +++ /dev/null @@ -1,101 +0,0 @@ -defmodule DotcomWeb.Components.TripPlanner.Leg do - @moduledoc """ - Renders information about an itinerary's leg, going from one place to another via either transit or walking. - """ - - use DotcomWeb, :component - - alias Dotcom.TripPlan.{PersonalDetail, TransitDetail} - alias OpenTripPlannerClient.Schema.Step - alias Stops.Stop - - attr(:from, :any) - attr(:to, :any) - attr(:start_time, :any) - attr(:end_time, :any) - attr(:mode, :any) - attr(:realtime, :boolean) - attr(:realtime_state, :string) - - def leg(assigns) do - assigns = - assign( - assigns, - :realtime_class, - if(assigns.realtime, - do: "bg-emerald-100 border-emerald-600 border-4", - else: "bg-zinc-100 border-zinc-300" - ) - ) - - ~H""" -
-
- {@from.name} - {format_time(@start_time)} -
-
-

Realtime data: {@realtime_state}

- <.mode mode={@mode} /> -
-
- {@to.name} - {format_time(@end_time)} -
-
- """ - end - - defp format_time(datetime), do: Timex.format!(datetime, "%-I:%M %p", :strftime) - - attr(:mode, :any) - - def mode(assigns) do - case assigns.mode do - %PersonalDetail{} -> - ~H""" - <.walking_steps steps={assigns.mode.steps} /> - """ - - %TransitDetail{} -> - ~H""" - <.transit - route={assigns.mode.route} - trip_id={assigns.mode.trip_id} - stops={assigns.mode.intermediate_stops} - /> - """ - end - end - - def transit(assigns) do - ~H""" -
- <.route_symbol route={@route} /> ({@route.name}) on trip {@trip_id} -
-
    -
  • - {stop.name} -
    - <.icon - type="icon-svg" - name="icon-accessible-small" - class="h-3 w-3 mr-0.5" - aria-hidden="true" - /> Accessible -
    -
  • -
- """ - end - - def walking_steps(assigns) do - ~H""" -
    -
  • - {Step.walk_summary(step)} -
  • -
- """ - end -end diff --git a/lib/dotcom_web/components/trip_planner/place.ex b/lib/dotcom_web/components/trip_planner/place.ex new file mode 100644 index 0000000000..b0e7d26383 --- /dev/null +++ b/lib/dotcom_web/components/trip_planner/place.ex @@ -0,0 +1,73 @@ +defmodule DotcomWeb.Components.TripPlanner.Place do + @moduledoc """ + A component to display a specific location in the itinerary detail. + """ + + use DotcomWeb, :component + + alias Routes.Route + alias Stops.Stop + + attr :place, :map, required: true + attr :time, :any, required: true + attr :route, :map, default: nil + + def place(assigns) do + stop_url = stop_url(assigns.route, assigns.place.stop) + + assigns = + assign(assigns, %{ + stop_url: stop_url, + tag_name: if(stop_url, do: "a", else: "div") + }) + + ~H""" + <.dynamic_tag + tag_name={@tag_name} + href={@stop_url} + class="bg-gray-bordered-background px-3 py-2 rounded-lg grid grid-cols-[1.5rem_auto_1fr] items-center gap-2 w-full hover:no-underline text-black" + > + <.location_icon route={@route} class="h-6 w-6" /> + + {@place.name} + <.icon + :if={!is_nil(@place.stop) and Stop.accessible?(@place.stop)} + type="icon-svg" + name="icon-accessible-default" + class="h-5 w-5 ml-0.5 shrink-0" + aria-hidden="true" + /> + + + + """ + end + + defp stop_url(%Route{external_agency_name: nil}, %Stop{} = stop) do + ~p"/stops/#{stop}" + end + + defp stop_url(_, _), do: nil + + defp location_icon(%{route: %Route{}} = assigns) do + icon_name = + if(Routes.Route.type_atom(assigns.route) in [:bus, :logan_express, :massport_shuttle], + do: "icon-stop-default", + else: "icon-circle-t-default" + ) + + assigns = assign(assigns, :icon_name, icon_name) + + ~H""" + <.icon type="icon-svg" class={@class} name={@icon_name} /> + """ + end + + defp location_icon(assigns) do + ~H""" + <.icon class={"#{@class} fill-brand-primary"} name="location-dot" /> + """ + end + + defp format_time(datetime), do: Timex.format!(datetime, "%-I:%M %p", :strftime) +end diff --git a/lib/dotcom_web/components/trip_planner/transit_leg.ex b/lib/dotcom_web/components/trip_planner/transit_leg.ex new file mode 100644 index 0000000000..41a61a7f86 --- /dev/null +++ b/lib/dotcom_web/components/trip_planner/transit_leg.ex @@ -0,0 +1,122 @@ +defmodule DotcomWeb.Components.TripPlanner.TransitLeg do + @moduledoc """ + A transit leg of a trip. + Includes styling for the traversed route & a list of intermediate stops. + """ + + use Phoenix.Component + + import DotcomWeb.Components.RouteSymbols, only: [route_symbol: 1] + import DotcomWeb.Components.TripPlanner.Place + import MbtaMetro.Components.Icon, only: [icon: 1] + + alias Dotcom.TripPlan.TransitDetail + alias Routes.Route + + @doc """ + Renders a transit leg. + + Must be given a `leg` + """ + + attr :leg, :any, required: true + + def transit_leg(assigns) do + ~H""" +
+ <.place + place={@leg.from} + time={@leg.start} + route={if(match?(%TransitDetail{}, @leg.mode), do: @leg.mode.route)} + /> +
+ <%= if Enum.count(@leg.mode.intermediate_stops) < 2 do %> + <.leg_summary leg={@leg} /> + <.leg_details leg={@leg} /> + <% else %> +
+ + <.leg_summary leg={@leg} /> + <.icon + name="chevron-up" + class="group-open:rotate-180 w-4 h-4 absolute top-3 right-3 fill-brand-primary" + /> + + <.leg_details leg={@leg} /> +
+ <% end %> +
+ <.place + place={@leg.to} + time={@leg.stop} + route={if(match?(%TransitDetail{}, @leg.mode), do: @leg.mode.route)} + /> +
+ """ + end + + defp leg_line_class(%Route{external_agency_name: "Massport"}) do + "border-massport" + end + + defp leg_line_class(%Route{external_agency_name: "Logan Express", name: name}) do + "border-logan-express-#{name}" + end + + defp leg_line_class(%Route{} = route) do + route + |> Routes.Route.icon_atom() + |> CSSHelpers.atom_to_class() + |> then(&"border-#{&1}") + end + + defp leg_line_class(_), do: "" + + defp leg_summary(assigns) do + assigns = assign(assigns, :stops_count, Enum.count(assigns.leg.mode.intermediate_stops) + 1) + + ~H""" +
+ <.route_symbol route={@leg.mode.route} /> + {@leg.mode.trip_id} +
+ Ride the {route_name(@leg.mode.route)} + {@stops_count} {Inflex.inflect("stop", @stops_count)} +
+
+ """ + end + + # Returns the name of the route for the given row. + # If there is an external agency name, we use the long name. + # If it is a bus, we use the short name. + # For all others, we use the long name. + defp route_name(%Route{external_agency_name: agency, long_name: long_name}) + when is_binary(agency) and is_binary(long_name), + do: long_name + + defp route_name(%Route{name: name, type: 3}) + when is_binary(name), + do: "#{name} bus" + + defp route_name(%Route{long_name: long_name}) + when is_binary(long_name), + do: long_name + + defp route_name(%Route{name: name}), do: name + defp route_name(_), do: nil + + defp leg_details(assigns) do + ~H""" +
    +
  • + <.icon name="circle" class="w-2 h-2 absolute -left-6 fill-white" /> + {stop.name} +
  • +
+ """ + end +end diff --git a/lib/dotcom_web/components/trip_planner/walking_leg.ex b/lib/dotcom_web/components/trip_planner/walking_leg.ex index 0b28db8887..8f659e3022 100644 --- a/lib/dotcom_web/components/trip_planner/walking_leg.ex +++ b/lib/dotcom_web/components/trip_planner/walking_leg.ex @@ -25,29 +25,32 @@ defmodule DotcomWeb.Components.TripPlanner.WalkingLeg do def walking_leg(assigns) do ~H""" -
- <.accordion> - <:heading> - <.icon name="person-walking" class="w-5 h-5 mr-1 fill-black" /> -
-
- Walk +
+
+ <.accordion> + <:heading> + <.icon name="person-walking" class="w-5 h-5 mr-1 fill-black" /> +
+
+ Walk +
+
+ {@leg.duration} min, {@leg.distance} mi +
-
- {@leg.duration} min, {@leg.distance} mi -
-
- - <:content> - <.list class="w-full m-0 ps-0"> - <:item :for={step <- @leg.mode.steps}> - - {Step.walk_summary(step)} - - - - - + + <:content> + <.list class="w-full m-0 ps-0"> + <:item :for={step <- @leg.mode.steps}> + + {Step.walk_summary(step)} + + + + + +
+
""" end end diff --git a/mix.exs b/mix.exs index b53d5e5b09..1fc0a88015 100644 --- a/mix.exs +++ b/mix.exs @@ -113,7 +113,7 @@ defmodule DotCom.Mixfile do {:nebulex_redis_adapter, "2.4.1"}, { :open_trip_planner_client, - [github: "mbta/open_trip_planner_client", tag: "v0.11.1"] + [github: "mbta/open_trip_planner_client", tag: "v0.11.2"] }, {:parallel_stream, "1.1.0"}, # latest version 1.7.14 diff --git a/mix.lock b/mix.lock index 922cad6063..d9c6680621 100644 --- a/mix.lock +++ b/mix.lock @@ -72,7 +72,7 @@ "nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "open_trip_planner_client": {:git, "https://github.com/mbta/open_trip_planner_client.git", "da20df3ba39314487b4ab050b42131cb01f21826", [tag: "v0.11.1"]}, + "open_trip_planner_client": {:git, "https://github.com/mbta/open_trip_planner_client.git", "3bf74242bda2c4174bbbc48363decd864562d0e9", [tag: "v0.11.2"]}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},