@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.
- package/dist/integrations/strapi/getFpmProjectsByBbox.d.ts +3 -0
- package/dist/integrations/strapi/getStrapiProjects.d.ts +3 -0
- package/dist/strapi-slices.cjs.development.js +184 -113
- 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 +184 -113
- package/dist/strapi-slices.esm.js.map +1 -1
- package/dist/utils/mergeProjectData.d.ts +4 -0
- package/package.json +1 -1
- package/src/integrations/strapi/getFpmProjectsByBbox.test.ts +115 -0
- package/src/integrations/strapi/getFpmProjectsByBbox.ts +24 -0
- package/src/integrations/strapi/getPortfolioProjects.ts +5 -46
- package/src/integrations/strapi/getStrapiProjects.test.ts +167 -0
- package/src/integrations/strapi/getStrapiProjects.ts +57 -0
- package/src/slices/ProjectsMap/ProjectsMap.test.tsx +5 -5
- package/src/slices/ProjectsMap/ProjectsMap.tsx +76 -36
- package/src/utils/mergeProjectData.ts +34 -0
- package/dist/integrations/strapi/getPortfolioProjectsByBbox.d.ts +0 -3
- package/src/integrations/strapi/getPortfolioProjectsByBbox.test.ts +0 -82
- package/src/integrations/strapi/getPortfolioProjectsByBbox.ts +0 -86
|
@@ -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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
14
|
+
import getFpmProjectsByBbox from '../../integrations/strapi/getFpmProjectsByBbox';
|
|
15
15
|
|
|
16
|
-
// Mock
|
|
17
|
-
jest.mock('../../integrations/strapi/
|
|
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(
|
|
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
|
-
(
|
|
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
|
|
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 [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
77
|
-
|
|
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
|
|
80
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
531
|
+
// Geolocation not supported - use fallback
|
|
492
532
|
initialBboxRef.current = FALLBACK_BBOX;
|
|
493
533
|
fetchProjectsData(FALLBACK_BBOX);
|
|
494
534
|
}
|