diff --git a/app/component/itinerary/navigator/NaviCard.js b/app/component/itinerary/navigator/NaviCard.js
index 0059c20feb..7518423229 100644
--- a/app/component/itinerary/navigator/NaviCard.js
+++ b/app/component/itinerary/navigator/NaviCard.js
@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { isRental } from '../../../util/legUtils';
+import React, { useState } from 'react';
+import { isAnyLegPropertyIdentical, isRental } from '../../../util/legUtils';
import { getRouteMode } from '../../../util/modeUtils';
import { configShape, legShape } from '../../../util/shapes';
import Icon from '../../Icon';
import NaviCardExtension from './NaviCardExtension';
import NaviInstructions from './NaviInstructions';
import { LEGTYPE } from './NaviUtils';
+import usePrevious from './hooks/usePrevious';
const iconMap = {
BICYCLE: 'icon-icon_cyclist',
@@ -27,95 +27,96 @@ const iconMap = {
};
export default function NaviCard(
- {
- leg,
- nextLeg,
- legType,
- cardExpanded,
- startTime,
- time,
- position,
- tailLength,
- },
+ { leg, nextLeg, legType, time, position, tailLength },
{ config },
) {
- let mainCardContent;
- if (legType === LEGTYPE.PENDING) {
- mainCardContent = (
-
- );
- } else if (legType === LEGTYPE.END) {
- mainCardContent = ;
- } else if (!leg && !nextLeg) {
+ const [cardExpanded, setCardExpanded] = useState(false);
+ const { isEqual: legChanged } = usePrevious(leg, (prev, current) =>
+ isAnyLegPropertyIdentical(prev, current, ['legId', 'mode']),
+ );
+ const handleClick = () => {
+ setCardExpanded(!cardExpanded);
+ };
+
+ if (legChanged) {
+ setCardExpanded(false);
+ }
+
+ if (
+ (!leg && !nextLeg) ||
+ legType === LEGTYPE.PENDING ||
+ legType === LEGTYPE.END
+ ) {
return null;
- } else {
- let iconColor = 'currentColor';
- let iconName;
- let instructions = '';
- if (legType === LEGTYPE.TRANSIT) {
- const m = getRouteMode(leg.route, config);
- iconColor = config.colors.iconColors[`mode-${m}`] || leg.route.color;
- iconName = iconMap[m.toUpperCase()];
+ }
- instructions = `navileg-in-transit`;
- } else if (
- legType !== LEGTYPE.WAIT &&
- legType !== LEGTYPE.WAIT_IN_VEHICLE &&
- isRental(leg, nextLeg)
- ) {
- if (leg.mode === 'WALK' && nextLeg?.mode === 'SCOOTER') {
- instructions = `navileg-rent-scooter`;
- } else {
- instructions = 'rent-cycle-at';
- }
- iconName = iconMap[leg.mode];
- } else if (legType === LEGTYPE.MOVE) {
- instructions = `navileg-${leg?.mode.toLowerCase()}`;
- iconName = iconMap.WALK;
- } else if (legType === LEGTYPE.WAIT) {
- iconName = iconMap.WAIT;
- } else if (legType === LEGTYPE.WAIT_IN_VEHICLE) {
- iconName = iconMap.WAIT_IN_VEHICLE;
+ let iconColor = 'currentColor';
+ let iconName;
+ let instructions = '';
+
+ if (legType === LEGTYPE.TRANSIT) {
+ const m = getRouteMode(leg.route, config);
+ iconColor = config.colors.iconColors[`mode-${m}`] || leg.route.color;
+ iconName = iconMap[m.toUpperCase()];
+
+ instructions = `navileg-in-transit`;
+ } else if (
+ legType !== LEGTYPE.WAIT &&
+ legType !== LEGTYPE.WAIT_IN_VEHICLE &&
+ isRental(leg, nextLeg)
+ ) {
+ if (leg.mode === 'WALK' && nextLeg?.mode === 'SCOOTER') {
+ instructions = `navileg-rent-scooter`;
+ } else {
+ instructions = 'rent-cycle-at';
}
+ iconName = iconMap[leg.mode];
+ } else if (legType === LEGTYPE.MOVE) {
+ instructions = `navileg-${leg?.mode.toLowerCase()}`;
+ iconName = iconMap.WALK;
+ } else if (legType === LEGTYPE.WAIT) {
+ iconName = iconMap.WAIT;
+ } else if (legType === LEGTYPE.WAIT_IN_VEHICLE) {
+ iconName = iconMap.WAIT_IN_VEHICLE;
+ }
- mainCardContent = (
- <>
-
-
-
+
+
+ {cardExpanded && (
+
-
-
-
-
- >
- );
- }
- return (
-
-
{mainCardContent}
- {cardExpanded && (
-
- )}
-
+ )}
+
+
);
}
@@ -123,8 +124,6 @@ NaviCard.propTypes = {
leg: legShape,
nextLeg: legShape,
legType: PropTypes.string.isRequired,
- cardExpanded: PropTypes.bool,
- startTime: PropTypes.string,
time: PropTypes.number.isRequired,
position: PropTypes.shape({
lat: PropTypes.number,
@@ -133,10 +132,8 @@ NaviCard.propTypes = {
tailLength: PropTypes.number.isRequired,
};
NaviCard.defaultProps = {
- cardExpanded: false,
leg: undefined,
nextLeg: undefined,
- startTime: '',
position: undefined,
};
diff --git a/app/component/itinerary/navigator/NaviCardContainer.js b/app/component/itinerary/navigator/NaviCardContainer.js
index b57ccc3d94..1bd7fd5ee6 100644
--- a/app/component/itinerary/navigator/NaviCardContainer.js
+++ b/app/component/itinerary/navigator/NaviCardContainer.js
@@ -3,27 +3,33 @@ import { matchShape, routerShape } from 'found';
import PropTypes from 'prop-types';
import React, { useEffect, useRef, useState } from 'react';
import { intlShape } from 'react-intl';
-import { legTime, legTimeStr } from '../../../util/legUtils';
+import {
+ isAnyLegPropertyIdentical,
+ legTime,
+ legTimeStr,
+} from '../../../util/legUtils';
import { configShape, legShape } from '../../../util/shapes';
+import { getTopics, updateClient } from '../ItineraryPageUtils';
import NaviCard from './NaviCard';
import NaviStack from './NaviStack';
+import NaviStarter from './NaviStarter';
import {
+ DESTINATION_RADIUS,
getAdditionalMessages,
getItineraryAlerts,
getTransitLegState,
itinerarySearchPath,
LEGTYPE,
- DESTINATION_RADIUS,
} from './NaviUtils';
-import { updateClient, getTopics } from '../ItineraryPageUtils';
+import usePrevious from './hooks/usePrevious';
const COUNT_AT_LEG_END = 2; // update cycles within DESTINATION_RADIUS from leg.to
const TOPBAR_PADDING = 8; // pixels
const HIDE_TOPCARD_DURATION = 2000; // milliseconds
-function addMessages(incominMessages, newMessages) {
+function addMessages(incomingMessages, newMessages) {
newMessages.forEach(m => {
- incominMessages.set(m.id, m);
+ incomingMessages.set(m.id, m);
});
}
@@ -35,7 +41,7 @@ const getLegType = (
interlineWithPreviousLeg,
) => {
let legType;
- if (time < legTime(firstLeg.start)) {
+ if (!firstLeg.forceStart && time < legTime(firstLeg.start)) {
legType = LEGTYPE.PENDING;
} else if (leg) {
if (!leg.transitLeg) {
@@ -67,20 +73,22 @@ function NaviCardContainer(
lastLeg,
previousLeg,
isJourneyCompleted,
+ startItinerary,
},
context,
) {
- const [cardExpanded, setCardExpanded] = useState(false);
// All notifications including those user has dismissed.
const [messages, setMessages] = useState(new Map());
// notifications that are shown to the user.
const [activeMessages, setActiveMessages] = useState([]);
const [legChanging, setLegChanging] = useState(false);
- const legRef = useRef(currentLeg);
+ const { isEqual: legChanged } = usePrevious(currentLeg, (prev, current) =>
+ isAnyLegPropertyIdentical(prev, current, ['legId', 'mode']),
+ );
+ const { isEqual: forceStart } = usePrevious(firstLeg?.forceStart);
const focusRef = useRef(false);
// Destination counter. How long user has been at the destination. * 10 seconds
const legEndRef = useRef(0);
- const cardRef = useRef(null);
const { intl, config, match, router } = context;
const handleRemove = index => {
const msg = messages.get(activeMessages[index].id);
@@ -88,10 +96,6 @@ function NaviCardContainer(
setActiveMessages(activeMessages.filter((_, i) => i !== index));
};
- const handleClick = () => {
- setCardExpanded(!cardExpanded);
- };
-
// track only relevant vehicles for the journey.
const getNaviTopics = () =>
getTopics(
@@ -117,13 +121,6 @@ function NaviCardContainer(
useEffect(() => {
const incomingMessages = new Map();
- const legChanged = legRef.current?.legId
- ? legRef.current.legId !== currentLeg?.legId
- : legRef.current?.mode !== currentLeg?.mode;
- if (legChanged) {
- legRef.current = currentLeg;
- }
-
// Alerts for NaviStack
addMessages(
incomingMessages,
@@ -177,9 +174,8 @@ function NaviCardContainer(
]);
}
let timeoutId;
- if (legChanged) {
+ if (legChanged || forceStart) {
updateClient(getNaviTopics(), context);
- setCardExpanded(false);
setLegChanging(true);
timeoutId = setTimeout(() => {
setLegChanging(false);
@@ -232,7 +228,7 @@ function NaviCardContainer(
}
return () => clearTimeout(timeoutId);
- }, [time]);
+ }, [time, firstLeg]);
// LegChange fires animation, we need to keep the old data until card goes out of the view.
const l = legChanging ? previousLeg : currentLeg;
@@ -259,23 +255,23 @@ function NaviCardContainer(
className={`navi-card-container ${className}`}
style={{ top: containerTopPosition }}
>
-
+ )}
{activeMessages.length > 0 && (
)}
@@ -303,6 +299,7 @@ NaviCardContainer.propTypes = {
lastLeg: legShape,
previousLeg: legShape,
isJourneyCompleted: PropTypes.bool,
+ startItinerary: PropTypes.func,
};
NaviCardContainer.defaultProps = {
@@ -314,6 +311,7 @@ NaviCardContainer.defaultProps = {
lastLeg: undefined,
previousLeg: undefined,
isJourneyCompleted: false,
+ startItinerary: () => {},
};
NaviCardContainer.contextTypes = {
diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js
index 8fb260d5df..95d1eeb6cc 100644
--- a/app/component/itinerary/navigator/NaviContainer.js
+++ b/app/component/itinerary/navigator/NaviContainer.js
@@ -49,6 +49,7 @@ function NaviContainer(
previousLeg,
currentLeg,
nextLeg,
+ startItinerary,
} = useRealtimeLegs(relayEnvironment, legs, position);
useEffect(() => {
@@ -106,6 +107,7 @@ function NaviContainer(
lastLeg={lastLeg}
isJourneyCompleted={isJourneyCompleted}
previousLeg={previousLeg}
+ startItinerary={startItinerary}
/>
{isJourneyCompleted && isNavigatorIntroDismissed && (
{
+ const { logo } = useLogo(config.trafficLightGraphic);
+
+ const handleClick = () => startItinerary(Date.now());
+
+ return (
+
+
+ {logo ? (
+

+ ) : (
+
+ )}
+
+
{time}
+
+
+
+
+
+
+ );
+};
+
+NaviStarter.propTypes = {
+ time: PropTypes.string.isRequired,
+ startItinerary: PropTypes.func.isRequired,
+};
+
+NaviStarter.contextTypes = {
+ intl: intlShape.isRequired,
+ config: configShape.isRequired,
+};
+
+export default NaviStarter;
diff --git a/app/component/itinerary/navigator/hooks/usePrevious.js b/app/component/itinerary/navigator/hooks/usePrevious.js
new file mode 100644
index 0000000000..ceedd4612f
--- /dev/null
+++ b/app/component/itinerary/navigator/hooks/usePrevious.js
@@ -0,0 +1,27 @@
+import { useRef } from 'react';
+
+const usePrevious = (value, comparingFunc) => {
+ const ref = useRef({
+ value,
+ prev: null,
+ });
+
+ const current = ref.current.value;
+ let isEqual = false;
+
+ if (
+ comparingFunc
+ ? !comparingFunc(current, value)
+ : JSON.stringify(value) !== JSON.stringify(current)
+ ) {
+ ref.current = {
+ value,
+ prev: current,
+ };
+ isEqual = true;
+ }
+
+ return { previous: ref.current.prev, isEqual };
+};
+
+export default usePrevious;
diff --git a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js
index d9434b05f0..27b7ca4cdc 100644
--- a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js
+++ b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js
@@ -192,7 +192,7 @@ const useRealtimeLegs = (relayEnvironment, initialLegs, position) => {
// rtLegMap does not contain legs that have ended in the past as they've been filtered before updates are queried
const rtLegs = prev.realTimeLegs.map(l => {
const rtLeg =
- l.legId && rtLegMap[l.legId] ? { ...rtLegMap[l.legId] } : null;
+ l.legId && rtLegMap?.[l.legId] ? { ...rtLegMap?.[l.legId] } : null;
if (rtLeg) {
// If start is frozen, the property is deleted to prevent it from affecting any views
if (l.freezeStart) {
@@ -217,13 +217,43 @@ const useRealtimeLegs = (relayEnvironment, initialLegs, position) => {
// Freezes any leg.start|end in the past
rtLegs.forEach(l => {
- l.freezeStart = legTime(l.start) <= now;
- l.freezeEnd = legTime(l.end) <= now;
+ l.freezeStart = l.freezeStart || legTime(l.start) <= now;
+ l.freezeEnd = l.freezeEnd || legTime(l.end) <= now;
});
return { ...prev, time: now, realTimeLegs: rtLegs };
});
- }, [queryAndMapRealtimeLegs]);
+ }, [realTimeLegs, queryAndMapRealtimeLegs]);
+
+ const startItinerary = startTimeInMS => {
+ if (startTimeInMS < legTime(realTimeLegs[0].start)) {
+ setTimeAndRealTimeLegs(prev => {
+ const firstLeg = prev.realTimeLegs[0];
+
+ if (firstLeg.transitLeg) {
+ firstLeg.forceStart = true;
+ return {
+ ...prev,
+ time: startTimeInMS,
+ };
+ }
+ const adjustment = legTime(realTimeLegs[0].start) - startTimeInMS;
+
+ firstLeg.start.scheduledTime = epochToIso(startTimeInMS);
+ firstLeg.end.scheduledTime = epochToIso(
+ legTime(realTimeLegs[0].end) - adjustment,
+ );
+ firstLeg.freezeStart = true;
+ firstLeg.freezeEnd = true;
+
+ return {
+ ...prev,
+ time: startTimeInMS,
+ realTimeLegs: prev.realTimeLegs,
+ };
+ });
+ }
+ };
useEffect(() => {
fetchAndSetRealtimeLegs();
@@ -252,6 +282,7 @@ const useRealtimeLegs = (relayEnvironment, initialLegs, position) => {
previousLeg,
currentLeg,
nextLeg,
+ startItinerary,
};
};
diff --git a/app/component/itinerary/navigator/navigator.scss b/app/component/itinerary/navigator/navigator.scss
index 5daa226a33..74ce4217d6 100644
--- a/app/component/itinerary/navigator/navigator.scss
+++ b/app/component/itinerary/navigator/navigator.scss
@@ -1,10 +1,9 @@
-$fixed-width-padding: 16px;
-
.navi-start-container {
padding: 0 10px;
+ width: 100%;
button {
- width: 100%;
+ width: inherit;
display: flex;
flex-direction: row;
align-items: center;
@@ -55,7 +54,14 @@ $fixed-width-padding: 16px;
.navi-card-container {
position: fixed;
- width: 100vw;
+ width: 100%;
+ padding: 0 var(--space-s);
+
+ svg.mode {
+ width: 32px;
+ height: 32px;
+ color: black;
+ }
&.slide-out {
animation: slideUpToTop 3s ease-out forwards;
@@ -139,8 +145,7 @@ $fixed-width-padding: 16px;
}
.navi-top-card {
- margin: 0 var(--space-s) 5px var(--space-s);
- border-radius: 8px;
+ border-radius: var(--radius-m);
min-height: 70px;
color: black;
background-color: white !important;
@@ -148,7 +153,7 @@ $fixed-width-padding: 16px;
align-items: center;
letter-spacing: -0.3px;
box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.2);
- width: calc(100vw - #{$fixed-width-padding});
+ width: inherit;
cursor: default;
font-size: $font-size-normal;
font-weight: $font-weight-book;
@@ -164,7 +169,7 @@ $fixed-width-padding: 16px;
}
.main-card {
- width: 100%;
+ width: inherit;
margin: var(--space-m) var(--space-l);
.content {
@@ -173,10 +178,7 @@ $fixed-width-padding: 16px;
color: black;
justify-content: center;
- .mode {
- width: 32px;
- height: 32px;
- color: black;
+ svg.mode {
margin-right: var(--space-s);
margin-top: var(--space-xxs);
}
@@ -400,6 +402,40 @@ $fixed-width-padding: 16px;
}
}
+.navi-initializer-container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xxs);
+ font-weight: 325;
+ font-size: 14px;
+ line-height: 21px;
+
+ .navi-initializer-card {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xxs);
+ align-items: center;
+ justify-content: center;
+ align-content: center;
+ flex-wrap: wrap;
+ background-color: white;
+ border-radius: var(--radius-m);
+ padding: var(--space-m);
+ box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.2);
+ text-align: center;
+
+ &.success {
+ background-color: $success-color;
+ gap: var(--space-xs);
+
+ button {
+ color: white;
+ background-color: $realtime-color;
+ }
+ }
+ }
+}
+
.navigator-modal-container {
position: fixed;
width: 100%;
@@ -436,20 +472,9 @@ $fixed-width-padding: 16px;
}
.info-stack {
- position: fixed;
- height: 69px;
+ position: relative;
letter-spacing: -0.3px;
- width: calc(100% - #{$fixed-width-padding});
- margin-right: 8px;
- margin-left: 8px;
-
- div:first-child {
- margin-top: 0;
- }
-
- .info-stack-item:not(:first-child) {
- margin-top: 5px;
- }
+ width: inherit;
&.slide-out {
animation:
@@ -476,6 +501,7 @@ $fixed-width-padding: 16px;
fadeIn 0.5s ease-out forwards;
font-size: $font-size-xsmall;
font-weight: $font-weight-book;
+ margin-top: 5px;
.icon-container {
display: flex;
diff --git a/app/component/map/map.scss b/app/component/map/map.scss
index 2c2863d710..60ad8751de 100644
--- a/app/component/map/map.scss
+++ b/app/component/map/map.scss
@@ -57,6 +57,7 @@ div.map {
.drawer-container {
overflow-y: scroll;
+ scrollbar-width: none;
height: 100%;
position: absolute;
width: 100%;
diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js
index 06949b855a..3ef6325d79 100644
--- a/app/configurations/config.hsl.js
+++ b/app/configurations/config.hsl.js
@@ -751,6 +751,7 @@ export default {
navigationLogo: 'hsl/navigator-logo.svg',
thumbsUpGraphic: 'hsl/thumbs-up.svg',
+ trafficLightGraphic: 'hsl/traffic-light.svg',
// features that should not be deployed to production
experimental: {
diff --git a/app/configurations/images/hsl/traffic-light.svg b/app/configurations/images/hsl/traffic-light.svg
new file mode 100644
index 0000000000..f9c8bd7dc5
--- /dev/null
+++ b/app/configurations/images/hsl/traffic-light.svg
@@ -0,0 +1,97 @@
+
\ No newline at end of file
diff --git a/app/translations.js b/app/translations.js
index c1a05d8316..e421534fb6 100644
--- a/app/translations.js
+++ b/app/translations.js
@@ -1326,7 +1326,9 @@ const translations = {
'navigation-intro-notifications-header':
'TODO_navigation-intro-login-prompt_EN',
'navigation-journey-end': 'Journey has ended',
- 'navigation-journey-start': 'Your journey starts {time}',
+ 'navigation-journey-start': 'Your journey starts at',
+ 'navigation-journey-start-early-prompt':
+ 'TODO_navigation-journey-start-early-prompt_EN',
'navigation-mode-canceled': 'TODO_{name} on peruuntunut',
'navigation-mode-early': 'TODO_{name} on etuajassa',
'navigation-mode-late:': 'TODO_{name} on myöhässä',
@@ -2617,7 +2619,8 @@ const translations = {
'Pysy ajan tasalla matkasi vaiheista',
'navigation-intro-notifications-header': 'Ilmoitukset ja muutokset',
'navigation-journey-end': 'Matka on päättynyt',
- 'navigation-journey-start': 'Matkasi alkaa {time}',
+ 'navigation-journey-start': 'Matkasi alkaa klo',
+ 'navigation-journey-start-early-prompt': 'Etkö halua odottaa?',
'navigation-mode-canceled': '{name} on peruuntunut',
'navigation-mode-early': '{name} on etuajassa',
'navigation-mode-late:': '{name} on myöhässä',
@@ -5561,7 +5564,9 @@ const translations = {
'navigation-intro-notifications-header':
'TODO_navigation-intro-login-prompt_SV',
'navigation-journey-end': 'Resan är över',
- 'navigation-journey-start': 'Din resa börjar {time}',
+ 'navigation-journey-start': 'Din resa börjar',
+ 'navigation-journey-start-early-prompt':
+ 'TODO_navigation-journey-start-early-prompt_SV',
'navigation-mode-canceled': 'TODO_{name} on peruuntunut',
'navigation-mode-early': 'TODO_{name} on etuajassa',
'navigation-mode-late:': 'TODO_{name} on myöhässä',
diff --git a/sass/themes/default/_theme.scss b/sass/themes/default/_theme.scss
index 45a189be56..4f672ce532 100644
--- a/sass/themes/default/_theme.scss
+++ b/sass/themes/default/_theme.scss
@@ -42,6 +42,7 @@ $desktop-title-color: $primary-color;
$desktop-title-arrow-icon-color: $secondary-color;
$infobox-color-generic-blue: #e5f2fa;
$info-icon-blue: #0074be;
+$success-color: #f1f8eb;
/* Component palette */
$title-color: $white;