@tpzdsp/next-toolkit 1.15.0 → 1.15.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpzdsp/next-toolkit",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "description": "A reusable React component library for Next.js applications",
5
5
  "engines": {
6
6
  "node": ">= 24.12.0",
@@ -11,6 +11,7 @@ type Props = {
11
11
  title: string;
12
12
  children: ReactNode;
13
13
  defaultOpen?: boolean;
14
+ disabled?: boolean;
14
15
  };
15
16
 
16
17
  export type AccordionProps = ExtendProps<'div', Props>;
@@ -19,6 +20,7 @@ export const Accordion = ({
19
20
  title,
20
21
  children,
21
22
  defaultOpen = false,
23
+ disabled = false,
22
24
  className,
23
25
  ...props
24
26
  }: AccordionProps) => {
@@ -28,14 +30,25 @@ export const Accordion = ({
28
30
  const [isOpen, setIsOpen] = useState(defaultOpen);
29
31
 
30
32
  return (
31
- <div className={cn('flex flex-col border-l-2 border-neutral-100', className)} {...props}>
33
+ <div
34
+ className={cn(
35
+ 'flex flex-col border-l-2 border-neutral-100',
36
+ disabled ? 'opacity-50' : '',
37
+ className,
38
+ )}
39
+ {...props}
40
+ >
32
41
  <button
33
- aria-expanded={isOpen}
34
- aria-controls={contentId}
35
- className="flex justify-between items-center px-2 py-1 bg-[#fefefefe] text-[color:#000000]
36
- border-y-2 border-neutral-100 focus-yellow"
42
+ aria-expanded={disabled ? undefined : isOpen}
43
+ aria-controls={disabled ? undefined : contentId}
44
+ disabled={disabled}
45
+ className={cn(
46
+ `flex justify-between items-center px-2 py-1 bg-[#fefefefe] text-[color:#000000]
47
+ border-y-2 border-neutral-100`,
48
+ disabled ? 'cursor-not-allowed' : 'focus-yellow',
49
+ )}
37
50
  id={buttonId}
38
- onClick={() => setIsOpen(!isOpen)}
51
+ onClick={disabled ? undefined : () => setIsOpen(!isOpen)}
39
52
  type="button"
40
53
  >
41
54
  <span>{title}</span>
@@ -45,14 +58,16 @@ export const Accordion = ({
45
58
  </span>
46
59
  </button>
47
60
 
48
- <section
49
- id={contentId}
50
- aria-labelledby={buttonId}
51
- aria-hidden={!isOpen}
52
- className={cn('p-2 bg-[#efefef]', isOpen ? 'block' : 'hidden')}
53
- >
54
- {children}
55
- </section>
61
+ {!disabled ? (
62
+ <section
63
+ id={contentId}
64
+ aria-labelledby={buttonId}
65
+ aria-hidden={!isOpen}
66
+ className={cn('p-2 bg-[#efefef]', isOpen ? 'block' : 'hidden')}
67
+ >
68
+ {children}
69
+ </section>
70
+ ) : null}
56
71
  </div>
57
72
  );
58
73
  };
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { memo, useEffect, useRef, useState } from 'react';
3
+ import { memo, useEffect, useRef, useState, type ReactNode } from 'react';
4
4
 
5
- import { Map, Overlay, View } from 'ol';
5
+ import { Feature, Map, Overlay, View } from 'ol';
6
6
  import { Attribution, ScaleLine, Zoom } from 'ol/control';
7
7
  import { fromLonLat } from 'ol/proj';
8
8
 
@@ -11,12 +11,13 @@ import { FullScreenControl } from './FullScreenControl';
11
11
  import { LayerSwitcherControl } from './LayerSwitcherControl';
12
12
  import { useMap } from './MapContext';
13
13
  import { Popup } from './Popup';
14
- import { getPopupPositionClass } from './utils';
14
+ import { getPopupPositionClass, LAYER_NAMES } from './utils';
15
15
  import type { PopupDirection } from '../types/map';
16
16
 
17
17
  export type MapComponentProps = {
18
18
  osMapsApiKey?: string;
19
19
  basePath: string;
20
+ children?: ReactNode;
20
21
  };
21
22
 
22
23
  const positionTransforms: Record<PopupDirection, string> = {
@@ -43,8 +44,7 @@ const arrowStyles: Record<PopupDirection, string> = {
43
44
  *
44
45
  * @return {*}
45
46
  */
46
- const MapComponentBase = ({ osMapsApiKey, basePath }: MapComponentProps) => {
47
- const [popupFeatures, setPopupFeatures] = useState([]);
47
+ const MapComponentBase = ({ osMapsApiKey, basePath, children }: MapComponentProps) => {
48
48
  const [popupCoordinate, setPopupCoordinate] = useState<number[] | null>(null);
49
49
  const [popupPositionClass, setPopupPositionClass] = useState<PopupDirection>('bottom-right');
50
50
 
@@ -57,6 +57,8 @@ const MapComponentBase = ({ osMapsApiKey, basePath }: MapComponentProps) => {
57
57
  mapConfig: { center, zoom },
58
58
  setMap,
59
59
  isDrawing,
60
+ popupFeatures,
61
+ setPopupFeatures,
60
62
  } = useMap();
61
63
 
62
64
  useEffect(() => {
@@ -117,17 +119,41 @@ const MapComponentBase = ({ osMapsApiKey, basePath }: MapComponentProps) => {
117
119
  return;
118
120
  }
119
121
 
120
- newMap.forEachFeatureAtPixel(event.pixel, (feature) => {
121
- const features = feature.get('features');
122
+ newMap.forEachFeatureAtPixel(event.pixel, (feature, layer) => {
123
+ const clusterFeatures = feature.get('features');
124
+ const layerName = (layer as { get(k: string): unknown } | null)?.get('name');
122
125
 
123
- if (features?.length > 0) {
126
+ if (clusterFeatures?.length > 0) {
124
127
  const coordinate = event.coordinate;
125
128
  const direction = getPopupPositionClass(coordinate, newMap);
126
129
 
127
- setPopupFeatures(features);
130
+ setPopupFeatures(clusterFeatures);
128
131
  setPopupCoordinate(coordinate);
129
132
  setPopupPositionClass(direction);
130
133
  overlay.setPosition(event.coordinate);
134
+
135
+ return true; // stop iteration
136
+ }
137
+
138
+ // Direct feature (e.g. polygon) — only show popup for the sampling points
139
+ // layer, not boundary or AOI layers which may also have name properties.
140
+ if (layerName !== LAYER_NAMES.SamplingPoints) {
141
+ return;
142
+ }
143
+
144
+ const name = feature.get('name');
145
+ const notation = feature.get('notation');
146
+
147
+ if (name || notation) {
148
+ const coordinate = event.coordinate;
149
+ const direction = getPopupPositionClass(coordinate, newMap);
150
+
151
+ setPopupFeatures([feature as Feature]);
152
+ setPopupCoordinate(coordinate);
153
+ setPopupPositionClass(direction);
154
+ overlay.setPosition(event.coordinate);
155
+
156
+ return true; // stop iteration
131
157
  }
132
158
  });
133
159
  });
@@ -163,12 +189,12 @@ const MapComponentBase = ({ osMapsApiKey, basePath }: MapComponentProps) => {
163
189
  >
164
190
  {popupFeatures.length > 0 ? (
165
191
  <Popup
166
- features={popupFeatures}
167
192
  onClose={closePopup}
168
193
  clickedCoord={popupCoordinate}
169
194
  arrowClasses={arrowStyles[popupPositionClass]}
170
- baseUrl={`${basePath}/sampling-point/`}
171
- />
195
+ >
196
+ {children}
197
+ </Popup>
172
198
  ) : null}
173
199
  </div>
174
200
  </div>
@@ -3,6 +3,7 @@
3
3
  import type { Dispatch, RefObject, ReactNode, SetStateAction } from 'react';
4
4
  import { createContext, useContext, useRef, useState, useMemo, useCallback } from 'react';
5
5
 
6
+ import { Feature } from 'ol';
6
7
  import type { Coordinate } from 'ol/coordinate';
7
8
  import BaseLayer from 'ol/layer/Base';
8
9
  import Layer from 'ol/layer/Layer';
@@ -33,6 +34,8 @@ export type MapContextType = {
33
34
  setSelectedLayer: Dispatch<SetStateAction<Layer | null>>;
34
35
  isDrawing: boolean;
35
36
  setIsDrawing: Dispatch<SetStateAction<boolean>>;
37
+ popupFeatures: Feature[];
38
+ setPopupFeatures: Dispatch<SetStateAction<Feature[]>>;
36
39
  };
37
40
 
38
41
  type MapProviderProps = {
@@ -60,6 +63,7 @@ export const MapProvider = ({ initialState = {}, children }: MapProviderProps) =
60
63
  const [aoi, setAoi] = useState<Coordinate[][] | null>(null);
61
64
  const [selectedLayer, setSelectedLayer] = useState<Layer | null>(null);
62
65
  const [isDrawing, setIsDrawing] = useState(false);
66
+ const [popupFeatures, setPopupFeatures] = useState<Feature[]>([]);
63
67
 
64
68
  const getLayers = useCallback(() => map?.getLayers().getArray(), [map]);
65
69
 
@@ -163,6 +167,8 @@ export const MapProvider = ({ initialState = {}, children }: MapProviderProps) =
163
167
  setIsDrawing,
164
168
  clearLayer,
165
169
  resetMap,
170
+ popupFeatures,
171
+ setPopupFeatures,
166
172
  ...initialState,
167
173
  }),
168
174
  [
@@ -177,6 +183,7 @@ export const MapProvider = ({ initialState = {}, children }: MapProviderProps) =
177
183
  isDrawing,
178
184
  clearLayer,
179
185
  resetMap,
186
+ popupFeatures,
180
187
  initialState,
181
188
  ],
182
189
  );
package/src/map/Popup.tsx CHANGED
@@ -1,27 +1,23 @@
1
1
  'use client';
2
2
 
3
- import { Feature } from 'ol';
4
- import { GoLinkExternal } from 'react-icons/go';
5
- import { IoMdCloseCircle } from 'react-icons/io';
3
+ import { type ReactNode } from 'react';
6
4
 
7
- import { ExternalLink } from '../components/link/ExternalLink';
5
+ import { IoMdCloseCircle } from 'react-icons/io';
8
6
 
9
7
  type PopupProps = {
10
- features: Feature[];
8
+ children: ReactNode;
11
9
  onClose: () => void;
12
10
  clickedCoord: number[] | null;
13
11
  arrowClasses: string;
14
- baseUrl: string;
15
12
  };
16
13
 
17
14
  export const Popup = ({
18
- features,
15
+ children,
19
16
  onClose,
20
17
  clickedCoord,
21
18
  arrowClasses = 'bottom-left',
22
- baseUrl,
23
19
  }: PopupProps) => {
24
- if (!features.length || !clickedCoord) {
20
+ if (!clickedCoord) {
25
21
  return null;
26
22
  }
27
23
 
@@ -40,29 +36,7 @@ export const Popup = ({
40
36
  className="space-y-2 pt-4 pb-1 overflow-y-auto max-h-[300px] bg-white border border-border
41
37
  rounded-lg divide-y divide-gray-300"
42
38
  >
43
- {features.map((feature) => {
44
- const id = feature.get('id');
45
- const name = feature.get('name');
46
- const notation = feature.get('notation');
47
- const url = `${baseUrl}${notation}`;
48
-
49
- return (
50
- <div key={id}>
51
- <strong>
52
- <ExternalLink
53
- href={url}
54
- className="px-4 my-2 text-blue-500 underline flex flex-row gap-1 items-center"
55
- >
56
- <span>{name}</span>
57
-
58
- <span className="flex-none w-5 h-5 flex items-center justify-center">
59
- <GoLinkExternal size={20} className="cursor-pointer" />
60
- </span>
61
- </ExternalLink>
62
- </strong>
63
- </div>
64
- );
65
- })}
39
+ {children}
66
40
  </div>
67
41
 
68
42
  <div