@treely/strapi-slices 7.12.0 → 7.13.0

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.
@@ -1,27 +1,27 @@
1
- import React, { useContext } from 'react';
2
- import {
3
- MAPBOX_INITIAL_ZOOM,
4
- MAPBOX_MAX_ZOOM,
5
- MAPBOX_TOKEN,
6
- MapBoxStyle,
7
- } from '../../constants/mapbox';
8
- import BBox from '../../models/BBox';
9
- import mergeBoundingBoxes from '../../utils/mergeBoundingBoxes';
10
- import { css } from '@emotion/react';
1
+ import React, {
2
+ useRef,
3
+ useEffect,
4
+ useState,
5
+ useCallback,
6
+ useContext,
7
+ } from 'react';
8
+ import mapboxgl from 'mapbox-gl';
11
9
  import {
12
10
  Box,
13
11
  DefaultSectionContainer,
14
12
  DefaultSectionHeader,
15
13
  Wrapper,
16
14
  } from 'boemly';
17
- import mapboxgl, { LngLatBoundsLike, LngLatLike, Map, Marker } from 'mapbox-gl';
18
- import { MutableRefObject, createRef, useEffect, useRef } from 'react';
19
- import { createRoot } from 'react-dom/client';
20
- import PortfolioProject from '../../models/PortfolioProject';
21
- import MinimalProviders from '../../components/MinimalProviders';
22
- import MapMarker from './MapMarker';
23
- import mapboxStyle from './mapboxStyle';
15
+ import { MAPBOX_MAX_ZOOM, MAPBOX_TOKEN } from '../../constants/mapbox';
24
16
  import { IntlContext } from '../../components/ContextProvider';
17
+ import mapboxStyle from './mapboxStyle';
18
+ import { FeatureCollection } from 'geojson';
19
+ import debounce from 'lodash/debounce';
20
+ import getPortfolioProjectsByBbox from '../../integrations/strapi/getPortfolioProjectsByBbox';
21
+ import { CreditAvailability } from '../../models/fpm/FPMProject';
22
+
23
+ const projectPinImage =
24
+ 'https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.0.2/assets/fill/map-pin-fill.svg';
25
25
 
26
26
  mapboxgl.accessToken = MAPBOX_TOKEN;
27
27
 
@@ -30,92 +30,479 @@ export interface ProjectsMapProps {
30
30
  tagline?: string;
31
31
  title?: string;
32
32
  text?: string;
33
-
34
- defaultCenterCoordinates?: {
35
- latitude: number;
36
- longitude: number;
37
- };
33
+ defaultCenterCoordinates?: { latitude: number; longitude: number };
38
34
  defaultZoomLevel?: number;
39
35
  };
40
-
41
- projects: PortfolioProject[];
42
36
  }
43
37
 
