From af4a31bf897f94f8ef1f1534982bafded9cf3b17 Mon Sep 17 00:00:00 2001 From: David Pomerenke <46022183+davidpomerenke@users.noreply.github.com> Date: Mon, 26 Aug 2024 23:18:28 +0200 Subject: [PATCH 01/28] chore: rephrase chart title, remove government events --- .../media_impact_monitor/data_loaders/protest/acled/acled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-python/media_impact_monitor/data_loaders/protest/acled/acled.py b/backend-python/media_impact_monitor/data_loaders/protest/acled/acled.py index fbfeb30..c87c657 100644 --- a/backend-python/media_impact_monitor/data_loaders/protest/acled/acled.py +++ b/backend-python/media_impact_monitor/data_loaders/protest/acled/acled.py @@ -111,7 +111,7 @@ def process_orgs(df: pd.DataFrame) -> pd.DataFrame: "Labor Group", "Women", "Christian Group", - "Government of Germany (2021-)", + "Government of Germany", "Civilians", "Protesters", ] From ff56d8d0122c67dacf9c954fa319a3bf007cb229 Mon Sep 17 00:00:00 2001 From: David Pomerenke <46022183+davidpomerenke@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:39:10 +0200 Subject: [PATCH 02/28] chore(notebooks): analyze how many articles we have per event (without proper filtering) --- backend-python/media_impact_monitor/types_.py | 2 +- .../2024-08-29-david-event-coverage.ipynb | 114 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 notebooks/2024-08-29-david-event-coverage.ipynb diff --git a/backend-python/media_impact_monitor/types_.py b/backend-python/media_impact_monitor/types_.py index 3e608cc..353d762 100644 --- a/backend-python/media_impact_monitor/types_.py +++ b/backend-python/media_impact_monitor/types_.py @@ -160,7 +160,7 @@ class PolicySearch(BaseModel): class FulltextSearch(BaseModel): """ - You can set parameters for media_source and date_range, and filter by one of the following: topic, organizers, query, or event_id. For now you cannot combine the latter filters, since they all affect the query in different ways. + You can set parameters for media_source, and filter by one of the following: topic, organizers, query, or event_id. For now you cannot combine the latter filters, since they all affect the query in different ways. """ media_source: MediaSource = Field( diff --git a/notebooks/2024-08-29-david-event-coverage.ipynb b/notebooks/2024-08-29-david-event-coverage.ipynb new file mode 100644 index 0000000..bb7fff5 --- /dev/null +++ b/notebooks/2024-08-29-david-event-coverage.ipynb @@ -0,0 +1,114 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from media_impact_monitor.events import get_events, EventSearch\n", + "from media_impact_monitor.fulltexts import get_fulltexts, FulltextSearch\n", + "from datetime import date\n", + "from tqdm.auto import tqdm\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "events = get_events(\n", + " EventSearch(source=\"acled\", start_date=date(2024, 4, 1), end_date=date(2024, 6, 30))\n", + ")\n", + "events = events.sample(200, random_state=0)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b5647fc6b3094603bfb09bc6c088b0f4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/200 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "mean_articles = {\n", + " org: events[events.organizers.apply(lambda x: org in x)].fulltexts.mean()\n", + " for org in events[\"organizers\"].explode().unique()\n", + "}\n", + "mean_articles = pd.Series(mean_articles)\n", + "mean_articles = mean_articles.sort_values(ascending=True)\n", + "mean_articles.plot(kind=\"barh\", figsize=(10, 10))\n", + "plt.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From bb90d4e08434341d2358cc608f6bd48aae443b8d Mon Sep 17 00:00:00 2001 From: David Pomerenke <46022183+davidpomerenke@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:55:53 +0200 Subject: [PATCH 03/28] fix(mediacloud_.py): use MIM collection if available and turn assertion into a warning --- .../data_loaders/news_online/mediacloud_.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend-python/media_impact_monitor/data_loaders/news_online/mediacloud_.py b/backend-python/media_impact_monitor/data_loaders/news_online/mediacloud_.py index 1360415..7f85ebe 100644 --- a/backend-python/media_impact_monitor/data_loaders/news_online/mediacloud_.py +++ b/backend-python/media_impact_monitor/data_loaders/news_online/mediacloud_.py @@ -223,7 +223,11 @@ def _resolve_country(country: str) -> list[int]: results = directory.collection_list(name=f"{country} - national")["results"] # ignore research collections results = [r for r in results if "(Research Only)" not in r["name"]] - assert len(results) == 1, f"Expected 1 result, got {len(results)} for {country}" + # if there is a specific collection for MIM, use it! + if any("Media Impact Monitor" in r["name"] for r in results): + results = [r for r in results if "Media Impact Monitor" in r["name"]] + if len(results) != 1: + print(f"Expected 1 result, got {len(results)} for {country}") national = results[0]["id"] # get regional newspapers results = directory.collection_list(name=f"{country} - state & local")["results"] From 0230f87e1e76ad98d20bf0b96d67ad95ba40f150 Mon Sep 17 00:00:00 2001 From: David Pomerenke <46022183+davidpomerenke@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:59:28 +0200 Subject: [PATCH 04/28] chore(dashboard_texts_en.json): rephrase --- frontend-nextjs/src/content-drafts/dashboard_texts_en.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend-nextjs/src/content-drafts/dashboard_texts_en.json b/frontend-nextjs/src/content-drafts/dashboard_texts_en.json index ab8a14c..5880ffb 100644 --- a/frontend-nextjs/src/content-drafts/dashboard_texts_en.json +++ b/frontend-nextjs/src/content-drafts/dashboard_texts_en.json @@ -25,7 +25,7 @@ ] }, "topics_trend": { - "heading": "What topics are the focus of public discourse?", + "heading": "How prominent are the issues in public discourse?", "description": [ "See how many articles are published on various topics over time.", "Use the filters to switch between online newspaper articles, print newspaper articles, and queries that people search for on Google." @@ -38,7 +38,6 @@ } ] }, - "topics_impact": { "heading": "Computed impacts", "description": [ @@ -83,4 +82,4 @@ "data_credit": [] } } -} +} \ No newline at end of file From 7f259e9c627d0bfc972f1779981aebc11135b94f Mon Sep 17 00:00:00 2001 From: Lucas Vogel Date: Sat, 31 Aug 2024 18:40:18 -0400 Subject: [PATCH 05/28] refactor(DraggableTimeFilterRange): Use different lib that has touch support --- frontend-nextjs/.vscode/settings.json | 5 +- frontend-nextjs/package-lock.json | 56 +-- frontend-nextjs/package.json | 2 +- .../components/DraggableTimeFilterRange.tsx | 469 +++++------------- .../src/react-range-slider-input.d.ts | 103 ++++ frontend-nextjs/src/styles/global.css | 95 ++-- 6 files changed, 317 insertions(+), 413 deletions(-) create mode 100644 frontend-nextjs/src/react-range-slider-input.d.ts diff --git a/frontend-nextjs/.vscode/settings.json b/frontend-nextjs/.vscode/settings.json index 93f1903..9cf4253 100644 --- a/frontend-nextjs/.vscode/settings.json +++ b/frontend-nextjs/.vscode/settings.json @@ -3,6 +3,9 @@ "editor.defaultFormatter": "biomejs.biome", "biome.lspBin": "./frontend-nextjs/node_modules/@biomejs/biome/bin/biome", "[typescriptreact]": { - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/frontend-nextjs/package-lock.json b/frontend-nextjs/package-lock.json index 66decc7..cf887e9 100644 --- a/frontend-nextjs/package-lock.json +++ b/frontend-nextjs/package-lock.json @@ -24,7 +24,6 @@ "@sentry/nextjs": "8.16.0", "@tanstack/react-query": "5.29.2", "@tanstack/react-query-next-experimental": "5.29.2", - "@tanstack/react-ranger": "0.0.4", "@tanstack/react-table": "8.12.0", "@types/bun": "1.1.1", "class-variance-authority": "0.7.0", @@ -44,6 +43,7 @@ "react": "18.3.1", "react-day-picker": "8.10.0", "react-dom": "18.3.1", + "react-range-slider-input": "3.0.7", "recharts": "2.13.0-alpha.4", "rehype-autolink-headings": "7.1.0", "rehype-slug": "6.0.0", @@ -4483,18 +4483,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tanstack/ranger": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@tanstack/ranger/-/ranger-0.0.3.tgz", - "integrity": "sha512-RYpW7MnEMxKLuskfadeyBfxpU8cUTGrtp5TnCV9PKEgMfrR6vwlk9/1/eU2EvJfYZbDAr1inphL4TGWCcRLAXQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@tanstack/react-query": { "version": "5.29.2", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.29.2.tgz", @@ -4541,21 +4529,6 @@ "react": "^18.0.0" } }, - "node_modules/@tanstack/react-ranger": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-ranger/-/react-ranger-0.0.4.tgz", - "integrity": "sha512-3re8xKJ6t3j8eKp6pdKi92FQegsxHipTgsvg7k0DP+SRWt/iJ2F8GSm8DuQVLHiE3R7kX1Q42fQhmku1PdcWOA==", - "dependencies": { - "@tanstack/ranger": "0.0.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@tanstack/react-table": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.12.0.tgz", @@ -6179,6 +6152,16 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, + "node_modules/core-js": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", + "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -11431,6 +11414,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-range-slider-input": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/react-range-slider-input/-/react-range-slider-input-3.0.7.tgz", + "integrity": "sha512-yAJDDMUNkILOcZSCLCVbwgnoAM3v0AfdDysTCMXDwY/+ZRNRlv98TyHbVCwPFEd7qiI8Ca/stKb0GAy//NybYw==", + "dependencies": { + "clsx": "^1.1.1", + "core-js": "^3.22.4" + } + }, + "node_modules/react-range-slider-input/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", diff --git a/frontend-nextjs/package.json b/frontend-nextjs/package.json index 8521c07..42a63d6 100644 --- a/frontend-nextjs/package.json +++ b/frontend-nextjs/package.json @@ -25,7 +25,6 @@ "@sentry/nextjs": "8.16.0", "@tanstack/react-query": "5.29.2", "@tanstack/react-query-next-experimental": "5.29.2", - "@tanstack/react-ranger": "0.0.4", "@tanstack/react-table": "8.12.0", "@types/bun": "1.1.1", "class-variance-authority": "0.7.0", @@ -45,6 +44,7 @@ "react": "18.3.1", "react-day-picker": "8.10.0", "react-dom": "18.3.1", + "react-range-slider-input": "3.0.7", "recharts": "2.13.0-alpha.4", "rehype-autolink-headings": "7.1.0", "rehype-slug": "6.0.0", diff --git a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx index b9c4070..f147082 100644 --- a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx +++ b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx @@ -1,388 +1,189 @@ -import { useFiltersStore } from "@/providers/FiltersStoreProvider"; -import { useToday } from "@/providers/TodayProvider"; -import { cn } from "@/utility/classNames"; -import { - type ComparableDateItemType, - dateToComparableDateItem, -} from "@/utility/comparableDateItemSchema"; -import { format } from "@/utility/dateUtil"; -import useEvents 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"; -import { type Ranger, useRanger } from "@tanstack/react-ranger"; -import { addDays, compareAsc, differenceInDays } from "date-fns"; -import { - type KeyboardEvent as ReactKeyboardEvent, - type MouseEvent as ReactMouseEvent, - type TouchEvent as ReactTouchEvent, - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import useTimelineEvents from "./EventsTimeline/useTimelineEvents"; - -type BtnMouseEvent = ReactMouseEvent; -type BtnTouchEvent = ReactTouchEvent; -type BtnEvent = BtnMouseEvent | BtnTouchEvent; +import { useFiltersStore } from '@/providers/FiltersStoreProvider' +import { useToday } from '@/providers/TodayProvider' +import { cn } from '@/utility/classNames' +import { dateToComparableDateItem } from '@/utility/comparableDateItemSchema' +import { format } from '@/utility/dateUtil' +import useEvents 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' +import { addDays, compareAsc, differenceInDays } from 'date-fns' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import RangeSlider from 'react-range-slider-input' +import 'react-range-slider-input/dist/style.css' +import useTimelineEvents from './EventsTimeline/useTimelineEvents' function DraggableTimeFilterRange() { - const rangerRef = useRef(null); - const midSegmentRef = useRef(null); const { from, to, setDateRange } = useFiltersStore((state) => ({ from: dateToComparableDateItem(state.from), to: dateToComparableDateItem(state.to), setDateRange: state.setDateRange, - })); - const { today, datasetStartDate, datasetEndDate } = useToday(); - const amountOfDays = differenceInDays(datasetEndDate, datasetStartDate) + 1; + })) + const { today, datasetStartDate, datasetEndDate } = useToday() + const amountOfDays = differenceInDays(datasetEndDate, datasetStartDate) + 1 const intervals = new Array(Math.abs(amountOfDays)) .fill(null) .map((_, i) => dateToComparableDateItem(addDays(datasetStartDate, i), today), - ); + ) - const rangerSteps = intervals.map((_, i) => i); - const rangerTicks = intervals.map((d) => d.time); const indexOfFrom = useMemo( - () => - intervals.findIndex((d) => - isInSameAggregationUnit("day", d, from), - ), + () => intervals.findIndex((d) => isInSameAggregationUnit('day', d, from)), [from, intervals], - ); + ) const indexOfTo = useMemo( - () => - intervals.findIndex((d) => - isInSameAggregationUnit("day", d, to), - ), + () => intervals.findIndex((d) => isInSameAggregationUnit('day', d, to)), [to, intervals], - ); - const [values, setValues] = useState>([ - indexOfFrom, - indexOfTo, - ]); - const [tempValues, setTempValues] = useState< - ReadonlyArray | undefined - >([indexOfFrom, indexOfTo]); - const [isDragging, setIsDragging] = useState(false); - const [setDebouncedIsDragging] = useDebounce(setIsDragging, 500, { - leading: false, - trailing: true, - }); - // biome-ignore lint/correctness/useExhaustiveDependencies: - const startDragging = useCallback(() => { - setIsDragging(true); - setDebouncedIsDragging(false); - }, []); + ) + const initialValues = [indexOfFrom, indexOfTo] as [number, number] + const [values, setValues] = useState<[number, number]>(initialValues) + const [isDragging, setIsDragging] = useState(false) + + const leftDate = useMemo(() => { + const d = intervals[values[0]].date + if (!d) return undefined + return format(d, 'dd. MMM. yyyy') + }, [intervals, values]) + const rightDate = useMemo(() => { + const d = intervals[values[1]].date + if (!d) return undefined + return format(d, 'dd. MMM. yyyy') + }, [intervals, values]) useEffect(() => { - setValues([indexOfFrom, indexOfTo]); - setTempValues([indexOfFrom, indexOfTo]); - }, [indexOfFrom, indexOfTo]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: - const onValuesChange = useCallback((vals: [number, number]) => { - const [from, to] = [ - intervals[vals[0]] ?? intervals[0], - intervals[vals[1]] ?? intervals[intervals.length - 1], - ] - .map((d) => new Date(d.time)) - .sort(compareAsc); - - setTempValues(vals); - setDateRange({ from, to }); - }, []); + setValues([indexOfFrom, indexOfTo]) + }, [indexOfFrom, indexOfTo]) const onChange = useCallback( - (instance: Ranger) => { - const [fromIdx, toIdx] = instance.sortedValues; - onValuesChange([ - Math.max(0, fromIdx), - Math.min(intervals.length - 1, toIdx), - ]); + ([fromIdx, toIdx]: [number, number]) => { + const [from, to] = [ + intervals[fromIdx] ?? intervals[0], + intervals[toIdx] ?? intervals[intervals.length - 1], + ] + .map((d) => d.date) + .sort(compareAsc) + + setDateRange({ from, to }) + setIsDragging(false) }, - [onValuesChange, intervals], - ); - - // biome-ignore lint/correctness/useExhaustiveDependencies: - const onDrag = useCallback((instance: Ranger) => { - const [fromIdx, toIdx] = instance.sortedValues; - setTempValues([fromIdx, toIdx]); - startDragging(); - }, []); - - const rangerInstance = useRanger({ - getRangerElement: () => rangerRef.current, - values: tempValues || values, - min: 0, - max: intervals.length - 1, - stepSize: 1, - steps: rangerSteps, - ticks: rangerTicks, - onChange, - onDrag, - }); + [intervals, setDateRange], + ) + const [debouncedOnChange] = useDebounce(onChange, 500) - const handleSegmentDrag = useCallback( - ( - e: MouseEvent | TouchEvent, - initialX: number, - values: ReadonlyArray, - ) => { - let clientX = e instanceof MouseEvent ? e.clientX : 0; - if (window.TouchEvent && e instanceof window.TouchEvent) { - clientX = e.changedTouches[0].clientX; - } - const initial = rangerInstance.getValueForClientX(initialX); - const newValue = rangerInstance.getValueForClientX(clientX); - const diff = newValue - initial; - if (diff) { - let actualDiff = 0; - if (diff > 0) { - const last = values[values.length - 1]; - const newRoundedLastValue = rangerInstance.roundToStep(last + diff); + return ( + <> +
- {steps.slice(0, 3).map(({ left, width }, i) => ( -
- - ); -} - -const Handle = memo( - ({ - value, - comparableDateObject, - onKeyDownHandler, - onMouseDownHandler, - onTouchStart, - isActive, - rangerInstance, - isDragging = false, - isStart = true, - }: { - value: number; - comparableDateObject: ComparableDateItemType; - onKeyDownHandler: (e: ReactKeyboardEvent) => void; - onMouseDownHandler: (e: ReactMouseEvent) => void; - onTouchStart: (e: ReactTouchEvent) => void; - isActive: boolean; - rangerInstance: Ranger; - isDragging?: boolean; - isStart?: boolean; - }) => { - const { date } = comparableDateObject; - const formattedDate = useMemo(() => format(date, "dd. MMM. yyyy"), [date]); - - const handleKeyDown = useCallback( - (e: ReactKeyboardEvent) => { - if (e.key === "Tab") return; - e.preventDefault(); - onKeyDownHandler(e); - }, - [onKeyDownHandler], - ); - - const handleMouseDown = useCallback( - (e: ReactMouseEvent) => { - e.preventDefault(); - onMouseDownHandler(e); - }, - [onMouseDownHandler], - ); - - const handleTouchStart = useCallback( - (e: ReactTouchEvent) => { - e.preventDefault(); - onTouchStart(e); - }, - [onTouchStart], - ); - - const left = useMemo( - () => `${rangerInstance.getPercentageForValue(value)}%`, - [rangerInstance, value], - ); - if (!comparableDateObject) return null; - return ( - - ); - }, -); + + + + ) +} const HandleTooptip = memo( ({ formattedDate, isStart, isDragging, - }: { formattedDate: string; isStart: boolean; isDragging: boolean }) => ( + }: { + formattedDate?: string + isStart: boolean + isDragging: boolean + }) => ( - - {formattedDate.split(" ").map((part, i) => ( - {part} - ))} - + {formattedDate + ?.split(' ') + .map((part, i) => {part})} ), -); +) const BackgroundVis = memo(() => { - const [parentRef, size] = useElementSize(); - const { datasetStartDate, datasetEndDate } = useToday(); + const [parentRef, size] = useElementSize() + const { datasetStartDate, datasetEndDate } = useToday() const { data } = useEvents({ from: datasetStartDate, to: datasetEndDate, - }); + }) const { eventColumns, columnsCount, sizeScale } = useTimelineEvents({ size, data, - aggregationUnit: "week", + aggregationUnit: 'week', config: { eventMinHeight: 2, eventMaxHeight: Math.floor(size.height * 0.9), @@ -390,7 +191,7 @@ const BackgroundVis = memo(() => { }, from: datasetStartDate, to: datasetEndDate, - }); + }) return ( { ))} - ); -}); + ) +}) -export default DraggableTimeFilterRange; +export default DraggableTimeFilterRange diff --git a/frontend-nextjs/src/react-range-slider-input.d.ts b/frontend-nextjs/src/react-range-slider-input.d.ts new file mode 100644 index 0000000..487f21e --- /dev/null +++ b/frontend-nextjs/src/react-range-slider-input.d.ts @@ -0,0 +1,103 @@ +declare module "react-range-slider-input" { + import type { FC } from "react" + + export type Orientation = "horizontal" | "vertical" + export type Step = number | "any" + + export type InputEvent = [number, number] + export type InputEventHandler = (event: InputEvent) => void + + export interface ReactRangeSliderInputProps { + /* @default null + * Identifier string (id attribute value) to be passed to the range slider element. + * */ + id?: string + + /* @default null + * String of classes to be passed to the range slider element. + * */ + className?: string + + /* @default 0 + * Number that specifies the lowest value in the range of permitted values. + * Its value must be less than that of max. + * */ + min?: number + + /* @default 100 + * Number that specifies the greatest value in the range of permitted values. + * Its value must be greater than that of min. + * */ + max?: number + + /* @default 1 + * Number that specifies the amount by which the slider value(s) will change upon user interaction. + * Other than numbers, the value of step can be a string value of any. + * */ + step?: number | "any" + + /* @default [25, 75] + * Array of two numbers that specify the default values of the lower and upper offsets of the range slider element respectively. + * If set, the range slider will be rendered as an uncontrolled element. To render it as a controlled element, set the value property. + * */ + defaultValue?: [number, number] + + /* @default [] + * Array of two numbers that specify the values of the lower and upper offsets of the range slider element respectively. + * If set, the range slider will be rendered as a controlled element. + * */ + value?: [number, number] + + /* + * Function to be called when there is a change in the value(s) of range sliders upon user interaction. + * */ + onInput?: InputEventHandler + + /* + * Function to be called when the pointerdown event is triggered for any of the thumbs. + * */ + onThumbDragStart?: () => void + + /* + * Function to be called when the pointerup event is triggered for any of the thumbs. + * */ + onThumbDragEnd?: () => void + + /* + * Function to be called when the pointerdown event is triggered for the range. + * */ + onRangeDragStart?: () => void + + /* + * Function to be called when the pointerup event is triggered for the range. + * */ + onRangeDragEnd?: () => void + + /* @default false + * Boolean that specifies if the range slider element is disabled or not. + * */ + disabled?: boolean + + /* @default false + * Boolean that specifies if the range is slidable or not. + * */ + rangeSlideDisabled?: boolean + + /* @default [false, false] + * Array of two Booleans which specify if the lower and upper thumbs are disabled or not, respectively. + * If only one Boolean value is passed instead of an array, the value will apply to both thumbs. + * */ + thumbsDisabled?: [boolean, boolean] + + /* @default 'horizontal' + * String that specifies the axis along which the user interaction is to be registered. + * By default, the range slider element registers the user interaction along the X-axis. + * It takes two different values: horizontal and vertical. + * */ + orientation?: Orientation + } + + const ReactRangeSliderInput: FC + + export default ReactRangeSliderInput +} diff --git a/frontend-nextjs/src/styles/global.css b/frontend-nextjs/src/styles/global.css index 6c73173..ba35f9b 100644 --- a/frontend-nextjs/src/styles/global.css +++ b/frontend-nextjs/src/styles/global.css @@ -25,11 +25,11 @@ :root, :root[data-theme='light'] { --pageMaxWidth: 1920px; - --pageFullWidth: min(100vw,var(--pageMaxWidth)); - --pagePadding: clamp(1.5rem,2vmax,4rem); - --headerHeight: 75px; - --filtersHeight: 141px; - --footerHeight: 251px; + --pageFullWidth: min(100vw, var(--pageMaxWidth)); + --pagePadding: clamp(1.5rem, 2vmax, 4rem); + --headerHeight: 75px; + --filtersHeight: 141px; + --footerHeight: 251px; --fg: var(--brandGreen); --bg: var(--brandWhite); @@ -40,29 +40,29 @@ --grayLight: #e1ecf2; --grayUltraLight: #f1faff; - --categorical-color-1: #F46A9B; - --categorical-color-2: #7EB0D5; - --categorical-color-3: #FFB55A; - --categorical-color-4: #8BD3C7; - --categorical-color-5: #FD7F6F; - --categorical-color-6: #B2E061; - --categorical-color-7: #BD7EBE; - --categorical-color-8: #FFEE65; - --categorical-color-9: #BEB9DB; - --categorical-color-10: #FDCCE5; - --categorical-color-11: #EF9B20; - --categorical-color-12: #EA5545; + --categorical-color-1: #f46a9b; + --categorical-color-2: #7eb0d5; + --categorical-color-3: #ffb55a; + --categorical-color-4: #8bd3c7; + --categorical-color-5: #fd7f6f; + --categorical-color-6: #b2e061; + --categorical-color-7: #bd7ebe; + --categorical-color-8: #ffee65; + --categorical-color-9: #beb9db; + --categorical-color-10: #fdcce5; + --categorical-color-11: #ef9b20; + --categorical-color-12: #ea5545; --categorical-color-13: #327483; - --categorical-color-14: #7EA73F; + --categorical-color-14: #7ea73f; - --keyword-climate-activism: #D53D4F; - --keyword-climate-crisis-framing:#FDAE61; - --keyword-climate-policy: #66C2A5; - --keyword-climate-science: #3288BD; + --keyword-climate-activism: #d53d4f; + --keyword-climate-crisis-framing: #fdae61; + --keyword-climate-policy: #66c2a5; + --keyword-climate-science: #3288bd; - --sentiment-positive: #229E74; - --sentiment-negative: #D55E00; - --sentiment-neutral: #E69F00; + --sentiment-positive: #229e74; + --sentiment-negative: #d55e00; + --sentiment-neutral: #e69f00; --protest-timeline-height: max(30vh, 20rem); --media-coverage-chart-height: max(30vh, 20rem); @@ -155,32 +155,30 @@ --input-color: var(--grayDark); --button-primary-border: 1px solid var(--fg); } - - :root[data-theme='dark'] #user-feedback-button { + + :root[data-theme='dark'] #user-feedback-button { --background: var(--bg) url(/images/noisy-inverted.webp) repeat center; } - @layer base { - input[type='text'], - input[type='password'], - input[type='email'], - input[type='number'], - input[type='url'], - input[type='date'], - input[type='datetime-local'], - input[type='month'], - input[type='week'], - input[type='time'], - input[type='search'], - input[type='tel'], - input[type='checkbox'], - select, - select[multiple], - textarea { - @apply border-grayMed focus-visible:outline-none focus-visible:ring-2; - @apply focus-visible:ring-offset-2 focus-visible:ring-fg focus-visible:ring-offset-bg; - @apply focus-visible:border-grayMed bg-bg; - } + input[type='text'], + input[type='password'], + input[type='email'], + input[type='number'], + input[type='url'], + input[type='date'], + input[type='datetime-local'], + input[type='month'], + input[type='week'], + input[type='time'], + input[type='search'], + input[type='tel'], + input[type='checkbox'], + select, + select[multiple], + textarea { + @apply border-grayMed focus-visible:outline-none focus-visible:ring-2; + @apply focus-visible:ring-offset-2 focus-visible:ring-fg focus-visible:ring-offset-bg; + @apply focus-visible:border-grayMed bg-bg; } input::-webkit-outer-spin-button, @@ -285,7 +283,6 @@ @apply bg-brandWhite dark:bg-[url(/images/noisy-light.webp)] bg-repeat bg-center bg-blend-multiply; } - .max-w-page { @apply max-w-[var(--pageMaxWidth)]; } From ee4fbba7a4d5f715295b9d3fcf780c4f66f96306 Mon Sep 17 00:00:00 2001 From: Lucas Vogel Date: Sat, 31 Aug 2024 20:40:19 -0400 Subject: [PATCH 06/28] refactor(DraggableTimeFilterRange): Add css as file an import it --- .../components/DraggableTimeFilterRange.tsx | 34 +------- .../styles/draggable-time-filter-range.css | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 frontend-nextjs/src/styles/draggable-time-filter-range.css diff --git a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx index f147082..c8ae0fe 100644 --- a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx +++ b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx @@ -1,5 +1,6 @@ import { useFiltersStore } from '@/providers/FiltersStoreProvider' import { useToday } from '@/providers/TodayProvider' +import '@/styles/draggable-time-filter-range.css' import { cn } from '@/utility/classNames' import { dateToComparableDateItem } from '@/utility/comparableDateItemSchema' import { format } from '@/utility/dateUtil' @@ -10,7 +11,6 @@ import useElementSize from '@custom-react-hooks/use-element-size' import { addDays, compareAsc, differenceInDays } from 'date-fns' import { memo, useCallback, useEffect, useMemo, useState } from 'react' import RangeSlider from 'react-range-slider-input' -import 'react-range-slider-input/dist/style.css' import useTimelineEvents from './EventsTimeline/useTimelineEvents' function DraggableTimeFilterRange() { @@ -72,38 +72,6 @@ function DraggableTimeFilterRange() { return ( <> -
Date: Sat, 31 Aug 2024 20:54:01 -0400 Subject: [PATCH 07/28] refactor(DraggableTimeFilterRange): Disable when querying events --- frontend-nextjs/src/components/DraggableTimeFilterRange.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx index c8ae0fe..fea84f1 100644 --- a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx +++ b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx @@ -14,6 +14,7 @@ import RangeSlider from 'react-range-slider-input' import useTimelineEvents from './EventsTimeline/useTimelineEvents' function DraggableTimeFilterRange() { + const { isLoading } = useEvents() const { from, to, setDateRange } = useFiltersStore((state) => ({ from: dateToComparableDateItem(state.from), to: dateToComparableDateItem(state.to), @@ -56,6 +57,7 @@ function DraggableTimeFilterRange() { const onChange = useCallback( ([fromIdx, toIdx]: [number, number]) => { + setIsDragging(false) const [from, to] = [ intervals[fromIdx] ?? intervals[0], intervals[toIdx] ?? intervals[intervals.length - 1], @@ -64,7 +66,6 @@ function DraggableTimeFilterRange() { .sort(compareAsc) setDateRange({ from, to }) - setIsDragging(false) }, [intervals, setDateRange], ) @@ -94,6 +95,7 @@ function DraggableTimeFilterRange() { onThumbDragStart={() => setIsDragging(true)} id="draggable-time-filter-range" className="group" + disabled={isLoading} /> Date: Sat, 31 Aug 2024 22:41:18 -0400 Subject: [PATCH 08/28] refactor(Charts): Improve performance and disable animation --- .../components/DraggableTimeFilterRange.tsx | 5 +- .../EventsTimeline/EventsTimelineLegend.tsx | 126 +++++++++--------- .../FullTextsSentimentChart/index.tsx | 119 +++++++++-------- .../components/MediaCoverageChart/index.tsx | 107 +++++++-------- .../components/MediaSentimentChart/index.tsx | 121 +++++++++-------- 5 files changed, 244 insertions(+), 234 deletions(-) diff --git a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx index fea84f1..20c2367 100644 --- a/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx +++ b/frontend-nextjs/src/components/DraggableTimeFilterRange.tsx @@ -94,7 +94,10 @@ function DraggableTimeFilterRange() { onRangeDragStart={() => setIsDragging(true)} onThumbDragStart={() => setIsDragging(true)} id="draggable-time-filter-range" - className="group" + className={cn( + 'group transition-opacity', + isLoading && 'opacity-0 pointer-events-none', + )} disabled={isLoading} /> & { - count?: number; -}; +export type LegendOrganisation = Omit & { + count?: number +} const placeholderOrganisations: LegendOrganisation[] = [ { - slug: "fridays-for-future" as EventOrganizerSlugType, - name: "Fridays for Future", - color: "var(--categorical-color-1)", + slug: 'fridays-for-future' as EventOrganizerSlugType, + name: 'Fridays for Future', + color: 'var(--categorical-color-1)', isMain: true, }, { - slug: "last-generation" as EventOrganizerSlugType, - name: "Last Generation", - color: "var(--categorical-color-2)", + slug: 'last-generation' as EventOrganizerSlugType, + name: 'Last Generation', + color: 'var(--categorical-color-2)', isMain: true, }, { - slug: "extinction-rebellion" as EventOrganizerSlugType, - name: "Extinction Rebellion", - color: "var(--categorical-color-3)", + slug: 'extinction-rebellion' as EventOrganizerSlugType, + name: 'Extinction Rebellion', + color: 'var(--categorical-color-3)', isMain: true, }, { - slug: "bund" as EventOrganizerSlugType, - name: "BUND", - color: "var(--categorical-color-4)", + slug: 'bund' as EventOrganizerSlugType, + name: 'BUND', + color: 'var(--categorical-color-4)', isMain: true, }, { - slug: "greenpeace" as EventOrganizerSlugType, - name: "Greenpeace", - color: "var(--categorical-color-5)", + slug: 'greenpeace' as EventOrganizerSlugType, + name: 'Greenpeace', + color: 'var(--categorical-color-5)', isMain: true, }, { - slug: "verdi-united-services-union" as EventOrganizerSlugType, - name: "Ver.di: United Services Union", - color: "var(--categorical-color-6)", + slug: 'verdi-united-services-union' as EventOrganizerSlugType, + name: 'Ver.di: United Services Union', + color: 'var(--categorical-color-6)', isMain: true, }, { - slug: "ende-gelaende" as EventOrganizerSlugType, - name: "Ende Gelaende", - color: "var(--categorical-color-7)", + slug: 'ende-gelaende' as EventOrganizerSlugType, + name: 'Ende Gelaende', + color: 'var(--categorical-color-7)', isMain: true, }, { - slug: "the-greens" as EventOrganizerSlugType, - name: "The Greens", - color: "var(--categorical-color-8)", + slug: 'the-greens' as EventOrganizerSlugType, + name: 'The Greens', + color: 'var(--categorical-color-8)', isMain: true, }, { - slug: "mlpd-marxist-leninist-party-of-germany" as EventOrganizerSlugType, - name: "MLPD: Marxist-Leninist Party of Germany", - color: "var(--categorical-color-9)", + slug: 'mlpd-marxist-leninist-party-of-germany' as EventOrganizerSlugType, + name: 'MLPD: Marxist-Leninist Party of Germany', + color: 'var(--categorical-color-9)', isMain: true, }, { - slug: "the-left" as EventOrganizerSlugType, - name: "The Left", - color: "var(--categorical-color-10)", + slug: 'the-left' as EventOrganizerSlugType, + name: 'The Left', + color: 'var(--categorical-color-10)', isMain: true, }, { - slug: "government-of-germany" as EventOrganizerSlugType, - name: "Government of Germany", - color: "var(--grayDark)", + slug: 'government-of-germany' as EventOrganizerSlugType, + name: 'Government of Germany', + color: 'var(--grayDark)', isMain: false, }, { - slug: "adfc-german-bicycle-club" as EventOrganizerSlugType, - name: "ADFC: German Bicycle Club", - color: "var(--categorical-color-11)", + slug: 'adfc-german-bicycle-club' as EventOrganizerSlugType, + name: 'ADFC: German Bicycle Club', + color: 'var(--categorical-color-11)', isMain: true, }, { - slug: "spd-social-democratic-party-of-germany" as EventOrganizerSlugType, - name: "SPD: Social Democratic Party of Germany", - color: "var(--categorical-color-12)", + slug: 'spd-social-democratic-party-of-germany' as EventOrganizerSlugType, + name: 'SPD: Social Democratic Party of Germany', + color: 'var(--categorical-color-12)', isMain: true, }, { - slug: "rebell-youth-league-rebel" as EventOrganizerSlugType, - name: "REBELL: Youth League Rebel", - color: "var(--categorical-color-14)", + slug: 'rebell-youth-league-rebel' as EventOrganizerSlugType, + name: 'REBELL: Youth League Rebel', + color: 'var(--categorical-color-14)', isMain: true, }, -]; +] function EventsTimelineLegend({ sizeScale = scaleLinear().domain([10, 100000]).range([10, 193.397]), selectedOrganisations = placeholderOrganisations, }: { - sizeScale?: ScaleLinear; - selectedOrganisations?: LegendOrganisation[]; + sizeScale?: ScaleLinear + selectedOrganisations?: LegendOrganisation[] }) { - const [parentRef, { width }] = useElementSize(); - const aggregationUnit = useAggregationUnit(width); + const [parentRef, { width }] = useElementSize() + const aggregationUnit = useAggregationUnit(width) return (
-
+
- ); + ) } -export default EventsTimelineLegend; +export default EventsTimelineLegend diff --git a/frontend-nextjs/src/components/FullTextsSentimentChart/index.tsx b/frontend-nextjs/src/components/FullTextsSentimentChart/index.tsx index 49678c4..519208b 100644 --- a/frontend-nextjs/src/components/FullTextsSentimentChart/index.tsx +++ b/frontend-nextjs/src/components/FullTextsSentimentChart/index.tsx @@ -1,6 +1,6 @@ -"use client"; -import type { TrendQueryProps } from "@/utility/mediaTrendUtil"; -import { Suspense, memo, useMemo } from "react"; +'use client' +import type { TrendQueryProps } from '@/utility/mediaTrendUtil' +import { Suspense, memo, useMemo } from 'react' import { Bar, BarChart, @@ -9,40 +9,36 @@ import { Tooltip, XAxis, YAxis, -} from "recharts"; +} from 'recharts' -import { slugifyCssClass } from "@/utility/cssSlugify"; -import { parseErrorMessage } from "@/utility/errorHandlingUtil"; +import { slugifyCssClass } from '@/utility/cssSlugify' +import { parseErrorMessage } from '@/utility/errorHandlingUtil' import { type ParsedFullTextsType, useFullTextsTrends, -} from "@/utility/useFullTextsTrends"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import { BarChartIcon } from "lucide-react"; -import { ErrorBoundary } from "next/dist/client/components/error-boundary"; -import ChartLimitations from "../ChartLimitations"; -import MediaSentimentChartEmpty from "../MediaSentimentChart/MediaSentimentChartEmpty"; -import MediaSentimentChartError from "../MediaSentimentChart/MediaSentimentChartError"; -import MediaSentimentChartLoading from "../MediaSentimentChart/MediaSentimentChartLoading"; -import TopicChartTooltip from "../TopicChartTooltip"; +} from '@/utility/useFullTextsTrends' +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { BarChartIcon } from 'lucide-react' +import { ErrorBoundary } from 'next/dist/client/components/error-boundary' +import ChartLimitations from '../ChartLimitations' +import MediaSentimentChartEmpty from '../MediaSentimentChart/MediaSentimentChartEmpty' +import MediaSentimentChartError from '../MediaSentimentChart/MediaSentimentChartError' +import MediaSentimentChartLoading from '../MediaSentimentChart/MediaSentimentChartLoading' +import TopicChartTooltip from '../TopicChartTooltip' export const FullTextSentimentChart = memo( - ({ - data, - }: { - data: ParsedFullTextsType["data"]; - }) => { + ({ data }: { data: ParsedFullTextsType['data'] }) => { const topics = useMemo(() => { - return ["positive", "neutral", "negative"].map((topic) => ({ + return ['positive', 'neutral', 'negative'].map((topic) => ({ topic, color: `var(--sentiment-${topic})`, sum: data?.trends?.reduce((acc, d) => { - const val = d[topic as "positive" | "neutral" | "negative"] ?? 0; - return acc + (val ?? 0); + const val = d[topic as 'positive' | 'neutral' | 'negative'] ?? 0 + return acc + (val ?? 0) }, 0) ?? 0, - })); - }, [data?.trends]); + })) + }, [data?.trends]) return (
@@ -75,7 +71,7 @@ export const FullTextSentimentChart = memo( tickSize={8} tickLine={{ strokeOpacity: 1, - stroke: "var(--grayDark)", + stroke: 'var(--grayDark)', }} /> `${value} articles`} - cursor={{ fill: "var(--bgOverlay)", fillOpacity: 0.5 }} + cursor={{ fill: 'var(--bgOverlay)', fillOpacity: 0.5 }} content={({ payload, active }) => { - const item = payload?.at(0)?.payload; - if (!active || !payload || !item) return null; + const item = payload?.at(0)?.payload + if (!active || !payload || !item) return null return ( - ); + ) }} /> - {topics.map(({ topic, color }) => ( - - ))} + {useMemo( + () => + topics.map(({ topic, color }) => ( + + )), + [topics], + )}
- ); + ) }, -); +) function FullTextsSentimentChartWithData({ reset, sentiment_target, event_id, }: { - reset?: () => void; - sentiment_target: TrendQueryProps["sentiment_target"]; - event_id: string; + reset?: () => void + sentiment_target: TrendQueryProps['sentiment_target'] + event_id: string }) { const { data: originalData, @@ -137,31 +138,31 @@ function FullTextsSentimentChartWithData({ } = useFullTextsTrends({ event_id, sentiment_target, - }); + }) const data = originalData || { applicability: false, limitations: [], trends: [], - }; - if (isPending) return ; + } + if (isPending) return if (isError) return ( - ); + ) if (isSuccess && data.applicability === false && data.limitations.length > 0) return ( - ); + ) if (isSuccess && data.applicability && (data.trends?.length ?? 0) > 0) - return ; - return ; + return + return } export default function MediaCoverageChartWithErrorBoundary({ sentiment_target, event_id, }: { - sentiment_target: TrendQueryProps["sentiment_target"]; - event_id: string; + sentiment_target: TrendQueryProps['sentiment_target'] + event_id: string }) { return ( @@ -184,5 +185,5 @@ export default function MediaCoverageChartWithErrorBoundary({ )} - ); + ) } diff --git a/frontend-nextjs/src/components/MediaCoverageChart/index.tsx b/frontend-nextjs/src/components/MediaCoverageChart/index.tsx index 4bf3f12..7edae16 100644 --- a/frontend-nextjs/src/components/MediaCoverageChart/index.tsx +++ b/frontend-nextjs/src/components/MediaCoverageChart/index.tsx @@ -1,6 +1,6 @@ -"use client"; -import useElementSize from "@custom-react-hooks/use-element-size"; -import { Suspense, memo } from "react"; +'use client' +import useElementSize from '@custom-react-hooks/use-element-size' +import { Suspense, memo, useMemo } from 'react' import { CartesianGrid, Line, @@ -9,29 +9,29 @@ import { Tooltip, XAxis, YAxis, -} from "recharts"; +} from 'recharts' -import TopicChartTooltip from "@/components/TopicChartTooltip"; -import { slugifyCssClass } from "@/utility/cssSlugify"; -import { parseErrorMessage } from "@/utility/errorHandlingUtil"; -import { texts } from "@/utility/textUtil"; -import useMediaTrends from "@/utility/useMediaTrends"; -import useTopics from "@/utility/useTopics"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import { LineChartIcon } from "lucide-react"; -import { ErrorBoundary } from "next/dist/client/components/error-boundary"; -import ChartLimitations from "../ChartLimitations"; -import MediaCoverageChartEmpty from "./MediaCoverageChartEmpty"; -import MediaCoverageChartError from "./MediaCoverageChartError"; -import MediaCoverageChartLoading from "./MediaCoverageChartLoading"; +import TopicChartTooltip from '@/components/TopicChartTooltip' +import { slugifyCssClass } from '@/utility/cssSlugify' +import { parseErrorMessage } from '@/utility/errorHandlingUtil' +import { texts } from '@/utility/textUtil' +import useMediaTrends from '@/utility/useMediaTrends' +import useTopics from '@/utility/useTopics' +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { LineChartIcon } from 'lucide-react' +import { ErrorBoundary } from 'next/dist/client/components/error-boundary' +import ChartLimitations from '../ChartLimitations' +import MediaCoverageChartEmpty from './MediaCoverageChartEmpty' +import MediaCoverageChartError from './MediaCoverageChartError' +import MediaCoverageChartLoading from './MediaCoverageChartLoading' const MediaCoverageChart = memo(() => { - const [parentRef, size] = useElementSize(); + const [parentRef, size] = useElementSize() const { topics, filteredData, aggregationUnit } = useTopics({ containerWidth: size.width, - trend_type: "keywords", + trend_type: 'keywords', sentiment_target: null, - }); + }) return (
@@ -67,7 +67,7 @@ const MediaCoverageChart = memo(() => { tickSize={8} tickLine={{ strokeOpacity: 1, - stroke: "var(--grayDark)", + stroke: 'var(--grayDark)', }} /> { /> `${value} articles`} - cursor={{ stroke: "var(--grayMed)" }} + cursor={{ stroke: 'var(--grayMed)' }} content={({ payload, active }) => { - const item = payload?.at(0)?.payload; - if (!active || !payload || !item) return null; + const item = payload?.at(0)?.payload + if (!active || !payload || !item) return null return ( - ); + ) }} /> - {topics.map(({ topic, color }, idx) => ( - - ))} + {useMemo( + () => + topics.map(({ topic, color }, idx) => ( + + )), + [topics], + )}
- ); -}); + ) +}) -function MediaCoverageChartWithData({ - reset, -}: { - reset?: () => void; -}) { +function MediaCoverageChartWithData({ reset }: { reset?: () => void }) { const { data, isError, isSuccess, isPending } = useMediaTrends({ - trend_type: "keywords", - }); - if (isPending) return ; + trend_type: 'keywords', + }) + if (isPending) return if (isError) return ( - ); + ) if (isSuccess && data.limitations.length > 0) return ( - ); - if (isSuccess && data.applicability) return ; - return ; + ) + if (isSuccess && data.applicability) return + return } export default function MediaCoverageChartWithErrorBoundary() { return ( @@ -151,5 +152,5 @@ export default function MediaCoverageChartWithErrorBoundary() { )} - ); + ) } diff --git a/frontend-nextjs/src/components/MediaSentimentChart/index.tsx b/frontend-nextjs/src/components/MediaSentimentChart/index.tsx index 416ccaf..9379efa 100644 --- a/frontend-nextjs/src/components/MediaSentimentChart/index.tsx +++ b/frontend-nextjs/src/components/MediaSentimentChart/index.tsx @@ -1,7 +1,7 @@ -"use client"; -import type { TrendQueryProps } from "@/utility/mediaTrendUtil"; -import useElementSize from "@custom-react-hooks/use-element-size"; -import { Suspense, memo, useMemo } from "react"; +'use client' +import type { TrendQueryProps } from '@/utility/mediaTrendUtil' +import useElementSize from '@custom-react-hooks/use-element-size' +import { Suspense, memo, useMemo } from 'react' import { Bar, BarChart, @@ -10,45 +10,45 @@ import { Tooltip, XAxis, YAxis, -} from "recharts"; +} from 'recharts' -import { slugifyCssClass } from "@/utility/cssSlugify"; -import { parseErrorMessage } from "@/utility/errorHandlingUtil"; -import useMediaTrends from "@/utility/useMediaTrends"; -import useTopics from "@/utility/useTopics"; -import { QueryErrorResetBoundary } from "@tanstack/react-query"; -import { BarChartIcon } from "lucide-react"; -import { ErrorBoundary } from "next/dist/client/components/error-boundary"; -import ChartLimitations from "../ChartLimitations"; -import TopicChartTooltip from "../TopicChartTooltip"; -import MediaSentimentChartEmpty from "./MediaSentimentChartEmpty"; -import MediaSentimentChartError from "./MediaSentimentChartError"; -import MediaSentimentChartLoading from "./MediaSentimentChartLoading"; +import { slugifyCssClass } from '@/utility/cssSlugify' +import { parseErrorMessage } from '@/utility/errorHandlingUtil' +import useMediaTrends from '@/utility/useMediaTrends' +import useTopics from '@/utility/useTopics' +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { BarChartIcon } from 'lucide-react' +import { ErrorBoundary } from 'next/dist/client/components/error-boundary' +import ChartLimitations from '../ChartLimitations' +import TopicChartTooltip from '../TopicChartTooltip' +import MediaSentimentChartEmpty from './MediaSentimentChartEmpty' +import MediaSentimentChartError from './MediaSentimentChartError' +import MediaSentimentChartLoading from './MediaSentimentChartLoading' export const MediaSentimentChart = memo( ({ sentiment_target, }: { - sentiment_target: TrendQueryProps["sentiment_target"]; + sentiment_target: TrendQueryProps['sentiment_target'] }) => { - const [parentRef, size] = useElementSize(); + const [parentRef, size] = useElementSize() const { topics: queryTopics, filteredData, aggregationUnit, } = useTopics({ containerWidth: size.width, - trend_type: "sentiment", + trend_type: 'sentiment', sentiment_target, - }); + }) const topics = useMemo(() => { - return ["positive", "neutral", "negative"].map((topic) => ({ + return ['positive', 'neutral', 'negative'].map((topic) => ({ topic, color: `var(--sentiment-${topic})`, sum: queryTopics.find((t) => t.topic === topic)?.sum ?? 0, - })); - }, [queryTopics]); + })) + }, [queryTopics]) return (
@@ -84,7 +84,7 @@ export const MediaSentimentChart = memo( tickSize={8} tickLine={{ strokeOpacity: 1, - stroke: "var(--grayDark)", + stroke: 'var(--grayDark)', }} /> `${value} articles`} - cursor={{ fill: "var(--bgOverlay)", fillOpacity: 0.5 }} + cursor={{ fill: 'var(--bgOverlay)', fillOpacity: 0.5 }} content={({ payload, active }) => { - const item = payload?.at(0)?.payload; - if (!active || !payload || !item) return null; + const item = payload?.at(0)?.payload + if (!active || !payload || !item) return null return ( - ); + ) }} /> - {topics.map(({ topic, color }) => ( - - ))} + {useMemo( + () => + topics.map(({ topic, color }) => ( + + )), + [topics], + )}
- ); + ) }, -); +) function MediaSentimentChartWithData({ reset, sentiment_target, event_id, }: { - reset?: () => void; - sentiment_target: TrendQueryProps["sentiment_target"]; - event_id?: string; + reset?: () => void + sentiment_target: TrendQueryProps['sentiment_target'] + event_id?: string }) { const { data: originalData, @@ -144,34 +149,34 @@ function MediaSentimentChartWithData({ isPending, isSuccess, } = useMediaTrends({ - trend_type: "sentiment", + trend_type: 'sentiment', sentiment_target, enabled: !event_id, - }); + }) const data = originalData || { applicability: false, limitations: [], trends: [], - }; - if (isPending) return ; + } + if (isPending) return if (isError) return ( - ); + ) if (isSuccess && data.applicability === false && data.limitations.length > 0) return ( - ); + ) if (isSuccess && data.applicability && (data.trends?.length ?? 0) > 0) - return ; - return ; + return + return } export default function MediaCoverageChartWithErrorBoundary({ sentiment_target, event_id, }: { - sentiment_target: TrendQueryProps["sentiment_target"]; - event_id?: string; + sentiment_target: TrendQueryProps['sentiment_target'] + event_id?: string }) { return ( @@ -194,5 +199,5 @@ export default function MediaCoverageChartWithErrorBoundary({ )} - ); + ) } From 8e975674ef908f2d954966c2ead8492a476003ca Mon Sep 17 00:00:00 2001 From: Lucas Vogel Date: Sat, 31 Aug 2024 23:15:11 -0400 Subject: [PATCH 09/28] refactor(misc): Different layout issues on safari and in general --- .../src/app/(pages)/(info)/layout.tsx | 22 ++-- .../src/app/(pages)/dashboard/page.tsx | 28 ++--- frontend-nextjs/src/app/(pages)/not-found.tsx | 19 +-- frontend-nextjs/src/app/(pages)/page.tsx | 66 +++++------ .../src/components/BaseLayout/index.tsx | 72 +++++------ .../src/components/DocsPrevNextNav.tsx | 56 ++++----- .../src/components/OrganisationsSelect.tsx | 112 +++++++++--------- .../src/components/ui/combobox.tsx | 64 +++++----- frontend-nextjs/src/components/ui/dialog.tsx | 106 ++++++++--------- frontend-nextjs/src/components/ui/drawer.tsx | 90 +++++++------- frontend-nextjs/src/styles/global.css | 14 +-- 11 files changed, 326 insertions(+), 323 deletions(-) diff --git a/frontend-nextjs/src/app/(pages)/(info)/layout.tsx b/frontend-nextjs/src/app/(pages)/(info)/layout.tsx index 4089623..593acc9 100644 --- a/frontend-nextjs/src/app/(pages)/(info)/layout.tsx +++ b/frontend-nextjs/src/app/(pages)/(info)/layout.tsx @@ -1,19 +1,21 @@ -import NewsletterFooterSection from "@/components/NewsletterFooterSection"; -import headerImage from "@/images/header-bg.webp"; -import { cn } from "@/utility/classNames"; -import Image from "next/image"; -import type { ReactNode } from "react"; +import NewsletterFooterSection from '@/components/NewsletterFooterSection' +import headerImage from '@/images/header-bg.webp' +import { cn } from '@/utility/classNames' +import Image from 'next/image' +import type { ReactNode } from 'react' export default async function AboutPageLayout({ children, -}: { children: ReactNode }) { +}: { + children: ReactNode +}) { return ( <>
-
+
- i - }`} + }`} className="text-pretty" > {desc} @@ -43,7 +41,7 @@ export default function EventsPageWithSuspense() { key={`${desc}-${ // biome-ignore lint/suspicious/noArrayIndexKey: i - }`} + }`} className="text-pretty" > {desc} @@ -69,7 +67,7 @@ export default function EventsPageWithSuspense() { key={`${desc}-${ // biome-ignore lint/suspicious/noArrayIndexKey: i - }`} + }`} className="text-pretty" > {desc} @@ -97,7 +95,7 @@ export default function EventsPageWithSuspense() { key={`${desc}-${ // biome-ignore lint/suspicious/noArrayIndexKey: i - }`} + }`} className="text-pretty" > {desc} @@ -119,5 +117,5 @@ export default function EventsPageWithSuspense() { - ); + ) } diff --git a/frontend-nextjs/src/app/(pages)/not-found.tsx b/frontend-nextjs/src/app/(pages)/not-found.tsx index 19ed202..ff0871b 100644 --- a/frontend-nextjs/src/app/(pages)/not-found.tsx +++ b/frontend-nextjs/src/app/(pages)/not-found.tsx @@ -1,6 +1,6 @@ -import InternalLink from "@/components/InternalLink"; -import { buttonVariants } from "@/components/ui/button"; -import { texts } from "@/utility/textUtil"; +import InternalLink from '@/components/InternalLink' +import { buttonVariants } from '@/components/ui/button' +import { texts } from '@/utility/textUtil' export const metadata = { title: `${texts.mainNavigation.fourOFour} | ${texts.seo.siteTitle}`, @@ -8,18 +8,21 @@ export const metadata = { export default function NotFound() { return ( -
+

404

-

{texts.fourOFour.heading}

+

+ {' '} + {texts.fourOFour.heading}{' '} +

{texts.fourOFour.description}

{texts.homepage.hero.buttons.goToDashboard}
- ); + ) } diff --git a/frontend-nextjs/src/app/(pages)/page.tsx b/frontend-nextjs/src/app/(pages)/page.tsx index 1ffc6e2..fd59ccc 100644 --- a/frontend-nextjs/src/app/(pages)/page.tsx +++ b/frontend-nextjs/src/app/(pages)/page.tsx @@ -1,13 +1,13 @@ -import InternalLink from "@/components/InternalLink"; -import { NewsletterForm } from "@/components/NewsletterForm"; -import AppLogo from "@/components/logos/AppLogo"; -import { buttonVariants } from "@/components/ui/button"; -import headerImage from "@/images/header-bg.webp"; -import dashboardScreenshotDark from "@/images/home-screenshot-dashboard-dark.webp"; -import dashboardScreenshotLight from "@/images/home-screenshot-dashboard-light.webp"; -import { cn } from "@/utility/classNames"; -import { texts } from "@/utility/textUtil"; -import Image from "next/image"; +import InternalLink from '@/components/InternalLink' +import { NewsletterForm } from '@/components/NewsletterForm' +import AppLogo from '@/components/logos/AppLogo' +import { buttonVariants } from '@/components/ui/button' +import headerImage from '@/images/header-bg.webp' +import dashboardScreenshotDark from '@/images/home-screenshot-dashboard-dark.webp' +import dashboardScreenshotLight from '@/images/home-screenshot-dashboard-light.webp' +import { cn } from '@/utility/classNames' +import { texts } from '@/utility/textUtil' +import Image from 'next/image' export const metadata = { title: `${texts.mainNavigation.home} | ${texts.seo.siteTitle}`, @@ -17,8 +17,8 @@ export default function HomePageWithSuspense() { return (
@@ -36,11 +36,11 @@ export default function HomePageWithSuspense() {
- {" "} + {' '} beta

@@ -52,44 +52,44 @@ export default function HomePageWithSuspense() { key={`${text}-${ // biome-ignore lint/suspicious/noArrayIndexKey: i - }`} + }`} > {text}

))}
{texts.homepage.hero.buttons.goToDashboard} {texts.homepage.hero.buttons.about} {texts.homepage.hero.buttons.docs} @@ -103,15 +103,15 @@ export default function HomePageWithSuspense() {

-
+
{texts.homepage.hero.backgroundImage.lightAlt} {texts.homepage.hero.backgroundImage.darkAlt}
- ); + ) } diff --git a/frontend-nextjs/src/components/BaseLayout/index.tsx b/frontend-nextjs/src/components/BaseLayout/index.tsx index 39ab78e..4129f3e 100644 --- a/frontend-nextjs/src/components/BaseLayout/index.tsx +++ b/frontend-nextjs/src/components/BaseLayout/index.tsx @@ -1,41 +1,41 @@ -"use client"; +'use client' -import Footer from "@/components/Footer"; -import "@/styles/global.css"; -import { cn } from "@/utility/classNames"; -import { motion } from "framer-motion"; -import { usePathname } from "next/navigation"; -import { type ReactNode, useMemo, useRef } from "react"; -import { Menu } from "../menu"; -import { doesPathnameShowAnyFilter } from "../menu/HeaderMenu"; +import Footer from '@/components/Footer' +import '@/styles/global.css' +import { cn } from '@/utility/classNames' +import { motion } from 'framer-motion' +import { usePathname } from 'next/navigation' +import { type ReactNode, useMemo, useRef } from 'react' +import { Menu } from '../menu' +import { doesPathnameShowAnyFilter } from '../menu/HeaderMenu' export function BaseLayout({ children, modal, }: { - children: ReactNode; - modal: ReactNode; + children: ReactNode + modal: ReactNode }) { - const pathname = usePathname(); - const currentPage = pathname.split("/")[1] || "home"; - const showFilters = doesPathnameShowAnyFilter(pathname); - const previouslyShown = useRef(showFilters); + const pathname = usePathname() + const currentPage = pathname.split('/')[1] || 'home' + const showFilters = doesPathnameShowAnyFilter(pathname) + const previouslyShown = useRef(showFilters) const shouldExit = useMemo(() => { - if (!previouslyShown.current) return showFilters; - const wasShown = previouslyShown.current; - previouslyShown.current = showFilters; - return wasShown && !showFilters; - }, [showFilters]); + if (!previouslyShown.current) return showFilters + const wasShown = previouslyShown.current + previouslyShown.current = showFilters + return wasShown && !showFilters + }, [showFilters]) return ( <> -
+