Skip to content

Commit a6a2481

Browse files
committed
feat(admin-2-input): add basic component
1 parent f860ae1 commit a6a2481

File tree

17 files changed

+367
-1104
lines changed

17 files changed

+367
-1104
lines changed

app/src/components/domain/Admin2Input/i18n.json

Whitespace-only changes.
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
import {
2+
useCallback,
3+
useMemo,
4+
} from 'react';
5+
import { CloseLineIcon } from '@ifrc-go/icons';
6+
import {
7+
Button,
8+
ButtonLayout,
9+
Container,
10+
InputError,
11+
ListView,
12+
Modal,
13+
} from '@ifrc-go/ui';
14+
import { useBooleanState } from '@ifrc-go/ui/hooks';
15+
import { isNotDefined } from '@togglecorp/fujs';
16+
import {
17+
MapBounds,
18+
MapLayer,
19+
MapSource,
20+
} from '@togglecorp/re-map';
21+
import turfBbox from '@turf/bbox';
22+
import {
23+
type FillLayer,
24+
type LineLayer,
25+
type MapboxGeoJSONFeature,
26+
type SymbolLayer,
27+
} from 'mapbox-gl';
28+
29+
import GoMapContainer from '#components/GoMapContainer';
30+
import useCountry from '#hooks/domain/useCountry';
31+
import {
32+
COLOR_BLACK,
33+
COLOR_DARK_GREY,
34+
COLOR_LIGHT_GREY,
35+
COLOR_PRIMARY_RED,
36+
COLOR_TEXT,
37+
COLOR_TEXT_ON_DARK,
38+
DEFAULT_MAP_PADDING,
39+
DURATION_MAP_ZOOM,
40+
} from '#utils/constants';
41+
42+
import BaseMap from '../BaseMap';
43+
44+
interface Props<NAME> {
45+
name: NAME;
46+
value: number[] | null | undefined;
47+
onChange: (newValue: number[] | undefined, name: NAME) => void;
48+
countryId: number;
49+
error?: React.ReactNode;
50+
}
51+
52+
function Admin2Input<const NAME>(props: Props<NAME>) {
53+
const {
54+
name,
55+
value,
56+
onChange,
57+
countryId,
58+
error,
59+
} = props;
60+
61+
const countryDetails = useCountry({ id: countryId });
62+
const iso3 = countryDetails?.iso3;
63+
64+
const bounds = useMemo(() => {
65+
if (!countryDetails) {
66+
return undefined;
67+
}
68+
69+
return turfBbox(countryDetails.bbox);
70+
}, [
71+
countryDetails,
72+
]);
73+
74+
const adminOneLabelLayerOptions: Omit<SymbolLayer, 'id'> = useMemo(() => ({
75+
type: 'symbol',
76+
paint: {
77+
'text-opacity': [
78+
'match',
79+
['get', 'country_id'],
80+
countryId,
81+
1,
82+
0,
83+
],
84+
},
85+
layout: {
86+
'text-offset': [
87+
0, 1,
88+
],
89+
visibility: 'visible',
90+
},
91+
}), [countryId]);
92+
93+
const adminTwoLineLayerOptions: Omit<LineLayer, 'id'> | undefined = useMemo(() => {
94+
if (!iso3) {
95+
return undefined;
96+
}
97+
98+
return {
99+
type: 'line',
100+
'source-layer': `go-admin2-${iso3}-staging`,
101+
paint: {
102+
'line-color': COLOR_BLACK,
103+
'line-opacity': 1,
104+
},
105+
layout: {
106+
visibility: 'visible',
107+
},
108+
};
109+
}, [iso3]);
110+
111+
const adminTwoFillLayerOptions = useMemo((): Omit<FillLayer, 'id'> | undefined => {
112+
if (!iso3) {
113+
return undefined;
114+
}
115+
const defaultColor: NonNullable<FillLayer['paint']>['fill-color'] = [
116+
'case',
117+
['boolean', ['feature-state', 'hovered'], false],
118+
COLOR_DARK_GREY,
119+
COLOR_LIGHT_GREY,
120+
];
121+
const options: Omit<FillLayer, 'id'> = {
122+
type: 'fill',
123+
'source-layer': `go-admin2-${iso3}-staging`,
124+
paint: {
125+
'fill-color': (!value || value.length <= 0)
126+
? defaultColor
127+
: [
128+
'match',
129+
['get', 'id'],
130+
...value.map((admin2Id) => [
131+
admin2Id,
132+
COLOR_PRIMARY_RED,
133+
]).flat(),
134+
defaultColor,
135+
],
136+
'fill-outline-color': COLOR_DARK_GREY,
137+
'fill-opacity': 1,
138+
},
139+
layout: {
140+
visibility: 'visible',
141+
},
142+
};
143+
return options;
144+
}, [iso3, value]);
145+
146+
const adminTwoLabelLayerOptions = useMemo((): Omit<SymbolLayer, 'id'> | undefined => {
147+
const textColor: NonNullable<SymbolLayer['paint']>['text-color'] = (
148+
value && value.length > 0
149+
? [
150+
'match',
151+
['get', 'id'],
152+
...value.map((admin2Id) => [
153+
admin2Id,
154+
COLOR_TEXT_ON_DARK,
155+
]).flat(),
156+
COLOR_TEXT,
157+
]
158+
: COLOR_TEXT
159+
);
160+
161+
const options: Omit<SymbolLayer, 'id'> = {
162+
type: 'symbol',
163+
'source-layer': `go-admin2-${iso3}-centroids`,
164+
paint: {
165+
'text-color': textColor,
166+
'text-opacity': 1,
167+
},
168+
layout: {
169+
'text-field': ['get', 'name'],
170+
'text-anchor': 'center',
171+
'text-size': 10,
172+
},
173+
};
174+
return options;
175+
}, [iso3, value]);
176+
177+
const handleAdmin2Click = useCallback((clickedFeature: MapboxGeoJSONFeature) => {
178+
const properties = clickedFeature?.properties as {
179+
id: number;
180+
admin1_id: number;
181+
code: string;
182+
admin1_name: string;
183+
name?: string;
184+
};
185+
if (isNotDefined(properties.id)) {
186+
return false;
187+
}
188+
189+
const valueIndex = value?.findIndex((admin2Id) => admin2Id === properties.id) ?? -1;
190+
191+
if (valueIndex === -1) {
192+
onChange([...(value ?? []), properties.id], name);
193+
} else {
194+
onChange(value?.toSpliced(valueIndex, 1), name);
195+
}
196+
197+
return false;
198+
}, [value, name, onChange]);
199+
200+
const [
201+
showModal,
202+
{
203+
setTrue: setShowModalTrue,
204+
setFalse: setShowModalFalse,
205+
},
206+
] = useBooleanState(false);
207+
208+
const removeSelection = useCallback((admin2Id: number) => {
209+
const index = value?.findIndex((selectedAdmin2Id) => selectedAdmin2Id === admin2Id) ?? -1;
210+
211+
if (index !== -1) {
212+
onChange(value?.toSpliced(index, 1), name);
213+
}
214+
}, [value, onChange, name]);
215+
216+
return (
217+
<ListView layout="block">
218+
<Container
219+
heading="Selected areas:"
220+
headingLevel={5}
221+
footer={(
222+
<Button
223+
name={undefined}
224+
onClick={setShowModalTrue}
225+
// FIXME: use label from props
226+
>
227+
Select areas
228+
</Button>
229+
)}
230+
withCompactMessage
231+
empty={!value || value.length === 0}
232+
>
233+
<ListView
234+
withWrap
235+
spacing="2xs"
236+
>
237+
{value?.map((admin2Id) => (
238+
<ButtonLayout
239+
spacing="2xs"
240+
after={(
241+
<Button
242+
name={admin2Id}
243+
onClick={removeSelection}
244+
styleVariant="action"
245+
>
246+
<CloseLineIcon />
247+
</Button>
248+
)}
249+
>
250+
{admin2Id}
251+
</ButtonLayout>
252+
))}
253+
</ListView>
254+
</Container>
255+
{error && (
256+
<InputError>
257+
{error}
258+
</InputError>
259+
)}
260+
{showModal && (
261+
<Modal
262+
onClose={setShowModalFalse}
263+
// FIXME: use strings
264+
heading="Select Admin-2"
265+
footerActions={(
266+
<Button
267+
name={undefined}
268+
onClick={setShowModalFalse}
269+
// FIXME: use strings
270+
>
271+
Done
272+
</Button>
273+
)}
274+
>
275+
<BaseMap
276+
baseLayers={(
277+
<MapLayer
278+
layerKey="admin-1-label"
279+
layerOptions={adminOneLabelLayerOptions}
280+
/>
281+
)}
282+
>
283+
<GoMapContainer
284+
title="Admin-2 Map"
285+
withoutDownloadButton
286+
/>
287+
{bounds && (
288+
<MapBounds
289+
duration={DURATION_MAP_ZOOM}
290+
padding={DEFAULT_MAP_PADDING}
291+
bounds={bounds}
292+
/>
293+
)}
294+
{/* eslint-disable-next-line max-len */}
295+
{adminTwoFillLayerOptions && adminTwoLineLayerOptions && adminTwoLabelLayerOptions && (
296+
<>
297+
<MapSource
298+
sourceKey="country-admin-2"
299+
sourceOptions={{
300+
type: 'vector',
301+
url: `mapbox://go-ifrc.go-admin2-${iso3}-staging`,
302+
}}
303+
>
304+
<MapLayer
305+
layerKey="admin-2-fill"
306+
layerOptions={adminTwoFillLayerOptions}
307+
onClick={handleAdmin2Click}
308+
hoverable
309+
/>
310+
<MapLayer
311+
layerKey="admin-2-line"
312+
layerOptions={adminTwoLineLayerOptions}
313+
/>
314+
</MapSource>
315+
<MapSource
316+
sourceKey="country-admin-2-labels"
317+
sourceOptions={{
318+
type: 'vector',
319+
url: `mapbox://go-ifrc.go-admin2-${iso3}-centroids`,
320+
}}
321+
>
322+
<MapLayer
323+
layerKey="admin-2-label"
324+
layerOptions={adminTwoLabelLayerOptions}
325+
/>
326+
</MapSource>
327+
</>
328+
)}
329+
</BaseMap>
330+
</Modal>
331+
)}
332+
</ListView>
333+
);
334+
}
335+
336+
export default Admin2Input;