38
+ const FALLBACK_BBOX =
39
+ '-1.9950830850086163,44.4464186384987,21.995083085002875,54.12644342419196';
40
+
44
41
  export const ProjectsMap: React.FC<ProjectsMapProps> = ({
45
42
  slice,
46
- projects,
47
43
  }: ProjectsMapProps) => {
48
- const { locale } = useContext(IntlContext);
49
-
50
- const filteredProjects = projects.filter(
51
- (project) => project.geom
52
- ) as (PortfolioProject & {
53
- geom: Pick<PortfolioProject, 'geom'>;
54
- })[];
44
+ const { locale, formatMessage } = useContext(IntlContext);
45
+ const mapContainer = useRef<HTMLDivElement>(null);
46
+ const map = useRef<mapboxgl.Map | null>(null);
47
+ const animationIntervalRef = useRef<NodeJS.Timeout | null>(null);
48
+ const [featureCollection, setFeatureCollection] =
49
+ useState<FeatureCollection | null>(null);
50
+ const [isLoading, setIsLoading] = useState(false);
51
+ const initialBboxRef = useRef<string | null>(null);
52
+ const [isMapReady, setIsMapReady] = useState(false);
53
+ const [userLocation, setUserLocation] = useState<{
54
+ lat: number;
55
+ lon: number;
56
+ } | null>(null);
55
57
 
56
- const center: LngLatLike | undefined = slice.defaultCenterCoordinates
57
- ? [
58
- slice.defaultCenterCoordinates.longitude,
59
- slice.defaultCenterCoordinates.latitude,
60
- ]
61
- : undefined;
62
-
63
- const bounds: LngLatBoundsLike | undefined = center
64
- ? undefined
65
- : mergeBoundingBoxes(
66
- filteredProjects.map(
67
- (p): BBox =>
68
- [
69
- ...p.geom.coordinates.map((c) => c - 0.2),
70
- ...p.geom.coordinates.map((c) => c + 0.2),
71
- ] as BBox
72
- )
58
+ const isBboxContained = useCallback(
59
+ (innerBbox: string, outerBbox: string): boolean => {
60
+ const [innerWest, innerSouth, innerEast, innerNorth] = innerBbox
61
+ .split(',')
62
+ .map(Number);
63
+ const [outerWest, outerSouth, outerEast, outerNorth] = outerBbox
64
+ .split(',')
65
+ .map(Number);
66
+ return (
67
+ innerWest >= outerWest &&
68
+ innerEast <= outerEast &&
69
+ innerSouth >= outerSouth &&
70
+ innerNorth <= outerNorth
73
71
  );
72
+ },
73
+ []
74
+ );
74
75
 
75
- const mapContainer = useRef<HTMLDivElement>(null);
76
+ const fetchProjectsData = useCallback(async (bbox: string) => {
77
+ setIsLoading(true);
78
+ try {
79
+ const data = await getPortfolioProjectsByBbox(bbox);
80
+ setFeatureCollection(data);
81
+ } catch (error) {
82
+ console.error('Error fetching projects:', error);
83
+ } finally {
84
+ setIsLoading(false);
85
+ }
86
+ }, []);
87
+
88
+ const debouncedUpdateBbox = useCallback(
89
+ debounce(() => {
90
+ if (!map.current || !initialBboxRef.current) return;
91
+ const bounds = map.current.getBounds();
92
+ const newBbox = `${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}`;
93
+ if (!isBboxContained(newBbox, initialBboxRef.current)) {
94
+ fetchProjectsData(newBbox);
95
+ initialBboxRef.current = newBbox;
96
+ }
97
+ }, 500),
98
+ [fetchProjectsData, isBboxContained]
99
+ );
100
+
101
+ const addProjectsLayer = useCallback(() => {
102
+ if (!map.current || !featureCollection || !map.current.isStyleLoaded()) {
103
+ return;
104
+ }
105
+
106
+ const filteredFeatureCollection = {
107
+ ...featureCollection,
108
+ features: featureCollection.features.filter(
109
+ (feature) => feature.properties?.isPublic !== false
110
+ ),
111
+ };
112
+
113
+ const source = map.current.getSource('projects') as mapboxgl.GeoJSONSource;
114
+
115
+ if (!source) {
116
+ map.current.addSource('projects', {
117
+ type: 'geojson',
118
+ data: filteredFeatureCollection,
119
+ cluster: true,
120
+ clusterMaxZoom: 14,
121
+ clusterRadius: 50,
122
+ });
123
+
124
+ map.current.addLayer({
125
+ id: 'clusters',
126
+ type: 'circle',
127
+ source: 'projects',
128
+ filter: ['has', 'point_count'],
129
+ paint: {
130
+ 'circle-color': [
131
+ 'step',
132
+ ['get', 'point_count'],
133
+ '#51bbd6',
134
+ 2,
135
+ '#2A3FBA',
136
+ ],
137
+ 'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40],
138
+ 'circle-radius-transition': { duration: 300 },
139
+ 'circle-stroke-width': 5,
140
+ 'circle-stroke-color': '#2A3FBA',
141
+ 'circle-stroke-opacity': 0.4,
142
+ },
143
+ });
144
+
145
+ if (animationIntervalRef.current) {
146
+ clearInterval(animationIntervalRef.current);
147
+ }
148
+ animationIntervalRef.current = setInterval(() => {
149
+ if (!map.current) return;
150
+ const now = Date.now() / 1000;
151
+ const pulseFactor = 1 + 0.05 * Math.sin((now * 2 * Math.PI) / 2.8);
152
+ const expression = [
153
+ 'step',
154
+ ['get', 'point_count'],
155
+ 20 * pulseFactor,
156
+ 10,
157
+ 30 * pulseFactor,
158
+ 30,
159
+ 40 * pulseFactor,
160
+ ] as mapboxgl.ExpressionSpecification;
161
+ map.current.setPaintProperty('clusters', 'circle-radius', expression);
162
+ }, 50);
163
+
164
+ map.current.addLayer({
165
+ id: 'cluster-count',
166
+ type: 'symbol',
167
+ source: 'projects',
168
+ filter: ['has', 'point_count'],
169
+ layout: {
170
+ 'text-field': '{point_count_abbreviated}',
171
+ 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
172
+ 'text-size': 12,
173
+ },
174
+ paint: {
175
+ 'text-color': '#fff',
176
+ },
177
+ });
178
+
179
+ // Load SVG as PNG
180
+ fetch(projectPinImage)
181
+ .then((response) => response.text())
182
+ .then((svgText) => {
183
+ // Modify SVG color
184
+ const modifiedSvg = svgText.replace(/fill="[^"]*"/, `fill="#2A3FBA"`);
185
+ const img = new Image();
186
+ img.src = `data:image/svg+xml;base64,${btoa(modifiedSvg)}`;
187
+ return new Promise<HTMLImageElement>((resolve, reject) => {
188
+ img.onload = () => resolve(img);
189
+ img.onerror = reject;
190
+ });
191
+ })
192
+ .then((img) => {
193
+ if (!map.current) return;
194
+ const canvas = document.createElement('canvas');
195
+ canvas.width = 80;
196
+ canvas.height = 80;
197
+ const ctx = canvas.getContext('2d');
198
+ if (!ctx) return;
199
+ ctx.drawImage(img, 10, 10, 60, 60);
200
+ const pngImg = new Image();
201
+ pngImg.src = canvas.toDataURL('image/png');
202
+ pngImg.onload = () => {
203
+ map.current?.addImage('project-pin', pngImg, { pixelRatio: 2 });
204
+ map.current?.addLayer({
205
+ id: 'unclustered-point',
206
+ type: 'symbol',
207
+ source: 'projects',
208
+ filter: ['!', ['has', 'point_count']],
209
+ layout: {
210
+ 'icon-image': 'project-pin',
211
+ },
212
+ });
213
+ };
214
+ })
215
+ .catch((error) => {
216
+ console.error('Error loading project pin image:', error);
217
+ });
218
+
219
+ const popup = new mapboxgl.Popup({
220
+ closeButton: true,
221
+ closeOnClick: false,
222
+ className: 'custom-popup',
223
+ offset: [0, -20],
224
+ });
225
+
226
+ const style = document.createElement('style');
227
+ style.textContent = `
228
+ .custom-popup .mapboxgl-popup-content {
229
+ border-radius: 8px;
230
+ padding: 12px;
231
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
232
+ background: white;
233
+ color: #333;
234
+ }
235
+ .mapboxgl-popup {
236
+ max-width: 300px !important;
237
+ }
238
+ .mapboxgl-popup-close-button {
239
+ padding: 4px 8px;
240
+ font-size: 16px;
241
+ color: #666;
242
+ }
243
+ .mapboxgl-popup-close-button:hover {
244
+ background: #f0f0f0;
245
+ color: #333;
246
+ }
247
+ `;
248
+ document.head.appendChild(style);
249
+
250
+ map.current.on('click', 'unclustered-point', (e) => {
251
+ if (!e.features || !e.features[0].properties) return;
252
+ map.current!.getCanvas().style.cursor = 'pointer';
253
+
254
+ const coordinates = (e.features[0].geometry as any).coordinates.slice();
255
+ const {
256
+ title,
257
+ projectDeveloper,
258
+ slug,
259
+ portfolioHost,
260
+ creditAvailability,
261
+ } = e.features[0].properties;
262
+
263
+ // Calculate if popup would go off screen at the top
264
+ const point = map.current!.project(coordinates);
265
+ const popupHeight = 150; // Approximate height of popup
266
+ const offset: [number, number] =
267
+ point.y - popupHeight - 20 < 0 ? [0, 20] : [0, -20];
268
+
269
+ let developer = 'Unknown';
270
+ try {
271
+ const projectDeveloperRaw = projectDeveloper;
272
+ if (projectDeveloperRaw) {
273
+ developer =
274
+ JSON.parse(projectDeveloperRaw as string)?.name ?? 'Unknown';
275
+ }
276
+ } catch {
277
+ developer = 'Unknown';
278
+ }
279
+
280
+ const projectUrl =
281
+ slug && portfolioHost ? `${portfolioHost}/portfolio/${slug}` : null;
282
+
283
+ console.log('projectUrl', projectUrl);
284
+ console.log('slug', slug);
285
+ console.log('portfolioHost', portfolioHost);
286
+
287
+ const getBadgeMessage = (status: string) => {
288
+ switch (status) {
289
+ case CreditAvailability.CREDITS_AVAILABLE:
290
+ return formatMessage({
291
+ id: 'components.creditsAvailableBadge.text.yes',
292
+ });
293
+ case CreditAvailability.NO_CREDITS_AVAILABLE:
294
+ return formatMessage({
295
+ id: 'components.creditsAvailableBadge.text.no',
296
+ });
297
+ case CreditAvailability.SOME_CREDITS_AVAILABLE:
298
+ return formatMessage({
299
+ id: 'components.creditsAvailableBadge.text.some',
300
+ });
301
+ case CreditAvailability.SOON_CREDITS_AVAILABLE:
302
+ return formatMessage({
303
+ id: 'components.creditsAvailableBadge.text.notYet',
304
+ });
305
+ default:
306
+ return '';
307
+ }
308
+ };
309
+
310
+ const getBadgeColor = (status: string) => {
311
+ switch (status) {
312
+ case CreditAvailability.CREDITS_AVAILABLE:
313
+ return '#15803d';
314
+ case CreditAvailability.NO_CREDITS_AVAILABLE:
315
+ return '#b91c1c';
316
+ case CreditAvailability.SOME_CREDITS_AVAILABLE:
317
+ return '#ea580c';
318
+ case CreditAvailability.SOON_CREDITS_AVAILABLE:
319
+ return '#2563eb';
320
+ default:
321
+ return '#e0e7ff';
322
+ }
323
+ };
324
+
325
+ const badgeColor = getBadgeColor(creditAvailability);
326
+ const badgeMessage = getBadgeMessage(creditAvailability);
327
+
328
+ const badge = projectUrl
329
+ ? `<a href="${projectUrl}" target="_blank" rel="noopener" style="text-decoration: none;"><span style="display: inline-block; background: ${badgeColor}; color: white; font-weight: 600; border-radius: 4px; padding: 2px 8px; font-size: 12px; margin-bottom: 6px; cursor: pointer;">${badgeMessage}</span></a>`
330
+ : `<span style="display: inline-block; background: ${badgeColor}; color: white; font-weight: 600; border-radius: 4px; padding: 2px 8px; font-size: 12px; margin-bottom: 6px;">${badgeMessage}</span>`;
331
+
332
+ const button = projectUrl
333
+ ? `<a href="${projectUrl}" target="_blank" rel="noopener" style="display: inline-block; margin-top: 12px; padding: 4px 8px; border: 1px solid #e2e8f0; border-radius: 4px; background: #fff; font-size: 14px; font-weight: 700; text-decoration: none;">Show more info</a>`
334
+ : '';
335
+
336
+ const description = `
337
+ <div style="padding: 2px; padding-right: 16px; min-width: 180px; max-width: 260px;">
338
+ ${badge}
339
+ <h3 style="font-size: 15px; font-weight: bold;">${title}</h3>
340
+ <p style="font-size: 15px; color: #64748b;">${developer}</p>
341
+ ${button}
342
+ </div>
343
+ `;
344
+
345
+ while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
346
+ coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
347
+ }
348
+
349
+ popup
350
+ .setOffset(offset)
351
+ .setLngLat(coordinates)
352
+ .setHTML(description)
353
+ .addTo(map.current!);
354
+ });
355
+
356
+ map.current.on('click', 'clusters', (e) => {
357
+ const features = map.current?.queryRenderedFeatures(e.point, {
358
+ layers: ['clusters'],
359
+ });
360
+ if (!features || !features[0].properties) return;
361
+
362
+ const clusterId = features[0].properties.cluster_id;
363
+ const projectSource = map.current?.getSource(
364
+ 'projects'
365
+ ) as mapboxgl.GeoJSONSource;
366
+
367
+ projectSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
368
+ if (err || !map.current) return;
369
+ const coordinates = (features[0].geometry as any).coordinates;
370
+ map.current.easeTo({ center: coordinates, zoom });
371
+ });
372
+ });
373
+
374
+ map.current.on('mouseenter', 'clusters', () => {
375
+ if (map.current) map.current.getCanvas().style.cursor = 'pointer';
376
+ });
377
+ map.current.on('mouseleave', 'clusters', () => {
378
+ if (map.current) map.current.getCanvas().style.cursor = '';
379
+ });
380
+
381
+ map.current.on('mouseenter', 'unclustered-point', () => {
382
+ if (map.current) map.current.getCanvas().style.cursor = 'pointer';
383
+ });
384
+ map.current.on('mouseleave', 'unclustered-point', () => {
385
+ if (map.current) map.current.getCanvas().style.cursor = '';
386
+ });
387
+ } else {
388
+ source.setData(filteredFeatureCollection);
389
+ }
390
+ }, [featureCollection, locale, formatMessage]);
76
391
 
