@treely/strapi-slices 7.13.0 → 7.13.1

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.
@@ -0,0 +1,4 @@
1
+ import { FeatureCollection } from 'geojson';
2
+ import { IStrapiData, StrapiProject } from '..';
3
+ declare const mergeProjectData: (featureCollection: FeatureCollection, strapiProjects: Map<string, IStrapiData<StrapiProject>>) => FeatureCollection;
4
+ export default mergeProjectData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treely/strapi-slices",
3
- "version": "7.13.0",
3
+ "version": "7.13.1",
4
4
  "license": "MIT",
5
5
  "author": "Tree.ly FlexCo",
6
6
  "description": "@treely/strapi-slices is a open source library maintained by Tree.ly.",
@@ -0,0 +1,115 @@
1
+ import MockAxios from 'jest-mock-axios';
2
+ import { FeatureCollection } from 'geojson';
3
+ import getFpmProjectsByBbox from './getFpmProjectsByBbox';
4
+
5
+ describe('The getFpmProjectsByBbox function', () => {
6
+ afterEach(() => {
7
+ MockAxios.reset();
8
+ });
9
+
10
+ const mockFeatureCollection: FeatureCollection = {
11
+ type: 'FeatureCollection',
12
+ features: [
13
+ {
14
+ type: 'Feature',
15
+ geometry: {
16
+ type: 'Point',
17
+ coordinates: [10.0, 20.0],
18
+ },
19
+ properties: {
20
+ id: 'test-project-1',
21
+ name: 'Test Project 1',
22
+ },
23
+ },
24
+ {
25
+ type: 'Feature',
26
+ geometry: {
27
+ type: 'Point',
28
+ coordinates: [15.0, 25.0],
29
+ },
30
+ properties: {
31
+ id: 'test-project-2',
32
+ name: 'Test Project 2',
33
+ },
34
+ },
35
+ ],
36
+ };
37
+
38
+ it('fetches FPM projects by bounding box successfully', async () => {
39
+ const bbox = '5,15,20,30';
40
+ const projectsPromise = getFpmProjectsByBbox(bbox);
41
+
42
+ MockAxios.mockResponseFor(
43
+ { url: '/public/projects' },
44
+ { data: mockFeatureCollection }
45
+ );
46
+
47
+ const result = await projectsPromise;
48
+
49
+ expect(result).toEqual(mockFeatureCollection);
50
+ expect(MockAxios.get).toHaveBeenCalledWith('/public/projects', {
51
+ params: {
52
+ bbox: '5,15,20,30',
53
+ },
54
+ cache: undefined,
55
+ });
56
+ });
57
+
58
+ it('handles preview mode correctly', async () => {
59
+ const bbox = '0,0,10,10';
60
+ const projectsPromise = getFpmProjectsByBbox(bbox, true);
61
+
62
+ MockAxios.mockResponseFor(
63
+ { url: '/public/projects' },
64
+ { data: mockFeatureCollection }
65
+ );
66
+
67
+ await projectsPromise;
68
+
69
+ expect(MockAxios.get).toHaveBeenCalledWith('/public/projects', {
70
+ params: {
71
+ bbox: '0,0,10,10',
72
+ },
73
+ cache: false,
74
+ });
75
+ });
76
+
77
+ it('handles non-preview mode with cache undefined', async () => {
78
+ const bbox = '0,0,10,10';
79
+ const projectsPromise = getFpmProjectsByBbox(bbox, false);
80
+
81
+ MockAxios.mockResponseFor(
82
+ { url: '/public/projects' },
83
+ { data: mockFeatureCollection }
84
+ );
85
+
86
+ await projectsPromise;
87
+
88
+ expect(MockAxios.get).toHaveBeenCalledWith('/public/projects', {
89
+ params: {
90
+ bbox: '0,0,10,10',
91
+ },
92
+ cache: undefined,
93
+ });
94
+ });
95
+
96
+ it('returns empty feature collection when no projects found', async () => {
97
+ const bbox = '0,0,10,10';
98
+ const emptyFeatureCollection: FeatureCollection = {
99
+ type: 'FeatureCollection',
100
+ features: [],
101
+ };
102
+
103
+ const projectsPromise = getFpmProjectsByBbox(bbox);
104
+
105
+ MockAxios.mockResponseFor(
106
+ { url: '/public/projects' },
107
+ { data: emptyFeatureCollection }
108
+ );
109
+
110
+ const result = await projectsPromise;
111
+
112
+ expect(result).toEqual(emptyFeatureCollection);
113
+ expect(result.features).toHaveLength(0);
114
+ });
115
+ });
@@ -0,0 +1,24 @@
1
+ import { FeatureCollection } from 'geojson';
2
+ import fpmClient from '../fpmClient';
3
+
4
+ const getFpmProjectsByBbox = async (
5
+ bbox: string,
6
+ preview: boolean = false
7
+ ): Promise<FeatureCollection> => {
8
+ const [west, south, east, north] = bbox.split(',').map(Number);
9
+ const cache = preview ? false : undefined;
10
+
11
+ const fpmResponse = await fpmClient.get<FeatureCollection>(
12
+ '/public/projects',
13
+ {
14
+ params: {
15
+ bbox: `${west},${south},${east},${north}`,
16
+ },
17
+ cache,
18
+ }
19
+ );
20
+
21
+ return fpmResponse.data;
22
+ };
23
+
24
+ export default getFpmProjectsByBbox;
@@ -1,61 +1,20 @@
1
- import {
2
- IStrapiData,
3
- IStrapiResponse,
4
- PortfolioProject,
5
- StrapiProject,
6
- } from '../..';
7
- import {
8
- STRAPI_DEFAULT_PAGE_SIZE,
9
- STRAPI_DEFAULT_POPULATE_DEPTH,
10
- } from '../../constants/strapi';
1
+ import { PortfolioProject } from '../..';
2
+ import { STRAPI_DEFAULT_POPULATE_DEPTH } from '../../constants/strapi';
11
3
  import FPMProject from '../../models/fpm/FPMProject';
