From 9d202cf12219bc3fe3f4b8dcb91a76f9ba1202f2 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 25 Jun 2026 09:12:41 +0200 Subject: [PATCH 1/9] use safer Number.isNaN (no coertion) --- webapp/src/pages/all4trees/dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index b8e056f3..cffa98ec 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -71,7 +71,7 @@ export default function DashboardPage() { const handleYearChange = (year: string) => { const numericYear = Number(year); - if (!isNaN(numericYear)) { + if (!Number.isNaN(numericYear)) { setSelectedYear(numericYear); setChartData(data[numericYear]?.beneficiary ?? {}); } else { From d68cc643453c4cf1f02bec268217c3e079c64153 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 25 Jun 2026 10:40:02 +0200 Subject: [PATCH 2/9] replace useEffect with (i18n'ed) ErrorBoundary + Suspense + use() --- webapp/package-lock.json | 10 ++ webapp/package.json | 1 + webapp/src/pages/all4trees/dashboard.tsx | 127 +++++++++++++----- .../i18n/translations/en/all4trees.json | 4 + .../i18n/translations/fr/all4trees.json | 4 + 5 files changed, 111 insertions(+), 35 deletions(-) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 64412313..cbbf1c35 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -32,6 +32,7 @@ "react": "^19.2.0", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", + "react-error-boundary": "^6.1.2", "react-i18next": "^16.5.4", "react-plotly.js": "^2.6.0", "react-resizable-panels": "^3.0.6", @@ -6997,6 +6998,15 @@ "react": "^19.2.4" } }, + "node_modules/react-error-boundary": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.2.tgz", + "integrity": "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-i18next": { "version": "16.5.4", "license": "MIT", diff --git a/webapp/package.json b/webapp/package.json index 2b94f3de..4647beeb 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -49,6 +49,7 @@ "react": "^19.2.0", "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", + "react-error-boundary": "^6.1.2", "react-i18next": "^16.5.4", "react-plotly.js": "^2.6.0", "react-resizable-panels": "^3.0.6", diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index cffa98ec..69cc1b16 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -1,4 +1,10 @@ -import { useEffect, useState } from "react"; +import type { TFunction } from "i18next"; +import { Suspense, use, useState } from "react"; +import { + ErrorBoundary, + type FallbackProps, + getErrorMessage, +} from "react-error-boundary"; import { ClipLoader } from "react-spinners"; import { DashboardHeader } from "@widgets/dashboard/dashboard-header"; @@ -10,6 +16,7 @@ import { import { LAYERS } from "@shared/api/layers"; import { useApi } from "@shared/hooks/useApi"; +import { useTranslation } from "@shared/i18n"; export type DataField = { value: number | null; error: number | null }; @@ -45,52 +52,71 @@ function formatBeneficiaryData( }; } -export default function DashboardPage() { +function Loading() { + return ( +
+ +
+ ); +} + +// ✅ Cache the Promise so the same one is reused across renders +// required by 'use()', see https://react.dev/reference/react/use#caching-promises-for-client-components +const cache = new Map< + (typeof LAYERS)[keyof typeof LAYERS], + Promise +>(); + +// TODO: bettter typing (no "as") +export function fetchData({ + getDashboardData, + layer, +}: { + getDashboardData: (layerId: string) => Promise; + layer: (typeof LAYERS)[keyof typeof LAYERS]; +}): Promise { + const cachedPromise = cache.get(layer); + if (cachedPromise) { + return cachedPromise; + } + const promise = getDashboardData(layer); + cache.set(layer, promise); + return promise; +} + +function Dashboard() { const api = useApi(); + + const data = use( + fetchData({ + getDashboardData: api.getDashboardData, + layer: LAYERS.INVENTARY, + }), + ); + + return ; +} + +function YearDashboard({ data }: { data: DashboardData }) { const [selectedYear, setSelectedYear] = useState(2024); - const [data, setData] = useState({}); - const [chartData, setChartData] = useState>({}); - const [loading, setLoading] = useState(true); - - // biome-ignore lint/correctness/useExhaustiveDependencies : - useEffect(() => { - loadDashboardData(); - }, []); - - const loadDashboardData = async () => { - try { - const dashboardData = await api.getDashboardData(LAYERS.INVENTARY); - setData(dashboardData); - setChartData(dashboardData[selectedYear]?.beneficiary ?? {}); - } catch (error) { - console.error("Erreur lors du chargement des données:", error); - } finally { - setLoading(false); - } - }; + const chartData = (data[selectedYear]?.beneficiary ?? {}) as Record< + string, + DataField + >; const handleYearChange = (year: string) => { const numericYear = Number(year); if (!Number.isNaN(numericYear)) { setSelectedYear(numericYear); - setChartData(data[numericYear]?.beneficiary ?? {}); } else { console.warn("Année sélectionnée invalide:", year); } }; - if (loading) { - return ( -
- -
- ); - } - return (
); } + +// t must be passed to the fallback render function because the error boundary fallback component cannot call hooks like useTranslation +function getFallbackRender({ t }: { t: TFunction<"all4trees", undefined> }) { + function FallbackRender({ error }: FallbackProps) { + const errorMessage = + getErrorMessage(error) ?? t("dashboard.error.unknownMessage"); + + return ( +
+

