Skip to content

Commit

Permalink
feat(TripPlanner): transit leg and itinerary locations (#2251)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Josh Larson <jlarson@mbta.com>
  • Loading branch information
thecristen and joshlarson authored Dec 10, 2024
1 parent acb1cba commit 92cc1b3
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 149 deletions.
4 changes: 3 additions & 1 deletion assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 15 additions & 23 deletions lib/dotcom_web/components/trip_planner/itinerary_detail.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(
%{
Expand Down Expand Up @@ -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"""
<div class="mt-4">
<div>
Depart at {Timex.format!(@itinerary.start, "%-I:%M%p", :strftime)}
<.route_symbol :for={route <- @all_routes} route={route} class="ml-2" />
</div>
<div :for={leg <- @itinerary.legs}>
<.place place={@start_place} time={@start_time} />
<div
:for={leg <- @itinerary.legs}
class={"#{if(match?(%TransitDetail{}, leg.mode), do: "bg-gray-bordered-background")}"}
>
<%= 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 %>
</div>
<.place place={@end_place} time={@end_time} />
</div>
"""
end
Expand Down
101 changes: 0 additions & 101 deletions lib/dotcom_web/components/trip_planner/leg.ex

This file was deleted.

73 changes: 73 additions & 0 deletions lib/dotcom_web/components/trip_planner/place.ex
Original file line number Diff line number Diff line change
@@ -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" />
<strong class="flex items-center gap-2">
{@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"
/>
</strong>
<time class="text-right no-wrap">{format_time(@time)}</time>
</.dynamic_tag>
"""
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
122 changes: 122 additions & 0 deletions lib/dotcom_web/components/trip_planner/transit_leg.ex
Original file line number Diff line number Diff line change
@@ -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"""
<div>
<.place
place={@leg.from}
time={@leg.start}
route={if(match?(%TransitDetail{}, @leg.mode), do: @leg.mode.route)}
/>
<div class={"bg-gray-bordered-background ml-5 border-l-8 #{leg_line_class(@leg.mode.route)}"}>
<%= if Enum.count(@leg.mode.intermediate_stops) < 2 do %>
<.leg_summary leg={@leg} />
<.leg_details leg={@leg} />
<% else %>
<details class="group">
<summary class="flex cursor-pointer list-none gap-2 relative">
<.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"
/>
</summary>
<.leg_details leg={@leg} />
</details>
<% end %>
</div>
<.place
place={@leg.to}
time={@leg.stop}
route={if(match?(%TransitDetail{}, @leg.mode), do: @leg.mode.route)}
/>
</div>
"""
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"""
<div class="gap-x-1 py-2 grid grid-rows-2 grid-cols-[min-content_max-content] pl-4">
<.route_symbol route={@leg.mode.route} />
<span class="font-semibold">{@leg.mode.trip_id}</span>
<div class="text-sm col-start-2 row-start-2">
Ride the {route_name(@leg.mode.route)}
<span class="font-semibold">{@stops_count} {Inflex.inflect("stop", @stops_count)}</span>
</div>
</div>
"""
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"""
<ul class="w-full m-0 pl-4 flex flex-col divide-y divide-gray-lighter">
<li
:for={stop <- @leg.mode.intermediate_stops}
class="inline-flex items-center gap-x-2 py-2 relative"
>
<.icon name="circle" class="w-2 h-2 absolute -left-6 fill-white" />
{stop.name}
</li>
</ul>
"""
end
end
Loading

0 comments on commit 92cc1b3

Please sign in to comment.