diff --git a/assets/css/colors.scss b/assets/css/colors.scss index 36a4f2b44..f8cba103b 100644 --- a/assets/css/colors.scss +++ b/assets/css/colors.scss @@ -13,6 +13,7 @@ $true-grey-45: #737373; $true-grey-70: #b2b1af; $warm-neutral-80: #cccbc8; +$warm-neutral-85: #d9d6d0; $warm-neutral-90: #e5e4e1; $cool-black-15: #171f26; diff --git a/assets/css/elevator_v2.scss b/assets/css/elevator_v2.scss index 06f089c72..ce9f5d94f 100644 --- a/assets/css/elevator_v2.scss +++ b/assets/css/elevator_v2.scss @@ -42,7 +42,7 @@ body { } } -.outside-elevator-closures, +.elevator-closures-list, .current-elevator-closed { position: relative; display: flex; @@ -51,21 +51,6 @@ body { color: $cool-black-15; background-color: $warm-neutral-90; - hr.thick { - min-height: 24px; - margin: 0; - background-color: $cool-black-15; - border: none; - } - - hr.thin { - min-height: 2px; - margin: 24px 0; - background-color: $true-grey-45; - border: none; - opacity: 0.5; - } - .subheading { font-size: 80px; font-weight: 700; @@ -84,4 +69,4 @@ body { } @import "v2/elevator/current_elevator_closed"; -@import "v2/elevator/outside_elevator_closures"; +@import "v2/elevator/elevator_closures_list"; diff --git a/assets/css/v2/elevator/outside_elevator_closures.scss b/assets/css/v2/elevator/elevator_closures_list.scss similarity index 72% rename from assets/css/v2/elevator/outside_elevator_closures.scss rename to assets/css/v2/elevator/elevator_closures_list.scss index 077f6ce2a..a37f9c3fd 100644 --- a/assets/css/v2/elevator/outside_elevator_closures.scss +++ b/assets/css/v2/elevator/elevator_closures_list.scss @@ -1,9 +1,10 @@ -.outside-elevator-closures { +.elevator-closures-list { display: flex; flex: 1; flex-direction: column; .in-station-summary { + position: relative; display: flex; justify-content: space-between; padding: 24px 48px; @@ -15,9 +16,19 @@ .text { margin-right: 82px; } + + &::after { + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 24px; + content: ""; + background: $cool-black-15; + } } - .outside-closure-list { + .closures-list { height: 100%; background-color: $warm-neutral-90; @@ -49,8 +60,35 @@ transform: translateX(calc(-100% * var(--closure-list-offset))); .closure-row { + position: relative; width: 1080px; - padding: 0 48px; + padding: 26px 48px 24px; + + &:not(&--current-station) { + &::after, + &:first-child::before { + position: absolute; + left: 0; + width: 100%; + height: 2px; + margin: 0 24px; + content: ""; + background: $true-grey-45; + opacity: 0.5; + } + + &::after { + top: 100%; + } + + &::before { + top: 0; + } + } + + &--current-station { + background-color: $warm-neutral-85; + } &__station-name { font-size: 62px; diff --git a/assets/package-lock.json b/assets/package-lock.json index 61a5b28d8..a3e21d2ce 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -38,6 +38,7 @@ "@sentry/webpack-plugin": "^2.22.6", "@svgr/webpack": "^5.5.0", "@types/lodash": "^4.17.13", + "@types/number-to-words": "^1.2.3", "@types/react": "^17.0.14", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.1.8", @@ -4423,6 +4424,13 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/number-to-words": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/number-to-words/-/number-to-words-1.2.3.tgz", + "integrity": "sha512-ruSA/QZ6JQofoEEN2zKmN9oihZmrCBgcw0zEzN0UA9Mw0pU5remTddZmrbHjsa99qz12Uxl82x+XkVpHu87oXw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "dev": true, diff --git a/assets/package.json b/assets/package.json index a949db536..e2372b20d 100644 --- a/assets/package.json +++ b/assets/package.json @@ -50,6 +50,7 @@ "@sentry/webpack-plugin": "^2.22.6", "@svgr/webpack": "^5.5.0", "@types/lodash": "^4.17.13", + "@types/number-to-words": "^1.2.3", "@types/react": "^17.0.14", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.1.8", diff --git a/assets/src/apps/v2/elevator.tsx b/assets/src/apps/v2/elevator.tsx index b43342a35..c05963ec5 100644 --- a/assets/src/apps/v2/elevator.tsx +++ b/assets/src/apps/v2/elevator.tsx @@ -14,7 +14,7 @@ import EvergreenContent from "Components/v2/evergreen_content"; import ScreenPage from "Components/v2/screen_page"; import { MappingContext } from "Components/v2/widget"; import MultiScreenPage from "Components/v2/multi_screen_page"; -import OutsideElevatorClosures from "Components/v2/elevator/outside_elevator_closures"; +import ElevatorClosuresList from "Components/v2/elevator/elevator_closures_list"; import CurrentElevatorClosed from "Components/v2/elevator/current_elevator_closed"; import SimulationScreenPage from "Components/v2/simulation_screen_page"; import Footer from "Components/v2/elevator/footer"; @@ -22,7 +22,7 @@ import NormalHeader from "Components/v2/normal_header"; const TYPE_TO_COMPONENT = { normal: NormalScreen, - outside_elevator_closures: OutsideElevatorClosures, + elevator_closures_list: ElevatorClosuresList, current_elevator_closed: CurrentElevatorClosed, evergreen_content: EvergreenContent, footer: Footer, diff --git a/assets/src/components/v2/elevator/outside_elevator_closures.tsx b/assets/src/components/v2/elevator/elevator_closures_list.tsx similarity index 63% rename from assets/src/components/v2/elevator/outside_elevator_closures.tsx rename to assets/src/components/v2/elevator/elevator_closures_list.tsx index 2618911ae..66b88a372 100644 --- a/assets/src/components/v2/elevator/outside_elevator_closures.tsx +++ b/assets/src/components/v2/elevator/elevator_closures_list.tsx @@ -13,11 +13,34 @@ import { import useClientPaging from "Hooks/v2/use_client_paging"; import NormalService from "Images/svgr_bundled/normal-service.svg"; import AccessibilityAlert from "Images/svgr_bundled/accessibility-alert.svg"; +import { classWithModifier } from "Util/util"; interface ClosureRowProps { station: StationWithClosures; } +const CurrentStationClosureRow = ({ station }: ClosureRowProps) => { + const { closures } = station; + + return ( +
+
+
At this station
+
+ {closures.map((closure) => ( +
1, + })} + > + {closure.elevator_name} ({closure.elevator_id}) +
+ ))} +
+ ); +}; + const ClosureRow = ({ station }: ClosureRowProps) => { const { name, closures, route_icons, id } = station; @@ -39,33 +62,54 @@ const ClosureRow = ({ station }: ClosureRowProps) => { {closure.elevator_name} ({closure.elevator_id}) ))} -
); }; -const InStationSummary = () => { +interface InStationSummaryProps { + closures: Closure[]; +} + +const InStationSummary = ({ closures }: InStationSummaryProps) => { + let text; + if (closures.length === 0) { + text = "All elevators at this station are currently working"; + } else if (closures.length === 1) { + text = ( + <> + This elevator is working. Another elevator at this station is + down. + + ); + } else { + text = ( + <> + This elevator is working. {closures.length} other elevators at + this station are down. + + ); + } + return ( <>
- - All elevators at this station are currently working - + {text}
-
); }; interface OutsideClosureListProps extends WrappedComponentProps { stations: StationWithClosures[]; + stationId: string; } const OutsideClosureList = ({ stations, + stationId, lastUpdate, onFinish, }: OutsideClosureListProps) => { @@ -103,7 +147,7 @@ const OutsideClosureList = ({ }, [stations]); return ( -
+
MBTA Elevator Closures
@@ -111,7 +155,6 @@ const OutsideClosureList = ({
-
{ @@ -124,9 +167,18 @@ const OutsideClosureList = ({ } ref={ref} > - {stations.map((station) => ( - - ))} + {stations + .sort((a, _b) => (a.id === stationId ? 0 : 1)) + .map((station) => + station.id === stationId ? ( + + ) : ( + + ), + )}
}
@@ -144,20 +196,26 @@ const OutsideClosureList = ({ interface Props extends WrappedComponentProps { id: string; - in_station_closures: Closure[]; - other_stations_with_closures: StationWithClosures[]; + stations_with_closures: StationWithClosures[]; + station_id: string; } -const OutsideElevatorClosures = ({ - other_stations_with_closures: stations, +const ElevatorClosuresList = ({ + stations_with_closures: stations, + station_id: stationId, lastUpdate, onFinish, }: Props) => { return ( -
- +
+ s.id === stationId) + .flatMap((s) => s.closures)} + /> @@ -166,5 +224,5 @@ const OutsideElevatorClosures = ({ }; export default makePersistent( - OutsideElevatorClosures as ComponentType, + ElevatorClosuresList as ComponentType, ); diff --git a/lib/screens/v2/candidate_generator/elevator/closures.ex b/lib/screens/v2/candidate_generator/elevator/closures.ex index 20c02e85e..eafb328eb 100644 --- a/lib/screens/v2/candidate_generator/elevator/closures.ex +++ b/lib/screens/v2/candidate_generator/elevator/closures.ex @@ -10,9 +10,9 @@ defmodule Screens.V2.CandidateGenerator.Elevator.Closures do alias Screens.V2.WidgetInstance.{ CurrentElevatorClosed, + ElevatorClosuresList, Footer, - NormalHeader, - OutsideElevatorClosures + NormalHeader } alias Screens.V2.WidgetInstance.Elevator.Closure @@ -50,21 +50,23 @@ defmodule Screens.V2.CandidateGenerator.Elevator.Closures do elevator_alerts = Enum.filter(alerts, &relevant_alert?(&1, stop_id)) routes_map = get_routes_map(elevator_alerts, stop_id) - {in_station_alerts, outside_alerts} = - split_closures_by_location(elevator_alerts, stop_id) - - in_station_closures = - Enum.map(in_station_alerts, &alert_to_elevator_closure/1) - - current_elevator_closure = Enum.find(in_station_closures, &(&1.elevator_id == elevator_id)) + current_elevator_closure = + elevator_alerts + |> get_in_station_alerts(stop_id) + |> Enum.map(&alert_to_elevator_closure/1) + |> Enum.find(&(&1.elevator_id == elevator_id)) {elevator_widget_instance, header_footer_variant} = if is_nil(current_elevator_closure) do - {%OutsideElevatorClosures{ - in_station_closures: in_station_closures, - other_stations_with_closures: - format_outside_closures(outside_alerts, parent_station_map, routes_map), - app_params: config + {%ElevatorClosuresList{ + stations_with_closures: + build_stations_with_closures( + elevator_alerts, + parent_station_map, + routes_map + ), + app_params: config, + station_id: stop_id }, nil} else {%CurrentElevatorClosed{closure: current_elevator_closure, app_params: config}, :closed} @@ -133,8 +135,8 @@ defmodule Screens.V2.CandidateGenerator.Elevator.Closures do |> Enum.uniq() end - defp split_closures_by_location(closures, home_stop_id) do - Enum.split_with(closures, fn %Alert{informed_entities: informed_entities} -> + defp get_in_station_alerts(alerts, home_stop_id) do + Enum.filter(alerts, fn %Alert{informed_entities: informed_entities} -> home_stop_id in Enum.map(informed_entities, & &1.stop) end) end @@ -156,18 +158,18 @@ defmodule Screens.V2.CandidateGenerator.Elevator.Closures do } end - defp format_outside_closures(closures, station_id_to_name, station_id_to_routes) do - closures + defp build_stations_with_closures(alerts, station_id_to_name, station_id_to_routes) do + alerts |> Enum.group_by(&get_parent_station_id_from_informed_entities(&1.informed_entities)) - |> Enum.map(fn {parent_station_id, closures} -> - closures_at_station = Enum.map(closures, &alert_to_elevator_closure/1) + |> Enum.map(fn {parent_station_id, alerts} -> + closures_at_station = Enum.map(alerts, &alert_to_elevator_closure/1) route_pills = station_id_to_routes |> Map.fetch!(parent_station_id) |> Enum.map(&RoutePill.serialize_icon/1) - %OutsideElevatorClosures.Station{ + %ElevatorClosuresList.Station{ id: parent_station_id, name: Map.fetch!(station_id_to_name, parent_station_id), route_icons: route_pills, diff --git a/lib/screens/v2/widget_instance/outside_elevator_closures.ex b/lib/screens/v2/widget_instance/elevator_closures_list.ex similarity index 57% rename from lib/screens/v2/widget_instance/outside_elevator_closures.ex rename to lib/screens/v2/widget_instance/elevator_closures_list.ex index bcc2f12e1..43143fc11 100644 --- a/lib/screens/v2/widget_instance/outside_elevator_closures.ex +++ b/lib/screens/v2/widget_instance/elevator_closures_list.ex @@ -1,16 +1,16 @@ -defmodule Screens.V2.WidgetInstance.OutsideElevatorClosures do +defmodule Screens.V2.WidgetInstance.ElevatorClosuresList do @moduledoc false alias Screens.Stops.Stop alias Screens.V2.WidgetInstance.Elevator.Closure alias ScreensConfig.V2.Elevator - defstruct ~w[app_params in_station_closures other_stations_with_closures]a + defstruct ~w[app_params stations_with_closures station_id]a @type t :: %__MODULE__{ app_params: Elevator.t(), - in_station_closures: list(Closure.t()), - other_stations_with_closures: list(__MODULE__.Station.t()) + stations_with_closures: list(__MODULE__.Station.t()), + station_id: String.t() } defmodule Station do @@ -33,26 +33,26 @@ defmodule Screens.V2.WidgetInstance.OutsideElevatorClosures do def serialize(%__MODULE__{ app_params: %Elevator{elevator_id: id}, - in_station_closures: in_station_closures, - other_stations_with_closures: other_stations_with_closures + stations_with_closures: stations_with_closures, + station_id: station_id }), do: %{ id: id, - in_station_closures: in_station_closures, - other_stations_with_closures: other_stations_with_closures + stations_with_closures: stations_with_closures, + station_id: station_id } defimpl Screens.V2.WidgetInstance do - alias Screens.V2.WidgetInstance.OutsideElevatorClosures + alias Screens.V2.WidgetInstance.ElevatorClosuresList def priority(_instance), do: [1] - def serialize(instance), do: OutsideElevatorClosures.serialize(instance) + def serialize(instance), do: ElevatorClosuresList.serialize(instance) def slot_names(_instance), do: [:main_content] - def widget_type(_instance), do: :outside_elevator_closures + def widget_type(_instance), do: :elevator_closures_list def valid_candidate?(_instance), do: true def audio_serialize(_instance), do: %{} def audio_sort_key(_instance), do: [0] def audio_valid_candidate?(_instance), do: false - def audio_view(_instance), do: ScreensWeb.V2.Audio.OutsideElevatorClosuresView + def audio_view(_instance), do: ScreensWeb.V2.Audio.ElevatorClosuresListView end end diff --git a/lib/screens_web/views/v2/audio/outside_elevator_closures_view.ex b/lib/screens_web/views/v2/audio/elevator_closures_list_view.ex similarity index 56% rename from lib/screens_web/views/v2/audio/outside_elevator_closures_view.ex rename to lib/screens_web/views/v2/audio/elevator_closures_list_view.ex index bfee0f336..d902ef91b 100644 --- a/lib/screens_web/views/v2/audio/outside_elevator_closures_view.ex +++ b/lib/screens_web/views/v2/audio/elevator_closures_list_view.ex @@ -1,4 +1,4 @@ -defmodule ScreensWeb.V2.Audio.OutsideElevatorClosuresView do +defmodule ScreensWeb.V2.Audio.ElevatorClosuresListView do use ScreensWeb, :view def render("_widget.ssml", _) do diff --git a/test/screens/v2/candidate_generator/elevator/closures_test.exs b/test/screens/v2/candidate_generator/elevator/closures_test.exs index eced7377b..a12401dcf 100644 --- a/test/screens/v2/candidate_generator/elevator/closures_test.exs +++ b/test/screens/v2/candidate_generator/elevator/closures_test.exs @@ -11,7 +11,7 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do CurrentElevatorClosed, Footer, NormalHeader, - OutsideElevatorClosures + ElevatorClosuresList } alias ScreensConfig.Screen @@ -83,18 +83,23 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do [ ^header_instance, - %OutsideElevatorClosures{ + %ElevatorClosuresList{ app_params: ^config, - in_station_closures: [ - %{ - id: "1", - description: nil, - elevator_name: "Test", - elevator_id: "facility-test", - header_text: nil + stations_with_closures: [ + %ElevatorClosuresList.Station{ + id: "place-test", + name: "Place Test", + route_icons: [%{type: :text, text: "RL", color: :red}], + closures: [ + %Closure{ + id: "1", + elevator_name: "Test", + elevator_id: "facility-test" + } + ] } ], - other_stations_with_closures: [] + station_id: "place-test" }, ^footer_instance ] = @@ -147,10 +152,9 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do [ ^header_instance, - %OutsideElevatorClosures{ + %ElevatorClosuresList{ app_params: ^config, - in_station_closures: [], - other_stations_with_closures: [ + stations_with_closures: [ %{ id: "place-haecl", name: "Haymarket", @@ -220,10 +224,9 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do [ ^header_instance, - %OutsideElevatorClosures{ + %ElevatorClosuresList{ app_params: ^config, - in_station_closures: [], - other_stations_with_closures: [] + stations_with_closures: [] }, ^footer_instance ] = @@ -288,7 +291,7 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do [ ^header_instance, - %Screens.V2.WidgetInstance.OutsideElevatorClosures{ + %ElevatorClosuresList{ app_params: %ScreensConfig.V2.Elevator{ elevator_id: "111", alternate_direction_text: "Test", @@ -297,22 +300,25 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do accessible_path_image_url: nil, accessible_path_image_here_coordinates: %{y: 0, x: 0} }, - in_station_closures: [ - %Screens.V2.WidgetInstance.Elevator.Closure{ - id: "1", - elevator_name: "In Station Elevator", - elevator_id: "112", - description: nil, - header_text: nil - } - ], - other_stations_with_closures: [ - %Screens.V2.WidgetInstance.OutsideElevatorClosures.Station{ + stations_with_closures: [ + %ElevatorClosuresList.Station{ + id: "place-test", + name: "This Station", + route_icons: [%{type: :text, text: "RL", color: :red}], + closures: [ + %Closure{ + id: "1", + elevator_name: "In Station Elevator", + elevator_id: "112" + } + ] + }, + %ElevatorClosuresList.Station{ id: "place-test-no-redundancy", name: "Other No Redundancy", route_icons: [%{type: :text, text: "RL", color: :red}], closures: [ - %Screens.V2.WidgetInstance.Elevator.Closure{ + %Closure{ id: "3", elevator_name: "Other Without Redundancy", elevator_id: "333", @@ -321,7 +327,8 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do } ] } - ] + ], + station_id: "place-test" }, ^footer_instance ] = @@ -422,18 +429,22 @@ defmodule Screens.V2.CandidateGenerator.Elevator.ClosuresTest do [ ^header_instance, - %OutsideElevatorClosures{ + %ElevatorClosuresList{ app_params: ^config, - in_station_closures: [ - %{ - id: "1", - description: nil, - elevator_name: "Test", - elevator_id: "facility-test", - header_text: nil + stations_with_closures: [ + %ElevatorClosuresList.Station{ + id: "place-test", + name: "Place Test", + closures: [ + %Closure{ + id: "1", + elevator_name: "Test", + elevator_id: "facility-test" + } + ] } ], - other_stations_with_closures: [] + station_id: "place-test" }, ^footer_instance ] = diff --git a/test/screens/v2/widget_instance/outside_elevator_closures_test.exs b/test/screens/v2/widget_instance/elevator_closures_list_test.exs similarity index 65% rename from test/screens/v2/widget_instance/outside_elevator_closures_test.exs rename to test/screens/v2/widget_instance/elevator_closures_list_test.exs index ee8397206..72dd3f2ac 100644 --- a/test/screens/v2/widget_instance/outside_elevator_closures_test.exs +++ b/test/screens/v2/widget_instance/elevator_closures_list_test.exs @@ -1,31 +1,22 @@ -defmodule Screens.V2.WidgetInstance.OutsideElevatorClosuresTest do +defmodule Screens.V2.WidgetInstance.ElevatorClosuresListTest do use ExUnit.Case, async: true alias Screens.V2.WidgetInstance alias Screens.V2.WidgetInstance.Elevator.Closure - alias Screens.V2.WidgetInstance.OutsideElevatorClosures + alias Screens.V2.WidgetInstance.ElevatorClosuresList alias ScreensConfig.V2.Elevator setup do %{ - instance: %OutsideElevatorClosures{ + instance: %ElevatorClosuresList{ app_params: struct(Elevator, elevator_id: "1", alternate_direction_text: "Test", accessible_path_direction_arrow: :n ), - in_station_closures: [ - %Closure{ - description: "Test Alert Description", - elevator_name: "Test Elevator", - elevator_id: "111", - id: "1", - header_text: "Test Alert Header" - } - ], - other_stations_with_closures: [ - %OutsideElevatorClosures.Station{ + stations_with_closures: [ + %ElevatorClosuresList.Station{ name: "Forest Hills", route_icons: ["Orange"], closures: [ @@ -52,9 +43,9 @@ defmodule Screens.V2.WidgetInstance.OutsideElevatorClosuresTest do describe "serialize/1" do test "returns map with id and closures", %{instance: instance} do assert %{ - in_station_closures: instance.in_station_closures, - other_stations_with_closures: instance.other_stations_with_closures, - id: instance.app_params.elevator_id + stations_with_closures: instance.stations_with_closures, + id: instance.app_params.elevator_id, + station_id: nil } == WidgetInstance.serialize(instance) end end @@ -66,8 +57,8 @@ defmodule Screens.V2.WidgetInstance.OutsideElevatorClosuresTest do end describe "widget_type/1" do - test "returns outside_elevator_closures", %{instance: instance} do - assert :outside_elevator_closures == WidgetInstance.widget_type(instance) + test "returns elevator_closures_list", %{instance: instance} do + assert :elevator_closures_list == WidgetInstance.widget_type(instance) end end @@ -90,8 +81,8 @@ defmodule Screens.V2.WidgetInstance.OutsideElevatorClosuresTest do end describe "audio_view/1" do - test "returns OutsideElevatorClosuresView", %{instance: instance} do - assert ScreensWeb.V2.Audio.OutsideElevatorClosuresView == + test "returns ElevatorClosuresListView", %{instance: instance} do + assert ScreensWeb.V2.Audio.ElevatorClosuresListView == WidgetInstance.audio_view(instance) end end