diff --git a/package.json b/package.json
index 3e6f1a63..adc6ca2d 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"dependencies": {
"@internationalized/date": "^3.6.0",
"@nextui-org/accordion": "^2.0.40",
+ "@nextui-org/alert": "^2.2.7",
"@nextui-org/button": "^2.0.38",
"@nextui-org/card": "^2.0.34",
"@nextui-org/chip": "^2.0.33",
@@ -37,7 +38,7 @@
"@nextui-org/switch": "^2.0.34",
"@nextui-org/system": "2.2.6",
"@nextui-org/table": "^2.0.40",
- "@nextui-org/theme": "2.2.11",
+ "@nextui-org/theme": "^2.3.0",
"@nextui-org/tooltip": "^2.0.41",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
diff --git a/src/app/comparison-portal/layout.tsx b/src/app/comparison-portal/layout.tsx
new file mode 100644
index 00000000..7443535c
--- /dev/null
+++ b/src/app/comparison-portal/layout.tsx
@@ -0,0 +1,20 @@
+import { Metadata } from 'next';
+
+import HungerMapChatbot from '@/components/Chatbot/Chatbot';
+import { Topbar } from '@/components/Topbar/Topbar';
+
+export const metadata: Metadata = {
+ title: 'Comparison Portal',
+};
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
diff --git a/src/app/comparison-portal/page.tsx b/src/app/comparison-portal/page.tsx
new file mode 100644
index 00000000..00d5e41b
--- /dev/null
+++ b/src/app/comparison-portal/page.tsx
@@ -0,0 +1,28 @@
+import { Suspense } from 'react';
+
+import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
+import CountryComparison from '@/components/ComparisonPortal/CountryComparison';
+import CountrySelectionSkeleton from '@/components/ComparisonPortal/CountrySelectSkeleton';
+import container from '@/container';
+import { GlobalDataRepository } from '@/domain/repositories/GlobalDataRepository';
+
+export default async function ComparisonPortal() {
+ const globalRepo = container.resolve('GlobalDataRepository');
+ const countryMapData = await globalRepo.getMapDataForCountries();
+ const globalFcsData = await globalRepo.getFcsData();
+ return (
+
+
Comparison Portal
+
+
+
+ >
+ }
+ >
+
+
+
+ );
+}
diff --git a/src/components/Charts/CategoricalChart.tsx b/src/components/Charts/CategoricalChart.tsx
index 24fe166a..aa76e876 100644
--- a/src/components/Charts/CategoricalChart.tsx
+++ b/src/components/Charts/CategoricalChart.tsx
@@ -49,7 +49,7 @@ export function CategoricalChart({
// handling the bar and pie chart switch and the theme switch;
useEffect(() => {
setChartOptions(CategoricalChartOperations.getHighChartOptions(data, showPieChart));
- }, [showPieChart, theme]);
+ }, [showPieChart, theme, data]);
const alternativeSwitchButtonProps = disablePieChartSwitch
? undefined
diff --git a/src/components/Charts/LineChart.tsx b/src/components/Charts/LineChart.tsx
index 52891746..b8f31153 100644
--- a/src/components/Charts/LineChart.tsx
+++ b/src/components/Charts/LineChart.tsx
@@ -2,7 +2,7 @@
import Highcharts from 'highcharts';
import { useTheme } from 'next-themes';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { ChartContainer } from '@/components/Charts/helpers/ChartContainer';
import { LineChartData } from '@/domain/entities/charts/LineChartData';
@@ -54,9 +54,15 @@ export function LineChart({
const lineChartOptions: Highcharts.Options = LineChartOperations.getHighChartOptions(lineChartData);
// the `selectedXAxisRange` saves the to be rendered x-axis range of the chart
- // can be changed using the `LinkeChartXAxisSlider` if the param `xAxisSlider==true`
- const xAxisLength: number = LineChartOperations.getDistinctXAxisValues(lineChartData).length;
- const [selectedXAxisRange, setSelectedXAxisRange] = useState([0, xAxisLength - 1]);
+ // can be changed using the `LineChartXAxisSlider` if the param `xAxisSlider==true`
+ const [selectedXAxisRange, setSelectedXAxisRange] = useState([0, 0]);
+ const xAxisLength = useMemo(() => {
+ return LineChartOperations.getDistinctXAxisValues(lineChartData).length;
+ }, [lineChartData]);
+
+ useEffect(() => {
+ setSelectedXAxisRange([0, xAxisLength - 1]);
+ }, [xAxisLength]);
// controlling if a line or bar chart is rendered; line chart is the default
const [showBarChart, setShowBarChart] = useState(false);
@@ -75,7 +81,7 @@ export function LineChart({
LineChartOperations.getHighChartOptions(lineChartData, selectedXAxisRange[0], selectedXAxisRange[1])
);
}
- }, [showBarChart, theme, selectedXAxisRange]);
+ }, [showBarChart, theme, selectedXAxisRange, lineChartData]);
// chart slider props - to manipulate the shown x-axis range
const sliderProps = disableXAxisSlider
diff --git a/src/components/ComparisonPortal/ComparisonAccordionSkeleton.tsx b/src/components/ComparisonPortal/ComparisonAccordionSkeleton.tsx
new file mode 100644
index 00000000..0e370ade
--- /dev/null
+++ b/src/components/ComparisonPortal/ComparisonAccordionSkeleton.tsx
@@ -0,0 +1,23 @@
+import { Skeleton } from '@nextui-org/skeleton';
+import React from 'react';
+import { v4 as uuid } from 'uuid';
+
+export default function ComparisonAccordionSkeleton() {
+ const N_ITEMS = 5;
+ return (
+
+
+ {[...Array(N_ITEMS)].map(() => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/ComparisonPortal/CountryComparison.tsx b/src/components/ComparisonPortal/CountryComparison.tsx
new file mode 100644
index 00000000..af1dd857
--- /dev/null
+++ b/src/components/ComparisonPortal/CountryComparison.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import { useState } from 'react';
+
+import { useSelectedCountries } from '@/domain/hooks/queryParamsHooks.ts';
+import CountryComparisonProps from '@/domain/props/CountryComparisonProps';
+
+import CountryComparisonAccordion from './CountryComparisonAccordion';
+import CountrySelection from './CountrySelection';
+
+export default function CountryComparison({ countryMapData, globalFcsData }: CountryComparisonProps) {
+ const [selectedCountries, setSelectedCountries] = useSelectedCountries(countryMapData);
+ const [disabledCountryIds, setDisabledCountryIds] = useState([]);
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/ComparisonPortal/CountryComparisonAccordion.tsx b/src/components/ComparisonPortal/CountryComparisonAccordion.tsx
new file mode 100644
index 00000000..c3438fdc
--- /dev/null
+++ b/src/components/ComparisonPortal/CountryComparisonAccordion.tsx
@@ -0,0 +1,74 @@
+'use client';
+
+import { useMemo } from 'react';
+
+import ComparisonAccordionSkeleton from '@/components/ComparisonPortal/ComparisonAccordionSkeleton';
+import { useSnackbar } from '@/domain/contexts/SnackbarContext';
+import { useCountryDataListQuery, useCountryIso3DataListQuery } from '@/domain/hooks/countryHooks';
+import CountryComparisonAccordionProps from '@/domain/props/CountryComparisonAccordionProps';
+import { CountryComparisonOperations } from '@/operations/comparison-portal/CountryComparisonOperations';
+
+import AccordionContainer from '../Accordions/AccordionContainer';
+
+export default function CountryComparisonAccordion({
+ selectedCountries,
+ setSelectedCountries,
+ setDisabledCountryIds,
+}: CountryComparisonAccordionProps) {
+ const { showSnackBar } = useSnackbar();
+
+ const countryDataQuery = useCountryDataListQuery(
+ // selected country ids
+ selectedCountries?.map((country) => country.properties.adm0_id),
+ // callback to show snackbar if data not found
+ (invalidCountryId) => {
+ if (!selectedCountries) return;
+ const invalidCountryName = CountryComparisonOperations.getCountryNameById(invalidCountryId, selectedCountries);
+ CountryComparisonOperations.showDataNotFoundSnackBar(showSnackBar, invalidCountryName);
+ setSelectedCountries(selectedCountries.filter((country) => country.properties.adm0_id !== invalidCountryId));
+ setDisabledCountryIds((prevDisabledCountryIds) => [...prevDisabledCountryIds, invalidCountryId.toString()]);
+ }
+ );
+
+ const countryIso3DataQuery = useCountryIso3DataListQuery(
+ // selected country iso3 codes
+ selectedCountries?.map((country) => country.properties.iso3),
+ // callback to show snackbar if data not found
+ (countryCode) => {
+ if (!selectedCountries) return;
+ const countryName = CountryComparisonOperations.getCountryNameByIso3(countryCode, selectedCountries);
+ CountryComparisonOperations.showDataNotFoundSnackBar(showSnackBar, countryName);
+ }
+ );
+
+ const isLoading = useMemo(() => {
+ return (
+ countryDataQuery.some((result) => result.isLoading) || countryIso3DataQuery.some((result) => result.isLoading)
+ );
+ }, [countryDataQuery, countryIso3DataQuery]);
+
+ const { countryDataList, countryIso3DataList } = useMemo(
+ () => CountryComparisonOperations.getFilteredCountryData(countryDataQuery, countryIso3DataQuery),
+ [countryDataQuery, countryIso3DataQuery]
+ );
+
+ const accordionItems = useMemo(() => {
+ if (!selectedCountries) return undefined;
+ const chartData = CountryComparisonOperations.getChartData(countryDataList, countryIso3DataList, selectedCountries);
+ const selectedCountryNames = selectedCountries.map((country) => country.properties.adm0_name);
+ return CountryComparisonOperations.getComparisonAccordionItems(chartData, selectedCountryNames, isLoading);
+ }, [countryDataList, countryIso3DataList, selectedCountries]);
+
+ if (!accordionItems || (countryDataList.length < 2 && isLoading)) return ;
+
+ if (countryDataList.length < 2) {
+ return (
+
+ Select {countryDataList.length === 1 ? 'one additional country' : 'two or more countries'} to start a
+ comparison.
+
+ );
+ }
+
+ return ;
+}
diff --git a/src/components/ComparisonPortal/CountrySelectSkeleton.tsx b/src/components/ComparisonPortal/CountrySelectSkeleton.tsx
new file mode 100644
index 00000000..cd46c64b
--- /dev/null
+++ b/src/components/ComparisonPortal/CountrySelectSkeleton.tsx
@@ -0,0 +1,16 @@
+import { Skeleton } from '@nextui-org/skeleton';
+import React from 'react';
+
+export default function CountrySelectionSkeleton() {
+ return (
+
+ );
+}
diff --git a/src/components/ComparisonPortal/CountrySelection.tsx b/src/components/ComparisonPortal/CountrySelection.tsx
new file mode 100644
index 00000000..ea3f558b
--- /dev/null
+++ b/src/components/ComparisonPortal/CountrySelection.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import { Select, SelectItem } from '@nextui-org/react';
+import { useMemo } from 'react';
+
+import { CountrySelectionProps } from '@/domain/props/CountrySelectionProps';
+import { CountrySelectionOperations } from '@/operations/comparison-portal/CountrySelectionOperations';
+import FcsChoroplethOperations from '@/operations/map/FcsChoroplethOperations';
+
+import { CustomButton } from '../Buttons/CustomButton';
+
+export default function CountrySelection({
+ countryMapData,
+ globalFcsData,
+ selectedCountries,
+ setSelectedCountries,
+ disabledCountryIds,
+}: CountrySelectionProps) {
+ const selectedKeys = useMemo(
+ () => selectedCountries?.map((country) => country.properties.adm0_id.toString()),
+ [selectedCountries]
+ );
+
+ const availableCountries = useMemo(() => {
+ return countryMapData.features.filter((country) => FcsChoroplethOperations.checkIfActive(country, globalFcsData));
+ }, [countryMapData, globalFcsData]);
+
+ const disabledKeys = useMemo(() => {
+ if (!selectedCountries) return [];
+ return availableCountries
+ .filter(
+ (country) =>
+ // if there are already 5 selected countries, disable the rest
+ selectedCountries.length >= 5 &&
+ !selectedCountries.find(
+ (selectedCountry) => selectedCountry.properties.adm0_id === country.properties.adm0_id
+ )
+ )
+ .map((country) => country.properties.adm0_id.toString())
+ .concat(disabledCountryIds);
+ }, [selectedCountries, availableCountries, disabledCountryIds]);
+
+ return (
+
+
+ setSelectedCountries([])}
+ isDisabled={selectedCountries === undefined || selectedCountries.length === 0}
+ >
+ Clear
+
+
+ );
+}
diff --git a/src/components/ComparisonPortal/NoDataHint.tsx b/src/components/ComparisonPortal/NoDataHint.tsx
new file mode 100644
index 00000000..23e22676
--- /dev/null
+++ b/src/components/ComparisonPortal/NoDataHint.tsx
@@ -0,0 +1,39 @@
+import { Alert } from '@nextui-org/alert';
+import { useEffect, useState } from 'react';
+
+import { isLineChartData } from '@/domain/entities/charts/LineChartData';
+import { NoDataHintProps } from '@/domain/props/NoDataHintProps.ts';
+
+export default function NoDataHint({ chartData, selectedCountryNames, isLoading }: NoDataHintProps) {
+ const [formattedMissingCountryNames, setFormattedMissingCountryNames] = useState(null);
+
+ useEffect(() => {
+ if (isLoading) return;
+
+ const countryNamesInChart = isLineChartData(chartData)
+ ? chartData.lines.map((line) => line.name)
+ : chartData.categories.map((category) => category.name);
+ const missingCountryNames = selectedCountryNames.filter(
+ (countryName) => !countryNamesInChart.includes(countryName)
+ );
+ switch (missingCountryNames.length) {
+ case 0:
+ setFormattedMissingCountryNames(null);
+ break;
+ case 1:
+ setFormattedMissingCountryNames(missingCountryNames[0]);
+ break;
+ default:
+ setFormattedMissingCountryNames(
+ `${missingCountryNames.slice(0, -1).join(', ')} and ${missingCountryNames.slice(-1)}`
+ );
+ }
+ }, [isLoading, chartData, selectedCountryNames]);
+
+ return formattedMissingCountryNames ? (
+
+ ) : null;
+}
diff --git a/src/domain/entities/ApiError.ts b/src/domain/entities/ApiError.ts
new file mode 100644
index 00000000..27becb03
--- /dev/null
+++ b/src/domain/entities/ApiError.ts
@@ -0,0 +1,5 @@
+export type ApiError = [{ Error: string }, number];
+
+export function isApiError(error: unknown): error is ApiError {
+ return error instanceof Array && error.length === 2 && typeof error[1] === 'number';
+}
diff --git a/src/domain/entities/charts/InflationGraphs.ts b/src/domain/entities/charts/InflationGraphs.ts
index 41c37e51..23123ace 100644
--- a/src/domain/entities/charts/InflationGraphs.ts
+++ b/src/domain/entities/charts/InflationGraphs.ts
@@ -5,9 +5,9 @@ import { ChartData } from '../common/ChartData';
export interface InflationGraphs {
type: LineChartDataType.INFLATION_CHARTS;
headline: {
- data: ChartData[];
+ data: ChartData[] | undefined;
};
food: {
- data: ChartData[];
+ data: ChartData[] | undefined;
};
}
diff --git a/src/domain/entities/charts/LineChartData.ts b/src/domain/entities/charts/LineChartData.ts
index 5066718b..a2f55b6d 100644
--- a/src/domain/entities/charts/LineChartData.ts
+++ b/src/domain/entities/charts/LineChartData.ts
@@ -51,3 +51,7 @@ export interface LineChartData {
verticalLines?: ChartVerticalLine[];
verticalBands?: ChartVerticalBand[];
}
+
+export function isLineChartData(data: unknown): data is LineChartData {
+ return (data as LineChartData).type === LineChartDataType.LINE_CHART_DATA;
+}
diff --git a/src/domain/entities/charts/RcsiChartData.ts b/src/domain/entities/charts/RcsiChartData.ts
index 891aa5a5..dfa69941 100644
--- a/src/domain/entities/charts/RcsiChartData.ts
+++ b/src/domain/entities/charts/RcsiChartData.ts
@@ -1,6 +1,6 @@
export interface RcsiChartData {
x: string;
- rcsi: number;
- rcsiHigh: number;
- rcsiLow: number;
+ rcsi: number | null;
+ rcsiHigh: number | null;
+ rcsiLow: number | null;
}
diff --git a/src/domain/entities/comparison/CountryComparisonChartdata.ts b/src/domain/entities/comparison/CountryComparisonChartdata.ts
new file mode 100644
index 00000000..80650fd3
--- /dev/null
+++ b/src/domain/entities/comparison/CountryComparisonChartdata.ts
@@ -0,0 +1,13 @@
+import { CategoricalChartData } from '../charts/CategoricalChartData';
+import { LineChartData } from '../charts/LineChartData';
+
+export interface CountryComparisonChartData {
+ fcsChartData?: LineChartData;
+ rcsiChartData?: LineChartData;
+ foodSecurityBarChartData?: CategoricalChartData;
+ populationBarChartData?: CategoricalChartData;
+ importDependencyBarChartData?: CategoricalChartData;
+ balanceOfTradeData?: LineChartData;
+ headlineInflationData?: LineChartData;
+ foodInflationData?: LineChartData;
+}
diff --git a/src/domain/entities/comparison/CountryComparisonData.ts b/src/domain/entities/comparison/CountryComparisonData.ts
new file mode 100644
index 00000000..0c8146c0
--- /dev/null
+++ b/src/domain/entities/comparison/CountryComparisonData.ts
@@ -0,0 +1,7 @@
+import { CountryDataRecord } from '../country/CountryData';
+import { CountryIso3DataRecord } from '../country/CountryIso3Data';
+
+export interface CountryComparisonData {
+ countryDataList: CountryDataRecord[];
+ countryIso3DataList: CountryIso3DataRecord[];
+}
diff --git a/src/domain/entities/country/CountryData.ts b/src/domain/entities/country/CountryData.ts
index 451399c3..86d35ed4 100644
--- a/src/domain/entities/country/CountryData.ts
+++ b/src/domain/entities/country/CountryData.ts
@@ -5,9 +5,11 @@ export interface CountryData {
fcs: number;
fcsMinus1: number;
fcsMinus3: number;
- importDependency: number;
+ importDependency: number | null;
population: number;
populationSource: string;
rcsiGraph: RcsiChartData[];
fcsGraph: FcsChartData[];
}
+
+export type CountryDataRecord = CountryData & { id: number };
diff --git a/src/domain/entities/country/CountryIso3Data.ts b/src/domain/entities/country/CountryIso3Data.ts
index cc4cdd1b..39bf31f9 100644
--- a/src/domain/entities/country/CountryIso3Data.ts
+++ b/src/domain/entities/country/CountryIso3Data.ts
@@ -17,3 +17,5 @@ export interface CountryIso3Data {
balanceOfTradeGraph: BalanceOfTradeGraph;
inflationGraphs: InflationGraphs;
}
+
+export type CountryIso3DataRecord = CountryIso3Data & { id: string };
diff --git a/src/domain/hooks/countryHooks.ts b/src/domain/hooks/countryHooks.ts
index cde4a599..94e10aa3 100644
--- a/src/domain/hooks/countryHooks.ts
+++ b/src/domain/hooks/countryHooks.ts
@@ -1,11 +1,12 @@
-import { useQuery } from '@tanstack/react-query';
+import { useQueries, useQuery, UseQueryResult } from '@tanstack/react-query';
import { cachedQueryClient } from '@/config/queryClient';
import container from '@/container';
+import { isApiError } from '../entities/ApiError';
import { AdditionalCountryData } from '../entities/country/AdditionalCountryData';
-import { CountryData } from '../entities/country/CountryData';
-import { CountryIso3Data } from '../entities/country/CountryIso3Data';
+import { CountryData, CountryDataRecord } from '../entities/country/CountryData';
+import { CountryIso3Data, CountryIso3DataRecord } from '../entities/country/CountryIso3Data';
import { RegionIpc } from '../entities/region/RegionIpc';
import CountryRepository from '../repositories/CountryRepository';
@@ -20,6 +21,34 @@ export const useCountryDataQuery = (countryId: number) =>
cachedQueryClient
);
+/**
+ * Fetches country data for a list of country IDs.
+ * If the data is not found for a country, the `onCountryDataNotFound` callback is called with the country ID.
+ * @param countryIds List of country IDs to fetch data for.
+ * @param onCountryDataNotFound Callback called when the data is not found for a country.
+ * @returns An array of query results for each country ID. The query result is `null` if the data is not found.
+ */
+export const useCountryDataListQuery = (
+ countryIds: number[] | undefined,
+ onCountryDataNotFound: (countryId: number) => void
+) =>
+ useQueries[]>(
+ {
+ queries: (countryIds ?? []).map((countryId) => ({
+ queryKey: ['fetchCountryData', countryId],
+ queryFn: async () => {
+ const countryData = await countryRepo.getCountryData(countryId);
+ if (isApiError(countryData)) {
+ onCountryDataNotFound(countryId);
+ return null;
+ }
+ return { ...countryData, id: countryId };
+ },
+ })),
+ },
+ cachedQueryClient
+ );
+
export const useRegionDataQuery = (countryId: number) =>
useQuery(
{
@@ -38,6 +67,34 @@ export const useCountryIso3DataQuery = (countryCode: string) =>
cachedQueryClient
);
+/**
+ * Fetches country ISO3 data for a list of country codes.
+ * If the data is not found for a country, the `onCountryDataNotFound` callback is called with the country code.
+ * @param countryCodes List of country codes to fetch data for.
+ * @param onCountryDataNotFound Callback called when the data is not found for a country.
+ * @returns An array of query results for each country code. The query result is `null` if the data is not found.
+ */
+export const useCountryIso3DataListQuery = (
+ countryCodes: string[] | undefined,
+ onCountryDataNotFound: (countryCode: string) => void
+) =>
+ useQueries[]>(
+ {
+ queries: (countryCodes ?? []).map((countryCode) => ({
+ queryKey: ['fetchCountryIso3Data', countryCode],
+ queryFn: async () => {
+ const countryIso3Data = await countryRepo.getCountryIso3Data(countryCode);
+ if (isApiError(countryIso3Data)) {
+ onCountryDataNotFound(countryCode);
+ return null;
+ }
+ return { ...countryIso3Data, id: countryCode };
+ },
+ })),
+ },
+ cachedQueryClient
+ );
+
export const useRegionIpcDataQuery = (countryId: number) =>
useQuery(
{
diff --git a/src/domain/hooks/queryParamsHooks.ts b/src/domain/hooks/queryParamsHooks.ts
new file mode 100644
index 00000000..0e020f22
--- /dev/null
+++ b/src/domain/hooks/queryParamsHooks.ts
@@ -0,0 +1,33 @@
+import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
+
+import { CountryMapData, CountryMapDataWrapper } from '@/domain/entities/country/CountryMapData.ts';
+
+// returns a state value that is synchronized with a query param
+// assumes there is one that single query param, all others will be erased
+export const useSelectedCountries = (countryMapData: CountryMapDataWrapper) => {
+ const PARAM_NAME = 'countries';
+
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const [selectedCountries, setSelectedCountries] = useState(undefined);
+
+ // get state values from query params
+ useEffect(() => {
+ const searchParamCountryCodes = searchParams.get(PARAM_NAME)?.split(',') ?? [];
+ const newSelectedCountries = countryMapData.features.filter((availableCountry) =>
+ searchParamCountryCodes.includes(availableCountry.properties.adm0_id.toString())
+ );
+ setSelectedCountries(newSelectedCountries);
+ }, [searchParams]);
+
+ // update state and query params with new value
+ const setSelectedCountriesFn = (newValue: CountryMapData[] | undefined) => {
+ setSelectedCountries(newValue);
+ const selectedCountryIds = newValue?.map((country) => country.properties.adm0_id) ?? [];
+ router.push(`${pathname}?${PARAM_NAME}=${selectedCountryIds.join(',')}`);
+ };
+
+ return [selectedCountries, setSelectedCountriesFn] as const;
+};
diff --git a/src/domain/props/CountryComparisonAccordionProps.ts b/src/domain/props/CountryComparisonAccordionProps.ts
new file mode 100644
index 00000000..b7f29aa9
--- /dev/null
+++ b/src/domain/props/CountryComparisonAccordionProps.ts
@@ -0,0 +1,9 @@
+import { Dispatch, SetStateAction } from 'react';
+
+import { CountryMapData } from '../entities/country/CountryMapData';
+
+export default interface CountryComparisonAccordionProps {
+ selectedCountries: CountryMapData[] | undefined;
+ setSelectedCountries: (newCountries: CountryMapData[]) => void;
+ setDisabledCountryIds: Dispatch>;
+}
diff --git a/src/domain/props/CountryComparisonProps.ts b/src/domain/props/CountryComparisonProps.ts
new file mode 100644
index 00000000..0bc02ce6
--- /dev/null
+++ b/src/domain/props/CountryComparisonProps.ts
@@ -0,0 +1,7 @@
+import { GlobalFcsData } from '../entities/country/CountryFcsData';
+import { CountryMapDataWrapper } from '../entities/country/CountryMapData';
+
+export default interface CountryComparisonProps {
+ countryMapData: CountryMapDataWrapper;
+ globalFcsData: GlobalFcsData;
+}
diff --git a/src/domain/props/CountrySelectionProps.ts b/src/domain/props/CountrySelectionProps.ts
new file mode 100644
index 00000000..ab8920fd
--- /dev/null
+++ b/src/domain/props/CountrySelectionProps.ts
@@ -0,0 +1,10 @@
+import { GlobalFcsData } from '../entities/country/CountryFcsData';
+import { CountryMapData, CountryMapDataWrapper } from '../entities/country/CountryMapData';
+
+export interface CountrySelectionProps {
+ countryMapData: CountryMapDataWrapper;
+ globalFcsData: GlobalFcsData;
+ selectedCountries?: CountryMapData[];
+ setSelectedCountries: (countries: CountryMapData[]) => void;
+ disabledCountryIds: string[];
+}
diff --git a/src/domain/props/NoDataHintProps.ts b/src/domain/props/NoDataHintProps.ts
new file mode 100644
index 00000000..30b2965f
--- /dev/null
+++ b/src/domain/props/NoDataHintProps.ts
@@ -0,0 +1,9 @@
+import { LineChartData } from '@/domain/entities/charts/LineChartData.ts';
+
+import { CategoricalChartData } from '../entities/charts/CategoricalChartData';
+
+export interface NoDataHintProps {
+ chartData: LineChartData | CategoricalChartData;
+ selectedCountryNames: string[];
+ isLoading: boolean;
+}
diff --git a/src/operations/charts/LineChartOperations.ts b/src/operations/charts/LineChartOperations.ts
index ea2269ee..dcbfaa6e 100644
--- a/src/operations/charts/LineChartOperations.ts
+++ b/src/operations/charts/LineChartOperations.ts
@@ -135,15 +135,17 @@ export default class LineChartOperations {
lines: [
{
name: 'Headline Inflation',
- dataPoints: data.headline.data.map((p) => {
- return { x: new Date(p.x).getTime(), y: p.y };
- }),
+ dataPoints:
+ data.headline.data?.map((p) => {
+ return { x: new Date(p.x).getTime(), y: p.y };
+ }) ?? [],
},
{
name: 'Food Inflation',
- dataPoints: data.food.data.map((p) => {
- return { x: new Date(p.x).getTime(), y: p.y };
- }),
+ dataPoints:
+ data.food.data?.map((p) => {
+ return { x: new Date(p.x).getTime(), y: p.y };
+ }) ?? [],
},
],
};
@@ -164,7 +166,7 @@ export default class LineChartOperations {
public static getDistinctXAxisValues(data: LineChartData): number[] {
const uniqueXValues = new Set();
data.lines.forEach((line) => {
- line.dataPoints.forEach((point) => {
+ line.dataPoints?.forEach((point) => {
uniqueXValues.add(point.x); // Add x-value to the Set
});
});
@@ -223,7 +225,7 @@ export default class LineChartOperations {
// collect series data
const seriesData: Highcharts.PointOptionsObject[] = [];
- lineData.dataPoints.forEach((p) => {
+ lineData.dataPoints?.forEach((p) => {
// check if datapoint x is in selected x-axis range
if (xAxisSelectedMin !== undefined && xAxisSelectedMax !== undefined) {
if (p.x < xAxisSelectedMin || xAxisSelectedMax < p.x) return;
@@ -277,7 +279,7 @@ export default class LineChartOperations {
if (lineData.showRange) {
// collect series area range data
const areaSeriesData: Highcharts.PointOptionsObject[] = [];
- lineData.dataPoints.forEach((p) => {
+ lineData.dataPoints?.forEach((p) => {
// check if datapoint x is in selected x-axis range
if (xAxisSelectedMin !== undefined && xAxisSelectedMax !== undefined) {
if (p.x < xAxisSelectedMin || xAxisSelectedMax < p.x) return;
diff --git a/src/operations/comparison-portal/CountryComparisonOperations.tsx b/src/operations/comparison-portal/CountryComparisonOperations.tsx
new file mode 100644
index 00000000..9b54633b
--- /dev/null
+++ b/src/operations/comparison-portal/CountryComparisonOperations.tsx
@@ -0,0 +1,362 @@
+import { Spacer } from '@nextui-org/react';
+import { UseQueryResult } from '@tanstack/react-query';
+
+import { CategoricalChart } from '@/components/Charts/CategoricalChart';
+import { LineChart } from '@/components/Charts/LineChart';
+import NoDataHint from '@/components/ComparisonPortal/NoDataHint';
+import CustomInfoCircle from '@/components/CustomInfoCircle/CustomInfoCircle';
+import { AccordionItemProps } from '@/domain/entities/accordions/Accordions';
+import { CategoricalChartData } from '@/domain/entities/charts/CategoricalChartData';
+import { LineChartData } from '@/domain/entities/charts/LineChartData';
+import { ChartData } from '@/domain/entities/common/ChartData.ts';
+import { CountryComparisonChartData } from '@/domain/entities/comparison/CountryComparisonChartdata';
+import { CountryComparisonData } from '@/domain/entities/comparison/CountryComparisonData';
+import { CountryDataRecord } from '@/domain/entities/country/CountryData';
+import { CountryIso3DataRecord } from '@/domain/entities/country/CountryIso3Data.ts';
+import { CountryMapData } from '@/domain/entities/country/CountryMapData';
+import { SNACKBAR_SHORT_DURATION } from '@/domain/entities/snackbar/Snackbar';
+import { LineChartDataType } from '@/domain/enums/LineChartDataType';
+import { SnackbarPosition, SnackbarStatus } from '@/domain/enums/Snackbar';
+import { SnackbarProps } from '@/domain/props/SnackbarProps';
+import { FcsAccordionOperations } from '@/operations/map/FcsAccordionOperations';
+import { formatToMillion } from '@/utils/formatting.ts';
+
+export class CountryComparisonOperations {
+ static getFcsChartData(countryDataList: CountryDataRecord[], countryMapData: CountryMapData[]): LineChartData {
+ return this.chartWithoutEmptyLines({
+ type: LineChartDataType.LINE_CHART_DATA,
+ xAxisType: 'datetime',
+ yAxisLabel: 'Mill',
+ lines: countryDataList.map((countryData) => ({
+ name: this.getCountryNameById(countryData.id, countryMapData),
+ showRange: true,
+ dataPoints: countryData.fcsGraph.map((fcsChartData) => ({
+ x: new Date(fcsChartData.x).getTime(),
+ y: formatToMillion(fcsChartData.fcs),
+ yRangeMin: formatToMillion(fcsChartData.fcsLow),
+ yRangeMax: formatToMillion(fcsChartData.fcsHigh),
+ })),
+ })),
+ });
+ }
+
+ static getRcsiChartData(countryDataList: CountryDataRecord[], countryMapData: CountryMapData[]): LineChartData {
+ return this.chartWithoutEmptyLines({
+ type: LineChartDataType.LINE_CHART_DATA,
+ xAxisType: 'datetime',
+ yAxisLabel: 'Mill',
+ lines: countryDataList.map((countryData) => ({
+ name: this.getCountryNameById(countryData.id, countryMapData),
+ showRange: true,
+ dataPoints: countryData.rcsiGraph
+ .filter((rcsiChartData) => rcsiChartData.rcsi !== null)
+ .map((rcsiChartData) => ({
+ x: new Date(rcsiChartData.x).getTime(),
+ y: formatToMillion(rcsiChartData.rcsi),
+ yRangeMin: formatToMillion(rcsiChartData.rcsiLow),
+ yRangeMax: formatToMillion(rcsiChartData.rcsiHigh),
+ })),
+ })),
+ });
+ }
+
+ static getPopulationBarChartData(
+ countryDataList: CountryDataRecord[],
+ countryMapData: CountryMapData[]
+ ): CategoricalChartData {
+ return {
+ yAxisLabel: 'Mill',
+ categories: countryDataList.map((countryData) => ({
+ name: this.getCountryNameById(countryData.id, countryMapData),
+ dataPoint: {
+ y: countryData.population,
+ },
+ })),
+ };
+ }
+
+ static getFoodSecurityBarChartData(
+ countryDataList: CountryDataRecord[],
+ countryMapData: CountryMapData[]
+ ): CategoricalChartData {
+ return {
+ yAxisLabel: 'Mill',
+ categories: countryDataList.map((countryData) => ({
+ name: this.getCountryNameById(countryData.id, countryMapData),
+ dataPoint: {
+ y: countryData.fcs,
+ },
+ })),
+ };
+ }
+
+ static getImportDependencyBarChartData(
+ countryDataList: CountryDataRecord[],
+ selectedCountries: CountryMapData[]
+ ): CategoricalChartData {
+ return {
+ yAxisLabel: '% of Cereals',
+ categories: countryDataList
+ .filter((countryData) => countryData.importDependency !== null)
+ .map((countryData) => ({
+ name: this.getCountryNameById(countryData.id, selectedCountries),
+ dataPoint: {
+ y: countryData.importDependency!,
+ },
+ })),
+ };
+ }
+
+ static getBalanceOfTradeData(
+ countryIso3DataList: CountryIso3DataRecord[],
+ selectedCountries: CountryMapData[]
+ ): LineChartData {
+ return this.chartWithoutEmptyLines({
+ type: LineChartDataType.LINE_CHART_DATA,
+ xAxisType: 'datetime',
+ yAxisLabel: 'Mill',
+ lines: countryIso3DataList.map((countryIso3Data) => ({
+ name: this.getCountryNameByIso3(countryIso3Data.id, selectedCountries),
+ dataPoints: countryIso3Data.balanceOfTradeGraph.data.map((p) => {
+ return { x: new Date(p.x).getTime(), y: formatToMillion(p.y) };
+ }),
+ })),
+ });
+ }
+
+ static getInflationData(
+ countryIso3DataList: CountryIso3DataRecord[],
+ selectedCountries: CountryMapData[],
+ type: 'headline' | 'food'
+ ): LineChartData {
+ return this.chartWithoutEmptyLines({
+ type: LineChartDataType.LINE_CHART_DATA,
+ xAxisType: 'datetime',
+ yAxisLabel: 'Rate in %',
+ lines: countryIso3DataList
+ .filter((countryIso3Data) => countryIso3Data.inflationGraphs[type].data !== undefined)
+ .map((countryIso3Data) => ({
+ name: this.getCountryNameByIso3(countryIso3Data.id, selectedCountries),
+ dataPoints: (countryIso3Data.inflationGraphs[type].data as ChartData[]).map((p) => {
+ return { x: new Date(p.x).getTime(), y: p.y };
+ }),
+ })),
+ });
+ }
+
+ static getCountryNameById(id: number, countryMapData: CountryMapData[]): string {
+ return countryMapData.find((country) => country.properties.adm0_id === id)?.properties.adm0_name || '';
+ }
+
+ static getCountryNameByIso3(iso3: string, countryMapData: CountryMapData[]): string {
+ return countryMapData.find((country) => country.properties.iso3 === iso3)?.properties.adm0_name || '';
+ }
+
+ static chartWithoutEmptyLines(chart: LineChartData): LineChartData {
+ return {
+ ...chart,
+ lines: chart.lines.filter((line) => line.dataPoints.length > 0),
+ };
+ }
+
+ static getChartData(
+ countryDataList: CountryDataRecord[],
+ countryIso3DataList: CountryIso3DataRecord[],
+ selectedCountries: CountryMapData[]
+ ): CountryComparisonChartData {
+ return {
+ fcsChartData: countryDataList.length > 1 ? this.getFcsChartData(countryDataList, selectedCountries) : undefined,
+ rcsiChartData: countryDataList.length > 1 ? this.getRcsiChartData(countryDataList, selectedCountries) : undefined,
+ foodSecurityBarChartData:
+ countryDataList.length > 1 ? this.getFoodSecurityBarChartData(countryDataList, selectedCountries) : undefined,
+ populationBarChartData:
+ countryDataList.length > 1 ? this.getPopulationBarChartData(countryDataList, selectedCountries) : undefined,
+ importDependencyBarChartData:
+ countryDataList.length > 1
+ ? this.getImportDependencyBarChartData(countryDataList, selectedCountries)
+ : undefined,
+ balanceOfTradeData:
+ countryIso3DataList.length > 1 ? this.getBalanceOfTradeData(countryIso3DataList, selectedCountries) : undefined,
+ headlineInflationData:
+ countryIso3DataList.length > 1
+ ? this.getInflationData(countryIso3DataList, selectedCountries, 'headline')
+ : undefined,
+ foodInflationData:
+ countryIso3DataList.length > 1
+ ? this.getInflationData(countryIso3DataList, selectedCountries, 'food')
+ : undefined,
+ };
+ }
+
+ static getFilteredCountryData(
+ countryDataQuery: UseQueryResult[],
+ countryIso3DataQuery: UseQueryResult[]
+ ): CountryComparisonData {
+ const countryDataList: CountryDataRecord[] = countryDataQuery
+ .map((result) => result.data)
+ .filter((data): data is CountryDataRecord => data !== null && data !== undefined);
+
+ const countryIso3DataList: CountryIso3DataRecord[] = countryIso3DataQuery
+ .map((result) => result.data)
+ .filter((data): data is CountryIso3DataRecord => data !== null && data !== undefined);
+
+ return { countryDataList, countryIso3DataList };
+ }
+
+ static showDataNotFoundSnackBar(showSnackBar: (props: SnackbarProps) => void, countryName: string): void {
+ showSnackBar({
+ message: `Error fetching country data for ${countryName}`,
+ status: SnackbarStatus.Error,
+ position: SnackbarPosition.BottomMiddle,
+ duration: SNACKBAR_SHORT_DURATION,
+ });
+ }
+
+ static getComparisonAccordionItems(
+ {
+ fcsChartData,
+ rcsiChartData,
+ foodSecurityBarChartData,
+ populationBarChartData,
+ importDependencyBarChartData,
+ balanceOfTradeData,
+ headlineInflationData,
+ foodInflationData,
+ }: CountryComparisonChartData,
+ selectedCountryNames: string[],
+ isLoading: boolean
+ ): AccordionItemProps[] {
+ return [
+ {
+ title: 'Food Security',
+ content: (
+
+ {foodSecurityBarChartData && (
+
+ )}
+ {populationBarChartData && (
+
+ )}
+
+ ),
+ },
+ {
+ title: 'Food Security Trends',
+ content: (
+
+ {fcsChartData && (
+ <>
+
+
+ >
+ )}
+
+ {rcsiChartData && (
+ <>
+
+
+ >
+ )}
+
+ ),
+ },
+ {
+ title: 'Macro-economic',
+ infoIcon: ,
+ popoverInfo: FcsAccordionOperations.getMacroEconomicPopoverInfo(),
+ content: (
+
+ {importDependencyBarChartData && (
+ <>
+
+
+ >
+ )}
+
+ ),
+ },
+ {
+ title: 'Balance of Trade',
+ infoIcon: ,
+ popoverInfo: FcsAccordionOperations.getBalanceOfTradePopoverInfo(),
+ content: (
+
+ {balanceOfTradeData && (
+ <>
+
+
+ >
+ )}
+
+ ),
+ },
+ {
+ title: 'Food and Headline Inflation',
+ infoIcon: ,
+ popoverInfo: FcsAccordionOperations.getHeadlineAndFoodInflationPopoverInfo(),
+ content: (
+
+ {headlineInflationData && (
+ <>
+
+
+ >
+ )}
+ {foodInflationData && (
+ <>
+
+
+ >
+ )}
+
+ ),
+ },
+ ];
+ }
+}
diff --git a/src/operations/comparison-portal/CountrySelectionOperations.ts b/src/operations/comparison-portal/CountrySelectionOperations.ts
new file mode 100644
index 00000000..c22aa502
--- /dev/null
+++ b/src/operations/comparison-portal/CountrySelectionOperations.ts
@@ -0,0 +1,16 @@
+import { SharedSelection } from '@nextui-org/react';
+
+import { CountryMapData, CountryMapDataWrapper } from '@/domain/entities/country/CountryMapData';
+
+export class CountrySelectionOperations {
+ static onSelectionChange(
+ keys: SharedSelection,
+ setSelectedCountries: (countries: CountryMapData[]) => void,
+ countryMapData: CountryMapDataWrapper
+ ) {
+ const keySet = new Set(keys as string);
+ setSelectedCountries(
+ countryMapData.features.filter((country) => keySet.has(country.properties.adm0_id.toString()))
+ );
+ }
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 4f24a79b..bcf690ce 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -49,7 +49,7 @@ const config = {
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/domain/constant/dataSourceTables/dataSourceAccordionItems.tsx',
- './src/operations/tables/*.tsx',
+ './src/operations/**/*.tsx',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}',
],
theme: {
diff --git a/yarn.lock b/yarn.lock
index 17133265..4cf49f0f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1452,6 +1452,18 @@
"@react-types/accordion" "3.0.0-alpha.21"
"@react-types/shared" "3.23.1"
+"@nextui-org/alert@^2.2.7":
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/@nextui-org/alert/-/alert-2.2.7.tgz#fc776a7858adc1d6d63623d197c2aa32c7f1aa63"
+ integrity sha512-HOIroKCa3z6hE58lAgF+JS8vsNFblOXcuMJwB+92t19q+jkoDCgo7pkPVqi4Hm4MRY2GpUQp+iAGZklA2P4n4Q==
+ dependencies:
+ "@nextui-org/button" "2.2.7"
+ "@nextui-org/react-utils" "2.1.1"
+ "@nextui-org/shared-icons" "2.1.1"
+ "@nextui-org/shared-utils" "2.1.1"
+ "@react-aria/utils" "3.26.0"
+ "@react-stately/utils" "3.10.5"
+
"@nextui-org/aria-utils@2.0.26":
version "2.0.26"
resolved "https://registry.yarnpkg.com/@nextui-org/aria-utils/-/aria-utils-2.0.26.tgz#0113247f80bc558aea650c15e38ee0ab7c7ac9f8"
@@ -1544,6 +1556,23 @@
"@react-types/button" "3.9.4"
"@react-types/shared" "3.23.1"
+"@nextui-org/button@2.2.7":
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/@nextui-org/button/-/button-2.2.7.tgz#032cd556870a3361be8acdddfe0e995ab4719ac5"
+ integrity sha512-3o7uAD7nFAUKJkg2PZ5ktJR8qqMrjQgzewXBWfz62RWya/uTFXzqwljy0JYGwq3tfvbmBzA1GGsAZdj5wUEymw==
+ dependencies:
+ "@nextui-org/react-utils" "2.1.1"
+ "@nextui-org/ripple" "2.2.5"
+ "@nextui-org/shared-utils" "2.1.1"
+ "@nextui-org/spinner" "2.2.4"
+ "@nextui-org/use-aria-button" "2.2.3"
+ "@react-aria/button" "3.11.0"
+ "@react-aria/focus" "3.19.0"
+ "@react-aria/interactions" "3.22.5"
+ "@react-aria/utils" "3.26.0"
+ "@react-types/button" "3.10.1"
+ "@react-types/shared" "3.26.0"
+
"@nextui-org/calendar@2.0.12":
version "2.0.12"
resolved "https://registry.yarnpkg.com/@nextui-org/calendar/-/calendar-2.0.12.tgz#e3e5df719cd08b710a51602d279a25d737b3c5b2"
@@ -1675,6 +1704,11 @@
"@nextui-org/system-rsc" "2.1.6"
"@react-types/shared" "3.23.1"
+"@nextui-org/dom-animation@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@nextui-org/dom-animation/-/dom-animation-2.1.1.tgz#42611dc7aff1a6ca11ff3d7683ea754a35ec8577"
+ integrity sha512-xLrVNf1EV9zyyZjk6j3RptOvnga1WUCbMpDgJLQHp+oYwxTfBy0SkXHuN5pRdcR0XpR/IqRBDIobMdZI0iyQyg==
+
"@nextui-org/dropdown@2.1.31", "@nextui-org/dropdown@^2.1.31":
version "2.1.31"
resolved "https://registry.yarnpkg.com/@nextui-org/dropdown/-/dropdown-2.1.31.tgz#4606509b00101f9a04a420400837485fe0390081"
@@ -1898,6 +1932,11 @@
resolved "https://registry.yarnpkg.com/@nextui-org/react-rsc-utils/-/react-rsc-utils-2.0.14.tgz#277523854e594858c0b713df783f2d5228915f83"
integrity sha512-s0GVgDhScyx+d9FtXd8BXf049REyaPvWsO4RRr7JDHrk91NlQ11Mqxka9o+8g5NX0rphI0rbe3/b1Dz+iQRx3w==
+"@nextui-org/react-rsc-utils@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@nextui-org/react-rsc-utils/-/react-rsc-utils-2.1.1.tgz#82a545ea952fa0f98769bb446ed448cab292e5bb"
+ integrity sha512-9uKH1XkeomTGaswqlGKt0V0ooUev8mPXtKJolR+6MnpvBUrkqngw1gUGF0bq/EcCCkks2+VOHXZqFT6x9hGkQQ==
+
"@nextui-org/react-utils@2.0.17":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@nextui-org/react-utils/-/react-utils-2.0.17.tgz#3ed7903496ca8a9f5678ad816a2b24f1cd0581f9"
@@ -1906,6 +1945,14 @@
"@nextui-org/react-rsc-utils" "2.0.14"
"@nextui-org/shared-utils" "2.0.8"
+"@nextui-org/react-utils@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@nextui-org/react-utils/-/react-utils-2.1.1.tgz#36f9c216c539e22a4d68faccac4d5b01e98d7966"
+ integrity sha512-cN3Z0b2bV6Nf0CYD4imsGdXbHMQqad8KivltpBv1ItbI1/FSTAv9AHTKSzDE15hd/UwOGYt3Qm7I6tWzqov55w==
+ dependencies:
+ "@nextui-org/react-rsc-utils" "2.1.1"
+ "@nextui-org/shared-utils" "2.1.1"
+
"@nextui-org/react@^2.4.8":
version "2.4.8"
resolved "https://registry.yarnpkg.com/@nextui-org/react/-/react-2.4.8.tgz#2a430cca1d2e1dc812a55ca763d61e2ba5b0e195"
@@ -1964,6 +2011,15 @@
"@nextui-org/react-utils" "2.0.17"
"@nextui-org/shared-utils" "2.0.8"
+"@nextui-org/ripple@2.2.5":
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/@nextui-org/ripple/-/ripple-2.2.5.tgz#6ddedd46d043294a3ee06e088b2284b9ee14e2e7"
+ integrity sha512-GNYcRbVrUtdkbHKyFGRb0W8dyudH6hYY6YxYtlE5I82YwN1FovLvTRajDBbd3Bh2qNIcyUoFlmbt4h/6mM8uOQ==
+ dependencies:
+ "@nextui-org/dom-animation" "2.1.1"
+ "@nextui-org/react-utils" "2.1.1"
+ "@nextui-org/shared-utils" "2.1.1"
+
"@nextui-org/scroll-shadow@2.1.20":
version "2.1.20"
resolved "https://registry.yarnpkg.com/@nextui-org/scroll-shadow/-/scroll-shadow-2.1.20.tgz#9d6934cf3f8807f66e5b0280d61d484817ab7ec6"
@@ -2001,11 +2057,26 @@
resolved "https://registry.yarnpkg.com/@nextui-org/shared-icons/-/shared-icons-2.0.9.tgz#ecc674ec51ba7f0570ee821aed317fba4cc70376"
integrity sha512-WG3yinVY7Tk9VqJgcdF4V8Ok9+fcm5ey7S1els7kujrfqLYxtqoKywgiY/7QHwZlfQkzpykAfy+NAlHkTP5hMg==
+"@nextui-org/shared-icons@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@nextui-org/shared-icons/-/shared-icons-2.1.1.tgz#c2001523e96db8bcbdb46e4fe52d8c38f5e1018d"
+ integrity sha512-mkiTpFJnCzB2M8Dl7IwXVzDKKq9ZW2WC0DaQRs1eWgqboRCP8DDde+MJZq331hC7pfH8BC/4rxXsKECrOUUwCg==
+
"@nextui-org/shared-utils@2.0.8":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@nextui-org/shared-utils/-/shared-utils-2.0.8.tgz#6e6e71a067c273581839c2226fd9fb4e1e3a3410"
integrity sha512-ZEtoMPXS+IjT8GvpJTS9IWDnT1JNCKV+NDqqgysAf1niJmOFLyJgl6dh/9n4ufcGf1GbSEQN+VhJasEw7ajYGQ==
+"@nextui-org/shared-utils@2.0.9":
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/@nextui-org/shared-utils/-/shared-utils-2.0.9.tgz#08e06464b9e8f9b1e7456ea97895f360e952262d"
+ integrity sha512-tb+bkkRb8Yb2kaQYKQF6VvxOCuIjwP9UWHUL7zgq3mjOVtHlzz9iqrNlcc3xCrOtNWIBQuI15MAUYeo3PXXeqg==
+
+"@nextui-org/shared-utils@2.1.1":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@nextui-org/shared-utils/-/shared-utils-2.1.1.tgz#e5a7edbdfe88c4b9595e413cfec958d95096f6f1"
+ integrity sha512-qE8gZO63GqUX1ljOi/4PlwGzE84dhUS3zFIq+10/N6ePAaNjM4DwtL4ocucG3abCz4iRUueYKLIxTO2+eYyAfw==
+
"@nextui-org/skeleton@2.0.32", "@nextui-org/skeleton@^2.0.32":
version "2.0.32"
resolved "https://registry.yarnpkg.com/@nextui-org/skeleton/-/skeleton-2.0.32.tgz#3815157bd137e5dd4004db22af0c56c6b216d7da"
@@ -2062,6 +2133,15 @@
"@nextui-org/shared-utils" "2.0.8"
"@nextui-org/system-rsc" "2.1.6"
+"@nextui-org/spinner@2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@nextui-org/spinner/-/spinner-2.2.4.tgz#72b9cb53a661d53ec99dc521c8951ba665a6ff93"
+ integrity sha512-QdCRD64+fjWmi5byO9sR3D4xbkFNh9cB01KnoR1/u7+7m/WWNFOhUkYbdkB2e53oQlNXxh0Mw02U346tC8B+5g==
+ dependencies:
+ "@nextui-org/react-utils" "2.1.1"
+ "@nextui-org/shared-utils" "2.1.1"
+ "@nextui-org/system-rsc" "2.3.4"
+
"@nextui-org/switch@2.0.34", "@nextui-org/switch@^2.0.34":
version "2.0.34"
resolved "https://registry.yarnpkg.com/@nextui-org/switch/-/switch-2.0.34.tgz#69569063aa35561220177beb2d392b3cfe87cea1"
@@ -2086,6 +2166,14 @@
"@react-types/shared" "3.23.1"
clsx "^1.2.1"
+"@nextui-org/system-rsc@2.3.4":
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/@nextui-org/system-rsc/-/system-rsc-2.3.4.tgz#a9a4d4ba317a4b6c5f954b2c0d3c475191dbd695"
+ integrity sha512-Y6OLFO7diYnUMe5ffDPt6sIqCaah7FOqRaJ3ZQ/We8gE8AgHnyNQxWllLtRzBqaCiIheHLo7dTMed1FFmb775A==
+ dependencies:
+ "@react-types/shared" "3.26.0"
+ clsx "^1.2.1"
+
"@nextui-org/system@2.2.6":
version "2.2.6"
resolved "https://registry.yarnpkg.com/@nextui-org/system/-/system-2.2.6.tgz#837223d2acbfb6499fbd03d6345387897fa3d2a0"
@@ -2157,6 +2245,20 @@
tailwind-merge "^1.14.0"
tailwind-variants "^0.1.20"
+"@nextui-org/theme@^2.3.0":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@nextui-org/theme/-/theme-2.3.0.tgz#f33faa02684c32dfe2950563f98b43a2b0a5790e"
+ integrity sha512-MTWpjXeG+i9svlib8xMz17aK4GWijGoLZ4BRQv8JxvhZMUMuqPxkVeuMAXqg3dK9eF13KmtjCDf4K7d9/WkzYg==
+ dependencies:
+ "@nextui-org/shared-utils" "2.0.9"
+ clsx "^1.2.1"
+ color "^4.2.3"
+ color2k "^2.0.2"
+ deepmerge "4.3.1"
+ flat "^5.0.2"
+ tailwind-merge "^2.5.2"
+ tailwind-variants "^0.1.20"
+
"@nextui-org/tooltip@2.0.41", "@nextui-org/tooltip@^2.0.41":
version "2.0.41"
resolved "https://registry.yarnpkg.com/@nextui-org/tooltip/-/tooltip-2.0.41.tgz#a2027bf1e0f836c98bfd6bcc541cfbdda711f4be"
@@ -2199,6 +2301,18 @@
"@react-types/button" "3.9.4"
"@react-types/shared" "3.23.1"
+"@nextui-org/use-aria-button@2.2.3":
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/@nextui-org/use-aria-button/-/use-aria-button-2.2.3.tgz#4a84b68e6b9093f7cbc196b6e09836f0395b8fca"
+ integrity sha512-KG5A3tgSxmwq07zYwAocnulIoyDQZ8qjmE+m0Gx0tkb438gyN4VMIv7wCWhtrmhlJCfiNLvXdxCS4MjCWv7YCQ==
+ dependencies:
+ "@nextui-org/shared-utils" "2.1.1"
+ "@react-aria/focus" "3.19.0"
+ "@react-aria/interactions" "3.22.5"
+ "@react-aria/utils" "3.26.0"
+ "@react-types/button" "3.10.1"
+ "@react-types/shared" "3.26.0"
+
"@nextui-org/use-aria-link@2.0.19":
version "2.0.19"
resolved "https://registry.yarnpkg.com/@nextui-org/use-aria-link/-/use-aria-link-2.0.19.tgz#c0d63e65b5ecca362120b12df0fff770b2e19e06"
@@ -2400,6 +2514,20 @@
"@react-types/shared" "^3.23.1"
"@swc/helpers" "^0.5.0"
+"@react-aria/button@3.11.0":
+ version "3.11.0"
+ resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.11.0.tgz#cb7790db23949ec9c1e698fa531ee5471cf2b515"
+ integrity sha512-b37eIV6IW11KmNIAm65F3SEl2/mgj5BrHIysW6smZX3KoKWTGYsYfcQkmtNgY0GOSFfDxMCoolsZ6mxC00nSDA==
+ dependencies:
+ "@react-aria/focus" "^3.19.0"
+ "@react-aria/interactions" "^3.22.5"
+ "@react-aria/toolbar" "3.0.0-beta.11"
+ "@react-aria/utils" "^3.26.0"
+ "@react-stately/toggle" "^3.8.0"
+ "@react-types/button" "^3.10.1"
+ "@react-types/shared" "^3.26.0"
+ "@swc/helpers" "^0.5.0"
+
"@react-aria/button@3.9.5":
version "3.9.5"
resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.9.5.tgz#f0082f58394394f3d16fdf45de57b382748f3345"
@@ -2514,7 +2642,7 @@
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"
-"@react-aria/focus@^3.17.1", "@react-aria/focus@^3.19.0":
+"@react-aria/focus@3.19.0", "@react-aria/focus@^3.17.1", "@react-aria/focus@^3.19.0":
version "3.19.0"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.19.0.tgz#82b9a5b83f023b943a7970df3d059f49d61df05d"
integrity sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A==
@@ -2604,7 +2732,7 @@
"@react-types/shared" "^3.23.1"
"@swc/helpers" "^0.5.0"
-"@react-aria/interactions@^3.21.3", "@react-aria/interactions@^3.22.5":
+"@react-aria/interactions@3.22.5", "@react-aria/interactions@^3.21.3", "@react-aria/interactions@^3.22.5":
version "3.22.5"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.22.5.tgz#9cd8c93b8b6988f1d315d3efb450119d1432bbb8"
integrity sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ==
@@ -2950,6 +3078,17 @@
"@react-types/shared" "^3.26.0"
"@swc/helpers" "^0.5.0"
+"@react-aria/toolbar@3.0.0-beta.11":
+ version "3.0.0-beta.11"
+ resolved "https://registry.yarnpkg.com/@react-aria/toolbar/-/toolbar-3.0.0-beta.11.tgz#019c9ff2a47ad207504a95afeb0f863cf71a114b"
+ integrity sha512-LM3jTRFNDgoEpoL568WaiuqiVM7eynSQLJis1hV0vlVnhTd7M7kzt7zoOjzxVb5Uapz02uCp1Fsm4wQMz09qwQ==
+ dependencies:
+ "@react-aria/focus" "^3.19.0"
+ "@react-aria/i18n" "^3.12.4"
+ "@react-aria/utils" "^3.26.0"
+ "@react-types/shared" "^3.26.0"
+ "@swc/helpers" "^0.5.0"
+
"@react-aria/tooltip@3.7.4":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@react-aria/tooltip/-/tooltip-3.7.4.tgz#0efe8b4cc543a39395e99861ad6f0c64cd746026"
@@ -2974,7 +3113,7 @@
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"
-"@react-aria/utils@^3.24.1", "@react-aria/utils@^3.26.0":
+"@react-aria/utils@3.26.0", "@react-aria/utils@^3.24.1", "@react-aria/utils@^3.26.0":
version "3.26.0"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.26.0.tgz#833cbfa33e75d15835b757791b3f754432d2f948"
integrity sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ==
@@ -3402,7 +3541,7 @@
dependencies:
"@swc/helpers" "^0.5.0"
-"@react-stately/utils@^3.10.1", "@react-stately/utils@^3.10.5":
+"@react-stately/utils@3.10.5", "@react-stately/utils@^3.10.1", "@react-stately/utils@^3.10.5":
version "3.10.5"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.5.tgz#47bb91cd5afd1bafe39353614e5e281b818ebccc"
integrity sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==
@@ -3441,6 +3580,13 @@
"@react-types/link" "^3.5.9"
"@react-types/shared" "^3.26.0"
+"@react-types/button@3.10.1", "@react-types/button@^3.10.1", "@react-types/button@^3.9.4":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.10.1.tgz#fc7ada2e83bc661b31c1473a82ec86dc11de057c"
+ integrity sha512-XTtap8o04+4QjPNAshFWOOAusUTxQlBjU2ai0BTVLShQEjHhRVDBIWsI2B2FKJ4KXT6AZ25llaxhNrreWGonmA==
+ dependencies:
+ "@react-types/shared" "^3.26.0"
+
"@react-types/button@3.9.4":
version "3.9.4"
resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.9.4.tgz#ec10452e870660d31db1994f6fe4abfe0c800814"
@@ -3448,13 +3594,6 @@
dependencies:
"@react-types/shared" "^3.23.1"
-"@react-types/button@^3.10.1", "@react-types/button@^3.9.4":
- version "3.10.1"
- resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.10.1.tgz#fc7ada2e83bc661b31c1473a82ec86dc11de057c"
- integrity sha512-XTtap8o04+4QjPNAshFWOOAusUTxQlBjU2ai0BTVLShQEjHhRVDBIWsI2B2FKJ4KXT6AZ25llaxhNrreWGonmA==
- dependencies:
- "@react-types/shared" "^3.26.0"
-
"@react-types/calendar@3.4.6":
version "3.4.6"
resolved "https://registry.yarnpkg.com/@react-types/calendar/-/calendar-3.4.6.tgz#66ddcefc3058492b3cce58a6e63b01558048b669"
@@ -3639,7 +3778,7 @@
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.23.1.tgz#2f23c81d819d0ef376df3cd4c944be4d6bce84c3"
integrity sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==
-"@react-types/shared@^3.23.1", "@react-types/shared@^3.26.0":
+"@react-types/shared@3.26.0", "@react-types/shared@^3.23.1", "@react-types/shared@^3.26.0":
version "3.26.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.26.0.tgz#21a8b579f0097ee78de18e3e580421ced89e4c8c"
integrity sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw==
@@ -10010,6 +10149,11 @@ tailwind-merge@^1.14.0:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==
+tailwind-merge@^2.5.2:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.5.5.tgz#98167859b856a2a6b8d2baf038ee171b9d814e39"
+ integrity sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==
+
tailwind-variants@0.1.20, tailwind-variants@^0.1.20:
version "0.1.20"
resolved "https://registry.yarnpkg.com/tailwind-variants/-/tailwind-variants-0.1.20.tgz#8aaed9094be0379a438641a42d588943e44c5fcd"