12
4
  import fpmClient from '../fpmClient';
13
- import strapiClient from './strapiClient';
14
-
15
- const FALLBACK_LOCALE = 'en';
5
+ import getStrapiProjects from './getStrapiProjects';
16
6
 
17
7
  const getPortfolioProjects = async (
18
8
  locale: string = 'en',
19
9
  preview: boolean = false
20
10
  ): Promise<PortfolioProject[]> => {
21
11
  const cache = preview ? false : undefined;
22
- const params: Record<string, any> = {
23
- pLevel: STRAPI_DEFAULT_POPULATE_DEPTH,
24
- locale,
25
- 'pagination[pageSize]': STRAPI_DEFAULT_PAGE_SIZE,
26
- status: preview ? 'draft' : 'published',
27
- };
28
12
 
29
- const [
30
- { data: fpmProjects },
31
- { data: strapiProjectsLocalized },
32
- { data: strapiProjectsEnglish },
33
- ] = await Promise.all([
13
+ const [{ data: fpmProjects }, strapiProjects] = await Promise.all([
34
14
  fpmClient.get<FPMProject[]>('/public/projects', { cache }),
35
- strapiClient.get<IStrapiResponse<IStrapiData<StrapiProject>[]>>(
36
- '/projects',
37
- { params, cache }
38
- ),
39
- strapiClient.get<IStrapiResponse<IStrapiData<StrapiProject>[]>>(
40
- '/projects',
41
- {
42
- params: { ...params, locale: FALLBACK_LOCALE },
43
- cache,
44
- }
45
- ),
15
+ getStrapiProjects(locale, STRAPI_DEFAULT_POPULATE_DEPTH, preview),
46
16
  ]);
47
17
 
48
- const strapiProjects = new Map<string, IStrapiData<StrapiProject>>();
49
-
50
- for (const project of [
51
- ...strapiProjectsEnglish.data,
52
- ...strapiProjectsLocalized.data,
53
- ]) {
54
- if (project.attributes.fpmProjectId) {
55
- strapiProjects.set(project.attributes.fpmProjectId, project);
56
- }
57
- }
58
-
59
18
  return fpmProjects.map((fpmProject: FPMProject) => {
60
19
  const strapiProject = strapiProjects.get(fpmProject.id);
61
20
 
@@ -0,0 +1,167 @@
1
+ import MockAxios from 'jest-mock-axios';
2
+ import { strapiProjectMock } from '../../test/strapiMocks/strapiProject';
3
+ import getStrapiProjects from './getStrapiProjects';
4
+
5
+ describe('The getStrapiProjects function', () => {
6
+ afterEach(() => {
7
+ MockAxios.reset();
8
+ });
9
+
10
+ const mockStrapiResponse = {
11
+ data: {
12
+ data: [strapiProjectMock],
13
+ },
14
+ };
15
+
16
+ const mockEmptyResponse = {
17
+ data: {
18
+ data: [],
19
+ },
20
+ };
21
+
22
+ it('fetches Strapi projects successfully with default parameters', async () => {
23
+ const projectsPromise = getStrapiProjects();
24
+
25
+ MockAxios.mockResponseFor({ url: '/projects' }, mockStrapiResponse);
26
+ MockAxios.mockResponseFor({ url: '/projects' }, mockStrapiResponse);
27
+
28
+ const result = await projectsPromise;
29
+
30
+ expect(result).toBeInstanceOf(Map);
31
+ expect(result.size).toBe(1);
32
+ expect(result.get(strapiProjectMock.attributes.fpmProjectId!)).toEqual(
33
+ strapiProjectMock
34
+ );
35
+ });
36
+
37
+ it('fetches projects in specified locale and English fallback', async () => {
38
+ const locale = 'de';
39
+ const pLevel = '2';
40
+ const projectsPromise = getStrapiProjects(locale, pLevel);
41
+
42
+ MockAxios.mockResponseFor({ url: '/projects' }, mockStrapiResponse);
43
+ MockAxios.mockResponseFor({ url: '/projects' }, mockStrapiResponse);
44
+
45
+ await projectsPromise;
46
+
47
+ expect(MockAxios.get).toHaveBeenCalledWith('/projects', {
48
+ params: {
49
+ pLevel: '2',
50
+ locale: 'de',
51
+ 'pagination[pageSize]': '100',
52
+ status: 'published',
53
+ },
54
+ cache: undefined,
55
+ });
56
+
57
+ expect(MockAxios.get).toHaveBeenCalledWith('/projects', {
58
+ params: {
59
+ pLevel: '2',
60
+ locale: 'en',
61
+ 'pagination[pageSize]': '100',
62
+ status: 'published',
63
+ },
64
+ cache: undefined,
65
+ });
66
+ });
67
+
68
+ it('handles preview mode correctly', async () => {
69
+ const projectsPromise = getStrapiProjects('en', '1', true);
70
+
71
+ MockAxios.mockResponseFor({ url: '/projects' }, mockStrapiResponse);
72
+ MockAxios.mockResponseFor({ url: '/projects' }, mockStrapiResponse);
73
+
74
+ await projectsPromise;
75
+
76
+ expect(MockAxios.get).toHaveBeenCalledWith('/projects', {
77
+ params: {
78
+ pLevel: '1',
79
+ locale: 'en',
80
+ 'pagination[pageSize]': '100',
81
+ status: 'draft',
82
+ },
83
+ cache: false,
84
+ });
85
+
86
+ expect(MockAxios.get).toHaveBeenCalledWith('/projects', {
87
+ params: {
88
+ pLevel: '1',
89
+ locale: 'en',
90
+ 'pagination[pageSize]': '100',
91
+ status: 'draft',
92
+ },
93
+ cache: false,
94
+ });
95
+ });
96
+
97
+ it('combines projects from both locale and English requests', async () => {
98
+ const germanProject = {
99
+ ...strapiProjectMock,
100
+ id: 2,
101
+ attributes: {
102
+ ...strapiProjectMock.attributes,
103
+ fpmProjectId: 'german-project-id',
104
+ },
105
+ };
106
+
107
+ const englishProject = {
108
+ ...strapiProjectMock,
109
+ id: 3,
110
+ attributes: {
111
+ ...strapiProjectMock.attributes,
112
+ fpmProjectId: 'english-project-id',
113
+ },
114
+ };
115
+
116
+ const projectsPromise = getStrapiProjects('de');
117
+
118
+ MockAxios.mockResponseFor(
119
+ { url: '/projects' },
120
+ { data: { data: [germanProject] } }
121
+ );
122
+ MockAxios.mockResponseFor(
123
+ { url: '/projects' },
124
+ { data: { data: [englishProject] } }
125
+ );
126
+
127
+ const result = await projectsPromise;
128
+
129
+ expect(result.size).toBe(2);
130
+ expect(result.get('german-project-id')).toEqual(germanProject);
131
+ expect(result.get('english-project-id')).toEqual(englishProject);
132
+ });
133
+
134
+ it('filters out projects without fpmProjectId', async () => {
135
+ const projectWithoutFpmId = {
136
+ ...strapiProjectMock,
137
+ attributes: {
138
+ ...strapiProjectMock.attributes,
139
+ fpmProjectId: null,
140
+ },
141
+ };
142
+
143
+ const projectsPromise = getStrapiProjects();
144
+
145
+ MockAxios.mockResponseFor(
146
+ { url: '/projects' },
147
+ { data: { data: [projectWithoutFpmId] } }
148
+ );
149
+ MockAxios.mockResponseFor({ url: '/projects' }, mockEmptyResponse);
150
+
151
+ const result = await projectsPromise;
152
+
153
+ expect(result.size).toBe(0);
154
+ });
155
+
156
+ it('returns empty map when no projects are found', async () => {
157
+ const projectsPromise = getStrapiProjects();
158
+
159
+ MockAxios.mockResponseFor({ url: '/projects' }, mockEmptyResponse);
160
+ MockAxios.mockResponseFor({ url: '/projects' }, mockEmptyResponse);
161
+
162
+ const result = await projectsPromise;
163
+
164
+ expect(result).toBeInstanceOf(Map);
165
+ expect(result.size).toBe(0);
166
+ });
167
+ });
@@ -0,0 +1,57 @@
1
+ import { IStrapiData, IStrapiResponse, StrapiProject } from '../..';
2
+ import {
3
+ STRAPI_DEFAULT_PAGE_SIZE,
4
+ STRAPI_DEFAULT_POPULATE_DEPTH,
5
+ } from '../../constants/strapi';
6
+ import strapiClient from './strapiClient';
7
+
8
+ const FALLBACK_LOCALE = 'en';
9
+
10
+ const getStrapiProjects = async (
11
+ locale: string = 'en',
12
+ pLevel: string = STRAPI_DEFAULT_POPULATE_DEPTH,
13
+ preview: boolean = false
14
+ ): Promise<Map<string, IStrapiData<StrapiProject>>> => {
15
+ const cache = preview ? false : undefined;
16
+ const strapiParams: Record<string, any> = {
17
+ pLevel,
18
+ locale,
19
+ 'pagination[pageSize]': STRAPI_DEFAULT_PAGE_SIZE,
20
+ status: preview ? 'draft' : 'published',
21
+ };
22
+
23
+ const strapiProjects = new Map<string, IStrapiData<StrapiProject>>();
24
+
25
+ try {
26
+ const [strapiProjectsLocalized, strapiProjectsEnglish] = await Promise.all([
27
+ strapiClient.get<IStrapiResponse<IStrapiData<StrapiProject>[]>>(
28
+ '/projects',
29
+ { params: strapiParams, cache }
30
+ ),
31
+ strapiClient.get<IStrapiResponse<IStrapiData<StrapiProject>[]>>(
32
+ '/projects',
33
+ {
34
+ params: { ...strapiParams, locale: FALLBACK_LOCALE },
35
+ cache,
36
+ }
37
+ ),
38
+ ]);
39
+
40
+ // Process Strapi data if we got it
41
+ for (const project of [
42
+ ...strapiProjectsEnglish.data.data,
43
+ ...strapiProjectsLocalized.data.data,
44
+ ]) {
45
+ if (project.attributes.fpmProjectId) {
46
+ strapiProjects.set(project.attributes.fpmProjectId, project);
47
+ }
48
+ }
49
+ } catch (error) {
50
+ console.warn('Failed to fetch Strapi data:', error);
51
+ // Return empty map on failure
52
+ }
53
+
54
+ return strapiProjects;
55
+ };
56
+
57
+ export default getStrapiProjects;
@@ -11,10 +11,10 @@ import {
11
11
  mapGetSourceSpy,
12
12
  mapQuerySourceFeaturesSpy,
13
13
  } from '../../../__mocks__/mapbox-gl';
14
- import getPortfolioProjectsByBbox from '../../integrations/strapi/getPortfolioProjectsByBbox';
14
+ import getFpmProjectsByBbox from '../../integrations/strapi/getFpmProjectsByBbox';
15
15
 
16
- // Mock getPortfolioProjectsByBbox
17
- jest.mock('../../integrations/strapi/getPortfolioProjectsByBbox', () => ({
16
+ // Mock getFpmProjectsByBbox
17
+ jest.mock('../../integrations/strapi/getFpmProjectsByBbox', () => ({
18
18
  __esModule: true,
19
19
  default: jest.fn().mockResolvedValue({
20
20
  type: 'FeatureCollection',
@@ -93,7 +93,7 @@ describe('The ProjectsMap component', () => {
93
93
 
94
94
  // Verify the API was called with the correct bbox
95
95
  await waitFor(() => {
96
- expect(getPortfolioProjectsByBbox).toHaveBeenCalledWith(
96
+ expect(getFpmProjectsByBbox).toHaveBeenCalledWith(
97
97
  '-1.9950830850086163,44.4464186384987,21.995083085002875,54.12644342419196'
98
98
  );
99
99
  });
@@ -124,7 +124,7 @@ describe('The ProjectsMap component', () => {
124
124
  ],
125
125
  };
126
126
 
127
- (getPortfolioProjectsByBbox as jest.Mock).mockResolvedValueOnce(
127
+ (getFpmProjectsByBbox as jest.Mock).mockResolvedValueOnce(
128
128
  mockFeatureCollection
129
129
  );
130
130
  mapGetSourceSpy.mockReturnValue(null);
@@ -17,8 +17,11 @@ import { IntlContext } from '../../components/ContextProvider';
17
17
  import mapboxStyle from './mapboxStyle';
18
18
  import { FeatureCollection } from 'geojson';
19
19
  import debounce from 'lodash/debounce';
20
- import getPortfolioProjectsByBbox from '../../integrations/strapi/getPortfolioProjectsByBbox';
20
+ import getFpmProjectsByBbox from '../../integrations/strapi/getFpmProjectsByBbox';
21
+ import getStrapiProjects from '../../integrations/strapi/getStrapiProjects';
22
+ import mergeProjectData from '../../utils/mergeProjectData';
21
23
  import { CreditAvailability } from '../../models/fpm/FPMProject';
24
+ import { IStrapiData, StrapiProject } from '../..';
22
25
 
23
26
  const projectPinImage =
24
27
  'https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.0.2/assets/fill/map-pin-fill.svg';
@@ -50,10 +53,10 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
50
53
  const [isLoading, setIsLoading] = useState(false);
51
54
  const initialBboxRef = useRef<string | null>(null);
52
55
  const [isMapReady, setIsMapReady] = useState(false);
53
- const [userLocation, setUserLocation] = useState<{
54
- lat: number;
55
- lon: number;
56
- } | null>(null);
56
+ const [strapiProjects, setStrapiProjects] = useState<Map<
57
+ string,
58
+ IStrapiData<StrapiProject>
59
+ > | null>(null);
57
60
 
58
61
  const isBboxContained = useCallback(
59
62
  (innerBbox: string, outerBbox: string): boolean => {
@@ -73,17 +76,37 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
73
76
  []
74
77
  );
75
78
 
76
- const fetchProjectsData = useCallback(async (bbox: string) => {
77
- setIsLoading(true);
79
+ const fetchStrapiData = useCallback(async () => {
80
+ if (strapiProjects) return; // If we already have Strapi data, don't fetch it again
81
+
78
82
  try {
79
- const data = await getPortfolioProjectsByBbox(bbox);
80
- setFeatureCollection(data);
83
+ const data = await getStrapiProjects(locale, '2'); // pLevel = Population depth which is a param in the API request. 2 is enough to get the slug and the portfolioHost
84
+ setStrapiProjects(data);
81
85
  } catch (error) {
82
- console.error('Error fetching projects:', error);
83
- } finally {
84
- setIsLoading(false);
86
+ console.error('Error fetching Strapi projects:', error);
85
87
  }
86
- }, []);
88
+ }, [locale, strapiProjects]);
89
+
90
+ const fetchProjectsData = useCallback(
91
+ async (bbox: string) => {
92
+ setIsLoading(true);
93
+ try {
94
+ const fpmData = await getFpmProjectsByBbox(bbox);
95
+
96
+ // If we have Strapi data, merge it, otherwise show FPM data immediately
97
+ const mergedData = strapiProjects
98
+ ? mergeProjectData(fpmData, strapiProjects)
99
+ : fpmData;
100
+
101
+ setFeatureCollection(mergedData);
102
+ } catch (error) {
103
+ console.error('Error fetching projects:', error);
104
+ } finally {
105
+ setIsLoading(false);
106
+ }
107
+ },
108
+ [strapiProjects]
109
+ );
87
110
 
88
111
  const debouncedUpdateBbox = useCallback(
89
112
  debounce(() => {
@@ -280,10 +303,6 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
280
303
  const projectUrl =
281
304
  slug && portfolioHost ? `${portfolioHost}/portfolio/${slug}` : null;
282
305
 
283
- console.log('projectUrl', projectUrl);
284
- console.log('slug', slug);
285
- console.log('portfolioHost', portfolioHost);
286
-
287
306
  const getBadgeMessage = (status: string) => {
288
307
  switch (status) {
289
308
  case CreditAvailability.CREDITS_AVAILABLE:
@@ -401,10 +420,8 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
401
420
  slice.defaultCenterCoordinates.latitude,
402
421
  ];
403
422
  initialZoom = slice.defaultZoomLevel;
404
- } else if (userLocation) {
405
- initialCenter = [userLocation.lon, userLocation.lat];
406
- initialZoom = 10;
407
423
  } else {
424
+ // Always start with fallback view - don't wait for user location
408
425
  const bbox = initialBboxRef.current || FALLBACK_BBOX;
409
426
  const [west, south, east, north] = bbox.split(',').map(Number);
410
427
  const bounds = new mapboxgl.LngLatBounds([west, south], [east, north]);
@@ -425,10 +442,7 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
425
442
 
426
443
  map.current.on('load', () => {
427
444
  setIsMapReady(true);
428
- if (
429
- !(slice.defaultCenterCoordinates && slice.defaultZoomLevel) &&
430
- !userLocation
431
- ) {
445
+ if (!(slice.defaultCenterCoordinates && slice.defaultZoomLevel)) {
432
446
  const bbox = initialBboxRef.current || FALLBACK_BBOX;
433
447
  const [west, south, east, north] = bbox.split(',').map(Number);
434
448
  const bounds = new mapboxgl.LngLatBounds([west, south], [east, north]);
@@ -453,12 +467,26 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
453
467
  }, [
454
468
  slice.defaultCenterCoordinates,
455
469
  slice.defaultZoomLevel,
456
- userLocation,
457
470
  debouncedUpdateBbox,
458
471
  ]);
459
472
 
473
+ // Fetch Strapi data once on component mount
474
+ useEffect(() => {
475
+ fetchStrapiData();
476
+ }, [fetchStrapiData]);
477
+
478
+ // Handle re-merging data when Strapi data becomes available
479
+ useEffect(() => {
480
+ if (strapiProjects && featureCollection) {
481
+ const mergedData = mergeProjectData(featureCollection, strapiProjects);
482
+ setFeatureCollection(mergedData);
483
+ }
484
+ }, [strapiProjects]);
485
+
486
+ // Request user location (non-blocking)
460
487
  useEffect(() => {
461
488
  if (slice.defaultCenterCoordinates && slice.defaultZoomLevel) {
489
+ // Use provided default coordinates
462
490
  const { latitude, longitude } = slice.defaultCenterCoordinates;
463
491
  const buffer = 10;
464
492
  const bbox = `${longitude - buffer},${latitude - buffer},${
@@ -467,28 +495,40 @@ export const ProjectsMap: React.FC<ProjectsMapProps> = ({
467
495
  initialBboxRef.current = bbox;
468
496
  fetchProjectsData(bbox);
469
497
  } else if (navigator.geolocation) {
498
+ // Set fallback immediately (non-blocking)
499
+ initialBboxRef.current = FALLBACK_BBOX;
500
+ fetchProjectsData(FALLBACK_BBOX);
501
+
502
+ // Request user location asynchronously
470
503
  navigator.geolocation.getCurrentPosition(
471
504
  (position) => {
472
505
  const userLoc = {
473
506
  lat: position.coords.latitude,
474
507
  lon: position.coords.longitude,
475
508
  };
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);
509
+ if (map.current) {
510
+ map.current.easeTo({
511
+ center: [userLoc.lon, userLoc.lat],
512
+ zoom: 10,
513
+ duration: 1500,
514
+ });
515
+
516
+ // Update bbox for this location
517
+ const buffer = 1;
518
+ const bbox = `${userLoc.lon - buffer},${userLoc.lat - buffer},${
519
+ userLoc.lon + buffer
520
+ },${userLoc.lat + buffer}`;
521
+ initialBboxRef.current = bbox;
522
+ fetchProjectsData(bbox);
523
+ }
483
524
  },
484
525
  () => {
485
- setUserLocation(null);
486
- initialBboxRef.current = FALLBACK_BBOX;
487
- fetchProjectsData(FALLBACK_BBOX);
526
+ // Permission denied or error - already have fallback data loaded
527
+ // No need to re-fetch since we already loaded FALLBACK_BBOX data
488
528
  }
489
529
  );
490
530
  } else {
491
- setUserLocation(null);
531
+ // Geolocation not supported - use fallback
492
532
  initialBboxRef.current = FALLBACK_BBOX;
493
533
  fetchProjectsData(FALLBACK_BBOX);
494
534
  }