diff --git a/.github/workflows/dev-pipeline.yml b/.github/workflows/dev-pipeline.yml index 0862963261..f572a12e6a 100644 --- a/.github/workflows/dev-pipeline.yml +++ b/.github/workflows/dev-pipeline.yml @@ -24,22 +24,22 @@ jobs: node-version: [20.x] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} # https://github.com/actions/cache/blob/8f1e2e02865c42348f9baddbbaafb1841dce610a/examples.md#node---yarn-2 - name: Cache node_modules id: cache-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }} - name: Cache .yarn/cache if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '.yarn/cache' key: yarn-v3-cache-${{ github.ref_name }} @@ -50,7 +50,7 @@ jobs: - name: cache shared components id: cache-shared-components - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'digitransit-*' key: ${{ runner.os }}-shared-components-${{ hashFiles('digitransit-*') }} @@ -66,26 +66,26 @@ jobs: node-version: [20.x] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set time zone to Europe/Helsinki" uses: zcong1993/setup-timezone@master with: timezone: "Europe/Helsinki" - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} # https://github.com/actions/cache/blob/8f1e2e02865c42348f9baddbbaafb1841dce610a/examples.md#node---yarn-2 - name: Cache node_modules id: cache-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }} - name: Cache .yarn/cache if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '.yarn/cache' key: yarn-v3-cache-${{ github.ref_name }} @@ -96,7 +96,7 @@ jobs: - name: cache shared components id: cache-shared-components - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'digitransit-*' key: ${{ runner.os }}-shared-components-${{ hashFiles('digitransit-*') }} @@ -120,26 +120,26 @@ jobs: node-version: [20.x] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set time zone to Europe/Helsinki" uses: zcong1993/setup-timezone@master with: timezone: "Europe/Helsinki" - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} # https://github.com/actions/cache/blob/8f1e2e02865c42348f9baddbbaafb1841dce610a/examples.md#node---yarn-2 - name: Cache node_modules id: cache-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }} - name: Cache .yarn/cache if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '.yarn/cache' key: yarn-v3-cache-${{ github.ref_name }} @@ -150,7 +150,7 @@ jobs: - name: cache shared components id: cache-shared-components - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'digitransit-*' key: ${{ runner.os }}-shared-components-${{ hashFiles('digitransit-*') }} @@ -160,7 +160,7 @@ jobs: - name: cache built Relay queries id: cache-relay - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./__generated__ @@ -186,26 +186,26 @@ jobs: node-version: [ 20.x ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Set time zone to Europe/Helsinki" uses: zcong1993/setup-timezone@master with: timezone: "Europe/Helsinki" - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} # https://github.com/actions/cache/blob/8f1e2e02865c42348f9baddbbaafb1841dce610a/examples.md#node---yarn-2 - name: Cache node_modules id: cache-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: ${{ runner.os }}-node-modules-${{ hashFiles('**/yarn.lock') }} - name: Cache .yarn/cache if: ${{ steps.cache-node-modules.outputs.cache-hit != 'true' }} - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '.yarn/cache' key: yarn-v3-cache-${{ github.ref_name }} @@ -216,7 +216,7 @@ jobs: - name: cache shared components id: cache-shared-components - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'digitransit-*' key: ${{ runner.os }}-shared-components-${{ hashFiles('digitransit-*') }} @@ -226,7 +226,7 @@ jobs: - name: cache built Relay queries id: cache-relay - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ./__generated__ @@ -280,7 +280,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set time zone to Europe/Helsinki uses: zcong1993/setup-timezone@master with: diff --git a/.github/workflows/prod-pipeline.yml b/.github/workflows/prod-pipeline.yml index b849d015bd..dc71163dd0 100644 --- a/.github/workflows/prod-pipeline.yml +++ b/.github/workflows/prod-pipeline.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check Tag id: check-tag run: | diff --git a/app/component/AboutPage.js b/app/component/AboutPage.js index 7626878836..0685a9ea49 100644 --- a/app/component/AboutPage.js +++ b/app/component/AboutPage.js @@ -27,7 +27,7 @@ const AboutPage = ({ currentLanguage }, { config }) => { /> ))} {section.link && ( - + { id="CookieConsent" src="https://policy.app.cookieinformation.com/uc.js" data-gcm-version="2.0" - data-culture="FI" + data-culture={lang.toUpperCase()} type="text/javascript" /> diff --git a/app/component/EmbeddedSearchGenerator.js b/app/component/EmbeddedSearchGenerator.js index da363d249b..c7674eeef5 100644 --- a/app/component/EmbeddedSearchGenerator.js +++ b/app/component/EmbeddedSearchGenerator.js @@ -456,7 +456,11 @@ const EmbeddedSearchGenerator = (props, context) => { {config.embeddedSearch?.cookieLink && (

- + {config.embeddedSearch.cookieLink[lang].text}