77
392
  useEffect(() => {
78
- const map = new Map({
79
- container: mapContainer.current || '',
80
- style: MapBoxStyle.CaliTerrain,
81
- center,
82
- zoom: slice.defaultZoomLevel || MAPBOX_INITIAL_ZOOM,
393
+ if (map.current || !mapContainer.current) return;
394
+
395
+ let initialCenter: [number, number];
396
+ let initialZoom: number;
397
+
398
+ if (slice.defaultCenterCoordinates && slice.defaultZoomLevel) {
399
+ initialCenter = [
400
+ slice.defaultCenterCoordinates.longitude,
401
+ slice.defaultCenterCoordinates.latitude,
402
+ ];
403
+ initialZoom = slice.defaultZoomLevel;
404
+ } else if (userLocation) {
405
+ initialCenter = [userLocation.lon, userLocation.lat];
406
+ initialZoom = 10;
407
+ } else {
408
+ const bbox = initialBboxRef.current || FALLBACK_BBOX;
409
+ const [west, south, east, north] = bbox.split(',').map(Number);
410
+ const bounds = new mapboxgl.LngLatBounds([west, south], [east, north]);
411
+ const center = bounds.getCenter();
412
+ initialCenter = [center.lng, center.lat];
413
+ initialZoom = 6;
414
+ }
415
+
416
+ map.current = new mapboxgl.Map({
417
+ container: mapContainer.current,
418
+ style: 'mapbox://styles/mapbox/streets-v12',
419
+ center: initialCenter,
420
+ zoom: initialZoom,
83
421
  maxZoom: MAPBOX_MAX_ZOOM,
84
- bounds,
85
422
  });
86
423
 
87
- filteredProjects
88
- // Sort by longitude, so that the markers are rendeed form right to left
89
- .sort((a, b) => b.geom.coordinates[0] - a.geom.coordinates[0])
90
- .forEach((project) => {
91
- const ref =
92
- createRef<HTMLDivElement>() as MutableRefObject<HTMLDivElement>;
93
- ref.current = document.createElement('div');
94
-
95
- createRoot(ref.current).render(
96
- <MinimalProviders locale={locale}>
97
- <MapMarker
98
- title={project.friendlyName || project.title}
99
- isPublic={project.isPublic}
100
- portfolioHost={project.portfolioHost}
101
- slug={project.slug}
102
- creditAvailability={project.creditAvailability}
103
- projectDeveloper={project.projectDeveloper?.name}
104
- />
105
- </MinimalProviders>
106
- );
107
-
108
- // Offset is needed to center the marker on the coordinates
109
- const marker = new Marker(ref.current, { offset: [-20, -40] });
110
-
111
- // No chaining here, because the mocks don't support it
112
- marker.setLngLat(project.geom.coordinates);
113
- marker.addTo(map);
114
- });
424
+ map.current.addControl(new mapboxgl.NavigationControl(), 'top-right');
425
+
426
+ map.current.on('load', () => {
427
+ setIsMapReady(true);
428
+ if (
429
+ !(slice.defaultCenterCoordinates && slice.defaultZoomLevel) &&
430
+ !userLocation
431
+ ) {
432
+ const bbox = initialBboxRef.current || FALLBACK_BBOX;
433
+ const [west, south, east, north] = bbox.split(',').map(Number);
434
+ const bounds = new mapboxgl.LngLatBounds([west, south], [east, north]);
435
+ map.current?.fitBounds(bounds, { padding: 20 });
436
+ }
437
+ });
438
+
439
+ map.current.on('moveend', () => {
440
+ if (initialBboxRef.current) {
441
+ debouncedUpdateBbox();
442
+ }
443
+ });
444
+
445
+ return () => {
446
+ debouncedUpdateBbox.cancel();
447
+ if (animationIntervalRef.current) {
448
+ clearInterval(animationIntervalRef.current);
449
+ }
450
+ map.current?.remove();
451
+ map.current = null;
452
+ };
453
+ }, [
454
+ slice.defaultCenterCoordinates,
455
+ slice.defaultZoomLevel,
456
+ userLocation,
457
+ debouncedUpdateBbox,
458
+ ]);
115
459
 
116
- // Clean up on unmount
117
- return () => map.remove();
118
- }, [locale]);
460
+ useEffect(() => {
461
+ if (slice.defaultCenterCoordinates && slice.defaultZoomLevel) {
462
+ const { latitude, longitude } = slice.defaultCenterCoordinates;
463
+ const buffer = 10;
464
+ const bbox = `${longitude - buffer},${latitude - buffer},${
465
+ longitude + buffer
466
+ },${latitude + buffer}`;
467
+ initialBboxRef.current = bbox;
468
+ fetchProjectsData(bbox);
469
+ } else if (navigator.geolocation) {
470
+ navigator.geolocation.getCurrentPosition(
471
+ (position) => {
472
+ const userLoc = {
473
+ lat: position.coords.latitude,
474
+ lon: position.coords.longitude,
475
+ };
476
+ setUserLocation(userLoc);
477
+ const buffer = 1;
478
+ const bbox = `${userLoc.lon - buffer},${userLoc.lat - buffer},${
479
+ userLoc.lon + buffer
480
+ },${userLoc.lat + buffer}`;
481
+ initialBboxRef.current = bbox;
482
+ fetchProjectsData(bbox);
483
+ },
484
+ () => {
485
+ setUserLocation(null);
486
+ initialBboxRef.current = FALLBACK_BBOX;
487
+ fetchProjectsData(FALLBACK_BBOX);
488
+ }
489
+ );
490
+ } else {
491
+ setUserLocation(null);
492
+ initialBboxRef.current = FALLBACK_BBOX;
493
+ fetchProjectsData(FALLBACK_BBOX);
494
+ }
495
+ }, [
496
+ slice.defaultCenterCoordinates,
497
+ slice.defaultZoomLevel,
498
+ fetchProjectsData,
499
+ ]);
500
+
501
+ useEffect(() => {
502
+ if (isMapReady && featureCollection && map.current?.isStyleLoaded()) {
503
+ addProjectsLayer();
504
+ }
505
+ }, [isMapReady, featureCollection, addProjectsLayer]);
119
506
 
