Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/lucky-pandas-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"go-web-app": minor
---

Add DREF landing page

- Add a page for DREF process under Respond
- Add an anticipatory pillar with imminent DREF and Early Action Protocols tabs, and a response pillar
- Add the DREF decision tree, accessible from navigation bar and from each pillar
- Add option to create a new DREF application from the decision tree outcomes and the pillar entry points
- Add operations and EAP maps and tables, FAQ sections and key DREF resources
5 changes: 5 additions & 0 deletions .changeset/tall-donkeys-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ifrc-go/ui": minor
---

Add a `baseline` option to the `contentAlignment` prop of `InlineLayout`, so a leading icon or marker can sit on the first text baseline instead of the vertical centre.
38 changes: 19 additions & 19 deletions app/src/App/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,46 +319,46 @@ const emergencyAdditionalInfo = customWrapRoute({
},
});

type DefaultDrefDetailChild = 'dref-detail';
const drefProcessLayout = customWrapRoute({
type DefaultDrefPillarChild = 'response-pillar';
const drefLayout = customWrapRoute({
parent: rootLayout,
path: 'dref-process',
forwardPath: 'dref-detail' satisfies DefaultDrefDetailChild,
path: 'dref',
forwardPath: 'response-pillar' satisfies DefaultDrefPillarChild,
component: {
render: () => import('#views/DrefProcess'),
render: () => import('#views/Dref'),
props: {},
},
wrapperComponent: Auth,
context: {
title: 'DREF Process',
title: 'Disaster Response Emergency Fund (DREF)',
visibility: 'anything',
},
});

const drefDetail = customWrapRoute({
parent: drefProcessLayout,
path: 'dref-detail' satisfies DefaultDrefDetailChild,
const drefResponsePillar = customWrapRoute({
parent: drefLayout,
path: 'response-pillar' satisfies DefaultDrefPillarChild,
component: {
render: () => import('#views/DrefDetail'),
render: () => import('#views/DrefResponsePillar'),
props: {},
},
wrapperComponent: Auth,
context: {
title: 'Response and Imminent DREF',
title: 'Response Pillar',
visibility: 'anything',
},
});

const eapDetail = customWrapRoute({
parent: drefProcessLayout,
path: 'eap-detail',
const drefAnticipatoryPillar = customWrapRoute({
parent: drefLayout,
path: 'anticipatory-pillar',
component: {
render: () => import('#views/EarlyActionProtocols'),
render: () => import('#views/DrefAnticipatoryPillar'),
props: {},
},
wrapperComponent: Auth,
context: {
title: 'Early Action Protocols',
title: 'Anticipatory Pillar',
visibility: 'anything',
},
});
Expand Down Expand Up @@ -1541,9 +1541,9 @@ const wrappedRoutes = {
// Redirects
preparednessOperationalLearning,
obsoleteFieldReportDetails,
drefDetail,
eapDetail,
drefProcessLayout,
drefLayout,
drefResponsePillar,
drefAnticipatoryPillar,
eapRegistrationLayout,
eapDevelopmentRegistrationForm,
eapFullExport,
Expand Down
11 changes: 7 additions & 4 deletions app/src/components/Navbar/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@
"userMenuEmergencies":"Emergencies",
"userMenuEmergenciesDescription": "Follow currently ongoing emergencies, as well as access the list of previous emergencies or IFRC-led operations.",
"userMenuEarlyWarning":"Early Warning",
"userMenuDrefProcess":"DREF Process ",
"userMenuDrefProcess":"Disaster Response Emergency Fund (DREF)",
"userMenuSurgeDeployments":"Surge/Deployments",
"userMenuOngoingEmergencies":"Ongoing Emergencies",
"userMenuCreateFieldReport":"Create Field Report",
"userMenuSubmit3WActivity":"Submit 3W Activity",
"userMenuEarlyWarningDescription":"Early Warning focuses on preparatory action ahead of possible disasters and events. Use one of the following links to submit information on early warning.",
"userMenuCreateEarlyActionFieldReport":"Create Early Action Field Report",
"userMenuCreateFlashUpdate":"Create Flash Update",
"userMenuDrefProcessDescription":"Disaster Response Emergency Fund (DREF) is the quickest way of getting funding directly to local humanitarian actors. Use one of the links below to submit a DREF Application or an update.",
"userMenuDrefProcessDescription":"The following are IFRC Applications that include Disaster Response Emergency Fund (DREF) and Early Action Protocols (EAP).",
"userMenuCreateDrefApplication":"Create DREF Application",
"myDrefApplications": "My DREF Applications",
"earlyActionProtocols": "Early Action Protocols (EAP)",
"userMenuDrefDecisionTree":"DREF Decision Tree",
"userMenuDrefAnticipatoryPillar":"Anticipatory Pillar",
"userMenuDrefResponsePillar":"Response Pillar",
"myDrefApplications":"My DREF Applications",
"earlyActionProtocols":"Early Action Protocols (EAP)",
"userMenuSurge":"The section displays the summary of deployments within current and ongoing emergencies. Login to see available details",
"userMenuSurgeGlobalOverview":"Surge Global Overview",
"userMenuOperationalToolbox":"Operational Toolbox",
Expand Down
46 changes: 37 additions & 9 deletions app/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,17 +334,45 @@ function Navbar(props: Props) {
name="dref-process"
description={strings.userMenuDrefProcessDescription}
footer={(
<DropdownMenuItem
type="link"
to="newDrefApplicationForm"
styleVariant="action"
colorVariant="primary"
withoutFullWidth
>
{strings.userMenuCreateDrefApplication}
</DropdownMenuItem>
<>
<DropdownMenuItem
type="link"
to="newDrefApplicationForm"
styleVariant="action"
colorVariant="primary"
withoutFullWidth
>
{strings.userMenuCreateDrefApplication}
</DropdownMenuItem>
<DropdownMenuItem
type="link"
to="drefResponsePillar"
state={{ openDecisionTree: true }}
styleVariant="action"
colorVariant="primary"
withoutFullWidth
>
{strings.userMenuDrefDecisionTree}
</DropdownMenuItem>
</>
)}
>
<DropdownMenuItem
type="link"
to="drefAnticipatoryPillar"
styleVariant="action"
withoutFullWidth
>
{strings.userMenuDrefAnticipatoryPillar}
</DropdownMenuItem>
<DropdownMenuItem
type="link"
to="drefResponsePillar"
styleVariant="action"
withoutFullWidth
>
{strings.userMenuDrefResponsePillar}
</DropdownMenuItem>
<DropdownMenuItem
type="link"
to="accountMyFormsDref"
Expand Down
180 changes: 180 additions & 0 deletions app/src/components/domain/CountryPointsMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import {
type ReactNode,
useCallback,
useMemo,
useState,
} from 'react';
import {
LegendItem,
ListView,
} from '@ifrc-go/ui';
import {
isDefined,
isNotDefined,
listToMap,
} from '@togglecorp/fujs';
import {
MapLayer,
MapSource,
} from '@togglecorp/re-map';
import {
type CircleLayer,
type CirclePaint,
} from 'mapbox-gl';

import GlobalMap, { type AdminZeroFeatureProperties } from '#components/domain/GlobalMap';
import GoMapContainer from '#components/GoMapContainer';
import Link from '#components/Link';
import MapPopup from '#components/MapPopup';
import useCountryRaw from '#hooks/domain/useCountryRaw';

const sourceOptions: mapboxgl.GeoJSONSourceRaw = {
type: 'geojson',
};

// Point colour comes from each feature's `color` property, so one layer serves any colouring.
const pointPaint: CirclePaint = {
'circle-radius': 8,
'circle-color': ['get', 'color'],
'circle-opacity': 0.8,
};
const pointLayerOptions: Omit<CircleLayer, 'id'> = {
type: 'circle',
paint: pointPaint,
};

export interface CountryPoint {
iso3: string;
color: string;
}

export interface MapLegendOption {
value: string | number;
label: string;
color: string;
}

interface ClickedPoint {
featureProperties: AdminZeroFeatureProperties;
lngLat: mapboxgl.LngLatLike;
}

interface Props {
className?: string;
mapTitle: string;
points: CountryPoint[];
// Optional — omit (or pass empty) to render no legend.
legendOptions?: MapLegendOption[];
// Popup content for a clicked country that has a point.
renderPopup: (iso3: string) => ReactNode;
}

function CountryPointsMap(props: Props) {
const {
className,
mapTitle,
points,
legendOptions,
renderPopup,
} = props;

const countryResponse = useCountryRaw();
const [clickedPoint, setClickedPoint] = useState<ClickedPoint | undefined>();

const colorByIso3 = useMemo(
() => listToMap(points, (point) => point.iso3, (point) => point.color),
[points],
);

const geoJson = useMemo<GeoJSON.FeatureCollection<GeoJSON.Geometry>>(
() => ({
type: 'FeatureCollection',
features: (countryResponse ?? [])
.map((country) => {
if (isNotDefined(country.centroid) || isNotDefined(country.iso3)) {
return undefined;
}
const color = colorByIso3[country.iso3];
if (isNotDefined(color)) {
return undefined;
}
return {
type: 'Feature' as const,
geometry: country.centroid as {
type: 'Point';
coordinates: [number, number];
},
properties: {
id: country.iso3,
color,
},
};
})
.filter(isDefined),
}),
[countryResponse, colorByIso3],
);

const handleCountryClick = useCallback(
(featureProperties: AdminZeroFeatureProperties, lngLat: mapboxgl.LngLatLike) => {
if (isDefined(colorByIso3[featureProperties.iso3])) {
setClickedPoint({ featureProperties, lngLat });
}
return true;
},
[colorByIso3],
);

const handlePointClose = useCallback(() => setClickedPoint(undefined), []);

const hasLegend = isDefined(legendOptions) && legendOptions.length > 0;

return (
<GlobalMap onAdminZeroFillClick={handleCountryClick}>
<GoMapContainer
className={className}
title={mapTitle}
footer={hasLegend ? (
<ListView withWrap withSpacingOpticalCorrection spacing="sm">
{legendOptions.map((legendItem) => (
<LegendItem
key={legendItem.value}
color={legendItem.color}
label={legendItem.label}
/>
))}
</ListView>
) : undefined}
/>
<MapSource
sourceKey="country-points"
sourceOptions={sourceOptions}
geoJson={geoJson}
>
<MapLayer
layerKey="point-circle"
layerOptions={pointLayerOptions}
/>
</MapSource>
{clickedPoint?.lngLat && (
<MapPopup
onCloseButtonClick={handlePointClose}
coordinates={clickedPoint.lngLat}
heading={(
<Link
to="countriesLayout"
urlParams={{ countryId: clickedPoint.featureProperties.country_id }}
>
{clickedPoint.featureProperties.name}
</Link>
)}
withPadding
>
{renderPopup(clickedPoint.featureProperties.iso3)}
</MapPopup>
)}
</GlobalMap>
);
}

export default CountryPointsMap;
Loading
Loading