diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json b/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json new file mode 100644 index 000000000..5179a5653 --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/i18n.json @@ -0,0 +1,6 @@ +{ + "namespace": "OperationalLearningMap", + "strings": { + "downloadMapTitle": "Operational learning map" + } +} diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx b/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx new file mode 100644 index 000000000..7d9a78ac7 --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/index.tsx @@ -0,0 +1,201 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + Container, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + _cs, + isDefined, + isNotDefined, +} from '@togglecorp/fujs'; +import { + MapLayer, + MapSource, +} from '@togglecorp/re-map'; + +import BaseMap from '#components/domain/BaseMap'; +import Link from '#components/Link'; +import MapContainerWithDisclaimer from '#components/MapContainerWithDisclaimer'; +import MapPopup from '#components/MapPopup'; +import useCountryRaw from '#hooks/domain/useCountryRaw'; +import { adminFillLayerOptions } from '#utils/map'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +const sourceOptions: mapboxgl.GeoJSONSourceRaw = { + type: 'geojson', +}; + +interface CountryProperties { + country_id: number; + name: string; + operation_count: number; +} + +interface ClickedPoint { + feature: GeoJSON.Feature; + lngLat: mapboxgl.LngLatLike; +} + +interface Props { + className?: string; +} + +function OperationalLearningMap(props: Props) { + const strings = useTranslation(i18n); + const { + className, + } = props; + + const [ + clickedPointProperties, + setClickedPointProperties, + ] = useState(); + + const countryResponse = useCountryRaw(); + + const countryCentroidGeoJson = useMemo( + (): GeoJSON.FeatureCollection => { + const learning_by_country = [ + { + country_name: 'Afghanistan', + country_id: 14, + operation_count: 4, + }, + { + country_name: 'Albania', + country_id: 15, + operation_count: 1, + }, + { + country_name: 'Argentina', + country_id: 20, + operation_count: 1, + }, + { + country_name: 'Australia', + country_id: 22, + operation_count: 1, + }, + { + country_name: 'Belgium', + country_id: 30, + operation_count: 1, + }, + { + country_name: 'Canada', + country_id: 42, + operation_count: 1, + }, + ]; + + const features = countryResponse + ?.map((country) => { + const learningList = learning_by_country.find( + (item) => item.country_id === country.id, + ); + if (isNotDefined(learningList)) { + return undefined; + } + + const units = learningList.operation_count ?? 0; + + return { + type: 'Feature' as const, + geometry: country.centroid as { + type: 'Point', + coordinates: [number, number], + }, + properties: { + country_id: country.id, + name: country.name, + operation_count: units, + }, + }; + }) + .filter(isDefined) ?? []; + + return { + type: 'FeatureCollection', + features, + }; + }, + [countryResponse], + ); + + const handleCountryClick = useCallback( + (feature: mapboxgl.MapboxGeoJSONFeature, lngLat: mapboxgl.LngLatLike) => { + setClickedPointProperties({ + feature: feature as unknown as ClickedPoint['feature'], + lngLat, + }); + return false; + }, + [], + ); + + const handlePointClose = useCallback(() => { + setClickedPointProperties(undefined); + }, []); + + return ( + + + )} + > + + + {clickedPointProperties?.lngLat && ( + + {clickedPointProperties.feature.properties.name} + + )} + childrenContainerClassName={styles.popupContent} + > + + + + + )} + + + ); +} + +export default OperationalLearningMap; diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css b/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css new file mode 100644 index 000000000..fa393c112 --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/styles.module.css @@ -0,0 +1,22 @@ +.learning-map { + + .map-container { + height: 40rem; + } +} + +.popup-content { + display: flex; + flex-direction: column; + gap: var(--go-ui-spacing-md); + + .popup-appeal { + gap: var(--go-ui-spacing-xs); + + .popup-appeal-detail { + display: flex; + flex-direction: column; + font-size: var(--go-ui-font-size-sm); + } + } +} diff --git a/app/src/views/OperationalLearning/OperationalLearningMap/utils.ts b/app/src/views/OperationalLearning/OperationalLearningMap/utils.ts new file mode 100644 index 000000000..fc8387dda --- /dev/null +++ b/app/src/views/OperationalLearning/OperationalLearningMap/utils.ts @@ -0,0 +1,50 @@ +// import type { +// CircleLayer, +// CirclePaint, +// FillLayer, +// } from 'mapbox-gl'; + +// import { +// COLOR_BLACK, +// COLOR_BLUE, +// COLOR_DARK_GREY, +// COLOR_LIGHT_GREY, +// COLOR_RED, +// COLOR_YELLOW, +// } from '#utils/constants'; + +// const COLOR_LEARNING_AND_PERSONNEL = COLOR_BLUE; +// const COLOR_LEARNING_ONLY = COLOR_RED; +// const COLOR_PERSONNEL_ONLY = COLOR_YELLOW; +// const COLOR_DEFAULT = COLOR_BLACK; + +// export const adminFillLayerOptions: Omit = { +// type: 'fill', +// paint: { +// 'fill-color': [ +// 'case', +// ['boolean', ['feature-state', 'hovered'], false], +// COLOR_DARK_GREY, +// COLOR_LIGHT_GREY, +// ], +// }, +// }; + +// const circleColor: CirclePaint['circle-color'] = [ +// 'case', +// COLOR_LEARNING_AND_PERSONNEL, +// COLOR_LEARNING_ONLY, +// COLOR_PERSONNEL_ONLY, +// COLOR_DEFAULT, +// ]; + +// const basePointPaint: CirclePaint = { +// 'circle-radius': 5, +// 'circle-color': circleColor, +// 'circle-opacity': 0.8, +// }; + +// export const basePointLayerOptions: Omit = { +// type: 'circle', +// paint: basePointPaint, +// }; diff --git a/app/src/views/OperationalLearning/index.tsx b/app/src/views/OperationalLearning/index.tsx index 1330314ac..6fef1e56d 100644 --- a/app/src/views/OperationalLearning/index.tsx +++ b/app/src/views/OperationalLearning/index.tsx @@ -64,6 +64,7 @@ import { import Filters, { type FilterValue } from './Filters'; import KeyInsights from './KeyInsights'; +import OperationalLearningMap from './OperationalLearningMap'; import Summary, { type Props as SummaryProps } from './Summary'; import i18n from './i18n.json'; @@ -102,29 +103,59 @@ const perComponentKeySelector = (option: PerComponent) => option.id; const disasterTypeKeySelector = (type: DisasterType) => type.id; const disasterTypeLabelSelector = (type: DisasterType) => type.name ?? '?'; -/** @knipignore */ - const responseData = { + operations_included: 9, + learning_extracts: 6, + sectors_covered: 6, + sources_used: 8, learning_by_region: [ - { region_name: 'Americas', region_id: 1, count: 2 }, - { region_name: 'Asia Pacific', region_id: 2, count: 5 }, - { region_name: 'Europe', region_id: 3, count: 2 }, - ], - learning_by_country: [ - { country_name: 'Afghanistan', country_id: 14, operation_count: 4 }, - { country_name: 'Albania', country_id: 15, operation_count: 1 }, - { country_name: 'Argentina', country_id: 20, operation_count: 1 }, - { country_name: 'Australia', country_id: 22, operation_count: 1 }, - { country_name: 'Belgium', country_id: 30, operation_count: 1 }, - { country_name: 'Canada', country_id: 42, operation_count: 1 }, + { + region_name: 'Americas', + region_id: 1, + count: 2, + }, + { + region_name: 'Asia Pacific', + region_id: 2, + count: 5, + }, + { + region_name: 'Europe', + region_id: 3, + count: 2, + }, ], learning_by_sector: [ - { id: 17, count: 1, title: 'health' }, - { id: 18, count: 1, title: 'education' }, - { id: 19, count: 3, title: 'Livelihoods and basic needs' }, - { id: 20, count: 4, title: 'Migration' }, - { id: 21, count: 1, title: 'WASH' }, - { id: 22, count: 1, title: 'Shelter' }, + { + id: 17, + count: 1, + title: 'health', + }, + { + id: 18, + count: 1, + title: 'education', + }, + { + id: 19, + count: 3, + title: 'Livelihoods and basic needs', + }, + { + id: 20, + count: 4, + title: 'Migration', + }, + { + id: 21, + count: 1, + title: 'WASH', + }, + { + id: 22, + count: 1, + title: 'Shelter', + }, ], sources_overtime: { DREF: [ @@ -176,11 +207,11 @@ const xAxisFormatter = (date: Date) => date.toLocaleString( { month: 'short' }, ); const keySelector = (datum: { key: number; value: number; label: string }) => datum.key; - const valueSelector = (d: { value: number }) => d.value; const labelSelector = (d: { label: string }) => d.label; const dateSelector = (d: { date: string }) => d.date; +/** @knipignore */ // eslint-disable-next-line import/prefer-default-export export function Component() { const strings = useTranslation(i18n); @@ -548,39 +579,44 @@ export function Component() {
-
- + -
+ -
-
+ + {showKeyInsights && (