120
507
  return (
121
508
  <DefaultSectionContainer>
@@ -144,19 +531,18 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
144
531
  ) : (
145
532
  <></>
146
533
  )}
147
-
148
534
  <Box
149
535
  height="xl"
150
536
  ref={mapContainer}
151
537
  borderRadius="xl"
152
538
  overflow="hidden"
153
539
  boxShadow={['md', null, null, 'none']}
154
- css={css`
155
- mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC);
156
- `}
157
540
  />
541
+ <>{isLoading && <Box>Loading projects...</Box>}</>
158
542
  </Wrapper>
159
543
  </Box>
160
544
  </DefaultSectionContainer>
161
545
  );
162
546
  };
547
+
548
+ export default ProjectsMap;
@@ -47,6 +47,7 @@ const portfolioProject: PortfolioProject = {
47
47
  },
48
48
  area: 2000000,
49
49
  location: 'Austria',
50
+ countryCode: 'AT',
50
51
  start: new Date('2020-01-01'),
51
52
  end: new Date('2050-12-31'),
52
53
  projectType: {
@@ -9,6 +9,7 @@ const fpmProjectMock: FPMProject = {
9
9
  },
10
10
  area: 1400000,
11
11
  location: 'Austria',
12
+ countryCode: 'AT',
12
13
  start: new Date('2020-01-01'),
13
14
  end: new Date('2050-12-31'),
14
15
  projectType: {
@@ -1,12 +0,0 @@
1
- import React from 'react';
2
- import { CreditAvailability } from '../../models/fpm/FPMProject';
3
- export interface MapMarkerProps {
4
- title: string;
5
- isPublic?: boolean;
6
- projectDeveloper?: string;
7
- slug?: string;
8
- portfolioHost?: string;
9
- creditAvailability: CreditAvailability;
10
- }
11
- declare const MapMarker: ({ title, projectDeveloper, slug, creditAvailability, portfolioHost, isPublic, }: MapMarkerProps) => React.JSX.Element;
12
- export default MapMarker;