Skip to content

Commit

Permalink
refactor: add types for DUP widgets
Browse files Browse the repository at this point in the history
This adds TypeScript types for most DUP widgets that were missing them.

Includes these related changes:

* DUPs have a unique "overnight" departure time type that displays a
  moon icon in place of a time. The backend serialized this as
  `%{type: :icon, icon: :overnight}`, but the frontend didn't use the
  `icon` field and unconditionally rendered a moon icon. This has been
  changed to just `%{type: :overnight}`.

* DUP departure times mistakenly looked at the `type` field of the
  predicted time to determine how to show the scheduled time, rather
  than the scheduled time's own `type`. This worked anyway, because
  scheduled times are currently only produced when the type of both
  times is the same (`timestamp` specifically), but the type checker
  caught this as potentially broken.
  • Loading branch information
digitalcora committed Jul 30, 2024
1 parent c6823fe commit fb9dddc
Show file tree
Hide file tree
Showing 14 changed files with 109 additions and 52 deletions.
9 changes: 8 additions & 1 deletion assets/src/components/v2/departures/departure_time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ const TimestampDepartureTime = ({ hour, minute }) => {
type DepartureTime =
| (TextDeparture & { type: "text" })
| (MinutesDeparture & { type: "minutes" })
| (TimestampDeparture & { type: "timestamp" });
| (TimestampDeparture & { type: "timestamp" })
// Note: `overnight` is only produced in the DUP code path, and so is only
// supported in the DUP version of this component.
| { type: "overnight" };

interface TextDeparture {
text: string;
Expand All @@ -35,6 +38,10 @@ interface MinutesDeparture {
interface TimestampDeparture {
hour: number;
minute: number;
// Note: `am_pm` fields are currently only supported by the DUP version of
// this component, but are always present in departures serialization.
am_pm: string;
show_am_pm: boolean;
}

const DepartureTime: ComponentType<DepartureTime> = ({ type, ...data }) => {
Expand Down
3 changes: 3 additions & 0 deletions assets/src/components/v2/departures/departure_times.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import DepartureCrowding, { CrowdingLevel } from "./departure_crowding";
export type TimeWithCrowding = {
id: string;
time: DepartureTime;
// Note: `scheduled_time` is currently only supported by the DUP version of
// `DepartureTime`, but is always present in departures serialization.
scheduled_time?: DepartureTime;
crowding: CrowdingLevel | null;
};

Expand Down
33 changes: 20 additions & 13 deletions assets/src/components/v2/dup/departures.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
import React from "react";
import React, { ComponentType } from "react";

import { type Section as SectionBase } from "Components/v2/departures/section";
import NormalSection from "./departures/normal_section";
import NoticeSection from "Components/v2/departures/notice_section";
import HeadwaySection from "./departures/headway_section";
import NoDataSection from "./departures/no_data_section";
import OvernightSection from "./departures/overnight_section";

const Departures = ({ sections }) => {
type Section =
| SectionBase
| (HeadwaySection & { type: "headway_section" })
| (NoDataSection & { type: "no_data_section" })
| (OvernightSection & { type: "overnight_section" });

interface Props {
sections: Section[];
}

const Departures: ComponentType<Props> = ({ sections }) => {
return (
<div className="departures-container">
<div className="departures">
{sections.map(({ type, ...data }, i) => {
switch (type) {
{sections.map((section, i) => {
switch (section.type) {
case "normal_section":
return <NormalSection rows={data.rows} key={i} />;
return <NormalSection {...section} key={i} />;
case "notice_section":
return <NoticeSection text={data.text} key={i} />;
return <NoticeSection {...section} key={i} />;
case "headway_section":
return (
<HeadwaySection text={data.text} layout={data.layout} key={i} />
);
return <HeadwaySection {...section} key={i} />;
case "no_data_section":
return <NoDataSection text={data.text} key={i} />;
return <NoDataSection {...section} key={i} />;
case "overnight_section":
return <OvernightSection text={data.text} key={i} />;
return <OvernightSection {...section} key={i} />;
}

throw new Error(`unimplemented section type: ${type}`);
})}
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions assets/src/components/v2/dup/departures/departure_row.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React from "react";
import React, { ComponentType } from "react";

import type DepartureRowBase from "Components/v2/departures/departure_row";
import RoutePill from "Components/v2/departures/route_pill";
import Destination from "./destination";
import DepartureTimes from "./departure_times";

const DepartureRow = ({
interface Props extends DepartureRowBase {
currentPage: number;
}

const DepartureRow: ComponentType<Props> = ({
headsign,
route,
times_with_crowding: timesWithCrowding,
Expand Down
26 changes: 19 additions & 7 deletions assets/src/components/v2/dup/departures/departure_time.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from "react";
import React, { ComponentType } from "react";
import { classWithModifier, classWithModifiers, imagePath } from "Util/util";

import type DepartureTimeBase from "Components/v2/departures/departure_time";

const TextDepartureTime = ({ text }) => {
return <div className="departure-time__text">{text}</div>;
};
Expand All @@ -26,9 +28,19 @@ const TimestampDepartureTime = ({ hour, minute, am_pm, show_am_pm }) => {
);
};

const DepartureTime = ({ scheduled_time, time, currentPage }) => {
interface Props {
time: DepartureTimeBase;
scheduled_time?: DepartureTimeBase;
currentPage: number;
}

const DepartureTime: ComponentType<Props> = ({
scheduled_time,
time,
currentPage,
}) => {
let predictedTime;
if (time.type === "icon") {
if (time.type === "overnight") {
predictedTime = (
<img className="departure-time__moon-icon" src={imagePath(`moon.svg`)} />
);
Expand All @@ -50,11 +62,11 @@ const DepartureTime = ({ scheduled_time, time, currentPage }) => {

let scheduledTime;

if (time.type === "text") {
if (scheduled_time.type === "text") {
scheduledTime = <TextDepartureTime {...scheduled_time} />;
} else if (time.type === "minutes") {
} else if (scheduled_time.type === "minutes") {
scheduledTime = <MinutesDepartureTime {...scheduled_time} />;
} else if (time.type === "timestamp") {
} else if (scheduled_time.type === "timestamp") {
scheduledTime = <TimestampDepartureTime {...scheduled_time} />;
}
if (currentPage === 0) {
Expand All @@ -67,7 +79,7 @@ const DepartureTime = ({ scheduled_time, time, currentPage }) => {
return (
<div
className={classWithModifiers("departure-time", [
time.type,
scheduled_time.type,
"disabled",
])}
>
Expand Down
13 changes: 11 additions & 2 deletions assets/src/components/v2/dup/departures/departure_times.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React from "react";
import React, { ComponentType } from "react";

import { type TimeWithCrowding } from "Components/v2/departures/departure_times";
import DepartureTime from "./departure_time";

const DepartureTimes = ({ timesWithCrowding, currentPage }) => {
interface Props {
timesWithCrowding: TimeWithCrowding[];
currentPage: number;
}

const DepartureTimes: ComponentType<Props> = ({
timesWithCrowding,
currentPage,
}) => {
return (
<>
{timesWithCrowding.map(({ id, time, scheduled_time }) => (
Expand Down
10 changes: 8 additions & 2 deletions assets/src/components/v2/dup/departures/destination.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useLayoutEffect, useRef, useState } from "react";
import React, { ComponentType, useLayoutEffect, useRef, useState } from "react";

import type DestinationBase from "Components/v2/departures/destination";

const LINE_HEIGHT = 138; // px

Expand Down Expand Up @@ -42,7 +44,11 @@ const RenderedDestination = ({ parts, index1, index2, currentPageIndex }) => {
);
};

const Destination = ({ headsign, currentPage }) => {
interface Props extends DestinationBase {
currentPage: number;
}

const Destination: ComponentType<Props> = ({ headsign, currentPage }) => {
const firstLineRef = useRef<HTMLDivElement>(null);
const secondLineRef = useRef<HTMLDivElement>(null);

Expand Down
11 changes: 8 additions & 3 deletions assets/src/components/v2/dup/departures/headway_section.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React from "react";
import FreeText from "Components/v2/free_text";
import React, { ComponentType } from "react";
import FreeText, { FreeTextType } from "Components/v2/free_text";
import { classWithModifier } from "Util/util";

const HeadwaySection = ({ text, layout }) => {
interface HeadwaySection {
layout: string;
text: FreeTextType;
}

const HeadwaySection: ComponentType<HeadwaySection> = ({ text, layout }) => {
return (
<div
className={`departures-section ${classWithModifier(
Expand Down
10 changes: 7 additions & 3 deletions assets/src/components/v2/dup/departures/no_data_section.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from "react";
import React, { ComponentType } from "react";

import NoConnection from "Images/svgr_bundled/live-data-none.svg";
import FreeText from "Components/v2/free_text";
import FreeText, { FreeTextType } from "Components/v2/free_text";

const NoDataSection = ({ text }) => {
interface NoDataSection {
text: FreeTextType;
}

const NoDataSection: ComponentType<NoDataSection> = ({ text }) => {
return (
<div className="departures-section no-data-section">
<div className="no-data-section__row">
Expand Down
16 changes: 8 additions & 8 deletions assets/src/components/v2/dup/departures/normal_section.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import React from "react";
import React, { ComponentType } from "react";

import { type NormalSection as Props } from "Components/v2/departures/normal_section";
import DepartureRow from "./departure_row";
import NoticeRow from "Components/v2/departures/notice_row";
import useCurrentPage from "Hooks/use_current_dup_page";

const NormalSection = ({ rows }) => {
const NormalSection: ComponentType<Props> = ({ rows }) => {
if (rows.length == 0) return null;

const currentPage = useCurrentPage();

return (
<div className="departures-section">
{rows.map((row, index) => {
const { id, type, ...data } = row;
if (type === "departure_row") {
return <DepartureRow {...data} key={id} currentPage={currentPage} />;
} else if (type === "notice_row") {
return <NoticeRow row={row} key={"notice" + index} />;
if (row.type === "departure_row") {
return (
<DepartureRow {...row} key={row.id} currentPage={currentPage} />
);
} else {
throw new Error(`unimplemented row type: ${type}`);
return <NoticeRow row={row} key={"notice" + index} />;
}
})}
</div>
Expand Down
10 changes: 7 additions & 3 deletions assets/src/components/v2/dup/departures/overnight_section.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React from "react";
import React, { ComponentType } from "react";

import FreeText from "Components/v2/free_text";
import FreeText, { FreeTextType } from "Components/v2/free_text";

const OvernightSection = ({ text }) => {
interface OvernightSection {
text: FreeTextType;
}

const OvernightSection: ComponentType<OvernightSection> = ({ text }) => {
return (
<div className="departures-section overnight-section">
<div className="overnight-section__row">
Expand Down
1 change: 1 addition & 0 deletions lib/screens/v2/candidate_generator/dup/departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ defmodule Screens.V2.CandidateGenerator.Dup.Departures do
) do
if DateTime.compare(now, last_schedule_today.departure_time) == :gt or
DateTime.compare(now, first_schedule_today.departure_time) == :lt do
# nil/nil acts as a flag for the serializer to produce an `overnight` departure time
%Departure{
schedule: %{last_schedule_today | departure_time: nil, arrival_time: nil}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/v2/widget_instance/departures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ defmodule Screens.V2.WidgetInstance.Departures do
_screen,
_now
),
do: %{time: %{type: :icon, icon: :overnight}}
do: %{time: %{type: :overnight}}

defp serialize_time(departure, _screen, now) do
stop_id = Departure.stop_id(departure)
Expand Down
8 changes: 1 addition & 7 deletions test/screens/v2/widget_instance/departures_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,7 @@ defmodule Screens.V2.WidgetInstance.DeparturesTest do
id: "1B2M2Y8AsgTpgAmY7PhCfg==",
inline_alerts: [],
route: %{color: :orange, text: "OL", type: :text},
times_with_crowding: [
%{
crowding: nil,
id: nil,
time: %{icon: :overnight, type: :icon}
}
],
times_with_crowding: [%{id: nil, crowding: nil, time: %{type: :overnight}}],
type: :departure_row
}
],
Expand Down

0 comments on commit fb9dddc

Please sign in to comment.