@eeacms/volto-cca-policy 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/locales/bg/LC_MESSAGES/volto.po +576 -0
  3. package/locales/cs/LC_MESSAGES/volto.po +576 -0
  4. package/locales/da/LC_MESSAGES/volto.po +576 -0
  5. package/locales/de/LC_MESSAGES/volto.po +576 -0
  6. package/locales/el/LC_MESSAGES/volto.po +576 -0
  7. package/locales/en/LC_MESSAGES/volto.po +576 -0
  8. package/locales/es/LC_MESSAGES/volto.po +576 -0
  9. package/locales/et/LC_MESSAGES/volto.po +576 -0
  10. package/locales/fi/LC_MESSAGES/volto.po +576 -0
  11. package/locales/fr/LC_MESSAGES/volto.po +576 -0
  12. package/locales/ga/LC_MESSAGES/volto.po +576 -0
  13. package/locales/hr/LC_MESSAGES/volto.po +576 -0
  14. package/locales/hu/LC_MESSAGES/volto.po +576 -0
  15. package/locales/is/LC_MESSAGES/volto.po +576 -0
  16. package/locales/it/LC_MESSAGES/volto.po +576 -0
  17. package/locales/lt/LC_MESSAGES/volto.po +576 -0
  18. package/locales/lv/LC_MESSAGES/volto.po +576 -0
  19. package/locales/mt/LC_MESSAGES/volto.po +576 -0
  20. package/locales/nl/LC_MESSAGES/volto.po +576 -0
  21. package/locales/nn/LC_MESSAGES/volto.po +576 -0
  22. package/locales/pl/LC_MESSAGES/volto.po +576 -0
  23. package/locales/pt/LC_MESSAGES/volto.po +576 -0
  24. package/locales/ro/LC_MESSAGES/volto.po +576 -0
  25. package/locales/sk/LC_MESSAGES/volto.po +576 -0
  26. package/locales/sl/LC_MESSAGES/volto.po +576 -0
  27. package/locales/sv/LC_MESSAGES/volto.po +576 -0
  28. package/locales/tr/LC_MESSAGES/volto.po +576 -0
  29. package/locales/volto.pot +577 -1
  30. package/package.json +1 -1
  31. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +10 -14
  32. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +6 -0
  33. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +1 -1
  34. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.jsx +268 -7
  35. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.test.jsx +160 -0
  36. package/src/components/Search/NavigatorCatalogue/utils.js +56 -0
  37. package/src/components/theme/CompareTools/CompareToolsView.jsx +15 -8
  38. package/src/components/theme/CompareTools/CompareToolsView.test.jsx +110 -0
  39. package/src/constants.js +0 -48
  40. package/theme/globals/navigator.less +178 -55
  41. package/artifacts/BLOCKS.md +0 -167
  42. package/artifacts/link-integrity-workflow/README.md +0 -76
  43. package/artifacts/link-integrity-workflow/index.md +0 -33
  44. package/artifacts/link-integrity-workflow/link-integrity-block-fields-report.md +0 -132
  45. package/artifacts/link-integrity-workflow/link-integrity-blocks-report.md +0 -52
  46. package/artifacts/link-integrity-workflow/understanding-link-integrity.md +0 -143
  47. package/artifacts/link-integrity-workflow/volto-block-link-analysis.md +0 -63
  48. package/artifacts/link-integrity-workflow/volto-block-link-discovery.md +0 -60
  49. package/artifacts/test-fixes/test-fixes-specification.md +0 -267
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -54,7 +54,7 @@ const publicationDateFormatter = new Intl.DateTimeFormat('en-GB', {
54
54
  timeZone: 'UTC',
55
55
  });
56
56
 
57
- const TagGroup = ({ intl, typeLabel, values, type }) => {
57
+ const TagGroup = ({ typeLabel, values, type }) => {
58
58
  const visible = values.slice(0, 3);
59
59
  const hidden = values.slice(3);
60
60
  const remaining = values.length - visible.length;
@@ -80,7 +80,13 @@ const TagGroup = ({ intl, typeLabel, values, type }) => {
80
80
  }
81
81
  position="bottom left"
82
82
  trigger={
83
- <span className={`navigator-tag ${type} more`}>+ {remaining}</span>
83
+ <button
84
+ type="button"
85
+ className={`navigator-tag ${type} more`}
86
+ aria-label={`${typeLabel}: ${hidden.join(', ')}`}
87
+ >
88
+ + {remaining}
89
+ </button>
84
90
  }
85
91
  />
86
92
  )}
