@eeacms/volto-bise-policy 1.3.11 → 1.3.13

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 (39) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/package.json +2 -1
  3. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.jsx +404 -0
  4. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.test.jsx +366 -0
  5. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +192 -91
  6. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +205 -58
  7. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +329 -178
  8. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +243 -123
  9. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.jsx +61 -0
  10. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.test.jsx +89 -0
  11. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.jsx +128 -0
  12. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.test.jsx +160 -0
  13. package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.jsx +180 -0
  14. package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.test.jsx +125 -0
  15. package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.jsx +171 -0
  16. package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.test.jsx +181 -0
  17. package/src/components/manage/Blocks/CaseStudyExplorer/Pagination.jsx +81 -0
  18. package/src/components/manage/Blocks/CaseStudyExplorer/filters.js +350 -0
  19. package/src/components/manage/Blocks/CaseStudyExplorer/filters.test.js +399 -0
  20. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +28 -4
  21. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.test.js +73 -0
  22. package/src/components/manage/Blocks/CaseStudyExplorer/messages.js +138 -0
  23. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +1157 -317
  24. package/src/components/manage/Blocks/CaseStudyExplorer/testUtils.js +44 -46
  25. package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +68 -184
  26. package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +91 -158
  27. package/src/index.js +39 -0
  28. package/src/openlayers.js +51 -0
  29. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +0 -383
  30. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +0 -90
  31. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +0 -342
  32. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +0 -196
  33. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +0 -24
  34. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +0 -49
  35. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +0 -100
  36. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +0 -189
  37. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +0 -111
  38. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +0 -159
  39. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +0 -201