app/src/components/domain/BaseMap/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export type Props = Omit<MapProps, overrides> & {
2828
withDisclaimer?: boolean;
2929
} & Partial<Pick<MapProps, overrides>>;
3030

31-
function BaseMap(props: Props) {
31+
function BaseMapWithoutErrorBoundary(props: Props) {
3232
const {
3333
baseLayers,
3434
mapStyle,
@@ -101,7 +101,7 @@ function BaseMap(props: Props) {
101101
);
102102
}
103103

104-
function BaseMapWithErrorBoundary(props: Props) {
104+
function BaseMap(props: Props) {
105105
return (
106106
<ErrorBoundary
107107
fallback={(
@@ -110,12 +110,12 @@ function BaseMapWithErrorBoundary(props: Props) {
110110
</div>
111111
)}
112112
>
113-
<BaseMap
113+
<BaseMapWithoutErrorBoundary
114114
// eslint-disable-next-line react/jsx-props-no-spreading
115115
{...props}
116116
/>
117117
</ErrorBoundary>
118118
);
119119
}
120120

121-
export default BaseMapWithErrorBoundary;
121+
export default BaseMap;

app/src/views/PerPrioritizationForm/ComponentInput/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,8 @@ function ComponentInput(props: Props) {
208208
{answerStats.map((answerStat) => (
209209
<ButtonLayout
210210
key={answerStat.answer}
211-
spacing="sm"
211+
spacing="xs"
212212
readOnly
213-
styleVariant="translucent"
214213
// FIXME: use tag component
215214
>
216215
<TextOutput

app/src/views/SimplifiedEapForm/EarlyAction/DistrictMap/DistrictMapModal/DistrictItem/i18n.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)