-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/f 142 comparison portal (#135)
* feat: simple country selection * feat: add chart and disabled countries * feat: use single selection component * feat: improved data fetching * feat: add alert * feat: add basic bar chart & fix country selection * feat: add comparison charts for import dependency, BOT, inflation * fix: disabled countries * fix: country selection and number formatting * feat: refactoring * feat: remove alert * feat: add query param support * fix: accept undefined inflation graphs * feat: show hint if not enough countries are selected * fix: remove redundant food security heading * fix: country selection update * feat: remove comments * feat: add missing x-axis sliders for comparisons * feat: remove countries without data from comparison charts * fix: reset selectedXAxisRange if data changes * feat: add hint if no data available * fix: downgrade @nextui-org/theme again * fix: do not show NoDataHint while loading * feat: handle importDependency = null * feat: add skeleton on load * feat: add comparison accordion suspense * feat: add country select suspense & skeleton * fix: return type * feat: minor refactoring * feat: bar chart & fix react warnings * fix: layout * fix: add removed code & add comments * feat: deselect error country * fix: no scroll in initial page * feat: deselect error country in url params * fix: do not auto-expand comparison accordions * feat: do not show 2 bar charts next to each other on mobile * fix: remove invalid country after selection * fix: remove unused code * fix: remove unused code * fix: move suspense boundary up * fix: selectedCountries can be undefined * fix: add query params hook to update query params and state simutaneously * feat: disable error country & remove countries without data from selection * feat: clear selection button * feat: transparent bg --------- Co-authored-by: Jakob Schödl <[email protected]>
- Loading branch information
1 parent
5272e1e
commit 0689a93
Showing
30 changed files
with
1,038 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className="min-h-screen flex flex-col"> | ||
<div className="mb-20"> | ||
<Topbar /> | ||
<HungerMapChatbot /> | ||
</div> | ||
<main className="flex flex-col gap-6 lg:gap-10 p-5 lg:p-10 text-content w-full">{children}</main> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>('GlobalDataRepository'); | ||
const countryMapData = await globalRepo.getMapDataForCountries(); | ||
const globalFcsData = await globalRepo.getFcsData(); | ||
return ( | ||
<div> | ||
<h1>Comparison Portal</h1> | ||
<Suspense | ||
fallback={ | ||
<> | ||
<CountrySelectionSkeleton /> | ||
<ComparisonAccordionSkeleton /> | ||
</> | ||
} | ||
> | ||
<CountryComparison countryMapData={countryMapData} globalFcsData={globalFcsData} /> | ||
</Suspense> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/components/ComparisonPortal/ComparisonAccordionSkeleton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className="overflow-x-auto rounded-lg shadow-none"> | ||
<div className="flex flex-col gap-2 mb-4"> | ||
{[...Array(N_ITEMS)].map(() => ( | ||
<div | ||
key={uuid()} | ||
className="rounded-medium last:border-b-0 bg-content1 white:bg-white overflow-hidden shadow-md" | ||
> | ||
<Skeleton className="rounded-lg bg-content1 dark:bg-content1"> | ||
<div className="h-[69px]" /> | ||
</Skeleton> | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string[]>([]); | ||
|
||
return ( | ||
<div> | ||
<CountrySelection | ||
countryMapData={countryMapData} | ||
globalFcsData={globalFcsData} | ||
selectedCountries={selectedCountries} | ||
setSelectedCountries={setSelectedCountries} | ||
disabledCountryIds={disabledCountryIds} | ||
/> | ||
<CountryComparisonAccordion | ||
selectedCountries={selectedCountries} | ||
setSelectedCountries={setSelectedCountries} | ||
setDisabledCountryIds={setDisabledCountryIds} | ||
/> | ||
</div> | ||
); | ||
} |
74 changes: 74 additions & 0 deletions
74
src/components/ComparisonPortal/CountryComparisonAccordion.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <ComparisonAccordionSkeleton />; | ||
|
||
if (countryDataList.length < 2) { | ||
return ( | ||
<p className="pb-4"> | ||
Select {countryDataList.length === 1 ? 'one additional country' : 'two or more countries'} to start a | ||
comparison. | ||
</p> | ||
); | ||
} | ||
|
||
return <AccordionContainer multipleSelectionMode loading={isLoading} items={accordionItems} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Skeleton } from '@nextui-org/skeleton'; | ||
import React from 'react'; | ||
|
||
export default function CountrySelectionSkeleton() { | ||
return ( | ||
<div className="pb-4 space-y-6"> | ||
<div className="group flex flex-col w-full"> | ||
<div className="w-full flex flex-col"> | ||
<Skeleton className="relative px-3 gap-3 w-full shadow-sm h-10 min-h-10 rounded-medium"> | ||
<div className="inline-flex h-full w-[calc(100%_-_theme(spacing.6))] min-h-4 items-center gap-1.5 box-border" /> | ||
</Skeleton> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className="pb-4 flex items-center gap-4"> | ||
<Select | ||
placeholder="Select up to 5 countries" | ||
aria-label="Select countries for comparison" | ||
selectionMode="multiple" | ||
onSelectionChange={(keys) => | ||
CountrySelectionOperations.onSelectionChange(keys, setSelectedCountries, countryMapData) | ||
} | ||
defaultSelectedKeys={selectedKeys} | ||
selectedKeys={selectedKeys} | ||
disabledKeys={disabledKeys} | ||
className="w-full" | ||
variant="faded" | ||
color="primary" | ||
> | ||
{availableCountries | ||
.sort((a, b) => a.properties.adm0_name.localeCompare(b.properties.adm0_name)) | ||
.map((country) => ( | ||
<SelectItem | ||
key={country.properties.adm0_id.toString()} | ||
aria-label={country.properties.adm0_name} | ||
className="transition-all hover:text-background dark:text-foreground" | ||
> | ||
{country.properties.adm0_name} | ||
</SelectItem> | ||
))} | ||
</Select> | ||
<CustomButton | ||
variant="bordered" | ||
onClick={() => setSelectedCountries([])} | ||
isDisabled={selectedCountries === undefined || selectedCountries.length === 0} | ||
> | ||
Clear | ||
</CustomButton> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string | null>(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 ? ( | ||
<Alert | ||
description={`No data for ${formattedMissingCountryNames}.`} | ||
classNames={{ mainWrapper: 'justify-center', iconWrapper: 'my-0.5' }} | ||
/> | ||
) : null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.