@@ -0,0 +1,366 @@
1
+ import './mockJsdom';
2
+ import React from 'react';
3
+ import '@testing-library/jest-dom';
4
+ import { render, screen, within, fireEvent } from '@testing-library/react';
5
+ import { IntlProvider } from 'react-intl';
6
+
7
+ import CaseStudyCard from './CaseStudyCard';
8
+
9
+ const item = {
10
+ geometry: { type: 'Point', coordinates: [14.4, 50.1] },
11
+ properties: {
12
+ title: 'Restored peatlands in the Bohemian Forest',
13
+ path: '/catalogue/peatlands-bohemian-forest',
14
+ image: 'https://example.com/image.jpg',
15
+ current_status: 'ongoing',
16
+ nrr_legislation: [
17
+ 'Article 4 - Restoration of terrestrial ecosystems',
18
+ 'Article 11 - Restoration of agricultural ecosystems',
19
+ 'Habitats Directive (HD)',
20
+ ],
21
+ habitat_ecosystem_type: [
22
+ 'Inland wetlands',
23
+ 'Forest and woodland',
24
+ 'Grassland (pastures, semi-natural and natural grasslands)',
25
+ ],
26
+ measures: [
27
+ {
28
+ id: 'measure-a',
29
+ title: 'Restore degraded peatlands',
30
+ path: '/catalogue/measures/restore-degraded-peatlands',
31
+ },
32
+ {
33
+ id: 'measure-b',
34
+ title: 'Rewetting of drained soils',
35
+ path: '/catalogue/measures/re-wetting',
36
+ },
37
+ {
38
+ id: 'measure-c',
39
+ title: 'Restore rivers and floodplains',
40
+ path: '/catalogue/measures/restore-rivers',
41
+ },
42
+ ],
43
+ },
44
+ };
45
+
46
+ const renderCard = (props = {}) =>
47
+ render(
48
+ <IntlProvider locale="en">
49
+ <CaseStudyCard item={item} {...props} />
50
+ </IntlProvider>,
51
+ );
52
+
53
+ /**
54
+ * jsdom does not lay anything out: fake a 300px wide list where 90px chips
55
+ * wrap every three per row and the "+N" trigger is 40px wide, so the two-row
56
+ * fit has something real to measure.
57
+ */
58
+ function mockChipLayout({
59
+ width = 300,
60
+ chipWidth = 90,
61
+ moreWidth = 40,
62
+ perRow = 3,
63
+ gap = 8,
64
+ rowHeight = 24,
65
+ } = {}) {
66
+ const indexOf = (element) =>
67
+ Array.prototype.indexOf.call(element.parentNode.children, element);
68
+ const descriptors = {
69
+ clientWidth: { get: () => width },
70
+ offsetWidth: {
71
+ get() {
72
+ if (!this.classList?.contains('cse-card-chip')) return 0;
73
+ return this.textContent.startsWith('+') ? moreWidth : chipWidth;
74
+ },
75
+ },
76
+ offsetTop: {
77
+ get() {
78
+ return Math.floor(indexOf(this) / perRow) * rowHeight;
79
+ },
80
+ },
81
+ offsetLeft: {
82
+ get() {
83
+ return (indexOf(this) % perRow) * (chipWidth + gap);
84
+ },
85
+ },
86
+ };
87
+ const original = {};
88
+
89
+ Object.entries(descriptors).forEach(([property, descriptor]) => {
90
+ original[property] = Object.getOwnPropertyDescriptor(
91
+ HTMLElement.prototype,
92
+ property,
93
+ );
94
+ Object.defineProperty(HTMLElement.prototype, property, {
95
+ configurable: true,
96
+ ...descriptor,
97
+ });
98
+ });
99
+
100
+ return () => {
101
+ Object.entries(original).forEach(([property, descriptor]) => {
102
+ if (descriptor) {
103
+ Object.defineProperty(HTMLElement.prototype, property, descriptor);
104
+ } else {
105
+ delete HTMLElement.prototype[property];
106
+ }
107
+ });
108
+ };
109
+ }
110
+
111
+ describe('CaseStudyCard', () => {
112
+ test('renders the title as a link to the case study, opening a new tab', () => {
113
+ renderCard();
114
+
115
+ const link = screen.getByRole('link', {
116
+ name: /Restored peatlands in the Bohemian Forest/,
117
+ });
118
+
119
+ expect(link).toHaveAttribute('href', item.properties.path);
120
+ expect(link).toHaveAttribute('target', '_blank');
121
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer');
122
+ });
123
+
124
+ test('renders the image with an empty alt (the title carries the meaning)', () => {
125
+ const { container } = renderCard();
126
+
127
+ const image = container.querySelector('.cse-card-media img');
128
+
129
+ expect(image).toHaveAttribute('src', item.properties.image);
130
+ expect(image).toHaveAttribute('alt', '');
131
+ // An empty alt makes the image decorative, so it is not exposed as an image
132
+ // in the accessibility tree: the title carries the meaning instead.
133
+ expect(screen.queryByRole('img')).toBeNull();
134
+ });
135
+
136
+ test('renders the status as a colored dot next to the title', () => {
137
+ renderCard();
138
+
139
+ const dot = document.querySelector('.cse-card-status-dot');
140
+
141
+ expect(dot).toHaveStyle({ backgroundColor: '#ff9933' });
142
+ expect(dot.closest('.cse-card-title')).not.toBeNull();
143
+ });
144
+
145
+ test('does not render the measure count in the meta row', () => {
146
+ renderCard();
147
+
148
+ expect(
149
+ screen.getByRole('button', { name: 'Show on map' }),
150
+ ).toBeInTheDocument();
151
+ expect(
152
+ within(document.querySelector('.cse-card-meta')).queryByText(
153
+ /NRR measures?$/,
154
+ ),
155
+ ).toBeNull();
156
+ expect(screen.getAllByText('3', { exact: true })).toHaveLength(1);
157
+ });
158
+
159
+ test('shows every ecosystem when the card cannot be measured', () => {
160
+ renderCard();
161
+
162
+ const list = document.querySelector(
163
+ '.cse-card-ecosystems:not(.cse-card-ecosystems-measure)',
164
+ );
165
+
166
+ item.properties.habitat_ecosystem_type.forEach((ecosystem) => {
167
+ expect(within(list).getByText(ecosystem)).toBeInTheDocument();
168
+ });
169
+ expect(within(list).queryByText(/^\+\d+$/)).toBeNull();
170
+ });
171
+
172
+ test('collapses the ecosystems that overflow two rows into a +N popup', () => {
173
+ const restore = mockChipLayout();
174
+ const ecosystems = [
175
+ 'Coastal beaches, dunes and wetlands',
176
+ 'Cropland',
177
+ 'Forest and woodland',
178
+ 'Grassland',
179
+ 'Heathland and shrub',
180
+ 'Inland wetlands',
181
+ 'Rivers and lakes',
182
+ 'Sparsely vegetated habitats',
183
+ ];
184
+
185
+ try {
186
+ render(
187
+ <IntlProvider locale="en">
188
+ <CaseStudyCard
189
+ item={{
190
+ ...item,
191
+ properties: {
192
+ ...item.properties,
193
+ habitat_ecosystem_type: ecosystems,
194
+ },
195
+ }}
196
+ />
197
+ </IntlProvider>,
198
+ );
199
+
200
+ const list = document.querySelector(
201
+ '.cse-card-ecosystems:not(.cse-card-ecosystems-measure)',
202
+ );
203
+ const visible = [...list.querySelectorAll('.cse-card-chip')].map(
204
+ (chip) => chip.textContent,
205
+ );
206
+
207
+ // Five chips plus the trigger still fit in two rows of three.
208
+ expect(visible.slice(0, 5)).toEqual(ecosystems.slice(0, 5));
209
+ expect(visible[5]).toMatch(/^\+3/);
210
+ expect(list.querySelectorAll('.cse-card-chip')).toHaveLength(6);
211
+
212
+ const trigger = screen.getByRole('button', { name: /^\+3/ });
213
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
214
+
215
+ fireEvent.click(trigger);
216
+
217
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
218
+ const dialog = screen.getByRole('dialog', {
219
+ name: 'More habitat/ecosystem values',
220
+ });
221
+ // Only the hidden values: the visible chips already speak for themselves.
222
+ ecosystems.slice(0, 5).forEach((ecosystem) => {
223
+ expect(within(dialog).queryByText(ecosystem)).toBeNull();
224
+ });
225
+ ecosystems.slice(5).forEach((ecosystem) => {
226
+ expect(within(dialog).getByText(ecosystem)).toBeInTheDocument();
227
+ });
228
+ expect(dialog.querySelectorAll('.cse-card-chip')).toHaveLength(3);
229
+
230
+ fireEvent.keyDown(document, { key: 'Escape' });
231
+ expect(screen.queryByRole('dialog')).toBeNull();
232
+ } finally {
233
+ restore();
234
+ }
235
+ });
236
+
237
+ test('collapses the related measures behind a bar owning the count', () => {
238
+ renderCard();
239
+
240
+ const toggle = screen.getByRole('button', {
241
+ name: /Related NRR measure\(s\)/,
242
+ });
243
+
244
+ expect(toggle).toHaveTextContent('Related NRR measure(s)');
245
+ expect(toggle).toHaveTextContent('3');
246
+ expect(toggle).toHaveAttribute('aria-expanded', 'false');
247
+ });
248
+
249
+ test('keeps the collapsed measures panel inert and out of the tab order', () => {
250
+ renderCard();
251
+
252
+ const toggle = screen.getByRole('button', {
253
+ name: /Related NRR measure\(s\)/,
254
+ });
255
+ const panel = document.getElementById(toggle.getAttribute('aria-controls'));
256
+
257
+ expect(panel).toHaveAttribute('inert');
258
+ expect(panel).toHaveAttribute('data-open', 'false');
259
+ });
260
+
261
+ test('expands to every related measure, with no truncation', () => {
262
+ renderCard();
263
+
264
+ const toggle = screen.getByRole('button', {
265
+ name: /Related NRR measure\(s\)/,
266
+ });
267
+
268
+ fireEvent.click(toggle);
269
+
270
+ expect(toggle).toHaveAttribute('aria-expanded', 'true');
271
+ expect(
272
+ document.getElementById(toggle.getAttribute('aria-controls')),
273
+ ).not.toHaveAttribute('inert');
274
+
275
+ item.properties.measures.forEach((measure) => {
276
+ expect(
277
+ screen.getByRole('link', { name: new RegExp(measure.title) }),
278
+ ).toHaveAttribute('href', measure.path);
279
+ });
280
+ expect(screen.queryByText(/\+\d+ more/)).toBeNull();
281
+ });
282
+
283
+ test('collapses again on a second click', () => {
284
+ renderCard();
285
+
286
+ const toggle = screen.getByRole('button', {
287
+ name: /Related NRR measure\(s\)/,
288
+ });
289
+
290
+ fireEvent.click(toggle);
291
+ fireEvent.click(toggle);
292
+
293
+ expect(toggle).toHaveAttribute('aria-expanded', 'false');
294
+ expect(
295
+ document.getElementById(toggle.getAttribute('aria-controls')),
296
+ ).toHaveAttribute('inert');
297
+ });
298
+
299
+ test('renders the open-in-new-tab affordance next to the title', () => {
300
+ renderCard();
301
+
302
+ const open = screen.getByRole('link', { name: 'Open the case study' });
303
+
304
+ expect(open).toHaveAttribute('href', item.properties.path);
305
+ expect(open.closest('.cse-card-head')).not.toBeNull();
306
+ expect(
307
+ open.closest('.cse-card-head').querySelector('.cse-card-title'),
308
+ ).not.toBeNull();
309
+ });
310
+
311
+ test('marks the card as selected', () => {
312
+ renderCard({ selected: true });
313
+
314
+ expect(screen.getByRole('article')).toHaveAttribute(
315
+ 'data-selected',
316
+ 'true',
317
+ );
318
+ });
319
+
320
+ test('asks the map to show the case study', () => {
321
+ const onShowOnMap = jest.fn();
322
+ renderCard({ onShowOnMap });
323
+
324
+ fireEvent.click(screen.getByRole('button', { name: 'Show on map' }));
325
+
326
+ expect(onShowOnMap).toHaveBeenCalledWith(item);
327
+ });
328
+
329
+ test('offers to reset the map once the card is the selected one', () => {
330
+ const onShowOnMap = jest.fn();
331
+ const onResetMap = jest.fn();
332
+ renderCard({ selected: true, onShowOnMap, onResetMap });
333
+
334
+ expect(screen.queryByRole('button', { name: 'Show on map' })).toBeNull();
335
+
336
+ fireEvent.click(screen.getByRole('button', { name: 'Reset map' }));
337
+
338
+ expect(onResetMap).toHaveBeenCalledWith(item);
339
+ expect(onShowOnMap).not.toHaveBeenCalled();
340
+ });
341
+
342
+ test('survives a case study with no measures, ecosystems or legislation', () => {
343
+ const bare = {
344
+ geometry: { type: 'Point', coordinates: [0, 0] },
345
+ properties: { title: 'Bare case', path: '/bare' },
346
+ };
347
+
348
+ render(
349
+ <IntlProvider locale="en">
350
+ <CaseStudyCard item={bare} />
351
+ </IntlProvider>,
352
+ );
353
+
354
+ expect(screen.getByText('Bare case')).toBeInTheDocument();
355
+ expect(
356
+ within(document.querySelector('.cse-card-meta')).queryByText(
357
+ /NRR measures?$/,
358
+ ),
359
+ ).toBeNull();
360
+ expect(
361
+ within(screen.getByRole('article')).queryByRole('button', {
362
+ name: /Related NRR measure\(s\)/,
363
+ }),
364
+ ).toBeNull();
365
+ });
366
+ });
@@ -1,113 +1,214 @@
1
1
  import React from 'react';
