mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2026-04-02 21:12:46 +02:00
320 lines
9 KiB
JavaScript
320 lines
9 KiB
JavaScript
import PropTypes from 'prop-types';
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
import { useFragment } from 'react-relay';
|
|
import { matchShape } from 'found';
|
|
import { DateTime } from 'luxon';
|
|
import cx from 'classnames';
|
|
import { SchedulePatternFragment } from './queries/SchedulePatternFragment';
|
|
import { ScheduleRouteFragment } from './queries/ScheduleRouteFragment';
|
|
import { ScheduleFirstDeparturesFragment } from './queries/ScheduleFirstDeparturesFragment';
|
|
import ScheduleHeader from './ScheduleHeader';
|
|
import ScheduleTripList from './ScheduleTripList';
|
|
import ScheduleConstantOperation from './ScheduleConstantOperation';
|
|
import SecondaryButton from '../../SecondaryButton';
|
|
import { DATE_FORMAT } from '../../../constants';
|
|
import { addAnalyticsEvent } from '../../../util/analyticsUtils';
|
|
import { useBreakpoint } from '../../../util/withBreakpoint';
|
|
import DateSelectGrouped from '../../stop/DateSelectGrouped';
|
|
import RouteControlPanel from '../RouteControlPanel';
|
|
import ScrollableWrapper from '../../ScrollableWrapper';
|
|
import { useConfigContext } from '../../../configurations/ConfigContext';
|
|
import { useTranslationsContext } from '../../../util/useTranslationsContext';
|
|
import { getTripsList } from './scheduleTripsUtils';
|
|
import { routeShape, patternShape } from '../../../util/shapes';
|
|
import { calculateRedirectDecision } from './scheduleParamUtils';
|
|
import { buildAvailableDates } from './scheduleDataUtils';
|
|
|
|
/**
|
|
* Open a route timetable PDF in a new window.
|
|
* @param {SyntheticEvent} e
|
|
* @param {{ href: string }} routePDFUrl
|
|
*/
|
|
const openRoutePDF = (e, routePDFUrl) => {
|
|
e.stopPropagation();
|
|
window.open(routePDFUrl.href);
|
|
};
|
|
|
|
/**
|
|
* Trigger browser print for the current timetable view.
|
|
* @param {SyntheticEvent} e
|
|
*/
|
|
const printRouteTimetable = e => {
|
|
e.stopPropagation();
|
|
window.print();
|
|
addAnalyticsEvent({
|
|
category: 'Route',
|
|
action: 'PrintTimetable',
|
|
name: null,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* ScheduleContainer is the main component for the route schedule page.
|
|
* It handles data fetching and state management for the schedule,
|
|
* and renders the header, trip list, date selector, and action buttons.
|
|
*/
|
|
const ScheduleContainer = ({
|
|
pattern: patternRef,
|
|
route: routeRef,
|
|
firstDepartures: firstDeparturesRef,
|
|
match,
|
|
}) => {
|
|
const breakpoint = useBreakpoint();
|
|
const pattern = useFragment(SchedulePatternFragment, patternRef);
|
|
const route = useFragment(ScheduleRouteFragment, routeRef);
|
|
const firstDepartures = useFragment(
|
|
ScheduleFirstDeparturesFragment,
|
|
firstDeparturesRef,
|
|
);
|
|
|
|
const intl = useTranslationsContext();
|
|
const config = useConfigContext();
|
|
|
|
const [from, setFrom] = useState(0);
|
|
const [to, setTo] = useState(Math.max((pattern.stops.length || 1) - 1, 0));
|
|
|
|
const { query } = match.location;
|
|
const serviceDayString = query.serviceDay;
|
|
const wantedDay = useMemo(
|
|
() =>
|
|
serviceDayString
|
|
? DateTime.fromFormat(serviceDayString, DATE_FORMAT)
|
|
: DateTime.local(),
|
|
[serviceDayString],
|
|
);
|
|
|
|
const availableDates = useMemo(
|
|
() => buildAvailableDates(firstDepartures),
|
|
[firstDepartures],
|
|
);
|
|
|
|
const patternCode = pattern.code;
|
|
const patternWithTrips = route.patterns?.find(p => p.code === patternCode);
|
|
|
|
const tripsResult = useMemo(
|
|
() =>
|
|
getTripsList({
|
|
patternWithTrips,
|
|
intl,
|
|
wantedDay,
|
|
}),
|
|
[patternWithTrips, wantedDay],
|
|
);
|
|
|
|
const routeId = route.gtfsId;
|
|
const { constantOperationRoutes } = config;
|
|
const { locale } = intl;
|
|
|
|
const constantOperationInfo =
|
|
routeId && constantOperationRoutes?.[routeId]
|
|
? constantOperationRoutes[routeId][locale]
|
|
: null;
|
|
|
|
const redirectDecision = calculateRedirectDecision({
|
|
wantedDay,
|
|
patternCode,
|
|
routeId,
|
|
availableDates,
|
|
hasTrips: !!tripsResult.trips,
|
|
});
|
|
|
|
const {
|
|
shouldRedirect,
|
|
redirectPath,
|
|
query: redirectQuery,
|
|
} = redirectDecision;
|
|
|
|
useEffect(() => {
|
|
if (shouldRedirect) {
|
|
const basePath = redirectPath
|
|
? { ...match.location, pathname: redirectPath }
|
|
: match.location;
|
|
|
|
match.router.replace({
|
|
...basePath,
|
|
query: { ...basePath.query, ...redirectQuery },
|
|
});
|
|
}
|
|
}, [shouldRedirect, redirectPath, redirectQuery?.serviceDay, match.location]);
|
|
|
|
useEffect(() => {
|
|
if (patternCode) {
|
|
setFrom(0);
|
|
setTo(pattern.stops.length - 1);
|
|
}
|
|
}, [patternCode, pattern.stops.length]);
|
|
|
|
// Handler for timetable origin stop selection
|
|
const onFromSelectChange = selectFrom => {
|
|
const fromValue = Number(selectFrom);
|
|
setFrom(fromValue);
|
|
setTo(prevTo => {
|
|
if (prevTo > fromValue) {
|
|
return prevTo;
|
|
}
|
|
return Math.min(fromValue + 1, pattern.stops.length - 1);
|
|
});
|
|
addAnalyticsEvent({
|
|
category: 'Route',
|
|
action: 'ChangeTimetableStartPoint',
|
|
name: null,
|
|
});
|
|
};
|
|
|
|
// Handler for timetable destination stop selection
|
|
const onToSelectChange = selectTo => {
|
|
setTo(Number(selectTo));
|
|
addAnalyticsEvent({
|
|
category: 'Route',
|
|
action: 'ChangeTimetableEndPoint',
|
|
name: null,
|
|
});
|
|
};
|
|
|
|
const changeDate = newServiceDay => {
|
|
const { location } = match;
|
|
addAnalyticsEvent({
|
|
category: 'Route',
|
|
action: 'ChangeTimetableDay',
|
|
name: null,
|
|
});
|
|
const newPath = {
|
|
...location,
|
|
query: {
|
|
...location.query,
|
|
serviceDay: newServiceDay,
|
|
},
|
|
};
|
|
match.router.replace(newPath);
|
|
};
|
|
|
|
const formattedServiceDate = wantedDay.toFormat(DATE_FORMAT);
|
|
const agencyId = routeId?.split(':')?.[0];
|
|
const hasRouteTimetableUrl = !!(
|
|
agencyId &&
|
|
formattedServiceDate &&
|
|
config.timetables?.[agencyId] &&
|
|
config.URL.ROUTE_TIMETABLES?.[agencyId]
|
|
);
|
|
|
|
const handlePrintPDF = e => {
|
|
if (!hasRouteTimetableUrl) {
|
|
return;
|
|
}
|
|
const routeTimetableHandler = config.timetables[agencyId];
|
|
const baseUrl = config.URL.ROUTE_TIMETABLES[agencyId];
|
|
const routeTimetableUrl = routeTimetableHandler.routeTimetableUrlResolver(
|
|
baseUrl,
|
|
route,
|
|
formattedServiceDate,
|
|
locale,
|
|
);
|
|
openRoutePDF(e, routeTimetableUrl);
|
|
addAnalyticsEvent({
|
|
category: 'Route',
|
|
action: 'PrintWeeklyTimetable',
|
|
name: null,
|
|
});
|
|
};
|
|
|
|
if (constantOperationInfo) {
|
|
return (
|
|
<ScheduleConstantOperation
|
|
constantOperationInfo={constantOperationInfo}
|
|
match={match}
|
|
route={route}
|
|
breakpoint={breakpoint}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const showTrips = tripsResult.trips;
|
|
|
|
return (
|
|
<>
|
|
<ScrollableWrapper
|
|
className={cx('route-schedule-container', {
|
|
mobile: breakpoint !== 'large',
|
|
})}
|
|
>
|
|
{route.patterns && (
|
|
<RouteControlPanel
|
|
match={match}
|
|
route={route}
|
|
breakpoint={breakpoint}
|
|
noInitialServiceDay
|
|
/>
|
|
)}
|
|
<div className="route-schedule-grouped-date-select">
|
|
<DateSelectGrouped
|
|
dateFormat={DATE_FORMAT}
|
|
selectedDay={wantedDay}
|
|
dates={availableDates}
|
|
onDateChange={changeDate}
|
|
/>
|
|
</div>
|
|
{!shouldRedirect && tripsResult.noTripsMessage}
|
|
{pattern && (
|
|
<div
|
|
className={cx('route-schedule-list-wrapper', {
|
|
'bp-large': breakpoint === 'large',
|
|
})}
|
|
aria-live="polite"
|
|
>
|
|
<ScheduleHeader
|
|
stops={pattern.stops}
|
|
from={from}
|
|
to={to}
|
|
onFromSelectChange={onFromSelectChange}
|
|
onToSelectChange={onToSelectChange}
|
|
/>
|
|
<div
|
|
className="route-schedule-list momentum-scroll"
|
|
role="list"
|
|
aria-live="off"
|
|
>
|
|
{Array.isArray(showTrips) && (
|
|
<ScheduleTripList trips={showTrips} fromIdx={from} toIdx={to} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</ScrollableWrapper>
|
|
{breakpoint === 'large' && <div className="after-scrollable-area" />}
|
|
<div className="route-page-action-bar">
|
|
<div className="print-button-container">
|
|
{hasRouteTimetableUrl && (
|
|
<SecondaryButton
|
|
ariaLabel="print-timetable"
|
|
buttonName="print-timetable"
|
|
buttonClickAction={handlePrintPDF}
|
|
buttonIcon="icon_print"
|
|
smallSize
|
|
/>
|
|
)}
|
|
<SecondaryButton
|
|
ariaLabel="print"
|
|
buttonName="print"
|
|
buttonClickAction={printRouteTimetable}
|
|
buttonIcon="icon_print"
|
|
smallSize
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
ScheduleContainer.propTypes = {
|
|
pattern: patternShape.isRequired,
|
|
route: routeShape.isRequired,
|
|
// firstDepartures is a Relay fragment with dynamic structure
|
|
// eslint-disable-next-line react/forbid-prop-types
|
|
firstDepartures: PropTypes.object.isRequired,
|
|
match: matchShape.isRequired,
|
|
};
|
|
|
|
ScheduleContainer.displayName = 'ScheduleContainer';
|
|
|
|
export { ScheduleContainer as Component };
|
|
export default ScheduleContainer;
|