@eeacms/volto-marine-policy 2.0.3 → 2.0.5

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 (25) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/jest-addon.config.js +4 -4
  3. package/package.json +6 -4
  4. package/src/components/Blocks/DemoSitesExplorer/DemoSitesExplorerEdit.js +5 -0
  5. package/src/components/Blocks/DemoSitesExplorer/DemoSitesExplorerView.js +94 -0
  6. package/src/components/Blocks/DemoSitesExplorer/DemoSitesFilters.jsx +406 -0
  7. package/src/components/Blocks/DemoSitesExplorer/DemoSitesFilters.test.jsxZ +91 -0
  8. package/src/components/Blocks/DemoSitesExplorer/DemoSitesListing.jsx +378 -0
  9. package/src/components/Blocks/DemoSitesExplorer/DemoSitesMap.jsx +221 -0
  10. package/src/components/Blocks/DemoSitesExplorer/FeatureDisplay.jsx +97 -0
  11. package/src/components/Blocks/DemoSitesExplorer/FeatureDisplay.test.jsxZ +48 -0
  12. package/src/components/Blocks/DemoSitesExplorer/FeatureInteraction.jsx +95 -0
  13. package/src/components/Blocks/DemoSitesExplorer/InfoOverlay.jsx +79 -0
  14. package/src/components/Blocks/DemoSitesExplorer/hooks.js +20 -0
  15. package/src/components/Blocks/DemoSitesExplorer/images/icon-depth.png +0 -0
  16. package/src/components/Blocks/DemoSitesExplorer/images/icon-light.png +0 -0
  17. package/src/components/Blocks/DemoSitesExplorer/images/search.svg +3 -0
  18. package/src/components/Blocks/DemoSitesExplorer/index.js +16 -0
  19. package/src/components/Blocks/DemoSitesExplorer/mockJsdom.js +8 -0
  20. package/src/components/Blocks/DemoSitesExplorer/styles.less +376 -0
  21. package/src/components/Blocks/DemoSitesExplorer/utils.js +211 -0
  22. package/src/components/Blocks/DemoSitesExplorer/utils.test.jsZ +63 -0
  23. package/src/components/index.js +1 -0
  24. package/src/express-middleware.js +37 -0
  25. package/src/index.js +11 -5
