@tpzdsp/next-toolkit 1.1.0 → 1.2.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/package.json +70 -8
- package/src/assets/styles/globals.css +2 -0
- package/src/assets/styles/ol.css +122 -0
- package/src/components/Button/Button.test.tsx +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Card/Card.test.tsx +1 -1
- package/src/components/ErrorText/ErrorText.test.tsx +1 -1
- package/src/components/ErrorText/ErrorText.tsx +1 -1
- package/src/components/Heading/Heading.test.tsx +1 -1
- package/src/components/Hint/Hint.test.tsx +1 -1
- package/src/components/Hint/Hint.tsx +1 -1
- package/src/components/Modal/Modal.stories.tsx +252 -0
- package/src/components/Modal/Modal.test.tsx +248 -0
- package/src/components/Modal/Modal.tsx +61 -0
- package/src/components/Paragraph/Paragraph.test.tsx +1 -1
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +1 -2
- package/src/components/accordion/Accordion.stories.tsx +235 -0
- package/src/components/accordion/Accordion.test.tsx +199 -0
- package/src/components/accordion/Accordion.tsx +47 -0
- package/src/components/divider/RuleDivider.stories.tsx +255 -0
- package/src/components/divider/RuleDivider.test.tsx +164 -0
- package/src/components/divider/RuleDivider.tsx +18 -0
- package/src/components/dropdown/DropdownMenu.test.tsx +1 -1
- package/src/components/dropdown/useDropdownMenu.ts +1 -1
- package/src/components/index.ts +6 -2
- package/src/components/layout/header/Header.tsx +2 -1
- package/src/components/layout/header/HeaderAuthClient.tsx +17 -9
- package/src/components/layout/header/HeaderNavClient.tsx +3 -3
- package/src/components/link/ExternalLink.tsx +1 -1
- package/src/components/link/Link.tsx +1 -1
- package/src/components/select/Select.stories.tsx +336 -0
- package/src/components/select/Select.test.tsx +473 -0
- package/src/components/select/Select.tsx +132 -0
- package/src/components/select/SelectSkeleton.stories.tsx +195 -0
- package/src/components/select/SelectSkeleton.test.tsx +105 -0
- package/src/components/select/SelectSkeleton.tsx +16 -0
- package/src/components/select/common.ts +4 -0
- package/src/contexts/index.ts +0 -5
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useClickOutside.test.ts +290 -0
- package/src/hooks/useClickOutside.ts +26 -0
- package/src/index.ts +3 -0
- package/src/map/LayerSwitcherControl.ts +147 -0
- package/src/map/Map.tsx +230 -0
- package/src/map/MapContext.tsx +211 -0
- package/src/map/Popup.tsx +74 -0
- package/src/map/basemaps.ts +79 -0
- package/src/map/geocoder.ts +61 -0
- package/src/map/geometries.ts +60 -0
- package/src/map/images/basemaps/OS.png +0 -0
- package/src/map/images/basemaps/dark.png +0 -0
- package/src/map/images/basemaps/sat-map-tiler.png +0 -0
- package/src/map/images/basemaps/satellite-map-tiler.png +0 -0
- package/src/map/images/basemaps/satellite.png +0 -0
- package/src/map/images/basemaps/streets.png +0 -0
- package/src/map/images/openlayers-logo.png +0 -0
- package/src/map/index.ts +10 -0
- package/src/map/map.ts +40 -0
- package/src/map/osOpenNamesSearch.ts +54 -0
- package/src/map/projections.ts +14 -0
- package/src/ol-geocoder.d.ts +1 -0
- package/src/test/index.ts +1 -0
- package/src/types/api.ts +52 -0
- package/src/types/auth.ts +13 -0
- package/src/types/index.ts +6 -0
- package/src/types/map.ts +26 -0
- package/src/types/navigation.ts +8 -0
- package/src/types/utils.ts +13 -0
- package/src/utils/auth.ts +1 -1
- package/src/utils/http.ts +143 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/utils.ts +1 -1
- package/src/components/link/NextLinkWrapper.tsx +0 -66
- package/src/contexts/ThemeContext.tsx +0 -72
- package/src/types.ts +0 -99
- /package/src/{utils → test}/renderers.tsx +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { StaticImageData } from 'next/image';
|
|
2
|
+
import TileLayer from 'ol/layer/Tile';
|
|
3
|
+
import OSM from 'ol/source/OSM';
|
|
4
|
+
import XYZ from 'ol/source/XYZ';
|
|
5
|
+
|
|
6
|
+
import OsImage from './images/basemaps/OS.png';
|
|
7
|
+
import SatelliteMapTilerImage from './images/basemaps/satellite-map-tiler.png';
|
|
8
|
+
import SatelliteImage from './images/basemaps/satellite.png';
|
|
9
|
+
import StreetsImage from './images/basemaps/streets.png';
|
|
10
|
+
|
|
11
|
+
export const initializeBasemapLayers = () => {
|
|
12
|
+
const osmLayer = new TileLayer({
|
|
13
|
+
preload: Infinity,
|
|
14
|
+
source: new OSM(),
|
|
15
|
+
visible: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
osmLayer.set('name', 'OSM');
|
|
19
|
+
osmLayer.set('basemap', true);
|
|
20
|
+
const osImageData = OsImage as unknown as StaticImageData;
|
|
21
|
+
|
|
22
|
+
osmLayer.set('image', osImageData.src);
|
|
23
|
+
|
|
24
|
+
const osMapsLight = new TileLayer({
|
|
25
|
+
// attributions: [`© Crown Copyright and Database Rights [insert year of creation] OS AC0000807064`],
|
|
26
|
+
source: new XYZ({
|
|
27
|
+
url: '/water-quality-archive/api/maps/raster/v1/zxy/Light_3857/{z}/{x}/{y}.png',
|
|
28
|
+
}),
|
|
29
|
+
visible: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
osMapsLight.set('name', 'OS Maps Light');
|
|
33
|
+
osMapsLight.set('basemap', true);
|
|
34
|
+
osMapsLight.set('image', osImageData.src);
|
|
35
|
+
|
|
36
|
+
const osMapsOutdoor = new TileLayer({
|
|
37
|
+
// attributions: [`© Crown Copyright and Database Rights [insert year of creation] OS AC0000807064`],
|
|
38
|
+
source: new XYZ({
|
|
39
|
+
url: '/water-quality-archive/api/maps/raster/v1/zxy/Outdoor_3857/{z}/{x}/{y}.png',
|
|
40
|
+
}),
|
|
41
|
+
visible: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
osMapsOutdoor.set('name', 'OS Maps Outdoor');
|
|
45
|
+
osMapsOutdoor.set('basemap', true);
|
|
46
|
+
const streetsImageData = StreetsImage as unknown as StaticImageData;
|
|
47
|
+
|
|
48
|
+
osMapsOutdoor.set('image', streetsImageData.src);
|
|
49
|
+
|
|
50
|
+
const osMapsRoad = new TileLayer({
|
|
51
|
+
// attributions: [`© Crown Copyright and Database Rights [insert year of creation] OS AC0000807064`],
|
|
52
|
+
source: new XYZ({
|
|
53
|
+
url: '/water-quality-archive/api/maps/raster/v1/zxy/Road_3857/{z}/{x}/{y}.png',
|
|
54
|
+
}),
|
|
55
|
+
visible: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
osMapsRoad.set('name', 'OS Maps Road');
|
|
59
|
+
osMapsRoad.set('basemap', true);
|
|
60
|
+
const satelliteMapTilerImageData = SatelliteMapTilerImage as unknown as StaticImageData;
|
|
61
|
+
|
|
62
|
+
osMapsRoad.set('image', satelliteMapTilerImageData.src);
|
|
63
|
+
|
|
64
|
+
const worldImagery = new TileLayer({
|
|
65
|
+
source: new XYZ({
|
|
66
|
+
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
|
67
|
+
maxZoom: 19,
|
|
68
|
+
}),
|
|
69
|
+
visible: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
worldImagery.set('name', 'ESRI World Imagery');
|
|
73
|
+
worldImagery.set('basemap', true);
|
|
74
|
+
const satelliteImageData = SatelliteImage as unknown as StaticImageData;
|
|
75
|
+
|
|
76
|
+
worldImagery.set('image', satelliteImageData.src);
|
|
77
|
+
|
|
78
|
+
return [osmLayer, osMapsLight, osMapsOutdoor, osMapsRoad, worldImagery];
|
|
79
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Map } from 'ol';
|
|
2
|
+
import { transformExtent } from 'ol/proj';
|
|
3
|
+
import Geocoder from 'ol-geocoder';
|
|
4
|
+
|
|
5
|
+
import { EPSG_3857, EPSG_4326 } from './geometries';
|
|
6
|
+
import { osOpenNamesSearch } from './osOpenNamesSearch';
|
|
7
|
+
|
|
8
|
+
type AddressChosenEvent = {
|
|
9
|
+
place: {
|
|
10
|
+
lon: number;
|
|
11
|
+
lat: number;
|
|
12
|
+
bbox?: [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]
|
|
13
|
+
address: {
|
|
14
|
+
name: string;
|
|
15
|
+
city?: string;
|
|
16
|
+
state?: string;
|
|
17
|
+
country?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const initializeGeocoder = (apiKey: string, geoCoderUrl: string, map: Map) => {
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
console.warn('No API key provided for geocoder initialization.');
|
|
25
|
+
|
|
26
|
+
throw new Error(
|
|
27
|
+
'API key is required for geocoder initialization. Please provide a valid API key.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const provider = osOpenNamesSearch({
|
|
32
|
+
url: geoCoderUrl,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const geocoder = new Geocoder('nominatim', {
|
|
36
|
+
provider,
|
|
37
|
+
key: apiKey,
|
|
38
|
+
lang: 'en',
|
|
39
|
+
placeholder: 'Search for ...',
|
|
40
|
+
targetType: 'text-input',
|
|
41
|
+
keepOpen: false,
|
|
42
|
+
preventMarker: true,
|
|
43
|
+
debug: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Since we provide a bbox in the custom provider response,
|
|
47
|
+
// we also need to handle the address chosen event.
|
|
48
|
+
geocoder.on('addresschosen', (event: AddressChosenEvent) => {
|
|
49
|
+
if (event.place.bbox) {
|
|
50
|
+
const extent3857 = transformExtent(event.place.bbox, EPSG_4326, EPSG_3857);
|
|
51
|
+
|
|
52
|
+
map.getView().fit(extent3857, {
|
|
53
|
+
duration: 500,
|
|
54
|
+
padding: [50, 50, 50, 50],
|
|
55
|
+
maxZoom: 18,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return geocoder;
|
|
61
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { transform } from 'ol/proj';
|
|
2
|
+
import proj4 from 'proj4';
|
|
3
|
+
|
|
4
|
+
import './projections';
|
|
5
|
+
|
|
6
|
+
export const EPSG_27700 = 'EPSG:27700';
|
|
7
|
+
export const EPSG_4326 = 'EPSG:4326';
|
|
8
|
+
export const EPSG_3857 = 'EPSG:3857';
|
|
9
|
+
const POINT_LENGTH = 2;
|
|
10
|
+
const BBOX_LENGTH = 4;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts coordinates from EPSG:27700 (OSGB36) to EPSG:4326 (WGS84).
|
|
14
|
+
* Supports single points (x, y) or bounding boxes (xmin, ymin, xmax, ymax).
|
|
15
|
+
*
|
|
16
|
+
* @param coords - Either an [x, y] array or a [xmin, ymin, xmax, ymax] array.
|
|
17
|
+
*
|
|
18
|
+
* @returns Converted coordinates in EPSG:4326.
|
|
19
|
+
*/
|
|
20
|
+
export const transform27700to4326 = (
|
|
21
|
+
coords: [number, number] | [number, number, number, number],
|
|
22
|
+
) => {
|
|
23
|
+
if (coords.length === POINT_LENGTH) {
|
|
24
|
+
// Single point transformation
|
|
25
|
+
return proj4(EPSG_27700, EPSG_4326, coords);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (coords.length === BBOX_LENGTH) {
|
|
29
|
+
// Bounding box transformation (SW and NE corners)
|
|
30
|
+
const [xmin, ymin, xmax, ymax] = coords;
|
|
31
|
+
|
|
32
|
+
const [swLon, swLat] = proj4(EPSG_27700, EPSG_4326, [xmin, ymin]);
|
|
33
|
+
const [neLon, neLat] = proj4(EPSG_27700, EPSG_4326, [xmax, ymax]);
|
|
34
|
+
|
|
35
|
+
const minLon = Math.min(swLon, neLon);
|
|
36
|
+
const maxLon = Math.max(swLon, neLon);
|
|
37
|
+
const minLat = Math.min(swLat, neLat);
|
|
38
|
+
const maxLat = Math.max(swLat, neLat);
|
|
39
|
+
|
|
40
|
+
return [minLon, minLat, maxLon, maxLat]; // Correct bbox
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error('Invalid coordinate input. Must be [x, y] or [xmin, ymin, xmax, ymax].');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Transforms polygon coordinates from one projection to another.
|
|
48
|
+
*
|
|
49
|
+
* @param coords - A polygon in the format [[[x, y], ...]].
|
|
50
|
+
* @param from - Source projection (e.g., 'EPSG:27700').
|
|
51
|
+
* @param to - Target projection (e.g., 'EPSG:3857').
|
|
52
|
+
* @returns Transformed polygon coordinates.
|
|
53
|
+
*/
|
|
54
|
+
export const transformPolygonCoords = (
|
|
55
|
+
coords: number[][][],
|
|
56
|
+
from: string,
|
|
57
|
+
to: string,
|
|
58
|
+
): number[][][] => {
|
|
59
|
+
return coords.map((ring) => ring.map(([x, y]) => transform([x, y], from, to)));
|
|
60
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/map/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './basemaps';
|
|
2
|
+
export * from './geocoder';
|
|
3
|
+
export * from './geometries';
|
|
4
|
+
export * from './LayerSwitcherControl';
|
|
5
|
+
export * from './map';
|
|
6
|
+
export * from './Map';
|
|
7
|
+
export * from './MapContext';
|
|
8
|
+
export * from './osOpenNamesSearch';
|
|
9
|
+
export * from './Popup';
|
|
10
|
+
export * from './projections';
|
package/src/map/map.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Map } from 'ol';
|
|
2
|
+
|
|
3
|
+
import type { PopupDirection } from '../types/map';
|
|
4
|
+
|
|
5
|
+
export const LAYER_NAMES = {
|
|
6
|
+
Aoi: 'aoi-layer',
|
|
7
|
+
Boundary: 'boundary-layer',
|
|
8
|
+
SamplingPoints: 'sampling-points-layer',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const POPUP_WIDTH = 300;
|
|
12
|
+
const POPUP_HEIGHT = 300;
|
|
13
|
+
|
|
14
|
+
export const getPopupPositionClass = (coordinate: number[], map: Map): PopupDirection => {
|
|
15
|
+
const pixel = map.getPixelFromCoordinate(coordinate);
|
|
16
|
+
const [x, y] = pixel;
|
|
17
|
+
|
|
18
|
+
const isTop = y > POPUP_HEIGHT;
|
|
19
|
+
const isBottom = y < POPUP_HEIGHT;
|
|
20
|
+
const isLeft = x > POPUP_WIDTH;
|
|
21
|
+
const isRight = x < POPUP_WIDTH;
|
|
22
|
+
|
|
23
|
+
if (isTop && isLeft) {
|
|
24
|
+
return 'top-left';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isBottom && isLeft) {
|
|
28
|
+
return 'bottom-left';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isBottom && isRight) {
|
|
32
|
+
return 'bottom-right';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isTop && isRight) {
|
|
36
|
+
return 'top-right';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'bottom-right';
|
|
40
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FeatureCollection } from 'geojson';
|
|
2
|
+
|
|
3
|
+
export type SearchOption = {
|
|
4
|
+
url?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom provider for OS OpenNames search.
|
|
9
|
+
*/
|
|
10
|
+
export const osOpenNamesSearch = (options?: SearchOption) => {
|
|
11
|
+
const { url } = options ?? {};
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
/**
|
|
15
|
+
* Get the URL and parameters needed for the request.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} opt - Options object containing the query.
|
|
18
|
+
*/
|
|
19
|
+
getParameters: (opt: { query: string }) => {
|
|
20
|
+
return {
|
|
21
|
+
url,
|
|
22
|
+
params: {
|
|
23
|
+
query: opt.query,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Process the API response and format it for ol-geocoder.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} results - OS Names API response.
|
|
32
|
+
*/
|
|
33
|
+
handleResponse: (response: FeatureCollection) => {
|
|
34
|
+
const { features } = response;
|
|
35
|
+
|
|
36
|
+
if (!features?.length) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return features.map((feature) => {
|
|
41
|
+
if (feature.geometry.type === 'Point') {
|
|
42
|
+
return {
|
|
43
|
+
lon: feature.geometry.coordinates[0],
|
|
44
|
+
lat: feature.geometry.coordinates[1],
|
|
45
|
+
address: feature?.properties?.address,
|
|
46
|
+
bbox: feature.bbox,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.error('Geometry type is not Point');
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { register } from 'ol/proj/proj4';
|
|
2
|
+
import proj4 from 'proj4';
|
|
3
|
+
|
|
4
|
+
// Define EPSG:27700 (British National Grid)
|
|
5
|
+
proj4.defs(
|
|
6
|
+
'EPSG:27700',
|
|
7
|
+
'+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' +
|
|
8
|
+
'+x_0=400000 +y_0=-100000 +ellps=airy ' +
|
|
9
|
+
'+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' +
|
|
10
|
+
'+units=m +no_defs',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
// Register the projection with OpenLayers
|
|
14
|
+
register(proj4);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module 'ol-geocoder';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './renderers';
|
package/src/types/api.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// API related types
|
|
2
|
+
export type ApiResponse<T = unknown> = {
|
|
3
|
+
data: T;
|
|
4
|
+
success: boolean;
|
|
5
|
+
message?: string;
|
|
6
|
+
errors?: Record<string, string[]>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PaginationMeta = {
|
|
10
|
+
page: number;
|
|
11
|
+
perPage: number;
|
|
12
|
+
total: number;
|
|
13
|
+
totalPages: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PaginatedResponse<T = unknown> = {
|
|
17
|
+
meta: PaginationMeta;
|
|
18
|
+
} & ApiResponse<T[]>;
|
|
19
|
+
|
|
20
|
+
// Since we only ever fetch a single API we don't need multiple failure types,
|
|
21
|
+
// so defining a global one as the default is fine. This can be changed per-app.
|
|
22
|
+
export type ApiFailure = { message: string };
|
|
23
|
+
|
|
24
|
+
type GenericResponse<Ok, Error> =
|
|
25
|
+
| ({
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
json(): Promise<Ok | null>;
|
|
28
|
+
} & globalThis.Response)
|
|
29
|
+
| ({
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
json(): Promise<Error>;
|
|
32
|
+
} & globalThis.Response);
|
|
33
|
+
|
|
34
|
+
export type Response<Ok, Error> = GenericResponse<Ok, Error>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Typesafe version of the normal `fetch` function. It takes 2 generics:
|
|
38
|
+
* - `Ok` which defines type type of a successful request
|
|
39
|
+
* - `Error` which defines the type of a failed request (defaults to the `ApiFailure` type)
|
|
40
|
+
*
|
|
41
|
+
* @param input The URL to fetch
|
|
42
|
+
* @param init Any fetch options, like cache, headers, body, etc.
|
|
43
|
+
*/
|
|
44
|
+
export declare function fetch<Ok = never, Error = ApiFailure>(
|
|
45
|
+
input: RequestInfo | URL,
|
|
46
|
+
init?: RequestInit,
|
|
47
|
+
): Promise<Response<Ok, Error>>;
|
|
48
|
+
|
|
49
|
+
export type WrappedApiResponse<T> = Promise<
|
|
50
|
+
| { success: true; data: T }
|
|
51
|
+
| { success: false; message: string; status?: number; details?: string[] }
|
|
52
|
+
>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Define an interface for the decoded JWT payload
|
|
2
|
+
export type DecodedJWT = {
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
groupInfoIds: string[];
|
|
6
|
+
exp?: number; // Optional: JWT expiration timestamp
|
|
7
|
+
[key: string]: unknown; // Additional claims
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type Credentials = {
|
|
11
|
+
token: string;
|
|
12
|
+
user: DecodedJWT;
|
|
13
|
+
};
|
package/src/types/map.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type PopupDirection = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
2
|
+
|
|
3
|
+
export type MapConfig = {
|
|
4
|
+
center: number[];
|
|
5
|
+
zoom: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type Layer = {
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type MapSearchResults = {
|
|
14
|
+
results: {
|
|
15
|
+
GAZETTEER_ENTRY: {
|
|
16
|
+
GEOMETRY_Y: number;
|
|
17
|
+
NAME1: string;
|
|
18
|
+
};
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type TransformedResult = {
|
|
23
|
+
lon: number;
|
|
24
|
+
lat: number;
|
|
25
|
+
address: { name: string };
|
|
26
|
+
}[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
|
|
3
|
+
// Utility types for better developer experience
|
|
4
|
+
export type PropsWithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
|
5
|
+
export type PropsWithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
6
|
+
|
|
7
|
+
export type ExtendProps<
|
|
8
|
+
// `ComponentProps` internally constrains `Comp` to be `JSXElementConstructor<any>`,
|
|
9
|
+
// and since our type must have the same constraints to avoid errors, `any` is required here
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
Comp extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>,
|
|
12
|
+
Props = object,
|
|
13
|
+
> = Props & Omit<ComponentProps<Comp>, keyof Props>;
|
package/src/utils/auth.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { NextRequest } from 'next/server';
|
|
|
5
5
|
|
|
6
6
|
import { COOKIE_NAME } from './constants';
|
|
7
7
|
import { decodeAuthToken } from './utils';
|
|
8
|
-
import type { Credentials } from '../types';
|
|
8
|
+
import type { Credentials } from '../types/auth';
|
|
9
9
|
|
|
10
10
|
export const getCredentials = async (source?: NextRequest | null): Promise<Credentials | null> => {
|
|
11
11
|
const cookieStore = source ? source?.cookies : await cookies();
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Response, ApiFailure } from '../types/api';
|
|
2
|
+
|
|
3
|
+
export const MimeTypes = {
|
|
4
|
+
Json: 'application/json',
|
|
5
|
+
GeoJson: 'application/geo+json',
|
|
6
|
+
Png: 'image/png',
|
|
7
|
+
XJsonLines: 'application/x-jsonlines',
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export const Http = {
|
|
11
|
+
Ok: 200,
|
|
12
|
+
Created: 201,
|
|
13
|
+
NoContent: 204,
|
|
14
|
+
BadRequest: 400,
|
|
15
|
+
NotFound: 404,
|
|
16
|
+
NotAllowed: 405,
|
|
17
|
+
InternalServerError: 500,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export const HttpMethod = {
|
|
21
|
+
Get: 'GET',
|
|
22
|
+
Post: 'POST',
|
|
23
|
+
Put: 'PUT',
|
|
24
|
+
Patch: 'PATCH',
|
|
25
|
+
Delete: 'DELETE',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
type NodeGlobal = {
|
|
29
|
+
process?: {
|
|
30
|
+
env?: Record<string, string | undefined>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type BrowserGlobal = {
|
|
35
|
+
btoa?: (input: string) => string;
|
|
36
|
+
Buffer?: {
|
|
37
|
+
from: (input: string) => {
|
|
38
|
+
toString: (encoding: string) => string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isNodeEnvironment = (): boolean => {
|
|
44
|
+
const global = globalThis as NodeGlobal;
|
|
45
|
+
|
|
46
|
+
return typeof global.process?.env !== 'undefined';
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getNodeEnvVar = (key: string): string | undefined => {
|
|
50
|
+
if (!isNodeEnvironment()) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const global = globalThis as NodeGlobal;
|
|
55
|
+
|
|
56
|
+
return global.process?.env?.[key];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const encodeBase64 = (input: string): string => {
|
|
60
|
+
const global = globalThis as BrowserGlobal;
|
|
61
|
+
|
|
62
|
+
// Check for Buffer (Node.js)
|
|
63
|
+
if (global.Buffer) {
|
|
64
|
+
return global.Buffer.from(input).toString('base64');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for btoa (Browser)
|
|
68
|
+
if (global.btoa) {
|
|
69
|
+
return global.btoa(input);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error('Base64 encoding not available in this environment');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getAuthorizationHeader = (): { Authorization: string } | Record<string, never> => {
|
|
76
|
+
const apiHasAuth = getNodeEnvVar('API_HAS_AUTH');
|
|
77
|
+
const apiUsername = getNodeEnvVar('API_USERNAME');
|
|
78
|
+
const apiPassword = getNodeEnvVar('API_PASSWORD');
|
|
79
|
+
|
|
80
|
+
if (apiHasAuth && apiUsername && apiPassword) {
|
|
81
|
+
const credentials = `${apiUsername}:${apiPassword}`;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
Authorization: `Basic ${encodeBase64(credentials)}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// common fetch wrapper to provide auth headers if necessary
|
|
92
|
+
declare const fetch: <Ok = unknown, Error = ApiFailure>(
|
|
93
|
+
input: RequestInfo | URL,
|
|
94
|
+
init?: RequestInit,
|
|
95
|
+
) => Promise<Response<Ok, Error>>;
|
|
96
|
+
|
|
97
|
+
export const fetchWithAuth = <Ok, Error = ApiFailure>(
|
|
98
|
+
url: string,
|
|
99
|
+
options?: RequestInit,
|
|
100
|
+
): Promise<Response<Ok, Error>> => {
|
|
101
|
+
const requestOptions = {
|
|
102
|
+
...options,
|
|
103
|
+
headers: { ...options?.headers, ...getAuthorizationHeader() },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Use your custom fetch declaration directly
|
|
107
|
+
return fetch<Ok, Error>(url, requestOptions);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const get = <Ok, Error = ApiFailure>(
|
|
111
|
+
url: string,
|
|
112
|
+
options?: RequestInit,
|
|
113
|
+
): Promise<Response<Ok, Error>> => {
|
|
114
|
+
const requestOptions = {
|
|
115
|
+
...options,
|
|
116
|
+
method: HttpMethod.Get,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return fetchWithAuth<Ok, Error>(url, requestOptions);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const post = <Ok, Error = ApiFailure>(
|
|
123
|
+
url: string,
|
|
124
|
+
body: unknown,
|
|
125
|
+
options?: RequestInit,
|
|
126
|
+
): Promise<Response<Ok, Error>> => {
|
|
127
|
+
const requestOptions = {
|
|
128
|
+
...options,
|
|
129
|
+
method: HttpMethod.Post,
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': MimeTypes.Json,
|
|
133
|
+
...options?.headers,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return fetchWithAuth<Ok, Error>(url, requestOptions);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const FetchWrapper = {
|
|
141
|
+
get,
|
|
142
|
+
post,
|
|
143
|
+
};
|
package/src/utils/index.ts
CHANGED
package/src/utils/utils.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decode } from 'jsonwebtoken';
|
|
2
2
|
import { twMerge } from 'tailwind-merge';
|
|
3
3
|
|
|
4
|
-
import type { Credentials, DecodedJWT } from '../types';
|
|
4
|
+
import type { Credentials, DecodedJWT } from '../types/auth';
|
|
5
5
|
|
|
6
6
|
export const decodeAuthToken = (token: string): Credentials | null => {
|
|
7
7
|
if (!token) {
|