Skip to content

Commit 2932d0c

Browse files
authored
Merge pull request #5799 from HSLdevcom/hsl-site-header-v3
Hsl site header v3
2 parents fe9bb2b + 4236f1a commit 2932d0c

7 files changed

Lines changed: 906 additions & 243 deletions

File tree

app/component/AppBarHsl.js

Lines changed: 141 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
33
import React, { useState, useEffect, useRef } from 'react';
44
import { matchShape } from 'found';
55
import { Helmet } from 'react-helmet';
6-
import SiteHeader from '@hsl-fi/site-header';
7-
import { useIntl } from 'react-intl';
6+
import { SiteHeader, UserMenu, QuickSearch } from '@hsl-fi/site-header';
87
import { favouriteShape, configShape } from '../util/shapes';
98
import { clearOldSearches, clearFutureRoutes } from '../util/storeUtils';
109
import { getJson } from '../util/xhrPromise';
@@ -18,7 +17,6 @@ const clearStorages = context => {
1817
const notificationAPI = '/api/user/notifications';
1918

2019
const AppBarHsl = ({ lang, user, favourites }, context) => {
21-
const intl = useIntl();
2220
const { config, match } = context;
2321
const { location } = match;
2422

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

30-
const [banners, setBanners] = useState([]);
28+
const [searchQuery, setSearchQuery] = useState('');
29+
const [searchLoading, setSearchLoading] = useState(false);
30+
const [searchError, setSearchError] = useState(false);
31+
const [searchHits, setSearchHits] = useState([]);
32+
const [searchHitsCount, setSearchHitsCount] = useState(0);
33+
const [userNotifications, setUserNotifications] = useState({
34+
unreadCount: 0,
35+
loading: false,
36+
error: null,
37+
notifications: [],
38+
refetch: () => {},
39+
onOpen: () => {},
40+
});
3141

3242
useEffect(() => {
33-
if (config.URL.BANNERS && process.env.NODE_ENV !== 'test') {
34-
getJson(`${config.URL.BANNERS}&language=${lang}`)
35-
.then(data => setBanners(data))
36-
.catch(() => setBanners([]));
43+
if (!user.sub) {
44+
return undefined;
3745
}
38-
}, [lang]);
46+
47+
const markAsRead = () => {
48+
fetch(notificationApiUrls.post, {
49+
method: 'POST',
50+
headers: { 'content-type': 'application/json' },
51+
})
52+
.then(() => {
53+
setUserNotifications(prev => ({ ...prev, unreadCount: 0 }));
54+
})
55+
.catch(() => {});
56+
};
57+
58+
const fetchNotifications = () => {
59+
setUserNotifications(prev => ({ ...prev, loading: true, error: null }));
60+
getJson(notificationApiUrls.get)
61+
.then(data => {
62+
setUserNotifications({
63+
unreadCount: data?.unreadCount || 0,
64+
loading: false,
65+
error: null,
66+
notifications: (data?.notifications || []).map(n => ({
67+
...n,
68+
link: n.link || {},
69+
})),
70+
refetch: fetchNotifications,
71+
onOpen: markAsRead,
72+
});
73+
})
74+
.catch(err => {
75+
setUserNotifications(prev => ({
76+
...prev,
77+
loading: false,
78+
error: err,
79+
}));
80+
});
81+
};
82+
83+
fetchNotifications();
84+
const interval = setInterval(fetchNotifications, 60000);
85+
return () => clearInterval(interval);
86+
}, [user.sub, lang]);
87+
88+
useEffect(() => {
89+
if (!searchQuery || !config.URL.HSL_FI_SUGGESTIONS) {
90+
setSearchHits([]);
91+
setSearchHitsCount(0);
92+
return undefined;
93+
}
94+
95+
const timer = setTimeout(() => {
96+
setSearchLoading(true);
97+
setSearchError(false);
98+
getJson(
99+
`${
100+
config.URL.HSL_FI_SUGGESTIONS
101+
}?language=${lang}&take=5&query=${encodeURIComponent(searchQuery)}`,
102+
)
103+
.then(data => {
104+
const hits = (data?.hits || []).map(h => ({
105+
id: h.id,
106+
title: h.title,
107+
type: h.type,
108+
link: { href: h.url },
109+
}));
110+
setSearchHits(hits);
111+
setSearchHitsCount(
112+
data?.totalHits != null ? data.totalHits : hits.length,
113+
);
114+
setSearchLoading(false);
115+
})
116+
.catch(() => {
117+
setSearchError(true);
118+
setSearchLoading(false);
119+
});
120+
}, 300);
121+
122+
return () => clearTimeout(timer);
123+
}, [searchQuery, lang]);
39124

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

48-
const languages = [
49-
{
50-
name: 'fi',
51-
url: `/fi${location.pathname}${location.search}`,
133+
const languages = {
134+
fi: {
135+
href: `/fi${location.pathname}${location.search}`,
52136
},
53-
{
54-
name: 'sv',
55-
url: `/sv${location.pathname}${location.search}`,
137+
sv: {
138+
href: `/sv${location.pathname}${location.search}`,
56139
},
57-
{
58-
name: 'en',
59-
url: `/en${location.pathname}${location.search}`,
140+
en: {
141+
href: `/en${location.pathname}${location.search}`,
60142
},
61-
];
143+
};
62144

63145
const { given_name, family_name } = user;
64146

65-
const initials =
66-
given_name && family_name
67-
? given_name.charAt(0) + family_name.charAt(0)
68-
: ''; // Authenticated user's initials, will be shown next to Person-icon.
69-
70147
const url = encodeURI(location.pathname);
71148
const params = location.search && location.search.substring(1);
149+
const travelersAccountLink = config.URL.TRAVELERS_ACCOUNT
150+
? { href: config.URL.TRAVELERS_ACCOUNT }
151+
: undefined;
152+
const myStopsAndRoutesLink = config.favouriteLink
153+
? { href: config.favouriteLink[lang] || config.favouriteLink.fi }
154+
: undefined;
72155
const userMenu =
73-
config.allowLogin && (user.sub || user.notLogged)
74-
? {
75-
userMenu: {
76-
isLoading: false, // When fetching for login-information, `isLoading`-property can be set to true. Spinner will be shown.
77-
isAuthenticated: !!user.sub, // If user is authenticated, set `isAuthenticated`-property to true.
78-
isSelected: false,
79-
loginUrl: `/login?url=${url}&${params}`, // Url that user will be redirect to when Person-icon is pressed and user is not logged in.
80-
initials,
81-
menuItems: [
82-
{
83-
name: intl.formatMessage({
84-
id: 'userinfo',
85-
defaultMessage: 'My information',
86-
}),
87-
url: `${config.URL.ROOTLINK}/omat-tiedot`,
88-
onClick: () => {},
89-
},
90-
{
91-
name: intl.formatMessage({
92-
id: 'logout',
93-
defaultMessage: 'Logout',
94-
}),
95-
url: '/logout',
96-
onClick: () => clearStorages(context),
97-
},
98-
],
99-
},
100-
}
101-
: {};
102-
103-
const siteHeaderRef = useRef(null);
156+
config.allowLogin && (user.sub || user.notLogged) ? (
157+
<UserMenu
158+
lang={lang}
159+
loading={false}
160+
authenticated={!!user.sub}
161+
loginLink={{ href: `/login?url=${url}&${params}` }}
162+
logoutLink={{ href: '/logout', onClick: () => clearStorages(context) }}
163+
name={{ givenName: given_name, familyName: family_name }}
164+
userNotifications={userNotifications}
165+
travelersAccountLink={travelersAccountLink}
166+
myStopsAndRoutesLink={myStopsAndRoutesLink}
167+
/>
168+
) : null;
169+
170+
const search = config.URL.HSL_FI_SUGGESTIONS ? (
171+
<QuickSearch
172+
searchPageLink={{ href: `${config.URL.ROOTLINK}/${lang}/haku` }}
173+
loading={searchLoading}
174+
error={searchError}
175+
query={searchQuery}
176+
onQueryChange={e => setSearchQuery(e.target.value)}
177+
hitsCount={searchHitsCount}
178+
hits={searchHits}
179+
lang={lang}
180+
/>
181+
) : null;
182+
104183
const notificationTime = useRef(0);
105184

106185
useEffect(() => {
107186
const now = Date.now();
108187
// refresh only once per 5 seconds
109188
if (now - notificationTime.current > 5000) {
110-
// Refetch notifications
111-
siteHeaderRef.current?.fetchNotifications();
189+
userNotifications.refetch();
112190
notificationTime.current = now;
113191
}
114192
}, [favourites]);
@@ -126,17 +204,14 @@ const AppBarHsl = ({ lang, user, favourites }, context) => {
126204
/>
127205
</Helmet>
128206
)}
129-
130207
{!config.hideHeader && (
131208
<SiteHeader
132-
ref={siteHeaderRef}
133-
hslFiUrl={config.URL.ROOTLINK}
209+
baseUrl={config.URL.ROOTLINK}
210+
staticAssetsUrl="/static-assets"
134211
lang={lang}
135-
{...userMenu}
136-
languageMenu={languages}
137-
banners={banners}
138-
suggestionsApiUrl={config.URL.HSL_FI_SUGGESTIONS}
139-
notificationApiUrls={notificationApiUrls}
212+
userMenu={userMenu}
213+
langMenu={languages}
214+
search={search}
140215
/>
141216
)}
142217
</>

app/configurations/config.hsl.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const BANNER_URL = process.env.CONTENT_DOMAIN
1717
const SUGGESTION_URL = process.env.CONTENT_DOMAIN
1818
? `${process.env.CONTENT_DOMAIN}/api/v1/search/suggestions`
1919
: 'https://content.hsl.fi/api/v1/search/suggestions'; // old url
20+
const travelersAccountUrl = process.env.TRAVELERS_ACCOUNT_URL;
21+
const staticAssetsUrl = process.env.STATIC_ASSETS_URL;
2022

2123
const virtualMonitorBaseUrl = IS_DEV
2224
? 'https://dev-hslmonitori.digitransit.fi'
@@ -67,6 +69,8 @@ export default {
6769
FONT: 'https://www.hsl.fi/fonts/784131/6C5FB8083F348CFBB.css',
6870
FONTCOUNTER: 'https://cloud.typography.com/6364294/7432412/css/fonts.css',
6971
ROOTLINK: rootLink,
72+
TRAVELERS_ACCOUNT: travelersAccountUrl,
73+
STATIC_ASSETS: staticAssetsUrl,
7074
BANNERS: BANNER_URL,
7175
HSL_FI_SUGGESTIONS: SUGGESTION_URL,
7276
EMBEDDED_SEARCH_GENERATION: '/reittiopas-elementti',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
"@hsl-fi/modal": " ^0.3.2",
141141
"@hsl-fi/sass": " 1.0.0",
142142
"@hsl-fi/shimmer": "0.1.2",
143-
"@hsl-fi/site-header": "4.5.2",
143+
"@hsl-fi/site-header": "6.4.0",
144144
"@mapbox/sphericalmercator": "1.1.0",
145145
"@mapbox/vector-tile": "1.3.1",
146146
"axios": "1.15.0",

server/server.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ function setUpMiddleware() {
123123
// proxy for dev-bundle
124124
app.use('/proxy/', proxy(`http://localhost:${hotloadPort}/`));
125125
}
126+
// Proxy static assets to avoid CORS issues when fetching from the browser
127+
// TODO this is a hacky solution, contact site-header admins to update site-header cors settings.
128+
const staticAssetsBaseUrl = process.env.STATIC_ASSETS_URL;
129+
if (staticAssetsBaseUrl) {
130+
app.use(
131+
'/static-assets',
132+
proxy(staticAssetsBaseUrl, {
133+
proxyReqPathResolver: req => req.url,
134+
}),
135+
);
136+
}
126137
}
127138

128139
function onError(err, req, res) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
1+
/* eslint-disable no-underscore-dangle */
12
require('@babel/register')({
23
// This will override `node_modules` ignoring - you can alternatively pass
34
// an array of strings to be explicitly matched or a regex / glob
45
ignore: [
56
/node_modules\/(?!react-leaflet|@babel\/runtime\/helpers\/esm|lodash-es|@digitransit-util|@digitransit-component)/,
67
],
78
});
9+
10+
// Prevent Node.js from trying to parse CSS files as JavaScript
11+
require.extensions['.css'] = () => {};
12+
13+
// Stub out @hsl-fi packages that are ESM-only — they can't be require()'d by
14+
// Node's CJS loader (ERR_REQUIRE_ESM). Unit tests don't need the real
15+
// implementations; stubs are sufficient for shallow rendering.
16+
const Module = require('module');
17+
18+
const originalLoad = Module._load;
19+
Module._load = function hslFiStub(...args) {
20+
const [request] = args;
21+
if (request.startsWith('@hsl-fi/')) {
22+
return new Proxy(
23+
function StubComponent() {
24+
return null;
25+
},
26+
{
27+
get(target, prop) {
28+
if (prop === '__esModule') {
29+
return true;
30+
}
31+
if (prop === 'default') {
32+
return target;
33+
}
34+
return function StubComponent() {
35+
return null;
36+
};
37+
},
38+
},
39+
);
40+
}
41+
return originalLoad.apply(this, args);
42+
};

webpack.config.babel.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@ module.exports = {
271271
},
272272
{
273273
test: /\.css$/,
274+
include: /node_modules\/@hsl-fi/,
275+
sideEffects: true,
276+
use: [
277+
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
278+
'css-loader',
279+
'postcss-loader',
280+
],
281+
},
282+
{
283+
test: /\.css$/,
284+
exclude: /node_modules\/@hsl-fi/,
274285
use: [
275286
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
276287
'css-loader',

0 commit comments

Comments
 (0)