diff --git a/app/component/Card.js b/app/component/Card.js index 0c0f382ff8..207a318980 100644 --- a/app/component/Card.js +++ b/app/component/Card.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { forwardRef } from 'react'; import cx from 'classnames'; -const Card = forwardRef(({ className, children, ...rest }, ref) => { +const Card = forwardRef(({ className = undefined, children, ...rest }, ref) => { return (
{children} @@ -17,6 +17,4 @@ Card.propTypes = { children: PropTypes.node.isRequired, }; -Card.defaultProps = { className: undefined }; - export default Card; diff --git a/app/component/Icon.js b/app/component/Icon.js index 174721a9b8..246218432e 100644 --- a/app/component/Icon.js +++ b/app/component/Icon.js @@ -30,6 +30,11 @@ const Icon = ({ viewBox={!omitViewBox ? viewBox : null} className={cx('icon', className)} aria-label={ariaLabel} + transform={ + background?.props?.shape === 'stopsign' + ? 'translate(0, -3.33)' + : undefined + } > {background} { @@ -10,23 +10,23 @@ const IconBackground = ({ shape, color }) => { } return ( <> - {shape === 'stopsign' && ( )} + {shape === 'square' && ( { - setActiveAlertId(id); - }; - - const { alerts } = useLazyLoadQuery(AlertsQuery, { - feedIds, - }); - - const filteredAlerts = useMemo( - () => filterAndSortAlerts(alerts, selectedFilters), - [alerts, selectedFilters], - ); - - const desktop = breakpoint === 'large'; - - return ( -
- {filteredAlerts.length === 0 ? ( - - ) : ( - <> - - {msg =>

{msg}

} -
-
- {filteredAlerts.map(a => ( - - ))} -
- - )} -
- ); -} - -Alerts.propTypes = {}; -Alerts.defaultProps = {}; diff --git a/app/component/trafficnow/CanceledTripCard.js b/app/component/trafficnow/CanceledTripCard.js new file mode 100644 index 0000000000..4670b51a40 --- /dev/null +++ b/app/component/trafficnow/CanceledTripCard.js @@ -0,0 +1,119 @@ +import React from 'react'; +import { useRouter } from 'found'; +import { DateTime } from 'luxon'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { useConfigContext } from '../../configurations/ConfigContext'; +import { PREFIX_TIMETABLE, routePagePath } from '../../util/path'; +import Card from '../Card'; +import Icon from '../Icon'; +import CancelledDepartures from './components/CancelledDepartures'; +import RouteBadgeGroup from './components/RouteBadgeGroup'; +import DisruptionBadge from './DisruptionBadge'; + +const CanceledTripCard = ({ mode, totalCount, trips }) => { + const { router } = useRouter(); + const { colors } = useConfigContext(); + + 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; + + return ( + +
+ + +
+
+ ({ + id: shortName, + name: shortName, + url: routePagePath(routeGtfsId, PREFIX_TIMETABLE), + gtfsId: routeGtfsId, + trips: groupedRouteTrips, + }), + )} + renderRouteSuffix={({ trips: groupedRouteTrips }) => + isSingleRoute ? ( + ({ + tripId, + departureTime, + }), + )} + /> + ) : null + } + renderSuffix={ + totalCount > trips.length ? ( + + + + ) : null + } + /> +
+
+ + +
+
+ ); +}; + +CanceledTripCard.propTypes = { + mode: PropTypes.string.isRequired, + totalCount: PropTypes.number.isRequired, + trips: PropTypes.arrayOf(PropTypes.shape({})).isRequired, +}; + +export default CanceledTripCard; diff --git a/app/component/trafficnow/CanceledTrips.js b/app/component/trafficnow/CanceledTrips.js new file mode 100644 index 0000000000..954103ff55 --- /dev/null +++ b/app/component/trafficnow/CanceledTrips.js @@ -0,0 +1,193 @@ +import React, { useMemo, 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 } from 'react-intl'; +import { usePaginationFragment } from 'react-relay/hooks'; +import { useConfigContext } from '../../configurations/ConfigContext'; +import { useTranslationsContext } from '../../util/useTranslationsContext'; +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 CanceledTripsPaginationFragment from './queries/CanceledTripsPaginationFragment'; + +const CANCELED_TRIPS_QUERY_AMOUNT = 20; + +const CanceledTrips = ({ query, isMobile = false, ...props }) => { + const { colors } = useConfigContext(); + const intl = useTranslationsContext(); + 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 + ].cancelledDepartures.push( + DateTime.fromISO(start?.schedule.time.departure).toFormat('HH:mm'), + ); + } else { + routeGroups[routeShortName].patterns[patternCode] = { + start, + end, + trip, + cancelledDepartures: [ + DateTime.fromISO(start?.schedule.time.departure).toFormat( + 'HH:mm', + ), + ], + }; + } + + return routeGroups; + }, {}), + [allEdges], + ); + + const content = ( + <> +
+ +
+ + +
+
+
+ {Object.entries(trips).map( + ([routeShortName, { routeGtfsId, patterns }], i, arr) => + isMobile ? ( + + + + ) : ( + + ), + )} +
+
+
+ + + {hasNext && ( +
+
+ + ); + + return ( + <> +
+ + + + {isMobile &&
} + +
+ +
+ {isMobile ? content : {content}} +
+ {!!detailsKey && ( + setDetailsKey(null)} + /> + )} + + ); +}; + +CanceledTrips.propTypes = { + mode: PropTypes.string.isRequired, + query: PropTypes.shape({}).isRequired, + isMobile: PropTypes.bool, +}; + +export default CanceledTrips; diff --git a/app/component/trafficnow/CanceledTripsContainer.js b/app/component/trafficnow/CanceledTripsContainer.js new file mode 100644 index 0000000000..7c234853cd --- /dev/null +++ b/app/component/trafficnow/CanceledTripsContainer.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useLazyLoadQuery } from 'react-relay/hooks'; +import CanceledTrips from './CanceledTrips'; +import CanceledTripsForModeQuery from './queries/CanceledTripsForModeQuery'; + +const CanceledTripsContainer = ({ mode, isMobile }) => { + const queryData = useLazyLoadQuery(CanceledTripsForModeQuery, { + first: 20, + mode: mode.toUpperCase(), + }); + + return ; +}; +CanceledTripsContainer.propTypes = { + mode: PropTypes.string.isRequired, + isMobile: PropTypes.bool, +}; + +export default CanceledTripsContainer; diff --git a/app/component/trafficnow/CanceledTripsModal.js b/app/component/trafficnow/CanceledTripsModal.js new file mode 100644 index 0000000000..d20932275a --- /dev/null +++ b/app/component/trafficnow/CanceledTripsModal.js @@ -0,0 +1,120 @@ +import React from 'react'; +import Button from '@hsl-fi/button'; +import Modal from '@hsl-fi/modal'; +import PropTypes from 'prop-types'; +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 { 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/Badge.js b/app/component/trafficnow/DisruptionBadge.js similarity index 84% rename from app/component/Badge.js rename to app/component/trafficnow/DisruptionBadge.js index ff768fae8d..79f97a5795 100644 --- a/app/component/Badge.js +++ b/app/component/trafficnow/DisruptionBadge.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import cx from 'classnames'; import { FormattedMessage } from 'react-intl'; import capitalize from 'lodash/capitalize'; -import Icon from './Icon'; -import { AlertSeverityLevelType } from '../constants'; +import Icon from '../Icon'; +import { AlertSeverityLevelType } from '../../constants'; const DISRUPTION_BADGE_PREFIX = 'disruption-badge-'; @@ -38,11 +38,11 @@ const getIcon = variant => { } }; -export default function Badge({ - label, - showIcon, - variant, - className, +export default function DisruptionBadge({ + label = undefined, + showIcon = false, + variant = 'info', + className = undefined, ...rest }) { return ( @@ -59,15 +59,9 @@ export default function Badge({ ); } -Badge.propTypes = { +DisruptionBadge.propTypes = { label: PropTypes.string, showIcon: PropTypes.bool, variant: variantValidator, className: PropTypes.string, }; -Badge.defaultProps = { - label: undefined, - variant: 'info', - showIcon: false, - className: undefined, -}; diff --git a/app/component/trafficnow/DisruptionCard.js b/app/component/trafficnow/DisruptionCard.js index c7d00822f7..b461035367 100644 --- a/app/component/trafficnow/DisruptionCard.js +++ b/app/component/trafficnow/DisruptionCard.js @@ -1,16 +1,16 @@ -import React, { useRef } from 'react'; -import cx from 'classnames'; -import { FormattedMessage } from 'react-intl'; +import React from 'react'; import Button from '@hsl-fi/button'; +import cx from 'classnames'; import PropTypes from 'prop-types'; -import Card from '../Card'; +import { FormattedMessage } from 'react-intl'; +import { useConfigContext } from '../../configurations/ConfigContext'; +import { AlertSeverityLevelType } from '../../constants'; import { alertShape } from '../../util/shapes'; +import { getFormattedTimeDate } from '../../util/timeUtils'; +import Card from '../Card'; +import DisruptionBadge from './DisruptionBadge'; import Icon from '../Icon'; -import { useConfigContext } from '../../configurations/ConfigContext'; -import Badge from '../Badge'; import RouteBadges from './RouteBadges'; -import { getFormattedTimeDate } from '../../util/timeUtils'; -import { AlertSeverityLevelType } from '../../constants'; const DATE_FORMAT = 'd.L.yyyy'; @@ -19,8 +19,9 @@ const handleExtraInfoClick = url => e => { window.location.href = url; }; -export default function DisruptionCard({ alert, isOpen, onClick }) { +export default function DisruptionCard({ alert, isOpen, onClick = () => {} }) { const { + id, alertSeverityLevel, alertEffect, alertHeaderText, @@ -31,7 +32,6 @@ export default function DisruptionCard({ alert, isOpen, onClick }) { alertUrl, } = alert; const { colors } = useConfigContext(); - const cardRef = useRef(null); const now = Date.now(); const isValid = @@ -48,42 +48,39 @@ export default function DisruptionCard({ alert, isOpen, onClick }) { return ( { - cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); - onClick(isOpen ? undefined : alert.id); + onClick(isOpen ? undefined : id); }} > -
- +
+ -
- {entities && ( - - )} + + {entities && }

{alertHeaderText}

{alertDescriptionText}

-
-
-
+
+
+
{isValid ? ( <> @@ -105,7 +102,7 @@ export default function DisruptionCard({ alert, isOpen, onClick }) { )}
{alertUrl && isOpen && ( -
+
- )} + {mode ? ( }> - + - + ) : ( + + {!mobile ? ( +
+ +
+ ) : ( +
+ setShowFiltersModal(false)} + /> +
+ )} + }> + + +
+ )}
); -} - -TrafficNow.propTypes = {}; +}; -TrafficNow.defaultProps = {}; +export default TrafficNow; diff --git a/app/component/trafficnow/Header.js b/app/component/trafficnow/TrafficNowHeader.js similarity index 95% rename from app/component/trafficnow/Header.js rename to app/component/trafficnow/TrafficNowHeader.js index 8bc088c0ed..eeb81a7494 100644 --- a/app/component/trafficnow/Header.js +++ b/app/component/trafficnow/TrafficNowHeader.js @@ -54,13 +54,13 @@ const AdditionalDescription = () => { ); }; -export default function Header() { +export default function TrafficNowHeader() { const breakpoint = useBreakpoint(); const { CONFIG } = useConfigContext(); const desktop = breakpoint === 'large'; return ( -
{CONFIG === 'hsl' && }

-
+ ); } -Header.propTypes = {}; -Header.defaultProps = {}; +TrafficNowHeader.propTypes = {}; diff --git a/app/component/trafficnow/TrafficNowLink.js b/app/component/trafficnow/TrafficNowLink.js index b8d57c21a2..66066cdfb5 100644 --- a/app/component/trafficnow/TrafficNowLink.js +++ b/app/component/trafficnow/TrafficNowLink.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import Icon from '../Icon'; -const TrafficNowLink = ({ handleClick, href }) => { +const TrafficNowLink = ({ handleClick, href = undefined }) => { return (
@@ -36,11 +36,7 @@ const TrafficNowLink = ({ handleClick, href }) => { TrafficNowLink.propTypes = { handleClick: PropTypes.func.isRequired, - href: PropTypes.string, -}; - -TrafficNowLink.defaultProps = { - href: undefined, + href: PropTypes.string.isRequired, }; export default TrafficNowLink; diff --git a/app/component/trafficnow/components/CancellationContainer.js b/app/component/trafficnow/components/CancellationContainer.js new file mode 100644 index 0000000000..3066a6011a --- /dev/null +++ b/app/component/trafficnow/components/CancellationContainer.js @@ -0,0 +1,94 @@ +import React from 'react'; +import Button from '@hsl-fi/button'; +import PropTypes from 'prop-types'; +import FavouriteRouteContainer from '../../routepage/FavouriteRouteContainer'; +import { PREFIX_TIMETABLE, routePagePath } from '../../../util/path'; +import { useTranslationsContext } from '../../../util/useTranslationsContext'; +import Icon from '../../Icon'; +import PatternWithCancellations from './PatternWithCancellations'; +import RouteBadgeGroup from './RouteBadgeGroup'; + +const CancellationContainer = ({ + item, + mode, + isMobile, + colors, + onShowDetailsClick, +}) => { + const { routeShortName, routeGtfsId, patterns, index, total } = item; + const intl = useTranslationsContext(); + + return ( +
+
+
+ +
+ +
+
+
+ {Object.entries(patterns).map(([patternCode, pattern]) => ( + + + + ))} +
+ {isMobile && ( + + )} +
+ + {!isMobile && ( +