2
- import { Grid } from 'semantic-ui-react'; // Dropdown,
2
+ import { useIntl } from 'react-intl';
3
3
  import { addAppURL } from '@plone/volto/helpers/Url/Url';
4
- // import config from '@plone/volto/registry';
5
- import CaseStudyMap from './CaseStudyMap';
6
- import { ActiveFilters, CaseStudyFilters, SearchBox } from './CaseStudyFilters';
7
- // import CaseStudyList from './CaseStudyListing';
8
4
 
9
- import { filterCases, filterCasesByPath, getFilters } from './utils';
5
+ import CaseStudyMap from './CaseStudyMapLazy';
6
+ import CaseStudyResults from './CaseStudyResults';
7
+ import FilterPanel from './FilterPanel';
8
+ import {
9
+ EMPTY_FILTERS,
10
+ buildFacets,
11
+ clearFilters,
12
+ filterCases,
13
+ sortCases,
14
+ } from './filters';
10
15
  import { useCases } from './hooks';
16
+ // import { prefersReducedMotion } from './utils';
17
+ import messages from './messages';
11
18
 
12
19
  import './styles.less';
13
20
 
21
+ const measureIdFromLocation = () => {
22
+ if (typeof window === 'undefined') return null;
23
+
24
+ return new URLSearchParams(window.location.search).get('measure');
25
+ };
26
+
27
+ const withoutSelection = (filters, facet, value) => {
28
+ const selected = filters[facet] || [];
29
+
30
+ return {
31
+ ...filters,
32
+ [facet]: selected.includes(value)
33
+ ? selected.filter((item) => item !== value)
34
+ : [...selected, value],
35
+ };
36
+ };
37
+
14
38
  export default function CaseStudyExplorerView(props) {
15
- const cases_url = '/@@case-studies-map.arcgis.json';
16
- let cases = useCases(addAppURL(cases_url));
17
- const { caseStudiesIds, data } = props; // case studies from measure view
18
- const { filter_path } = data || {};
19
- const [selectedCase, onSelectedCase] = React.useState(null);
20
- const [searchInput, setSearchInput] = React.useState('');
21
- const hideFilters = caseStudiesIds ? true : false;
22
-
23
- const [activeFilters, setActiveFilters] = React.useState({
24
- nrr_legislation: [],
25
- nrr_biogeographical_region: [],
26
- measures_implemented: [],
27
- typology_of_measures: [],
28
- current_status: [],
29
- habitat_ecosystem_type: [],
30
- scale_of_planning: [],
31
- });
32
-
33
- const [activeItems, setActiveItems] = React.useState(cases);
34
- const [filters, setFilters] = React.useState([]);
39
+ const intl = useIntl();
40
+ // const cases_url = 'http://localhost:3000/@@case-studies-map.arcgis.json';
41
+ const cases_url = addAppURL('/@@case-studies-map.arcgis.json');
42
+ const { cases, loading, error, reload } = useCases(cases_url);
43
+
44
+ const { filter_path } = props.data || {};
45
+ const filterPath = filter_path;
46
+ const [measureId] = React.useState(measureIdFromLocation);
47
+
48
+ const [filters, setFilters] = React.useState(EMPTY_FILTERS);
49
+ const [selectedPath, setSelectedPath] = React.useState(null);
50
+ const [sortBy, setSortBy] = React.useState('title');
51
+ const [perPage, setPerPage] = React.useState(4);
52
+ const [page, setPage] = React.useState(1);
53
+ const [focusRequest, setFocusRequest] = React.useState(null);
54
+ const [resetRequest, setResetRequest] = React.useState(null);
35
55
  const [map, setMap] = React.useState();
36
- const pathFilteredCases = React.useMemo(
37
- () => filterCasesByPath(cases, filter_path),
38
- [cases, filter_path],
39
- );
56
+ // The explorer only exists on the client (it fetches its own data and the map
57
+ // needs OpenLayers), but the first client render has to match the empty
58
+ // server output. `mounted` keeps both sides in sync: SSR and hydration
59
+ // render nothing, the next render shows the explorer.
60
+ const [mounted, setMounted] = React.useState(false);
40
61
 
41
62
  React.useEffect(() => {
42
- const _filters = getFilters(pathFilteredCases);
43
- setFilters(_filters);
44
- }, [pathFilteredCases, activeFilters, activeItems.length]);
63
+ setMounted(true);
64
+ }, []);
45
65
 
