Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8538763
update schema
hjvuor Jul 15, 2026
83452b7
refactor cancellations to use new canceledTripsSummary api
hjvuor Jul 15, 2026
4e6e05f
refactor CanceledTripCard unit test
hjvuor Jul 16, 2026
b351e03
show cancelled departuretimes on summary card in case of a single trip
hjvuor Jul 16, 2026
9811857
fix duplicate key
hjvuor Jul 16, 2026
7ebec6a
add feedId filtering to cancelations
hjvuor Jul 17, 2026
a4749dd
Merge branch 'v3' into AB#637
hjvuor Jul 17, 2026
f08dafe
cancelation card render hidden routes count instead of three dots
hjvuor Jul 17, 2026
c314b4b
add serviceDateRanges to framgent spread
hjvuor Jul 17, 2026
6880a43
add favourite indicator to route badge
hjvuor Jul 31, 2026
4cb0756
implement new design utilizing canceledTripsSummary
hjvuor Aug 5, 2026
3933239
patch tests
hjvuor Aug 5, 2026
998c57b
mobile style fixes and another fixed test
hjvuor Aug 5, 2026
dd9ba86
implement show all departures button
hjvuor Aug 5, 2026
d8ac839
add test for CanceledDepartures
hjvuor Aug 5, 2026
a733aae
fix test assertion
hjvuor Aug 5, 2026
7c223f5
Merge branch 'v3' into AB#637
hjvuor Aug 6, 2026
2e21a82
functionality and styling for inline departures on CanceledTripCard
hjvuor Aug 6, 2026
723b5ed
fix date badge padding
hjvuor Aug 6, 2026
e859a46
sort routes with cancellations by favourites
hjvuor Aug 6, 2026
5892d31
show pattern on cards even when theres only one
hjvuor Aug 7, 2026
7c9a05b
remove unused modal and update readme
hjvuor Aug 7, 2026
6f6c351
fix unique key issue
hjvuor Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 75 additions & 62 deletions app/component/trafficnow/CanceledTripCard.js
Original file line number Diff line number Diff line change
@@ -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 (
<Card
Expand All @@ -63,7 +42,6 @@ const CanceledTripCard = ({ mode, totalCount, trips, isMobile = false }) => {
<DisruptionBadge showIcon variant="WARNING" label="NO_SERVICE" />
{!isMobile && (
<>
{' '}
<div className="separator vertical" />
<DisruptionStatus
active
Expand All @@ -85,32 +63,48 @@ const CanceledTripCard = ({ mode, totalCount, trips, isMobile = false }) => {
<RouteBadgeGroup
mode={mode}
stopPropagation
routes={Object.entries(groupedTrips).map(
([shortName, { routeGtfsId, trips: groupedRouteTrips }]) => ({
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 ? (
<CanceledDepartures
departures={groupedRouteTrips.map(
({ tripId, departureTime }) => ({
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 ? (
<span style={{ backgroundColor: '#F2F5F7' }}>
<Icon img="icon_three-dots" width={1.3} height={1.3} />
</span>
routes.length > trafficNowMaxRoutesPerCard ? (
<EntityBadge
className="more-routes"
entity={{
name: `+${routes.length - trafficNowMaxRoutesPerCard}`,
}}
mode={mode}
ariaLabel={intl.formatMessage(
{ id: 'traffic-now_more-routes' },
{ count: routes.length - trafficNowMaxRoutesPerCard },
)}
/>
) : null
}
/>
Expand All @@ -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 };
Loading
Loading