+ {t("dashboard.error.title")} +

+

{errorMessage}

+
+ ); + } + + return FallbackRender; +} + +export default function DashboardPage() { + const { t } = useTranslation("all4trees"); + + return ( + + }> + + + + ); +} diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index 08232962..6eed8053 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -1,5 +1,9 @@ { "dashboard": { + "error": { + "title": "Error while loading data", + "unknownMessage": "An unknown error occurred. Please try again later." + }, "select": { "year": "Year" } diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index 82472bfa..8f311087 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -1,5 +1,9 @@ { "dashboard": { + "error": { + "title": "Erreur lors du chargement des données", + "unknownMessage": "Une erreur inconnue s'est produite. Veuillez réessayer plus tard." + }, "select": { "year": "Année" } From d4b42ce3f0b89dc1df502b5fd051cab82906750a Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 25 Jun 2026 10:40:51 +0200 Subject: [PATCH 3/9] use a theme color (light green) instead of blue --- webapp/src/pages/all4trees/dashboard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index 69cc1b16..b87ec6e8 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -56,7 +56,12 @@ function Loading() { return (
From ff28c05382472e985c55d6ee5fc40f454869fe1f Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 25 Jun 2026 11:32:52 +0200 Subject: [PATCH 4/9] rename --- webapp/src/pages/all4trees/dashboard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index b87ec6e8..aa2a8fd6 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -103,10 +103,10 @@ function Dashboard() { }), ); - return ; + return ; } -function YearDashboard({ data }: { data: DashboardData }) { +function LoadedDashboard({ data }: { data: DashboardData }) { const [selectedYear, setSelectedYear] = useState(2024); const chartData = (data[selectedYear]?.beneficiary ?? {}) as Record< string, From 95e73b8ad15aa3a2de7050b5eba6974229fe4e88 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 25 Jun 2026 11:42:57 +0200 Subject: [PATCH 5/9] move components to widgets --- webapp/src/pages/all4trees/dashboard.tsx | 166 +----------------- webapp/src/widgets/dashboard/dashboard.tsx | 45 +++++ .../dashboard/error-boundary-fallback.tsx | 25 +++ .../widgets/dashboard/loaded-dashboard.tsx | 80 +++++++++ webapp/src/widgets/dashboard/loading.tsx | 18 ++ 5 files changed, 173 insertions(+), 161 deletions(-) create mode 100644 webapp/src/widgets/dashboard/dashboard.tsx create mode 100644 webapp/src/widgets/dashboard/error-boundary-fallback.tsx create mode 100644 webapp/src/widgets/dashboard/loaded-dashboard.tsx create mode 100644 webapp/src/widgets/dashboard/loading.tsx diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index aa2a8fd6..f0d44da0 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -1,168 +1,12 @@ -import type { TFunction } from "i18next"; -import { Suspense, use, useState } from "react"; -import { - ErrorBoundary, - type FallbackProps, - getErrorMessage, -} from "react-error-boundary"; -import { ClipLoader } from "react-spinners"; +import { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; -import { DashboardHeader } from "@widgets/dashboard/dashboard-header"; +import Dashboard from "@widgets/dashboard/dashboard"; +import { getFallbackRender } from "@widgets/dashboard/error-boundary-fallback"; +import Loading from "@widgets/dashboard/loading"; -import { - ChartForestPotential, - type ChartForestPotentialData, -} from "@features/charts/biodiversity/chart-forest-potential"; - -import { LAYERS } from "@shared/api/layers"; -import { useApi } from "@shared/hooks/useApi"; import { useTranslation } from "@shared/i18n"; -export type DataField = { value: number | null; error: number | null }; - -export type DashboardData = Record< - number, - { beneficiary: Record; control: Record } ->; - -function twoDecimals(data: Record) { - return Object.fromEntries( - Object.entries(data).map(([key, { value, error }]) => [ - key, - { - error: error == null ? 0 : Number(error.toFixed(2)), - value: value == null ? 0 : Number(value.toFixed(2)), - }, - ]), - ) as Record; -} - -function formatBeneficiaryData( - beneficiary: Record, -): ChartForestPotentialData { - return { - deadWood: beneficiary.epf_deadWood.value ?? 0, - density: beneficiary.epf_tree_density.value ?? 0, - diameterDistribution: beneficiary.epf_diameter_distribution.value ?? 0, - diversity: beneficiary.epf_tree_diversity.value ?? 0, - dominantHeight: beneficiary.epf_dominant_height.value ?? 0, - microHabitat: beneficiary.epf_microhabitats.value ?? 0, - spatialDistribution: beneficiary.epf_spatial_distribution.value ?? 0, - verticalDistribution: beneficiary.epf_vertical_distribution.value ?? 0, - }; -} - -function Loading() { - return ( -
- -
- ); -} - -// ✅ Cache the Promise so the same one is reused across renders -// required by 'use()', see https://react.dev/reference/react/use#caching-promises-for-client-components -const cache = new Map< - (typeof LAYERS)[keyof typeof LAYERS], - Promise ->(); - -// TODO: bettter typing (no "as") -export function fetchData({ - getDashboardData, - layer, -}: { - getDashboardData: (layerId: string) => Promise; - layer: (typeof LAYERS)[keyof typeof LAYERS]; -}): Promise { - const cachedPromise = cache.get(layer); - if (cachedPromise) { - return cachedPromise; - } - const promise = getDashboardData(layer); - cache.set(layer, promise); - return promise; -} - -function Dashboard() { - const api = useApi(); - - const data = use( - fetchData({ - getDashboardData: api.getDashboardData, - layer: LAYERS.INVENTARY, - }), - ); - - return ; -} - -function LoadedDashboard({ data }: { data: DashboardData }) { - const [selectedYear, setSelectedYear] = useState(2024); - const chartData = (data[selectedYear]?.beneficiary ?? {}) as Record< - string, - DataField - >; - - const handleYearChange = (year: string) => { - const numericYear = Number(year); - if (!Number.isNaN(numericYear)) { - setSelectedYear(numericYear); - } else { - console.warn("Année sélectionnée invalide:", year); - } - }; - - return ( -
- -
- -
-
- ); -} - -// t must be passed to the fallback render function because the error boundary fallback component cannot call hooks like useTranslation -function getFallbackRender({ t }: { t: TFunction<"all4trees", undefined> }) { - function FallbackRender({ error }: FallbackProps) { - const errorMessage = - getErrorMessage(error) ?? t("dashboard.error.unknownMessage"); - - return ( -
-

- {t("dashboard.error.title")} -

-

{errorMessage}

-
- ); - } - - return FallbackRender; -} - export default function DashboardPage() { const { t } = useTranslation("all4trees"); diff --git a/webapp/src/widgets/dashboard/dashboard.tsx b/webapp/src/widgets/dashboard/dashboard.tsx new file mode 100644 index 00000000..db25f380 --- /dev/null +++ b/webapp/src/widgets/dashboard/dashboard.tsx @@ -0,0 +1,45 @@ +import { use } from "react"; + +import LoadedDashboard, { + type DashboardData, +} from "@widgets/dashboard/loaded-dashboard"; + +import { LAYERS } from "@shared/api/layers"; +import { useApi } from "@shared/hooks/useApi"; + +// ✅ Cache the Promise so the same one is reused across renders +// required by 'use()', see https://react.dev/reference/react/use#caching-promises-for-client-components +const cache = new Map< + (typeof LAYERS)[keyof typeof LAYERS], + Promise +>(); + +// TODO: bettter typing (no "as") +export function fetchData({ + getDashboardData, + layer, +}: { + getDashboardData: (layerId: string) => Promise; + layer: (typeof LAYERS)[keyof typeof LAYERS]; +}): Promise { + const cachedPromise = cache.get(layer); + if (cachedPromise) { + return cachedPromise; + } + const promise = getDashboardData(layer); + cache.set(layer, promise); + return promise; +} + +export default function Dashboard() { + const api = useApi(); + + const data = use( + fetchData({ + getDashboardData: api.getDashboardData, + layer: LAYERS.INVENTARY, + }), + ); + + return ; +} diff --git a/webapp/src/widgets/dashboard/error-boundary-fallback.tsx b/webapp/src/widgets/dashboard/error-boundary-fallback.tsx new file mode 100644 index 00000000..caf5e143 --- /dev/null +++ b/webapp/src/widgets/dashboard/error-boundary-fallback.tsx @@ -0,0 +1,25 @@ +import type { TFunction } from "i18next"; +import { type FallbackProps, getErrorMessage } from "react-error-boundary"; + +// t must be passed to the fallback render function because the error boundary fallback component cannot call hooks like useTranslation +export function getFallbackRender({ + t, +}: { + t: TFunction<"all4trees", undefined>; +}) { + function FallbackRender({ error }: FallbackProps) { + const errorMessage = + getErrorMessage(error) ?? t("dashboard.error.unknownMessage"); + + return ( +
+

+ {t("dashboard.error.title")} +

+

{errorMessage}

+
+ ); + } + + return FallbackRender; +} diff --git a/webapp/src/widgets/dashboard/loaded-dashboard.tsx b/webapp/src/widgets/dashboard/loaded-dashboard.tsx new file mode 100644 index 00000000..5e1af443 --- /dev/null +++ b/webapp/src/widgets/dashboard/loaded-dashboard.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; + +import { DashboardHeader } from "@widgets/dashboard/dashboard-header"; + +import { + ChartForestPotential, + type ChartForestPotentialData, +} from "@features/charts/biodiversity/chart-forest-potential"; + +export type DataField = { value: number | null; error: number | null }; + +export type DashboardData = Record< + number, + { beneficiary: Record; control: Record } +>; + +function twoDecimals(data: Record) { + return Object.fromEntries( + Object.entries(data).map(([key, { value, error }]) => [ + key, + { + error: error == null ? 0 : Number(error.toFixed(2)), + value: value == null ? 0 : Number(value.toFixed(2)), + }, + ]), + ) as Record; +} + +function formatBeneficiaryData( + beneficiary: Record, +): ChartForestPotentialData { + return { + deadWood: beneficiary.epf_deadWood.value ?? 0, + density: beneficiary.epf_tree_density.value ?? 0, + diameterDistribution: beneficiary.epf_diameter_distribution.value ?? 0, + diversity: beneficiary.epf_tree_diversity.value ?? 0, + dominantHeight: beneficiary.epf_dominant_height.value ?? 0, + microHabitat: beneficiary.epf_microhabitats.value ?? 0, + spatialDistribution: beneficiary.epf_spatial_distribution.value ?? 0, + verticalDistribution: beneficiary.epf_vertical_distribution.value ?? 0, + }; +} + +export default function LoadedDashboard({ data }: { data: DashboardData }) { + const [selectedYear, setSelectedYear] = useState(2024); + const chartData = (data[selectedYear]?.beneficiary ?? {}) as Record< + string, + DataField + >; + + const handleYearChange = (year: string) => { + const numericYear = Number(year); + if (!Number.isNaN(numericYear)) { + setSelectedYear(numericYear); + } else { + console.warn("Année sélectionnée invalide:", year); + } + }; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/webapp/src/widgets/dashboard/loading.tsx b/webapp/src/widgets/dashboard/loading.tsx new file mode 100644 index 00000000..14cfed58 --- /dev/null +++ b/webapp/src/widgets/dashboard/loading.tsx @@ -0,0 +1,18 @@ +import { ClipLoader } from "react-spinners"; + +export default function Loading() { + return ( +
+ +
+ ); +} From c5588df0b13f1d282875cbbda24b4ae8ba82741a Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 25 Jun 2026 16:17:25 +0200 Subject: [PATCH 6/9] improve promises cache (per API token, and don't cache errors) --- webapp/src/widgets/dashboard/dashboard.tsx | 36 +++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/webapp/src/widgets/dashboard/dashboard.tsx b/webapp/src/widgets/dashboard/dashboard.tsx index db25f380..75cf6f6d 100644 --- a/webapp/src/widgets/dashboard/dashboard.tsx +++ b/webapp/src/widgets/dashboard/dashboard.tsx @@ -7,27 +7,47 @@ import LoadedDashboard, { import { LAYERS } from "@shared/api/layers"; import { useApi } from "@shared/hooks/useApi"; -// ✅ Cache the Promise so the same one is reused across renders +type GetDashboardData = (layer: string) => Promise; +type Layer = (typeof LAYERS)[keyof typeof LAYERS]; + +// ✅ Cache Promises so the same one is reused across renders // required by 'use()', see https://react.dev/reference/react/use#caching-promises-for-client-components -const cache = new Map< - (typeof LAYERS)[keyof typeof LAYERS], - Promise +// Cache is scoped by API client (auth token) + layer to avoid leaking data across sessions. +const cache = new WeakMap< + GetDashboardData, + Map> >(); -// TODO: bettter typing (no "as") +function getPerApiCache(getDashboardData: GetDashboardData) { + const perApiCache = cache.get(getDashboardData); + if (perApiCache) { + return perApiCache; + } + const newPerApiCache = new Map>(); + cache.set(getDashboardData, newPerApiCache); + return newPerApiCache; +} + export function fetchData({ getDashboardData, layer, }: { - getDashboardData: (layerId: string) => Promise; - layer: (typeof LAYERS)[keyof typeof LAYERS]; + getDashboardData: GetDashboardData; + layer: Layer; }): Promise { + const cache = getPerApiCache(getDashboardData); const cachedPromise = cache.get(layer); + if (cachedPromise) { return cachedPromise; } - const promise = getDashboardData(layer); + const promise = getDashboardData(layer).catch((err) => { + // Don't cache failures forever; allow retries (e.g. after navigation / remount). + cache.delete(layer); + throw err; + }); cache.set(layer, promise); + return promise; } From 2483ed1905b427b9986a825b6b1d0b190105a94b Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Tue, 30 Jun 2026 16:08:56 +0200 Subject: [PATCH 7/9] fix fetch logic and error management --- webapp/src/pages/all4trees/dashboard.tsx | 17 +-------- .../i18n/translations/en/all4trees.json | 1 + .../i18n/translations/fr/all4trees.json | 1 + webapp/src/widgets/dashboard/dashboard.tsx | 37 +++++++++++++++---- .../dashboard/error-boundary-fallback.tsx | 13 ++++++- .../widgets/dashboard/loaded-dashboard.tsx | 10 ++++- 6 files changed, 52 insertions(+), 27 deletions(-) diff --git a/webapp/src/pages/all4trees/dashboard.tsx b/webapp/src/pages/all4trees/dashboard.tsx index f0d44da0..4ac6920c 100644 --- a/webapp/src/pages/all4trees/dashboard.tsx +++ b/webapp/src/pages/all4trees/dashboard.tsx @@ -1,20 +1,5 @@ -import { Suspense } from "react"; -import { ErrorBoundary } from "react-error-boundary"; - import Dashboard from "@widgets/dashboard/dashboard"; -import { getFallbackRender } from "@widgets/dashboard/error-boundary-fallback"; -import Loading from "@widgets/dashboard/loading"; - -import { useTranslation } from "@shared/i18n"; export default function DashboardPage() { - const { t } = useTranslation("all4trees"); - - return ( - - }> - - - - ); + return ; } diff --git a/webapp/src/shared/i18n/translations/en/all4trees.json b/webapp/src/shared/i18n/translations/en/all4trees.json index 6eed8053..a1a079b7 100644 --- a/webapp/src/shared/i18n/translations/en/all4trees.json +++ b/webapp/src/shared/i18n/translations/en/all4trees.json @@ -1,6 +1,7 @@ { "dashboard": { "error": { + "retry": "Retry", "title": "Error while loading data", "unknownMessage": "An unknown error occurred. Please try again later." }, diff --git a/webapp/src/shared/i18n/translations/fr/all4trees.json b/webapp/src/shared/i18n/translations/fr/all4trees.json index 8f311087..f53adf8e 100644 --- a/webapp/src/shared/i18n/translations/fr/all4trees.json +++ b/webapp/src/shared/i18n/translations/fr/all4trees.json @@ -1,6 +1,7 @@ { "dashboard": { "error": { + "retry": "Réessayer", "title": "Erreur lors du chargement des données", "unknownMessage": "Une erreur inconnue s'est produite. Veuillez réessayer plus tard." }, diff --git a/webapp/src/widgets/dashboard/dashboard.tsx b/webapp/src/widgets/dashboard/dashboard.tsx index 75cf6f6d..8be7c85c 100644 --- a/webapp/src/widgets/dashboard/dashboard.tsx +++ b/webapp/src/widgets/dashboard/dashboard.tsx @@ -1,11 +1,15 @@ -import { use } from "react"; +import { Suspense, useCallback, useMemo, useState } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { getFallbackRender } from "@widgets/dashboard/error-boundary-fallback"; import LoadedDashboard, { type DashboardData, } from "@widgets/dashboard/loaded-dashboard"; +import Loading from "@widgets/dashboard/loading"; import { LAYERS } from "@shared/api/layers"; import { useApi } from "@shared/hooks/useApi"; +import { useTranslation } from "@shared/i18n"; type GetDashboardData = (layer: string) => Promise; type Layer = (typeof LAYERS)[keyof typeof LAYERS]; @@ -28,7 +32,7 @@ function getPerApiCache(getDashboardData: GetDashboardData) { return newPerApiCache; } -export function fetchData({ +function fetchData({ getDashboardData, layer, }: { @@ -52,14 +56,31 @@ export function fetchData({ } export default function Dashboard() { + const { t } = useTranslation("all4trees"); const api = useApi(); + const fetch = useCallback( + () => + fetchData({ + getDashboardData: api.getDashboardData, + layer: LAYERS.INVENTARY, + }), + [api], + ); + const [dataPromise, setDataPromise] = useState(fetch); - const data = use( - fetchData({ - getDashboardData: api.getDashboardData, - layer: LAYERS.INVENTARY, - }), + const retry = useCallback(() => { + setDataPromise(fetch()); + }, [fetch]); + const fallbackRender = useMemo( + () => getFallbackRender({ retry, t }), + [retry, t], ); - return ; + return ( + + }> + + + + ); } diff --git a/webapp/src/widgets/dashboard/error-boundary-fallback.tsx b/webapp/src/widgets/dashboard/error-boundary-fallback.tsx index caf5e143..d2ab8704 100644 --- a/webapp/src/widgets/dashboard/error-boundary-fallback.tsx +++ b/webapp/src/widgets/dashboard/error-boundary-fallback.tsx @@ -3,8 +3,10 @@ import { type FallbackProps, getErrorMessage } from "react-error-boundary"; // t must be passed to the fallback render function because the error boundary fallback component cannot call hooks like useTranslation export function getFallbackRender({ + retry, t, }: { + retry?: () => void; t: TFunction<"all4trees", undefined>; }) { function FallbackRender({ error }: FallbackProps) { @@ -13,10 +15,19 @@ export function getFallbackRender({ return (
-

+

{t("dashboard.error.title")}

{errorMessage}

+ {retry && ( + + )}
); } diff --git a/webapp/src/widgets/dashboard/loaded-dashboard.tsx b/webapp/src/widgets/dashboard/loaded-dashboard.tsx index 5e1af443..66bee46d 100644 --- a/webapp/src/widgets/dashboard/loaded-dashboard.tsx +++ b/webapp/src/widgets/dashboard/loaded-dashboard.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { use, useState } from "react"; import { DashboardHeader } from "@widgets/dashboard/dashboard-header"; @@ -41,7 +41,13 @@ function formatBeneficiaryData( }; } -export default function LoadedDashboard({ data }: { data: DashboardData }) { +export default function LoadedDashboard({ + dataPromise, +}: { + dataPromise: Promise; +}) { + const data = use(dataPromise); + const [selectedYear, setSelectedYear] = useState(2024); const chartData = (data[selectedYear]?.beneficiary ?? {}) as Record< string, From 362f5c55cd44859b8ac69fdd46390500f36139cf Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Tue, 30 Jun 2026 16:09:16 +0200 Subject: [PATCH 8/9] adapt data to the new backend format --- .../src/widgets/dashboard/loaded-dashboard.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/webapp/src/widgets/dashboard/loaded-dashboard.tsx b/webapp/src/widgets/dashboard/loaded-dashboard.tsx index 66bee46d..b8255d34 100644 --- a/webapp/src/widgets/dashboard/loaded-dashboard.tsx +++ b/webapp/src/widgets/dashboard/loaded-dashboard.tsx @@ -30,14 +30,14 @@ function formatBeneficiaryData( beneficiary: Record, ): ChartForestPotentialData { return { - deadWood: beneficiary.epf_deadWood.value ?? 0, - density: beneficiary.epf_tree_density.value ?? 0, - diameterDistribution: beneficiary.epf_diameter_distribution.value ?? 0, - diversity: beneficiary.epf_tree_diversity.value ?? 0, - dominantHeight: beneficiary.epf_dominant_height.value ?? 0, - microHabitat: beneficiary.epf_microhabitats.value ?? 0, - spatialDistribution: beneficiary.epf_spatial_distribution.value ?? 0, - verticalDistribution: beneficiary.epf_vertical_distribution.value ?? 0, + deadWood: beneficiary.bio_idx_deadWood.value ?? 0, + density: beneficiary.bio_idx_tree_density.value ?? 0, + diameterDistribution: beneficiary.bio_idx_diametric_distribution.value ?? 0, + diversity: beneficiary.bio_idx_tree_diversity.value ?? 0, + dominantHeight: beneficiary.bio_idx_dominant_height.value ?? 0, + microHabitat: beneficiary.bio_idx_microhabitats.value ?? 0, + spatialDistribution: beneficiary.bio_idx_spatial_distribution.value ?? 0, + verticalDistribution: beneficiary.bio_idx_vertical_distribution.value ?? 0, }; } From 04f97b24e89fe7cae368bea65ee7df3eb1ca6e45 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Tue, 30 Jun 2026 16:25:31 +0200 Subject: [PATCH 9/9] format --- webapp/src/widgets/dashboard/dashboard.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/src/widgets/dashboard/dashboard.tsx b/webapp/src/widgets/dashboard/dashboard.tsx index 8be7c85c..7a7e505f 100644 --- a/webapp/src/widgets/dashboard/dashboard.tsx +++ b/webapp/src/widgets/dashboard/dashboard.tsx @@ -77,7 +77,10 @@ export default function Dashboard() { ); return ( - + }>