46
- React.useEffect(() => {
47
- let activeItems = filterCases(
48
- cases,
49
- activeFilters,
50
- caseStudiesIds,
51
- searchInput,
52
- filter_path,
53
- );
66
+ const narrowing = React.useMemo(
67
+ () => ({ filterPath, measureId }),
68
+ [filterPath, measureId],
69
+ );
70
+ const facets = React.useMemo(
71
+ () => buildFacets(cases, filters, narrowing),
72
+ [cases, filters, narrowing],
73
+ );
74
+ const narrowedResults = React.useMemo(
75
+ () => sortCases(filterCases(cases, filters, narrowing), sortBy),
76
+ [cases, filters, narrowing, sortBy],
77
+ );
78
+
79
+ // Every filter change is applied immediately: there is no Apply step.
80
+ const toggleFacet = React.useCallback((facet, value) => {
81
+ setFilters((current) => withoutSelection(current, facet, value));
82
+ setPage(1);
83
+ }, []);
84
+
85
+ const changeQuery = React.useCallback((query) => {
86
+ setFilters((current) => ({ ...current, query }));
87
+ setPage(1);
88
+ }, []);
89
+
90
+ const resetFilters = React.useCallback(() => {
91
+ setFilters(clearFilters());
92
+ setPage(1);
93
+ setSelectedPath(null);
94
+ }, []);
54
95
 
55
- setActiveItems(activeItems);
56
- }, [caseStudiesIds, activeFilters, cases, searchInput, filter_path]);
96
+ // Selecting a case study on the map also brings its card into view: if it
97
+ // lives on another page of the results, jump to that page first.
98
+ const selectCase = React.useCallback(
99
+ (path) => {
100
+ setSelectedPath(path);
101
+ if (!path) return;
57
102
 
58
- if (__SERVER__) return '';
103
+ const index = narrowedResults.findIndex(
104
+ (item) => item.properties?.path === path,
105
+ );
106
+ if (index === -1) return;
107
+
108
+ setPage(Math.floor(index / perPage) + 1);
109
+
110
+ // const card = document.getElementById(
111
+ // `cse-card-${encodeURIComponent(path)}`,
112
+ // );
113
+ // card?.scrollIntoView({
114
+ // behavior: prefersReducedMotion() ? 'auto' : 'smooth',
115
+ // block: 'center',
116
+ // });
117
+ },
118
+ [narrowedResults, perPage],
119
+ );
120
+
121
+ const showOnMap = React.useCallback((item) => {
122
+ const path = item?.properties?.path;
123
+ if (!path) return;
124
+
125
+ setSelectedPath(path);
126
+ setFocusRequest({ path, nonce: Date.now() });
127
+ // scrollToElement('cse-map-container');
128
+ }, []);
129
+
130
+ // "Reset map" on an already selected card: drop the selection and send the
131
+ // map back to its default extent, exactly like the map's own reset button.
132
+ const resetMap = React.useCallback(() => {
133
+ setSelectedPath(null);
134
+ setResetRequest({ nonce: Date.now() });
135
+ }, []);
136
+
137
+ if (!mounted) return null;
138
+
139
+ // The explorer is empty until the backend answers, so say what is going on
140
+ // instead of flashing an empty map and a "no results" message.
141
+ if (loading) {
142
+ return (
143
+ <div className="cse-explorer">
144
+ <div className="cse-loading" role="status" aria-live="polite">
145
+ <span className="cse-loading-spinner" aria-hidden="true" />
146
+ {intl.formatMessage(messages.loadingCaseStudies)}
147
+ </div>
148
+ </div>
149
+ );
150
+ }
151
+
152
+ if (error) {
153
+ return (
154
+ <div className="cse-explorer">
155
+ <div className="cse-loading" role="alert">
156
+ <p className="cse-loading-message">
157
+ {intl.formatMessage(messages.errorCaseStudies)}
158
+ </p>
159
+ <button type="button" className="cse-loading-retry" onClick={reload}>
160
+ {intl.formatMessage(messages.retry)}
161
+ </button>
162
+ </div>
163
+ </div>
164
+ );
165
+ }
59
166
 
