diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index a9213835a7..f550defea3 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -1087,7 +1087,6 @@ export default function ItineraryPage(props, context) { const cancelNavigatorUsage = () => { setNavigation(false); - toggleNavigatorIntro(); }; const walkPlan = altStates[PLANTYPE.WALK][0].plan; @@ -1183,12 +1182,13 @@ export default function ItineraryPage(props, context) { /> )} ); diff --git a/app/component/itinerary/navigator/NaviBottom.js b/app/component/itinerary/navigator/NaviBottom.js index adafb6c7e4..097e5adcfa 100644 --- a/app/component/itinerary/navigator/NaviBottom.js +++ b/app/component/itinerary/navigator/NaviBottom.js @@ -10,7 +10,7 @@ export default function NaviBottom( ) { const remainingDuration = Math.ceil((arrival - time) / 60000); // ms to minutes return ( -
+
); } @@ -219,6 +215,12 @@ NaviCardContainer.propTypes = { x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, }).isRequired, + currentLeg: legShape, + nextLeg: legShape, + firstLeg: legShape, + lastLeg: legShape, + isJourneyCompleted: PropTypes.bool, + /* focusToPoint: PropTypes.func.isRequired, */ @@ -227,6 +229,11 @@ NaviCardContainer.propTypes = { NaviCardContainer.defaultProps = { focusToLeg: undefined, position: undefined, + currentLeg: undefined, + nextLeg: undefined, + firstLeg: undefined, + lastLeg: undefined, + isJourneyCompleted: false, }; NaviCardContainer.contextTypes = { diff --git a/app/component/itinerary/navigator/NaviContainer.js b/app/component/itinerary/navigator/NaviContainer.js index aeb09d426e..c083f9a140 100644 --- a/app/component/itinerary/navigator/NaviContainer.js +++ b/app/component/itinerary/navigator/NaviContainer.js @@ -1,44 +1,43 @@ +import distance from '@digitransit-search-util/digitransit-search-util-distance'; import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; -import polyUtil from 'polyline-encoded'; import { legTime } from '../../../util/legUtils'; import { checkPositioningPermission } from '../../../action/PositionActions'; -import { GeodeticToEcef, GeodeticToEnu } from '../../../util/geo-utils'; -import { itineraryShape, relayShape } from '../../../util/shapes'; +import { legShape, relayShape } from '../../../util/shapes'; import NaviBottom from './NaviBottom'; import NaviCardContainer from './NaviCardContainer'; import { useRealtimeLegs } from './hooks/useRealtimeLegs'; +import NavigatorOutroModal from './navigatoroutro/NavigatorOutroModal'; + +const DESTINATION_RADIUS = 20; // meters +const ADDITIONAL_ARRIVAL_TIME = 60000; // 60 seconds in ms function NaviContainer( { - itinerary, + legs, focusToLeg, relayEnvironment, setNavigation, + isNavigatorIntroDismissed, mapRef, mapLayerRef, }, { getStore }, ) { - const [planarLegs, setPlanarLegs] = useState([]); - const [origin, setOrigin] = useState(); const [isPositioningAllowed, setPositioningAllowed] = useState(false); const position = getStore('PositionStore').getLocationState(); - useEffect(() => { - const { lat, lon } = itinerary.legs[0].from; - const orig = GeodeticToEcef(lat, lon); - const legs = itinerary.legs.map(leg => { - const geometry = polyUtil.decode(leg.legGeometry.points); - return { - ...leg, - geometry: geometry.map(p => GeodeticToEnu(p[0], p[1], orig)), - }; - }); - setPlanarLegs(legs); - setOrigin(orig); - }, [itinerary]); + const { + realTimeLegs, + time, + origin, + firstLeg, + lastLeg, + previousLeg, + currentLeg, + nextLeg, + } = useRealtimeLegs(relayEnvironment, legs); useEffect(() => { if (position.hasLocation) { @@ -54,20 +53,22 @@ function NaviContainer( } }, [mapRef]); - const { realTimeLegs, time } = useRealtimeLegs( - planarLegs, - mapRef, - relayEnvironment, - ); - - if (!realTimeLegs.length) { + if (!realTimeLegs?.length) { return null; } + const arrivalTime = legTime(lastLeg.end); + + const isDestinationReached = + position && lastLeg && distance(position, lastLeg.to) <= DESTINATION_RADIUS; + + const isPastExpectedArrival = time > arrivalTime + ADDITIONAL_ARRIVAL_TIME; + + const isJourneyCompleted = isDestinationReached || isPastExpectedArrival; + return ( <> arrivalTime ? previousLeg : currentLeg} + nextLeg={nextLeg} + firstLeg={firstLeg} + lastLeg={lastLeg} + isJourneyCompleted={isJourneyCompleted} /> + {isJourneyCompleted && isNavigatorIntroDismissed && ( + setNavigation(false)} + /> + )} @@ -87,10 +99,11 @@ function NaviContainer( } NaviContainer.propTypes = { - itinerary: itineraryShape.isRequired, + legs: PropTypes.arrayOf(legShape).isRequired, focusToLeg: PropTypes.func.isRequired, relayEnvironment: relayShape.isRequired, setNavigation: PropTypes.func.isRequired, + isNavigatorIntroDismissed: PropTypes.bool, // eslint-disable-next-line mapRef: PropTypes.object, mapLayerRef: PropTypes.func.isRequired, @@ -100,6 +113,9 @@ NaviContainer.contextTypes = { getStore: PropTypes.func.isRequired, }; -NaviContainer.defaultProps = { mapRef: undefined }; +NaviContainer.defaultProps = { + mapRef: undefined, + isNavigatorIntroDismissed: false, +}; export default NaviContainer; diff --git a/app/component/itinerary/navigator/NaviInstructions.js b/app/component/itinerary/navigator/NaviInstructions.js index b4b1095595..0786f8bbbb 100644 --- a/app/component/itinerary/navigator/NaviInstructions.js +++ b/app/component/itinerary/navigator/NaviInstructions.js @@ -27,7 +27,10 @@ export default function NaviInstructions( remainingTraversal = 1.0 - traversed; } else { // estimate from elapsed time - remainingTraversal = (legTime(leg.end) - time) / (leg.duration * 1000); + remainingTraversal = Math.max( + (legTime(leg.end) - time) / (leg.duration * 1000), + 0, + ); } const duration = leg.duration * remainingTraversal; const distance = leg.distance * remainingTraversal; diff --git a/app/component/itinerary/navigator/NavigatorModal.js b/app/component/itinerary/navigator/NavigatorModal.js new file mode 100644 index 0000000000..5c309fbedd --- /dev/null +++ b/app/component/itinerary/navigator/NavigatorModal.js @@ -0,0 +1,49 @@ +import Modal from '@hsl-fi/modal'; +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { intlShape } from 'react-intl'; +import { isBrowser } from '../../../util/browser'; +import { configShape } from '../../../util/shapes'; + +const NavigatorModal = ({ withBackdrop, isOpen, children, slideUp }) => { + const overlayClass = cx('navigator-modal-container', { + 'navigator-modal-backdrop': withBackdrop, + }); + + const modalClass = cx('navigator-modal', { + 'slide-in': slideUp, + }); + + return ( + +
{children}
+
+ ); +}; + +NavigatorModal.propTypes = { + children: PropTypes.node, + withBackdrop: PropTypes.bool, + isOpen: PropTypes.bool, + slideUp: PropTypes.bool, +}; + +NavigatorModal.defaultProps = { + children: undefined, + withBackdrop: false, + isOpen: false, + slideUp: false, +}; + +NavigatorModal.contextTypes = { + intl: intlShape.isRequired, + config: configShape.isRequired, +}; + +export default NavigatorModal; diff --git a/app/component/itinerary/navigator/hooks/useLogo.js b/app/component/itinerary/navigator/hooks/useLogo.js new file mode 100644 index 0000000000..bb8279b067 --- /dev/null +++ b/app/component/itinerary/navigator/hooks/useLogo.js @@ -0,0 +1,29 @@ +import { useState, useEffect, useCallback } from 'react'; + +const useLogo = logoPath => { + const [logo, setLogo] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchLogo = useCallback(async () => { + setLoading(true); + try { + const importedLogo = await import( + /* webpackChunkName: "main" */ `../../../../configurations/images/${logoPath}` + ); + setLogo(importedLogo.default); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error loading logo:', error); + } finally { + setLoading(false); + } + }, [logoPath]); + + useEffect(() => { + fetchLogo(); + }, [fetchLogo]); + + return { logo, loading }; +}; + +export { useLogo }; diff --git a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js index 92efd821ca..83bdc655d2 100644 --- a/app/component/itinerary/navigator/hooks/useRealtimeLegs.js +++ b/app/component/itinerary/navigator/hooks/useRealtimeLegs.js @@ -1,8 +1,10 @@ -import { useCallback, useEffect, useState } from 'react'; +import polyUtil from 'polyline-encoded'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { fetchQuery } from 'react-relay'; -import { legQuery } from '../../queries/LegQuery'; +import { GeodeticToEcef, GeodeticToEnu } from '../../../../util/geo-utils'; import { legTime } from '../../../../util/legUtils'; import { epochToIso } from '../../../../util/timeUtils'; +import { legQuery } from '../../queries/LegQuery'; function nextTransitIndex(legs, i) { for (let j = i; j < legs.length; j++) { @@ -85,10 +87,42 @@ function matchLegEnds(legs) { } } -const useRealtimeLegs = (initialLegs, mapRef, relayEnvironment) => { - const [realTimeLegs, setRealTimeLegs] = useState(initialLegs); +function getLegsOfInterest(legs, time) { + if (!legs?.length) { + return { + firstLeg: undefined, + lastLeg: undefined, + currentLeg: undefined, + nextLeg: undefined, + }; + } + + const firstLeg = legs[0]; + const lastLeg = legs[legs.length - 1]; + const nextLeg = legs.find(({ start }) => legTime(start) > time); + const previousLeg = legs.findLast(({ end }) => legTime(end) < time); + const currentLeg = legs.find( + ({ start, end }) => legTime(start) <= time && legTime(end) >= time, + ); + + return { + firstLeg, + lastLeg, + previousLeg, + currentLeg, + nextLeg, + }; +} + +const useRealtimeLegs = (relayEnvironment, initialLegs = []) => { + const [realTimeLegs, setRealTimeLegs] = useState(); const [time, setTime] = useState(Date.now()); + const origin = useMemo( + () => GeodeticToEcef(initialLegs[0].from.lat, initialLegs[0].from.lon), + [initialLegs[0]], + ); + const queryAndMapRealtimeLegs = useCallback( async legs => { if (!legs.length) { @@ -115,12 +149,27 @@ const useRealtimeLegs = (initialLegs, mapRef, relayEnvironment) => { ); const fetchAndSetRealtimeLegs = useCallback(async () => { - const rtLegMap = await queryAndMapRealtimeLegs(initialLegs).catch(err => + if ( + !initialLegs?.length || + time >= legTime(initialLegs[initialLegs.length - 1].end) + ) { + return; + } + + const planarLegs = initialLegs.map(leg => { + const geometry = polyUtil.decode(leg.legGeometry.points); + return { + ...leg, + geometry: geometry.map(p => GeodeticToEnu(p[0], p[1], origin)), + }; + }); + + const rtLegMap = await queryAndMapRealtimeLegs(planarLegs).catch(err => // eslint-disable-next-line no-console console.error('Failed to query and map real time legs', err), ); - const rtLegs = initialLegs.map(l => { + const rtLegs = planarLegs.map(l => { const rtLeg = l.legId ? rtLegMap[l.legId] : null; if (rtLeg) { return { @@ -141,6 +190,7 @@ const useRealtimeLegs = (initialLegs, mapRef, relayEnvironment) => { useEffect(() => { fetchAndSetRealtimeLegs(); + const interval = setInterval(() => { fetchAndSetRealtimeLegs(); setTime(Date.now()); @@ -149,7 +199,19 @@ const useRealtimeLegs = (initialLegs, mapRef, relayEnvironment) => { return () => clearInterval(interval); }, [fetchAndSetRealtimeLegs]); - return { realTimeLegs, time }; + const { firstLeg, lastLeg, currentLeg, nextLeg, previousLeg } = + getLegsOfInterest(realTimeLegs, time); + + return { + realTimeLegs, + time, + origin, + firstLeg, + lastLeg, + previousLeg, + currentLeg, + nextLeg, + }; }; export { useRealtimeLegs }; diff --git a/app/component/itinerary/navigator/navigator.scss b/app/component/itinerary/navigator/navigator.scss index 254d15f523..d783dcbdff 100644 --- a/app/component/itinerary/navigator/navigator.scss +++ b/app/component/itinerary/navigator/navigator.scss @@ -41,9 +41,17 @@ font-weight: $font-weight-medium; } +.navi-card-container { + position: fixed; + width: 100vw; + + &.slide-out { + animation: slideUpToTop 3s ease-out forwards; + } +} + .navitop { margin-bottom: 5px; - position: fixed; width: 92%; margin-left: 8px; border-radius: 8px; @@ -311,50 +319,39 @@ } } -@keyframes slideIn { - from { - transform: translateY(-100%); - opacity: 0; - } - - to { - transform: translateY(0); - opacity: 1; - } +.navigator-modal-container { + position: fixed; + width: 100%; + bottom: 0; + left: 0; + z-index: 1000; // higher than navbar + display: flex; + flex-direction: row; + align-items: flex-end; } -@keyframes slideOut { - from { - transform: translateY(0); - opacity: 1; - } - - to { - transform: translateY(-100%); - opacity: 0; - } +.navigator-modal-backdrop { + background-color: rgba(0, 0, 0, 0.2); + z-index: 999; // higher than navbar, below modal body + height: 100%; } -@keyframes slideOutRight { - from { - transform: translateX(0); - opacity: 1; - } +.navigator-modal { + position: fixed; + background: white; + width: 100%; + border-radius: var(--radius-xl) var(--radius-xl) 0 0; - to { - transform: translateX(-100%); - opacity: 0; + &.slide-in { + animation: slideUpFromBottom 0.5s ease-in-out; } } -@keyframes fadeOut { - from { - opacity: 1; - } - - to { - opacity: 0; - } +.navigator-modal-content { + display: flex; + flex-direction: column; + padding: var(--space-xl) var(--space-l) var(--space-l) var(--space-l); + gap: var(--space-xl); } .info-stack { @@ -373,12 +370,16 @@ } &.slide-out { - animation: slideOut 0.5s ease-out forwards; + animation: + slideUpFromBottom 0.5s ease-out forwards, + fadeOut 0.5s ease-out forwards; pointer-events: none; } &.slide-in { - animation: slideIn 0.5s ease-out forwards; + animation: + slideDownFromTop 0.5s ease-out forwards, + fadeIn 0.5s ease-out forwards; } .info-stack-item { @@ -388,7 +389,9 @@ align-items: center; padding: 16px; box-shadow: 0 2px 4px 0 rgba(51, 51, 51, 0.2); - animation: slideIn 0.5s ease-out forwards; + animation: + slideDownFromTop 0.5s ease-out forwards, + fadeIn 0.5s ease-out forwards; .icon-container { display: flex; @@ -454,7 +457,9 @@ } &.slide-out-right { - animation: slideOutRight 0.5s ease-out forwards; + animation: + slideLeft 0.5s ease-out forwards, + fadeOut 0.5s ease-out forwards; pointer-events: none; } @@ -481,7 +486,9 @@ } &.slide-in { - animation: slideIn 0.5s ease-out forwards; + animation: + slideDownFromTop 0.5s ease-out forwards, + fadeOut 0.5s ease-out forwards; } .navi-info { @@ -519,7 +526,7 @@ } } -.navibottomsheet { +.navi-bottom-sheet { .divider { width: 100%; height: 1px; @@ -544,13 +551,14 @@ height: 40px; text-align: center; background-color: white; - border-color: red; - color: red; + border-color: $cancelation-red; + color: $cancelation-red; } .navi-time { display: flex; flex-direction: column; + color: $black; .navi-daytime { font-size: $font-size-xsmall; @@ -574,3 +582,63 @@ } } } + +@keyframes slideDownFromTop { + from { + transform: translateY(-100%); + } + + to { + transform: translateY(0); + } +} + +@keyframes slideUpFromBottom { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } +} + +@keyframes slideUpToTop { + from { + transform: translateY(0); + } + + to { + transform: translateY(calc(-100vh - 100%)); + } +} + +@keyframes slideLeft { + from { + transform: translateX(0); + } + + to { + transform: translateX(-100%); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} diff --git a/app/component/itinerary/navigator/navigatorintro/NavigatorIntro.js b/app/component/itinerary/navigator/navigatorintro/NavigatorIntro.js index 4e7641ba97..6563a54092 100644 --- a/app/component/itinerary/navigator/navigatorintro/NavigatorIntro.js +++ b/app/component/itinerary/navigator/navigatorintro/NavigatorIntro.js @@ -1,27 +1,22 @@ import Button from '@hsl-fi/button'; -import { connectToStores } from 'fluxible-addons-react'; import PropTypes from 'prop-types'; import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import { configShape } from '../../../../util/shapes'; -import Icon from '../../../Icon'; import NavigatorIntroFeature from './NavigatorIntroFeature'; -const NavigatorIntro = ( - { logo, onPrimaryClick, onClose, isLoggedIn }, - context, -) => { +const NavigatorIntro = ({ logo, onPrimaryClick, onClose }, context) => { const { config, intl } = context; const primaryColor = config.colors?.accessiblePrimary || config.colors?.primary || 'black'; return ( -
-
+ <> +
{logo && navigator logo} -
+
- {config.allowLogin && !isLoggedIn && ( -
- - -
- )}
-
+
-
+ ); }; @@ -70,13 +59,11 @@ NavigatorIntro.propTypes = { logo: PropTypes.string, onClose: PropTypes.func.isRequired, onPrimaryClick: PropTypes.func, - isLoggedIn: PropTypes.bool, }; NavigatorIntro.defaultProps = { logo: undefined, onPrimaryClick: undefined, - isLoggedIn: false, }; NavigatorIntro.contextTypes = { @@ -84,11 +71,4 @@ NavigatorIntro.contextTypes = { config: configShape.isRequired, }; -export default connectToStores( - NavigatorIntro, - ['UserStore'], - ({ config, getStore }) => ({ - isLoggedIn: - config?.allowLogin && getStore('UserStore')?.getUser()?.sub !== undefined, - }), -); +export default NavigatorIntro; diff --git a/app/component/itinerary/navigator/navigatorintro/NavigatorIntroFeature.js b/app/component/itinerary/navigator/navigatorintro/NavigatorIntroFeature.js index 26a662de28..4856e9705b 100644 --- a/app/component/itinerary/navigator/navigatorintro/NavigatorIntroFeature.js +++ b/app/component/itinerary/navigator/navigatorintro/NavigatorIntroFeature.js @@ -11,7 +11,7 @@ const NavigatorIntroFeature = ({ body, }) => { return ( -
+
{icon && ( { - const { config } = context; - const [logo, setLogo] = useState(); +const NavigatorIntroModal = ({ onPrimaryClick, onClose }, { config }) => { + const { logo, loading } = useLogo(config.navigationLogo); - useEffect(() => { - if (!config.navigationLogo) { - return; - } - - const loadLogo = async () => { - try { - const importedLogo = await import( - /* webpackChunkName: "main" */ `../../../../configurations/images/${config.navigationLogo}` - ); - setLogo(importedLogo.default); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error loading logo:', error); - } - }; - - loadLogo(); - }, []); + if (loading) { + return null; + } return ( - + - + ); }; diff --git a/app/component/itinerary/navigator/navigatorintro/navigator-intro.scss b/app/component/itinerary/navigator/navigatorintro/navigator-intro.scss index 2e41f20656..eb91922fba 100644 --- a/app/component/itinerary/navigator/navigatorintro/navigator-intro.scss +++ b/app/component/itinerary/navigator/navigatorintro/navigator-intro.scss @@ -1,43 +1,16 @@ -.navigator-intro-modal-overlay { - position: fixed; - height: 100%; - width: 100%; - top: 0; - left: 0; - background-color: rgba(0, 0, 0, 0.2); - z-index: 1000; // higher than navbar - display: flex; - flex-direction: row; - align-items: flex-end; -} - -.navigator-intro-modal { - position: fixed; - background: white; - width: 100%; - border-radius: 30px 30px 0 0; - box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2); -} - -.navigator-intro-modal-content { - display: flex; - flex-direction: column; - padding: 0 var(--space-s); - - .body { +.navigator-modal-content { + .intro-body { display: flex; flex-direction: column; flex-wrap: wrap; align-items: stretch; - padding: var(--space-xl) 0 var(--space-s) 0; + align-content: center; gap: var(--space-m); - h2 { - align-self: center; - } - + h2, p { margin: unset; + align-self: center; } .login-tip { @@ -46,19 +19,17 @@ flex-wrap: nowrap; justify-content: center; gap: var(--space-xs); - padding: var(--space-xxs) var(--space-l); } - .navigation-intro-body { + .content { display: flex; flex-wrap: wrap; flex-direction: column; align-content: center; align-items: stretch; gap: var(--space-m); - padding: 0 var(--space-m); - .content-box { + .feature { display: flex; flex-direction: row; align-items: center; @@ -74,11 +45,10 @@ } } - .buttons { + .intro-buttons { display: flex; flex-direction: column; width: 100%; - gap: var(--space-xs); - padding: var(--space-s) var(--space-s) var(--space-m) var(--space-s); + gap: var(--space-s); } } diff --git a/app/component/itinerary/navigator/navigatoroutro/NavigatorOutro.js b/app/component/itinerary/navigator/navigatoroutro/NavigatorOutro.js new file mode 100644 index 0000000000..5d63d50328 --- /dev/null +++ b/app/component/itinerary/navigator/navigatoroutro/NavigatorOutro.js @@ -0,0 +1,53 @@ +import Button from '@hsl-fi/button'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { FormattedMessage, intlShape } from 'react-intl'; + +const NavigatorOutro = ({ onClose, destination, logo } /* ,context */) => { + // const { intl } = context; + const [place, address] = destination?.split(/, (.+)/) || []; + return ( + <> +
+ {logo && thumbs up} +
+
+ +
+

{place}

+

{address}

+
+
+
+
+ + ); +}; + +NavigatorOutro.propTypes = { + onClose: PropTypes.func.isRequired, + destination: PropTypes.string.isRequired, + logo: PropTypes.string, +}; + +NavigatorOutro.defaultProps = { + logo: undefined, +}; + +NavigatorOutro.contextTypes = { + intl: intlShape.isRequired, +}; + +export default NavigatorOutro; diff --git a/app/component/itinerary/navigator/navigatoroutro/NavigatorOutroModal.js b/app/component/itinerary/navigator/navigatoroutro/NavigatorOutroModal.js new file mode 100644 index 0000000000..0b74373e9c --- /dev/null +++ b/app/component/itinerary/navigator/navigatoroutro/NavigatorOutroModal.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { intlShape } from 'react-intl'; +import { configShape } from '../../../../util/shapes'; +import { useLogo } from '../hooks/useLogo'; +import NavigatorModal from '../NavigatorModal'; +import NavigatorOutro from './NavigatorOutro'; + +const NavigatorOutroModal = ({ onClose, destination }, { config }) => { + const { logo, loading } = useLogo(config.thumbsUpGraphic); + + if (loading) { + return null; + } + + return ( + + + + ); +}; + +NavigatorOutroModal.propTypes = { + onClose: PropTypes.func.isRequired, + destination: PropTypes.string.isRequired, +}; + +NavigatorOutroModal.contextTypes = { + intl: intlShape.isRequired, + config: configShape.isRequired, +}; + +export default NavigatorOutroModal; diff --git a/app/component/itinerary/navigator/navigatoroutro/navigator-outro.scss b/app/component/itinerary/navigator/navigatoroutro/navigator-outro.scss new file mode 100644 index 0000000000..0c5cbc53f4 --- /dev/null +++ b/app/component/itinerary/navigator/navigatoroutro/navigator-outro.scss @@ -0,0 +1,55 @@ +.navigator-modal-content { + .outro-logo-container { + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -50%); + + img { + background-color: white; /* Just for visualization */ + border-radius: var(--radius-pill); + } + } + + .outro-body { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + align-content: center; + padding-top: var(--space-xl); + gap: var(--space-xs); + + h2, + p { + margin: unset; + align-self: center; + } + + .destination { + display: flex; + flex-direction: column; + align-items: center; + gap: calc(var(--space-xxs) / 2); + + .place { + color: $black; + } + + .address { + color: $gray; + } + } + } + + .outro-buttons { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--space-s); + + .close-button { + background-color: $realtime-color; + } + } +} diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index 151cba6367..dced04e5f7 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -751,6 +751,7 @@ export default { startSearchFromUserLocation: true, navigationLogo: 'hsl/navigator-logo.svg', + thumbsUpGraphic: 'hsl/thumbs-up.svg', // features that should not be deployed to production experimental: { diff --git a/app/configurations/images/hsl/thumbs-up.svg b/app/configurations/images/hsl/thumbs-up.svg new file mode 100644 index 0000000000..343bda94d7 --- /dev/null +++ b/app/configurations/images/hsl/thumbs-up.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/translations.js b/app/translations.js index 078393179f..4be1ad7fa6 100644 --- a/app/translations.js +++ b/app/translations.js @@ -1312,6 +1312,7 @@ const translations = { 'navigation-mode-canceled': 'TODO_{mode} on peruuntunut', 'navigation-mode-early': 'TODO_{mode} on etuajassa', 'navigation-mode-late:': 'TODO_{mode} on myöhässä', + 'navigation-outro-header': 'TODO_navigation-outro-header_EN', 'navigation-quit': 'Quit', 'navigation-remember-ticket': 'TODO_Muistithan ostaa lipun?', 'navigation-start': 'Start journey', @@ -2578,6 +2579,7 @@ const translations = { 'navigation-mode-canceled': '{mode} on peruuntunut', 'navigation-mode-early': '{mode} on etuajassa', 'navigation-mode-late:': '{mode} on myöhässä', + 'navigation-outro-header': 'Olet perillä!', 'navigation-quit': 'Poistu', 'navigation-remember-ticket': 'Muistithan ostaa lipun?', 'navigation-start': 'Matkalle', @@ -5495,6 +5497,7 @@ const translations = { 'navigation-mode-canceled': 'TODO_{mode} on peruuntunut', 'navigation-mode-early': 'TODO_{mode} on etuajassa', 'navigation-mode-late:': 'TODO_{mode} on myöhässä', + 'navigation-outro-header': 'TODO_navigation-outro-header_SV', 'navigation-quit': 'Sluta', 'navigation-remember-ticket': 'TODO_Muistithan ostaa lipun?', 'navigation-start': 'På resa', diff --git a/sass/_main.scss b/sass/_main.scss index 72407fcc07..c064f8441f 100644 --- a/sass/_main.scss +++ b/sass/_main.scss @@ -13,6 +13,7 @@ $body-font-weight: $font-weight-medium; @import '~zurb-foundation-5/scss/foundation/components/grid'; @import 'base/helper-classes-after-foundations'; @import 'base/spacing'; +@import 'base/radius'; // Some of these files override sass variables Foundation uses, // so they must be loaded before the relevant foundation modules @@ -65,6 +66,7 @@ $body-font-weight: $font-weight-medium; @import 'base/button'; @import '../app/component/itinerary/navigator/navigator'; @import '../app/component/itinerary/navigator/navigatorintro/navigator-intro'; +@import '../app/component/itinerary/navigator/navigatoroutro/navigator-outro'; /* Modal */ @import '~foundation-apps/scss/helpers/breakpoints'; diff --git a/sass/base/_radius.scss b/sass/base/_radius.scss new file mode 100644 index 0000000000..5c1782d634 --- /dev/null +++ b/sass/base/_radius.scss @@ -0,0 +1,7 @@ +:root { + --radius-s: 4px; + --radius-m: 8px; + --radius-l: 16px; + --radius-xl: 24px; + --radius-pill: 999px; +} diff --git a/test/unit/views/ItineraryPage/component/NavigatorIntro.test.js b/test/unit/views/ItineraryPage/component/NavigatorIntro.test.js index 5c3ee1cfb9..71585b7bea 100644 --- a/test/unit/views/ItineraryPage/component/NavigatorIntro.test.js +++ b/test/unit/views/ItineraryPage/component/NavigatorIntro.test.js @@ -25,9 +25,7 @@ describe('', () => { }, ); - expect( - wrapper.find('div.navigator-intro-modal-content img'), - ).to.have.lengthOf(1); + expect(wrapper.find('div.intro-body img')).to.have.lengthOf(1); }); it('should not render logo if prop is missing', () => { @@ -38,51 +36,6 @@ describe('', () => { childContextTypes: { ...mockChildContextTypes }, }); - assert(wrapper.find('div.navigator-intro-modal-content img'), undefined); - }); - - it('should render login tip if login is allowed and user is not logged in', () => { - const wrapper = mountWithIntl( - , - { - context: { - ...mockContext, - config: { CONFIG: 'default', allowLogin: true }, - }, - childContextTypes: { ...mockChildContextTypes }, - }, - ); - - expect(wrapper.find('div.login-tip')).to.have.lengthOf(1); - }); - - it('should not render login tip if login is not allowed and user is not logged in', () => { - const wrapper = mountWithIntl( - , - { - context: { - ...mockContext, - config: { CONFIG: 'default', allowLogin: true }, - }, - childContextTypes: { ...mockChildContextTypes }, - }, - ); - - assert(wrapper.find('div.login-tip'), undefined); - }); - - it('should not render login tip if login is allowed and user logged in', () => { - const wrapper = mountWithIntl( - , - { - context: { - ...mockContext, - config: { CONFIG: 'default', allowLogin: true }, - }, - childContextTypes: { ...mockChildContextTypes }, - }, - ); - - assert(wrapper.find('div.login-tip'), undefined); + assert(wrapper.find('div.intro-body img'), undefined); }); });