@@ -0,0 +1,91 @@
1
+ import './mockJsdom';
2
+ import React from 'react';
3
+ import '@testing-library/jest-dom/extend-expect';
4
+ import { render } from '@testing-library/react';
5
+
6
+ import {
7
+ DemoSitesFilters,
8
+ ActiveFilters,
9
+ SearchBox,
10
+ DemoSitesFilter,
11
+ } from './DemoSitesFilters';
12
+
13
+ describe('DemoSitesFilters', () => {
14
+ const mockSetActiveFilters = jest.fn();
15
+ window.URL.createObjectURL = function () {};
16
+ global.URL.createObjectURL = jest.fn();
17
+
18
+ const mockFilters = {
19
+ sectors: { sector1: 'Sector 1', sector2: 'Sector 2' },
20
+ };
21
+
22
+ it('renders without crashing', () => {
23
+ const { container } = render(
24
+ <DemoSitesFilters
25
+ filters={mockFilters}
26
+ activeFilters={{ sectors: [] }}
27
+ setActiveFilters={mockSetActiveFilters}
28
+ />,
29
+ );
30
+
31
+ expect(container.querySelector('.filter-wrapper')).toBeInTheDocument();
32
+ });
33
+ });
34
+
35
+ describe('ActiveFilters', () => {
36
+ const mockSetActiveFilters = jest.fn();
37
+ const mockFilters = {
38
+ sectors: { sector1: 'Sector 1', sector2: 'Sector 2' },
39
+ };
40
+
41
+ it('renders without crashing', () => {
42
+ render(
43
+ <ActiveFilters
44
+ filters={mockFilters}
45
+ activeFilters={{ sectors: [] }}
46
+ setActiveFilters={mockSetActiveFilters}
47
+ />,
48
+ );
49
+ });
50
+ });
51
+
52
+ describe('SearchBox', () => {
53
+ const mockSetActiveFilters = jest.fn();
54
+ const mockFilters = {
55
+ sectors: { sector1: 'Sector 1', sector2: 'Sector 2' },
56
+ };
57
+
58
+ const mockSetSearchInput = jest.fn();
59
+ const mockSearchInput = 'freshwater';
60
+
61
+ it('renders without crashing', () => {
62
+ render(
63
+ <SearchBox
64
+ filters={mockFilters}
65
+ activeFilters={{ sectors: [] }}
66
+ setActiveFilters={mockSetActiveFilters}
67
+ searchInput={mockSearchInput}
68
+ setSearchInput={mockSetSearchInput}
69
+ />,
70
+ );
71
+ });
72
+ });
73
+
74
+ describe('DemoSitesFilter', () => {
75
+ const mockSetActiveFilters = jest.fn();
76
+ const mockFilters = {
77
+ sectors: { sector1: 'Sector 1', sector2: 'Sector 2' },
78
+ };
79
+
80
+ it('renders without crashing', () => {
81
+ render(
82
+ <DemoSitesFilter
83
+ filterTitle={'Case study filter'}
84
+ filters={mockFilters}
85
+ activeFilters={{ sectors: [] }}
86
+ setActiveFilters={mockSetActiveFilters}
87
+ filterName={'Filter name'}
88
+ />,
89
+ );
90
+ });
91
+ });
@@ -0,0 +1,378 @@
1
+ import React from 'react';
2
+ import { centerAndResetMapZoom, zoomMapToFeatures, isValidURL } from './utils';
3
+
4
+ const showPageNr = (pageNr, currentPage, numberOfPages) => {
5
+ // show first 5 pages
6
+ if (currentPage < 4 && pageNr <= 5) {
7
+ return true;
8
+ }
9
+
10
+ // show last 5 pages
11
+ if (numberOfPages - currentPage < 4 && numberOfPages - pageNr < 5) {
12
+ return true;
13
+ }
14
+
15
+ if (
16
+ currentPage >= 4 &&
17
+ numberOfPages - currentPage >= 4 &&
18
+ pageNr >= currentPage - 2 &&
19
+ pageNr <= currentPage + 2
20
+ ) {
21
+ return true;
22
+ }
23
+
24
+ return false;
25
+ };
26
+
27
+ export default function DemoSitesList(props) {
28
+ const { selectedCase, onSelectedCase, pointsSource, map } = props;
29
+ // const reSearch = new RegExp(`\\b(${searchInput})\\b`, 'gi');
30
+ const [currentPage, setCurrentPage] = React.useState(1);
31
+
32
+ const features = pointsSource
33
+ .getFeatures(selectedCase)
34
+ .sort((item1, item2) =>
35
+ item1.values_.title.localeCompare(item2.values_.title),
36
+ );
37
+ const numberOfPages = Math.ceil(features.length / 10);
38
+
39
+ const displayFatures = features.slice(
40
+ 10 * (currentPage - 1),
41
+ 10 * currentPage,
42
+ );
43
+
44
+ return displayFatures.length === 0 ? (
45
+ <>
46
+ <h3 style={{ margin: 'calc(2rem - 0.1em) 0 1rem' }}>
47
+ We could not find any results for your search criteria
48
+ </h3>
49
+ <ul>
50
+ <li>check the selected filters</li>
51
+ </ul>
52
+ </>
53
+ ) : (
54
+ <>
55
+ <div className="listing">
56
+ {selectedCase ? (
57
+ <div
58
+ className="content-box u-item listing-item result-item"
59
+ style={{
60
+ marginTop: '2em',
61
+ padding: 'em',
62
+ // border: '3px solid #f2f2f2',
63
+ // borderTop: '1em solid #f2f2f2',
64
+ paddingTop: 0,
65
+ backgroundColor: '#f2f2f2',
66
+ border: 'none',
67
+ }}
68
+ >
69
+ <div className="slot-top">
70
+ <div className="listing-body">
71
+ <h3 className="listing-header">
72
+ <a
73
+ target="_blank"
74
+ rel="noopener noreferrer"
75
+ href={selectedCase.path}
76
+ title={selectedCase.title}
77
+ >
78
+ {selectedCase.title}
79
+ </a>
80
+ </h3>
81
+ <p className="listing-description">
82
+ {selectedCase.description}
83
+ </p>
84
+ <div className="slot-bottom">
85
+ <div className="result-bottom">
86
+ {selectedCase.info ? (
87
+ <div className="result-info">
88
+ <span className="result-info-title">Info: </span>
89
+ <span>
90
+ {isValidURL(selectedCase.info) ? (
91
+ <a
92
+ href={selectedCase.info}
93
+ target="_blank"
94
+ rel="noopener noreferrer"
95
+ >
96
+ {selectedCase.info}
97
+ </a>
98
+ ) : (
99
+ <span>{selectedCase.info}</span>
100
+ )}
101
+ </span>
102
+ </div>
103
+ ) : (
104
+ ''
105
+ )}
106
+
107
+ {selectedCase.project ? (
108
+ <div className="result-info">
109
+ <span className="result-info-title">Project: </span>
110
+ <span>{selectedCase.project}</span>
111
+ </div>
112
+ ) : (
113
+ ''
114
+ )}
115
+
116
+ {selectedCase.country ? (
117
+ <div className="result-info">
118
+ <span className="result-info-title">Country: </span>
119
+ <span>{selectedCase.country}</span>
120
+ </div>
121
+ ) : (
122
+ ''
123
+ )}
124
+
125
+ {selectedCase.project_link ? (
126
+ <div className="result-info">
127
+ <span className="result-info-title">
128
+ Project link:{' '}
129
+ </span>
130
+ <span>
131
+ {isValidURL(selectedCase.project_link) ? (
132
+ <a
133
+ href={selectedCase.project_link}
134
+ target="_blank"
135
+ rel="noopener noreferrer"
136
+ >
137
+ {selectedCase.project_link}
138
+ </a>
139
+ ) : (
140
+ <span>{selectedCase.project_link}</span>
141
+ )}
142
+ </span>
143
+ </div>
144
+ ) : (
145
+ ''
146
+ )}
147
+
148
+ <div
149
+ className="result-info show-on-map"
150
+ tabIndex="0"
151
+ role="button"
152
+ onKeyDown={() => {}}
153
+ onClick={() => {
154
+ // scroll to the map
155
+ // scrollToElement('search-input');
156
+ // reset map zoom
157
+ onSelectedCase(null);
158
+ centerAndResetMapZoom(map);
159
+ map.getInteractions().array_[9].getFeatures().clear();
160
+ }}
161
+ >
162
+ <span className="result-info-title">Reset map</span>
163
+ <i className="icon ri-map-2-line"></i>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ ) : (
171
+ displayFatures.map((item, index) => {
172
+ return (
173
+ <div className="u-item listing-item result-item" key={index}>
174
+ <div className="slot-top">
175
+ <div className="listing-body">
176
+ <h3 className="listing-header">
177
+ <a
178
+ target="_blank"
179
+ rel="noopener noreferrer"
180
+ href={item.values_.path}
181
+ title={item.values_.title}
182
+ >
183
+ {item.values_.title}
184
+ </a>
185
+ </h3>
186
+ {/* <p
187
+ className="listing-description"
188
+ dangerouslySetInnerHTML={{
189
+ __html: searchInput
190
+ ? item.values_.description.replaceAll(
191
+ reSearch,
192
+ '<b>$1</b>',
193
+ )
194
+ : item.values_.description,
195
+ }}
196
+ ></p> */}
197
+ <div className="slot-bottom">
198
+ <div className="result-bottom">
199
+ {item.values_.info ? (
200
+ <div className="result-info">
201
+ <span className="result-info-title">Info: </span>
202
+ <span>
203
+ {isValidURL(item.values_.info) ? (
204
+ <a
205
+ href={item.values_.info}
206
+ target="_blank"
207
+ rel="noopener noreferrer"
208
+ >
209
+ {item.values_.info}
210
+ </a>
211
+ ) : (
212
+ <span>{item.values_.info}</span>
213
+ )}
214
+ </span>
215
+ </div>
216
+ ) : (
217
+ ''
218
+ )}
219
+
220
+ {item.values_.project ? (
221
+ <div className="result-info">
222
+ <span className="result-info-title">Project: </span>
223
+ <span>{item.values_.project}</span>
224
+ </div>
225
+ ) : (
226
+ ''
227
+ )}
228
+
229
+ {item.values_.country ? (
230
+ <div className="result-info">
231
+ <span className="result-info-title">Country: </span>
232
+ <span>{item.values_.country}</span>
233
+ </div>
234
+ ) : (
235
+ ''
236
+ )}
237
+
238
+ {item.values_.project_link ? (
239
+ <div className="result-info">
240
+ <span className="result-info-title">
241
+ Project link:{' '}
242
+ </span>
243
+ <span>
244
+ {isValidURL(item.values_.project_link) ? (
245
+ <a
246
+ href={item.values_.project_link}
247
+ target="_blank"
248
+ rel="noopener noreferrer"
249
+ >
250
+ {item.values_.project_link}
251
+ </a>
252
+ ) : (
253
+ <span>{item.values_.project_link}</span>
254
+ )}
255
+ </span>
256
+ </div>
257
+ ) : (
258
+ ''
259
+ )}
260
+
261
+ <div
262
+ className="result-info show-on-map"
263
+ tabIndex="0"
264
+ role="button"
265
+ onKeyDown={() => {}}
266
+ onClick={() => {
267
+ map
268
+ .getInteractions()
269
+ .array_[9].getFeatures()
270
+ .clear();
271
+ // scroll to the map
272
+ // scrollToElement('ol-map-container');
273
+
274
+ zoomMapToFeatures(map, [item], 5000);
275
+ onSelectedCase(item.values_);
276
+
277
+ const popupOverlay =
278
+ document.getElementById('popup-overlay');
279
+ popupOverlay.style.visibility = 'visible';
280
+
281
+ setTimeout(() => {
282
+ const coords =
283
+ item.values_.geometry.flatCoordinates;
284
+ const pixel = map.getPixelFromCoordinate(coords);
285
+ map
286
+ .getInteractions()
287
+ .array_[9].getFeatures()
288
+ .push(map.getFeaturesAtPixel(pixel)[0]);
289
+ }, 1100);
290
+ }}
291
+ >
292
+ <span className="result-info-title">Show on map</span>
293
+ <i className="icon ri-road-map-line"></i>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ );
301
+ })
302
+ )}
303
+ </div>
304
+ {!selectedCase ? (
305
+ <div className="search-body-footer">
306
+ <div className="ui centered grid">
307
+ <div className="center aligned column">
308
+ <div className="prev-next-paging">
309
+ <div className="paging-wrapper">
310
+ {currentPage !== 1 ? (
311
+ <button
312
+ className="ui button prev double-angle"
313
+ onClick={() => {
314
+ setCurrentPage(1);
315
+ }}
316
+ ></button>
317
+ ) : (
318
+ ''
319
+ )}
320
+ {currentPage !== 1 ? (
321
+ <button
322
+ className="ui button prev single-angle"
323
+ onClick={() => {
324
+ setCurrentPage(currentPage - 1);
325
+ }}
326
+ ></button>
327
+ ) : (
328
+ ''
329
+ )}
330
+ {Array.from(Array(numberOfPages).keys()).map((index) => {
331
+ const pageNr = index + 1;
332
+ return showPageNr(pageNr, currentPage, numberOfPages) ? (
333
+ <button
334
+ className={
335
+ 'ui button pagination-item' +
336
+ (currentPage === pageNr ? ' active' : '')
337
+ }
338
+ onClick={() => {
339
+ setCurrentPage(pageNr);
340
+ }}
341
+ >
342
+ {pageNr}
343
+ </button>
344
+ ) : (
345
+ ''
346
+ );
347
+ })}
348
+ {currentPage !== numberOfPages ? (
349
+ <button
350
+ className="ui button next single-angle"
351
+ onClick={() => {
352
+ setCurrentPage(currentPage + 1);
353
+ }}
354
+ ></button>
355
+ ) : (
356
+ ''
357
+ )}
358
+ {currentPage !== numberOfPages ? (
359
+ <button
360
+ className="ui button next double-angle"
361
+ onClick={() => {
362
+ setCurrentPage(numberOfPages);
363
+ }}
364
+ ></button>
365
+ ) : (
366
+ ''
367
+ )}{' '}
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ ) : (
374
+ ''
375
+ )}
376
+ </>
377
+ );
378
+ }
@@ -0,0 +1,221 @@
1
+ import React from 'react';
2
+
3
+ import cx from 'classnames';
4
+
5
+ import { Map, Layer, Layers, Controls } from '@eeacms/volto-openlayers-map/api';
6
+ import { openlayers as ol } from '@eeacms/volto-openlayers-map';
7
+
8
+ import InfoOverlay from './InfoOverlay';
9
+ import FeatureInteraction from './FeatureInteraction';
10
+ import { useMapContext } from '@eeacms/volto-openlayers-map/api';
11
+
12
+ import { centerAndResetMapZoom, getFeatures } from './utils';
13
+
14
+ const styleCache = {};
15
+ const MapContextGateway = ({ setMap }) => {
16
+ const { map } = useMapContext();
17
+ React.useEffect(() => {
18
+ setMap(map);
19
+ }, [map, setMap]);
20
+ return null;
21
+ };
22
+
23
+ export default function DemoSitesMap(props) {
24
+ const {
25
+ items,
26
+ activeItems,
27
+ hideFilters,
28
+ selectedCase,
29
+ onSelectedCase,
30
+ map,
31
+ setMap,
32
+ } = props;
33
+ const features = getFeatures(items);
34
+ const [resetMapButtonClass, setResetMapButtonClass] =
35
+ React.useState('inactive');
36
+
37
+ const [tileWMSSources] = React.useState([
38
+ new ol.source.TileWMS({
39
+ url: 'https://gisco-services.ec.europa.eu/maps/service',
40
+ params: {
41
+ // LAYERS: 'OSMBlossomComposite', OSMCartoComposite, OSMPositronComposite
42
+ LAYERS: 'OSMPositronComposite',
43
+ TILED: true,
44
+ },
45
+ serverType: 'geoserver',
46
+ transition: 0,
47
+ }),
48
+ ]);
49
+ const [pointsSource] = React.useState(
50
+ new ol.source.Vector({
51
+ features,
52
+ }),
53
+ );
54
+
55
+ const [clusterSource] = React.useState(
56
+ new ol.source.Cluster({
57
+ distance: 19,
58
+ source: pointsSource,
59
+ }),
60
+ );
61
+
62
+ React.useEffect(() => {
63
+ if (activeItems) {
64
+ pointsSource.clear();
65
+ pointsSource.addFeatures(getFeatures(activeItems));
66
+ }
67
+ }, [activeItems, pointsSource]);
68
+
69
+ React.useEffect(() => {
70
+ if (!map) return null;
71
+
72
+ const moveendListener = (e) => {
73
+ // console.log('map.getView()', map.getView());
74
+ // console.log('selectedCase', selectedCase);
75
+ const mapZoom = Math.round(map.getView().getZoom() * 10) / 10;
76
+ const mapCenter = map.getView().getCenter();
77
+
78
+ if (selectedCase) {
79
+ const coords = selectedCase.geometry.flatCoordinates;
80
+ const pixel = map.getPixelFromCoordinate(coords);
81
+ map.getInteractions().array_[9].getFeatures().clear();
82
+ map
83
+ .getInteractions()
84
+ .array_[9].getFeatures()
85
+ .push(map.getFeaturesAtPixel(pixel)[0]);
86
+ } else {
87
+ map.getInteractions().array_[9].getFeatures().clear();
88
+ }
89
+
90
+ if (
91
+ mapZoom === 4 &&
92
+ JSON.stringify(mapCenter) ===
93
+ JSON.stringify(ol.proj.transform([10, 49], 'EPSG:4326', 'EPSG:3857'))
94
+ ) {
95
+ setResetMapButtonClass('inactive');
96
+ } else {
97
+ setResetMapButtonClass('active');
98
+ }
99
+ };
100
+
101
+ map.on('moveend', moveendListener);
102
+
103
+ return () => {
104
+ map.un('moveend', moveendListener);
105
+ };
106
+ }, [map, selectedCase, resetMapButtonClass, setResetMapButtonClass]);
107
+
108
+ const clusterStyle = React.useMemo(
109
+ () => selectedClusterStyle(selectedCase),
110
+ [selectedCase],
111
+ );
112
+
113
+ const MapWithSelection = React.useMemo(() => Map, []);
114
+ // console.log('render');
115
+
116
+ return features.length > 0 ? (
117
+ <div id="ol-map-container">
118
+ <MapWithSelection
119
+ view={{
120
+ center: ol.proj.fromLonLat([10, 54]),
121
+ showFullExtent: true,
122
+ zoom: 2.5,
123
+ }}
124
+ pixelRatio={1}
125
+ // controls={ol.control.defaults({ attribution: false })}
126
+ >
127
+ <Controls attribution={false} />
128
+ <Layers>
129
+ {hideFilters ? null : (
130
+ <button
131
+ className={cx(
132
+ 'reset-map-button ui button secondary',
133
+ String(resetMapButtonClass),
134
+ )}
135
+ onClick={() => {
136
+ // scrollToElement('search-input');
137
+ onSelectedCase(null);
138
+ centerAndResetMapZoom(map);
139
+ map.getInteractions().array_[9].getFeatures().clear();
140
+ }}
141
+ >
142
+ <span className="result-info-title">Reset map</span>
143
+ <i className="icon ri-map-2-line"></i>
144
+ </button>
145
+ )}
146
+ <InfoOverlay
147
+ selectedFeature={selectedCase}
148
+ onFeatureSelect={onSelectedCase}
149
+ layerId={tileWMSSources[0]}
150
+ hideFilters={hideFilters}
151
+ />
152
+ <FeatureInteraction
153
+ onFeatureSelect={onSelectedCase}
154
+ hideFilters={hideFilters}
155
+ selectedCase={selectedCase}
156
+ />
157
+ <Layer.Tile source={tileWMSSources[0]} zIndex={0} />
158
+ <Layer.Vector
159
+ style={clusterStyle}
160
+ source={clusterSource}
161
+ zIndex={1}
162
+ />
163
+ <MapContextGateway setMap={setMap} />
164
+ </Layers>
165
+ </MapWithSelection>
166
+ </div>
167
+ ) : null;
168
+ }
169
+
170
+ const selectedClusterStyle = (selectedFeature) => {
171
+ function _clusterStyle(feature, selectedFeature) {
172
+ const size = feature.get('features').length;
173
+ let style = styleCache[size];
174
+
175
+ if (!style) {
176
+ style = new ol.style.Style({
177
+ image: new ol.style.Circle({
178
+ radius: 12 + Math.min(Math.floor(size / 3), 10),
179
+ stroke: new ol.style.Stroke({
180
+ color: '#fff',
181
+ }),
182
+ fill: new ol.style.Fill({
183
+ // 309ebc blue 3 + green 3 mix
184
+ color: '#006BB8', // #006BB8 #309ebc
185
+ }),
186
+ }),
187
+ text: new ol.style.Text({
188
+ text: size.toString(),
189
+ fill: new ol.style.Fill({
190
+ color: '#fff',
191
+ }),
192
+ }),
193
+ });
194
+ styleCache[size] = style;
195
+ }
196
+
197
+ if (size === 1) {
198
+ let color = feature.values_.features[0].values_['color'];
199
+ let width = feature.values_.features[0].values_['width'];
200
+ let radius = feature.values_.features[0].values_['radius'];
201
+ // console.log(color)
202
+ // let color = '#0083E0'; // #0083E0 #50B0A4
203
+
204
+ return new ol.style.Style({
205
+ image: new ol.style.Circle({
206
+ radius: radius,
207
+ fill: new ol.style.Fill({
208
+ color: '#fff',
209
+ }),
210
+ stroke: new ol.style.Stroke({
211
+ color: color,
212
+ width: width,
213
+ }),
214
+ }),
215
+ });
216
+ } else {
217
+ return style;
218
+ }
219
+ }
220
+ return _clusterStyle;
221
+ };