60
167
  return (
61
- <div className="searchlib-block">
62
- <Grid.Row>
63
- {hideFilters ? null : (
64
- <SearchBox
65
- filters={filters}
66
- activeFilters={activeFilters}
67
- setActiveFilters={setActiveFilters}
68
- searchInput={searchInput}
69
- setSearchInput={setSearchInput}
168
+ <div className="cse-explorer">
169
+ <FilterPanel
170
+ facets={facets}
171
+ filters={filters}
172
+ onToggle={toggleFacet}
173
+ onQueryChange={changeQuery}
174
+ onReset={resetFilters}
175
+ />
176
+
177
+ <div className="cse-layout">
178
+ <div className="cse-layout-map">
179
+ <CaseStudyMap
180
+ items={narrowedResults}
181
+ selectedPath={selectedPath}
182
+ onSelect={selectCase}
70
183
  map={map}
184
+ setMap={setMap}
185
+ focusRequest={focusRequest}
186
+ resetRequest={resetRequest}
71
187
  />
72
- )}
73
- </Grid.Row>
74
- <Grid.Row>
75
- {hideFilters ? null : (
76
- <ActiveFilters
77
- filters={filters}
78
- activeFilters={activeFilters}
79
- setActiveFilters={setActiveFilters}
80
- />
81
- )}
82
- </Grid.Row>
83
- <Grid.Row stretched={true} id="cse-filter">
84
- {hideFilters ? null : (
85
- <CaseStudyFilters
86
- filters={filters}
87
- activeFilters={activeFilters}
88
- setActiveFilters={setActiveFilters}
89
- map={map}
188
+ </div>
189
+
190
+ <div className="cse-layout-results" id="cse-results">
191
+ <CaseStudyResults
192
+ items={narrowedResults}
193
+ selectedPath={selectedPath}
194
+ onShowOnMap={showOnMap}
195
+ onResetMap={resetMap}
196
+ sortBy={sortBy}
197
+ onSortChange={(value) => {
198
+ setSortBy(value);
199
+ setPage(1);
200
+ }}
201
+ perPage={perPage}
202
+ onPerPageChange={(value) => {
203
+ setPerPage(value);
204
+ setPage(1);
205
+ }}
206
+ page={page}
207
+ onPageChange={setPage}
208
+ onResetFilters={resetFilters}
90
209
  />
91
- )}
92
- </Grid.Row>
93
- <Grid.Row>
94
- {pathFilteredCases.length ? (
95
- <Grid columns={12}>
96
- <Grid.Column mobile={12} tablet={12} computer={12}>
97
- <CaseStudyMap
98
- items={pathFilteredCases}
99
- activeItems={activeItems}
100
- hideFilters={hideFilters}
101
- selectedCase={selectedCase}
102
- onSelectedCase={onSelectedCase}
103
- searchInput={searchInput}
104
- map={map}
105
- setMap={setMap}
106
- />
107
- </Grid.Column>
108
- </Grid>
109
- ) : null}
110
- </Grid.Row>
210
+ </div>
211
+ </div>
111
212
  </div>
112
213
  );
113
214
  }