@@ -172,18 +178,8 @@ const NavigatorCatalogueCardItem = (props) => {
172
178
  </p>
173
179
 
174
180
  <div className="catalogue-taxonomy">
175
- <TagGroup
176
- intl={intl}
177
- typeLabel={sectorLabel}
178
- values={sectors}
179
- type="sector"
180
- />
181
- <TagGroup
182
- intl={intl}
183
- typeLabel={hazardLabel}
184
- values={hazards}
185
- type="hazard"
186
- />
181
+ <TagGroup typeLabel={sectorLabel} values={sectors} type="sector" />
182
+ <TagGroup typeLabel={hazardLabel} values={hazards} type="hazard" />
187
183
  </div>
188
184
 
189
185
  <div className="catalogue-item-footer">
@@ -115,6 +115,12 @@ describe('NavigatorCatalogueCardItem', () => {
115
115
  expect(screen.getByText('Agriculture')).toBeInTheDocument();
116
116
  expect(screen.getByText('Energy')).toBeInTheDocument();
117
117
  expect(screen.getByText('Wildfires')).toBeInTheDocument();
118
+ expect(
119
+ screen.getByRole('button', { name: 'Sector: Energy' }),
120
+ ).toBeInTheDocument();
121
+ expect(
122
+ screen.getByRole('button', { name: 'Hazard: Wildfires' }),
123
+ ).toBeInTheDocument();
118
124
  expect(screen.getByText('Step 1')).toBeInTheDocument();
119
125
  expect(screen.queryByText('Step 4')).not.toBeInTheDocument();
120
126
  expect(
@@ -160,7 +160,7 @@ const NavigatorCatalogueContentView = (props) => {
160
160
  <ResultViewComponent>{children}</ResultViewComponent>
161
161
  <CompareToolsPanel appConfig={appConfig} />
162
162
 
163
- {children.length > 0 && (
163
+ {children.length > 0 && activeNavigatorViewId !== 'map' && (
164
164
  <div className="search-body-footer">
165
165
  <Grid columns={2}>
166
166
  <Grid.Column>
@@ -1,11 +1,272 @@
1
1
  import React from 'react';
2
+ import { compose } from 'redux';
3
+ import { Icon, Button } from 'semantic-ui-react';
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+ import { useSearchContext, useViews } from '@eeacms/search/lib/hocs';
6
+ import { withOpenLayers } from '@eeacms/volto-openlayers-map';
7
+ import { useMapContext } from '@eeacms/volto-openlayers-map/api';
8
+ import { Map, Layer, Layers, Controls } from '@eeacms/volto-openlayers-map/api';
9
+ import { withGeoJsonData } from '@eeacms/volto-cca-policy/helpers/country_map/hocs';
10
+ import { euCountryNames } from '@eeacms/volto-cca-policy/helpers/country_map/countryMap';
11
+ import { clientOnly } from '@eeacms/volto-cca-policy/helpers';
12
+ import {
13
+ withResponsiveContainer,
14
+ withVisibilitySensor,
15
+ } from '@eeacms/volto-cca-policy/hocs';
16
+ import {
17
+ getColorForCount,
18
+ normalizeCountryName,
19
+ buildCountryCounts,
20
+ mapLegendItems,
21
+ } from './utils';
2
22
 
3
- const NavigatorCatalogueMapView = () => (
4
- <div className="navigator-catalogue-map-placeholder">
5
- <div>
6
- <p>Map view.</p>
23
+ const messages = defineMessages({
24
+ toolAvailability: {
25
+ id: '{count, plural, =0 {No tools available} one {# tool available} other {# tools available}}',
26
+ defaultMessage:
27
+ '{count, plural, =0 {No tools available} one {# tool available} other {# tools available}}',
28
+ },
29
+ exploreTools: {
30
+ id: 'Explore tools',
31
+ defaultMessage: 'Explore tools',
32
+ },
33
+ toolsPerCountry: {
34
+ id: 'Tools per country',
35
+ defaultMessage: 'Tools per country',
36
+ },
37
+ close: {
38
+ id: 'Close',
39
+ defaultMessage: 'Close',
40
+ },
41
+ });
42
+
43
+ /* istanbul ignore next */
44
+ const CountryClickInteractions = ({ ol, countryCounts, onExploreTools }) => {
45
+ const { map } = useMapContext();
46
+ const intl = useIntl();
47
+ const [tooltipData, setTooltipData] = React.useState(null);
48
+
49
+ React.useEffect(() => {
50
+ if (!map) return;
51
+
52
+ const handleClick = (evt) => {
53
+ if (evt.dragging) return;
54
+ const pixel = map.getEventPixel(evt.originalEvent);
55
+ const feature = map.forEachFeatureAtPixel(pixel, (f) => f);
56
+ if (!feature) {
57
+ setTooltipData(null);
58
+ return;
59
+ }
60
+
61
+ const name = feature.get('na');
62
+ const normalized = normalizeCountryName(name);
63
+ const count = countryCounts[normalized] || 0;
64
+
65
+ setTooltipData({
66
+ name: normalized,
67
+ count,
68
+ pixel: [evt.originalEvent.clientX, evt.originalEvent.clientY],
69
+ mapRect: map.getTargetElement().getBoundingClientRect(),
70
+ });
71
+ };
72
+
73
+ const handlePointerMove = (evt) => {
74
+ if (evt.dragging) return;
75
+ const pixel = map.getEventPixel(evt.originalEvent);
76
+ const feature = map.forEachFeatureAtPixel(pixel, (f) => f);
77
+ map.getTargetElement().style.cursor = feature ? 'pointer' : '';
78
+ };
79
+
80
+ map.on('singleclick', handleClick);
81
+ map.on('pointermove', handlePointerMove);
82
+
83
+ return () => {
84
+ map.un('singleclick', handleClick);
85
+ map.un('pointermove', handlePointerMove);
86
+ };
87
+ }, [map, countryCounts]);
88
+
89
+ // Render tooltip as a portal-like overlay
90
+ return (
91
+ <>
92
+ {tooltipData && (
93
+ <div
94
+ className="navigator-catalogue-map-tooltip"
95
+ style={{
96
+ left: `${tooltipData.pixel[0] - tooltipData.mapRect.left + 15}px`,
97
+ top: `${tooltipData.pixel[1] - tooltipData.mapRect.top - 10}px`,
98
+ }}
99
+ >
100
+ <div className="tooltip-header">
101
+ <strong>{tooltipData.name}</strong>
102
+ </div>
103
+ <div className="tooltip-body">
104
+ <span className="tooltip-count">
105
+ {intl.formatMessage(messages.toolAvailability, {
106
+ count: tooltipData.count,
107
+ })}
108
+ </span>
109
+ </div>
110
+ {tooltipData.count > 0 && (
111
+ <div className="tooltip-actions">
112
+ <Button
113
+ labelPosition="right"
114
+ className="primary small icon"
115
+ onClick={() => onExploreTools(tooltipData.name)}
116
+ >
117
+ {intl.formatMessage(messages.exploreTools)}
118
+ <Icon className="ri-arrow-right-line" />
119
+ </Button>
120
+ </div>
121
+ )}
122
+ <Button
123
+ className="tooltip-close"
124
+ onClick={() => setTooltipData(null)}
125
+ aria-label={intl.formatMessage(messages.close)}
126
+ >
127
+ <Icon className="ri-close-line" aria-hidden="true" />
128
+ </Button>
129
+ </div>
130
+ )}
131
+ </>
132
+ );
133
+ };
134
+
135
+ /* istanbul ignore next */
136
+ const NavigatorCatalogueMapViewInner = (props) => {
137
+ const { geofeatures, projection, ol } = props;
138
+ const intl = useIntl();
139
+ const searchContext = useSearchContext();
140
+ const views = useViews();
141
+
142
+ // Handler: replace country filter + switch to list view
143
+ const handleExploreTools = React.useCallback(
144
+ (countryName) => {
145
+ const field = 'cca_geographic_countries.keyword';
146
+ // Clear existing country filter, then set the new one
147
+ searchContext?.removeFilter(field);
148
+ searchContext?.addFilter(field, countryName, 'any');
149
+ // Switch to listing view
150
+ views.setActiveViewId('listing');
151
+ },
152
+ [searchContext, views],
153
+ );
154
+
155
+ // Build lookup: countryName -> count
156
+ const countryCounts = React.useMemo(
157
+ () => buildCountryCounts(searchContext?.facets),
158
+ [searchContext?.facets],
159
+ );
160
+
161
+ const [tileWMSSources, setTileWMSSources] = React.useState(null);
162
+ const [euCountriesSource, setEuCountriesSource] = React.useState(null);
163
+
164
+ React.useEffect(() => {
165
+ // Tile layer
166
+ setTileWMSSources([
167
+ new ol.source.TileWMS({
168
+ url: 'https://gisco-services.ec.europa.eu/maps/service',
169
+ params: {
170
+ LAYERS: 'OSMBrightBackground',
171
+ TILED: true,
172
+ },
173
+ serverType: 'geoserver',
174
+ transition: 0,
175
+ }),
176
+ ]);
177
+
178
+ // Country vector layer with per-feature styles
179
+ const features = new ol.format.GeoJSON().readFeatures(geofeatures, {
180
+ dataProjection: 'EPSG:4326',
181
+ featureProjection: 'EPSG:3857',
182
+ });
183
+ const filtered = features.filter((f) =>
184
+ euCountryNames.includes(f.get('na')),
185
+ );
186
+
187
+ // Apply style to each feature
188
+ filtered.forEach((feature) => {
189
+ const name = feature.get('na');
190
+ const normalized = normalizeCountryName(name);
191
+ const count = countryCounts[normalized] || 0;
192
+ const fillColor = getColorForCount(count);
193
+
194
+ feature.setStyle(
195
+ new ol.style.Style({
196
+ stroke: new ol.style.Stroke({
197
+ color: '#333333',
198
+ width: 1,
199
+ }),
200
+ fill: new ol.style.Fill({
201
+ color: fillColor,
202
+ }),
203
+ }),
204
+ );
205
+ });
206
+
207
+ setEuCountriesSource(new ol.source.Vector({ features: filtered }));
208
+ }, [geofeatures, ol, countryCounts]);
209
+
210
+ if (!tileWMSSources || !euCountriesSource) {
211
+ return (
212
+ <div className="navigator-catalogue-map-loading">
213
+ <i className="icon loading" />
214
+ Loading map...
215
+ </div>
216
+ );
217
+ }
218
+
219
+ return (
220
+ <div className="navigator-catalogue-map">
221
+ <div className="navigator-catalogue-map-canvas">
222
+ <Map
223
+ view={{
224
+ center: ol.proj.fromLonLat([14.5, 57], projection),
225
+ projection,
226
+ showFullExtent: true,
227
+ zoom: 3.6,
228
+ }}
229
+ pixelRatio={1}
230
+ >
231
+ <Controls attribution={false} />
232
+ <Layers>
233
+ <CountryClickInteractions
234
+ ol={ol}
235
+ countryCounts={countryCounts}
236
+ onExploreTools={handleExploreTools}
237
+ />
238
+ <Layer.Vector source={euCountriesSource} zIndex={2} />
239
+ <Layer.Tile source={tileWMSSources[0]} zIndex={0} />
240
+ </Layers>
241
+ </Map>
242
+ </div>
243
+
244
+ <aside
245
+ className="navigator-catalogue-map-legend"
246
+ aria-label={intl.formatMessage(messages.toolsPerCountry)}
247
+ >
248
+ <h4 className="legend-title">
249
+ {intl.formatMessage(messages.toolsPerCountry)}
250
+ </h4>
251
+ {mapLegendItems.map((item) => (
252
+ <div key={item.label} className="legend-item">
253
+ <span
254
+ className="legend-color"
255
+ style={{ backgroundColor: item.color }}
256
+ />
257
+ <span>{item.label}</span>
258
+ </div>
259
+ ))}
260
+ </aside>
7
261
  </div>
8
- </div>
9
- );
262
+ );
263
+ };
10
264
 
11
- export default NavigatorCatalogueMapView;
265
+ /* istanbul ignore next */
266
+ export default compose(
267
+ clientOnly,
268
+ withGeoJsonData(true),
269
+ withResponsiveContainer('navigatorCatalogueMap'),
270
+ withVisibilitySensor(),
271
+ withOpenLayers,
272
+ )(NavigatorCatalogueMapViewInner);
@@ -0,0 +1,160 @@
1
+ import '@testing-library/jest-dom';
2
+ import {
3
+ getColorForCount,
4
+ normalizeCountryName,
5
+ buildCountryCounts,
6
+ mapLegendItems,
7
+ } from './utils';
8
+
9
+ describe('getColorForCount', () => {
10
+ it('returns darkest green for 10+ tools', () => {
11
+ expect(getColorForCount(10)).toBe('#0a5c4e');
12
+ expect(getColorForCount(15)).toBe('#0a5c4e');
13
+ expect(getColorForCount(100)).toBe('#0a5c4e');
14
+ });
15
+
16
+ it('returns dark green for 7-9 tools', () => {
17
+ expect(getColorForCount(7)).toBe('#0d7a68');
18
+ expect(getColorForCount(8)).toBe('#0d7a68');
19
+ expect(getColorForCount(9)).toBe('#0d7a68');
20
+ });
21
+
22
+ it('returns base accent green for 4-6 tools', () => {
23
+ expect(getColorForCount(4)).toBe('#289588');
24
+ expect(getColorForCount(5)).toBe('#289588');
25
+ expect(getColorForCount(6)).toBe('#289588');
26
+ });
27
+
28
+ it('returns light green for 1-3 tools', () => {
29
+ expect(getColorForCount(1)).toBe('#6fc4b8');
30
+ expect(getColorForCount(2)).toBe('#6fc4b8');
31
+ expect(getColorForCount(3)).toBe('#6fc4b8');
32
+ });
33
+
34
+ it('returns lightest gray-green for 0 or falsy counts', () => {
35
+ expect(getColorForCount(0)).toBe('#e8eded');
36
+ expect(getColorForCount(null)).toBe('#e8eded');
37
+ expect(getColorForCount(undefined)).toBe('#e8eded');
38
+ });
39
+ });
40
+
41
+ describe('normalizeCountryName', () => {
42
+ it('normalizes Türkiye to Turkey', () => {
43
+ expect(normalizeCountryName('Türkiye')).toBe('Turkey');
44
+ });
45
+
46
+ it('normalizes Czech Rep. to Czechia', () => {
47
+ expect(normalizeCountryName('Czech Rep.')).toBe('Czechia');
48
+ });
49
+
50
+ it('normalizes Bosnia and Herz. to Bosnia and Herzegovina', () => {
51
+ expect(normalizeCountryName('Bosnia and Herz.')).toBe(
52
+ 'Bosnia and Herzegovina',
53
+ );
54
+ });
55
+
56
+ it('returns unchanged name for non-special countries', () => {
57
+ expect(normalizeCountryName('Germany')).toBe('Germany');
58
+ expect(normalizeCountryName('Italy')).toBe('Italy');
59
+ expect(normalizeCountryName('France')).toBe('France');
60
+ expect(normalizeCountryName('Spain')).toBe('Spain');
61
+ });
62
+
63
+ it('handles null and undefined', () => {
64
+ expect(normalizeCountryName(null)).toBeNull();
65
+ expect(normalizeCountryName(undefined)).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe('buildCountryCounts', () => {
70
+ it('builds correct lookup from facet data array', () => {
71
+ const facets = {
72
+ 'cca_geographic_countries.keyword': [
73
+ {
74
+ field: 'cca_geographic_countries.keyword',
75
+ type: 'value',
76
+ data: [
77
+ { value: 'Germany', count: 5 },
78
+ { value: 'Italy', count: 3 },
79
+ { value: 'France', count: 1 },
80
+ ],
81
+ },
82
+ ],
83
+ };
84
+
85
+ const counts = buildCountryCounts(facets);
86
+ expect(counts).toEqual({
87
+ Germany: 5,
88
+ Italy: 3,
89
+ France: 1,
90
+ });
91
+ });
92
+
93
+ it('handles empty facet data', () => {
94
+ const facets = {
95
+ 'cca_geographic_countries.keyword': [
96
+ { field: 'cca_geographic_countries.keyword', data: [] },
97
+ ],
98
+ };
99
+ const counts = buildCountryCounts(facets);
100
+ expect(Object.keys(counts)).toHaveLength(0);
101
+ });
102
+
103
+ it('handles missing facet gracefully', () => {
104
+ const facets = {};
105
+ const counts = buildCountryCounts(facets);
106
+ expect(counts).toEqual({});
107
+ });
108
+
109
+ it('handles undefined facets gracefully', () => {
110
+ const counts = buildCountryCounts(undefined);
111
+ expect(counts).toEqual({});
112
+ });
113
+
114
+ it('handles null facets gracefully', () => {
115
+ const counts = buildCountryCounts(null);
116
+ expect(counts).toEqual({});
117
+ });
118
+
119
+ it('handles non-array facet value', () => {
120
+ const facets = {
121
+ 'cca_geographic_countries.keyword': {
122
+ data: [{ value: 'Germany', count: 5 }],
123
+ },
124
+ };
125
+ const counts = buildCountryCounts(facets);
126
+ expect(counts).toEqual({ Germany: 5 });
127
+ });
128
+
129
+ it('handles missing count values', () => {
130
+ const facets = {
131
+ 'cca_geographic_countries.keyword': [{ data: [{ value: 'Germany' }] }],
132
+ };
133
+ const counts = buildCountryCounts(facets);
134
+ expect(counts.Germany).toBeUndefined();
135
+ });
136
+ });
137
+
138
+ describe('mapLegendItems', () => {
139
+ it('has 5 legend items', () => {
140
+ expect(mapLegendItems).toHaveLength(5);
141
+ });
142
+
143
+ it('colors match getColorForCount scale', () => {
144
+ expect(mapLegendItems[0].color).toBe(getColorForCount(10));
145
+ expect(mapLegendItems[1].color).toBe(getColorForCount(7));
146
+ expect(mapLegendItems[2].color).toBe(getColorForCount(4));
147
+ expect(mapLegendItems[3].color).toBe(getColorForCount(1));
148
+ expect(mapLegendItems[4].color).toBe(getColorForCount(0));
149
+ });
150
+
151
+ it('has correct labels', () => {
152
+ expect(mapLegendItems.map((item) => item.label)).toEqual([
153
+ '10+',
154
+ '7–9',
155
+ '4–6',
156
+ '1–3',
157
+ 'None',
158
+ ]);
159
+ });
160
+ });
@@ -1,3 +1,59 @@
1
+ /**
2
+ * Color scale for tool counts (EEA green tones)
3
+ * Used by the map view to color-code countries
4
+ */
5
+ export const getColorForCount = (count) => {
6
+ if (count >= 10) return '#0a5c4e'; // darkest
7
+ if (count >= 7) return '#0d7a68';
8
+ if (count >= 4) return '#289588'; // base accent
9
+ if (count >= 1) return '#6fc4b8'; // light
10
+ return '#e8eded'; // none (very light gray-green)
11
+ };
12
+
13
+ /**
14
+ * Normalize country names between GeoJSON and facet data
15
+ */
16
+ export const normalizeCountryName = (name) => {
17
+ if (!name) return name;
18
+ const normalizer = {
19
+ Türkiye: 'Turkey',
20
+ 'United Kingdom': 'United Kingdom',
21
+ 'Czech Rep.': 'Czechia',
22
+ 'Bosnia and Herz.': 'Bosnia and Herzegovina',
23
+ };
24
+ return normalizer[name] || name;
25
+ };
26
+
27
+ /**
28
+ * Build a country -> count lookup from facet data
29
+ * @param {Object} facets - searchContext.facets
30
+ * @returns {Object} countryName -> count
31
+ */
32
+ export const buildCountryCounts = (facets) => {
33
+ const countriesFacetArray = facets?.['cca_geographic_countries.keyword'];
34
+ const countriesFacet = Array.isArray(countriesFacetArray)
35
+ ? countriesFacetArray[0]
36
+ : countriesFacetArray;
37
+ const countryData = countriesFacet?.data || [];
38
+
39
+ const counts = {};
40
+ countryData.forEach((entry) => {
41
+ counts[entry.value] = entry.count;
42
+ });
43
+ return counts;
44
+ };
45
+
46
+ /**
47
+ * Legend items for the map view
48
+ */
49
+ export const mapLegendItems = [
50
+ { label: '10+', color: '#0a5c4e' },
51
+ { label: '7–9', color: '#0d7a68' },
52
+ { label: '4–6', color: '#289588' },
53
+ { label: '1–3', color: '#6fc4b8' },
54
+ { label: 'None', color: '#e8eded' },
55
+ ];
56
+
1
57
  export const asArray = (value) => {
2
58
  if (!value) return [];
3
59
  const raw = value.raw !== undefined ? value.raw : value;
@@ -67,6 +67,10 @@ const messages = defineMessages({
67
67
  id: 'Open tool',
68
68
  defaultMessage: 'Open tool',
69
69
  },
70
+ removeTool: {
71
+ id: 'Remove {title} from comparison',
72
+ defaultMessage: 'Remove {title} from comparison',
73
+ },
70
74
  backToResults: {
71
75
  id: 'Back to results',
72
76
  defaultMessage: 'Back to results',
@@ -319,7 +323,7 @@ const CompareToolsView = () => {
319
323
  )}
320
324
 
321
325
  {!isLoading && hasEnoughTools && (
322
- <Table celled>
326
+ <Table celled aria-label={compareToolsTitle} unstackable>
323
327
  <Table.Header>
324
328
  <Table.Row>
325
329
  <Table.HeaderCell>
@@ -353,9 +357,12 @@ const CompareToolsView = () => {
353
357
  )}
354
358
  <Button
355
359
  className="icon compare-tool-clear"
360
+ aria-label={intl.formatMessage(messages.removeTool, {
361
+ title: tool.title,
362
+ })}
356
363
  onClick={() => removeTool(tool.id)}
357
364
  >
358
- <Icon className="ri-close-line" />
365
+ <Icon className="ri-close-line" aria-hidden="true" />
359
366
  </Button>
360
367
  </div>
361
368
  </div>
@@ -365,7 +372,7 @@ const CompareToolsView = () => {
365
372
  </Table.Header>
366
373
  <Table.Body>
367
374
  <Table.Row>
368
- <Table.Cell>
375
+ <Table.Cell as="th" scope="row">
369
376
  <div className="compare-criteria">
370
377
  <div className="compare-criteria-title">
371
378
  {intl.formatMessage(messages.usability)}
@@ -381,7 +388,7 @@ const CompareToolsView = () => {
381
388
  ))}
382
389
  </Table.Row>
383
390
  <Table.Row>
384
- <Table.Cell>
391
+ <Table.Cell as="th" scope="row">
385
392
  <div className="compare-criteria">
386
393
  <div className="compare-criteria-title">
387
394
  {intl.formatMessage(messages.functionality)}
@@ -399,7 +406,7 @@ const CompareToolsView = () => {
399
406
  ))}
400
407
  </Table.Row>
401
408
  <Table.Row>
402
- <Table.Cell>
409
+ <Table.Cell as="th" scope="row">
403
410
  <div className="compare-criteria">
404
411
  <div className="compare-criteria-title">
405
412
  {intl.formatMessage(messages.spatialScale)}
@@ -413,7 +420,7 @@ const CompareToolsView = () => {
413
420
  ))}
414
421
  </Table.Row>
415
422
  <Table.Row>
416
- <Table.Cell>
423
+ <Table.Cell as="th" scope="row">
417
424
  <div className="compare-criteria">
418
425
  <div className="compare-criteria-title">
419
426
  {intl.formatMessage(messages.outputType)}
@@ -427,7 +434,7 @@ const CompareToolsView = () => {
427
434
  ))}
428
435
  </Table.Row>
429
436
  <Table.Row>
430
- <Table.Cell>
437
+ <Table.Cell as="th" scope="row">
431
438
  <div className="compare-criteria">
432
439
  <div className="compare-criteria-title">
433
440
  {intl.formatMessage(messages.adaptationSupportCycleStep)}
@@ -444,7 +451,7 @@ const CompareToolsView = () => {
444
451
  ))}
445
452
  </Table.Row>
446
453
  <Table.Row>
447
- <Table.Cell>
454
+ <Table.Cell as="th" scope="row">
448
455
  <div className="compare-criteria">
449
456
  <div className="compare-criteria-title">
450
457
  {intl.formatMessage(messages.sector)}