diff --git a/README.md b/README.md
index e5df693b..190595ba 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ This is the repository for the World Food Program's HungerMap.
- [next-themes](https://github.com/pacocoursey/next-themes)
- [leaflet](https://leafletjs.com/), [react-leaflet](https://react-leaflet.js.org/)
- [Highcharts](https://www.highcharts.com/), [highcharts-react](https://github.com/highcharts/highcharts-react)
+- [Tanstack Query](https://tanstack.com/query/latest)
## How to Use
@@ -65,7 +66,5 @@ What this means for a React application, is that instead of using one global sta
The point of DI is that the dependencies of services should not be hardcoded, instead they should receive them as parameters, where the type of the parameter is an interface. This makes testing easier and the code more reusable, since the same service can be used with different implementations of the same interface (for example mock implementations for testing).
-For a React application, the most common depeendency that needs mocking is the one that makes the HTTP call to the API. Thus contexts or hooks that call the backend should receive the HTTP library implementation as a prop or from a higher-order context.
-
- Example of how to use the container to resolve a dependency can be found in `src/app/elements/page.tsx`.
- A list of all dependencies and their implementations can be found in `src/container.tsx`.
diff --git a/package.json b/package.json
index c652e1e5..d8abcf89 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@nextui-org/theme": "2.2.11",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
+ "@tanstack/react-query": "^5.59.17",
"clsx": "2.1.1",
"framer-motion": "~11.1.1",
"highcharts": "^11.4.8",
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e686de9a..3647bdff 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,5 +1,11 @@
import MapLoader from '@/components/Map/MapLoader';
+import container from '@/container';
+import { GlobalDataRepository } from '@/domain/repositories/GlobalDataRepository';
-export default function Home() {
- return ;
+export default async function Home() {
+ const globalRepo = container.resolve('GlobalDataRepository');
+ const countryMapDataPromise = globalRepo.getMapDataForCountries();
+ const disputedAreasPromise = globalRepo.getDisputedAreas();
+ const [countryMapData, disputedAreas] = await Promise.all([countryMapDataPromise, disputedAreasPromise]);
+ return ;
}
diff --git a/src/components/Map/CountryPolygon.tsx b/src/components/Map/CountryPolygon.tsx
new file mode 100644
index 00000000..be2ca163
--- /dev/null
+++ b/src/components/Map/CountryPolygon.tsx
@@ -0,0 +1,21 @@
+import { useState } from 'react';
+import { Polygon } from 'react-leaflet';
+
+import { CountryMapData } from '@/domain/entities/country/CountryMapData';
+
+import { CountryPopup } from './CountryPopup';
+
+export function CountryPolygon({ country }: { country: CountryMapData }) {
+ const [active, setActive] = useState(false);
+
+ return (
+ <>
+ setActive(true) }}
+ />
+ {active && setActive(false)} />}
+ >
+ );
+}
diff --git a/src/components/Map/CountryPopup.tsx b/src/components/Map/CountryPopup.tsx
new file mode 100644
index 00000000..20f4cb17
--- /dev/null
+++ b/src/components/Map/CountryPopup.tsx
@@ -0,0 +1,20 @@
+import { Popup } from 'react-leaflet';
+
+import { CountryMapData } from '@/domain/entities/country/CountryMapData';
+import { useCountryDataQuery } from '@/domain/hooks/countryHooks';
+
+/**
+ * This is just an example of how the sidebars could fetch the data when a country is clicked
+ */
+export function CountryPopup({ country, onClose }: { country: CountryMapData; onClose: () => void }) {
+ const { data, isPending } = useCountryDataQuery(country.properties.adm0_id);
+
+ return (
+
+ FCS: {isPending || !data ? 'Loading...' : data.fcs}
+
+ );
+}
diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx
index 3b74b0a3..938ccca9 100644
--- a/src/components/Map/Map.tsx
+++ b/src/components/Map/Map.tsx
@@ -4,13 +4,23 @@ import 'leaflet/dist/leaflet.css';
import { MapContainer, TileLayer } from 'react-leaflet';
-export default function Map() {
+import { MapProps } from '@/domain/props/MapProps';
+
+import { CountryPolygon } from './CountryPolygon';
+
+export default function Map({ countries }: MapProps) {
return (
+ {countries.features
+ .filter((c) => c.properties.interactive)
+ .map((c) => (
+ // TODO fix the layout, this is just an example
+
+ ))}
);
}
diff --git a/src/components/Map/MapLoader.tsx b/src/components/Map/MapLoader.tsx
index b4680ddf..a568bcb6 100644
--- a/src/components/Map/MapLoader.tsx
+++ b/src/components/Map/MapLoader.tsx
@@ -3,12 +3,13 @@
import dynamic from 'next/dynamic';
import MapSkeleton from '@/components/Map/MapSkeleton';
+import { MapProps } from '@/domain/props/MapProps';
const LazyMap = dynamic(() => import('@/components/Map/Map'), {
ssr: false,
loading: () => ,
});
-export default function MapLoader() {
- return ;
+export default function MapLoader(props: MapProps) {
+ return ;
}
diff --git a/src/config/queryClient.ts b/src/config/queryClient.ts
new file mode 100644
index 00000000..e1c6f06e
--- /dev/null
+++ b/src/config/queryClient.ts
@@ -0,0 +1,15 @@
+import { QueryClient } from '@tanstack/react-query';
+
+/**
+ * Only refetches data if it's more than 1 hour old
+ */
+export const cachedQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ gcTime: 1000 * 60 * 60,
+ },
+ },
+});
diff --git a/src/domain/entities/common/Geometry.ts b/src/domain/entities/common/Geometry.ts
index f47f483a..82e604d6 100644
--- a/src/domain/entities/common/Geometry.ts
+++ b/src/domain/entities/common/Geometry.ts
@@ -1,4 +1,6 @@
+import { LatLngExpression } from 'leaflet';
+
export interface Geometry {
type: string;
- coordinates: number[][][]; // Maybe a common type is not best idea here, the coordinate arrays seem to have different dephts.
+ coordinates: LatLngExpression[][][]; // Maybe a common type is not best idea here, the coordinate arrays seem to have different dephts.
}
diff --git a/src/domain/entities/common/ResponseWrapper.ts b/src/domain/entities/common/ResponseWrapper.ts
index 362e8711..4fdaf8d4 100644
--- a/src/domain/entities/common/ResponseWrapper.ts
+++ b/src/domain/entities/common/ResponseWrapper.ts
@@ -1,4 +1,4 @@
export interface ResponseWrapper {
statusCode: string;
- body: T[];
+ body: T;
}
diff --git a/src/domain/entities/Adm0Data.ts b/src/domain/entities/country/CountryMapData.ts
similarity index 75%
rename from src/domain/entities/Adm0Data.ts
rename to src/domain/entities/country/CountryMapData.ts
index 430ce968..a297152a 100644
--- a/src/domain/entities/Adm0Data.ts
+++ b/src/domain/entities/country/CountryMapData.ts
@@ -1,10 +1,7 @@
-import { Coordinate } from './common/Coordinate';
-import { Feature } from './common/Feature';
+import { Coordinate } from '../common/Coordinate';
+import { Feature } from '../common/Feature';
-/**
- * Not really sure what this represents...
- */
-export type Adm0Data = Feature<{
+export type CountryMapData = Feature<{
OBJECTID: number;
adm0_name: string;
map_lab: string;
@@ -39,3 +36,7 @@ export type Adm0Data = Feature<{
centroid: Coordinate;
dataType?: string;
}>;
+export interface CountryMapDataWrapper {
+ type: string;
+ features: CountryMapData[];
+}
diff --git a/src/domain/hooks/alertHooks.ts b/src/domain/hooks/alertHooks.ts
new file mode 100644
index 00000000..9b14ee0a
--- /dev/null
+++ b/src/domain/hooks/alertHooks.ts
@@ -0,0 +1,28 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { cachedQueryClient } from '@/config/queryClient';
+import container from '@/container';
+
+import { Conflict } from '../entities/alerts/Conflict';
+import { Hazard } from '../entities/alerts/Hazard';
+import { AlertRepository } from '../repositories/AlertRepository';
+
+const alertsRepo = container.resolve('AlertRepository');
+
+export const useConflictQuery = () =>
+ useQuery(
+ {
+ queryKey: ['fetchConflicts'],
+ queryFn: alertsRepo.getConflictData,
+ },
+ cachedQueryClient
+ );
+
+export const useHazardQuery = () =>
+ useQuery(
+ {
+ queryKey: ['fetchHazards'],
+ queryFn: alertsRepo.getHazardData,
+ },
+ cachedQueryClient
+ );
diff --git a/src/domain/hooks/countryHooks.ts b/src/domain/hooks/countryHooks.ts
new file mode 100644
index 00000000..cde4a599
--- /dev/null
+++ b/src/domain/hooks/countryHooks.ts
@@ -0,0 +1,48 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { cachedQueryClient } from '@/config/queryClient';
+import container from '@/container';
+
+import { AdditionalCountryData } from '../entities/country/AdditionalCountryData';
+import { CountryData } from '../entities/country/CountryData';
+import { CountryIso3Data } from '../entities/country/CountryIso3Data';
+import { RegionIpc } from '../entities/region/RegionIpc';
+import CountryRepository from '../repositories/CountryRepository';
+
+const countryRepo = container.resolve('CountryRepository');
+
+export const useCountryDataQuery = (countryId: number) =>
+ useQuery(
+ {
+ queryKey: ['fetchCountryData', countryId],
+ queryFn: async () => countryRepo.getCountryData(countryId),
+ },
+ cachedQueryClient
+ );
+
+export const useRegionDataQuery = (countryId: number) =>
+ useQuery(
+ {
+ queryKey: ['fetchRegionData', countryId],
+ queryFn: async () => countryRepo.getRegionData(countryId),
+ },
+ cachedQueryClient
+ );
+
+export const useCountryIso3DataQuery = (countryCode: string) =>
+ useQuery(
+ {
+ queryKey: ['fetchCountryIso3Data', countryCode],
+ queryFn: async () => countryRepo.getCountryIso3Data(countryCode),
+ },
+ cachedQueryClient
+ );
+
+export const useRegionIpcDataQuery = (countryId: number) =>
+ useQuery(
+ {
+ queryKey: ['fetchRegionIpcData', countryId],
+ queryFn: async () => countryRepo.getRegionIpcData(countryId),
+ },
+ cachedQueryClient
+ );
diff --git a/src/domain/hooks/globalHooks.ts b/src/domain/hooks/globalHooks.ts
new file mode 100644
index 00000000..2aac1777
--- /dev/null
+++ b/src/domain/hooks/globalHooks.ts
@@ -0,0 +1,18 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { cachedQueryClient } from '@/config/queryClient';
+import container from '@/container';
+
+import { CountryIpcData } from '../entities/country/CountryIpcData';
+import { GlobalDataRepository } from '../repositories/GlobalDataRepository';
+
+const globalRepo = container.resolve('GlobalDataRepository');
+
+export const useIpcQuery = () =>
+ useQuery(
+ {
+ queryKey: ['fetchIpcData'],
+ queryFn: globalRepo.getIpcData,
+ },
+ cachedQueryClient
+ );
diff --git a/src/domain/props/MapProps.tsx b/src/domain/props/MapProps.tsx
new file mode 100644
index 00000000..30585834
--- /dev/null
+++ b/src/domain/props/MapProps.tsx
@@ -0,0 +1,7 @@
+import { CountryMapDataWrapper } from '../entities/country/CountryMapData';
+import { DisputedAreas } from '../entities/DisputedAreas';
+
+export interface MapProps {
+ countries: CountryMapDataWrapper;
+ disputedAreas: DisputedAreas;
+}
diff --git a/src/domain/repositories/CountryRepository.ts b/src/domain/repositories/CountryRepository.ts
index 067db26a..62e5edd3 100644
--- a/src/domain/repositories/CountryRepository.ts
+++ b/src/domain/repositories/CountryRepository.ts
@@ -1,5 +1,4 @@
import { AdditionalCountryData } from '../entities/country/AdditionalCountryData';
-import { CountryCodesData } from '../entities/country/CountryCodesData';
import { CountryData } from '../entities/country/CountryData';
import { CountryIso3Data } from '../entities/country/CountryIso3Data';
import { CountryIso3Notes } from '../entities/country/CountryIso3Notes';
@@ -7,11 +6,6 @@ import { CountryMimiData } from '../entities/country/CountryMimiData';
import { RegionIpc } from '../entities/region/RegionIpc';
export default interface CountryRepository {
- /**
- * Returns the ID and ISO codes of all the countries, plus a summary report for each.
- */
- getCountryCodes(): Promise;
-
/**
* Returns the population, FCS and RCS data and news about a country
* @param countryId
diff --git a/src/domain/repositories/GlobalDataRepository.ts b/src/domain/repositories/GlobalDataRepository.ts
index b421c02e..37b53534 100644
--- a/src/domain/repositories/GlobalDataRepository.ts
+++ b/src/domain/repositories/GlobalDataRepository.ts
@@ -1,19 +1,25 @@
-import { Adm0Data } from '../entities/Adm0Data';
import { ChangeLogItem } from '../entities/ChangeLogItem';
+import { CountryCodesData } from '../entities/country/CountryCodesData';
import { CountryIpcData } from '../entities/country/CountryIpcData';
+import { CountryMapDataWrapper } from '../entities/country/CountryMapData';
import { DisputedAreas } from '../entities/DisputedAreas';
import { YearInReview } from '../entities/YearInReview';
export interface GlobalDataRepository {
+ /**
+ * Returns the ID and ISO codes of all the countries, plus a summary report for each.
+ */
+ getCountryCodes(): Promise;
/**
* Returns the national IPC data of all the countries
*/
getIpcData(): Promise;
/**
- * Not sure what this returns :/
+ * Returns the polygons for the countries and whether any alerts are active in the country
+ * (The alerts shown on the Current Food Consuption map, e.g. countries with >=1 fatality...)
*/
- getAdm0Data(): Promise;
+ getMapDataForCountries(): Promise;
/**
* Returns all the disputed areas around the world
diff --git a/src/infrastructure/repositories/AlertRepositoryImpl.ts b/src/infrastructure/repositories/AlertRepositoryImpl.ts
index 66e4bee3..40817d33 100644
--- a/src/infrastructure/repositories/AlertRepositoryImpl.ts
+++ b/src/infrastructure/repositories/AlertRepositoryImpl.ts
@@ -7,7 +7,7 @@ import { AlertRepository } from '@/domain/repositories/AlertRepository';
export class AlertRepositoryImpl implements AlertRepository {
async getHazardData(): Promise {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/pdc.json`);
- const data: ResponseWrapper = await response.json();
+ const data: ResponseWrapper = await response.json();
return data.body;
}
diff --git a/src/infrastructure/repositories/CountryRepositoryImpl.ts b/src/infrastructure/repositories/CountryRepositoryImpl.ts
index f3ee0bd3..c59d9f78 100644
--- a/src/infrastructure/repositories/CountryRepositoryImpl.ts
+++ b/src/infrastructure/repositories/CountryRepositoryImpl.ts
@@ -1,5 +1,4 @@
import { AdditionalCountryData } from '@/domain/entities/country/AdditionalCountryData';
-import { CountryCodesData } from '@/domain/entities/country/CountryCodesData';
import { CountryData } from '@/domain/entities/country/CountryData';
import { CountryIso3Data } from '@/domain/entities/country/CountryIso3Data';
import { CountryIso3Notes } from '@/domain/entities/country/CountryIso3Notes';
@@ -8,11 +7,6 @@ import { RegionIpc } from '@/domain/entities/region/RegionIpc';
import CountryRepository from '@/domain/repositories/CountryRepository';
export default class CountryRepositoryImpl implements CountryRepository {
- async getCountryCodes(): Promise {
- const response = await fetch(`https://static.hungermapdata.org/insight-reports/latest/country.json`);
- return response.json();
- }
-
async getCountryData(countryId: number): Promise {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/adm0/${countryId}/countryData.json`);
return response.json();
diff --git a/src/infrastructure/repositories/GlobalDataRepositoryImpl.ts b/src/infrastructure/repositories/GlobalDataRepositoryImpl.ts
index a122c649..acc14001 100644
--- a/src/infrastructure/repositories/GlobalDataRepositoryImpl.ts
+++ b/src/infrastructure/repositories/GlobalDataRepositoryImpl.ts
@@ -1,12 +1,18 @@
-import { Adm0Data } from '@/domain/entities/Adm0Data';
import { ChangeLogItem } from '@/domain/entities/ChangeLogItem';
import { ResponseWrapper } from '@/domain/entities/common/ResponseWrapper';
+import { CountryCodesData } from '@/domain/entities/country/CountryCodesData';
import { CountryIpcData } from '@/domain/entities/country/CountryIpcData';
+import { CountryMapDataWrapper } from '@/domain/entities/country/CountryMapData';
import { DisputedAreas } from '@/domain/entities/DisputedAreas';
import { YearInReview } from '@/domain/entities/YearInReview';
import { GlobalDataRepository } from '@/domain/repositories/GlobalDataRepository';
export default class GlobalDataRepositoryImpl implements GlobalDataRepository {
+ async getCountryCodes(): Promise {
+ const response = await fetch(`https://static.hungermapdata.org/insight-reports/latest/country.json`);
+ return response.json();
+ }
+
async getYearInReviews(): Promise {
const response = await fetch(`https://static.hungermapdata.org/year-in-review/config.json`);
return response.json();
@@ -14,24 +20,28 @@ export default class GlobalDataRepositoryImpl implements GlobalDataRepository {
async getChangeLog(): Promise {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/changelog.json`);
- const data: ResponseWrapper = await response.json();
+ const data: ResponseWrapper = await response.json();
return data.body;
}
async getDisputedAreas(): Promise {
- const response = await fetch(`https://cdn.hungermapdata.org/hungermap/adm0_disputed_areas_lowres.json`);
+ const response = await fetch(`https://cdn.hungermapdata.org/hungermap/adm0_disputed_areas_lowres.json`, {
+ next: { revalidate: 3600 * 12 },
+ });
return response.json();
}
- async getAdm0Data(): Promise {
- const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/adm0data.json`);
- const data: ResponseWrapper = await response.json();
+ async getMapDataForCountries(): Promise {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/adm0data.json`, {
+ next: { revalidate: 3600 * 12 }, // Next can't actually cache this, because the response is larger than 2MB
+ });
+ const data: ResponseWrapper = await response.json();
return data.body;
}
async getIpcData(): Promise {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/ipc.json`);
- const data: ResponseWrapper = await response.json();
+ const data: ResponseWrapper = await response.json();
return data.body;
}
}
diff --git a/yarn.lock b/yarn.lock
index 40844bf3..f3addb52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1314,6 +1314,18 @@
dependencies:
tslib "^2.4.0"
+"@tanstack/query-core@5.59.17":
+ version "5.59.17"
+ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.59.17.tgz#bda3bb678be48e2f6ee692abd1cfc2db3d455e4b"
+ integrity sha512-jWdDiif8kaqnRGHNXAa9CnudtxY5v9DUxXhodgqX2Rwzj+1UwStDHEbBd9IA5C7VYAaJ2s+BxFR6PUBs8ERorA==
+
+"@tanstack/react-query@^5.59.17":
+ version "5.59.17"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.59.17.tgz#ecb4a2948d69cad7745eb7a2c587b5b52f456cc3"
+ integrity sha512-2taBKHT3LrRmS9ttUOmtaekVOXlZ5JXzNhL9Kmi6BSBdfIAZwEinMXZ8hffVuDpFoRCWlBaGcNkhP/zXgzq5ow==
+ dependencies:
+ "@tanstack/query-core" "5.59.17"
+
"@types/conventional-commits-parser@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8"