diff --git a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx index c0fc12b..d1f38dc 100644 --- a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx +++ b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx @@ -4,7 +4,7 @@ import '@/styles/draggable-time-filter-range.css' import { cn } from '@/utility/classNames' import { dateToComparableDateItem } from '@/utility/comparableDateItemSchema' import { format } from '@/utility/dateUtil' -import useEvents from '@/utility/useEvents' +import { useAllEvents } from '@/utility/useEvents' import { isInSameAggregationUnit } from '@/utility/useTimeIntervals' import useDebounce from '@custom-react-hooks/use-debounce' import useElementSize from '@custom-react-hooks/use-element-size' @@ -14,7 +14,7 @@ import RangeSlider from 'react-range-slider-input' import useTimelineEvents from './EventsTimeline/useTimelineEvents' function DraggableTimeFilterRange() { - const { isLoading } = useEvents() + const { isLoading } = useAllEvents() const from = useFiltersStore(({ from }) => dateToComparableDateItem(from)) const to = useFiltersStore(({ to }) => dateToComparableDateItem(to)) const setDateRange = useFiltersStore(({ setDateRange }) => setDateRange) @@ -147,10 +147,8 @@ const HandleTooptip = memo( const BackgroundVis = memo(() => { const [parentRef, size] = useElementSize() const { datasetStartDate, datasetEndDate } = useToday() - const { data } = useEvents() const { eventColumns, columnsCount, sizeScale } = useTimelineEvents({ size, - data, aggregationUnit: 'week', config: { eventMinHeight: 2, diff --git a/frontend-nextjs/src/components/EventsTimeline/EventsTimeline.tsx b/frontend-nextjs/src/components/EventsTimeline/EventsTimeline.tsx index b8eae6e..ed48986 100644 --- a/frontend-nextjs/src/components/EventsTimeline/EventsTimeline.tsx +++ b/frontend-nextjs/src/components/EventsTimeline/EventsTimeline.tsx @@ -4,7 +4,12 @@ import { cn } from '@/utility/classNames' import { type ComparableDateItemType } from '@/utility/comparableDateItemSchema' import { format } from '@/utility/dateUtil' import { parseErrorMessage } from '@/utility/errorHandlingUtil' -import useEvents, { type UseEventsReturnType } from '@/utility/useEvents' +import { useAllEvents, useFilteredEvents } from '@/utility/useEvents' +import { + useAllOrganisations, + useFilteredEventsOrganisations, + useSelectedOrganisations, +} from '@/utility/useOrganisations' import { isInSameAggregationUnit } from '@/utility/useTimeIntervals' import useElementSize from '@custom-react-hooks/use-element-size' import { QueryErrorResetBoundary } from '@tanstack/react-query' @@ -28,13 +33,15 @@ type DataSourceInsertionType = { name: string } -function EventsTimeline({ data }: { data: UseEventsReturnType['data'] }) { +function EventsTimeline() { const [parentRef, size] = useElementSize() - const { organisations, eventsByOrgs, selectedOrganisations } = data const aggregationUnit = useAggregationUnit(size.width) + const { filteredEvents, isLoading } = useFilteredEvents() + const { organisations } = useAllOrganisations() + const { filteredEventsOrganisations } = useFilteredEventsOrganisations() + const { selectedOrganisations } = useSelectedOrganisations() const { eventColumns, columnsCount, sizeScale } = useTimelineEvents({ size, - data, aggregationUnit, }) // const { today } = useToday(); @@ -54,7 +61,8 @@ function EventsTimeline({ data }: { data: UseEventsReturnType['data'] }) { // name: "Press Releases by Last Generation", // }, ] - if (eventsByOrgs.length === 0) return + + if (!isLoading && filteredEvents.length === 0) return return ( @@ -119,7 +127,7 @@ function EventsTimeline({ data }: { data: UseEventsReturnType['data'] }) { key={event.event_id} event={event} organisations={organisations} - selectedOrganisations={selectedOrganisations} + selectedOrganisations={filteredEventsOrganisations} height={sizeScale(event.size_number ?? 0)} /> ))} @@ -129,7 +137,7 @@ function EventsTimeline({ data }: { data: UseEventsReturnType['data'] }) { sumSize={sumSize} height={Math.ceil(sizeScale(sumSize))} organisations={combinedOrganizers} - selectedOrganisations={combinedSelectedOrganizers} + selectedOrganisations={filteredEventsOrganisations} events={eventsWithSize} aggregationUnit={aggregationUnit} /> @@ -147,6 +155,7 @@ function EventsTimeline({ data }: { data: UseEventsReturnType['data'] }) { @@ -154,11 +163,11 @@ function EventsTimeline({ data }: { data: UseEventsReturnType['data'] }) { } function EventsTimelineWithData({ reset }: { reset?: () => void }) { - const { data, isFetching, isPending, error, isError } = useEvents() + const { allEvents, isFetching, isPending, error, isError } = useAllEvents() if (isFetching || isPending) return if (isError) return - if (data?.events.length > 0) return + if (allEvents.length > 0) return return } export default function EventsTimelineWithErrorBoundary() { diff --git a/frontend-nextjs/src/components/EventsTimeline/EventsTimelineLegend.tsx b/frontend-nextjs/src/components/EventsTimeline/EventsTimelineLegend.tsx index 35854e9..d209a86 100644 --- a/frontend-nextjs/src/components/EventsTimeline/EventsTimelineLegend.tsx +++ b/frontend-nextjs/src/components/EventsTimeline/EventsTimelineLegend.tsx @@ -13,6 +13,7 @@ import useAggregationUnit from './useAggregationUnit' export type LegendOrganisation = Omit & { count?: number + isActive?: boolean } const placeholderOrganisations: LegendOrganisation[] = [ @@ -105,9 +106,11 @@ const placeholderOrganisations: LegendOrganisation[] = [ function EventsTimelineLegend({ sizeScale = scaleLinear().domain([10, 100000]).range([10, 193.397]), selectedOrganisations = placeholderOrganisations, + organisations = placeholderOrganisations, }: { sizeScale?: ScaleLinear selectedOrganisations?: LegendOrganisation[] + organisations?: LegendOrganisation[] }) { const [parentRef, { width }] = useElementSize() const aggregationUnit = useAggregationUnit(width) @@ -126,7 +129,10 @@ function EventsTimelineLegend({ sizeScale={sizeScale} aggragationUnit={aggregationUnit} /> - + { - if (!data?.allEvents) return []; + if (!allEvents) return []; return intervals.map((comparableDateObject) => { - const columnEvents = data.allEvents.filter((evt) => - isInSameUnit(evt, comparableDateObject), - ); + const columnEvents = allEvents.filter((evt) => { + const isInUnit = isInSameUnit(evt, comparableDateObject) + if (!isInUnit || selectedOrganisations.length === 0) return false + return selectedOrganisations.find((org) => evt.organizers.find((x) => x.slug === org.slug)) + }); const eventsWithSize = columnEvents.sort((a, b) => { const aSize = a.size_number ?? 0; const bSize = b.size_number ?? 0; @@ -80,19 +84,19 @@ function useTimelineEvents({ columnEvents, } }) - }, [data.allEvents, intervals, isInSameUnit]) + }, [allEvents, selectedOrganisations, intervals, isInSameUnit]) const eventColumns = useMemo(() => { - if (!data?.organisations) return []; + if (!organisations) return []; return eventColumnsWithoutOrgs.map((d) => ({ ...d, - combinedOrganizers: combineOrganizers(d.columnEvents, data.organisations), + combinedOrganizers: combineOrganizers(d.columnEvents, organisations), combinedSelectedOrganizers: combineOrganizers( d.columnEvents, - data.selectedOrganisations, + selectedOrganisations, ), })); - }, [data.organisations, data.selectedOrganisations, eventColumnsWithoutOrgs]); + }, [organisations, selectedOrganisations, eventColumnsWithoutOrgs]); const sizeScale = useMemo(() => { const displayedEvents = eventColumns.reduce((acc, day) => { diff --git a/frontend-nextjs/src/components/ImpactChart/ImpactChartColumnDescriptions.tsx b/frontend-nextjs/src/components/ImpactChart/ImpactChartColumnDescriptions.tsx index 4c299b2..220b558 100644 --- a/frontend-nextjs/src/components/ImpactChart/ImpactChartColumnDescriptions.tsx +++ b/frontend-nextjs/src/components/ImpactChart/ImpactChartColumnDescriptions.tsx @@ -11,7 +11,7 @@ import type { import type { ParsedMediaImpactItemType } from '@/utility/mediaImpactUtil' import { texts, titleCase } from '@/utility/textUtil' import { topicIsSentiment } from '@/utility/topicsUtil' -import useEvents from '@/utility/useEvents' +import { useAllEvents, useFilteredEvents } from '@/utility/useEvents' import { useOrganisation } from '@/utility/useOrganisations' import { scaleQuantize } from 'd3-scale' import { @@ -38,20 +38,21 @@ const SelectedTimeframeTooltip = memo( ({ organisation }: { organisation?: OrganisationType }) => { const from = useFiltersStore(({ from }) => format(from, 'LLLL d, yyyy')) const to = useFiltersStore(({ to }) => format(to, 'LLLL d, yyyy')) - const { data } = useEvents() + const { allEvents } = useAllEvents() + const { filteredEvents } = useFilteredEvents() const minPercentageConsideredGood = 40 const { color, percentageOfOrgsInTimeframe, showNotice } = useMemo(() => { - if (!data || !organisation) + if (!allEvents || !organisation) return { color: undefined, percentageOfOrgsInTimeframe: undefined, showNotice: false, } - const allEventsFromOrg = data.allEvents.filter((e) => + const allEventsFromOrg = allEvents.filter((e) => e.organizers.find((o) => o.slug === organisation.slug), ).length - const eventsFromOrgInTimeframe = data.events.filter((e) => + const eventsFromOrgInTimeframe = filteredEvents.filter((e) => e.organizers.find((o) => o.slug === organisation.slug), ).length const percentageOfOrgsInTimeframe = @@ -68,7 +69,7 @@ const SelectedTimeframeTooltip = memo( percentageOfOrgsInTimeframe, showNotice: percentageOfOrgsInTimeframe < minPercentageConsideredGood, } - }, [data, organisation]) + }, [allEvents, filteredEvents, organisation]) return ( diff --git a/frontend-nextjs/src/components/ImpactChart/index.tsx b/frontend-nextjs/src/components/ImpactChart/index.tsx index f62c6ea..bd747e1 100644 --- a/frontend-nextjs/src/components/ImpactChart/index.tsx +++ b/frontend-nextjs/src/components/ImpactChart/index.tsx @@ -1,36 +1,36 @@ -"use client"; -import { useFiltersStore } from "@/providers/FiltersStoreProvider"; -import { cn } from "@/utility/classNames"; -import { parseErrorMessage } from "@/utility/errorHandlingUtil"; -import type { EventOrganizerSlugType } from "@/utility/eventsUtil"; -import type { TrendQueryProps } from "@/utility/mediaTrendUtil"; -import useEvents from "@/utility/useEvents"; -import useMediaImpactData from "@/utility/useMediaImpact"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import type { icons } from "lucide-react"; -import { ErrorBoundary } from "next/dist/client/components/error-boundary"; -import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; -import ChartLoadingPlaceholder from "../ChartLoadingPlaceholder"; -import ComponentError from "../ComponentError"; -import ImpactChart from "./ImpactChart"; +'use client' +import { useFiltersStore } from '@/providers/FiltersStoreProvider' +import { cn } from '@/utility/classNames' +import { parseErrorMessage } from '@/utility/errorHandlingUtil' +import type { EventOrganizerSlugType } from '@/utility/eventsUtil' +import type { TrendQueryProps } from '@/utility/mediaTrendUtil' +import useMediaImpactData from '@/utility/useMediaImpact' +import { useAllOrganisations } from '@/utility/useOrganisations' +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import type { icons } from 'lucide-react' +import { ErrorBoundary } from 'next/dist/client/components/error-boundary' +import { Suspense, useCallback, useEffect, useMemo, useState } from 'react' +import ChartLoadingPlaceholder from '../ChartLoadingPlaceholder' +import ComponentError from '../ComponentError' +import ImpactChart from './ImpactChart' type ImpactChartWithDataProps = { - trend_type: TrendQueryProps["trend_type"]; - sentiment_target: TrendQueryProps["sentiment_target"]; - reset?: () => void; - unitLabel?: string; - icon?: keyof typeof icons; -}; + trend_type: TrendQueryProps['trend_type'] + sentiment_target: TrendQueryProps['sentiment_target'] + reset?: () => void + unitLabel?: string + icon?: keyof typeof icons +} -type ImpactChartErrorProps = Pick & - ReturnType; +type ImpactChartErrorProps = Pick & + ReturnType function ImpactChartError(props: ImpactChartErrorProps) { return (
- ); + ) } function ImpactChartLoading() { - return ; + return } function ImpactChartEmpty() { return (
No data for the current configuration
- ); + ) } function ImpactChartWithData({ reset, - unitLabel = "articles", - trend_type = "keywords", + unitLabel = 'articles', + trend_type = 'keywords', sentiment_target = null, }: ImpactChartWithDataProps) { - const { - data: { organisations }, - } = useEvents(); - const selectedOrgs = useFiltersStore((state) => state.organizers.sort()); + const { organisations } = useAllOrganisations() + const selectedOrgs = useFiltersStore((state) => state.organizers.sort()) const orgs = useMemo( () => selectedOrgs.length === 0 ? organisations.map(({ slug }) => slug) : selectedOrgs, [selectedOrgs, organisations], - ); + ) - const [org1, setOrg1] = useState(orgs[0]); - const [org2, setOrg2] = useState(orgs[1]); - const [org3, setOrg3] = useState(orgs[2]); + const [org1, setOrg1] = useState(orgs[0]) + const [org2, setOrg2] = useState(orgs[1]) + const [org3, setOrg3] = useState(orgs[2]) useEffect(() => { - setOrg1(orgs[0]); - setOrg2(orgs[1]); - setOrg3(orgs[2]); - }, [orgs]); + setOrg1(orgs[0]) + setOrg2(orgs[1]) + setOrg3(orgs[2]) + }, [orgs]) const getUpdateOrgHandler = useCallback( (idx: number) => (slug: EventOrganizerSlugType) => { - const orgChangeHandlers = [setOrg1, setOrg2, setOrg3]; - if (orgs.length === 0 || !slug) return; - const handler = orgChangeHandlers[idx] ?? (() => undefined); - handler(slug); + const orgChangeHandlers = [setOrg1, setOrg2, setOrg3] + if (orgs.length === 0 || !slug) return + const handler = orgChangeHandlers[idx] ?? (() => undefined) + handler(slug) }, [orgs], - ); + ) - const orgValues = [org1, org2, org3]; + const orgValues = [org1, org2, org3] const org1Query = useMediaImpactData({ organizer: org1, trend_type, sentiment_target, - }); + }) const org2Query = useMediaImpactData({ organizer: org2, trend_type, sentiment_target, - }); + }) const org3Query = useMediaImpactData({ organizer: org3, trend_type, sentiment_target, - }); + }) - const queries = {} as Record>; - if (org1) queries[org1] = org1Query; - if (org2) queries[org2] = org1Query; - if (org3) queries[org3] = org1Query; + const queries = {} as Record> + if (org1) queries[org1] = org1Query + if (org2) queries[org2] = org1Query + if (org3) queries[org3] = org1Query - const queriesArray = Array.from(Object.values(queries)); + const queriesArray = Array.from(Object.values(queries)) const data = {} as Record< string, - ReturnType["data"] - >; - if (org1) data[org1] = org1Query.data; - if (org2) data[org2] = org2Query.data; - if (org3) data[org3] = org3Query.data; + ReturnType['data'] + > + if (org1) data[org1] = org1Query.data + if (org2) data[org2] = org2Query.data + if (org3) data[org3] = org3Query.data const anyLoading = - queriesArray.length === 0 || queriesArray.some((query) => query.isPending); + queriesArray.length === 0 || queriesArray.some((query) => query.isPending) const isEmpty = !anyLoading && queriesArray.every( (query) => query?.data?.data && query.data.data.length === 0, - ); + ) const allErroring = - !anyLoading && queriesArray.every((query) => query.isError === true); + !anyLoading && queriesArray.every((query) => query.isError === true) if (allErroring) { - const firstError = queriesArray.find((query) => query.error)?.error; - return ( - - ); + const firstError = queriesArray.find((query) => query.error)?.error + return } - if (isEmpty) return ; + if (isEmpty) return return ( - ); + ) } export default function ImpactChartWithErrorBoundary({ - trend_type = "keywords", + trend_type = 'keywords', sentiment_target = null, -}: Pick) { +}: Pick) { return ( {({ reset }) => ( @@ -185,5 +181,5 @@ export default function ImpactChartWithErrorBoundary({ )} - ); + ) } diff --git a/frontend-nextjs/src/components/OrganisationPageHeader.tsx b/frontend-nextjs/src/components/OrganisationPageHeader.tsx index 68d1598..9e37bec 100644 --- a/frontend-nextjs/src/components/OrganisationPageHeader.tsx +++ b/frontend-nextjs/src/components/OrganisationPageHeader.tsx @@ -1,18 +1,23 @@ -"use client"; -import { cn } from "@/utility/classNames"; -import { parseErrorMessage } from "@/utility/errorHandlingUtil"; -import type { EventOrganizerSlugType } from "@/utility/eventsUtil"; -import { getOrgStats } from "@/utility/orgsUtil"; -import { texts } from "@/utility/textUtil"; -import useEvents from "@/utility/useEvents"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import { ArrowLeft } from "lucide-react"; -import { ErrorBoundary } from "next/dist/client/components/error-boundary"; -import Image from "next/image"; -import { Suspense, memo, useMemo } from "react"; -import placeholderImage from "../assets/images/placeholder-image.avif"; -import ComponentError from "./ComponentError"; -import InternalLink from "./InternalLink"; +'use client' +import { cn } from '@/utility/classNames' +import { parseErrorMessage } from '@/utility/errorHandlingUtil' +import type { EventOrganizerSlugType } from '@/utility/eventsUtil' +import { getOrgStats } from '@/utility/orgsUtil' +import { texts } from '@/utility/textUtil' +import { useTimeFilteredEvents } from '@/utility/useEvents' +import { + useAllOrganisations, + useOrganisation, +} from '@/utility/useOrganisations' +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { ArrowLeft } from 'lucide-react' +import { ErrorBoundary } from 'next/dist/client/components/error-boundary' +import Image from 'next/image' +import { Suspense, memo, useMemo } from 'react' +import placeholderImage from '../assets/images/placeholder-image.avif' +import ComponentError from './ComponentError' +import InternalLink from './InternalLink' +import OrgsTooltip from './OrgsTooltip' const PlaceholderSkeleton = memo( ({ width, height }: { width: number | string; height?: number | string }) => ( @@ -21,46 +26,48 @@ const PlaceholderSkeleton = memo( style={{ width, height }} /> ), -); +) -const OrganisationPageWithPopulatedData = memo( - ({ - slug, - data, - }: { - data?: ReturnType["data"]; - slug: EventOrganizerSlugType; - }) => { - const org = data?.organisations.find((x) => x.slug === slug); +function formatNumber(num: number) { + if (Number.isNaN(num)) return '?' + return Number.parseFloat(num.toFixed(2)).toLocaleString(texts.language) +} + +const OrganisationPageHeader = memo( + ({ slug }: { slug?: EventOrganizerSlugType }) => { + const { organisation } = useOrganisation(slug) + const { organisations } = useAllOrganisations() + const { timeFilteredEvents } = useTimeFilteredEvents() const title = useMemo( () => ( <>