diff --git a/lib/dotcom/routes.ex b/lib/dotcom/routes.ex
index 4c2b8037ec..33fe5b2cff 100644
--- a/lib/dotcom/routes.ex
+++ b/lib/dotcom/routes.ex
@@ -3,11 +3,53 @@ defmodule Dotcom.Routes do
A collection of functions that help to work with routes in a unified way.
"""
- @subway_route_ids ["Blue", "Green", "Orange", "Red"]
+ alias Routes.Route
+
+ @subway_branch_ids GreenLine.branch_ids() ++ ["Mattapan"]
+ @subway_line_names ["Blue", "Green", "Orange", "Red"]
+
+ # Association of subway line names to all their respective subway routes.
+ # This could later be derived from the GTFS line/route relationships.
+ @subway_line_route_map %{
+ "Blue" => ["Blue"],
+ "Green" => GreenLine.branch_ids(),
+ "Orange" => ["Orange"],
+ "Red" => ["Red", "Mattapan"]
+ }
+
+ @doc """
+ For a given route ID, return the relevant subway line name.
+
+ ```elixir
+ line_name_for_subway_route("Green-B") == "Green"
+ line_name_for_subway_route("CR-Greenbush") == nil
+ ```
+ """
+ @spec line_name_for_subway_route(Route.id_t()) :: String.t() | Route.id_t() | nil
+ def line_name_for_subway_route(route_id) do
+ with {line_name, _} <-
+ Enum.find(@subway_line_route_map, fn {_, route_ids} ->
+ route_id in route_ids
+ end) do
+ line_name
+ end
+ end
+
+ @doc """
+ Returns a list of all subway route IDs which we'd like to show as branches.
+ """
+ @spec subway_branch_ids() :: [Route.id_t()]
+ def subway_branch_ids, do: @subway_branch_ids
+
+ @doc """
+ Returns a list of all subway line names.
+ """
+ @spec subway_line_names() :: [Route.id_t() | String.t()]
+ def subway_line_names, do: @subway_line_names
@doc """
- Returns a list of route ids for all subway routes.
+ Returns the list of all subway route IDs.
"""
- @spec subway_route_ids() :: [String.t()]
- def subway_route_ids(), do: @subway_route_ids
+ @spec subway_route_ids() :: [Route.id_t()]
+ def subway_route_ids, do: Map.values(@subway_line_route_map) |> List.flatten()
end
diff --git a/lib/dotcom_web/components/route_pills.ex b/lib/dotcom_web/components/route_pills.ex
deleted file mode 100644
index d1ddae5215..0000000000
--- a/lib/dotcom_web/components/route_pills.ex
+++ /dev/null
@@ -1,73 +0,0 @@
-defmodule DotcomWeb.Components.RoutePills do
- @moduledoc """
- Reusable components for displaying subway routes.
- """
-
- use DotcomWeb, :component
-
- attr :route_id, :string, required: true
- attr :modifier_ids, :list, default: []
- attr :modifier_class, :string, default: ""
-
- def route_pill(%{modifier_ids: []} = assigns) do
- ~H"""
-
- {pill_text(@route_id)}
-
- """
- end
-
- def route_pill(assigns) do
- ~H"""
-
- <.route_pill route_id={@route_id} />
-
- <.route_modifier
- :for={modifier_id <- @modifier_ids}
- modifier_id={modifier_id}
- class={@modifier_class}
- />
-
-
- """
- end
-
- defp route_modifier(assigns) do
- ~H"""
-
- {modifier_text(@modifier_id)}
-
- """
- end
-
- defp pill_text(route_id) do
- (route_id |> String.at(0)) <> "L"
- end
-
- defp modifier_text("Green-" <> branch_id), do: branch_id
- defp modifier_text("Mattapan"), do: "M"
-
- defp bg_color_class("Green-" <> _), do: bg_color_class("Green")
- defp bg_color_class("Mattapan"), do: bg_color_class("Red")
-
- defp bg_color_class(route_id) do
- "bg-#{route_id |> String.downcase()}-line"
- end
-
- defp shared_icon_classes() do
- [
- "rounded-full h-6",
- "flex items-center justify-center",
- "text-white font-bold font-heading select-none leading-none"
- ]
- end
-end
diff --git a/lib/dotcom_web/components/route_symbols.ex b/lib/dotcom_web/components/route_symbols.ex
index 6ed4ee1843..1b76fa519a 100644
--- a/lib/dotcom_web/components/route_symbols.ex
+++ b/lib/dotcom_web/components/route_symbols.ex
@@ -12,6 +12,8 @@ defmodule DotcomWeb.Components.RouteSymbols do
@logan_express_icon_names Route.logan_express_icon_names()
@massport_icon_names Route.massport_icon_names()
+ @subway_branch_ids Dotcom.Routes.subway_branch_ids()
+ @subway_line_names Dotcom.Routes.subway_line_names()
variant(
:size,
@@ -178,6 +180,85 @@ defmodule DotcomWeb.Components.RouteSymbols do
"""
end
+ attr :class, :string, default: ""
+
+ attr :route_ids, :list,
+ doc: "A list of route IDs. These should all be associated with subway routes.",
+ examples: [["Blue"], ["Green-B", "Green-C"]]
+
+ @doc """
+ Renders a symbol for one or more subway routes, consisting of a colored pill
+ with optionally a number of circles representing branches of a subway line.
+ """
+ def subway_route_pill(%{route_ids: [route_id]} = assigns) when route_id in @subway_line_names do
+ assigns =
+ assign(assigns, %{
+ bg_color_class: "bg-#{String.downcase(route_id)}-line",
+ route_abbreviation: String.at(route_id, 0) <> "L"
+ })
+
+ ~H"""
+
+ {@route_abbreviation}
+
+ """
+ end
+
+ # Add the subway line name to the list of subway branch route_ids, since it is
+ # needed to render the larger pill
+ def subway_route_pill(%{route_ids: [route_id]} = assigns) when route_id in @subway_branch_ids do
+ assigns = assign(assigns, :route_ids, [subway_line_name(route_id) | assigns.route_ids])
+
+ ~H"""
+ <.subway_route_pill {assigns} />
+ """
+ end
+
+ # A list of any length - find the relevant subway line. If there's any number
+ # of subway lines represented here other than one, fall back to subway icon.
+ def subway_route_pill(%{route_ids: route_ids} = assigns) when is_list(route_ids) do
+ with subway_line_names <- Enum.map(route_ids, &subway_line_name/1),
+ [subway_line_name] <- Enum.uniq(subway_line_names) |> Enum.reject(&is_nil/1) do
+ branch_ids = Enum.reject(assigns.route_ids, &(&1 == subway_line_name)) |> Enum.sort()
+
+ if branch_ids == GreenLine.branch_ids() do
+ assigns = assign(assigns, :route_ids, ["Green"])
+
+ ~H"""
+ <.subway_route_pill {assigns} />
+ """
+ else
+ assigns =
+ assign(assigns, %{
+ route_ids: [subway_line_name],
+ branch_ids: branch_ids
+ })
+
+ ~H"""
+
+ <.subway_route_pill route_ids={@route_ids} class={"#{@class} -mr-1"} />
+ <.route_symbol
+ :for={route_id <- @branch_ids}
+ route={%Routes.Route{id: route_id}}
+ class={"#{@class} rounded-full ring-white ring-2 mr-0.5"}
+ />
+
+ """
+ end
+ else
+ _ ->
+ ~H"""
+ <.icon type="icon-svg" name="icon-mode-subway-default" class="h-6 w-6" />
+ """
+ end
+ end
+
# Given a route, return a machine-readable label.
defp route_label(%Route{type: 2}), do: "Commuter Rail"
defp route_label(%Route{type: 4}), do: "Ferry"
@@ -202,4 +283,11 @@ defmodule DotcomWeb.Components.RouteSymbols do
end
defp route_label(%Route{long_name: long_name}), do: long_name
+
+ defp subway_line_name(route_id) when route_id in @subway_branch_ids do
+ Dotcom.Routes.line_name_for_subway_route(route_id)
+ end
+
+ defp subway_line_name(route_id) when route_id in @subway_line_names, do: route_id
+ defp subway_line_name(_), do: nil
end
diff --git a/lib/dotcom_web/components/system_status/subway_status.ex b/lib/dotcom_web/components/system_status/subway_status.ex
index de07212cfe..b1d384b751 100644
--- a/lib/dotcom_web/components/system_status/subway_status.ex
+++ b/lib/dotcom_web/components/system_status/subway_status.ex
@@ -6,7 +6,7 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do
use DotcomWeb, :component
import DotcomWeb.Components, only: [bordered_container: 1, lined_list: 1]
- import DotcomWeb.Components.RoutePills
+ import DotcomWeb.Components.RouteSymbols, only: [subway_route_pill: 1]
import DotcomWeb.Components.SystemStatus.StatusLabel
@max_rows 5
@@ -35,10 +35,9 @@ defmodule DotcomWeb.Components.SystemStatus.SubwayStatus do
]}
>
- <.route_pill
- route_id={row.route_info.route_id}
- modifier_ids={row.route_info.branch_ids}
- modifier_class="group-hover/row:ring-brand-primary-lightest"
+ <.subway_route_pill
+ class="group-hover/row:ring-brand-primary-lightest"
+ route_ids={[row.route_info.route_id | row.route_info.branch_ids]}
/>
Route Pills
-
- <.route_pill route_id="Blue" />
- <.route_pill route_id="Green" />
- <.route_pill route_id="Orange" />
- <.route_pill route_id="Red" />
- <.route_pill route_id="Green" modifier_ids={["Green-B", "Green-C"]} />
-
+ <%= for ids <- [
+ ["Blue"],
+ ["Green"],
+ ["Orange"],
+ ["Red"],
+ ["Mattapan"],
+ ["Red", "Mattapan"],
+ ["Orange", "Mattapan"],
+ ["Green-E", "Green-B", "Green-D"],
+ ["Fake News"],
+ ["Green-B", "Green-C", "Green-D", "Green-E"]
+ ] do %>
+
+ <.subway_route_pill route_ids={ids} class="group-hover/row:ring-slate-600" /> {inspect(ids)}
+
+ <% end %>
"""
end
diff --git a/test/dotcom_web/components/route_symbols_test.exs b/test/dotcom_web/components/route_symbols_test.exs
index 7fbae8be0a..1c9e0e8e2e 100644
--- a/test/dotcom_web/components/route_symbols_test.exs
+++ b/test/dotcom_web/components/route_symbols_test.exs
@@ -102,6 +102,62 @@ defmodule DotcomWeb.Components.RouteSymbolsTest do
end
end
+ describe "subway_route_pill/1" do
+ test "Subway lines render one element" do
+ for route_id <- Dotcom.Routes.subway_line_names() do
+ html =
+ render_component(&subway_route_pill/1, %{
+ route_ids: [route_id]
+ })
+ |> Floki.parse_fragment!()
+ |> List.first()
+
+ assert html
+ assert Floki.children(html, include_text: false) == []
+ end
+ end
+
+ test "Mattapan renders pill + icon" do
+ html =
+ render_component(&subway_route_pill/1, %{
+ route_ids: ["Mattapan"]
+ })
+ |> Floki.parse_fragment!()
+ |> List.first()
+
+ assert [{"div", _, _}, {"svg", _, _}] = Floki.children(html, include_text: false)
+ end
+
+ test "Multiple branches render pill + multiple icons" do
+ num_branches = Faker.Util.pick([2, 3])
+
+ html =
+ render_component(&subway_route_pill/1, %{
+ route_ids: Enum.take(GreenLine.branch_ids(), num_branches)
+ })
+ |> Floki.parse_fragment!()
+ |> List.first()
+
+ assert [{"div", _, _} | icons] = Floki.children(html, include_text: false)
+ assert [{"svg", _, _} | _] = icons
+ assert Enum.count(icons) == num_branches
+ end
+
+ test "List of all Green Line branches renders one pill" do
+ all_gl_branches_pill =
+ render_component(&subway_route_pill/1, %{
+ route_ids: GreenLine.branch_ids()
+ })
+
+ green_line_pill =
+ render_component(&subway_route_pill/1, %{
+ route_ids: ["Green"]
+ })
+
+ assert all_gl_branches_pill == green_line_pill
+ end
+ end
+
defp matches_title?(html, text) do
title =
html