diff --git a/app/component/FavouriteVehicleRentalStationContainer.js b/app/component/FavouriteVehicleRentalStationContainer.js index aab671c10c..b2c7919ec4 100644 --- a/app/component/FavouriteVehicleRentalStationContainer.js +++ b/app/component/FavouriteVehicleRentalStationContainer.js @@ -17,7 +17,7 @@ const FavouriteVehicleRentalStationContainer = connectToStores( context.executeAction(saveFavourite, { lat: vehicleRentalStation.lat, lon: vehicleRentalStation.lon, - network: vehicleRentalStation.network, + network: vehicleRentalStation.rentalNetwork.networkId, name: vehicleRentalStation.name, stationId: vehicleRentalStation.stationId, type: 'bikeStation', @@ -35,7 +35,7 @@ const FavouriteVehicleRentalStationContainer = connectToStores( .getStore('FavouriteStore') .getByStationIdAndNetworks( vehicleRentalStation.stationId, - vehicleRentalStation.network, + vehicleRentalStation.rentalNetwork.networkId, ); context.executeAction(deleteFavourite, vehicleRentalStationToDelete); addAnalyticsEvent({ diff --git a/app/component/FavouritesContainer.js b/app/component/FavouritesContainer.js index cd0f7a357a..8edd951dac 100644 --- a/app/component/FavouritesContainer.js +++ b/app/component/FavouritesContainer.js @@ -331,7 +331,10 @@ class FavouritesContainer extends React.Component { item => item.type === 'place', ); if ( - useCitybikes(this.context.config.cityBike?.networks, this.context.config) + useCitybikes( + this.context.config.vehicleRental?.networks, + this.context.config, + ) ) { targets.push('VehicleRentalStations'); } diff --git a/app/component/IndexPage.js b/app/component/IndexPage.js index bc42fdcd02..c607bebf9a 100644 --- a/app/component/IndexPage.js +++ b/app/component/IndexPage.js @@ -9,6 +9,7 @@ import DTAutoSuggest from '@digitransit-component/digitransit-component-autosugg import DTAutosuggestPanel from '@digitransit-component/digitransit-component-autosuggest-panel'; import { getModesWithAlerts } from '@digitransit-search-util/digitransit-search-util-query-utils'; import { createUrl } from '@digitransit-store/digitransit-store-future-route'; +import inside from 'point-in-polygon'; import { configShape, locationShape } from '../util/shapes'; import storeOrigin from '../action/originActions'; import storeDestination from '../action/destinationActions'; @@ -36,6 +37,10 @@ import { getNearYouModes, useCitybikes, } from '../util/modeUtils'; +import { + checkPositioningPermission, + startLocationWatch, +} from '../action/PositionActions'; const StopRouteSearch = withSearchContext(DTAutoSuggest); const LocationSearch = withSearchContext(DTAutosuggestPanel); @@ -105,6 +110,16 @@ class IndexPage extends React.Component { this.context.executeAction(storeDestination, destination); } + if (this.context.config.startSearchFromUserLocation) { + checkPositioningPermission().then(permission => { + if ( + permission.state === 'granted' && + this.props.locationState.status === 'no-location' + ) { + this.context.executeAction(startLocationWatch); + } + }); + } scrollTop(); } @@ -128,6 +143,18 @@ class IndexPage extends React.Component { const { router, match, config } = this.context; const { location } = match; + const currentLocation = + config.startSearchFromUserLocation && + !this.props.origin.address && + this.props.locationState?.hasLocation && + this.props.locationState; + if (currentLocation && !currentLocation.isReverseGeocodingInProgress) { + const originPoint = [currentLocation.lon, currentLocation.lat]; + if (inside(originPoint, config.areaPolygon)) { + this.context.executeAction(storeOrigin, currentLocation); + } + } + if (definesItinerarySearch(origin, destination)) { const newLocation = { ...location, @@ -280,7 +307,7 @@ class IndexPage extends React.Component { 'Stops', ]; - if (useCitybikes(config.cityBike?.networks, config)) { + if (useCitybikes(config.vehicleRental?.networks, config)) { stopAndRouteSearchTargets.push('VehicleRentalStations'); locationSearchTargets.push('VehicleRentalStations'); } diff --git a/app/component/MainMenu.js b/app/component/MainMenu.js index 80cb639c06..ee786afea2 100644 --- a/app/component/MainMenu.js +++ b/app/component/MainMenu.js @@ -17,8 +17,8 @@ import intializeSearchContext from '../util/DTSearchContextInitializer'; function MainMenu(props, { config, intl, executeAction }) { const [countries, setCountries] = useState(props.countries); const appBarLinkHref = - config.appBarLink.alternativeHref?.[props.currentLanguage] || - config.appBarLink.href; + config.appBarLink?.alternativeHref?.[props.currentLanguage] || + config.appBarLink?.href; return (
@@ -67,7 +67,11 @@ function MainMenu(props, { config, intl, executeAction }) { )} {config.mainMenu.stopMonitor.show && (
- +
@@ -117,25 +121,25 @@ function MainMenu(props, { config, intl, executeAction }) {
))} - {config.appBarLink?.name && - appBarLinkHref && - !config.hideAppBarLink && ( -
- { - addAnalyticsEvent({ - category: 'Navigation', - action: 'appBarLink', - name: null, - }); - }} - > - {config.appBarLink.name} - -
- )} + {config.appBarLink?.name && appBarLinkHref && ( +
+ { + addAnalyticsEvent({ + category: 'Navigation', + action: 'appBarLink', + name: null, + }); + }} + rel="noreferrer" + > + {config.appBarLink.name} + +
+ )}
)} {showRentalVehiclesOfType( - config.cityBike?.networks, + config.vehicleRental?.networks, config, TransportMode.Citybike, ) && ( @@ -236,7 +236,7 @@ class MapLayersDialogContent extends React.Component { /> )} {showRentalVehiclesOfType( - config.cityBike?.networks, + config.vehicleRental?.networks, config, TransportMode.Scooter, ) && ( diff --git a/app/component/MapRoutingButton.js b/app/component/MapRoutingButton.js index 2700ec564b..e2544404e1 100644 --- a/app/component/MapRoutingButton.js +++ b/app/component/MapRoutingButton.js @@ -35,10 +35,14 @@ export default function MapRoutingButton( const locationWithoutQuery = { ...location, query: {}, search: '' }; const time = Math.floor(Date.now() / 1000); const onSelectLocation = (item, id) => { + const address = + item.name.toLowerCase() === 'scooter' + ? intl.formatMessage({ id: 'e-scooter' }) + : item.name; let newLocation; const place = { ...item, - address: item.name, + address, gtfsId: match.params.stopId || match.params.terminalId, }; if (id === 'origin') { diff --git a/app/component/MenuItem.js b/app/component/MenuItem.js index 7591d67951..410f05cb5a 100644 --- a/app/component/MenuItem.js +++ b/app/component/MenuItem.js @@ -3,14 +3,9 @@ import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import { routerShape } from 'found'; -const mapToLink = (href, children, onClick, openInNewTab) => ( +const mapToLink = (href, children, onClick) => ( - + {children} @@ -32,7 +27,7 @@ const mapToRoute = (router, route, children, onClick) => ( ); export default function MenuItem( - { name, href, label, route, onClick, openInNewTab }, + { name, href, label, route, onClick }, { router, intl }, ) { const displayLabel = label || ( @@ -41,9 +36,9 @@ export default function MenuItem( let item = {displayLabel}; if (href) { if (typeof href === 'object') { - item = mapToLink(href[intl.locale], item, onClick, openInNewTab); + item = mapToLink(href[intl.locale], item, onClick); } else { - item = mapToLink(href, item, onClick, openInNewTab); + item = mapToLink(href, item, onClick); } } else if (route) { item = mapToRoute(router, route, item, onClick); @@ -62,7 +57,6 @@ MenuItem.propTypes = { route: PropTypes.string, label: PropTypes.string, onClick: PropTypes.func, - openInNewTab: PropTypes.bool, }; MenuItem.defaultProps = { @@ -71,7 +65,6 @@ MenuItem.defaultProps = { route: undefined, label: undefined, onClick: undefined, - openInNewTab: false, }; MenuItem.contextTypes = { diff --git a/app/component/MessageBarMessage.js b/app/component/MessageBarMessage.js index 59cb926d27..3ee96aceeb 100644 --- a/app/component/MessageBarMessage.js +++ b/app/component/MessageBarMessage.js @@ -21,6 +21,8 @@ export default function MessageBarMessage( {link.content || link.href} diff --git a/app/component/ParkOrStationHeader.js b/app/component/ParkOrStationHeader.js index 8ad14d5031..884959d5af 100644 --- a/app/component/ParkOrStationHeader.js +++ b/app/component/ParkOrStationHeader.js @@ -9,12 +9,8 @@ import { getJson } from '../util/xhrPromise'; import getZoneId from '../util/zoneIconUtils'; import ZoneIcon from './ZoneIcon'; import withBreakpoint from '../util/withBreakpoint'; -import { - hasVehicleRentalCode, - getRentalNetworkConfig, -} from '../util/vehicleRentalUtils'; +import { hasVehicleRentalCode } from '../util/vehicleRentalUtils'; import { getIdWithoutFeed } from '../util/feedScopedIdUtils'; -import { TransportMode } from '../constants'; const modules = { FavouriteVehicleRentalStationContainer: () => @@ -50,13 +46,10 @@ const ParkOrBikeStationHeader = ( }); }, []); - const { name, stationId, network } = parkOrStation; - const networkConfig = getRentalNetworkConfig(network, config); + const { name, stationId } = parkOrStation; const parkHeaderId = parkType === 'bike' ? 'bike-park' : 'car-park'; - const noIdHeaderName = - networkConfig.type === TransportMode.Citybike.toLowerCase() - ? 'citybike-station-no-id' - : 'e-scooter-station'; + const isRentalStation = stationId; + return (
{breakpoint === 'large' && ( @@ -68,8 +61,10 @@ const ParkOrBikeStationHeader = (

{name}

- - {stationId && hasVehicleRentalCode(parkOrStation.stationId) && ( + + {isRentalStation && hasVehicleRentalCode(stationId) && ( )} {zoneId && ( @@ -79,7 +74,7 @@ const ParkOrBikeStationHeader = ( )}
- {stationId && ( + {isRentalStation && ( {({ FavouriteVehicleRentalStationContainer }) => ( { const disabled = !rentalVehicle.operative; const vehicleIcon = getRentalNetworkIcon( - getRentalNetworkConfig(rentalVehicle.network, config), + getRentalNetworkConfig(rentalVehicle.rentalNetwork.networkId, config), disabled, ); return ( @@ -23,7 +23,7 @@ const RentalVehicle = ({ rentalVehicle }, { config }) => { RentalVehicle.contextTypes = { config: PropTypes.shape({ - cityBike: { networks: PropTypes.arrayOf(PropTypes.string.isRequired) }, + vehicleRental: { networks: PropTypes.arrayOf(PropTypes.string.isRequired) }, }).isRequired, }; RentalVehicle.propTypes = { diff --git a/app/component/RentalVehicleContent.js b/app/component/RentalVehicleContent.js index 8ab2fda81b..1e21d5fea7 100644 --- a/app/component/RentalVehicleContent.js +++ b/app/component/RentalVehicleContent.js @@ -41,7 +41,10 @@ const RentalVehicleContent = ( } return null; } - const networkConfig = getRentalNetworkConfig(rentalVehicle.network, config); + const networkConfig = getRentalNetworkConfig( + rentalVehicle.rentalNetwork.networkId, + config, + ); const vehicleIcon = getRentalNetworkIcon( networkConfig, !rentalVehicle.operative, @@ -68,7 +71,8 @@ const RentalVehicleContent = (

- {networkConfig.name[language] || rentalVehicle.network} + {networkConfig.name[language] || + rentalVehicle.rentalNetwork.networkId}

@@ -95,7 +99,10 @@ const RentalVehicleContent = ( /> )}
-

{networkConfig.name[language] || rentalVehicle.network}

+

+ {networkConfig.name[language] || + rentalVehicle.rentalNetwork.networkId} +

@@ -154,7 +161,6 @@ const containerComponent = createFragmentContainer(connectedComponent, { lat lon name - network vehicleId rentalUris { android @@ -162,6 +168,7 @@ const containerComponent = createFragmentContainer(connectedComponent, { web } rentalNetwork { + networkId url } } diff --git a/app/component/RentalVehiclePageMapContainer.js b/app/component/RentalVehiclePageMapContainer.js index 8474bccb84..f9a24aae05 100644 --- a/app/component/RentalVehiclePageMapContainer.js +++ b/app/component/RentalVehiclePageMapContainer.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { createFragmentContainer, graphql } from 'react-relay'; +import { FormattedMessage } from 'react-intl'; import StopPageMap from './map/StopPageMap'; import { rentalVehicleShape } from '../util/shapes'; @@ -8,7 +9,13 @@ const RentalVehiclePageMapContainer = ({ rentalVehicle }) => { if (!rentalVehicle) { return false; } - return ; + const stopName = ( + + ); + return ; }; RentalVehiclePageMapContainer.contextTypes = { diff --git a/app/component/RouteNumber.js b/app/component/RouteNumber.js index b4a01191e1..0bb9a7cddd 100644 --- a/app/component/RouteNumber.js +++ b/app/component/RouteNumber.js @@ -101,7 +101,8 @@ function RouteNumber(props, context) { })} role="img" > - {!props.isTransitLeg && !props.renderModeIcons && ( + {((!props.isTransitLeg && !props.renderModeIcons) || + props.appendClass === 'scooter') && (
)} {props.isTransitLeg === true ? ( diff --git a/app/component/SelectedStopPopupContent.js b/app/component/SelectedStopPopupContent.js index c3aa40891f..c604697827 100644 --- a/app/component/SelectedStopPopupContent.js +++ b/app/component/SelectedStopPopupContent.js @@ -1,10 +1,11 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { stopShape } from '../util/shapes'; -const SelectedStopPopupContent = ({ stop }) => ( +const SelectedStopPopupContent = ({ stop, name }) => (
-
{stop.name}
+
{name || stop.name}
{(stop.code || stop.desc) && (
@@ -20,7 +21,15 @@ const SelectedStopPopupContent = ({ stop }) => (
); -SelectedStopPopupContent.propTypes = { stop: stopShape.isRequired }; +SelectedStopPopupContent.propTypes = { + stop: stopShape.isRequired, + name: PropTypes.node, +}; + +SelectedStopPopupContent.defaultProps = { + name: undefined, +}; + SelectedStopPopupContent.displayName = 'SelectedStopPopupContent'; export default SelectedStopPopupContent; diff --git a/app/component/StopNearYou.js b/app/component/StopNearYou.js index 9d3b935e8b..177de36d80 100644 --- a/app/component/StopNearYou.js +++ b/app/component/StopNearYou.js @@ -73,7 +73,11 @@ const StopNearYou = (
{constantOperationStops[stop.gtfsId][locale].text} - + {constantOperationStops[stop.gtfsId][locale].link} diff --git a/app/component/StopPageContentContainer.js b/app/component/StopPageContentContainer.js index 98aa231f26..0b7049b800 100644 --- a/app/component/StopPageContentContainer.js +++ b/app/component/StopPageContentContainer.js @@ -77,7 +77,11 @@ class StopPageContent extends React.Component { {constantOperationStops[stopId][locale].text} {/* Next span inline-block so that the link doesn't render on multiple lines */} - + {constantOperationStops[stopId][locale].link} diff --git a/app/component/StopsNearYouContainer.js b/app/component/StopsNearYouContainer.js index 3fd856f6c4..f601fbb16b 100644 --- a/app/component/StopsNearYouContainer.js +++ b/app/component/StopsNearYouContainer.js @@ -186,11 +186,11 @@ class StopsNearYouContainer extends React.Component { let sortedPatterns; if (isCityBikeView) { const withNetworks = stopPatterns.filter(pattern => { - return !!pattern.node.place?.network; + return !!pattern.node.place?.rentalNetwork?.networkId; }); const filteredCityBikeStopPatterns = withNetworks.filter(pattern => { return getDefaultNetworks(this.context.config).includes( - pattern.node.place?.network, + pattern.node.place?.rentalNetwork?.networkId, ); }); sortedPatterns = filteredCityBikeStopPatterns @@ -367,7 +367,9 @@ const refetchContainer = createPaginationContainer( ... on VehicleRentalStation { ...VehicleRentalStationNearYou_stop stationId - network + rentalNetwork { + networkId + } } ... on Stop { ...StopNearYouContainer_stop diff --git a/app/component/StopsNearYouFavouritesContainer.js b/app/component/StopsNearYouFavouritesContainer.js index 5f42708478..2491be8c26 100644 --- a/app/component/StopsNearYouFavouritesContainer.js +++ b/app/component/StopsNearYouFavouritesContainer.js @@ -116,7 +116,9 @@ const refetchContainer = createFragmentContainer( total } capacity - network + rentalNetwork { + networkId + } lat lon operative diff --git a/app/component/StopsNearYouMapContainer.js b/app/component/StopsNearYouMapContainer.js index 4036c859d5..5990365524 100644 --- a/app/component/StopsNearYouMapContainer.js +++ b/app/component/StopsNearYouMapContainer.js @@ -71,7 +71,9 @@ const containerComponent = createPaginationContainer( lat lon stationId - network + rentalNetwork { + networkId + } } ... on Stop { gtfsId diff --git a/app/component/StopsNearYouPage.js b/app/component/StopsNearYouPage.js index 07fa0de5be..2c8059654c 100644 --- a/app/component/StopsNearYouPage.js +++ b/app/component/StopsNearYouPage.js @@ -454,18 +454,18 @@ class StopsNearYouPage extends React.Component { variables={this.getQueryVariables(nearByStopMode)} environment={this.props.relayEnvironment} render={({ props }) => { - const { cityBike } = this.context.config; + const { vehicleRental } = this.context.config; // Use buy instructions if available - const cityBikeBuyUrl = cityBike.buyUrl; + const cityBikeBuyUrl = vehicleRental.buyUrl; const buyInstructions = cityBikeBuyUrl - ? cityBike.buyInstructions?.[this.props.lang] + ? vehicleRental.buyInstructions?.[this.props.lang] : undefined; let cityBikeNetworkUrl; // Use general information about using city bike, if one network config is available - if (Object.keys(cityBike.networks).length === 1) { + if (Object.keys(vehicleRental.networks).length === 1) { cityBikeNetworkUrl = getRentalNetworkConfig( - getRentalNetworkId(Object.keys(cityBike.networks)), + getRentalNetworkId(Object.keys(vehicleRental.networks)), this.context.config, ).url; } @@ -522,6 +522,8 @@ class StopsNearYouPage extends React.Component { @@ -529,6 +531,8 @@ class StopsNearYouPage extends React.Component { {cityBikeBuyUrl && ( stop.type === 'station') .map(stop => stop.gtfsId); let favouriteVehicleStationIds = []; - if (useCitybikes(context.config.cityBike?.networks, context.config)) { + if (useCitybikes(context.config.vehicleRental?.networks, context.config)) { favouriteVehicleStationIds = context .getStore('FavouriteStore') .getVehicleRentalStations() diff --git a/app/component/Timetable.js b/app/component/Timetable.js index ca9d4197fc..2ed3363d04 100644 --- a/app/component/Timetable.js +++ b/app/component/Timetable.js @@ -243,7 +243,11 @@ class Timetable extends React.Component {
{constantOperationStops[stopId][locale].text} - + {constantOperationStops[stopId][locale].link} diff --git a/app/component/VehicleRentalStation.js b/app/component/VehicleRentalStation.js index b2fad4f812..3a6854400d 100644 --- a/app/component/VehicleRentalStation.js +++ b/app/component/VehicleRentalStation.js @@ -13,7 +13,7 @@ import { const VehicleRentalStation = ({ vehicleRentalStation }, { config }) => { const vehicleCapacity = getVehicleCapacity( config, - vehicleRentalStation.network, + vehicleRentalStation.rentalNetwork.networkId, ); if (vehicleCapacity === BIKEAVL_UNKNOWN) { return null; @@ -32,7 +32,7 @@ const VehicleRentalStation = ({ vehicleRentalStation }, { config }) => { } const disabled = !vehicleRentalStation.operative; const networkConfig = getRentalNetworkConfig( - vehicleRentalStation.network, + vehicleRentalStation.rentalNetwork.networkId, config, ); const vehicleIcon = getRentalNetworkIcon(networkConfig, disabled); diff --git a/app/component/VehicleRentalStationContent.js b/app/component/VehicleRentalStationContent.js index acc21712c6..5dfcfa058b 100644 --- a/app/component/VehicleRentalStationContent.js +++ b/app/component/VehicleRentalStationContent.js @@ -47,7 +47,7 @@ const VehicleRentalStationContent = ( const isFull = vehiclesAvailable >= capacity; const networkConfig = getRentalNetworkConfig( - vehicleRentalStation.network, + vehicleRentalStation.rentalNetwork.networkId, config, ); const cityBikeNetworkUrl = networkConfig?.url?.[language]; @@ -55,10 +55,10 @@ const VehicleRentalStationContent = ( if (networkConfig.returnInstructions) { returnInstructionsUrl = networkConfig.returnInstructions[language]; } - const { cityBike } = config; - const cityBikeBuyUrl = cityBike.buyUrl?.[language]; + const { vehicleRental } = config; + const cityBikeBuyUrl = vehicleRental.buyUrl?.[language]; const buyInstructions = cityBikeBuyUrl - ? cityBike.buyInstructions?.[language] + ? vehicleRental.buyInstructions?.[language] : undefined; return ( @@ -68,7 +68,7 @@ const VehicleRentalStationContent = ( breakpoint={breakpoint} /> - {cityBike.showFullInfo && isFull && ( + {vehicleRental.showFullInfo && isFull && (
{' '} @@ -91,7 +93,12 @@ const VehicleRentalStationContent = (
{buyInstructions || ( - + )} @@ -103,6 +110,8 @@ const VehicleRentalStationContent = ( }} className="external-link" href={cityBikeBuyUrl} + target="_blank" + rel="noreferrer" > @@ -155,7 +164,9 @@ const containerComponent = createFragmentContainer(connectedComponent, { total } capacity - network + rentalNetwork { + networkId + } stationId operative } diff --git a/app/component/VehicleRentalStationNearYou.js b/app/component/VehicleRentalStationNearYou.js index ca9c1f7fea..1384d7ddec 100644 --- a/app/component/VehicleRentalStationNearYou.js +++ b/app/component/VehicleRentalStationNearYou.js @@ -76,7 +76,9 @@ VehicleRentalStationNearYou.propTypes = { lat: PropTypes.number, lon: PropTypes.number, name: PropTypes.string, - network: PropTypes.string, + rentalNetwork: PropTypes.shape({ + networkId: PropTypes.string, + }), operative: PropTypes.bool, stationId: PropTypes.string, type: PropTypes.string, @@ -107,7 +109,9 @@ const containerComponent = createRefetchContainer( total } capacity - network + rentalNetwork { + networkId + } operative } `, diff --git a/app/component/departure.scss b/app/component/departure.scss index 1722fd1197..aa2440781f 100644 --- a/app/component/departure.scss +++ b/app/component/departure.scss @@ -213,6 +213,10 @@ &.return-citybike { height: 13px; } + + &.scooter { + height: 10px; + } } } } diff --git a/app/component/itinerary/BicycleLeg.js b/app/component/itinerary/BicycleLeg.js index bb006b91c2..33531a3a6b 100644 --- a/app/component/itinerary/BicycleLeg.js +++ b/app/component/itinerary/BicycleLeg.js @@ -1,10 +1,11 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState } from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import cx from 'classnames'; import Link from 'found/Link'; -import { legShape, configShape } from '../../util/shapes'; -import { legTimeStr } from '../../util/legUtils'; +import { fetchQuery } from 'react-relay'; +import { legShape, configShape, relayShape } from '../../util/shapes'; +import { legTimeStr, legDestination } from '../../util/legUtils'; import Icon from '../Icon'; import ItineraryMapAction from './ItineraryMapAction'; import { displayDistance } from '../../util/geo-utils'; @@ -23,6 +24,7 @@ import { getSettings } from '../../util/planParamUtil'; import { isKeyboardSelectionEvent } from '../../util/browser'; import StopCode from '../StopCode'; import PlatformNumber from '../PlatformNumber'; +import nearestQuery from './NearestQuery'; export default function BicycleLeg( { @@ -33,6 +35,7 @@ export default function BicycleLeg( bicycleWalkLeg, openSettings, nextLegMode, + relayEnvironment, }, { config, intl }, ) { @@ -51,7 +54,8 @@ export default function BicycleLeg( let modeClassName = 'bicycle'; const [address, place] = splitStringToAddressAndPlace(leg.from.name); const rentalVehicleNetwork = - leg.from.vehicleRentalStation?.network || leg.from.rentalVehicle?.network; + leg.from.vehicleRentalStation?.rentalNetwork.networkId || + leg.from.rentalVehicle?.rentalNetwork.networkId; const networkConfig = leg.rentedBike && rentalVehicleNetwork && @@ -61,6 +65,15 @@ export default function BicycleLeg( networkConfig && networkConfig.type === RentalNetworkType.Scooter; const settings = getSettings(config); const scooterSettingsOn = settings.scooterNetworks?.length > 0; + const LOADSTATE = { + UNSET: 'unset', + DONE: 'done', + }; + const [nearestScooterState, setNearestScooterState] = useState({ + nearest: [], + loading: LOADSTATE.UNSET, + }); + if (leg.mode === 'WALK' || leg.mode === 'BICYCLE_WALK') { modeClassName = leg.mode.toLowerCase(); stopsDescription = ( @@ -88,7 +101,7 @@ export default function BicycleLeg( modeClassName = 'citybike'; legDescription = ( ); } else if (bicycleWalkLeg) { @@ -138,22 +158,65 @@ export default function BicycleLeg( ); } const fromStop = leg?.from.stop || bicycleWalkLeg?.from.stop; - const getToMode = () => { - if (leg.to.bikePark) { - return 'bike-park'; - } - if (leg.to.stop?.vehicleMode) { - return leg.to.stop?.vehicleMode.toLowerCase(); - } - if (bicycleWalkLeg?.to.stop?.vehicleMode) { - return bicycleWalkLeg.to.stop?.vehicleMode.toLowerCase(); - } - return 'place'; - }; const origin = bicycleWalkLeg?.from.stop ? bicycleWalkLeg.from.name : address; const destination = bicycleWalkLeg?.to.stop ? bicycleWalkLeg?.to.name : leg.to.name; + + async function makeNearestScooterQuery(from) { + const planParams = { + lat: from.lat, + lon: from.lon, + maxResults: config.vehicleRental.maxNearbyRentalVehicleAmount, + first: config.vehicleRental.maxNearbyRentalVehicleAmount, + maxDistance: config.vehicleRental.maxDistanceToRentalVehiclesInMeters, + filterByModes: ['SCOOTER'], + filterByPlaceTypes: ['VEHICLE_RENT'], + }; + + try { + const result = await fetchQuery( + relayEnvironment, + nearestQuery, + planParams, + { + force: true, + }, + ).toPromise(); + if (!result) { + setNearestScooterState({ loading: LOADSTATE.DONE }); + } else { + const nearest = result.viewer.nearest.edges; + // filter out the ones that are not scooters or from the same network as the original scooter + const filteredNearest = nearest + ?.filter( + n => + n.node.place.__typename === 'RentalVehicle' && // eslint-disable-line no-underscore-dangle + n.node.place.rentalNetwork.networkId !== rentalVehicleNetwork, + ) + // show only one scooter from each network + .filter( + (n, i, self) => + self.findIndex( + t => + t.node.place.rentalNetwork.networkId === + n.node.place.rentalNetwork.networkId, + ) === i, + ); + + setNearestScooterState({ + nearest: filteredNearest, + loading: LOADSTATE.DONE, + }); + } + } catch (error) { + setNearestScooterState({ nearest: [], loading: LOADSTATE.DONE }); + } + } + + if (isScooter && nearestScooterState.loading === LOADSTATE.UNSET) { + makeNearestScooterQuery(leg.from); + } return (
@@ -161,12 +224,18 @@ export default function BicycleLeg( {(leg.mode === 'WALK' || leg.mode === 'BICYCLE_WALK') && stopsDescription} {isFirstLeg(index) || bicycleWalkLeg?.from.stop ? ( @@ -233,6 +310,7 @@ export default function BicycleLeg( isScooter={isScooter} vehicleRentalStation={leg.from.vehicleRentalStation} rentalVehicle={leg.from.rentalVehicle} + nearestScooters={nearestScooterState.nearest} />
)} @@ -374,17 +452,14 @@ export default function BicycleLeg(
)} {isScooter && ( -
-
- -
+ )}
@@ -399,11 +474,13 @@ BicycleLeg.propTypes = { focusToLeg: PropTypes.func.isRequired, openSettings: PropTypes.func.isRequired, nextLegMode: PropTypes.string, + relayEnvironment: relayShape, }; BicycleLeg.defaultProps = { bicycleWalkLeg: undefined, nextLegMode: undefined, + relayEnvironment: undefined, }; BicycleLeg.contextTypes = { diff --git a/app/component/itinerary/BikeParkLeg.js b/app/component/itinerary/BikeParkLeg.js index e0075d6686..30c81c124b 100644 --- a/app/component/itinerary/BikeParkLeg.js +++ b/app/component/itinerary/BikeParkLeg.js @@ -3,7 +3,7 @@ import cx from 'classnames'; import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import { legShape, parkShape, configShape } from '../../util/shapes'; -import { legTimeStr } from '../../util/legUtils'; +import { legTimeStr, legDestination } from '../../util/legUtils'; import { displayDistance } from '../../util/geo-utils'; import { durationToString } from '../../util/timeUtils'; import ItineraryCircleLineWithIcon from './ItineraryCircleLineWithIcon'; @@ -28,12 +28,7 @@ const BikeParkLeg = ( values={{ time, distance, - to: intl.formatMessage({ - id: `modes.to-${ - leg.to.stop?.vehicleMode?.toLowerCase() || 'place' - }`, - defaultMessage: 'modes.to-stop', - }), + to: legDestination(intl, leg), origin: leg.from ? leg.from.name : '', destination: leg.to ? leg.to.name : '', duration, diff --git a/app/component/itinerary/CarLeg.js b/app/component/itinerary/CarLeg.js index 242756d8e2..c019576969 100644 --- a/app/component/itinerary/CarLeg.js +++ b/app/component/itinerary/CarLeg.js @@ -7,7 +7,7 @@ import ItineraryMapAction from './ItineraryMapAction'; import { displayDistance } from '../../util/geo-utils'; import { durationToString } from '../../util/timeUtils'; import ItineraryCircleLineWithIcon from './ItineraryCircleLineWithIcon'; -import { legTimeStr } from '../../util/legUtils'; +import { legTimeStr, legDestination } from '../../util/legUtils'; export default function CarLeg(props, { config, intl }) { const distance = displayDistance( @@ -30,9 +30,7 @@ export default function CarLeg(props, { config, intl }) { values={{ time: legTimeStr(props.leg.start), distance, - to: intl.formatMessage({ - id: `modes.to-${props.leg.to.carPark ? 'car-park' : 'place'}`, - }), + to: legDestination(intl, props.leg), origin: props.leg.from ? props.leg.from.name : '', destination: props.leg.to ? props.leg.to.name : '', duration, diff --git a/app/component/itinerary/CarParkLeg.js b/app/component/itinerary/CarParkLeg.js index cae664eb06..1e99e67836 100644 --- a/app/component/itinerary/CarParkLeg.js +++ b/app/component/itinerary/CarParkLeg.js @@ -11,7 +11,7 @@ import { durationToString } from '../../util/timeUtils'; import ItineraryCircleLineWithIcon from './ItineraryCircleLineWithIcon'; import { PREFIX_CARPARK } from '../../util/path'; import ItineraryCircleLine from './ItineraryCircleLine'; -import { legTimeStr } from '../../util/legUtils'; +import { legTimeStr, legDestination } from '../../util/legUtils'; function CarParkLeg(props, { config, intl }) { const distance = displayDistance( @@ -32,12 +32,7 @@ function CarParkLeg(props, { config, intl }) { values={{ time: legTimeStr(props.leg.start), distance, - to: intl.formatMessage({ - id: `modes.to-${ - props.leg.to.stop?.vehicleMode?.toLowerCase() || 'place' - }`, - defaultMessage: 'modes.to-stop', - }), + to: legDestination(intl, props.leg), origin: props.leg.from ? props.leg.from.name : '', destination: props.leg.to ? props.leg.to.name : '', duration, diff --git a/app/component/itinerary/CustomizeSearch.js b/app/component/itinerary/CustomizeSearch.js index bdf1cc580b..591965d318 100644 --- a/app/component/itinerary/CustomizeSearch.js +++ b/app/component/itinerary/CustomizeSearch.js @@ -8,6 +8,7 @@ import FareZoneSelector from './customizesearch/FareZoneSelector'; import StreetModeSelectorPanel from './customizesearch/StreetModeSelectorPanel'; import TransportModesSection from './customizesearch/TransportModesSection'; import WalkingOptionsSection from './customizesearch/WalkingOptionsSection'; +import MinTransferTimeSection from './customizesearch/MinTransferTimeSection'; import AccessibilityOptionSection from './customizesearch/AccessibilityOptionSection'; import TransferOptionsSection from './customizesearch/TransferOptionsSection'; import RentalNetworkSelector from './customizesearch/RentalNetworkSelector'; @@ -128,6 +129,13 @@ class CustomizeSearch extends React.Component {
)} + {config.minTransferTimeSelection && ( + + )}
- {useCitybikes(config.cityBike?.networks, config) && ( + {useCitybikes(config.vehicleRental?.networks, config) && (
@@ -159,7 +167,7 @@ class CustomizeSearch extends React.Component {
)} - {useScooters(config.cityBike?.networks) && ( + {useScooters(config.vehicleRental?.networks) && (
diff --git a/app/component/itinerary/FareDisclaimer.js b/app/component/itinerary/FareDisclaimer.js index 0d693a24d2..110abaffb9 100644 --- a/app/component/itinerary/FareDisclaimer.js +++ b/app/component/itinerary/FareDisclaimer.js @@ -17,7 +17,11 @@ export default function FareDisclaimer({
{text || } - {href && {linkText}} + {href && ( + + {linkText} + + )}
); diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js index 079e4864af..af4b12d6f3 100644 --- a/app/component/itinerary/Itinerary.js +++ b/app/component/itinerary/Itinerary.js @@ -174,7 +174,10 @@ export const ModeLeg = ( networkIcon = leg.from.vehicleRentalStation && getRentalNetworkIcon( - getRentalNetworkConfig(leg.from.vehicleRentalStation.network, config), + getRentalNetworkConfig( + leg.from.vehicleRentalStation.rentalNetwork.networkId, + config, + ), ); } else if (mode === 'SCOOTER') { networkIcon = 'icon-icon_scooter_rider'; @@ -267,6 +270,7 @@ const Itinerary = ( { intl, intl: { formatMessage }, config }, ) => { const isTransitLeg = leg => leg.transitLeg; + const isTransitOrRentalLeg = leg => leg.transitLeg || leg.rentedBike; const isLegOnFoot = leg => leg.mode === 'WALK' || leg.mode === 'BICYCLE_WALK'; const usingOwnBicycle = itinerary.legs.some( leg => getLegMode(leg) === 'BICYCLE' && leg.rentedBike === false, @@ -288,6 +292,7 @@ const Itinerary = ( })); let intermediateSlack = 0; let transitLegCount = 0; + let containsScooterLeg = false; compressedLegs.forEach((leg, i) => { if (isTransitLeg(leg)) { noTransitLegs = false; @@ -300,6 +305,7 @@ const Itinerary = ( intermediateSlack += legTime(leg.start) - legTime(compressedLegs[i - 1].end); // calculate time spent at each intermediate place } + containsScooterLeg = leg.mode === 'SCOOTER' || containsScooterLeg; }); const durationWithoutSlack = duration - intermediateSlack; // don't include time spent at intermediate places in calculations for bar lengths const relativeLength = durationMs => @@ -425,15 +431,15 @@ const Itinerary = ( ) { const bikingTime = Math.floor(leg.duration / 60); // eslint-disable-next-line prefer-destructuring - bikeNetwork = leg.from.vehicleRentalStation.network; + bikeNetwork = leg.from.vehicleRentalStation.rentalNetwork.networkId; if ( - config.cityBike.networks && - config.cityBike.networks[bikeNetwork]?.timeBeforeSurcharge && - config.cityBike.networks[bikeNetwork]?.durationInstructions + config.vehicleRental.networks && + config.vehicleRental.networks[bikeNetwork]?.timeBeforeSurcharge && + config.vehicleRental.networks[bikeNetwork]?.durationInstructions ) { const rentDurationOverSurchargeLimit = leg.duration > - config.cityBike?.networks[bikeNetwork].timeBeforeSurcharge; + config.vehicleRental?.networks[bikeNetwork].timeBeforeSurcharge; if (rentDurationOverSurchargeLimit) { citybikeNetworks.add(bikeNetwork); } @@ -457,6 +463,12 @@ const Itinerary = ( large={breakpoint === 'large'} />, ); + vehicleNames.push( + formatMessage({ + id: `to-bicycle`, + }), + ); + stopNames.push(leg.from.name); } else if (leg.mode === 'SCOOTER' && leg.rentedBike) { const scooterDuration = Math.floor(leg.duration / 60); legs.push( @@ -471,6 +483,12 @@ const Itinerary = ( large={breakpoint === 'large'} />, ); + vehicleNames.push( + formatMessage({ + id: `to-e-scooter`, + }), + ); + stopNames.push(''); } else if (leg.mode === 'CAR') { const drivingTime = Math.floor(leg.duration / 60); legs.push( @@ -640,7 +658,7 @@ const Itinerary = (
{getVehicleCapacity( config, - firstDeparture.from.vehicleRentalStation.network, + firstDeparture.from.vehicleRentalStation.rentalNetwork.networkId, ) !== BIKEAVL_UNKNOWN && ( ), @@ -728,7 +759,11 @@ const Itinerary = ( return null; } return formatMessage( - { id: 'itinerary-summary-row.transfers' }, + { + id: stopNames[index] + ? 'itinerary-summary-row.transfers' + : 'itinerary-summary-row.transfers-to-rental', + }, { vehicle: name, stopName: stopNames[index], @@ -775,6 +810,11 @@ const Itinerary = ( ) : ( date ); + const showCo2Info = + config.showCO2InItinerarySummary && + co2value !== null && + co2value >= 0 && + !containsScooterLeg; return (

@@ -786,10 +826,7 @@ const Itinerary = ( />

{textSummary} - {config.showCO2InItinerarySummary && - co2value !== null && - co2value >= 0 && - co2summary} + {showCo2Info && co2summary}
{/* This next clickable region does not have proper accessible role, tabindex and keyboard handler because screen reader works weirdly with nested buttons. Same functonality works from the inner button */ @@ -833,16 +870,14 @@ const Itinerary = ( {(getTotalDistance(itinerary) / 1000).toFixed(1)} km
)} - {config.showCO2InItinerarySummary && - co2value !== null && - co2value >= 0 && ( -
- {lowestCo2value === co2value && ( - - )} -
{co2value} g
-
- )} + {showCo2Info && ( +
+ {lowestCo2value === co2value && ( + + )} +
{co2value} g
+
+ )}
@@ -875,7 +910,7 @@ const Itinerary = ( id="citybike-duration-info-short" values={{ duration: - config.cityBike.networks[bikeNetwork] + config.vehicleRental.networks[bikeNetwork] .timeBeforeSurcharge / 60, }} defaultMessage="" @@ -1030,7 +1065,9 @@ const containerComponent = createFragmentContainer(ItineraryWithBreakpoint, { availableVehicles { total } - network + rentalNetwork { + networkId + } } } to { diff --git a/app/component/itinerary/ItineraryCircleLineWithIcon.js b/app/component/itinerary/ItineraryCircleLineWithIcon.js index 6c35534011..3c73d5a8d5 100644 --- a/app/component/itinerary/ItineraryCircleLineWithIcon.js +++ b/app/component/itinerary/ItineraryCircleLineWithIcon.js @@ -15,6 +15,7 @@ class ItineraryCircleLineWithIcon extends React.Component { color: PropTypes.string, appendClass: PropTypes.string, icon: PropTypes.string, + style: PropTypes.shape({}), }; static defaultProps = { @@ -24,6 +25,7 @@ class ItineraryCircleLineWithIcon extends React.Component { carPark: false, appendClass: undefined, icon: undefined, + style: {}, }; state = { @@ -104,7 +106,7 @@ class ItineraryCircleLineWithIcon extends React.Component { render() { const topMarker = this.getMarker(true); const bottomMarker = this.getMarker(false); - const legBeforeLineStyle = { color: this.props.color }; + const legBeforeLineStyle = { color: this.props.color, ...this.props.style }; if ( isBrowser && (this.props.modeClassName === 'walk' || diff --git a/app/component/itinerary/ItineraryDetails.js b/app/component/itinerary/ItineraryDetails.js index a9656b9267..963a52a3c6 100644 --- a/app/component/itinerary/ItineraryDetails.js +++ b/app/component/itinerary/ItineraryDetails.js @@ -5,12 +5,12 @@ import cx from 'classnames'; import { matchShape, routerShape } from 'found'; import { FormattedMessage, intlShape } from 'react-intl'; import connectToStores from 'fluxible-addons-react/connectToStores'; -import get from 'lodash/get'; -import { configShape, itineraryShape } from '../../util/shapes'; +import { configShape, itineraryShape, relayShape } from '../../util/shapes'; import TicketInformation from './TicketInformation'; import ItinerarySummary from './ItinerarySummary'; import Legs from './Legs'; import BackButton from '../BackButton'; +import StartNavi from './StartNavi'; import MobileTicketPurchaseInformation from './MobileTicketPurchaseInformation'; import { compressLegs, @@ -54,7 +54,9 @@ class ItineraryDetails extends React.Component { currentLanguage: PropTypes.string, changeHash: PropTypes.func, openSettings: PropTypes.func.isRequired, + setNavigation: PropTypes.func, bikeAndPublicItineraryCount: PropTypes.number, + relayEnvironment: relayShape, }; static defaultProps = { @@ -63,6 +65,8 @@ class ItineraryDetails extends React.Component { changeHash: () => {}, bikeAndPublicItineraryCount: 0, carEmissions: undefined, + relayEnvironment: undefined, + setNavigation: undefined, }; static contextTypes = { @@ -141,6 +145,7 @@ class ItineraryDetails extends React.Component { legContainsRentalBike(leg), ); const legswithBikePark = compressLegs(itinerary.legs).filter(leg => legContainsBikePark(leg)); + const legsWithScooter = compressLegs(itinerary.legs).some(leg => leg.mode === 'SCOOTER'); const containsBiking = biking.duration > 0 && biking.distance > 0; const showBikeBoardingInformation = containsBiking && bikeAndPublicItineraryCount > 0 && legswithBikePark.length === 0; const rentalBikeNetworks = new Set(); @@ -148,14 +153,14 @@ class ItineraryDetails extends React.Component { if (legsWithRentalBike.length > 0) { for (let i = 0; i < legsWithRentalBike.length; i++) { const leg = legsWithRentalBike[i]; - const network = leg.from.vehicleRentalStation?.network; + const network = leg.from.vehicleRentalStation?.rentalNetwork.networkId; if ( - config.cityBike.networks[network]?.timeBeforeSurcharge && - config.cityBike.networks[network]?.durationInstructions + config.vehicleRental.networks[network]?.timeBeforeSurcharge && + config.vehicleRental.networks[network]?.durationInstructions ) { const rentDurationOverSurchargeLimit = leg.duration > - config.cityBike.networks[network].timeBeforeSurcharge; + config.vehicleRental.networks[network].timeBeforeSurcharge; if (rentDurationOverSurchargeLimit) { rentalBikeNetworks.add(network); showRentalBikeDurationWarning = @@ -176,7 +181,9 @@ class ItineraryDetails extends React.Component { const disclaimers = []; - if (shouldShowFareInfo(config) && fares.some(fare => fare.isUnknown)) { + const externalOperatorJourneys = legsWithScooter; + + if (shouldShowFareInfo(config) && (fares.some(fare => fare.isUnknown) || externalOperatorJourneys) ) { const found = {}; itinerary.legs.forEach(leg => { if (config.modeDisclaimers?.[leg.mode] && !found[leg.mode]) { @@ -214,7 +221,8 @@ class ItineraryDetails extends React.Component { key="faredisclaimer-separate-ticket-key" textId="separate-ticket-required-disclaimer" values={{ - agencyName: get(config, 'ticketInformation.primaryAgencyName'), + agencyName: typeof config.primaryAgencyName === 'string' ? config.primaryAgencyName : + config.primaryAgencyName?.[currentLanguage] }} />, ); @@ -287,7 +295,15 @@ class ItineraryDetails extends React.Component { legs={itinerary.legs} /> )), - config.showCO2InItinerarySummary && ( + + this.props.setNavigation && ( +
+ +
+ ), + config.showCO2InItinerarySummary && !legsWithScooter && (
- {config.showCO2InItinerarySummary && ( + {config.showCO2InItinerarySummary && !legsWithScooter && ( { - if (breakpoint !== 'large') { + const focusToPoint = (lat, lon, maximize = true) => { + if (breakpoint !== 'large' && maximize) { window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); expandMapRef.current += 1; } @@ -813,8 +820,8 @@ export default function ItineraryPage(props, context) { setMapState({ center: { lat, lon }, bounds: null }); }; - const focusToLeg = leg => { - if (breakpoint !== 'large') { + const focusToLeg = (leg, maximize = true) => { + if (breakpoint !== 'large' && maximize) { window.scrollTo({ top: 0, left: 0, behavior: 'smooth' }); expandMapRef.current += 1; } @@ -1053,22 +1060,42 @@ export default function ItineraryPage(props, context) {
); } else if (detailView) { - let carEmissions = carPlan?.edges?.[0]?.node.emissionsPerPerson?.co2; - carEmissions = carEmissions ? Math.round(carEmissions) : undefined; - content = ( - - ); + if (navigation) { + content = ( + + ); + } else { + let carEmissions = carPlan?.edges?.[0]?.node.emissionsPerPerson?.co2; + const pastSearch = + Date.parse(combinedEdges[selectedIndex]?.node.end) < Date.now(); + const navigateHook = + !desktop && config.navigation && !pastSearch + ? setNavigation + : undefined; + carEmissions = carEmissions ? Math.round(carEmissions) : undefined; + content = ( + + ); + } } else { if (state.loading === LOADSTATE.UNSET) { return null; // do not render 'no itineraries' before searches diff --git a/app/component/itinerary/ItineraryPageUtils.js b/app/component/itinerary/ItineraryPageUtils.js index 9aed9d68d5..f307fbab6c 100644 --- a/app/component/itinerary/ItineraryPageUtils.js +++ b/app/component/itinerary/ItineraryPageUtils.js @@ -356,15 +356,41 @@ export function transitEdges(edges) { } /** - * Filters away itineraries that don't use scooters + * Filters away itineraries that + * 1. don't use scooters + * 2. only use scooters + * 3. use scooters that are not vehicles */ export function scooterEdges(edges) { if (!edges) { return []; } - return edges.filter(edge => - edge.node.legs.some(leg => leg.mode === 'SCOOTER'), - ); + + const filteredEdges = []; + + edges.forEach(edge => { + let hasScooterLeg = false; + let hasNonScooterLeg = false; + let allScooterLegsHaveRentalVehicle = true; + + edge.node.legs.forEach(leg => { + if (leg.mode === 'SCOOTER' && leg.from.rentalVehicle) { + hasScooterLeg = true; + } else if (leg.mode !== 'SCOOTER' && leg.mode !== 'WALK') { + hasNonScooterLeg = true; + } + + if (leg.mode === 'SCOOTER' && !leg.from.rentalVehicle) { + allScooterLegsHaveRentalVehicle = false; + } + }); + + if (hasScooterLeg && hasNonScooterLeg && allScooterLegsHaveRentalVehicle) { + filteredEdges.push(edge); + } + }); + + return filteredEdges; } /** @@ -437,15 +463,24 @@ export function mergeBikeTransitPlans(bikeParkPlan, bikeTransitPlan) { * Combine a scooter edge with the main transit edges. */ export function mergeScooterTransitPlan(scooterPlan, transitPlan) { - const scooterTransitEdges = scooterEdges(scooterPlan?.edges); - const publicTransitEdges = transitEdges(transitPlan?.edges); + const transitPlanEdges = transitPlan.edges || []; + const scooterTransitEdges = scooterEdges(scooterPlan.edges); const maxTransitEdges = - scooterTransitEdges.length > 0 ? 4 : publicTransitEdges.length; + scooterTransitEdges.length > 0 ? 4 : transitPlanEdges.length; + + // special case: if transitplan only has one walk itinerary, don't show scooter plan if it arrives later. + if ( + transitPlanEdges.length === 1 && + transitPlanEdges[0].node.legs.every(leg => leg.mode === 'WALK') && + transitPlanEdges[0].node.end < scooterTransitEdges[0]?.node.end + ) { + return transitPlan; + } return { edges: [ ...scooterTransitEdges.slice(0, 1), - ...publicTransitEdges.slice(0, maxTransitEdges), + ...transitPlanEdges.slice(0, maxTransitEdges), ] .sort((a, b) => { return a.node.end > b.node.end; diff --git a/app/component/itinerary/Legs.js b/app/component/itinerary/Legs.js index 51424375ab..6a46710ac2 100644 --- a/app/component/itinerary/Legs.js +++ b/app/component/itinerary/Legs.js @@ -1,7 +1,12 @@ /* eslint-disable react/no-array-index-key */ import PropTypes from 'prop-types'; import React from 'react'; -import { configShape, fareShape, itineraryShape } from '../../util/shapes'; +import { + configShape, + fareShape, + itineraryShape, + relayShape, +} from '../../util/shapes'; import TransitLeg from './TransitLeg'; import WalkLeg from './WalkLeg'; import WaitLeg from './WaitLeg'; @@ -42,6 +47,7 @@ export default class Legs extends React.Component { tabIndex: PropTypes.number, openSettings: PropTypes.func.isRequired, showBikeBoardingInformation: PropTypes.bool, + relayEnvironment: relayShape, }; static contextTypes = { config: configShape }; @@ -51,6 +57,7 @@ export default class Legs extends React.Component { changeHash: undefined, tabIndex: undefined, showBikeBoardingInformation: false, + relayEnvironment: undefined, }; getChildContext() { @@ -73,7 +80,8 @@ export default class Legs extends React.Component { }; render() { - const { itinerary, fares, showBikeBoardingInformation } = this.props; + const { itinerary, fares, showBikeBoardingInformation, relayEnvironment } = + this.props; const { waitThreshold } = this.context.config.itinerary; const compressedLegs = compressLegs(itinerary.legs, true).map(leg => ({ @@ -249,6 +257,8 @@ export default class Legs extends React.Component { {...legProps} bicycleWalkLeg={bicycleWalkLeg} openSettings={this.props.openSettings} + nextLegMode={nextLeg?.mode} + relayEnvironment={relayEnvironment} />, ); } else if (leg.mode === 'CAR') { diff --git a/app/component/itinerary/NationalServiceLink.js b/app/component/itinerary/NationalServiceLink.js index e8c158d2ac..2074bdb22a 100644 --- a/app/component/itinerary/NationalServiceLink.js +++ b/app/component/itinerary/NationalServiceLink.js @@ -16,7 +16,7 @@ function NationalServiceLink({ currentLanguage, nationalServiceLink }) { id="use-national-service-prefix" defaultMessage="You can also try the national service available at" /> - + {name} diff --git a/app/component/itinerary/NaviDestination.js b/app/component/itinerary/NaviDestination.js new file mode 100644 index 0000000000..b1299f311b --- /dev/null +++ b/app/component/itinerary/NaviDestination.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, intlShape } from 'react-intl'; +import Icon from '../Icon'; +import StopCode from '../StopCode'; +import { legShape } from '../../util/shapes'; + +function NaviDestination({ leg, focusToLeg }) { + const { stop, rentalVehicle, vehicleParking, vehicleRentalStation } = leg.to; + + let toIcon; + if (stop) { + toIcon = `icon-icon_${stop.vehicleMode.toLowerCase()}-stop-lollipop`; + } else if (rentalVehicle) { + toIcon = 'icon-icon_scooter-lollipop'; + } else if (vehicleParking) { + toIcon = 'icon-bike_parking'; + } else if (vehicleRentalStation) { + toIcon = 'icon-icon_citybike'; + } + + const handleFocusToLeg = (l, maximize) => () => { + focusToLeg(l, maximize); + }; + + return ( +
+ {toIcon && } +
+ {stop?.name}  + {stop?.code && } + {rentalVehicle?.rentalNetwork.networkId} + {vehicleParking?.name} + {vehicleRentalStation?.rentalNetwork.networkId}  + {vehicleRentalStation?.name} +
+ +
+ ); +} + +NaviDestination.propTypes = { + leg: legShape.isRequired, + focusToLeg: PropTypes.func.isRequired, +}; + +NaviDestination.contextTypes = { + intl: intlShape.isRequired, +}; + +export default NaviDestination; diff --git a/app/component/itinerary/NaviLeg.js b/app/component/itinerary/NaviLeg.js new file mode 100644 index 0000000000..7e6a114db5 --- /dev/null +++ b/app/component/itinerary/NaviLeg.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { FormattedMessage, intlShape } from 'react-intl'; +import { legShape } from '../../util/shapes'; +import Icon from '../Icon'; +import { legDestination } from '../../util/legUtils'; +import NaviDestination from './NaviDestination'; + +const iconMap = { + BICYCLE: 'icon-icon_cyclist', + CAR: 'icon-icon_car-withoutBox', + SCOOTER: 'icon-icon_scooter_rider', + WALK: 'icon-icon_walk', +}; + +/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ +export default function NaviLeg({ leg, focusToLeg }, { intl }) { + const iconName = iconMap[leg.mode]; + const goTo = `navileg-${leg.mode.toLowerCase()}`; + + return ( +
+
+ + +   + {legDestination(intl, leg)} +
+
+
+ +
+
+ ); +} + +NaviLeg.propTypes = { + leg: legShape.isRequired, + focusToLeg: PropTypes.func.isRequired, +}; + +NaviLeg.contextTypes = { + intl: intlShape.isRequired, +}; diff --git a/app/component/itinerary/Navigator.js b/app/component/itinerary/Navigator.js new file mode 100644 index 0000000000..70a2a2f490 --- /dev/null +++ b/app/component/itinerary/Navigator.js @@ -0,0 +1,169 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; +import { FormattedMessage, intlShape } from 'react-intl'; +import { createFragmentContainer, graphql } from 'react-relay'; +import { itineraryShape } from '../../util/shapes'; +import { legTime, legTimeStr } from '../../util/legUtils'; +import Icon from '../Icon'; +import NaviLeg from './NaviLeg'; + +/* + const legQuery = graphql` + query legQuery($id: String!) { + node(id: $id) { + ... on Leg { + start { + scheduledTime + estimated { + time + } + } + end { + scheduledTime + estimated { + time + } + } + } + } + } +`; +*/ + +function Navigator({ itinerary, focusToLeg, setNavigation }, context) { + const [time, setTime] = useState(Date.now()); + const [currentLeg, setCurrentLeg] = useState(null); + + // update view after every 5 seconds + useEffect(() => { + const interval = setInterval(() => { + setTime(Date.now()); + }, 5000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const newLeg = itinerary.legs.find(leg => { + return legTime(leg.start) <= time && time <= legTime(leg.end); + }); + + if (newLeg && newLeg !== currentLeg) { + setCurrentLeg(newLeg); + focusToLeg(newLeg, false); + } + }, [time]); + + const first = itinerary.legs[0]; + let info; + if (time < legTime(first.start)) { + info = ( + + ); + } else if (currentLeg) { + if (!currentLeg.transitLeg) { + info = ; + } else { + info = `Tracking ${currentLeg?.mode} leg`; + } + } + return ( +
+
+ + +
+
+
{info}
+
+ ); +} + +Navigator.propTypes = { + itinerary: itineraryShape.isRequired, + focusToLeg: PropTypes.func.isRequired, + setNavigation: PropTypes.func.isRequired, + /* + focusToPoint: PropTypes.func.isRequired, + relayEnvironment: relayShape.isRequired, + */ +}; + +Navigator.contextTypes = { + intl: intlShape.isRequired, +}; + +const withRelay = createFragmentContainer(Navigator, { + itinerary: graphql` + fragment Navigator_itinerary on Itinerary { + start + end + legs { + mode + transitLeg + start { + scheduledTime + estimated { + time + } + } + end { + scheduledTime + estimated { + time + } + } + legGeometry { + points + } + from { + lat + lon + } + to { + lat + lon + stop { + name + code + platformCode + vehicleMode + } + vehicleParking { + name + } + vehicleRentalStation { + name + rentalNetwork { + networkId + } + availableVehicles { + total + } + } + rentalVehicle { + rentalNetwork { + networkId + url + } + } + } + } + } + `, +}); + +export { Navigator as Component, withRelay as default }; diff --git a/app/component/itinerary/NearestQuery.js b/app/component/itinerary/NearestQuery.js new file mode 100644 index 0000000000..b9ede44a94 --- /dev/null +++ b/app/component/itinerary/NearestQuery.js @@ -0,0 +1,55 @@ +import { graphql } from 'react-relay'; + +const nearestQuery = graphql` + query NearestQuery( + $lat: Float! + $lon: Float! + $filterByPlaceTypes: [FilterPlaceType] + $filterByModes: [Mode] + $first: Int! + $after: String + $maxResults: Int! + $maxDistance: Int! + $filterByNetwork: [String!] + ) { + viewer { + nearest( + lat: $lat + lon: $lon + filterByPlaceTypes: $filterByPlaceTypes + filterByModes: $filterByModes + first: $first + after: $after + maxResults: $maxResults + maxDistance: $maxDistance + filterByNetwork: $filterByNetwork + ) @connection(key: "StopsNearYouContainer_nearest") { + edges { + node { + distance + place { + __typename + ... on RentalVehicle { + lat + lon + name + vehicleId + rentalUris { + android + ios + web + } + rentalNetwork { + networkId + url + } + } + } + } + } + } + } + } +`; + +export default nearestQuery; diff --git a/app/component/itinerary/OriginDestinationBar.js b/app/component/itinerary/OriginDestinationBar.js index 5e459bab72..0af108f125 100644 --- a/app/component/itinerary/OriginDestinationBar.js +++ b/app/component/itinerary/OriginDestinationBar.js @@ -130,7 +130,7 @@ class OriginDestinationBar extends React.Component { props.locationState, ); const desktopTargets = ['Locations', 'CurrentPosition', 'Stops']; - if (useCitybikes(config.cityBike?.networks, config)) { + if (useCitybikes(config.vehicleRental?.networks, config)) { desktopTargets.push('VehicleRentalStations'); } const mobileTargets = [...desktopTargets, 'MapPosition']; diff --git a/app/component/itinerary/PlanConnection.js b/app/component/itinerary/PlanConnection.js index b8cab9adaf..191711fe9a 100644 --- a/app/component/itinerary/PlanConnection.js +++ b/app/component/itinerary/PlanConnection.js @@ -59,6 +59,7 @@ const planConnection = graphql` ...ItineraryListContainer_planEdges node { ...ItineraryDetails_itinerary + ...Navigator_itinerary duration walkDistance emissionsPerPerson { @@ -91,6 +92,9 @@ const planConnection = graphql` vehicleRentalStation { stationId } + rentalVehicle { + vehicleId + } } to { lat diff --git a/app/component/itinerary/ScooterLinkContainer.js b/app/component/itinerary/ScooterLinkContainer.js new file mode 100644 index 0000000000..d2e39a69c6 --- /dev/null +++ b/app/component/itinerary/ScooterLinkContainer.js @@ -0,0 +1,102 @@ +import { FormattedMessage, intlShape } from 'react-intl'; +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import connectToStores from 'fluxible-addons-react/connectToStores'; +import { configShape, rentalVehicleShape } from '../../util/shapes'; +import { + getRentalNetworkConfig, + getRentalNetworkIcon, + getRentalVehicleLink, + useDeepLink, +} from '../../util/vehicleRentalUtils'; + +import withBreakpoint from '../../util/withBreakpoint'; +import Icon from '../Icon'; +import ExternalLink from '../ExternalLink'; + +function ScooterLinkContainer( + { rentalVehicle, language, mobileReturn }, + { config }, +) { + const network = rentalVehicle.rentalNetwork.networkId; + const networkConfig = getRentalNetworkConfig(network, config); + const vehicleIcon = getRentalNetworkIcon(networkConfig); + const scooterHeadsign = ( + + ); + const rentalVehicleLink = getRentalVehicleLink(rentalVehicle, networkConfig); + const onClick = rentalVehicleLink.startsWith('http') + ? () => {} + : () => useDeepLink(rentalVehicleLink, rentalVehicle.rentalNetwork.url); + + return ( +
+
+
+
+ +
+
+ + + {scooterHeadsign} + + +
+
+
+ + + +
+
+
+ ); +} + +ScooterLinkContainer.propTypes = { + rentalVehicle: rentalVehicleShape.isRequired, + language: PropTypes.string.isRequired, + mobileReturn: PropTypes.bool, +}; + +ScooterLinkContainer.defaultProps = { + mobileReturn: false, +}; + +ScooterLinkContainer.contextTypes = { + config: configShape.isRequired, + intl: intlShape.isRequired, +}; +const ScooterLinkWithBreakpoint = withBreakpoint(ScooterLinkContainer); + +const connectedComponent = connectToStores( + ScooterLinkWithBreakpoint, + ['PreferencesStore'], + ({ getStore }) => { + const language = getStore('PreferencesStore').getLanguage(); + return { language }; + }, +); + +export { connectedComponent as default, ScooterLinkContainer as Component }; diff --git a/app/component/itinerary/StartNavi.js b/app/component/itinerary/StartNavi.js new file mode 100644 index 0000000000..b3c7fcdcac --- /dev/null +++ b/app/component/itinerary/StartNavi.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import Icon from '../Icon'; + +/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ +export default function StartNavi({ setNavigation }) { + return ( +
+
+ + +
+ +
+ ); +} + +StartNavi.propTypes = { + setNavigation: PropTypes.func.isRequired, +}; diff --git a/app/component/itinerary/TicketInformation.js b/app/component/itinerary/TicketInformation.js index ce1da6caa4..ee8bc052b9 100644 --- a/app/component/itinerary/TicketInformation.js +++ b/app/component/itinerary/TicketInformation.js @@ -59,7 +59,7 @@ export default function TicketInformation(
) : ( (config.ticketLink && ( - +
{config.useTicketIcons ? renderZoneTicket(fare.ticketName, alternativeFares) diff --git a/app/component/itinerary/TransitLeg.js b/app/component/itinerary/TransitLeg.js index ba048073ff..2cfa59d98b 100644 --- a/app/component/itinerary/TransitLeg.js +++ b/app/component/itinerary/TransitLeg.js @@ -361,6 +361,8 @@ class TransitLeg extends React.Component { {createNotification(notification)} diff --git a/app/component/itinerary/VehicleRentalDurationInfo.js b/app/component/itinerary/VehicleRentalDurationInfo.js index 64909be5cf..d4131691ec 100644 --- a/app/component/itinerary/VehicleRentalDurationInfo.js +++ b/app/component/itinerary/VehicleRentalDurationInfo.js @@ -18,10 +18,10 @@ function VehicleRentalDurationInfo(props) { getRentalNetworkConfig(vehicleRentalStationNetwork, config), ); const vehicleRentalStationNetworkDurationInfoLink = - config.cityBike.networks[vehicleRentalStationNetwork] + config.vehicleRental.networks[vehicleRentalStationNetwork] .durationInstructions[lang]; const duration = - config.cityBike.networks[vehicleRentalStationNetwork] + config.vehicleRental.networks[vehicleRentalStationNetwork] .timeBeforeSurcharge / 60; return ( @@ -44,7 +44,11 @@ function VehicleRentalDurationInfo(props) { defaultMessage="" />   - +

@@ -58,7 +62,7 @@ function VehicleRentalDurationInfo(props) { const durationInfoLinks = {}; for (let i = 0; i < networks.length; i++) { durationInfoLinks[networks[i]] = - config.cityBike.networks[networks[i]].durationInstructions[lang]; + config.vehicleRental.networks[networks[i]].durationInstructions[lang]; } return ( @@ -79,11 +83,13 @@ function VehicleRentalDurationInfo(props) { <> - {config.cityBike.networks[value].name[lang]} + {config.vehicleRental.networks[value].name[lang]} {' - '} › diff --git a/app/component/itinerary/VehicleRentalLeg.js b/app/component/itinerary/VehicleRentalLeg.js index 781656d6ed..81e0db0629 100644 --- a/app/component/itinerary/VehicleRentalLeg.js +++ b/app/component/itinerary/VehicleRentalLeg.js @@ -15,7 +15,6 @@ import { getRentalNetworkConfig, getRentalNetworkIcon, hasVehicleRentalCode, - getRentalVehicleLink, } from '../../util/vehicleRentalUtils'; import withBreakpoint from '../../util/withBreakpoint'; @@ -25,8 +24,8 @@ import { getVehicleAvailabilityTextColor, getVehicleAvailabilityIndicatorColor, } from '../../util/legUtils'; -import ExternalLink from '../ExternalLink'; import { getIdWithoutFeed } from '../../util/feedScopedIdUtils'; +import ScooterLinkContainer from './ScooterLinkContainer'; function VehicleRentalLeg( { @@ -38,19 +37,28 @@ function VehicleRentalLeg( rentalVehicle, language, nextLegMode, + nearestScooters, }, { config, intl }, ) { if (!vehicleRentalStation && !isScooter) { return null; } - const network = vehicleRentalStation?.network || rentalVehicle?.network; + const network = + vehicleRentalStation?.rentalNetwork.networkId || + rentalVehicle?.rentalNetwork.networkId; // eslint-disable-next-line no-nested-ternary const rentMessageId = isScooter ? 'rent-e-scooter-at' : 'rent-cycle-at'; const returnMessageId = isScooter ? 'return-e-scooter-to' : 'return-cycle-to'; const id = returnBike ? returnMessageId : rentMessageId; const legDescription = ( - + ); @@ -70,122 +78,95 @@ function VehicleRentalLeg( : null; const mobileReturn = breakpoint === 'small' && returnBike; const vehicleCapacity = vehicleRentalStation - ? getVehicleCapacity(config, vehicleRentalStation?.network) + ? getVehicleCapacity(config, vehicleRentalStation?.rentalNetwork.networkId) : null; - const scooterHeadsign = ( - - ); const rentalStationLink = `/${PREFIX_BIKESTATIONS}/${vehicleRentalStation?.stationId}`; - const rentalVehicleLink = getRentalVehicleLink( - rentalVehicle, - network, - networkConfig, - ); return ( <> {(!isScooter || (nextLegMode !== 'WALK' && isScooter)) && (
{legDescription}
)} - {(!isScooter || (isScooter && !returnBike)) && ( + {vehicleRentalStation && (
- {isScooter ? ( - - ) : ( - - )} +
- {!isScooter && ( - - {stationName} - - )} - {isScooter && ( - - {scooterHeadsign} - - )} + + {stationName} + + + + {intl.formatMessage({ + id: 'citybike-station-no-id', + defaultMessage: 'Bike station', + })} + {vehicleRentalStation && + hasVehicleRentalCode(vehicleRentalStation.stationId) && ( + + {getIdWithoutFeed(vehicleRentalStation?.stationId)} + + )} - - {!isScooter && ( - - {intl.formatMessage({ - id: 'citybike-station-no-id', - defaultMessage: 'Bike station', - })} - {vehicleRentalStation && - hasVehicleRentalCode(vehicleRentalStation.stationId) && ( - - {getIdWithoutFeed(vehicleRentalStation?.stationId)} - - )} - - )}
- {isScooter ? ( -
- - - -
- ) : ( -
- - - -
- )} +
+ + + +
)} + {rentalVehicle && !returnBike && isScooter && ( + + )} + {nearestScooters && + !returnBike && + isScooter && + nearestScooters.map(nearestScooter => { + return ( + + ); + })} ); } @@ -199,6 +180,7 @@ VehicleRentalLeg.propTypes = { rentalVehicle: rentalVehicleShape, language: PropTypes.string.isRequired, nextLegMode: PropTypes.string, + nearestScooters: PropTypes.arrayOf(rentalVehicleShape), }; VehicleRentalLeg.defaultProps = { @@ -209,6 +191,7 @@ VehicleRentalLeg.defaultProps = { rentalVehicle: undefined, stationName: undefined, nextLegMode: undefined, + nearestScooters: [], }; VehicleRentalLeg.contextTypes = { diff --git a/app/component/itinerary/ViaLeg.js b/app/component/itinerary/ViaLeg.js index 8af671d0db..3550011c46 100644 --- a/app/component/itinerary/ViaLeg.js +++ b/app/component/itinerary/ViaLeg.js @@ -4,7 +4,7 @@ import { FormattedMessage, intlShape } from 'react-intl'; import { legShape, legTimeShape, configShape } from '../../util/shapes'; import { displayDistance } from '../../util/geo-utils'; import { durationToString } from '../../util/timeUtils'; -import { legTime, legTimeStr } from '../../util/legUtils'; +import { legTime, legTimeStr, legDestination } from '../../util/legUtils'; import ItineraryCircleLineWithIcon from './ItineraryCircleLineWithIcon'; import ItineraryMapAction from './ItineraryMapAction'; import { splitStringToAddressAndPlace } from '../../util/otpStrings'; @@ -70,12 +70,7 @@ function ViaLeg(props, { config, intl }) { } values={{ time: startTime, - to: intl.formatMessage({ - id: `modes.to-${ - props.leg.to.stop?.vehicleMode.toLowerCase() || 'place' - }`, - defaultMessage: 'modes.to-stop', - }), + to: legDestination(intl, props.leg), distance, origin: props.leg.from ? props.leg.from.name : '', destination: props.leg.to ? props.leg.to.name : '', diff --git a/app/component/itinerary/WalkLeg.js b/app/component/itinerary/WalkLeg.js index abd044dcf9..963d177d5e 100644 --- a/app/component/itinerary/WalkLeg.js +++ b/app/component/itinerary/WalkLeg.js @@ -4,7 +4,7 @@ import React from 'react'; import { FormattedMessage, intlShape } from 'react-intl'; import Link from 'found/Link'; import { legShape, configShape } from '../../util/shapes'; -import { legTime, legTimeStr } from '../../util/legUtils'; +import { legTime, legTimeStr, legDestination } from '../../util/legUtils'; import Icon from '../Icon'; import ItineraryMapAction from './ItineraryMapAction'; import ItineraryCircleLineWithIcon from './ItineraryCircleLineWithIcon'; @@ -42,32 +42,39 @@ function WalkLeg( const isFirstLeg = i => i === 0; const [address, place] = splitStringToAddressAndPlace(leg[toOrFrom].name); const network = - previousLeg?.[toOrFrom]?.vehicleRentalStation?.network || - previousLeg?.[toOrFrom]?.rentalVehicle?.network; + previousLeg?.[toOrFrom]?.vehicleRentalStation?.rentalNetwork.networkId || + previousLeg?.[toOrFrom]?.rentalVehicle?.rentalNetwork.networkId; const networkType = getRentalNetworkConfig( previousLeg?.rentedBike && network, config, ).type; const isScooter = networkType === RentalNetworkType.Scooter; - const returnNotice = - previousLeg && previousLeg.rentedBike && !isScooter ? ( - - ) : null; + const returnNotice = previousLeg?.rentedBike ? ( + + ) : null; let appendClass; if (returnNotice) { - appendClass = 'return-citybike'; + appendClass = !isScooter ? 'return-citybike' : ''; } + const destinationLabel = + leg.to?.name?.toLowerCase() === 'scooter' + ? intl.formatMessage({ + id: 'e-scooter', + defaultMessage: 'scooter', + }) + : leg.to?.name; + return (
@@ -76,16 +83,11 @@ function WalkLeg( id="itinerary-details.walk-leg" values={{ time: legTimeStr(leg.start), - to: intl.formatMessage({ - id: `modes.to-${ - leg.to.stop?.vehicleMode?.toLowerCase() || 'place' - }`, - defaultMessage: 'modes.to-stop', - }), + to: legDestination(intl, leg), distance, duration, origin: leg[toOrFrom] ? leg[toOrFrom].name : '', - destination: leg.to ? leg.to.name : '', + destination: leg.to ? destinationLabel : '', }} /> @@ -132,11 +134,12 @@ function WalkLeg(
) : (
{leg[toOrFrom].stop ? ( @@ -165,13 +168,18 @@ function WalkLeg( ) : (
{returnNotice ? ( - + <> +
+ + ) : ( leg[toOrFrom].name )} diff --git a/app/component/itinerary/WeatherDetailsPopup.js b/app/component/itinerary/WeatherDetailsPopup.js index a4ab27e3b5..4c28016dea 100644 --- a/app/component/itinerary/WeatherDetailsPopup.js +++ b/app/component/itinerary/WeatherDetailsPopup.js @@ -50,7 +50,7 @@ WeatherDetailsPopup.propTypes = { weatherData: PropTypes.shape({ temperature: PropTypes.number, iconId: PropTypes.number, - time: PropTypes.number, + time: PropTypes.string, }).isRequired, onClose: PropTypes.func.isRequired, }; diff --git a/app/component/itinerary/customizesearch/MinTransferTimeSection.js b/app/component/itinerary/customizesearch/MinTransferTimeSection.js new file mode 100644 index 0000000000..def515d94c --- /dev/null +++ b/app/component/itinerary/customizesearch/MinTransferTimeSection.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { intlShape } from 'react-intl'; +import { saveRoutingSettings } from '../../../action/SearchSettingsActions'; +import { settingsShape, minTransferTimeShape } from '../../../util/shapes'; +import { addAnalyticsEvent } from '../../../util/analyticsUtils'; +import SearchSettingsDropdown from './SearchSettingsDropdown'; + +const MinTransferTimeSection = ( + { currentSettings, defaultSettings, minTransferTimeOptions }, + { intl, executeAction }, + options = minTransferTimeOptions, + currentSelection = options.find( + option => option.value === currentSettings.minTransferTime, + ), +) => ( +
+ { + executeAction(saveRoutingSettings, { + minTransferTime: value, + }); + addAnalyticsEvent({ + category: 'ItinerarySettings', + action: 'ChangeMinTransferTime', + name: value, + }); + }} + options={options} + labelText={intl.formatMessage({ id: 'min-transfer-time' })} + highlightDefaulValue + formatOptions + name="minTransferTime" + translateLabels={false} + /> +
+); + +MinTransferTimeSection.propTypes = { + defaultSettings: settingsShape.isRequired, + minTransferTimeOptions: minTransferTimeShape.isRequired, + currentSettings: settingsShape.isRequired, +}; + +MinTransferTimeSection.contextTypes = { + intl: intlShape.isRequired, + executeAction: PropTypes.func.isRequired, +}; + +export default MinTransferTimeSection; diff --git a/app/component/itinerary/itinerary.scss b/app/component/itinerary/itinerary.scss index 8fb91faa46..848b819162 100644 --- a/app/component/itinerary/itinerary.scss +++ b/app/component/itinerary/itinerary.scss @@ -1171,21 +1171,21 @@ $itinerary-tab-switch-height: 48px; &.scooter { border-left: 6px solid; border-radius: 3px; - height: 54%; + height: calc(58% + var(--scooter-amount)); left: 8px; &.settings { - height: 58%; + height: calc(56% + var(--scooter-amount)); } &.bottom { - height: 25%; - top: 82%; + height: calc(23% + var(--scooter-amount) * 1.7); overflow: hidden; + top: calc(90% - var(--scooter-amount) * 3.4); &.settings { - height: 30%; - top: 75%; + height: calc(30% + var(--scooter-amount)); + top: calc(75% - var(--scooter-amount)); } } } @@ -1549,14 +1549,22 @@ $itinerary-tab-switch-height: 48px; .itinerary-leg-row-bike { display: flex; flex-direction: column; - min-height: 34px; color: #333; - padding: 0.625em 0; + + &.withPadding { + padding: 0.625em 0; + } .citybike-leg-header { font-size: 15px; font-weight: $font-weight-book; max-height: 33px; + + &.scooter-return { + min-height: 40px; + align-items: center; + display: flex; + } } } @@ -1571,6 +1579,10 @@ $itinerary-tab-switch-height: 48px; color: #333; font-size: 0.9375rem; align-items: center; + + &.scooter { + min-height: 40px; + } } .itinerary-leg-first-row { diff --git a/app/component/itinerary/mobile-ticket-purchase-information.scss b/app/component/itinerary/mobile-ticket-purchase-information.scss index 85a4282381..737f9ca687 100644 --- a/app/component/itinerary/mobile-ticket-purchase-information.scss +++ b/app/component/itinerary/mobile-ticket-purchase-information.scss @@ -56,8 +56,8 @@ justify-content: space-evenly; align-items: center; padding: 3px 10px 5px 10px; - min-width: 10.9em; - height: 2.4em; + width: 140px; + height: 40px; right: 15px; top: 25px; background: #fff; diff --git a/app/component/itinerary/navigator.scss b/app/component/itinerary/navigator.scss new file mode 100644 index 0000000000..e542973727 --- /dev/null +++ b/app/component/itinerary/navigator.scss @@ -0,0 +1,118 @@ +.navi-start-container { + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + flex-direction: row; + background-color: #e5f2fa; + color: $black; + margin-right: 10px; + margin-left: 10px; + margin-top: 16px; + border-radius: 10px; +} + +.navi-start-leftgroup { + display: flex; + flex-direction: row; + margin-top: 5px; + margin-left: 10px; + font-size: $font-size-xsmall; + font-weight: $font-weight-book; +} + +.navi-start-icon { + margin-right: 12px; + margin-left: 5px; +} + +.navi-start-button { + margin-right: 16px; + background-color: $primary-color; + color: $white; + + &:hover { + background-color: $standalone-btn-hover-color; + color: $white; + } + + font-size: $font-size-normal; + border-radius: 20px; + width: 140px; + height: 40px; + text-align: center; +} + +.navigator { + .navigator-top-section { + font-size: $font-size-xsmall; + text-align: center; + margin-top: 10px; + + .close-navigator { + position: absolute; + right: 0; + padding-right: 20px; + color: $secondary-color; + } + } + + .divider { + width: 100%; + height: 1px; + border-top: 1px solid #ddd; + margin-bottom: 10px; + margin-top: 10px; + } + + .info { + margin: 15px 20px 0 20px; + width: 100%; + + .navileg-goto { + display: flex; + flex-direction: row; + align-items: center; + + .navileg-mode { + width: 32px; + height: 32px; + margin-right: 12px; + } + } + + .navileg-destination { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 10px; + font-size: $font-size-xsmall; + font-weight: $font-weight-book; + + .navi-left-bar { + margin-left: 10px; + margin-right: 10px; + width: 8px; + height: 80px; + border-radius: 4px; + background-color: #e5f2fa; + } + + .navileg-destination-details { + display: flex; + flex-direction: column; + + .navi-destination-icon { + width: 24px; + height: 24px; + } + + .navileg-focus { + font-weight: $font-weight-medium; + color: $primary-color; + margin-top: 8px; + } + } + } + } +} diff --git a/app/component/map/ItineraryLine.js b/app/component/map/ItineraryLine.js index 41465382ec..1d10b7046e 100644 --- a/app/component/map/ItineraryLine.js +++ b/app/component/map/ItineraryLine.js @@ -93,8 +93,8 @@ class ItineraryLine extends React.Component { leg.from.vehicleRentalStation?.stationId || leg.from.rentalVehicle?.vehicleId; const rentalNetwork = - leg.from.vehicleRentalStation?.network || - leg.from.rentalVehicle?.network; + leg.from.vehicleRentalStation?.rentalNetwork.networkId || + leg.from.rentalVehicle?.rentalNetwork.networkId; if (interliningLegs.length > 0) { // merge the geometries of legs where user can wait in the vehicle and find the middle point @@ -276,14 +276,18 @@ export default createFragmentContainer(ItineraryLine, { lat lon stationId - network + rentalNetwork { + networkId + } availableVehicles { total } } rentalVehicle { vehicleId - network + rentalNetwork { + networkId + } } stop { gtfsId @@ -300,7 +304,9 @@ export default createFragmentContainer(ItineraryLine, { lat lon stationId - network + rentalNetwork { + networkId + } availableVehicles { total } diff --git a/app/component/map/MapWithTracking.js b/app/component/map/MapWithTracking.js index 0378f44cd3..14928f8f7c 100644 --- a/app/component/map/MapWithTracking.js +++ b/app/component/map/MapWithTracking.js @@ -98,7 +98,7 @@ class MapWithTrackingStateHandler extends React.Component { mapRef: undefined, children: undefined, leafletObjs: undefined, - mapTracking: false, + mapTracking: undefined, onStartNavigation: undefined, onEndNavigation: undefined, onMapTracking: undefined, diff --git a/app/component/map/StopPageMap.js b/app/component/map/StopPageMap.js index 8842e97922..f725f62445 100644 --- a/app/component/map/StopPageMap.js +++ b/app/component/map/StopPageMap.js @@ -49,7 +49,15 @@ const getModeFromProps = props => { }; function StopPageMap( - { stop, breakpoint, currentTime, locationState, mapLayers, mapLayerOptions }, + { + stop, + breakpoint, + currentTime, + locationState, + mapLayers, + mapLayerOptions, + stopName, + }, { config, match }, ) { if (!stop) { @@ -137,7 +145,7 @@ function StopPageMap( if (breakpoint === 'large') { leafletObjs.push( - + , ); if (config.useCookiesPrompt) { @@ -225,11 +233,13 @@ StopPageMap.propTypes = { mapLayers: mapLayerShape.isRequired, mapLayerOptions: mapLayerOptionsShape.isRequired, parkType: PropTypes.string, + stopName: PropTypes.node, }; StopPageMap.defaultProps = { stop: undefined, parkType: undefined, + stopName: undefined, }; const componentWithBreakpoint = withBreakpoint(StopPageMap); diff --git a/app/component/map/StopsNearYouMap.js b/app/component/map/StopsNearYouMap.js index 39f88a2985..64ca638c3a 100644 --- a/app/component/map/StopsNearYouMap.js +++ b/app/component/map/StopsNearYouMap.js @@ -361,11 +361,11 @@ function StopsNearYouMap( let sortedEdges; if (!isTransitMode) { const withNetworks = stopsNearYou.nearest.edges.filter(edge => { - return !!edge.node.place?.network; + return !!edge.node.place?.rentalNetwork?.networkId; }); const filteredCityBikeEdges = withNetworks.filter(pattern => { return getDefaultNetworks(context.config).includes( - pattern.node.place?.network, + pattern.node.place?.rentalNetwork.networkId, ); }); sortedEdges = filteredCityBikeEdges diff --git a/app/component/map/non-tile-layer/VehicleMarkerContainer.js b/app/component/map/non-tile-layer/VehicleMarkerContainer.js index b24ad0f5b7..257d45a1f1 100644 --- a/app/component/map/non-tile-layer/VehicleMarkerContainer.js +++ b/app/component/map/non-tile-layer/VehicleMarkerContainer.js @@ -35,7 +35,7 @@ class VehicleMarkerContainer extends React.Component { render() { if ( this.props.leaflet.map.getZoom() < - this.context.config.cityBike.cityBikeMinZoom + this.context.config.vehicleRental.cityBikeMinZoom ) { return false; } @@ -51,7 +51,9 @@ class VehicleMarkerContainer extends React.Component { lat lon stationId - network + rentalNetwork { + networkId + } availableVehicles { total } diff --git a/app/component/map/tile-layer/MarkerSelectPopup.js b/app/component/map/tile-layer/MarkerSelectPopup.js index 3ed0c47dc9..c8f6c839ee 100644 --- a/app/component/map/tile-layer/MarkerSelectPopup.js +++ b/app/component/map/tile-layer/MarkerSelectPopup.js @@ -58,7 +58,7 @@ function MarkerSelectPopup(props, { intl }) { prefix={PREFIX_RENTALVEHICLES} id={option.feature.properties.scooterId} desc={intl.formatMessage({ - id: 'scooter', + id: 'e-scooter', defaultMessage: 'scooter', })} isScooter @@ -73,7 +73,7 @@ function MarkerSelectPopup(props, { intl }) { key={`scooter:${option.feature.properties.id}`} prefix={PREFIX_RENTALVEHICLES} desc={intl.formatMessage({ - id: 'scooter', + id: 'e-scooter', defaultMessage: 'scooter', })} icon="icon-icon_scooter-lollipop" @@ -90,7 +90,7 @@ function MarkerSelectPopup(props, { intl }) { const { vehicleParking } = option.feature.properties; if (Array.isArray(vehicleParking) && vehicleParking.length > 0) { return ( - + {vehicleParking.map(parking => { return getRowForParking(parking, option.layer); })} diff --git a/app/component/map/tile-layer/RentalVehicles.js b/app/component/map/tile-layer/RentalVehicles.js index f0160db21a..8b15d5f442 100644 --- a/app/component/map/tile-layer/RentalVehicles.js +++ b/app/component/map/tile-layer/RentalVehicles.js @@ -37,7 +37,7 @@ class RentalVehicles { fetchAndDraw = lang => { const zoomedIn = - this.tile.coords.z > this.config.cityBike.cityBikeSmallIconZoom; + this.tile.coords.z > this.config.vehicleRental.cityBikeSmallIconZoom; const baseUrl = getLayerBaseUrl(this.config.URL.RENTAL_VEHICLE_MAP, lang); const tileUrl = `${baseUrl}${ this.tile.coords.z + (this.tile.props.zoomOffset || 0) @@ -72,7 +72,7 @@ class RentalVehicles { this.shouldShowRentalVehicle( feature.properties.id, feature.properties.network, - feature.properties.isDisabled, + feature.properties.pickupAllowed, feature.properties.formFactor, ) ) { @@ -163,14 +163,14 @@ class RentalVehicles { ); }; - shouldShowRentalVehicle = (id, network, isDisabled, formFactor) => + shouldShowRentalVehicle = (id, network, pickupAllowed, formFactor) => (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && (!network || - (this.config.cityBike.networks[network].enabled && - this.config.cityBike.networks[network].showRentalVehicles && - this.config.cityBike.networks[network].type === + (this.config.vehicleRental.networks[network].enabled && + this.config.vehicleRental.networks[network].showRentalVehicles && + this.config.vehicleRental.networks[network].type === formFactor.toLowerCase())) && - !isDisabled; + pickupAllowed; static getName = () => 'scooter'; diff --git a/app/component/map/tile-layer/VehicleRentalStations.js b/app/component/map/tile-layer/VehicleRentalStations.js index a58760aa8d..ff8d8dec4b 100644 --- a/app/component/map/tile-layer/VehicleRentalStations.js +++ b/app/component/map/tile-layer/VehicleRentalStations.js @@ -51,7 +51,7 @@ class VehicleRentalStations { fetchAndDraw = lang => { const zoomedIn = - this.tile.coords.z > this.config.cityBike.cityBikeSmallIconZoom; + this.tile.coords.z > this.config.vehicleRental.cityBikeSmallIconZoom; const baseUrl = zoomedIn ? getLayerBaseUrl(this.config.URL.REALTIME_RENTAL_STATION_MAP, lang) : getLayerBaseUrl(this.config.URL.RENTAL_STATION_MAP, lang); @@ -192,11 +192,13 @@ class VehicleRentalStations { }; shouldShowStation = (id, network) => - (this.config.cityBike.networks[network].showRentalStations === undefined || - this.config.cityBike.networks[network].showRentalStations) && + this.config.vehicleRental.networks[network] && + (this.config.vehicleRental.networks[network].showRentalStations === + undefined || + this.config.vehicleRental.networks[network].showRentalStations) && (!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) && !this.tile.objectsToHide.vehicleRentalStations.includes(id) && - showCitybikeNetwork(this.config.cityBike.networks[network]); + showCitybikeNetwork(this.config.vehicleRental.networks[network]); static getName = () => 'citybike'; } diff --git a/app/component/routepage/PatternStopsContainer.js b/app/component/routepage/PatternStopsContainer.js index 0dc4ff7998..d27c66c029 100644 --- a/app/component/routepage/PatternStopsContainer.js +++ b/app/component/routepage/PatternStopsContainer.js @@ -65,7 +65,11 @@ class PatternStopsContainer extends React.PureComponent {
{constantOperationRoutes[routeId][locale].text} - + {constantOperationRoutes[routeId][locale].link} diff --git a/app/component/routepage/RouteNotification.js b/app/component/routepage/RouteNotification.js index 372cb68b07..47f57a82a4 100644 --- a/app/component/routepage/RouteNotification.js +++ b/app/component/routepage/RouteNotification.js @@ -38,6 +38,8 @@ const RouteNotification = (props, context) => { {props.link} diff --git a/app/component/routepage/ScheduleContainer.js b/app/component/routepage/ScheduleContainer.js index f39456db64..b944a2d94f 100644 --- a/app/component/routepage/ScheduleContainer.js +++ b/app/component/routepage/ScheduleContainer.js @@ -717,7 +717,11 @@ class ScheduleContainer extends PureComponent {
{constantOperationRoutes[routeId][locale].text} - + {constantOperationRoutes[routeId][locale].link} diff --git a/app/config.js b/app/config.js index 2edc12113c..8d8a7d102b 100644 --- a/app/config.js +++ b/app/config.js @@ -110,7 +110,7 @@ export function getNamedConfiguration(configName) { const config = baseConfig ? configMerger(baseConfig, additionalConfig) : configMerger(defaultConfig, additionalConfig); - config.cityBike.seasonSet = false; // this is unique per config and should not be inherited + config.vehicleRental.seasonSet = false; // this is unique per config and should not be inherited if (config.useSearchPolygon && config.areaPolygon) { // pass poly as 'lon lat, lon lat, lon lat ...' sequence @@ -164,15 +164,15 @@ export function getNamedConfiguration(configName) { } } if ( - Object.keys(conf.cityBike?.networks || {}).length && + Object.keys(conf.vehicleRental?.networks || {}).length && citybikeSeasonDefinitions?.length && - !conf.cityBike.seasonSet + !conf.vehicleRental.seasonSet ) { - conf.cityBike.seasonSet = true; + conf.vehicleRental.seasonSet = true; - if (conf.cityBike.useAllSeasons) { + if (conf.vehicleRental.useAllSeasons) { citybikeSeasonDefinitions.forEach(seasonDef => { - const confCitybike = conf.cityBike.networks[seasonDef.networkName]; + const confCitybike = conf.vehicleRental.networks[seasonDef.networkName]; if (confCitybike) { confCitybike.enabled = seasonDef.enabled; confCitybike.season = seasonDef.season; @@ -183,7 +183,7 @@ export function getNamedConfiguration(configName) { seasonDef => configName === seasonDef.configName, ); seasonDefinitions.forEach(seasonDef => { - const confCitybike = conf.cityBike.networks[seasonDef.networkName]; + const confCitybike = conf.vehicleRental.networks[seasonDef.networkName]; confCitybike.enabled = seasonDef.enabled; confCitybike.season = seasonDef.season; }); diff --git a/app/configurations/config.default.js b/app/configurations/config.default.js index 0b6265ee09..68611b7dd8 100644 --- a/app/configurations/config.default.js +++ b/app/configurations/config.default.js @@ -308,7 +308,7 @@ export default { showStopMarkerPopupOnMobile: true, showScaleBar: true, attribution: - '© OpenStreetMap', + '© OpenStreetMap', useModeIconsInNonTileLayer: false, // areBounds is for keeping map and user inside given area @@ -332,7 +332,7 @@ export default { locationAware: true, }, - cityBike: { + vehicleRental: { // Config for map features. NOTE: availability for routing is controlled by // transportModes.citybike.availableForSelection showFullInfo: false, @@ -347,6 +347,8 @@ export default { sv: 'Köp ett abonnemang för en dag, en vecka eller för en hel säsong', en: 'Buy a daily, weekly or season pass', }, + maxNearbyRentalVehicleAmount: 5, + maxDistanceToRentalVehiclesInMeters: 100, }, // Lowest level for stops and terminals are rendered @@ -365,7 +367,6 @@ export default { default: 18, }, - appBarLink: { name: 'Digitransit', href: 'https://www.digitransit.fi/' }, appBarStyle: 'default', colors: { @@ -419,17 +420,6 @@ export default { }, hideExternalOperator: () => false, - // Ticket information feature toggle - showTicketInformation: false, - ticketInformation: { - // This is the name of the primary agency operating in the area. - // It is used when a ticket price cannot be shown to the user, indicating - // that the primary agency is not responsible for ticketing. - /* - primaryAgencyName: ..., - */ - }, - useTicketIcons: false, // Control what transport modes that should be possible to select in the UI @@ -596,7 +586,7 @@ export default { { header: 'Tietolähteet', paragraphs: [ - 'Tiedot perustuvat joukkoliikenneviranomaisten, liikennöitsijöiden, VR:n ja Finavian toimittamiin tietoihin. Tietolähteinä hyödynnetään Fintrafficin liikkumisen tietopalveluita, erityisesti liikkumispalveluiden avointa yhteyspistettä Finap-palvelua. Kartat, tiedot kaduista, rakennuksista, pysäkkien sijainnista ynnä muusta tarjoaa © OpenStreetMap contributors. Osoitetiedot tuodaan Digi- ja väestötietoviraston rakennusten osoitetietokannasta.', + 'Tiedot perustuvat joukkoliikenneviranomaisten, liikennöitsijöiden, VR:n ja Finavian toimittamiin tietoihin. Tietolähteinä hyödynnetään Fintrafficin liikkumisen tietopalveluita, erityisesti liikkumispalveluiden avointa yhteyspistettä Finap-palvelua. Kartat, tiedot kaduista, rakennuksista, pysäkkien sijainnista ynnä muusta tarjoaa © OpenStreetMap contributors. Osoitetiedot tuodaan Digi- ja väestötietoviraston rakennusten osoitetietokannasta.', ], }, ], @@ -617,7 +607,7 @@ export default { { header: 'Datakällor', paragraphs: [ - 'Tjänsteinformationen baseras på information om kollektivtrafiken som tillhandahålls av kollektivtrafikmyndigheter, trafik operatörer, VR och Finavia. Fintraffics mobilitetsinformationstjänster används som datakällor, särskilt National Access Point för mobilitetstjänster FINAP Kartor, information om gator, byggnader, hållplatser och mer tillhandahålls av © OpenStreetMap-bidragsgivare. Adressuppgifter importeras från adressdatabasen till Myndigheten för Digitalisering och Befolkninsdata (DVV).', + 'Tjänsteinformationen baseras på information om kollektivtrafiken som tillhandahålls av kollektivtrafikmyndigheter, trafik operatörer, VR och Finavia. Fintraffics mobilitetsinformationstjänster används som datakällor, särskilt National Access Point för mobilitetstjänster FINAP Kartor, information om gator, byggnader, hållplatser och mer tillhandahålls av © OpenStreetMap-bidragsgivare. Adressuppgifter importeras från adressdatabasen till Myndigheten för Digitalisering och Befolkninsdata (DVV).', ], }, ], @@ -638,7 +628,7 @@ export default { { header: 'Data sources', paragraphs: [ - "The service information is based on public transport route information provided by public transport authorities, operators, VR and Finavia. Fintraffic's mobility information services are used as data sources, especially National Access Point for mobility services FINAP. Maps, information about streets, buildings, bus stop locations and more is provided by © OpenStreetMap contributors. Address information is imported from the address database of the buildings of the Digital and Population Data Services Agency (DVV).", + "The service information is based on public transport route information provided by public transport authorities, operators, VR and Finavia. Fintraffic's mobility information services are used as data sources, especially National Access Point for mobility services FINAP. Maps, information about streets, buildings, bus stop locations and more is provided by © OpenStreetMap contributors. Address information is imported from the address database of the buildings of the Digital and Population Data Services Agency (DVV).", ], }, ], diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js index efbb7e0e7a..9006b58903 100644 --- a/app/configurations/config.hsl.js +++ b/app/configurations/config.hsl.js @@ -299,8 +299,10 @@ export default { hideExternalOperator: agency => agency.name === 'Helsingin seudun liikenne', showTicketInformation: true, - ticketInformation: { - primaryAgencyName: 'HSL', + primaryAgencyName: { + fi: 'HSL', + sv: 'HRT', + en: 'HSL', }, maxNearbyStopAmount: 5, @@ -399,8 +401,6 @@ export default { showLayerSelector: false, showStopMarkerPopupOnMobile: false, showScaleBar: true, - attribution: - '© OpenStreetMap', // areBounds is for keeping map and user inside given area // HSL region + Lahti areaBounds: { @@ -430,7 +430,7 @@ export default { localStorageEmitter, - cityBike: { + vehicleRental: { minZoomStopsNearYou: 10, showFullInfo: true, networks: { @@ -491,7 +491,7 @@ export default { timeBeforeSurcharge: 120 * 60, showRentalStations: true, }, - bolt: { + bolt_helsinki: { enabled: true, season: { alwaysOn: true, @@ -558,9 +558,10 @@ export default { }, showSimilarRoutesOnRouteDropDown: true, - useRealtimeTravellerCapacities: true, + navigation: true, + stopCard: { header: { virtualMonitorBaseUrl: 'https://omatnaytot.hsl.fi/', @@ -728,4 +729,6 @@ export default { }, }, }, + + startSearchFromUserLocation: true, }; diff --git a/app/configurations/config.kela.js b/app/configurations/config.kela.js index f4f41296c8..1306c3fd20 100644 --- a/app/configurations/config.kela.js +++ b/app/configurations/config.kela.js @@ -1,12 +1,4 @@ /* eslint-disable prefer-template */ -import HSLConfig from './config.hsl'; -import TurkuConfig from './config.turku'; -import LappeenrantaConfig from './config.lappeenranta'; -import TampereConfig from './config.tampere'; -import KotkaConfig from './config.kotka'; -import KouvolaConfig from './config.kouvola'; -import KuopioConfig from './config.kuopio'; -import LahtiConfig from './config.lahti'; const matkaConfig = require('./config.matka').default; const HSLParkAndRideUtils = require('../util/ParkAndRideUtils').default.HSL; @@ -39,7 +31,6 @@ export default { feedIds: ['kela', 'matkahuolto', 'lansilinjat'], textLogo: true, logo: null, // override default logo from matka config - appBarLink: false, // override default config - would show Traficom otherwise meta: { description: APP_DESCRIPTION, @@ -105,7 +96,6 @@ export default { availableLanguages: ['fi', 'sv', 'en'], defaultLanguage: 'fi', - hideAppBarLink: true, socialMedia: { title: APP_TITLE, @@ -170,20 +160,6 @@ export default { }, suggestBikeMaxDistance: 2000000, - cityBike: { - useAllSeasons: true, - networks: { - ...HSLConfig.cityBike.networks, - ...TampereConfig.cityBike.networks, - ...TurkuConfig.cityBike.networks, - ...KuopioConfig.cityBike.networks, - ...LahtiConfig.cityBike.networks, - ...LappeenrantaConfig.cityBike.networks, - ...KotkaConfig.cityBike.networks, - ...KouvolaConfig.cityBike.networks, - }, - }, - getAutoSuggestIcons: { citybikes: station => { if ( diff --git a/app/configurations/config.kotka.js b/app/configurations/config.kotka.js index 6d27cb57c5..619c830e26 100644 --- a/app/configurations/config.kotka.js +++ b/app/configurations/config.kotka.js @@ -47,7 +47,7 @@ export default configMerger(walttiConfig, { }, }, - cityBike: { + vehicleRental: { networks: { donkey_kotka: { enabled: true, diff --git a/app/configurations/config.kouvola.js b/app/configurations/config.kouvola.js index 5555665770..e92360c7eb 100644 --- a/app/configurations/config.kouvola.js +++ b/app/configurations/config.kouvola.js @@ -42,7 +42,7 @@ export default configMerger(walttiConfig, { }, }, - cityBike: { + vehicleRental: { networks: { donkey_kouvola: { enabled: true, diff --git a/app/configurations/config.kuopio.js b/app/configurations/config.kuopio.js index 85f061c3a1..baa73725e7 100644 --- a/app/configurations/config.kuopio.js +++ b/app/configurations/config.kuopio.js @@ -36,11 +36,10 @@ export default configMerger(walttiConfig, { feedIds: ['Kuopio', 'digitraffic'], useTicketIcons: true, - showTicketInformation: true, showTicketPrice: true, - ticketInformation: { - primaryAgencyName: 'Kuopion seudun joukkoliikenne', - }, + showTicketInformation: true, + primaryAgencyName: 'Kuopion seudun joukkoliikenne', + ticketLink: 'https://vilkku.kuopio.fi/lipputyypit-hinnat/lippujen-hinnat', searchParams: { @@ -67,7 +66,7 @@ export default configMerger(walttiConfig, { showVehiclesOnStopPage: true, showVehiclesOnItineraryPage: true, - cityBike: { + vehicleRental: { networks: { freebike_kuopio: { enabled: true, diff --git a/app/configurations/config.lahti.js b/app/configurations/config.lahti.js index adb7279766..f8446f358a 100644 --- a/app/configurations/config.lahti.js +++ b/app/configurations/config.lahti.js @@ -148,7 +148,7 @@ export default configMerger(walttiConfig, { }, }, - cityBike: { + vehicleRental: { networks: { freebike_lahti: { enabled: true, diff --git a/app/configurations/config.lappeenranta.js b/app/configurations/config.lappeenranta.js index 58b5a67422..c76d7be1e9 100644 --- a/app/configurations/config.lappeenranta.js +++ b/app/configurations/config.lappeenranta.js @@ -31,7 +31,7 @@ export default configMerger(walttiConfig, { favicon: './app/configurations/images/lappeenranta/lappeenranta-favicon.jpg', - cityBike: { + vehicleRental: { networks: { donkey_lappeenranta: { enabled: true, diff --git a/app/configurations/config.matka.js b/app/configurations/config.matka.js index 0de80ece0f..50dbb42010 100644 --- a/app/configurations/config.matka.js +++ b/app/configurations/config.matka.js @@ -35,7 +35,6 @@ export default { availableLanguages: ['fi', 'sv', 'en'], defaultLanguage: 'fi', - hideAppBarLink: true, socialMedia: { title: APP_TITLE, @@ -165,17 +164,17 @@ export default { }, suggestBikeMaxDistance: 2000000, - cityBike: { + vehicleRental: { useAllSeasons: true, networks: { - ...HSLConfig.cityBike.networks, - ...TampereConfig.cityBike.networks, - ...TurkuConfig.cityBike.networks, - ...KuopioConfig.cityBike.networks, - ...LahtiConfig.cityBike.networks, - ...LappeenrantaConfig.cityBike.networks, - ...KotkaConfig.cityBike.networks, - ...KouvolaConfig.cityBike.networks, + ...HSLConfig.vehicleRental.networks, + ...TampereConfig.vehicleRental.networks, + ...TurkuConfig.vehicleRental.networks, + ...KuopioConfig.vehicleRental.networks, + ...LahtiConfig.vehicleRental.networks, + ...LappeenrantaConfig.vehicleRental.networks, + ...KotkaConfig.vehicleRental.networks, + ...KouvolaConfig.vehicleRental.networks, }, }, @@ -390,4 +389,33 @@ export default { }, // Include both bike and park and bike and public, if bike is enabled includePublicWithBikePlan: true, + + startSearchFromUserLocation: true, + + minTransferTimeSelection: [ + { + title: '1.5 min', + value: 90, + }, + { + title: '3 min', + value: 180, + }, + { + title: '5 min', + value: 300, + }, + { + title: '7 min', + value: 420, + }, + { + title: '10 min', + value: 600, + }, + { + title: '30 min', + value: 1800, + }, + ], }; diff --git a/app/configurations/config.tampere.js b/app/configurations/config.tampere.js index 31ce757b29..88859b1a72 100644 --- a/app/configurations/config.tampere.js +++ b/app/configurations/config.tampere.js @@ -62,9 +62,8 @@ export default configMerger(walttiConfig, { useTicketIcons: true, showTicketInformation: true, - ticketInformation: { - primaryAgencyName: 'Tampereen seudun joukkoliikenne', - }, + primaryAgencyName: 'Tampereen seudun joukkoliikenne', + ticketLink: 'https://www.nysse.fi/liput-ja-hinnat.html', callAgencyInfo: { @@ -232,7 +231,7 @@ export default configMerger(walttiConfig, { tampere: tampereTimetables, }, - cityBike: { + vehicleRental: { networks: { inurba_tampere: { capacity: BIKEAVL_WITHMAX, @@ -290,4 +289,9 @@ export default configMerger(walttiConfig, { }, showTenWeeksOnRouteSchedule: true, + + parkAndRide: { + showParkAndRide: true, + showParkAndRideForBikes: true, + }, }); diff --git a/app/configurations/config.turku.js b/app/configurations/config.turku.js index cafe5e7059..c39021532d 100644 --- a/app/configurations/config.turku.js +++ b/app/configurations/config.turku.js @@ -83,7 +83,7 @@ export default configMerger(walttiConfig, { // Navbar logo logo: 'turku/foli-logo.png', - cityBike: { + vehicleRental: { networks: { donkey_turku: { capacity: BIKEAVL_WITHMAX, diff --git a/app/configurations/config.varely.js b/app/configurations/config.varely.js index fb3121c93b..3a543d0a09 100644 --- a/app/configurations/config.varely.js +++ b/app/configurations/config.varely.js @@ -39,6 +39,8 @@ export default configMerger(walttiConfig, { hover: '#00BF6F', iconColors: { 'mode-bus': colorPrimary, + 'mode-ferry': '#0064f0', + 'mode-ferry-pier': '#666666', }, }, @@ -64,6 +66,12 @@ export default configMerger(walttiConfig, { availableForSelection: true, color: colorPrimary, }, + + ferry: { + availableForSelection: true, + defaultValue: true, + color: '#0064f0', + }, }, searchParams: { diff --git a/app/configurations/config.waltti.js b/app/configurations/config.waltti.js index b24257a877..68853ea50f 100644 --- a/app/configurations/config.waltti.js +++ b/app/configurations/config.waltti.js @@ -41,7 +41,7 @@ export default { stopsMinZoom: 14, - cityBike: {}, + vehicleRental: {}, search: { minimalRegexp: /.+/, @@ -180,8 +180,8 @@ export default { parkingAreaSources: ['liipi'], parkAndRide: { - showParkAndRide: true, - showParkAndRideForBikes: true, + showParkAndRide: false, + showParkAndRideForBikes: false, parkAndRideMinZoom: 13, pageContent: { default: HSLParkAndRideUtils, @@ -242,4 +242,29 @@ export default { ? fareId.substring(fareId.indexOf(':') + 1) : ''; }, + + startSearchFromUserLocation: true, + + minTransferTimeSelection: [ + { + title: '1.5 min', + value: 90, + }, + { + title: '3 min', + value: 180, + }, + { + title: '5 min', + value: 300, + }, + { + title: '7 min', + value: 420, + }, + { + title: '10 min', + value: 600, + }, + ], }; diff --git a/app/store/MapLayerStore.js b/app/store/MapLayerStore.js index 1506c332e8..9a298d7920 100644 --- a/app/store/MapLayerStore.js +++ b/app/store/MapLayerStore.js @@ -38,14 +38,14 @@ class MapLayerStore extends Store { const { config } = dispatcher.getContext(); this.mapLayers.citybike = showRentalVehiclesOfType( - config.cityBike?.networks, + config.vehicleRental?.networks, config, TransportMode.Citybike, ); this.mapLayers.scooter = config.transportModes.scooter?.showIfSelectedForRouting && showRentalVehiclesOfType( - config.cityBike?.networks, + config.vehicleRental?.networks, config, TransportMode.Scooter, ); diff --git a/app/translations.js b/app/translations.js index df44c1e75c..5991ef654e 100644 --- a/app/translations.js +++ b/app/translations.js @@ -1047,16 +1047,15 @@ const translations = { 'distance-between': 'Distance {distance1} m — {distance2} m', 'distance-total': 'Total distance', 'distance-under': 'Distance less than {distance} m', - 'e-scooter': 'Sähköpotkulauta', + 'e-scooter': 'Electric scooter', 'e-scooter-alternative': - 'Entä jos kulkisit osan matkasta sähköpotkulaudalla? {paymentInfo}', + 'How about using an electric scooter for part of your journey? {paymentInfo}', 'e-scooter-or-taxi-alternative': - 'Entä jos kulkisit osan matkasta taksilla tai sähköpotkulaudalla? {paymentInfo}', - 'e-scooter-quantity': 'Sähköpotkulautoja vapaana juuri nyt: ', - 'e-scooter-station': 'Sähköpotkulauta-asema', - 'e-scooters': 'Sähköpotkulaudat', + 'How about using a taxi or an electric scooter for part of your journey? {paymentInfo}', + 'e-scooter-station': 'Electric scooter station', + 'e-scooters': 'Electric scooters', 'e-scooters-available': - 'Alueella sähköpotkulautoja vapaana. Ota sähköpotkulauta käyttöön sovelluksella.', + 'There are electric scooters available in the area. Use an app to unlock the electric scooter.', earlier: 'Earlier departures', 'edit-favourites': 'Edit the location in the Favorites', 'elevation-gained-total': 'Elevation gained', @@ -1067,7 +1066,6 @@ const translations = { 'embedded-search.form-heading': 'Embedding tools', engine_ignition_aid: 'Engine starting aid', english: 'English', - 'exit-vehicle': 'Nouse pois kyydistä ja kävele laiturille', explanations: 'Explanations', 'extra-info': 'More info', 'favourite-failed-text': 'Please try again in a while.', @@ -1081,6 +1079,7 @@ const translations = { 'from-bus': 'bus', 'from-ferry': 'ferry', 'from-rail': 'train', + 'from-scooter-location': 'the scooter location', 'from-station': 'from station', 'from-stop': 'from stop', 'from-subway': 'subway', @@ -1142,7 +1141,7 @@ const translations = { 'itinerary-details.route-has-unknown-alert': 'Route has disruptions.', 'itinerary-details.route-has-warning-alert': 'Route has disruptions.', 'itinerary-details.scooter-leg': - 'At {time} ride your kick scooter {distance} from {origin} to {destination}. Estimated time {duration}', + 'At {time} travel by scooter {distance} from {origin} to {destination}. Estimated time {duration}', 'itinerary-details.transit-leg-part-1': 'At {time} {realtime} take', 'itinerary-details.transit-leg-part-2': 'from stop {startStop} {startZoneInfo} {trackInfo} to stop {endStop} {endZoneInfo}. Estimated duration {duration}', @@ -1183,9 +1182,12 @@ const translations = { 'Leaves at {firstDepartureTime} {firstDepartureStopType} {firstDepartureStop}{firstDeparturePlatform}', 'itinerary-summary-row.first-leg-start-time-citybike': 'Departure at {firstDepartureTime} from {firstDepartureStop} bike station', + 'itinerary-summary-row.first-leg-start-time-scooter': + 'Departure at {firstDepartureTime} by a scooter', 'itinerary-summary-row.no-transit-legs': 'Leave when it suits you', 'itinerary-summary-row.transfers': 'Transfer to {vehicle} on stop {stopName}', + 'itinerary-summary-row.transfers-to-rental': 'Transfer to {vehicle}', 'itinerary-summary-row.walking-distance': 'Total walking distance {totalDistance}.', 'itinerary-summary.bike-boarding-information': @@ -1261,6 +1263,7 @@ const translations = { 'menu-link-to-privacy-policy': 'Privacy policy', 'messagebar-label-close-message-bar': 'Close banner', 'messagebar-label-page': 'Page', + 'min-transfer-time': 'Interchange time', 'minute-short': 'min', minutes: 'min', 'minutes-or-route': 'Min / Route', @@ -1280,7 +1283,17 @@ const translations = { 'more-settings': 'More settings', 'move-on-map': 'Move on the map', 'move-to-tab': 'Go to tab {number}', + 'navidest-show-on-map': 'Show route on map', navigate: 'Navigate', + 'navigation-description': 'Journey guidance', + 'navigation-header': 'Journey tracking', + 'navigation-journey-start': 'Your journey starts {time}', + 'navigation-label-close': 'Stop journey guidance', + 'navigation-start': 'Start journey', + 'navileg-bicycle': 'Cycle to', + 'navileg-car': 'Drive to', + 'navileg-scooter': 'Travel by scooter to', + 'navileg-walk': 'Walk to', nearest: '{ mode } near you', 'nearest-favorites': 'Stops nearby me', 'nearest-favorites-browse-stops': 'Browse and select stops', @@ -1344,8 +1357,8 @@ const translations = { 'Log in to the service to save your favorites and utilize them on other devices', 'one-way-journey': 'The length of a one-way journey', 'open-app': 'Open the app', - 'open-operator-app': 'Open {operator} app', - 'open-settings': 'Avaa asetukset', + 'open-operator-app': 'Open the {operator} app', + 'open-settings': 'Open the settings', 'option-default': 'Average', 'option-least': 'Slow', 'option-less': 'Calm', @@ -1366,9 +1379,9 @@ const translations = { 'park-and-ride-availability': 'Spaces available:', 'pay-attention': 'N.B.', 'payment-info-e-scooter': - 'Huomaathan, että potkulautojen käyttö ja maksaminen tapahtuu toimijoiden omilla sovelluksilla.', + 'Please note that you need to use the app of the operator in question in order to use and pay for the scooters.', 'payment-info-e-scooter-taxi': - 'Huomaathan, että potkulautojen ja taksin käyttö ja maksaminen tapahtuu toimijoiden omilla sovelluksilla.', + 'Please note that you need to use the app of the operator in question in order to use and pay for the taxis or scooters.', 'payment-info-taxi-lifts': 'Huomaathan, että taksien ja kyytipalveluiden käyttö ja maksaminen tapahtuu toimijoiden omilla sovelluksilla.', payment_at_gate: 'Payment at the gate ', @@ -1413,11 +1426,11 @@ const translations = { 'remove-favourite': 'Remove from favorites', 'remove-via-button-label': 'Remove via point {index}', 'rent-cycle-at': 'Fetch a city bike:', - 'rent-e-scooter-at': 'Ota sähköpotkulauta käyttöön sovelluksella', + 'rent-e-scooter-at': 'Use an app to unlock the electric scooter', 'rent-scooter-at': 'Rent a kick scooter at {station} station', 'required-ticket': 'Ticket required for the journey', 'return-cycle-to': 'Return the bike:', - 'return-e-scooter-to': 'Jätä potkulauta turvalliseen paikkaan', + 'return-e-scooter-to': 'Leave the scooter in a safe location', 'return-scooter-to': 'Return the kick scooter to {station} station', 'right-now': 'Right now', route: 'Route', @@ -1490,7 +1503,7 @@ const translations = { scooter: 'Scooter', 'scooter-availability': 'Kick scooters available at the station right now', 'scooter-distance-duration': - 'Ride your kick scooter {duration} ({distance})', + 'Travel by scooter for {duration} ({distance})', 'scooter-register-required': 'To use kick scooters, you need to register', 'scooterwalk-distance-duration': 'Walk your kick scooter {duration} ({distance})', @@ -1529,17 +1542,17 @@ const translations = { 'settings-dropdown-close-label': 'Close options', 'settings-dropdown-open-label': 'Open options', 'settings-e-scooter': - 'Voit valita haluamasi sähköpotkulaudat, jolloin ne huomioidaan osana reititystä. {paymentInfo}', + 'You can select the electric scooters you want and make them part of the route suggestions. {paymentInfo}', 'settings-e-scooter-on': - 'Laita sähköpotkulauta päälle asetuksista pysyvästi', - 'settings-e-scooter-routes': 'Sähköpotkulaudat osaksi reittejäsi!', + 'Make electric scooters a permanent option in your settings', + 'settings-e-scooter-routes': 'Make electric scooters part of your routes!', 'settings-label-change': 'Change settings', 'settings-missing-itineraries-body': 'Some selections in the settings exclude certain route alternatives.', 'settings-missing-itineraries-header': 'Are some route alternatives missing?', 'settings-taxi-lift': - 'Voit valita haluamasi taksi ja kyytipalvelut, jolloin ne huomioidaan osana reititystä.', + 'Voit valita haluamasi taksi ja kyytipalvelut, jolloin ne ovat osa reititystä.', share: 'Share itinerary', show: 'Show', 'show-departures': 'Show departures', @@ -1625,7 +1638,9 @@ const translations = { 'time-selector-hours-label': 'Hour', 'time-selector-minutes-label': 'Minute', timetable: 'Timetable', + 'to-bicycle': 'bicycle', 'to-bus': 'bus', + 'to-e-scooter': 'scooter', 'to-ferry': 'ferry', 'to-frontpage': 'To the front page', 'to-rail': 'train', @@ -2264,7 +2279,6 @@ const translations = { 'Entä jos kulkisit osan matkasta sähköpotkulaudalla? {paymentInfo}', 'e-scooter-or-taxi-alternative': 'Entä jos kulkisit osan matkasta taksilla tai sähköpotkulaudalla? {paymentInfo}', - 'e-scooter-quantity': 'Sähköpotkulautoja vapaana juuri nyt: ', 'e-scooter-station': 'Sähköpotkulauta-asema', 'e-scooters': 'Sähköpotkulaudat', 'e-scooters-available': @@ -2279,7 +2293,6 @@ const translations = { 'embedded-search.form-heading': 'Upotusvalinnat', engine_ignition_aid: 'Käynnistysapu', english: 'Englanti', - 'exit-vehicle': 'Nouse pois kyydistä ja kävele laiturille', explanations: 'Selitykset', 'extra-info': 'Lisätiedot', 'favourite-failed-text': 'Yritä hetken päästä uudelleen.', @@ -2293,6 +2306,7 @@ const translations = { 'from-bus': 'bussista', 'from-ferry': 'lautasta', 'from-rail': 'junasta', + 'from-scooter-location': 'potkulaudan sijainti', 'from-station': 'asemalta', 'from-stop': 'pysäkiltä', 'from-subway': 'metrosta', @@ -2350,7 +2364,7 @@ const translations = { 'itinerary-details.route-has-unknown-alert': 'Reitillä on häiriöitä.', 'itinerary-details.route-has-warning-alert': 'Reitillä on häiriöitä.', 'itinerary-details.scooter-leg': - '{time} potkulautaile {distance} kohteesta {origin} kohteeseen {destination}. Matka-aika {duration}', + '{time} potkulautaile kohteesta {origin} {distance} {to} {destination}. Matka-aika {duration}', 'itinerary-details.transit-leg-part-1': '{time} {realtime} ota', 'itinerary-details.transit-leg-part-2': 'pysäkiltä {startStop} {startZoneInfo} {trackInfo} pysäkille {endStop} {endZoneInfo}. Arvioitu matka-aika {duration}', @@ -2389,8 +2403,11 @@ const translations = { 'Lähtee klo {firstDepartureTime} {firstDepartureStopType} {firstDepartureStop}{firstDeparturePlatform}', 'itinerary-summary-row.first-leg-start-time-citybike': 'Lähtö klo {firstDepartureTime} kaupunkipyöräasemalta {firstDepartureStop}', + 'itinerary-summary-row.first-leg-start-time-scooter': + 'Lähtö klo {firstDepartureTime} sähköpotkulaudalla', 'itinerary-summary-row.no-transit-legs': 'Lähde, kun sinulle sopii', 'itinerary-summary-row.transfers': 'Vaihto {vehicle} pysäkillä {stopName}', + 'itinerary-summary-row.transfers-to-rental': 'Vaihto {vehicle}', 'itinerary-summary-row.walking-distance': 'Kävelyä yhteensä {totalDistance}.', 'itinerary-summary.bike-boarding-information': @@ -2466,6 +2483,7 @@ const translations = { 'menu-link-to-privacy-policy': 'Rekisteriseloste', 'messagebar-label-close-message-bar': 'Sulje banneri', 'messagebar-label-page': 'Sivu', + 'min-transfer-time': 'Vaihtoaika', 'minute-short': 'min', minutes: 'min', 'minutes-or-route': 'Min / Linja', @@ -2485,7 +2503,17 @@ const translations = { 'more-settings': 'Lisäasetukset', 'move-on-map': 'Siirry kartalla', 'move-to-tab': 'Siirry välilehdelle {number}', + 'navidest-show-on-map': 'Näytä reitti kartalla', navigate: 'Navigoi', + 'navigation-description': 'Löydä perille ohjatusti', + 'navigation-header': 'Matkan seuranta', + 'navigation-journey-start': 'Matkasi alkaa {time}', + 'navigation-label-close': 'Lopeta matkaopastus', + 'navigation-start': 'Matkalle', + 'navileg-bicycle': 'Pyöräile', + 'navileg-car': 'Aja', + 'navileg-scooter': 'Potkulautaile', + 'navileg-walk': 'Kävele', nearest: 'Lähimmät {mode}', 'nearest-favorites': 'Omat lähipysäkit', 'nearest-favorites-browse-stops': 'Selaa ja valitse pysäkkejä', @@ -2732,16 +2760,15 @@ const translations = { 'settings-dropdown-close-label': 'Sulje vaihtoehdot', 'settings-dropdown-open-label': 'Avaa vaihtoehdot', 'settings-e-scooter': - 'Voit valita haluamasi sähköpotkulaudat, jolloin ne huomioidaan osana reititystä. {paymentInfo}', - 'settings-e-scooter-on': - 'Laita sähköpotkulauta päälle asetuksista pysyvästi', + 'Voit valita haluamasi sähköpotkulaudat, jolloin ne ovat osa reititystä. {paymentInfo}', + 'settings-e-scooter-on': 'Lisää sähköpotkulauta pysyvästi asetuksiisi', 'settings-e-scooter-routes': 'Sähköpotkulaudat osaksi reittejäsi!', 'settings-label-change': 'Muuta reittihaun asetuksia', 'settings-missing-itineraries-body': 'Asetuksissa on päällä valintoja, jotka rajaavat pois joitain reittivaihtoehtoja.', 'settings-missing-itineraries-header': 'Puuttuuko reittivaihtoehtoja?', 'settings-taxi-lift': - 'Voit valita haluamasi taksi ja kyytipalvelut, jolloin ne huomioidaan osana reititystä.', + 'Voit valita haluamasi taksi ja kyytipalvelut, jolloin ne ovat osa reititystä.', share: 'Jaa ohje', show: 'Näytä', 'show-departures': 'Näytä lähdöt', @@ -2830,7 +2857,9 @@ const translations = { 'time-selector-hours-label': 'Tunti', 'time-selector-minutes-label': 'Minuutti', timetable: 'Aikataulu', + 'to-bicycle': 'polkupyörään', 'to-bus': 'bussiin', + 'to-e-scooter': 'sähköpotkulautaan', 'to-ferry': 'lauttaan', 'to-frontpage': 'Etusivulle', 'to-rail': 'junaan', @@ -5109,16 +5138,15 @@ const translations = { 'distance-between': 'Avstånd {distance1} m — {distance2} m', 'distance-total': 'Resans längd', 'distance-under': 'Avstånd mindre än {distance} m', - 'e-scooter': 'Sähköpotkulauta', + 'e-scooter': 'Elsparkcykel', 'e-scooter-alternative': - 'Entä jos kulkisit osan matkasta sähköpotkulaudalla? {paymentInfo}', + 'Och om du skulle åka elsparkcykel en del av din resa? {paymentInfo}', 'e-scooter-or-taxi-alternative': - 'Entä jos kulkisit osan matkasta taksilla tai sähköpotkulaudalla? {paymentInfo}', - 'e-scooter-quantity': 'Sähköpotkulautoja vapaana juuri nyt: ', - 'e-scooter-station': 'Sähköpotkulauta-asema', - 'e-scooters': 'Sähköpotkulaudat', + 'Och om du skulle åka elsparkcykel eller taxi en del av din resa? {paymentInfo}', + 'e-scooter-station': 'Elsparkcykelstation', + 'e-scooters': 'Elsparkcyklar', 'e-scooters-available': - 'Alueella sähköpotkulautoja vapaana. Ota sähköpotkulauta käyttöön sovelluksella.', + 'Det finns lediga elsparkcyklar i området. Ta en elsparkcykel i bruk med en app.', earlier: 'Tidigare avgångar', 'edit-favourites': 'Redigera favoritdestination', 'elevation-gained-total': 'Uppförsbacke totalt', @@ -5129,7 +5157,6 @@ const translations = { 'embedded-search.form-heading': 'Inbäddningsverktyg', engine_ignition_aid: 'Starthjälp', english: 'Engelsk', - 'exit-vehicle': 'Nouse pois kyydistä ja kävele laiturille', explanations: 'Förklaringar', 'extra-info': 'Mer info', 'favourite-failed-text': 'Försök på nytt senare.', @@ -5143,6 +5170,7 @@ const translations = { 'from-bus': 'bussen', 'from-ferry': 'färjan', 'from-rail': 'tåget', + 'from-scooter-location': 'platsen för sparkcykel', 'from-station': 'från stationen', 'from-stop': 'från hållplats', 'from-subway': 'metron', @@ -5203,7 +5231,7 @@ const translations = { 'itinerary-details.route-has-unknown-alert': 'Störningar längs rutten..', 'itinerary-details.route-has-warning-alert': 'Störningar längs rutten..', 'itinerary-details.scooter-leg': - '{time} sparkcykla {distance} från {origin} till destinationen {destination}. Restid {duration}', + '{time} åk elsparkcykel {distance} från {origin} till destinationen {destination}. Restid {duration}', 'itinerary-details.transit-leg-part-1': '{time} {realtime} ta', 'itinerary-details.transit-leg-part-2': 'från hållplats {startStop} {startZoneInfo} {trackInfo} till hållplats {endStop} {endZoneInfo}. Beräknad restid {duration}', @@ -5243,9 +5271,12 @@ const translations = { 'Avgår kl {firstDepartureTime} {firstDepartureStopType} {firstDepartureStop}{firstDeparturePlatform}', 'itinerary-summary-row.first-leg-start-time-citybike': 'Avgång kl {firstDepartureTime} från {firstDepartureStop} stadscykelstation', + 'itinerary-summary-row.first-leg-start-time-scooter': + 'Avgång kl {firstDepartureTime} med en sparkcykel', 'itinerary-summary-row.no-transit-legs': 'Avgå när det passar för dig', 'itinerary-summary-row.transfers': 'Byte {vehicle} vid hållplats {stopName}', + 'itinerary-summary-row.transfers-to-rental': 'Byte {vehicle}', 'itinerary-summary-row.walking-distance': 'Promenad sammanlagt {totalDistance}.', 'itinerary-summary.bike-boarding-information': @@ -5319,6 +5350,7 @@ const translations = { 'menu-link-to-privacy-policy': 'Registerbeskrivning', 'messagebar-label-close-message-bar': 'Stäng banner', 'messagebar-label-page': 'Sidan', + 'min-transfer-time': 'Bytestid', 'minute-short': 'min', minutes: 'min', 'minutes-or-route': 'Min / Linje', @@ -5338,7 +5370,17 @@ const translations = { 'more-settings': 'Fler inställningar', 'move-on-map': 'Flytta på kartan', 'move-to-tab': 'Gå till fliken {number}', + 'navidest-show-on-map': 'Visa rutt på kartan', navigate: 'Navigera', + 'navigation-description': 'Hitta fram med vägledning.', + 'navigation-header': 'Följa', + 'navigation-journey-start': 'Din resa börjar {time}', + 'navigation-label-close': 'Stopp reseledare', + 'navigation-start': 'På resa', + 'navileg-bicycle': 'Cycla till', + 'navileg-car': 'Kör till', + 'navileg-scooter': 'Åk elsparkcykel', + 'navileg-walk': 'Gå till', nearest: 'Närmaste { mode }', 'nearest-favorites': 'Hållplatser nära mig', 'nearest-favorites-browse-stops': 'Bläddra och välj hållplatser', @@ -5400,7 +5442,7 @@ const translations = { 'Genom att logga in kan du spara dina favoriter och använda dem med dina andra enheter.', 'one-way-journey': ' Längden på en enkel resa', 'open-app': ' Öppna appen', - 'open-operator-app': 'Öppna {operator} appen', + 'open-operator-app': 'Öppna appen {operator}', 'open-settings': 'Öppna inställningar', 'option-default': 'Standard', 'option-least': 'Minst', @@ -5423,9 +5465,9 @@ const translations = { 'park-and-ride-availability': 'Lediga platser:', 'pay-attention': 'Obs!', 'payment-info-e-scooter': - 'Huomaathan, että potkulautojen käyttö ja maksaminen tapahtuu toimijoiden omilla sovelluksilla.', + 'Vänligen observera att användning och betalning av elsparkcyklar görs via operatörernas egna appar.', 'payment-info-e-scooter-taxi': - 'Huomaathan, että potkulautojen ja taksin käyttö ja maksaminen tapahtuu toimijoiden omilla sovelluksilla.', + 'Vänligen observera att användning och betalning av elsparkcyklar och taxi görs via operatörernas egna appar.', 'payment-info-taxi-lifts': 'Huomaathan, että taksien ja kyytipalveluiden käyttö ja maksaminen tapahtuu toimijoiden omilla sovelluksilla.', payment_at_gate: 'Betalning vid porten', @@ -5469,11 +5511,11 @@ const translations = { 'remove-favourite': 'Ta bort favoritmarkeringen', 'remove-via-button-label': 'Ta bort viapunkt {index}', 'rent-cycle-at': 'Sök en stadscykel:', - 'rent-e-scooter-at': 'Ota sähköpotkulauta käyttöön sovelluksella', + 'rent-e-scooter-at': 'Ta en elsparkcykel i bruk med en app.', 'rent-scooter-at': 'Hyr en sparkcykel från stationen {station}', 'required-ticket': 'Biljett som behövs under resan', 'return-cycle-to': 'Returnera stadscykeln: ', - 'return-e-scooter-to': 'Jätä potkulauta turvalliseen paikkaan', + 'return-e-scooter-to': 'Lämna elsparkcykeln på ett säkert ställe', 'return-scooter-to': 'Returnera sparkcykeln till stationen {station}', 'right-now': 'Just nu', route: 'Linje', @@ -5544,7 +5586,7 @@ const translations = { save: 'Spara', scooter: 'Scooter', 'scooter-availability': 'Antal sparkcyklar just nu', - 'scooter-distance-duration': 'Sparkcykla {duration} ({distance})', + 'scooter-distance-duration': 'Åk elsparkcykel {duration} ({distance})', 'scooter-register-required': 'Det krävs registrering för att kunna använda sparkcyklar', 'scooterwalk-distance-duration': 'Led sparkcykeln {duration} ({distance})', @@ -5577,7 +5619,7 @@ const translations = { 'separate-ticket-required': 'Denna etapp av resan kräver en separat biljett.', 'separate-ticket-required-disclaimer': - 'Resan består av en eller fleta etapper vilka kräver biljetter som inte säljs av {agencyName}.', + 'Resan består av en eller flera etapper vilka kräver biljetter som inte säljs av {agencyName}.', 'separate-ticket-required-for-call-agency-disclaimer': 'I resan ingår anropsbusstrafiken. Bekanta dig med betalningsmetoder och praxis på ', 'set-specific-settings': 'Ställa avancerade inställningar', @@ -5588,16 +5630,16 @@ const translations = { 'settings-dropdown-close-label': 'Stäng alternativen', 'settings-dropdown-open-label': 'Öppna alternativen', 'settings-e-scooter': - 'Voit valita haluamasi sähköpotkulaudat, jolloin ne huomioidaan osana reititystä. {paymentInfo}', + 'Du kan välja de elsparkcyklar du vill och åka en del av din rutt med den. {paymentInfo}', 'settings-e-scooter-on': - 'Laita sähköpotkulauta päälle asetuksista pysyvästi', - 'settings-e-scooter-routes': 'Sähköpotkulaudat osaksi reittejäsi!', + 'Lägg till elsparkcyklar permanent i dina inställningar', + 'settings-e-scooter-routes': 'Åk elsparkcykel under en del av din resa!', 'settings-label-change': 'Anpassa sökning', 'settings-missing-itineraries-body': 'Dina val i inställningar utesluter vissa ruttalternativ.', 'settings-missing-itineraries-header': 'Saknas det några ruttförslag?', 'settings-taxi-lift': - 'Voit valita haluamasi taksi ja kyytipalvelut, jolloin ne huomioidaan osana reititystä.', + 'Voit valita haluamasi taksi ja kyytipalvelut, jolloin ne ovat osa reititystä.', share: 'Dela resan', show: 'Visa', 'show-departures': 'Visa avgångarna', @@ -5685,7 +5727,9 @@ const translations = { 'time-selector-hours-label': 'Timme', 'time-selector-minutes-label': 'Minut', timetable: 'Tidtabell', + 'to-bicycle': 'stadscykeln', 'to-bus': 'bussen', + 'to-e-scooter': 'skotern', 'to-ferry': 'färjan', 'to-frontpage': 'Till startsidan', 'to-rail': 'tåget', diff --git a/app/util/DTSearchContextInitializer.js b/app/util/DTSearchContextInitializer.js index e32297e0b4..bf09babced 100644 --- a/app/util/DTSearchContextInitializer.js +++ b/app/util/DTSearchContextInitializer.js @@ -39,7 +39,7 @@ export default function intializeSearchContext(context, searchContext) { // FeedId's like [HSL, HSLLautta] searchContext.feedIDs = config.feedIds; searchContext.cityBikeNetworks = useCitybikes( - config.cityBike.networks, + config.vehicleRental.networks, config, ) ? getDefaultNetworks(config).map(t => `citybikes${t}`) diff --git a/app/util/legUtils.js b/app/util/legUtils.js index e248cb630d..2f6a308bb0 100644 --- a/app/util/legUtils.js +++ b/app/util/legUtils.js @@ -43,8 +43,8 @@ export function isCallAgencyDeparture(departure) { function sameBicycleNetwork(leg1, leg2) { if (leg1.from.vehicleRentalStation && leg2.from.vehicleRentalStation) { return ( - leg1.from.vehicleRentalStation.network === - leg2.from.vehicleRentalStation.network + leg1.from.vehicleRentalStation.rentalNetwork.networkId === + leg2.from.vehicleRentalStation.rentalNetwork.networkId ); } return true; @@ -384,7 +384,7 @@ export function getVehicleAvailabilityIndicatorColor(available, config) { // eslint-disable-next-line no-nested-ternary available === 0 ? '#DC0451' - : available > config.cityBike.fewAvailableCount + : available > config.vehicleRental.fewAvailableCount ? '#3B7F00' : '#FCBC19' ); @@ -396,7 +396,7 @@ export function getVehicleAvailabilityIndicatorColor(available, config) { * @param {*} config the configuration for the software installation/ */ export function getVehicleAvailabilityTextColor(available, config) { - return available <= config.cityBike.fewAvailableCount && available > 0 + return available <= config.vehicleRental.fewAvailableCount && available > 0 ? '#333' : '#fff'; } @@ -413,7 +413,7 @@ export function getLegBadgeProps(leg, config) { !leg.rentedBike || !leg.from || !leg.from.vehicleRentalStation || - config.cityBike.capacity === BIKEAVL_UNKNOWN || + config.vehicleRental.capacity === BIKEAVL_UNKNOWN || leg.mode === 'WALK' || leg.mode === 'SCOOTER' ) { @@ -605,3 +605,27 @@ export const showBikeBoardingNote = (leg, config) => { bikeBoardingModes && bikeBoardingModes[leg.mode]?.showNotification === true ); }; + +/** + * Return translated string that describes leg destination + * + * @param {object} intl - rect-intl context + * @param {object} leg - The leg object. + * @param {object} secondary - optional walk leg + * @returns {string} + */ +export const legDestination = (intl, leg, secondary) => { + const { to } = leg; + let id = 'modes.to-place'; + + if (leg.mode === 'BICYCLE' && to.vehicleParking) { + id = 'modes.to-bike-park'; + } else if (leg.mode === 'CAR' && to.vehicleParking) { + id = 'modes.to-car-park'; + } + const mode = to.stop?.vehicleMode || secondary?.stop?.vehicleMode; + if (mode) { + id = `modes.to-${mode.toLowerCase()}`; + } + return intl.formatMessage({ id, defaultMessage: 'place' }); +}; diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js index 6a7dc4947a..aee5f90a6e 100644 --- a/app/util/modeUtils.js +++ b/app/util/modeUtils.js @@ -86,10 +86,10 @@ export function showRentalVehiclesOfType(networks, config, type) { } export function getNearYouModes(config) { - if (!config.cityBike?.networks) { + if (!config.vehicleRental?.networks) { return config.nearYouModes; } - if (!useCitybikes(config.cityBike.networks, config)) { + if (!useCitybikes(config.vehicleRental.networks, config)) { return config.nearYouModes.filter(mode => mode !== 'citybike'); } return config.nearYouModes; @@ -98,11 +98,11 @@ export function getNearYouModes(config) { export function getTransportModes(config) { let citybikeConfig = {}; let scooterConfig = {}; - if (config.cityBike?.networks) { - if (!useCitybikes(config.cityBike.networks, config)) { + if (config.vehicleRental?.networks) { + if (!useCitybikes(config.vehicleRental.networks, config)) { citybikeConfig = { citybike: { availableForSelection: false } }; } - if (!useScooters(config.cityBike.networks)) { + if (!useScooters(config.vehicleRental.networks)) { scooterConfig = { scooter: { availableForSelection: false } }; } } diff --git a/app/util/path.js b/app/util/path.js index 41c6979742..9b4331635a 100644 --- a/app/util/path.js +++ b/app/util/path.js @@ -20,7 +20,6 @@ export const stopUrl = id => id; export const LOCAL_STORAGE_EMITTER_PATH = '/local-storage-emitter'; export const EMBEDDED_SEARCH_PATH = '/haku'; export const PREFIX_RENTALVEHICLES = 'skuutit'; -export const PREFIX_RENTALVEHICLES_CLUSTER = 'skuuttiryhmat'; /** * Join argument with slash separator. diff --git a/app/util/planParamUtil.js b/app/util/planParamUtil.js index 20ae2353bf..846d489f51 100644 --- a/app/util/planParamUtil.js +++ b/app/util/planParamUtil.js @@ -197,11 +197,12 @@ export function planQueryNeeded( ); case PLANTYPE.SCOOTERTRANSIT: - /* special logic: relaxed scooter query is made only if no networks allowed, and scooters are available for selection */ + /* special logic: relaxed scooter query is made only if no networks allowed */ return ( + config.transportModes.scooter.availableForSelection && transitModes.length > 0 && !wheelchair && - (relaxSettings && config.transportModes.scooter.availableForSelection + (relaxSettings ? settings.scooterNetworks.length === 0 : settings.scooterNetworks.length > 0) ); @@ -326,6 +327,7 @@ export function getPlanParams( case PLANTYPE.SCOOTERTRANSIT: access = ['WALK', 'SCOOTER_RENTAL']; egress = access; + direct = access; break; default: // direct modes direct = [planType]; diff --git a/app/util/shapes.js b/app/util/shapes.js index a0b7d96bb7..c8d0fd1845 100644 --- a/app/util/shapes.js +++ b/app/util/shapes.js @@ -94,7 +94,9 @@ export const parkShape = PropTypes.shape({ export const vehicleRentalStationShape = PropTypes.shape({ availableVehicles: PropTypes.shape({ total: PropTypes.number }), availableSpaces: PropTypes.shape({ total: PropTypes.number }), - network: PropTypes.string, + rentalNetwork: PropTypes.shape({ + networkId: PropTypes.string, + }), capacity: PropTypes.number, operative: PropTypes.bool, }); @@ -103,7 +105,6 @@ export const rentalVehicleShape = PropTypes.shape({ id: PropTypes.string, vehicleId: PropTypes.string, name: PropTypes.string, - network: PropTypes.string, lat: PropTypes.number, lon: PropTypes.number, rentalUris: PropTypes.shape({ @@ -113,6 +114,7 @@ export const rentalVehicleShape = PropTypes.shape({ }), rentalNetwork: PropTypes.shape({ url: PropTypes.string, + networkId: PropTypes.string, }), }); @@ -400,3 +402,10 @@ export const vehicleShape = PropTypes.shape({ heading: PropTypes.number, headsign: PropTypes.string, }); + +export const minTransferTimeShape = PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + value: PropTypes.number, + }), +); diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js index 6c65e41eab..3e6ec7cc5d 100644 --- a/app/util/vehicleRentalUtils.js +++ b/app/util/vehicleRentalUtils.js @@ -52,17 +52,17 @@ export const getRentalNetworkConfig = (networkId, config) => { } const id = networkId.toLowerCase(); if ( - config.cityBike?.networks?.[id] && - Object.keys(config.cityBike.networks[id]).length > 0 + config.vehicleRental?.networks?.[id] && + Object.keys(config.vehicleRental.networks[id]).length > 0 ) { - return config.cityBike.networks[id]; + return config.vehicleRental.networks[id]; } return defaultNetworkConfig; }; export const getDefaultNetworks = config => { const mappedNetworks = []; - Object.entries(config.cityBike.networks).forEach(n => { + Object.entries(config.vehicleRental.networks).forEach(n => { if ( networkIsActive(n[1]) && n[1]?.type !== RentalNetworkType.Scooter // scooter networks are never on by default @@ -75,7 +75,7 @@ export const getDefaultNetworks = config => { export const getAllNetworksOfType = (config, type) => { const mappedNetworks = []; - Object.entries(config.cityBike.networks).forEach(n => { + Object.entries(config.vehicleRental.networks).forEach(n => { if (n[1].type.toLowerCase() === type.toLowerCase()) { mappedNetworks.push(n[0]); } @@ -85,11 +85,11 @@ export const getAllNetworksOfType = (config, type) => { export const mapDefaultNetworkProperties = config => { const mappedNetworks = []; - Object.keys(config.cityBike.networks).forEach(key => { - if (networkIsActive(config.cityBike.networks[key])) { + Object.keys(config.vehicleRental.networks).forEach(key => { + if (networkIsActive(config.vehicleRental.networks[key])) { mappedNetworks.push({ networkName: key, - ...config.cityBike.networks[key], + ...config.vehicleRental.networks[key], }); } }); @@ -98,7 +98,8 @@ export const mapDefaultNetworkProperties = config => { export const getVehicleCapacity = (config, network = undefined) => { return ( - config.cityBike?.networks[network]?.capacity || config.cityBike.capacity + config.vehicleRental?.networks[network]?.capacity || + config.vehicleRental.capacity ); }; /** @@ -160,10 +161,10 @@ export const updateVehicleNetworks = (currentSettings, newValue) => { }; export const getVehicleMinZoomOnStopsNearYou = (config, override) => { - if (override && config.cityBike.minZoomStopsNearYou) { - return config.cityBike.minZoomStopsNearYou; + if (override && config.vehicleRental.minZoomStopsNearYou) { + return config.vehicleRental.minZoomStopsNearYou; } - return config.cityBike.cityBikeMinZoom; + return config.vehicleRental.cityBikeMinZoom; }; /** * @@ -204,28 +205,39 @@ export const mapVehicleRentalToStore = vehicleRentalStation => { return newStation; }; -export const getRentalVehicleLink = (rentalVehicle, network, networkConfig) => { +export const getRentalVehicleLink = (rentalVehicle, networkConfig) => { if (!networkConfig || !rentalVehicle) { return null; } - const { ios, android, web } = rentalVehicle?.rentalUris || {}; + const { ios, android, web } = rentalVehicle.rentalUris || {}; + const networkName = getRentalNetworkName(networkConfig).toLowerCase(); - if (isIOS && ios?.startsWith(`${network}://`)) { + if (isIOS && ios?.startsWith(`${networkName}://`)) { return ios; } - if (isAndroid && android?.startsWith(`${network}://`)) { + if (isAndroid && android?.startsWith(`${networkName}://`)) { return android; } - if (web?.includes(network)) { + if (web?.includes(networkName)) { return web; } - if (rentalVehicle?.rentalNetwork?.url?.includes(network)) { + if (rentalVehicle.rentalNetwork?.url?.includes(networkName)) { return rentalVehicle.rentalNetwork.url; } return null; }; + +export const useDeepLink = (deepLink, fallBackAddress) => { + window.location.href = deepLink; + setTimeout(() => { + if (!document.hidden && document.hasFocus()) { + // If the document is still visible and has focus, the deep link must have failed + window.location.href = fallBackAddress; + } + }, 500); +}; diff --git a/digitransit-component/packages/digitransit-component-control-panel/package.json b/digitransit-component/packages/digitransit-component-control-panel/package.json index 1dd0b6ef9d..7b5c56a801 100644 --- a/digitransit-component/packages/digitransit-component-control-panel/package.json +++ b/digitransit-component/packages/digitransit-component-control-panel/package.json @@ -1,6 +1,6 @@ { "name": "@digitransit-component/digitransit-component-control-panel", - "version": "1.1.8", + "version": "2.0.0", "description": "digitransit-component control-panel module", "main": "index.js", "files": [ diff --git a/digitransit-component/packages/digitransit-component-control-panel/src/index.js b/digitransit-component/packages/digitransit-component-control-panel/src/index.js index a37e345afe..8a783fad54 100644 --- a/digitransit-component/packages/digitransit-component-control-panel/src/index.js +++ b/digitransit-component/packages/digitransit-component-control-panel/src/index.js @@ -4,8 +4,6 @@ import PropTypes from 'prop-types'; import React, { Fragment, useEffect, useState } from 'react'; import i18next from 'i18next'; -import { useCookies } from 'react-cookie'; -import cx from 'classnames'; import Icon from '@digitransit-component/digitransit-component-icon'; import styles from './helpers/styles.scss'; import translations from './helpers/translations'; @@ -67,73 +65,6 @@ OriginToDestination.defaultProps = { language: 'fi', }; -function BubbleDialog({ title, content, closeDialog, shouldRender, lang }) { - const [show, setShow] = useState(false); - useEffect(() => { - setTimeout(() => { - setShow(true); - }, 500); - }, [show]); - - return ( -
-
-
-
- {title} -
-
- {content} -
- -
-
-
-
-
-
- ); -} - -BubbleDialog.propTypes = { - title: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - closeDialog: PropTypes.func.isRequired, - shouldRender: PropTypes.bool.isRequired, - lang: PropTypes.string.isRequired, -}; - /** * Show button links to near you page for different travel modes * @@ -196,15 +127,11 @@ function NearStopsAndRoutes({ modeSet, modeIconColors, fontWeights, - showTeaser, }) { const [modesWithAlerts, setModesWithAlerts] = useState([]); - const [cookies, setCookie] = useCookies(['nearbyTeaserShown']); useEffect(() => { - Object.keys(translations).forEach(lang => { - i18next.addResourceBundle(lang, 'translation', translations[lang]); - }); + i18next.changeLanguage(language); if (alertsContext) { alertsContext .getModesWithAlerts(alertsContext.currentTime, alertsContext.feedIds) @@ -214,12 +141,6 @@ function NearStopsAndRoutes({ } }, []); - const closeBubbleDialog = () => - setCookie('nearbyTeaserShown', true, { - path: '/', - maxAge: 10 * 365 * 24 * 60 * 60, - }); - let urlStart; if (omitLanguageUrl) { urlStart = urlPrefix; @@ -328,20 +249,6 @@ function NearStopsAndRoutes({ : title[language]} )} - {showTeaser && !cookies?.nearbyTeaserShown && ( - - )}
{ + i18next.addResourceBundle(lang, 'translation', translations[lang]); + }); + } + render() { const className = this.props.position === 'bottom' ? styles['main-bottom'] : styles['main-left']; - const children = React.Children.map(this.props.children, child => { - if (child) { - let lang = this.props.language; - if (lang === undefined) { - lang = 'fi'; - } - i18next.changeLanguage(lang); - return React.cloneElement(child, { lang }); - } - return null; - }); return (
- {children} + {this.props.children}
); } diff --git a/digitransit-component/packages/digitransit-component/package.json b/digitransit-component/packages/digitransit-component/package.json index 10045a9523..ffbee607d6 100644 --- a/digitransit-component/packages/digitransit-component/package.json +++ b/digitransit-component/packages/digitransit-component/package.json @@ -16,7 +16,7 @@ "dependencies": { "@digitransit-component/digitransit-component-autosuggest": "^2.0.8", "@digitransit-component/digitransit-component-autosuggest-panel": "^3.0.8", - "@digitransit-component/digitransit-component-control-panel": "^1.1.8", + "@digitransit-component/digitransit-component-control-panel": "^2.0.0", "@digitransit-component/digitransit-component-favourite-bar": "2.0.5", "@digitransit-component/digitransit-component-favourite-editing-modal": "^2.0.2", "@digitransit-component/digitransit-component-favourite-modal": "^1.0.6", diff --git a/sass/_main.scss b/sass/_main.scss index d65d8a8e96..5c5f3534f5 100644 --- a/sass/_main.scss +++ b/sass/_main.scss @@ -62,6 +62,7 @@ $body-font-weight: $font-weight-medium; @import '../app/component/itinerary/settings-panel'; @import '../app/component/itinerary/mobile-ticket-purchase-information'; @import 'base/button'; +@import '../app/component/itinerary/navigator'; /* Modal */ @import '~foundation-apps/scss/helpers/breakpoints'; diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000..0613f70d9f --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,16 @@ +# Using scripts + +## Before using: +``` +source ui.sh +``` +## Usage examples + +Using remote instance of OTP with subscription key. +``` +SUBSCRIPTION_KEY= ui hsl +``` +Using local instance of OTP on port `9080`. +``` +uiotp matka +``` \ No newline at end of file diff --git a/scripts/ui.sh b/scripts/ui.sh new file mode 100755 index 0000000000..3c1cd304ea --- /dev/null +++ b/scripts/ui.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +ui () { + CONFIG=$1 API_SUBSCRIPTION_QUERY_PARAMETER_NAME=digitransit-subscription-key API_SUBSCRIPTION_HEADER_NAME=digitransit-subscription-key API_SUBSCRIPTION_TOKEN=$SUBSCRIPTION_KEY yarn run dev +} + +uiotp () { + if [ "$1" = "hsl" ]; then + OTP_URL=http://localhost:9080/otp/routers/hsl/ ui hsl + elif [ "$1" = "matka" ]; then + OTP_URL=http://localhost:9080/otp/routers/finland/ ui matka + elif [ "$1" = "kela" ]; then + OTP_URL=http://localhost:9080/otp/routers/kela/ ui kela + else + OTP_URL=http://localhost:9080/otp/routers/waltti/ ui $1 + fi +} diff --git a/server/server.js b/server/server.js index 068335a57c..5f1e18e3b8 100644 --- a/server/server.js +++ b/server/server.js @@ -424,8 +424,8 @@ function fetchCitybikeConfigurations() { // eslint-disable-next-line import/no-dynamic-require const conf = require(`${configsDir}/${file}`); const configName = conf.default.CONFIG; - const { cityBike } = conf.default; - if (cityBike && Object.keys(cityBike).length > 0) { + const { vehicleRental } = conf.default; + if (vehicleRental && Object.keys(vehicleRental).length > 0) { promises.push( new Promise(resolve => { resolve( diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg index 27393ee4fb..48df87502b 100644 --- a/static/assets/svg-sprite.default.svg +++ b/static/assets/svg-sprite.default.svg @@ -279,7 +279,7 @@ - + diff --git a/test/unit/component/BicycleLeg.test.js b/test/unit/component/BicycleLeg.test.js index 55bd7e41ea..8d9c947d58 100644 --- a/test/unit/component/BicycleLeg.test.js +++ b/test/unit/component/BicycleLeg.test.js @@ -23,7 +23,7 @@ describe('', () => { name: 'Hertanmäenkatu', vehicleRentalStation: { vehiclesAvailable: 0, - network: 'foobar', + rentalNetwork: { networkId: 'foobar' }, }, }, to: { @@ -34,7 +34,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { + vehicleRental: { networks: { foobar: { type: RentalNetworkType.CityBike } }, }, defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, @@ -64,7 +64,7 @@ describe('', () => { name: 'Hertanmäenkatu', vehicleRentalStation: { vehiclesAvailable: 0, - network: 'foobar', + rentalNetwork: { networkId: 'foobar' }, }, }, to: { @@ -75,7 +75,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { + vehicleRental: { networks: { foobar: { type: RentalNetworkType.Scooter } }, }, defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, @@ -84,7 +84,7 @@ describe('', () => { }, }); expect(wrapper.find(FormattedMessage).at(0).prop('id')).to.equal( - 'rent-scooter-at', + 'rent-e-scooter-at', ); }); @@ -105,7 +105,7 @@ describe('', () => { name: 'Hertanmäenkatu', vehicleRentalStation: { vehiclesAvailable: 0, - network: 'foobar', + rentalNetwork: { networkId: 'foobar' }, }, }, to: { @@ -116,7 +116,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { + vehicleRental: { networks: { foobar: { type: RentalNetworkType.CityBike } }, }, defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, @@ -149,7 +149,7 @@ describe('', () => { name: 'Hertanmäenkatu', vehicleRentalStation: { vehiclesAvailable: 0, - network: 'foobar', + rentalNetwork: { networkId: 'foobar' }, }, }, to: { @@ -160,7 +160,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { + vehicleRental: { networks: { foobar: { type: RentalNetworkType.Scooter } }, }, defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, @@ -193,7 +193,7 @@ describe('', () => { name: 'Hertanmäenkatu', vehicleRentalStation: { vehiclesAvailable: 0, - network: 'foobar', + rentalNetwork: { networkId: 'foobar' }, }, }, to: { @@ -204,7 +204,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { + vehicleRental: { networks: { foobar: { type: RentalNetworkType.CityBike } }, }, defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, @@ -234,7 +234,7 @@ describe('', () => { name: 'Hertanmäenkatu', vehicleRentalStation: { vehiclesAvailable: 0, - network: 'foobar', + rentalNetwork: { networkId: 'foobar' }, }, }, to: { @@ -245,7 +245,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { + vehicleRental: { networks: { foobar: { type: RentalNetworkType.Scooter } }, }, defaultSettings: { walkSpeed: 1, bikeSpeed: 1 }, diff --git a/test/unit/component/Itinerary.test.js b/test/unit/component/Itinerary.test.js index f5bc89b6fe..3894419812 100644 --- a/test/unit/component/Itinerary.test.js +++ b/test/unit/component/Itinerary.test.js @@ -53,7 +53,7 @@ describe('', () => { const wrapper = mountWithIntl(, { context: { ...mockContext, - config: { CONFIG: 'default', cityBike: { fewAvailableCount: 3 } }, + config: { CONFIG: 'default', vehicleRental: { fewAvailableCount: 3 } }, }, childContextTypes: { ...mockChildContextTypes }, }); diff --git a/test/unit/component/MapLayersDialogContent.test.js b/test/unit/component/MapLayersDialogContent.test.js index 2933b4a703..67cd95dd75 100644 --- a/test/unit/component/MapLayersDialogContent.test.js +++ b/test/unit/component/MapLayersDialogContent.test.js @@ -218,7 +218,7 @@ describe('', () => { const context = { config: { CONFIG: 'default', - cityBike: { + vehicleRental: { networks: { foo: { type: 'citybike', diff --git a/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js b/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js index 236025919b..0c38f64b04 100644 --- a/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js +++ b/test/unit/component/map/tile-layer/SelectVehicleRentalStationRow.test.js @@ -29,7 +29,7 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { - cityBike: { networks: { scooter_network: { icon: 'scooter' } } }, + vehicleRental: { networks: { scooter_network: { icon: 'scooter' } } }, }, }, }); diff --git a/test/unit/component/map/tile-layer/TileLayerContainer.test.js b/test/unit/component/map/tile-layer/TileLayerContainer.test.js index 472695df8c..8ea2474815 100644 --- a/test/unit/component/map/tile-layer/TileLayerContainer.test.js +++ b/test/unit/component/map/tile-layer/TileLayerContainer.test.js @@ -127,7 +127,7 @@ describe('', () => { }), on: () => {}, }), - config: { CONFIG: 'default', cityBike: {} }, + config: { CONFIG: 'default', vehicleRental: {} }, }, }, ); diff --git a/test/unit/test-data/dcw12.js b/test/unit/test-data/dcw12.js index d6a4db5c1b..aa5e6552d1 100644 --- a/test/unit/test-data/dcw12.js +++ b/test/unit/test-data/dcw12.js @@ -250,7 +250,7 @@ export default { lat: 60.2010614, lon: 24.9440506, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, @@ -311,7 +311,7 @@ export default { lat: 60.179765, lon: 24.9554618, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, @@ -332,7 +332,7 @@ export default { lat: 60.160925600000006, lon: 24.941921, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, @@ -353,7 +353,7 @@ export default { lat: 60.160952472069354, lon: 24.9418835642972, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, @@ -414,7 +414,7 @@ export default { lat: 60.1618359, lon: 24.9368179, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, @@ -435,7 +435,7 @@ export default { lat: 60.16185062722838, lon: 24.936860480731475, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, @@ -456,7 +456,7 @@ export default { lat: 60.1617163, lon: 24.937049100000003, stop: null, - vehicleRentalStation: { networks: ['smoove'] }, + vehicleRentalStation: { rentalNetwork: { networkId: 'smoove' } }, }, to: { stop: null }, }, diff --git a/test/unit/util/planParamUtil.test.js b/test/unit/util/planParamUtil.test.js index eb39245bb5..64edc5dd63 100644 --- a/test/unit/util/planParamUtil.test.js +++ b/test/unit/util/planParamUtil.test.js @@ -29,7 +29,7 @@ const config = { }, }, modePolygons: {}, - cityBike: { + vehicleRental: { networks: { smoove: { icon: 'citybike', diff --git a/test/unit/util/vehiclerental.test.js b/test/unit/util/vehiclerental.test.js index 321d051857..d8a049d175 100644 --- a/test/unit/util/vehiclerental.test.js +++ b/test/unit/util/vehiclerental.test.js @@ -32,24 +32,24 @@ describe('vehiclerental', () => { expect(getRentalNetworkConfig('Smoove', {})).to.equal( defaultNetworkConfig, ); - expect(getRentalNetworkConfig('Smoove', { cityBike: {} })).to.equal( + expect(getRentalNetworkConfig('Smoove', { vehicleRental: {} })).to.equal( defaultNetworkConfig, ); expect( getRentalNetworkConfig('Smoove', { - cityBike: { networks: {} }, + vehicleRental: { networks: {} }, }), ).to.equal(defaultNetworkConfig); expect( getRentalNetworkConfig('Smoove', { - cityBike: { networks: { smoove: {} } }, + vehicleRental: { networks: { smoove: {} } }, }), ).to.equal(defaultNetworkConfig); }); it('should return the configuration by the given network id', () => { const config = { - cityBike: { + vehicleRental: { networks: { foobar: { icon: 'citybike', @@ -59,13 +59,13 @@ describe('vehiclerental', () => { }, }; expect(getRentalNetworkConfig('foobar', config)).to.equal( - config.cityBike.networks.foobar, + config.vehicleRental.networks.foobar, ); }); it('should convert networkId to lowercase', () => { const config = { - cityBike: { + vehicleRental: { networks: { foobar: { icon: 'citybike', @@ -75,7 +75,7 @@ describe('vehiclerental', () => { }, }; expect(getRentalNetworkConfig('Foobar', config)).to.equal( - config.cityBike.networks.foobar, + config.vehicleRental.networks.foobar, ); }); }); diff --git a/yarn.lock b/yarn.lock index abe77cbc2d..1b00cb003e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2013,7 +2013,7 @@ __metadata: languageName: unknown linkType: soft -"@digitransit-component/digitransit-component-control-panel@^1.1.8, @digitransit-component/digitransit-component-control-panel@workspace:digitransit-component/packages/digitransit-component-control-panel": +"@digitransit-component/digitransit-component-control-panel@^2.0.0, @digitransit-component/digitransit-component-control-panel@workspace:digitransit-component/packages/digitransit-component-control-panel": version: 0.0.0-use.local resolution: "@digitransit-component/digitransit-component-control-panel@workspace:digitransit-component/packages/digitransit-component-control-panel" peerDependencies: @@ -2168,7 +2168,7 @@ __metadata: dependencies: "@digitransit-component/digitransit-component-autosuggest": ^2.0.8 "@digitransit-component/digitransit-component-autosuggest-panel": ^3.0.8 - "@digitransit-component/digitransit-component-control-panel": ^1.1.8 + "@digitransit-component/digitransit-component-control-panel": ^2.0.0 "@digitransit-component/digitransit-component-favourite-bar": 2.0.5 "@digitransit-component/digitransit-component-favourite-editing-modal": ^2.0.2 "@digitransit-component/digitransit-component-favourite-modal": ^1.0.6 @@ -8770,9 +8770,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001524, caniuse-lite@npm:^1.0.30001580": - version: 1.0.30001585 - resolution: "caniuse-lite@npm:1.0.30001585" - checksum: c5994f0b5de857349ae0c157a3c61883e800ed154bbeab339aecf01a0a0fd24f67d23ebb48bc995c4c9cde2a281a51b682d1b14bbf2f832f6b2261119f450af4 + version: 1.0.30001651 + resolution: "caniuse-lite@npm:1.0.30001651" + checksum: c31a5a01288e70cdbbfb5cd94af3df02f295791673173b8ce6d6a16db4394a6999197d44190be5a6ff06b8c2c7d2047e94dfd5e5eb4c103ab000fca2d370afc7 languageName: node linkType: hard