-
}}
- testID="moreDetailsButton"
- />
+
+
+ {displayTripPattern.legs.map((leg, index) => (
+
+ ))}
+
+
+
+ ,
+ }}
+ testID="moreDetailsButton"
+ />
+
)}
diff --git a/src/page-modules/assistant/trip/trip-pattern/price/index.tsx b/src/page-modules/assistant/trip/trip-pattern/price/index.tsx
index e869b5d8c..65e2e1dfe 100644
--- a/src/page-modules/assistant/trip/trip-pattern/price/index.tsx
+++ b/src/page-modules/assistant/trip/trip-pattern/price/index.tsx
@@ -18,7 +18,6 @@ type NotFoundBehaviour = 'show-text' | 'hide-text';
type PriceProps = {
tripPattern: ExtendedTripPatternWithDetailsType;
- inView: boolean;
size?: 'small' | 'regular';
behaviour?: {
ifFound?: FoundBehaviour;
@@ -28,7 +27,6 @@ type PriceProps = {
export function Price({
tripPattern,
- inView,
size = 'regular',
behaviour,
}: PriceProps) {
@@ -40,9 +38,8 @@ export function Price({
const { ifFound = 'hide-icon', ifNotFound = 'hide-text' } = behaviour ?? {};
- const shouldFetch =
+ const enableFetch =
isEnabled &&
- inView &&
!disableBoatCombinationTripPatterns(
tripPattern,
!!disableBoatComboPriceSearch,
@@ -55,7 +52,7 @@ export function Price({
} = useTripPatternPrice(
new Date(tripPattern.legs[0].serviceDate),
tripPattern.legs,
- shouldFetch,
+ enableFetch,
);
if (!isEnabled) {
diff --git a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/__tests__/trip-pattern-header.test.tsx b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/__tests__/trip-pattern-header.test.tsx
index 9f266e047..5fe3e8025 100644
--- a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/__tests__/trip-pattern-header.test.tsx
+++ b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/__tests__/trip-pattern-header.test.tsx
@@ -2,11 +2,7 @@ import { cleanup, render, screen } from '@testing-library/react';
import mockRouter from 'next-router-mock';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
-import {
- getQuayOrPlaceName,
- getStartModeAndPlaceText,
- TripPatternHeader,
-} from '..';
+import { getQuayOrPlaceName, TripPatternHeader } from '..';
import {
tripPatternFixture,
tripPatternWithDetailsFixture,
@@ -40,11 +36,11 @@ const customRender = (
};
describe('trip pattern header', function () {
- it('should render trip pattern header', async () => {
+ it('should render trip pattern header with duration and ended status', async () => {
render(
);
- expect(screen.getByText('Buss fra From 1')).toBeInTheDocument();
- expect(screen.getByText('1 time')).toBeInTheDocument();
+ expect(screen.getByTestId('resultDuration')).toBeInTheDocument();
+ expect(screen.getByText('Reisen er ferdig')).toBeInTheDocument();
});
it('should render trip pattern header in english', async () => {
@@ -60,8 +56,7 @@ describe('trip pattern header', function () {
},
);
- expect(screen.getByText('Bus from From 1')).toBeInTheDocument();
- expect(screen.getByText('1 hour')).toBeInTheDocument();
+ expect(screen.getByText('Trip ended')).toBeInTheDocument();
});
it('should render trip pattern header in nynorsk', async () => {
@@ -77,8 +72,7 @@ describe('trip pattern header', function () {
},
);
- expect(screen.getByText('Buss frå From 1')).toBeInTheDocument();
- expect(screen.getByText('1 time')).toBeInTheDocument();
+ expect(screen.getByText('Reisa er ferdig')).toBeInTheDocument();
});
it('should get quay name from quay', () => {
@@ -186,27 +180,4 @@ describe('trip pattern header', function () {
expect(screen.getByText('FromPlaceName')).toBeInTheDocument();
});
-
- it('should get start mode and start place from trip', () => {
- const Test = function () {
- const { t } = useTranslation();
-
- const startModeAndPlaceText = getStartModeAndPlaceText(
- tripPatternWithDetailsFixture,
- t,
- );
- return
{startModeAndPlaceText}
;
- };
-
- customRender(
, {
- providerProps: {
- initialCookies: {
- darkmode: false,
- language: 'no',
- },
- },
- });
-
- expect(screen.getByText('Buss fra From 1')).toBeInTheDocument();
- });
});
diff --git a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/index.tsx b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/index.tsx
index 6591aeb6f..758b1bb4a 100644
--- a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/index.tsx
+++ b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/index.tsx
@@ -1,22 +1,98 @@
import style from './trip-pattern-header.module.css';
import { Typo } from '@atb/components/typography';
import { useTranslation, PageText, TranslateFunction } from '@atb/translations';
-import { formatTripDuration } from '@atb/utils/date';
-import { flatMap } from 'lodash';
-import { RailReplacementBusMessage } from './rail-replacement-bus';
-import { SituationOrNoticeIcon } from '@atb/modules/situations-and-notices';
-import { isSubModeBoat } from '@atb/modules/transport-mode';
-import { ColorIcon } from '@atb/components/icon';
+import {
+ formatToClock,
+ formatTripDuration,
+ isInPast,
+ secondsBetween,
+} from '@atb/utils/date';
import { Assistant } from '@atb/translations/pages';
-import { getNoticesForLeg } from '@atb/page-modules/assistant/utils.ts';
import { ExtendedTripPatternWithDetailsType } from '@atb/page-modules/assistant';
import { QuayFragment } from '@atb/page-modules/assistant/journey-gql/trip-with-details.generated.ts';
+import { StatusText, StatusType } from './status-text';
+import { getBookingStatus } from '@atb/modules/flexible';
+
+const DEFAULT_THRESHOLD_AIMED_EXPECTED_IN_SECONDS = 60;
type TripPatternHeaderProps = {
tripPattern: ExtendedTripPatternWithDetailsType;
isCancelled?: boolean;
};
+type StatusConfig = {
+ statusType: StatusType;
+ text: string;
+};
+
+function getStatusConfig(
+ tripPattern: ExtendedTripPatternWithDetailsType,
+ isCancelled: boolean,
+ t: ReturnType
['t'],
+): StatusConfig | undefined {
+ if (isCancelled) {
+ return {
+ statusType: 'error',
+ text: t(PageText.Assistant.trip.tripPattern.statusText.cancelled),
+ };
+ }
+
+ if (tripPattern.status === 'impossible') {
+ return {
+ statusType: 'error',
+ text: t(PageText.Assistant.trip.tripPattern.statusText.impossible),
+ };
+ }
+
+ const endIsInPast = isInPast(tripPattern.expectedEndTime);
+ const startIsInPast = isInPast(tripPattern.expectedStartTime);
+
+ if (endIsInPast) {
+ return {
+ statusType: 'error',
+ text: t(PageText.Assistant.trip.tripPattern.statusText.ended),
+ };
+ }
+
+ if (startIsInPast) {
+ return {
+ statusType: 'info',
+ text: t(PageText.Assistant.trip.tripPattern.statusText.started),
+ };
+ }
+
+ const bookingLegs = tripPattern.legs.filter((leg) => leg.bookingArrangements);
+ if (bookingLegs.length > 0) {
+ const anyDeadlineExceeded = bookingLegs.some(
+ (leg) =>
+ getBookingStatus(leg.bookingArrangements!, leg.aimedStartTime) ===
+ 'late',
+ );
+ if (anyDeadlineExceeded) {
+ return {
+ statusType: 'interactive',
+ text: t(
+ PageText.Assistant.trip.tripPattern.statusText
+ .bookingDeadlineExceeded,
+ ),
+ };
+ }
+ return {
+ statusType: 'info',
+ text: t(PageText.Assistant.trip.tripPattern.statusText.requiresBooking),
+ };
+ }
+
+ if (tripPattern.status === 'stale') {
+ return {
+ statusType: 'info',
+ text: t(PageText.Assistant.trip.tripPattern.statusText.stale),
+ };
+ }
+
+ return undefined;
+}
+
export function TripPatternHeader({
tripPattern,
isCancelled = false,
@@ -29,103 +105,77 @@ export function TripPatternHeader({
language,
);
- const startModeAndPlaceText = getStartModeAndPlaceText(tripPattern, t);
+ const expectedStartTimeLabel = formatToClock(
+ tripPattern.expectedStartTime,
+ language,
+ 'floor',
+ );
+ const expectedEndTimeLabel = formatToClock(
+ tripPattern.expectedEndTime,
+ language,
+ 'ceil',
+ );
+
+ const aimedStartTime =
+ tripPattern.aimedStartTime ?? tripPattern.legs[0]?.aimedStartTime;
+ const aimedEndTime =
+ tripPattern.aimedEndTime ??
+ tripPattern.legs[tripPattern.legs.length - 1]?.aimedEndTime;
+
+ const aimedStartTimeLabel = formatToClock(aimedStartTime, language, 'floor');
+ const aimedEndTimeLabel = formatToClock(aimedEndTime, language, 'ceil');
+
+ const startTimeDiffers =
+ Math.abs(secondsBetween(aimedStartTime, tripPattern.expectedStartTime)) >
+ DEFAULT_THRESHOLD_AIMED_EXPECTED_IN_SECONDS;
+ const endTimeDiffers =
+ Math.abs(secondsBetween(aimedEndTime, tripPattern.expectedEndTime)) >
+ DEFAULT_THRESHOLD_AIMED_EXPECTED_IN_SECONDS;
+ const showAimedTime = startTimeDiffers || endTimeDiffers;
+
+ const statusConfig = getStatusConfig(tripPattern, isCancelled, t);
return (
- {isCancelled ? (
-
- ) : (
- leg.situations)}
- notices={tripPattern.legs.flatMap(getNoticesForLeg)}
- cancellation={isCancelled}
- iconSize="large"
- />
- )}
-
-
- {startModeAndPlaceText}
- {isCancelled &&
- ` (${t(
- PageText.Assistant.trip.tripPattern.isCancelled.title,
- ).toUpperCase()})`}
-
-
- {duration}
-
+
+ {statusConfig && (
+
+ )}
+
+
+ {`${expectedStartTimeLabel} - ${expectedEndTimeLabel}`}
+
+
+ {showAimedTime && (
+
+ {t(PageText.Assistant.trip.tripPattern.originalTime)}{' '}
+ {aimedStartTimeLabel} - {aimedEndTimeLabel}
+
+ )}
+
+
+
+ {duration}
+
+
);
}
-export function getStartModeAndPlaceText(
- tripPattern: ExtendedTripPatternWithDetailsType,
- t: TranslateFunction,
-): string {
- let startLeg = tripPattern.legs[0];
- let tmpStartName = startLeg.fromPlace.name;
-
- if (tripPattern.legs[0].mode === 'foot' && tripPattern.legs[1]) {
- startLeg = tripPattern.legs[1];
- tmpStartName = getQuayOrPlaceName(
- t,
- startLeg.fromPlace.quay,
- startLeg.fromPlace.name,
- );
- } else if (tripPattern.legs[0].mode !== 'foot') {
- tmpStartName = getQuayOrPlaceName(
- t,
- startLeg.fromPlace.quay,
- startLeg.fromPlace.name,
- );
- }
-
- const startName: string =
- tmpStartName ??
- t(PageText.Assistant.trip.tripPattern.travelFrom.unknownPlace);
-
- switch (startLeg.mode) {
- case 'bus':
- case 'coach':
- return t(PageText.Assistant.trip.tripPattern.travelFrom.bus(startName));
- case 'tram':
- return t(PageText.Assistant.trip.tripPattern.travelFrom.tram(startName));
- case 'metro':
- return t(PageText.Assistant.trip.tripPattern.travelFrom.metro(startName));
- case 'rail':
- return t(PageText.Assistant.trip.tripPattern.travelFrom.rail(startName));
- case 'water':
- if (
- startLeg.transportSubmode &&
- isSubModeBoat(startLeg.transportSubmode)
- ) {
- return t(
- PageText.Assistant.trip.tripPattern.travelFrom.boat(startName),
- );
- } else {
- return t(
- PageText.Assistant.trip.tripPattern.travelFrom.ferry(startName),
- );
- }
- case 'air':
- return t(PageText.Assistant.trip.tripPattern.travelFrom.air(startName));
- case 'bicycle':
- return t(
- PageText.Assistant.trip.tripPattern.travelFrom.bicycle(startName),
- );
- case 'foot':
- return t(PageText.Assistant.trip.tripPattern.travelFrom.foot(startName));
- default:
- return t(
- PageText.Assistant.trip.tripPattern.travelFrom.unknown(startName),
- );
- }
-}
-
export function getQuayOrPlaceName(
t: TranslateFunction,
quay?: QuayFragment,
diff --git a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/status-text.module.css b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/status-text.module.css
new file mode 100644
index 000000000..c0173646c
--- /dev/null
+++ b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/status-text.module.css
@@ -0,0 +1,17 @@
+.container {
+ display: flex;
+ align-items: center;
+ gap: token('spacing.xSmall');
+}
+
+.error {
+ color: token('color.foreground.emphasis.error');
+}
+
+.info {
+ color: token('color.foreground.emphasis.info');
+}
+
+.interactive {
+ color: token('color.foreground.emphasis.interactive');
+}
diff --git a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/status-text.tsx b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/status-text.tsx
new file mode 100644
index 000000000..4dd45d137
--- /dev/null
+++ b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/status-text.tsx
@@ -0,0 +1,41 @@
+import { ColorIcon } from '@atb/components/icon';
+import { Typo } from '@atb/components/typography';
+import style from './status-text.module.css';
+
+export type StatusType = 'error' | 'info' | 'interactive';
+
+type StatusTextProps = {
+ statusType: StatusType;
+ text: string;
+ showIcon?: boolean;
+};
+
+export function StatusText({
+ statusType,
+ text,
+ showIcon = false,
+}: StatusTextProps) {
+ return (
+
+ {showIcon && (
+
+ )}
+
+ {text}
+
+
+ );
+}
+
+function statusTypeToColorIcon(
+ statusType: StatusType,
+): 'status/Error' | 'status/Info' | 'status/Warning' {
+ switch (statusType) {
+ case 'error':
+ return 'status/Error';
+ case 'interactive':
+ return 'status/Warning';
+ default:
+ return 'status/Info';
+ }
+}
diff --git a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/trip-pattern-header.module.css b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/trip-pattern-header.module.css
index 936cc22fc..0b9ce0db4 100644
--- a/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/trip-pattern-header.module.css
+++ b/src/page-modules/assistant/trip/trip-pattern/trip-pattern-header/trip-pattern-header.module.css
@@ -1,19 +1,40 @@
.header {
- display: flex;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ grid-template-areas: 'times duration';
gap: token('spacing.small');
+ align-items: start;
+ padding: token('spacing.medium') token('spacing.medium') 0;
+}
+
+.header__timesArea {
+ grid-area: times;
+ display: flex;
+ flex-direction: column;
+ gap: token('spacing.xSmall');
+ min-width: 0;
+}
+
+.header__timesRow {
+ display: flex;
align-items: center;
- padding: token('spacing.small') token('spacing.medium');
+ gap: token('spacing.small');
}
-@media (max-width: 650px) {
- .header {
- align-items: center;
- }
+.header__aimedTime {
+ color: token('color.foreground.dynamic.secondary');
}
.header__duration {
+ grid-area: duration;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: token('spacing.xSmall');
+}
+
+.header__durationText {
color: token('color.foreground.dynamic.secondary');
- margin-left: auto;
}
.situationIcon {
diff --git a/src/page-modules/assistant/trip/trip-pattern/trip-pattern.module.css b/src/page-modules/assistant/trip/trip-pattern/trip-pattern.module.css
index cb9a4c810..f96857629 100644
--- a/src/page-modules/assistant/trip/trip-pattern/trip-pattern.module.css
+++ b/src/page-modules/assistant/trip/trip-pattern/trip-pattern.module.css
@@ -21,160 +21,53 @@
border-radius: token('border.radius.regular');
}
-.header {
- display: flex;
- justify-content: space-between;
- padding: token('spacing.small') token('spacing.medium');
-}
-
-.header__duration {
- color: token('color.foreground.dynamic.secondary');
-}
-
.details {
display: flex;
flex-direction: column;
- padding: 0 token('spacing.medium');
-}
-
-.legs {
- display: flex;
- align-items: center;
- padding: token('spacing.small') token('spacing.medium');
-}
-
-.legs__expandedLegsContainer {
- display: flex;
- flex-grow: 1;
- overflow: hidden;
- max-width: 100%;
- margin-right: token('spacing.xSmall');
-}
-
-.legs__expandedLegs {
- display: flex;
- align-items: flex-start;
- position: relative;
- margin-right: token('spacing.xSmall');
-}
-
-.legs__leg {
- display: flex;
- flex-direction: column;
- gap: token('spacing.small');
-}
-
-.legs__legLineContainer,
-.legs__lastLegLineContainer {
- /* The height of the icon in a leg. */
- --inner-height: 20px;
- display: flex;
- align-items: center;
- gap: 0.125rem;
- padding: 0 token('spacing.xSmall');
- /* Set the container to the same height as leg boxes to vertically center the line correctly. */
- height: calc(var(--inner-height) + token('spacing.small') * 2);
-}
-.legs__legLine,
-.legs__lastLegLine {
- width: 0.3125rem;
- height: 0.125rem;
- background-color: token('color.interactive.1.disabled.background');
- border-radius: token('border.radius.circle');
-}
-
-.legs__lastLegLineContainer {
- padding: 0;
-}
-
-.legs__lastLegLineContainer,
-.legs__lastLegLine {
- flex: 1 1;
-}
-
-.legs__leg__walkIcon {
- background-color: token('color.background.neutral.2.background');
- border-radius: token('border.radius.small');
- display: grid;
- place-content: center;
- padding: token('spacing.small');
+ border-top: token('border.width.slim') solid
+ token('color.background.neutral.1.background');
+ background-color: token('color.background.neutral.0.background');
}
-.legs__collapsedLegs {
- background-color: token('color.background.neutral.2.background');
- border-radius: token('border.radius.small');
- padding: token('spacing.small');
+.detailsGrid {
display: grid;
- place-content: center;
+ grid-template-columns: 3fr 2fr;
+ gap: token('spacing.xLarge');
+ padding: token('spacing.large') token('spacing.medium')
+ token('spacing.medium');
}
-.legs__collapsedLegs > span {
- display: grid;
- place-content: center;
- height: 20px;
- width: 20px;
+@media (max-width: 650px) {
+ .detailsGrid {
+ grid-template-columns: 1fr;
+ }
}
-.legs__lastLeg {
- margin-left: auto;
+.detailsAside {
display: flex;
- gap: token('spacing.small');
flex-direction: column;
- justify-content: center;
- align-items: center;
-}
-
-.legs__lastLeg__icon {
- background-color: token('color.background.neutral.2.background');
- border-radius: token('border.radius.small');
- display: grid;
- place-content: center;
- padding: token('spacing.small');
-}
-
-.footer {
- border-top: 1px solid token('color.background.neutral.1.background');
- display: grid;
- grid-template-areas: 'price button';
- width: 100%;
justify-content: space-between;
- align-items: center;
- padding: token('spacing.small') token('spacing.medium');
- gap: token('spacing.small');
+ gap: token('spacing.medium');
}
-.footer__details {
+.legs {
display: flex;
- gap: token('spacing.small');
align-items: center;
- color: token('color.interactive.0.default.foreground.primary');
-}
-
-.footer__link {
- text-decoration: none;
- padding: token('spacing.xSmall') token('spacing.medium');
- border-radius: token('border.radius.circle');
- background-color: token('color.interactive.0.default.background');
-}
-
-.accordionBody {
padding: token('spacing.medium');
+ gap: token('spacing.small');
+ min-width: 0;
}
-.accordionFooter {
- display: flex;
- justify-content: flex-end;
- padding: token('spacing.small') token('spacing.medium');
+.legs__legsAndLine {
+ flex: 1;
+ --overflow-container-gap: token('spacing.xSmall');
}
-.timeStartContainer {
+.accordionBody {
display: flex;
- direction: row;
- gap: token('spacing.xSmall');
-}
-
-.outdatet {
- color: token('color.foreground.dynamic.secondary');
+ flex-direction: column;
+ gap: token('spacing.medium');
+ background-color: token('color.background.neutral.0.background');
}
.chevron {
@@ -186,28 +79,29 @@
}
.seeMoreButton {
- grid-area: button;
+ flex-shrink: 0;
+ align-self: flex-end;
+}
+.seeMoreButton {
+ padding-right: 0;
+ color: token('color.foreground.emphasis.interactive');
+}
+.seeMoreButton:hover,
+.seeMoreButton:active {
+ color: token('color.foreground.emphasis.interactive');
}
.goToDetailsButton {
- border: 2px solid token('color.foreground.dynamic.secondary');
- align-self: flex-end;
+ border: token('border.width.medium') solid
+ token('color.foreground.emphasis.interactive');
+ text-align: center;
}
-
+.goToDetailsButton:hover,
.goToDetailsButton:active {
- border: 2px solid token('color.interactive.0.outline.background');
+ color: token('color.foreground.emphasis.interactive');
}
-.info__container {
- display: flex;
- flex-direction: row;
- gap: token('spacing.small');
- align-items: center;
-}
-
-@media (max-width: 650px) {
- .info__container {
- flex-direction: column;
- align-items: flex-start;
- }
+.goToDetailsButton:active {
+ border: token('border.width.medium') solid
+ token('color.interactive.0.outline.background');
}
diff --git a/src/page-modules/assistant/trip/trip-pattern/utils.ts b/src/page-modules/assistant/trip/trip-pattern/utils.ts
index acfe2ef16..bb9faca72 100644
--- a/src/page-modules/assistant/trip/trip-pattern/utils.ts
+++ b/src/page-modules/assistant/trip/trip-pattern/utils.ts
@@ -5,10 +5,14 @@ import {
} from '@atb/utils/date';
import { Language, TranslateFunction, PageText } from '@atb/translations';
import dictionary from '@atb/translations/dictionary';
+import { humanizeDistance } from '@atb/utils/distance';
import { screenReaderPause } from '@atb/components/typography/utils';
import { transportModeToTranslatedString } from '@atb/modules/transport-mode';
import { getTimeRepresentationType } from '@atb/modules/time-representation';
-import { ExtendedTripPatternWithDetailsType } from '@atb/page-modules/assistant';
+import {
+ ExtendedLegType,
+ ExtendedTripPatternWithDetailsType,
+} from '@atb/page-modules/assistant';
import { getQuayOrPlaceName } from '@atb/page-modules/assistant/trip/trip-pattern/trip-pattern-header';
import { LegWithDetailsFragment } from '../../journey-gql/trip-with-details.generated';
@@ -20,13 +24,7 @@ export const tripSummary = (
listPosition: number,
isCancelled: boolean,
) => {
- const distance = Math.round(tripPattern.legs[0].distance);
- let humanizedDistance;
- if (distance >= 1000) {
- humanizedDistance = `${distance / 1000} ${t(dictionary.distance.km)}`;
- } else {
- humanizedDistance = `${distance} ${t(dictionary.distance.m)}`;
- }
+ const humanizedDistance = humanizeDistance(tripPattern.legs[0].distance, t);
let startText = '';
@@ -239,14 +237,11 @@ function isSignificantFootLegWalkOrWaitTime(
export function getFilteredLegsByWalkOrWaitTime(
tripPattern: ExtendedTripPatternWithDetailsType,
-) {
- if (!!tripPattern?.legs?.length) {
- return tripPattern.legs.filter((leg, i) =>
- isSignificantFootLegWalkOrWaitTime(leg, tripPattern.legs[i + 1]),
- );
- } else {
- return [];
- }
+): ExtendedLegType[] {
+ const legs = tripPattern.legs;
+ return legs.filter((leg, i) =>
+ isSignificantFootLegWalkOrWaitTime(leg, legs[i + 1]),
+ );
}
function isLegFlexibleTransport(leg: LegWithDetailsFragment): boolean {
diff --git a/src/page-modules/assistant/trip/trip.module.css b/src/page-modules/assistant/trip/trip.module.css
index 82838fd27..eb2259fde 100644
--- a/src/page-modules/assistant/trip/trip.module.css
+++ b/src/page-modules/assistant/trip/trip.module.css
@@ -7,7 +7,7 @@
.tripResults {
display: flex;
flex-direction: column;
- gap: token('spacing.large');
+ gap: token('spacing.medium');
}
.nonTransitResult {
diff --git a/src/page-modules/assistant/types.ts b/src/page-modules/assistant/types.ts
index 2cd141437..b3bae2ee7 100644
--- a/src/page-modules/assistant/types.ts
+++ b/src/page-modules/assistant/types.ts
@@ -142,19 +142,16 @@ export type ExtendedLegType = LegWithDetailsFragment & {
*/
export type TripPatternStatus = 'valid' | 'impossible' | 'stale';
-export type ExtendedTripPatternWithDetailsType =
- TripPatternWithDetailsFragment & {
- compressedQuery: string;
- legs: ExtendedLegType[];
- status?: TripPatternStatus;
- /**
- * Trip-level aimed times, derived from the legs by refreshSingleTrip.
- * Non-transit legs have no real schedule, so these are anchored to the
- * nearest transit legs.
- */
- aimedStartTime?: string;
- aimedEndTime?: string;
- };
+export type ExtendedTripPatternWithDetailsType = Omit<
+ TripPatternWithDetailsFragment,
+ 'legs'
+> & {
+ compressedQuery: string;
+ legs: ExtendedLegType[];
+ status?: TripPatternStatus;
+ aimedStartTime?: string;
+ aimedEndTime?: string;
+};
export type TripWithDetailsType = TripsWithDetailsQuery & {
trip: {
tripPatterns: ExtendedTripPatternWithDetailsType[];
diff --git a/src/translations/pages/assistant.ts b/src/translations/pages/assistant.ts
index d98ebdead..ff9cb748f 100644
--- a/src/translations/pages/assistant.ts
+++ b/src/translations/pages/assistant.ts
@@ -98,6 +98,7 @@ const AssistantInternal = {
'Reiseforslag funne',
),
tripPattern: {
+ originalTime: _('Opprinnelig', 'Original', 'Opphavleg'),
travelFrom: {
bus: (place: string) =>
_(`Buss fra ${place}`, `Bus from ${place}`, `Buss frå ${place}`),
@@ -203,6 +204,27 @@ const AssistantInternal = {
'Denne reisa er innstilt',
),
},
+ statusText: {
+ cancelled: _('Innstilt', 'Cancelled', 'Innstilt'),
+ impossible: _('Ikke mulig', 'Not possible', 'Ikkje mogleg'),
+ ended: _('Reisen er ferdig', 'Trip ended', 'Reisa er ferdig'),
+ started: _('Reisen har startet', 'Trip started', 'Reisa har starta'),
+ bookingDeadlineExceeded: _(
+ 'Bestillingsfristen er passert',
+ 'Booking deadline exceeded',
+ 'Bestillingsfristen er passert',
+ ),
+ requiresBooking: _(
+ 'Krever bestilling',
+ 'Requires booking',
+ 'Krev bestilling',
+ ),
+ stale: _(
+ 'Resultatene kan være utdaterte',
+ 'Results may be outdated',
+ 'Resultata kan vera utdaterte',
+ ),
+ },
},
fetchMore: _(
'Last inn flere reiseforslag',
@@ -438,8 +460,6 @@ const AssistantInternal = {
'Total walking distance',
'Total gangavstand',
),
- walkDistanceValue: (distance: string) =>
- _(`${distance} m`, `${distance} m`, `${distance} m`),
priceLabel: (traveller: string) =>
_(
`Pris (1 ${traveller})`,
@@ -451,6 +471,12 @@ const AssistantInternal = {
walk: {
label: (duration: string) =>
_(`Gå i ${duration}`, `Walk for ${duration}`, `Gå i ${duration}`),
+ labelWithDistance: (duration: string, distance: string) =>
+ _(
+ `Gå ${distance} (${duration})`,
+ `Walk ${distance} (${duration})`,
+ `Gå ${distance} (${duration})`,
+ ),
},
shortWalk: _(
`Gå i mindre enn ett minutt`,
diff --git a/src/utils/distance.ts b/src/utils/distance.ts
new file mode 100644
index 000000000..b84bce4a4
--- /dev/null
+++ b/src/utils/distance.ts
@@ -0,0 +1,13 @@
+import dictionary from '@atb/translations/dictionary';
+import { TranslateFunction } from '@atb/translations';
+
+export function humanizeDistance(
+ distanceInMeters: number,
+ t: TranslateFunction,
+): string {
+ const rounded = Math.round(distanceInMeters);
+ if (rounded >= 1000) {
+ return `${(rounded / 1000).toFixed(1)} ${t(dictionary.distance.km)}`;
+ }
+ return `${rounded} ${t(dictionary.distance.m)}`;
+}
diff --git a/src/utils/use-isomorphic-layout-effect.ts b/src/utils/use-isomorphic-layout-effect.ts
new file mode 100644
index 000000000..f3946ec46
--- /dev/null
+++ b/src/utils/use-isomorphic-layout-effect.ts
@@ -0,0 +1,9 @@
+import { useEffect, useLayoutEffect } from 'react';
+
+// React logs a warning when useLayoutEffect is registered during SSR. The
+// /assistant page can server-render (SWR-seeded from a cached
+// fallback in getServerSideProps), so any layout effect inside it would fire
+// that warning on cache hits. This shim aliases useLayoutEffect on the client
+// and useEffect on the server to silence it. Standard ecosystem pattern.
+export const useIsomorphicLayoutEffect =
+ typeof window !== 'undefined' ? useLayoutEffect : useEffect;