diff --git a/common/api/alerts.ts b/common/api/alerts.ts index 6274f8fa6..e655dd858 100644 --- a/common/api/alerts.ts +++ b/common/api/alerts.ts @@ -1,74 +1,82 @@ import type { AlertsResponse, OldAlert } from '../types/alerts'; import type { LineShort } from '../types/lines'; -import { APP_DATA_BASE_PATH } from '../utils/constants'; +import { getStationKeysFromStations } from '../utils/stations'; +import { apiFetch } from './utils/fetch'; const alertsAPIConfig = { activity: 'BOARD,EXIT,RIDE', }; +const accessibilityAlertsAPIConfig = { + activity: 'USING_ESCALATOR,USING_WHEELCHAIR', +}; + export const fetchAlerts = async ( - route: LineShort, + line: LineShort, busRoute?: string ): Promise => { - if (route === 'Bus' && busRoute) { + if (line === 'Bus' && busRoute) { return fetchAlertsForBus(busRoute); } - return fetchAlertsForLine(route); + return fetchAlertsForLine(line); }; -const fetchAlertsForLine = async (route: LineShort): Promise => { - const url = new URL(`${APP_DATA_BASE_PATH}/api/alerts`, window.location.origin); +const fetchAlertsForLine = async (line: LineShort): Promise => { const options = { ...alertsAPIConfig }; - if (route === 'Green') { + if (line === 'Green') { // route_type 0 is light rail (green line & Mattapan) options['route_type'] = '0'; } else { options['route_type'] = '1'; - options['route'] = route; + options['route'] = line; } - Object.entries(options).forEach(([key, value]) => { - url.searchParams.append(key, value.toString()); + + return await apiFetch({ + path: '/api/alerts', + options, + errorMessage: `Failed to fetch alerts for line ${line}`, }); - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error('Failed to fetch alerts'); - } - return await response.json(); }; const fetchAlertsForBus = async (busRoute: string): Promise => { - const url = new URL(`${APP_DATA_BASE_PATH}/api/alerts`, window.location.origin); - const options = { ...alertsAPIConfig }; + const options = { ...alertsAPIConfig, route: busRoute }; options['route_type'] = '3'; - options['route'] = busRoute; - Object.entries(options).forEach(([key, value]) => { - url.searchParams.append(key, value.toString()); + + return await apiFetch({ + path: '/api/alerts', + options, + errorMessage: `Failed to fetch alerts for bus route ${busRoute}`, + }); +}; + +export const fetchAccessibilityAlertsForLine = async ( + line: LineShort +): Promise => { + const stationKeys = getStationKeysFromStations(line); + const options = { ...accessibilityAlertsAPIConfig, stop: stationKeys.join(',') }; + + return await apiFetch({ + path: '/api/alerts', + options, + errorMessage: 'Failed to fetch accessibility alerts', }); - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error('Failed to fetch alerts'); - } - return await response.json(); }; export const fetchHistoricalAlerts = async ( date: string | undefined, - route: LineShort, + line: LineShort, busRoute?: string ): Promise => { - const url = new URL(`${APP_DATA_BASE_PATH}/api/alerts/${date}`, window.location.origin); const options = { route: '' }; - if (route === 'Bus' && busRoute) { + if (line === 'Bus' && busRoute) { options['route'] = busRoute; } else { - options['route'] = route; + options['route'] = line; } - Object.entries(options).forEach(([key, value]) => { - url.searchParams.append(key, value.toString()); + + return await apiFetch({ + path: `/api/alerts/${date}`, + options, + errorMessage: 'Failed to fetch historical alerts', }); - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error('Failed to fetch alerts'); - } - return await response.json(); }; diff --git a/common/api/facilities.ts b/common/api/facilities.ts new file mode 100644 index 000000000..498c6874e --- /dev/null +++ b/common/api/facilities.ts @@ -0,0 +1,17 @@ +import type { FacilitiesResponse } from '../types/facilities'; +import type { LineShort } from '../types/lines'; +import { getStationKeysFromStations } from '../utils/stations'; +import { apiFetch } from './utils/fetch'; + +export const fetchAllElevatorsAndEscalators = async ( + line: LineShort +): Promise => { + const stationKeys = getStationKeysFromStations(line); + const options = { type: 'ESCALATOR,ELEVATOR', stop: stationKeys.join(',') }; + + return await apiFetch({ + path: '/api/facilities', + options, + errorMessage: 'Failed to fetch elevators and escalators', + }); +}; diff --git a/common/api/hooks/alerts.ts b/common/api/hooks/alerts.ts index 7b205986c..ae70a4d83 100644 --- a/common/api/hooks/alerts.ts +++ b/common/api/hooks/alerts.ts @@ -1,18 +1,18 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import { fetchAlerts, fetchHistoricalAlerts } from '../alerts'; +import { fetchAlerts, fetchHistoricalAlerts, fetchAccessibilityAlertsForLine } from '../alerts'; import type { LineShort } from '../../types/lines'; import { FIVE_MINUTES, ONE_MINUTE } from '../../constants/time'; import type { AlertsResponse } from '../../types/alerts'; export const useHistoricalAlertsData = ( date: string | undefined, - route: LineShort, + line: LineShort, busRoute?: string ) => { return useQuery( - ['alerts', date, route, busRoute], - () => fetchHistoricalAlerts(date, route, busRoute), + ['alerts', date, line, busRoute], + () => fetchHistoricalAlerts(date, line, busRoute), { staleTime: FIVE_MINUTES, enabled: date !== undefined, @@ -21,10 +21,16 @@ export const useHistoricalAlertsData = ( }; export const useAlertsData = ( - route: LineShort, + line: LineShort, busRoute?: string ): UseQueryResult => { - return useQuery(['alerts', route, busRoute], () => fetchAlerts(route, busRoute), { + return useQuery(['alerts', line, busRoute], () => fetchAlerts(line, busRoute), { + staleTime: ONE_MINUTE, + }); +}; + +export const useAccessibilityAlertsData = (line: LineShort) => { + return useQuery(['accessibilityAlerts', line], () => fetchAccessibilityAlertsForLine(line), { staleTime: ONE_MINUTE, }); }; diff --git a/common/api/hooks/facilities.ts b/common/api/hooks/facilities.ts new file mode 100644 index 000000000..0227af771 --- /dev/null +++ b/common/api/hooks/facilities.ts @@ -0,0 +1,7 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchAllElevatorsAndEscalators } from '../facilities'; +import type { LineShort } from '../../types/lines'; + +export const useElevatorsAndEscalators = (line: LineShort) => { + return useQuery(['elevAndEsc', line], () => fetchAllElevatorsAndEscalators(line)); +}; diff --git a/common/api/hooks/ridership.ts b/common/api/hooks/ridership.ts index 48d71e0ac..6541ddebe 100644 --- a/common/api/hooks/ridership.ts +++ b/common/api/hooks/ridership.ts @@ -3,8 +3,8 @@ import type { FetchRidershipOptions } from '../../types/api'; import { fetchLandingRidership, fetchRidership } from '../ridership'; import { ONE_HOUR } from '../../constants/time'; -export const useRidershipData = (params: FetchRidershipOptions, enabled?: boolean) => { - return useQuery(['trips', params], () => fetchRidership(params), { +export const useRidershipData = (options: FetchRidershipOptions, enabled?: boolean) => { + return useQuery(['trips', options], () => fetchRidership(options), { enabled: enabled, staleTime: ONE_HOUR, }); diff --git a/common/api/hooks/service.ts b/common/api/hooks/service.ts index fb9f4d14e..9c3e4ace2 100644 --- a/common/api/hooks/service.ts +++ b/common/api/hooks/service.ts @@ -3,8 +3,8 @@ import type { FetchScheduledServiceOptions } from '../../types/api'; import { ONE_HOUR } from '../../constants/time'; import { fetchScheduledService } from '../service'; -export const useScheduledService = (params: FetchScheduledServiceOptions, enabled?: boolean) => { - return useQuery(['scheduledservice', params], () => fetchScheduledService(params), { +export const useScheduledService = (options: FetchScheduledServiceOptions, enabled?: boolean) => { + return useQuery(['scheduledservice', options], () => fetchScheduledService(options), { enabled: enabled, staleTime: ONE_HOUR, }); diff --git a/common/api/hooks/speed.ts b/common/api/hooks/speed.ts index f2968837e..e701be99d 100644 --- a/common/api/hooks/speed.ts +++ b/common/api/hooks/speed.ts @@ -3,8 +3,8 @@ import { fetchSpeeds } from '../speed'; import type { FetchSpeedsOptions } from '../../types/api'; import { FIVE_MINUTES } from '../../constants/time'; -export const useSpeedData = (params: FetchSpeedsOptions, enabled?: boolean) => { - return useQuery(['speed', params], () => fetchSpeeds(params), { +export const useSpeedData = (options: FetchSpeedsOptions, enabled?: boolean) => { + return useQuery(['speed', options], () => fetchSpeeds(options), { enabled: enabled, staleTime: FIVE_MINUTES, }); diff --git a/common/api/hooks/tripmetrics.ts b/common/api/hooks/tripmetrics.ts index 919e538f9..88ac98dae 100644 --- a/common/api/hooks/tripmetrics.ts +++ b/common/api/hooks/tripmetrics.ts @@ -4,10 +4,10 @@ import { FIVE_MINUTES } from '../../constants/time'; import { fetchActualTripsByLine, fetchLandingTripMetrics } from '../tripmetrics'; export const useDeliveredTripMetrics = ( - params: FetchDeliveredTripMetricsOptions, + options: FetchDeliveredTripMetricsOptions, enabled?: boolean ) => { - return useQuery(['actualTrips', params], () => fetchActualTripsByLine(params), { + return useQuery(['actualTrips', options], () => fetchActualTripsByLine(options), { enabled: enabled, staleTime: FIVE_MINUTES, }); diff --git a/common/api/ridership.ts b/common/api/ridership.ts index d98ff9269..a9c8f92b1 100644 --- a/common/api/ridership.ts +++ b/common/api/ridership.ts @@ -2,19 +2,18 @@ import { FetchRidershipParams } from '../types/api'; import type { FetchRidershipOptions } from '../types/api'; import type { RidershipCount } from '../types/dataPoints'; import { type Line } from '../types/lines'; -import { APP_DATA_BASE_PATH } from '../utils/constants'; +import { apiFetch } from './utils/fetch'; export const fetchRidership = async ( - params: FetchRidershipOptions + options: FetchRidershipOptions ): Promise => { - if (!params[FetchRidershipParams.lineId]) return undefined; - const url = new URL(`${APP_DATA_BASE_PATH}/api/ridership`, window.location.origin); - Object.keys(params).forEach((paramKey) => { - url.searchParams.append(paramKey, params[paramKey]); + if (!options[FetchRidershipParams.lineId]) return undefined; + + return await apiFetch({ + path: '/api/ridership', + options, + errorMessage: 'Failed to fetch ridership counts', }); - const response = await fetch(url.toString()); - if (!response.ok) throw new Error('Failed to fetch ridership counts'); - return await response.json(); }; export const fetchLandingRidership = async (): Promise<{ [key in Line]: RidershipCount[] }> => { diff --git a/common/api/service.ts b/common/api/service.ts index 9239278dc..ea97b752a 100644 --- a/common/api/service.ts +++ b/common/api/service.ts @@ -1,18 +1,16 @@ import type { FetchScheduledServiceOptions } from '../types/api'; import { FetchScheduledServiceParams } from '../types/api'; import type { ScheduledService } from '../types/dataPoints'; -import { APP_DATA_BASE_PATH } from '../utils/constants'; +import { apiFetch } from './utils/fetch'; export const fetchScheduledService = async ( - params: FetchScheduledServiceOptions + options: FetchScheduledServiceOptions ): Promise => { - if (!params[FetchScheduledServiceParams.routeId]) return undefined; - const url = new URL(`${APP_DATA_BASE_PATH}/api/scheduledservice`, window.location.origin); - Object.keys(params).forEach((paramKey) => { - url.searchParams.append(paramKey, params[paramKey]); - }); - const response = await fetch(url.toString()); - if (!response.ok) throw new Error('Failed to fetch trip counts'); + if (!options[FetchScheduledServiceParams.routeId]) return undefined; - return await response.json(); + return await apiFetch({ + path: '/api/scheduledservice', + options, + errorMessage: 'Failed to fetch trip counts', + }); }; diff --git a/common/api/slowzones.ts b/common/api/slowzones.ts index 875272a20..541af8ea4 100644 --- a/common/api/slowzones.ts +++ b/common/api/slowzones.ts @@ -4,8 +4,8 @@ import type { SpeedRestriction, } from '../../common/types/dataPoints'; import type { FetchSpeedRestrictionsOptions, FetchSpeedRestrictionsResponse } from '../types/api'; -import { APP_DATA_BASE_PATH } from '../utils/constants'; import { getGtfsRailLineId } from '../utils/lines'; +import { apiFetch } from './utils/fetch'; export const fetchDelayTotals = (): Promise => { const url = new URL(`/static/slowzones/delay_totals.json`, window.location.origin); @@ -21,18 +21,18 @@ export const fetchSpeedRestrictions = async ( options: FetchSpeedRestrictionsOptions ): Promise => { const { lineId, date: requestedDate } = options; - const params = new URLSearchParams({ line_id: getGtfsRailLineId(lineId), date: requestedDate }); - const speedRestrictionsUrl = new URL( - `${APP_DATA_BASE_PATH}/api/speed_restrictions?${params.toString()}`, - window.location.origin - ); - const today = new Date(); - const response = await fetch(speedRestrictionsUrl.toString()); + const { available, date: resolvedDate, zones, - }: FetchSpeedRestrictionsResponse = await response.json(); + }: FetchSpeedRestrictionsResponse = await apiFetch({ + path: `/api/speed_restrictions`, + options: { line_id: getGtfsRailLineId(lineId), date: requestedDate }, + errorMessage: 'Failed to fetch speed restrictions', + }); + + const today = new Date(); if (available) { return zones.map((zone) => ({ ...zone, diff --git a/common/api/speed.ts b/common/api/speed.ts index 8e3cba7aa..5ed2eb4a2 100644 --- a/common/api/speed.ts +++ b/common/api/speed.ts @@ -1,16 +1,14 @@ import type { FetchSpeedsOptions as FetchSpeedsOptions } from '../types/api'; import { FetchSpeedsParams as FetchSpeedsParams } from '../types/api'; import type { SpeedDataPoint } from '../types/dataPoints'; -import { APP_DATA_BASE_PATH } from '../utils/constants'; +import { apiFetch } from './utils/fetch'; -export const fetchSpeeds = async (params: FetchSpeedsOptions): Promise => { - if (!params[FetchSpeedsParams.line]) return []; - const url = new URL(`${APP_DATA_BASE_PATH}/api/speed`, window.location.origin); - Object.keys(params).forEach((paramKey) => { - url.searchParams.append(paramKey, params[paramKey]); - }); - const response = await fetch(url.toString()); - if (!response.ok) throw new Error('Failed to fetch traversal times'); +export const fetchSpeeds = async (options: FetchSpeedsOptions): Promise => { + if (!options[FetchSpeedsParams.line]) return []; - return await response.json(); + return await apiFetch({ + path: '/api/speed', + options, + errorMessage: 'Failed to fetch traversal times', + }); }; diff --git a/common/api/tripmetrics.ts b/common/api/tripmetrics.ts index f09be6ed1..e7fb810e2 100644 --- a/common/api/tripmetrics.ts +++ b/common/api/tripmetrics.ts @@ -2,19 +2,18 @@ import type { FetchDeliveredTripMetricsOptions } from '../types/api'; import { FetchDeliveredTripMetricsParams } from '../types/api'; import type { DeliveredTripMetrics } from '../types/dataPoints'; import type { Line } from '../types/lines'; -import { APP_DATA_BASE_PATH } from '../utils/constants'; +import { apiFetch } from './utils/fetch'; export const fetchActualTripsByLine = async ( - params: FetchDeliveredTripMetricsOptions + options: FetchDeliveredTripMetricsOptions ): Promise => { - if (!params[FetchDeliveredTripMetricsParams.line]) return []; - const url = new URL(`${APP_DATA_BASE_PATH}/api/tripmetrics`, window.location.origin); - Object.keys(params).forEach((paramKey) => { - url.searchParams.append(paramKey, params[paramKey]); + if (!options[FetchDeliveredTripMetricsParams.line]) return []; + + return await apiFetch({ + path: '/api/tripmetrics', + options, + errorMessage: 'Failed to fetch trip metrics', }); - const response = await fetch(url.toString()); - if (!response.ok) throw new Error('Failed to fetch trip metrics'); - return await response.json(); }; export const fetchLandingTripMetrics = (): Promise<{ [key in Line]: DeliveredTripMetrics[] }> => { diff --git a/common/api/utils/fetch.ts b/common/api/utils/fetch.ts new file mode 100644 index 000000000..70dce84ea --- /dev/null +++ b/common/api/utils/fetch.ts @@ -0,0 +1,11 @@ +import { APP_DATA_BASE_PATH } from '../../utils/constants'; + +export const apiFetch = async ({ path, options, errorMessage }) => { + const url = new URL(`${APP_DATA_BASE_PATH}${path}`, window.location.origin); + Object.entries(options).forEach(([key, value]: [string, any]) => { + url.searchParams.append(key, value.toString()); + }); + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(errorMessage); + return await response.json(); +}; diff --git a/common/types/alerts.ts b/common/types/alerts.ts index 77fc5f8ab..2edec7b15 100644 --- a/common/types/alerts.ts +++ b/common/types/alerts.ts @@ -27,6 +27,7 @@ export interface FormattedAlert { stops: string[]; routes?: string[]; header: string; + description?: string; } export interface AlertsResponse { diff --git a/common/types/facilities.ts b/common/types/facilities.ts new file mode 100644 index 000000000..3525e4b2c --- /dev/null +++ b/common/types/facilities.ts @@ -0,0 +1,30 @@ +export interface FacilityProperty { + value: string; + name: string; +} + +export interface Facility { + type: 'facility'; + relationships: { + stop: { + data: { + type: 'stop'; + id: string; + }; + }; + }; + id: string; + attributes: { + type: 'ELEVATOR' | 'ESCALATOR'; + short_name: string; + properties: FacilityProperty[]; + longitude: number; + long_name: string; + latitude: number; + }; +} + +export interface FacilitiesResponse { + jsonApi: string; + data: Facility[]; +} diff --git a/common/types/stations.ts b/common/types/stations.ts index 772870171..857607719 100644 --- a/common/types/stations.ts +++ b/common/types/stations.ts @@ -20,6 +20,10 @@ export interface Station { short?: string; } +export const isLineMap = (obj: LineMap | Station[]): obj is LineMap => { + return (obj as LineMap).stations !== undefined; +}; + export interface LineMap { type: string; direction: Direction; diff --git a/common/utils/stations.ts b/common/utils/stations.ts index 18607fbd2..aef64e3d5 100644 --- a/common/utils/stations.ts +++ b/common/utils/stations.ts @@ -1,5 +1,5 @@ import type { Line, LineShort } from '../../common/types/lines'; -import type { Station } from '../../common/types/stations'; +import { isLineMap, type Station } from '../../common/types/stations'; import type { Location } from '../types/charts'; import type { Direction } from '../types/dataPoints'; import { stations, rtStations, busStations } from './../constants/stations'; @@ -113,3 +113,12 @@ export const getLocationDetails = ( direction: travelDirection(from, to), }; }; + +export const getStationKeysFromStations = (line: LineShort): string[] => { + const lineStations = stations[line].stations; + if (isLineMap(lineStations)) { + return lineStations.stations.map((station: Station) => station.station); + } else { + return lineStations.map((station: Station) => station.station); + } +}; diff --git a/modules/alerts/AlertsWidget.tsx b/modules/alerts/AlertsWidget.tsx new file mode 100644 index 000000000..155e377d6 --- /dev/null +++ b/modules/alerts/AlertsWidget.tsx @@ -0,0 +1,19 @@ +import { useAccessibilityAlertsData, useAlertsData } from '../../common/api/hooks/alerts'; +import { useDelimitatedRoute } from '../../common/utils/router'; +import type { LineShort } from '../../common/types/lines'; +import { Alerts } from '../commute/alerts/Alerts'; + +export const AlertsWidget = ({ lineShort }: { lineShort: LineShort }) => { + const { + query: { busRoute }, + } = useDelimitatedRoute(); + const rideAlerts = useAlertsData(lineShort, busRoute); + const accessibilityAlerts = useAccessibilityAlertsData(lineShort); + + return ( + <> + + + + ); +}; diff --git a/modules/commute/alerts/AccessibilityAlert.tsx b/modules/commute/alerts/AccessibilityAlert.tsx new file mode 100644 index 000000000..e88cd97b2 --- /dev/null +++ b/modules/commute/alerts/AccessibilityAlert.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type { FormattedAlert, UpcomingOrCurrent } from '../../../common/types/alerts'; +import { AlertEffect } from '../../../common/types/alerts'; +import EscalatorIcon from '../../../public/Icons/EscalatorIcon.svg'; +import ElevatorIcon from '../../../public/Icons/ElevatorIcon.svg'; +import { stations } from '../../../common/constants/stations'; +import type { LineShort } from '../../../common/types/lines'; +import { isLineMap } from '../../../common/types/stations'; +import { AlertBoxInner } from './AlertBoxInner'; + +interface DelayAlertProps { + alert: FormattedAlert; + type: UpcomingOrCurrent; + lineShort: LineShort; +} + +export const AccessibilityAlert: React.FC = ({ alert, type, lineShort }) => { + const lineStations = stations[lineShort].stations; + const stops = alert.stops + .map((stop) => + isLineMap(lineStations) + ? lineStations.stations.find((station) => station.station === stop) + : lineStations.find((station) => station.station === stop) + ) + .filter((stop) => stop !== undefined); + + const alertText = + alert.type === AlertEffect.ESCALATOR_CLOSURE + ? 'Escalator out of service at' + : 'Elevator out of service at'; + const stopsText = stops.map((stop) => stop?.stop_name).join(', '); + + return ( + + + {alertText} {stopsText} + + + ); +}; diff --git a/modules/commute/alerts/AlertBox.tsx b/modules/commute/alerts/AlertBox.tsx index 2f8e788d5..a70e7259f 100644 --- a/modules/commute/alerts/AlertBox.tsx +++ b/modules/commute/alerts/AlertBox.tsx @@ -10,6 +10,7 @@ import { DelayAlert } from './DelayAlert'; import { ShuttleAlert } from './ShuttleAlert'; import { StopClosure } from './StopClosureAlert'; import { SuspensionAlert } from './SuspensionAlert'; +import { AccessibilityAlert } from './AccessibilityAlert'; interface AlertBoxProps { alerts: AlertsResponse[]; @@ -58,6 +59,9 @@ const getAlertComponent = ( if (alert.type === AlertEffect.STOP_CLOSURE && busRoute) { return ; } + if ([AlertEffect.ELEVATOR_CLOSURE, AlertEffect.ESCALATOR_CLOSURE].includes(alert.type)) { + return ; + } }; export const AlertBox: React.FC = ({ alerts, lineShort, busRoute, type }) => { @@ -71,11 +75,12 @@ export const AlertBox: React.FC = ({ alerts, lineShort, busRoute, ); } else { + const alertComponents = relevantAlerts.map((alert: FormattedAlert) => + getAlertComponent(alert, lineShort, type, busRoute) + ); return (
- {relevantAlerts.map((alert: FormattedAlert) => - getAlertComponent(alert, lineShort, type, busRoute) - )} + {alertComponents}
); } diff --git a/modules/commute/alerts/AlertBoxInner.tsx b/modules/commute/alerts/AlertBoxInner.tsx index 49973a92f..d328cb231 100644 --- a/modules/commute/alerts/AlertBoxInner.tsx +++ b/modules/commute/alerts/AlertBoxInner.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import type { FormattedAlert, UpcomingOrCurrent } from '../../../common/types/alerts'; import { useBreakpoint } from '../../../common/hooks/useBreakpoint'; import { AlertModal } from './AlertModal'; -import { CurrentTime, UpcomingTime } from './Time'; +import { CurrentTime, EffectiveTime, UpcomingTime } from './Time'; interface AlertBoxInnerProps { header: string; @@ -12,6 +12,7 @@ interface AlertBoxInnerProps { type: UpcomingOrCurrent; children: React.ReactNode; noShrink?: boolean; + showEffectiveTime?: boolean; } export const AlertBoxInner: React.FC = ({ @@ -21,6 +22,7 @@ export const AlertBoxInner: React.FC = ({ type, noShrink, children, + showEffectiveTime, }) => { const [showModal, setShowModal] = useState(false); const isMobile = !useBreakpoint('md'); @@ -40,6 +42,7 @@ export const AlertBoxInner: React.FC = ({ showModal={showModal} setShowModal={setShowModal} header={header} + description={alert.description} Icon={Icon} type={alert.type} /> @@ -50,7 +53,7 @@ export const AlertBoxInner: React.FC = ({
@@ -64,7 +67,11 @@ export const AlertBoxInner: React.FC = ({ )} > {type === 'current' ? ( - + showEffectiveTime ? ( + + ) : ( + + ) ) : ( )} diff --git a/modules/commute/alerts/AlertModal.tsx b/modules/commute/alerts/AlertModal.tsx index a355fe1d8..442a2496b 100644 --- a/modules/commute/alerts/AlertModal.tsx +++ b/modules/commute/alerts/AlertModal.tsx @@ -10,6 +10,7 @@ interface AlertModalProps { showModal: boolean; setShowModal: React.Dispatch>; header: string; + description?: string; Icon: React.ElementType; type: string; } @@ -18,6 +19,7 @@ export const AlertModal: React.FC = ({ showModal, setShowModal, header, + description, Icon, type, }) => { @@ -50,7 +52,12 @@ export const AlertModal: React.FC = ({ >
-
+
@@ -62,6 +69,7 @@ export const AlertModal: React.FC = ({

{header}

+ {description &&

{description}

}
diff --git a/modules/commute/alerts/Alerts.tsx b/modules/commute/alerts/Alerts.tsx index 091403677..db28ab501 100644 --- a/modules/commute/alerts/Alerts.tsx +++ b/modules/commute/alerts/Alerts.tsx @@ -1,22 +1,27 @@ import React from 'react'; import classNames from 'classnames'; +import type { UseQueryResult } from '@tanstack/react-query'; import { useDelimitatedRoute } from '../../../common/utils/router'; import { lineColorBackground } from '../../../common/styles/general'; import { Divider } from '../../../common/components/general/Divider'; import { ChartPlaceHolder } from '../../../common/components/graphics/ChartPlaceHolder'; -import { useAlertsData } from '../../../common/api/hooks/alerts'; +import type { AlertsResponse } from '../../../common/types/alerts'; import { AlertBox } from './AlertBox'; -export const Alerts: React.FC = () => { +interface AlertsProps { + title: string; + alerts: UseQueryResult; +} + +export const Alerts: React.FC = ({ title, alerts }) => { const { line, lineShort, query: { busRoute }, } = useDelimitatedRoute(); - const alerts = useAlertsData(lineShort, busRoute); const divStyle = classNames( - 'flex flex-col rounded-md py-4 text-white shadow-dataBox w-full xl:w-1/3 gap-y-2 md:max-h-[309px] md:overflow-y-auto', + 'flex flex-col rounded-md py-4 text-white shadow-dataBox w-full gap-y-2 md:max-h-[309px] md:overflow-y-auto', lineColorBackground[line ?? 'DEFAULT'] ); @@ -32,7 +37,7 @@ export const Alerts: React.FC = () => { } return (
-

Alerts

+

{title}

diff --git a/modules/commute/alerts/Time.tsx b/modules/commute/alerts/Time.tsx index dfd3a849f..0ada39a3b 100644 --- a/modules/commute/alerts/Time.tsx +++ b/modules/commute/alerts/Time.tsx @@ -70,3 +70,13 @@ export const UpcomingTime: React.FC = ({ times }) => { ); }; + +export const EffectiveTime: React.FC = ({ times }) => { + const timeString = dayjs(times[0].start).format('MMM D YYYY'); + return ( + <> + + Since {timeString} + + ); +}; diff --git a/modules/dashboard/Overview.tsx b/modules/dashboard/Overview.tsx index d409c2b36..0a5ee797d 100644 --- a/modules/dashboard/Overview.tsx +++ b/modules/dashboard/Overview.tsx @@ -8,6 +8,8 @@ import { Layout } from '../../common/layouts/layoutTypes'; import { RidershipWidget } from '../ridership/RidershipWidget'; import { HEAVY_RAIL_LINES } from '../../common/types/lines'; import { useRewriteV3Route } from '../../common/utils/middleware'; +import { LINE_OBJECTS } from '../../common/constants/lines'; +import { AlertsWidget } from '../alerts/AlertsWidget'; export function Overview() { const { tab, line } = useDelimitatedRoute(); @@ -15,6 +17,9 @@ export function Overview() { useRewriteV3Route(); const isHeavyRailLine = line ? HEAVY_RAIL_LINES.includes(line) : false; + + const lineShort = line && line !== 'line-bus' ? LINE_OBJECTS[line].short : null; + return (
@@ -22,6 +27,7 @@ export function Overview() { {tab === 'Subway' && } {tab === 'Subway' && isHeavyRailLine && } + {tab === 'Subway' && lineShort && }
); diff --git a/modules/dashboard/Today.tsx b/modules/dashboard/Today.tsx index 843629b9a..5dee44844 100644 --- a/modules/dashboard/Today.tsx +++ b/modules/dashboard/Today.tsx @@ -1,10 +1,12 @@ import React from 'react'; import { Alerts } from '../commute/alerts/Alerts'; -import { Speed } from '../commute/speed/Speed'; import { SlowZonesMap } from '../slowzones/map'; import { WidgetDiv } from '../../common/components/widgets/WidgetDiv'; import { useSlowzoneAllData, useSpeedRestrictionData } from '../../common/api/hooks/slowzones'; import { PageWrapper } from '../../common/layouts/PageWrapper'; +import { Speed } from '../commute/speed/Speed'; +import { useAccessibilityAlertsData, useAlertsData } from '../../common/api/hooks/alerts'; +import { useDelimitatedRoute } from '../../common/utils/router'; import type { Line } from '../../common/types/lines'; import { WidgetTitle } from './WidgetTitle'; @@ -20,12 +22,19 @@ export const Today: React.FC = ({ lineShort }) => { }); const canShowSlowZonesMap = lineShort !== 'Green'; + const { + query: { busRoute }, + } = useDelimitatedRoute(); + const rideAlerts = useAlertsData(lineShort, busRoute); + const accessibilityAlerts = useAccessibilityAlertsData(lineShort); + return (
- + {canShowSlowZonesMap && } +
{canShowSlowZonesMap && allSlow.data && speedRestrictions.data && ( diff --git a/public/Icons/ElevatorIcon.svg b/public/Icons/ElevatorIcon.svg new file mode 100644 index 000000000..45e5c97d2 --- /dev/null +++ b/public/Icons/ElevatorIcon.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/Icons/EscalatorIcon.svg b/public/Icons/EscalatorIcon.svg new file mode 100644 index 000000000..31820e9b5 --- /dev/null +++ b/public/Icons/EscalatorIcon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/server/app.py b/server/app.py index a95c39830..7c3b4dfb0 100644 --- a/server/app.py +++ b/server/app.py @@ -167,7 +167,7 @@ def get_git_id(): @app.route("/api/alerts", cors=cors_config) def get_alerts(): - response = mbta_v3.getV3("alerts", app.current_request.query_params) + response = mbta_v3.getAlerts(app.current_request.query_params) return json.dumps(response, indent=4, sort_keys=True, default=str) @@ -207,6 +207,12 @@ def get_ridership(): return json.dumps(response) +@app.route("/api/facilities", cors=cors_config) +def get_facilities(): + response = mbta_v3.getV3("facilities", app.current_request.query_params) + return json.dumps(response, indent=4, sort_keys=True, default=str) + + @app.route("/api/speed_restrictions", cors=cors_config) def get_speed_restrictions(): query = app.current_request.query_params diff --git a/server/chalicelib/mbta_v3.py b/server/chalicelib/mbta_v3.py index 8d00a2ad0..011267ba5 100644 --- a/server/chalicelib/mbta_v3.py +++ b/server/chalicelib/mbta_v3.py @@ -54,7 +54,23 @@ def delay_alert(attributes, id): } -def format_response(alerts_data): # TODO: separate logic for bus to avoid repeat stops. +def accessibility_alert(attributes, id): + """Format alerts for escalators""" + stops = set() # Eliminate duplicates (bus alerts sometimes have entries for multiple routes for one stop.) + for entity in attributes["informed_entity"]: + if entity.get("stop") and not entity["stop"].isdigit(): # Only get `place-` stop types - no numbers. + stops.add(entity["stop"]) + return { + "id": id, + "stops": list(stops), + "header": attributes["header"], + "description": attributes["description"], + "type": attributes["effect"], + "active_period": format_active_alerts(attributes["active_period"]), + } + + +def format_alerts_response(alerts_data): # TODO: separate logic for bus to avoid repeat stops. alerts_filtered = [] for alert in alerts_data: attributes = alert["attributes"] @@ -66,6 +82,8 @@ def format_response(alerts_data): # TODO: separate logic for bus to avoid repea alerts_filtered.append(shuttle_alert(attributes, alert["id"])) if attributes["effect"] == "DELAY" or attributes["effect"] == "DETOUR": alerts_filtered.append(delay_alert(attributes, alert["id"])) + if attributes["effect"] == "ESCALATOR_CLOSURE" or attributes["effect"] == "ELEVATOR_CLOSURE": + alerts_filtered.append(accessibility_alert(attributes, alert["id"])) return alerts_filtered @@ -111,6 +129,11 @@ def format_active_alerts(alert_active_period): return list(map(get_active, alert_active_period)) +def getAlerts(params={}): + response = getV3("alerts", params) + return format_alerts_response(response["data"]) + + def getV3(command, params={}): """Make a GET request against the MBTA v3 API""" url = BASE_URL_V3.format(command=command, parameters=format_parameters(params)) @@ -123,4 +146,4 @@ def getV3(command, params={}): print(response.content.decode("utf-8")) raise # TODO: catch this gracefully data = json.loads(response.content.decode("utf-8"), parse_float=Decimal, parse_int=Decimal) - return format_response(data["data"]) + return data