diff --git a/app/component/trafficnow/CanceledTripCard.js b/app/component/trafficnow/CanceledTripCard.js index b3a139736a..d8c07f03a8 100644 --- a/app/component/trafficnow/CanceledTripCard.js +++ b/app/component/trafficnow/CanceledTripCard.js @@ -1,57 +1,36 @@ import React from 'react'; import { useRouter } from 'found'; -import { DateTime } from 'luxon'; import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; +import { connectToStores } from 'fluxible-addons-react'; import { useConfigContext } from '../../configurations/ConfigContext'; -import { PREFIX_TIMETABLE, TRAFFICNOW, routePagePath } from '../../util/path'; +import { TRAFFICNOW, routePagePath } from '../../util/path'; import Card from '../Card'; import Icon from '../Icon'; import CanceledDepartures from './components/CanceledDepartures'; import DisruptionStatus from './components/DisruptionStatus'; import RouteBadgeGroup from './components/RouteBadgeGroup'; import DisruptionBadge from './DisruptionBadge'; +import EntityBadge from './components/EntityBadge'; +import { favouriteShape, patternShape, routeShape } from '../../util/shapes'; +import { useFilterContext } from './filters/FiltersContext'; -const CanceledTripCard = ({ mode, totalCount, trips, isMobile = false }) => { +const CanceledTripCard = ({ + mode, + routes, + isMobile = false, + favourites = [], +}) => { const { router } = useRouter(); - const { colors } = useConfigContext(); - + const intl = useIntl(); + const { colors, trafficNowMaxRoutesPerCard } = useConfigContext(); + const { selectedFilters } = useFilterContext(); const handleRouteBadgeClick = url => e => { e.preventDefault(); e.stopPropagation(); router.push(url); }; - - /* eslint-disable no-param-reassign */ - const groupedTrips = trips.reduce((container, { start, trip }) => { - if (!trip?.route?.gtfsId || !start?.schedule?.time?.departure) { - return container; - } - - const shortName = trip?.route?.shortName || 'unknown'; - if (container[shortName]) { - container[shortName].trips.push({ - ...trip, - departureTime: DateTime.fromISO( - start?.schedule.time.departure, - ).toFormat('HH:mm'), - }); - } else { - container[shortName] = { - routeGtfsId: trip.route.gtfsId, - trips: [ - { - ...trip, - departureTime: DateTime.fromISO( - start?.schedule.time.departure, - ).toFormat('HH:mm'), - }, - ], - }; - } - return container; - }, {}); - - const isSingleRoute = Object.keys(groupedTrips).length === 1; + const favRoutes = favourites.map(({ gtfsId }) => gtfsId); return ( { {!isMobile && ( <> - {' '}
{ ({ - id: shortName, - name: shortName, - url: routePagePath(routeGtfsId, PREFIX_TIMETABLE), - gtfsId: routeGtfsId, - trips: groupedRouteTrips, - }), - )} - renderRouteSuffix={({ trips: groupedRouteTrips }) => - isSingleRoute ? ( + highlightedGtfsId={selectedFilters.entity?.gtfsId} + routes={routes + .slice() + .sort( + (a, b) => + Number(favRoutes.includes(b.route.gtfsId)) - + Number(favRoutes.includes(a.route.gtfsId)), + ) + .slice(0, trafficNowMaxRoutesPerCard) + .map(({ route }) => ({ + name: route.shortName, + gtfsId: route.gtfsId, + id: route.id, + url: routePagePath(route.gtfsId), + }))} + renderRouteSuffix={route => + routes.length === 1 ? ( ({ - tripId, - departureTime, - }), - )} + inline + mode={mode} + departureLimit={5} + patterns={routes + .find( + routeSummary => routeSummary.route.gtfsId === route.gtfsId, + ) + .patterns.map(p => p.pattern)} /> ) : null } renderSuffix={ - totalCount > trips.length ? ( - - - + routes.length > trafficNowMaxRoutesPerCard ? ( + ) : null } /> @@ -124,9 +118,28 @@ const CanceledTripCard = ({ mode, totalCount, trips, isMobile = false }) => { CanceledTripCard.propTypes = { mode: PropTypes.string.isRequired, - totalCount: PropTypes.number.isRequired, - trips: PropTypes.arrayOf(PropTypes.shape({})).isRequired, isMobile: PropTypes.bool, + favourites: PropTypes.arrayOf(favouriteShape), + routes: PropTypes.arrayOf( + PropTypes.shape({ + cancellationCount: PropTypes.number.isRequired, + route: routeShape.isRequired, + patterns: PropTypes.arrayOf( + PropTypes.shape({ + pattern: patternShape.isRequired, + cancellationCount: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + }), + ).isRequired, }; -export default CanceledTripCard; +const connectedComponent = connectToStores( + CanceledTripCard, + ['FavouriteStore'], + context => ({ + favourites: context.getStore('FavouriteStore').getFavourites(), + }), +); + +export { connectedComponent as default, CanceledTripCard as Component }; diff --git a/app/component/trafficnow/CanceledTrips.js b/app/component/trafficnow/CanceledTrips.js index 1642e12777..9485378c92 100644 --- a/app/component/trafficnow/CanceledTrips.js +++ b/app/component/trafficnow/CanceledTrips.js @@ -1,150 +1,84 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import Button from '@hsl-fi/button'; import cx from 'classnames'; import Link from 'found/Link'; -import { DateTime } from 'luxon'; import PropTypes from 'prop-types'; import { FormattedMessage, useIntl } from 'react-intl'; -import { usePaginationFragment } from 'react-relay/hooks'; import { useConfigContext } from '../../configurations/ConfigContext'; import Card from '../Card'; import Icon from '../Icon'; -import CanceledTripsModal from './CanceledTripsModal'; import CancellationContainer from './components/CancellationContainer'; import ResultsProgressBar from './components/ResultsProgressBar'; -import DisruptionBadge from './DisruptionBadge'; -import DisruptionStatus from './components/DisruptionStatus'; -import CanceledTripsPaginationFragment from './queries/CanceledTripsPaginationFragment'; +import { patternShape, routeShape } from '../../util/shapes'; -const CANCELED_TRIPS_QUERY_AMOUNT = 20; +const DEFAULT_ROUTES_SHOWN_AMOUNT = 8; -const CanceledTrips = ({ query, isMobile = false, ...props }) => { +const CanceledTrips = ({ canceledRoutes = [], mode, isMobile = false }) => { const { colors } = useConfigContext(); const intl = useIntl(); - const [detailsKey, setDetailsKey] = useState(null); - - const { - data: { canceledTrips }, - loadNext, - isLoadingNext, - hasNext, - } = usePaginationFragment(CanceledTripsPaginationFragment, query); - - const mode = props.mode.toLowerCase(); - - const allEdges = canceledTrips?.edges ?? []; - - const trips = useMemo( - () => - /* eslint-disable no-param-reassign */ - allEdges.reduce((routeGroups, { node }) => { - if ( - !node?.trip?.route?.gtfsId || - !node?.start?.schedule?.time?.departure - ) { - return routeGroups; - } - - const { start, end, trip } = node; - const routeShortName = trip?.route?.shortName; - const patternCode = trip?.pattern?.code; - - if (!routeGroups[routeShortName]) { - routeGroups[routeShortName] = { - routeGtfsId: trip.route.gtfsId, - patterns: {}, - }; - } - - if (routeGroups[routeShortName].patterns[patternCode]) { - routeGroups[routeShortName].patterns[ - patternCode - ].canceledDepartures.push( - DateTime.fromISO(start?.schedule.time.departure).toFormat('HH:mm'), - ); - } else { - routeGroups[routeShortName].patterns[patternCode] = { - start, - end, - trip, - canceledDepartures: [ - DateTime.fromISO(start?.schedule.time.departure).toFormat( - 'HH:mm', - ), - ], - }; - } - - return routeGroups; - }, {}), - [allEdges], + const [showAmount, setShowAmount] = useState( + DEFAULT_ROUTES_SHOWN_AMOUNT > canceledRoutes.length + ? canceledRoutes.length + : DEFAULT_ROUTES_SHOWN_AMOUNT, ); const content = ( <> -
- - -
- {Object.entries(trips).map( - ([routeShortName, { routeGtfsId, patterns }], i, arr) => - isMobile ? ( - - - - ) : ( - - ), - )} + {canceledRoutes.slice(0, showAmount).map((routeSummary, i) => { + const cancellationContainer = ( + + ); + + return isMobile ? ( + + {cancellationContainer} + + ) : ( + + {cancellationContainer} + + ); + })}
- {hasNext && ( + {showAmount < canceledRoutes.length && (
@@ -166,24 +100,25 @@ const CanceledTrips = ({ query, isMobile = false, ...props }) => {
-
- {isMobile ? content : {content}} -
- {!!detailsKey && ( - setDetailsKey(null)} - /> - )} +
{content}
); }; CanceledTrips.propTypes = { + canceledRoutes: PropTypes.arrayOf( + PropTypes.shape({ + cancellationCount: PropTypes.number.isRequired, + route: routeShape.isRequired, + patterns: PropTypes.arrayOf( + PropTypes.shape({ + cancellationCount: PropTypes.number.isRequired, + pattern: patternShape.isRequired, + }), + ).isRequired, + }), + ), mode: PropTypes.string.isRequired, - query: PropTypes.shape({}).isRequired, isMobile: PropTypes.bool, }; diff --git a/app/component/trafficnow/CanceledTripsContainer.js b/app/component/trafficnow/CanceledTripsContainer.js index 7c234853cd..85c368ac8c 100644 --- a/app/component/trafficnow/CanceledTripsContainer.js +++ b/app/component/trafficnow/CanceledTripsContainer.js @@ -1,20 +1,45 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useLazyLoadQuery } from 'react-relay/hooks'; +import { connectToStores } from 'fluxible-addons-react'; +import { DateTime } from 'luxon'; import CanceledTrips from './CanceledTrips'; import CanceledTripsForModeQuery from './queries/CanceledTripsForModeQuery'; +import { favouriteShape } from '../../util/shapes'; -const CanceledTripsContainer = ({ mode, isMobile }) => { - const queryData = useLazyLoadQuery(CanceledTripsForModeQuery, { - first: 20, +const CanceledTripsContainer = ({ mode, isMobile, favourites = [] }) => { + const { canceledTripsSummary } = useLazyLoadQuery(CanceledTripsForModeQuery, { mode: mode.toUpperCase(), + serviceDateRanges: [{ start: DateTime.now().toISODate(), end: null }], }); + const favRoutes = favourites.map(({ gtfsId }) => gtfsId); - return ; + return ( + + Number(favRoutes.includes(b.route.gtfsId)) - + Number(favRoutes.includes(a.route.gtfsId)), + )} + mode={mode} + isMobile={isMobile} + /> + ); }; CanceledTripsContainer.propTypes = { mode: PropTypes.string.isRequired, isMobile: PropTypes.bool, + favourites: PropTypes.arrayOf(favouriteShape), }; -export default CanceledTripsContainer; +const connectedComponent = connectToStores( + CanceledTripsContainer, + ['FavouriteStore'], + context => ({ + favourites: context.getStore('FavouriteStore').getFavourites(), + }), +); + +export default connectedComponent; diff --git a/app/component/trafficnow/CanceledTripsModal.js b/app/component/trafficnow/CanceledTripsModal.js deleted file mode 100644 index 5eb1e501c3..0000000000 --- a/app/component/trafficnow/CanceledTripsModal.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import Button from '@hsl-fi/button'; -import Modal from '@hsl-fi/modal'; -import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; -import { useRouter } from 'found'; -import { PREFIX_TIMETABLE, routePagePath } from '../../util/path'; -import Icon from '../Icon'; -import IconBackground from '../icon/IconBackground'; -import PatternWithCancellations from './components/PatternWithCancellations'; -import RouteBadgeGroup from './components/RouteBadgeGroup'; - -const CanceledTripsModal = ({ - mode, - detailsKey = undefined, - trips, - onClose, -}) => { - const intl = useIntl(); - const { router } = useRouter(); - - const handleRouteBadgeClick = url => e => { - e.preventDefault(); - router.push(url); - }; - - return ( - -
- - -
-
- {Object.entries(trips[detailsKey].patterns).map( - ([patternCode, pattern], i, arr) => ( - -
- -
- {i + 1 < arr.length && ( -
- )} - - ), - )} -
- - ); -}; - -CanceledTripsModal.propTypes = { - mode: PropTypes.string.isRequired, - detailsKey: PropTypes.string, - trips: PropTypes.shape({}).isRequired, - onClose: PropTypes.func.isRequired, - appElement: PropTypes.shape({}), -}; - -export default CanceledTripsModal; diff --git a/app/component/trafficnow/Disruptions.js b/app/component/trafficnow/Disruptions.js index 46c54b79f4..27759a7604 100644 --- a/app/component/trafficnow/Disruptions.js +++ b/app/component/trafficnow/Disruptions.js @@ -2,6 +2,7 @@ import React, { useMemo, useRef } from 'react'; import cx from 'classnames'; import { FormattedMessage } from 'react-intl'; import { useRouter } from 'found'; +import { DateTime } from 'luxon'; import { useLazyLoadQuery } from 'react-relay/hooks'; import { useConfigContext } from '../../configurations/ConfigContext'; import { TransportMode } from '../../constants'; @@ -15,8 +16,20 @@ import AlertsQuery from './queries/AlertsQuery'; import CanceledTripsOverviewQuery from './queries/CanceledTripsOverviewQuery'; import { buildDisruptionCards } from './utils'; import { TRAFFICNOW } from '../../util/path'; +import { splitGtfsId } from '../../util/gtfs'; -const CANCELED_TRIPS_OVERVIEW_QUERY_AMOUNT = 20; +// filters out routes with non relevant feedId:s and modes without any routes +export function getCanceledModes(cancelationsByMode, feedIds) { + return Object.entries(cancelationsByMode) + .map(([key, value]) => ({ + key, + ...value, + routes: value.routes.filter(({ route }) => + feedIds.includes(splitGtfsId(route.gtfsId).feedId), + ), + })) + .filter(({ routes }) => routes.length); +} export default function Disruptions() { const breakpoint = useBreakpoint(); @@ -25,7 +38,7 @@ export default function Disruptions() { const { router } = useRouter(); const { selectedFilters } = useFilterContext(); - const handleCardClick = id => { + const disruptionCardOnClick = id => { router.push(`/${TRAFFICNOW}/hairio/${id}`); }; @@ -45,7 +58,12 @@ export default function Disruptions() { ] : selectedFilters.vehicleModes.map(mode => mode.toUpperCase()); const canceledTripsVars = { - amount: CANCELED_TRIPS_OVERVIEW_QUERY_AMOUNT, + serviceDateRanges: [ + { + start: DateTime.now().toISODate(), + end: null, + }, + ], fetchBus: modesToFetch.includes(TransportMode.Bus), fetchTram: modesToFetch.includes(TransportMode.Tram), fetchRail: modesToFetch.includes(TransportMode.Rail), @@ -53,11 +71,15 @@ export default function Disruptions() { fetchFerry: modesToFetch.includes(TransportMode.Ferry), }; - const { bus, tram, rail, subway, ferry } = useLazyLoadQuery( + const cancelationsByMode = useLazyLoadQuery( CanceledTripsOverviewQuery, canceledTripsVars, ); + const canceledModes = useMemo( + () => getCanceledModes(cancelationsByMode, config.feedIds), + [cancelationsByMode, config.feedIds], + ); const disruptions = useMemo( () => filterAndSortAlerts(alerts, selectedFilters), [alerts, selectedFilters], @@ -70,21 +92,9 @@ export default function Disruptions() { const mobile = breakpoint !== 'large'; - const canceledModes = [ - bus && { key: 'bus', ...bus }, - tram && { key: 'tram', ...tram }, - rail && { key: 'rail', ...rail }, - subway && { key: 'subway', ...subway }, - ferry && { key: 'ferry', ...ferry }, - ].filter(Boolean); + const noResults = !disruptions.length && !canceledModes.length; - const noResults = - !disruptions.length && !canceledModes.some(mode => mode.totalCount > 0); - - const resultAmount = canceledModes.reduce( - (sum, mode) => (mode.totalCount > 0 ? sum + 1 : sum), - disruptionCards.length, - ); + const resultAmount = canceledModes.length + disruptionCards.length; return (

{msg}

}
- {canceledModes.map( - ({ key, totalCount, edges }) => - edges.length > 0 && ( - node)} - /> - ), - )} + {canceledModes.map(({ key, routes }) => ( + + ))} {disruptionCards.map(({ key, alert, mode }) => ( ))} diff --git a/app/component/trafficnow/README.md b/app/component/trafficnow/README.md index 6cc9c33f08..28ab7b43ac 100644 --- a/app/component/trafficnow/README.md +++ b/app/component/trafficnow/README.md @@ -25,11 +25,12 @@ The feature has two user flows: - alert cards second (`DisruptionCard`) ### Mode-specific cancellations flow -1. `CanceledTripsContainer.js` and `CanceledTrips.js` load paginated mode-specific data via Relay pagination fragment/query. +1. `CanceledTripsContainer.js` and `CanceledTrips.js` load paginated mode-specific data via canceledTripsSummary query 2. Trips are grouped for display by: - `routeShortName` - then `patternCode` -3. Details are shown in `CanceledTripsModal` when selected. +- canceled trips are shown under each mode, grouped per day. + By default 10 departure times are shown initially with the ability to expand the list when theres more available ## Data Sources and GraphQL @@ -40,7 +41,7 @@ The feature has two user flows: - entities (`Stop`, `Route`, `StopOnRoute`) ### Canceled trips overview -`queries/CanceledTripsOverviewQuery.js` requests one canceledTrips connection per mode: +`queries/CanceledTripsOverviewQuery.js` requests one canceledTripsSummary per mode: - BUS - TRAM - RAIL @@ -56,13 +57,14 @@ Each mode query is conditionally included with `@include` booleans from availabl ### Canceled trips cards (`CanceledTripCard.js`) Rules in overview: -1. One card per mode appears when that mode has `edges.length > 0`. +1. One card per mode appears when that mode has more than one cancellation. 2. A node is skipped if either is missing: - `trip.route.gtfsId` - `start.schedule.time.departure` 3. Card-internal grouping key is `trip.route.shortName`. 4. Departure times are formatted to `HH:mm`. 5. If exactly one grouped route exists, canceled departure times are displayed directly in the card. + - Pattern and date are also then showed inline with the departures. 6. If `totalCount > trips.length`, an ellipsis indicator is shown. ### Alert cards (`DisruptionCard.js`) @@ -140,11 +142,5 @@ Important behavior: ## TODO - Move `CanceledTripsContainer` under `FilterContextProvider` and have the selected filters affect CanceledTrips as well - Added filtering based on mode selection, more filters could be added when the api supports them -- Add a reasonable unit test suite -- Change CanceledTrips view to render on route basis when OTP endpoint supports such response - - Currently the response contains cancellations on cancellation basis which results in bad UX if all cancellations don't fit in the initial 20 node quota. - Change `Disruptions` view to render paginated results when OTP endpoint supports pagination - Blocked until API updates -- Define whether and how to present `canceledTrips` that are in future (i.e. tomorrow and beyond) - - Show cancelations on the current day as ongoing, future cancelations as upcoming - - Needs to wait until api updates diff --git a/app/component/trafficnow/RouteBadges.js b/app/component/trafficnow/RouteBadges.js index 73dac45169..6f604b8a52 100644 --- a/app/component/trafficnow/RouteBadges.js +++ b/app/component/trafficnow/RouteBadges.js @@ -8,6 +8,7 @@ import { AlertEntityType } from '../../constants'; import { groupEntitiesByMode } from './utils'; import { useFilterContext } from './filters/FiltersContext'; import RouteBadgeGroup from './components/RouteBadgeGroup'; +import EntityBadge from './components/EntityBadge'; export default function RouteBadges({ entities: rawEntities, @@ -55,6 +56,7 @@ export default function RouteBadges({ return ( ({ id, @@ -66,15 +68,14 @@ export default function RouteBadges({ highlightedGtfsId={selectedFilters.entity?.gtfsId} renderSuffix={ hiddenCount > 0 ? ( - - {`+${hiddenCount}`} - + /> ) : null } /> diff --git a/app/component/trafficnow/TrafficNow.js b/app/component/trafficnow/TrafficNow.js index 6f2c628dc6..6c048a9c16 100644 --- a/app/component/trafficnow/TrafficNow.js +++ b/app/component/trafficnow/TrafficNow.js @@ -63,7 +63,6 @@ const TrafficNow = () => { )} ( -
- {departures.map(({ tripId, departureTime }) => ( - - {departureTime} - - ))} -
-); +const DEPARTURE_LIMIT = 10; + +const CanceledDepartures = ({ + patterns: patternRefs, + inline = false, + departureLimit = DEPARTURE_LIMIT, + mode, +}) => { + const patterns = useFragment(CanceledDeparturesFragment, patternRefs); + const { formatMessage } = useIntl(); + const [expandedDates, setExpandedDates] = useState([]); + + const patternsWithVisibleDeparturesByDate = patterns + .map(pattern => { + const visibleCanceledTrips = inline + ? pattern.canceledTrips.slice(0, departureLimit) + : pattern.canceledTrips; + + return { + ...pattern, + hiddenDepartureCount: inline + ? Math.max(pattern.canceledTrips.length - departureLimit, 0) + : 0, + canceledTripsByDate: Object.groupBy( + visibleCanceledTrips, + ({ serviceDate }) => serviceDate, + ), + }; + }) + .filter(pattern => !inline || pattern.canceledTrips.length); + + return ( +
+ {patternsWithVisibleDeparturesByDate.map(pattern => ( + + {(patterns.length > 1 || inline) && ( + + )} + {Object.entries(pattern.canceledTripsByDate).map( + ([serviceDate, canceledTrips]) => ( +
+ {serviceDate !== DateTime.now().toISODate() && ( +
+ +
+ {serviceDate === + DateTime.now().plus({ days: 1 }).toISODate() + ? formatMessage({ id: 'tomorrow' }) + : DateTime.fromISO(serviceDate).toFormat('d.L.')} +
+
+ )} +
+ {canceledTrips + .filter( + (_, i) => + inline || + i < departureLimit || + expandedDates.includes(serviceDate), + ) + .map(({ trip }) => ( + + + {DateTime.fromISO(serviceDate) + .plus(trip.stoptimes[0].scheduledDeparture * 1000) + .toFormat(' HH:mm ')} + + + ))} +
+ {canceledTrips.length > departureLimit && + !expandedDates.includes(serviceDate) && + !inline && ( + + )} +
+ ), + )} + {inline && pattern.hiddenDepartureCount > 0 && ( + + + +{pattern.hiddenDepartureCount} + + + )} +
+ ))} +
+ ); +}; CanceledDepartures.propTypes = { - departures: PropTypes.arrayOf( + patterns: PropTypes.arrayOf( PropTypes.shape({ - tripId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - .isRequired, - departureTime: PropTypes.string.isRequired, - }), + code: PropTypes.string.isRequired, + stops: PropTypes.arrayOf(stopShape).isRequired, + canceledTrips: PropTypes.arrayOf( + PropTypes.shape({ + trip: PropTypes.shape({ + gtfsId: PropTypes.string.isRequired, + stoptimes: PropTypes.arrayOf( + PropTypes.shape({ + scheduledDeparture: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + }).isRequired, + }).isRequired, + ), + }).isRequired, ).isRequired, + departureLimit: PropTypes.number, + inline: PropTypes.bool, + mode: PropTypes.string, }; export default CanceledDepartures; diff --git a/app/component/trafficnow/components/CancellationContainer.js b/app/component/trafficnow/components/CancellationContainer.js index 1c57b6851c..df4765a9bc 100644 --- a/app/component/trafficnow/components/CancellationContainer.js +++ b/app/component/trafficnow/components/CancellationContainer.js @@ -1,22 +1,17 @@ import React from 'react'; -import Button from '@hsl-fi/button'; import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; import FavouriteRouteContainer from '../../routepage/FavouriteRouteContainer'; import { PREFIX_TIMETABLE, routePagePath } from '../../../util/path'; -import Icon from '../../Icon'; import PatternWithCancellations from './PatternWithCancellations'; import RouteBadgeGroup from './RouteBadgeGroup'; +import DisruptionBadge from '../DisruptionBadge'; +import { patternShape, routeShape } from '../../../util/shapes'; -const CancellationContainer = ({ - item, - mode, - isMobile, - colors, - onShowDetailsClick, -}) => { - const { routeShortName, routeGtfsId, patterns, index, total } = item; - const intl = useIntl(); +const CancellationContainer = ({ routeSummary, mode }) => { + const { + route: { shortName, gtfsId }, + patterns, + } = routeSummary; return (
@@ -27,68 +22,47 @@ const CancellationContainer = ({ headsignGroupClassName={mode} routes={[ { - id: routeShortName, - name: routeShortName, - url: routePagePath(routeGtfsId, PREFIX_TIMETABLE), - gtfsId: routeGtfsId, + id: shortName, + name: shortName, + url: routePagePath(gtfsId, PREFIX_TIMETABLE), + gtfsId, }, ]} /> +
- +
- {Object.entries(patterns).map(([patternCode, pattern]) => ( - + {patterns.map(({ pattern }) => ( + ))}
- {isMobile && ( - - )}
- - {!isMobile && ( -