Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 141 additions & 66 deletions app/component/AppBarHsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
import React, { useState, useEffect, useRef } from 'react';
import { matchShape } from 'found';
import { Helmet } from 'react-helmet';
import SiteHeader from '@hsl-fi/site-header';
import { useIntl } from 'react-intl';
import { SiteHeader, UserMenu, QuickSearch } from '@hsl-fi/site-header';
import { favouriteShape, configShape } from '../util/shapes';
import { clearOldSearches, clearFutureRoutes } from '../util/storeUtils';
import { getJson } from '../util/xhrPromise';
Expand All @@ -18,7 +17,6 @@ const clearStorages = context => {
const notificationAPI = '/api/user/notifications';

const AppBarHsl = ({ lang, user, favourites }, context) => {
const intl = useIntl();
const { config, match } = context;
const { location } = match;

Expand All @@ -27,15 +25,102 @@ const AppBarHsl = ({ lang, user, favourites }, context) => {
post: `${notificationAPI}?language=${lang}`,
};

const [banners, setBanners] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [searchLoading, setSearchLoading] = useState(false);
const [searchError, setSearchError] = useState(false);
const [searchHits, setSearchHits] = useState([]);
const [searchHitsCount, setSearchHitsCount] = useState(0);
const [userNotifications, setUserNotifications] = useState({
unreadCount: 0,
loading: false,
error: null,
notifications: [],
refetch: () => {},
onOpen: () => {},
});

useEffect(() => {
if (config.URL.BANNERS && process.env.NODE_ENV !== 'test') {
getJson(`${config.URL.BANNERS}&language=${lang}`)
.then(data => setBanners(data))
.catch(() => setBanners([]));
if (!user.sub) {
return undefined;
}
}, [lang]);

const markAsRead = () => {
fetch(notificationApiUrls.post, {
method: 'POST',
headers: { 'content-type': 'application/json' },
})
.then(() => {
setUserNotifications(prev => ({ ...prev, unreadCount: 0 }));
})
.catch(() => {});
};

const fetchNotifications = () => {
setUserNotifications(prev => ({ ...prev, loading: true, error: null }));
getJson(notificationApiUrls.get)
.then(data => {
setUserNotifications({
unreadCount: data?.unreadCount || 0,
loading: false,
error: null,
notifications: (data?.notifications || []).map(n => ({
...n,
link: n.link || {},
})),
refetch: fetchNotifications,
onOpen: markAsRead,
});
})
.catch(err => {
setUserNotifications(prev => ({
...prev,
loading: false,
error: err,
}));
});
};

fetchNotifications();
const interval = setInterval(fetchNotifications, 60000);
return () => clearInterval(interval);
}, [user.sub, lang]);

useEffect(() => {
if (!searchQuery || !config.URL.HSL_FI_SUGGESTIONS) {
setSearchHits([]);
setSearchHitsCount(0);
return undefined;
}

const timer = setTimeout(() => {
setSearchLoading(true);
setSearchError(false);
getJson(
`${
config.URL.HSL_FI_SUGGESTIONS
}?language=${lang}&take=5&query=${encodeURIComponent(searchQuery)}`,
)
.then(data => {
const hits = (data?.hits || []).map(h => ({
id: h.id,
title: h.title,
type: h.type,
link: { href: h.url },
}));
setSearchHits(hits);
setSearchHitsCount(
data?.totalHits != null ? data.totalHits : hits.length,
);
setSearchLoading(false);
})
.catch(() => {
setSearchError(true);
setSearchLoading(false);
});
}, 300);

return () => clearTimeout(timer);
}, [searchQuery, lang]);

useEffect(() => {
if (config.URL.FONTCOUNTER && process.env.NODE_ENV === 'production') {
Expand All @@ -45,70 +130,63 @@ const AppBarHsl = ({ lang, user, favourites }, context) => {
}
}, []);

const languages = [
{
name: 'fi',
url: `/fi${location.pathname}${location.search}`,
const languages = {
fi: {
href: `/fi${location.pathname}${location.search}`,
},
{
name: 'sv',
url: `/sv${location.pathname}${location.search}`,
sv: {
href: `/sv${location.pathname}${location.search}`,
},
{
name: 'en',
url: `/en${location.pathname}${location.search}`,
en: {
href: `/en${location.pathname}${location.search}`,
},
];
};

const { given_name, family_name } = user;

const initials =
given_name && family_name
? given_name.charAt(0) + family_name.charAt(0)
: ''; // Authenticated user's initials, will be shown next to Person-icon.

const url = encodeURI(location.pathname);
const params = location.search && location.search.substring(1);
const travelersAccountLink = config.URL.TRAVELERS_ACCOUNT
? { href: config.URL.TRAVELERS_ACCOUNT }
: undefined;
const myStopsAndRoutesLink = config.favouriteLink
? { href: config.favouriteLink[lang] || config.favouriteLink.fi }
: undefined;
const userMenu =
config.allowLogin && (user.sub || user.notLogged)
? {
userMenu: {
isLoading: false, // When fetching for login-information, `isLoading`-property can be set to true. Spinner will be shown.
isAuthenticated: !!user.sub, // If user is authenticated, set `isAuthenticated`-property to true.
isSelected: false,
loginUrl: `/login?url=${url}&${params}`, // Url that user will be redirect to when Person-icon is pressed and user is not logged in.
initials,
menuItems: [
{
name: intl.formatMessage({
id: 'userinfo',
defaultMessage: 'My information',
}),
url: `${config.URL.ROOTLINK}/omat-tiedot`,
onClick: () => {},
},
{
name: intl.formatMessage({
id: 'logout',
defaultMessage: 'Logout',
}),
url: '/logout',
onClick: () => clearStorages(context),
},
],
},
}
: {};

const siteHeaderRef = useRef(null);
config.allowLogin && (user.sub || user.notLogged) ? (
<UserMenu
lang={lang}
loading={false}
authenticated={!!user.sub}
loginLink={{ href: `/login?url=${url}&${params}` }}
logoutLink={{ href: '/logout', onClick: () => clearStorages(context) }}
name={{ givenName: given_name, familyName: family_name }}
userNotifications={userNotifications}
travelersAccountLink={travelersAccountLink}
myStopsAndRoutesLink={myStopsAndRoutesLink}
/>
) : null;

const search = config.URL.HSL_FI_SUGGESTIONS ? (
<QuickSearch
searchPageLink={{ href: `${config.URL.ROOTLINK}/${lang}/haku` }}
loading={searchLoading}
error={searchError}
query={searchQuery}
onQueryChange={e => setSearchQuery(e.target.value)}
hitsCount={searchHitsCount}
hits={searchHits}
lang={lang}
/>
) : null;

const notificationTime = useRef(0);

useEffect(() => {
const now = Date.now();
// refresh only once per 5 seconds
if (now - notificationTime.current > 5000) {
// Refetch notifications
siteHeaderRef.current?.fetchNotifications();
userNotifications.refetch();
notificationTime.current = now;
}
}, [favourites]);
Expand All @@ -126,17 +204,14 @@ const AppBarHsl = ({ lang, user, favourites }, context) => {
/>
</Helmet>
)}

{!config.hideHeader && (
<SiteHeader
ref={siteHeaderRef}
hslFiUrl={config.URL.ROOTLINK}
baseUrl={config.URL.ROOTLINK}
staticAssetsUrl="/static-assets"
lang={lang}
{...userMenu}
languageMenu={languages}
banners={banners}
suggestionsApiUrl={config.URL.HSL_FI_SUGGESTIONS}
notificationApiUrls={notificationApiUrls}
userMenu={userMenu}
langMenu={languages}
search={search}
/>
)}
</>
Expand Down
4 changes: 4 additions & 0 deletions app/configurations/config.hsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const BANNER_URL = process.env.CONTENT_DOMAIN
const SUGGESTION_URL = process.env.CONTENT_DOMAIN
? `${process.env.CONTENT_DOMAIN}/api/v1/search/suggestions`
: 'https://content.hsl.fi/api/v1/search/suggestions'; // old url
const travelersAccountUrl = process.env.TRAVELERS_ACCOUNT_URL;
const staticAssetsUrl = process.env.STATIC_ASSETS_URL;

const virtualMonitorBaseUrl = IS_DEV
? 'https://dev-hslmonitori.digitransit.fi'
Expand Down Expand Up @@ -67,6 +69,8 @@ export default {
FONT: 'https://www.hsl.fi/fonts/784131/6C5FB8083F348CFBB.css',
FONTCOUNTER: 'https://cloud.typography.com/6364294/7432412/css/fonts.css',
ROOTLINK: rootLink,
TRAVELERS_ACCOUNT: travelersAccountUrl,
STATIC_ASSETS: staticAssetsUrl,
BANNERS: BANNER_URL,
HSL_FI_SUGGESTIONS: SUGGESTION_URL,
EMBEDDED_SEARCH_GENERATION: '/reittiopas-elementti',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"@hsl-fi/modal": " ^0.3.2",
"@hsl-fi/sass": " 1.0.0",
"@hsl-fi/shimmer": "0.1.2",
"@hsl-fi/site-header": "4.5.2",
"@hsl-fi/site-header": "6.4.0",
"@mapbox/sphericalmercator": "1.1.0",
"@mapbox/vector-tile": "1.3.1",
"axios": "1.15.0",
Expand Down
11 changes: 11 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ function setUpMiddleware() {
// proxy for dev-bundle
app.use('/proxy/', proxy(`http://localhost:${hotloadPort}/`));
}
// Proxy static assets to avoid CORS issues when fetching from the browser
// TODO this is a hacky solution, contact site-header admins to update site-header cors settings.
const staticAssetsBaseUrl = process.env.STATIC_ASSETS_URL;
if (staticAssetsBaseUrl) {
app.use(
'/static-assets',
proxy(staticAssetsBaseUrl, {
proxyReqPathResolver: req => req.url,
}),
);
}
}

function onError(err, req, res) {
Expand Down
35 changes: 35 additions & 0 deletions test/unit/helpers/babel-register.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
/* eslint-disable no-underscore-dangle */
require('@babel/register')({
// This will override `node_modules` ignoring - you can alternatively pass
// an array of strings to be explicitly matched or a regex / glob
ignore: [
/node_modules\/(?!react-leaflet|@babel\/runtime\/helpers\/esm|lodash-es|@digitransit-util|@digitransit-component)/,
],
});

// Prevent Node.js from trying to parse CSS files as JavaScript
require.extensions['.css'] = () => {};

// Stub out @hsl-fi packages that are ESM-only — they can't be require()'d by
// Node's CJS loader (ERR_REQUIRE_ESM). Unit tests don't need the real
// implementations; stubs are sufficient for shallow rendering.
const Module = require('module');

const originalLoad = Module._load;
Module._load = function hslFiStub(...args) {
const [request] = args;
if (request.startsWith('@hsl-fi/')) {
return new Proxy(
function StubComponent() {
return null;
},
{
get(target, prop) {
if (prop === '__esModule') {
return true;
}
if (prop === 'default') {
return target;
}
return function StubComponent() {
return null;
};
},
},
);
}
return originalLoad.apply(this, args);
};
11 changes: 11 additions & 0 deletions webpack.config.babel.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,17 @@ module.exports = {
},
{
test: /\.css$/,
include: /node_modules\/@hsl-fi/,
sideEffects: true,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
{
test: /\.css$/,
exclude: /node_modules\/@hsl-fi/,
use: [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
Expand Down
Loading
Loading