@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.
- package/dist/__mocks__/mapbox-gl.d.ts +11 -2
- package/dist/integrations/strapi/getPortfolioProjectsByBbox.d.ts +3 -0
- package/dist/models/fpm/FPMProject.d.ts +3 -0
- package/dist/slices/ProjectsMap/ProjectsMap.d.ts +1 -2
- package/dist/strapi-slices.cjs.development.js +479 -168
- package/dist/strapi-slices.cjs.development.js.map +1 -1
- package/dist/strapi-slices.cjs.production.min.js +1 -1
- package/dist/strapi-slices.cjs.production.min.js.map +1 -1
- package/dist/strapi-slices.esm.js +541 -231
- package/dist/strapi-slices.esm.js.map +1 -1
- package/package.json +2 -1
- package/src/components/SliceRenderer/SliceRenderer.tsx +0 -1
- package/src/integrations/strapi/getPortfolioProjectsByBbox.test.ts +82 -0
- package/src/integrations/strapi/getPortfolioProjectsByBbox.ts +86 -0
- package/src/models/fpm/FPMProject.ts +3 -0
- package/src/slices/ProjectsMap/ProjectsMap.stories.tsx +0 -48
- package/src/slices/ProjectsMap/ProjectsMap.test.tsx +111 -39
- package/src/slices/ProjectsMap/ProjectsMap.tsx +477 -91
- package/src/slices/TextWithCard/TextWithCard.stories.tsx +1 -0
- package/src/test/integrationMocks/fpmProjectMock.ts +1 -0
- package/dist/slices/ProjectsMap/MapMarker.d.ts +0 -12
- package/src/slices/ProjectsMap/MapMarker.test.tsx +0 -101
- package/src/slices/ProjectsMap/MapMarker.tsx +0 -102
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
@@ -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;
|