From 039f3baf8ead3363090f6be1bc9aab6eab41e909 Mon Sep 17 00:00:00 2001 From: Artur Pata Date: Tue, 13 Aug 2024 11:39:35 +0300 Subject: [PATCH] Add Typescript (#4427) * Add typescript, rewrite Countries map * Add back DB IP geolocation notice * Silence all current eslint warnings: to be removed gradually * Reconfigure eslint import plugin for typescript * Insert formatting pragma by default, but ignore files without pragma in CI --- .tool-versions | 2 +- assets/.eslintrc.json | 39 +- assets/.prettierrc.json | 2 +- assets/css/app.css | 5 - assets/js/dashboard.js | 13 +- assets/js/dashboard/components/lazy-loader.js | 3 +- assets/js/dashboard/datepicker.js | 3 +- assets/js/dashboard/extra/funnel.js | 2 + assets/js/dashboard/filters.js | 2 + .../dashboard/navigation/use-app-navigate.js | 65 - .../dashboard/navigation/use-app-navigate.tsx | 85 + assets/js/dashboard/pinned-header-hoc.js | 2 +- assets/js/dashboard/site-context.js | 59 - assets/js/dashboard/site-context.tsx | 69 + assets/js/dashboard/stats/behaviours/index.js | 4 + assets/js/dashboard/stats/behaviours/props.js | 2 + .../dashboard/stats/graph/interval-picker.js | 1 + .../js/dashboard/stats/graph/visitor-graph.js | 1 + .../stats/locations/geolocation-notice.tsx | 18 + assets/js/dashboard/stats/locations/index.js | 2 +- .../dashboard/stats/locations/map-tooltip.tsx | 39 + assets/js/dashboard/stats/locations/map.js | 180 -- assets/js/dashboard/stats/locations/map.tsx | 329 ++++ .../stats/modals/filter-modal-row.js | 1 + assets/js/dashboard/stats/more-link.js | 2 - assets/js/dashboard/stats/reports/list.js | 3 + assets/js/dashboard/stats/reports/metrics.js | 1 + assets/js/dashboard/theme-context.tsx | 62 + assets/js/dashboard/user-context.js | 14 - assets/js/dashboard/user-context.tsx | 35 + assets/js/dashboard/util/storage.js | 2 +- assets/js/dashboard/util/url.js | 129 -- assets/js/dashboard/util/url.ts | 149 ++ assets/js/liveview/dropdown.js | 2 +- assets/js/polyfills/closest.js | 1 + assets/package-lock.json | 1577 ++++++++++++----- assets/package.json | 24 +- assets/tailwind.config.js | 2 +- assets/tsconfig.json | 12 + config/config.exs | 2 +- config/dev.exs | 1 + mix.exs | 1 + 42 files changed, 1975 insertions(+), 972 deletions(-) delete mode 100644 assets/js/dashboard/navigation/use-app-navigate.js create mode 100644 assets/js/dashboard/navigation/use-app-navigate.tsx delete mode 100644 assets/js/dashboard/site-context.js create mode 100644 assets/js/dashboard/site-context.tsx create mode 100644 assets/js/dashboard/stats/locations/geolocation-notice.tsx create mode 100644 assets/js/dashboard/stats/locations/map-tooltip.tsx delete mode 100644 assets/js/dashboard/stats/locations/map.js create mode 100644 assets/js/dashboard/stats/locations/map.tsx create mode 100644 assets/js/dashboard/theme-context.tsx delete mode 100644 assets/js/dashboard/user-context.js create mode 100644 assets/js/dashboard/user-context.tsx delete mode 100644 assets/js/dashboard/util/url.js create mode 100644 assets/js/dashboard/util/url.ts create mode 100644 assets/tsconfig.json diff --git a/.tool-versions b/.tool-versions index 18cb90df0..8d70cf79e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ erlang 27.0 elixir 1.17.1-otp-27 -nodejs 21.0.0 +nodejs 21.7.3 diff --git a/assets/.eslintrc.json b/assets/.eslintrc.json index 604701d06..7f9f1f40f 100644 --- a/assets/.eslintrc.json +++ b/assets/.eslintrc.json @@ -1,22 +1,20 @@ { - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, + "root": true, "env": { "browser": true, "es6": true }, + "plugins": ["import"], "extends": [ "eslint:recommended", + "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended", - "plugin:import/recommended", "plugin:react/recommended", "plugin:react-hooks/recommended", "prettier" ], "rules": { - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "import/no-unresolved": "error", "react/destructuring-assignment": [0], "react/prop-types": [0], "max-classes-per-file": [0], @@ -24,20 +22,39 @@ "react/jsx-one-expression-per-line": [0], "react/self-closing-comp": [0], "no-unused-expressions": [1, { "allowShortCircuit": true }], - "no-unused-vars": [2, { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": [ + 2, + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ], "no-prototype-builtins": [0], "react/jsx-props-no-spreading": [0], "jsx-a11y/click-events-have-key-events": [0], "jsx-a11y/no-static-element-interactions": [0], "react/no-did-update-set-state": [0], - "react/no-unknown-property": [2, {"ignore": ["tooltip"]}] + "react/no-unknown-property": [2, { "ignore": ["tooltip"] }] }, "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] + }, + "import/resolver": { + "typescript": { + "alwaysTryTypes": true // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` + } + }, "react": { "createClass": "createReactClass", // Regex for Component Factory to use, - // default to "createReactClass" - "pragma": "React", // Pragma to use, default to "React" - "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment" + // default to "createReactClass" + "pragma": "React", // Pragma to use, default to "React" + "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment" "version": "detect" } } diff --git a/assets/.prettierrc.json b/assets/.prettierrc.json index d29fc41d0..5c42a1aa9 100644 --- a/assets/.prettierrc.json +++ b/assets/.prettierrc.json @@ -1,6 +1,6 @@ { "singleQuote": true, - "requirePragma": true, + "insertPragma": true, "trailingComma": "none", "semi": false } diff --git a/assets/css/app.css b/assets/css/app.css index 273900ea6..7d294a127 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -255,11 +255,6 @@ blockquote { cursor: pointer; } -/* Only because the map handler doesn't expose an easier way to change the shadow color */ -.dark .hoverinfo { - box-shadow: 1px 1px 5px rgb(26 32 44); -} - .fullwidth-shadow::before { @apply absolute top-0 w-screen h-full bg-gray-50 dark:bg-gray-850; diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 87f7800be..da555891d 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -10,6 +10,7 @@ import * as timer from './dashboard/util/realtime-update-timer' import { filtersBackwardsCompatibilityRedirect } from './dashboard/query'; import SiteContextProvider, { parseSiteFromDataset } from './dashboard/site-context'; import UserContextProvider from './dashboard/user-context' +import ThemeContextProvider from './dashboard/theme-context' timer.start() @@ -32,11 +33,13 @@ if (container) { const router = createAppRouter(site); const app = ( - - - - - + + + + + + + ) diff --git a/assets/js/dashboard/components/lazy-loader.js b/assets/js/dashboard/components/lazy-loader.js index 33adc4ff5..e0832c97d 100644 --- a/assets/js/dashboard/components/lazy-loader.js +++ b/assets/js/dashboard/components/lazy-loader.js @@ -10,8 +10,9 @@ export default function LazyLoader(props) { useEffect(() => { if (inView && !hasBecomeVisibleYet) { setHasBecomeVisibleYet(true) - props.onVisible && props.onVisible() + if (props.onVisible) props.onVisible() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [inView]) return ( diff --git a/assets/js/dashboard/datepicker.js b/assets/js/dashboard/datepicker.js index 0f7cc8bd8..7d7bf0540 100644 --- a/assets/js/dashboard/datepicker.js +++ b/assets/js/dashboard/datepicker.js @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import React, { Fragment, useState, useEffect, useCallback, useRef } from "react"; import { useAppNavigate } from "./navigation/use-app-navigate"; import Flatpickr from "react-flatpickr"; @@ -291,7 +292,7 @@ function DatePicker() { } function openCalendar() { - calendar.current && calendar.current.flatpickr.open(); + calendar.current?.flatpickr.open(); } function renderLink(period, text, opts = {}) { diff --git a/assets/js/dashboard/extra/funnel.js b/assets/js/dashboard/extra/funnel.js index d972a1da4..c3dd72040 100644 --- a/assets/js/dashboard/extra/funnel.js +++ b/assets/js/dashboard/extra/funnel.js @@ -46,12 +46,14 @@ export default function Funnel({ funnelName, tabs }) { } } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [query, funnelName, visible, isSmallScreen]) useEffect(() => { if (canvasRef.current && funnel && visible && !isSmallScreen) { initialiseChart() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [funnel, visible]) useEffect(() => { diff --git a/assets/js/dashboard/filters.js b/assets/js/dashboard/filters.js index c2621bb84..1d9c19de2 100644 --- a/assets/js/dashboard/filters.js +++ b/assets/js/dashboard/filters.js @@ -137,6 +137,7 @@ function Filters() { window.removeEventListener('resize', handleResize, false) document.removeEventListener("keyup", handleKeyup) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { @@ -145,6 +146,7 @@ function Filters() { useEffect(() => { if (wrapped === WRAPSTATE.waiting) { updateDisplayMode() } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [wrapped]) diff --git a/assets/js/dashboard/navigation/use-app-navigate.js b/assets/js/dashboard/navigation/use-app-navigate.js deleted file mode 100644 index 9448b1f8a..000000000 --- a/assets/js/dashboard/navigation/use-app-navigate.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { forwardRef, useCallback } from 'react' -import { Link, useLocation, useNavigate, generatePath } from 'react-router-dom' -import { parseSearch, stringifySearch } from '../util/url' - -const getNavigateToOptions = ( - currentSearchString, - { path, params, search } -) => { - const searchRecord = parseSearch(currentSearchString) - const updatedSearchRecord = search && search(searchRecord) - const updatedPath = path && generatePath(path, params) - return { - pathname: updatedPath, - search: updatedSearchRecord && stringifySearch(updatedSearchRecord) - } -} - -export const useGetNavigateOptions = () => { - const location = useLocation() - /** - * @param path - path to target, for example `"/posts"` or `"/posts/:id"` - * @param params - dictionary of param keys with their values, if needed, for example `{ id: "some-id" }` - * @param search - - * function in the form of `(currentSearchRecord) => newSearchRecord` to set link search value, for example - * `(s) => s` preserves current search value, - * `() => ({page: 5})` sets the search to `?page=5`, - * `undefined` empties the search - * @returns the appropriate value for `react-router-dom` `Link` and `navigate` `to` property - */ - const getToOptions = useCallback( - ({ path, params, search }) => { - return getNavigateToOptions(location.search, { path, params, search }) - }, - [location.search] - ) - return getToOptions -} - -export const useAppNavigate = () => { - const _navigate = useNavigate() - const getToOptions = useGetNavigateOptions() - const navigate = useCallback( - ({ path, params, search, ...options }) => { - return _navigate(getToOptions({ path, params, search }), options) - }, - [getToOptions, _navigate] - ) - return navigate -} - -export const AppNavigationLink = forwardRef( - ({ path, params, search, ...options }, ref) => { - const getToOptions = useGetNavigateOptions() - - return ( - - ) - } -) - -AppNavigationLink.displayName = 'AppNavigationLink' diff --git a/assets/js/dashboard/navigation/use-app-navigate.tsx b/assets/js/dashboard/navigation/use-app-navigate.tsx new file mode 100644 index 000000000..88b152f25 --- /dev/null +++ b/assets/js/dashboard/navigation/use-app-navigate.tsx @@ -0,0 +1,85 @@ +/* @format */ +import React, { forwardRef, useCallback } from 'react' +import { + Link, + useLocation, + useNavigate, + generatePath, + Params, + NavigateOptions, + LinkProps +} from 'react-router-dom' +import { parseSearch, stringifySearch } from '../util/url' + +type AppNavigationTarget = { + /** + * path to target, for example `"/posts"` or `"/posts/:id"` + */ + path?: string + /** + * dictionary of param keys with their values, if needed, for example `{ id: "some-id" }` + */ + params?: Params + /** + * function in the form of `(currentSearchRecord) => newSearchRecord` to set link search value, for example + * - `(s) => s` preserves current search value, + * - `(s) => ({ ...s, calendar: !s.calendar })` toggles the value for calendar search parameter, + * - `() => ({ page: 5 })` sets the search to `?page=5`, + * - `undefined` empties the search + */ + search?: (search: Record) => Record +} + +const getNavigateToOptions = ( + currentSearchString: string, + { path, params, search }: AppNavigationTarget +) => { + const searchRecord = parseSearch(currentSearchString) + const updatedSearchRecord = search && search(searchRecord) + const updatedPath = path && generatePath(path, params) + return { + pathname: updatedPath, + search: updatedSearchRecord && stringifySearch(updatedSearchRecord) + } +} + +export const useGetNavigateOptions = () => { + const location = useLocation() + const getToOptions = useCallback( + ({ path, params, search }: AppNavigationTarget) => { + return getNavigateToOptions(location.search, { path, params, search }) + }, + [location.search] + ) + return getToOptions +} + +export const useAppNavigate = () => { + const _navigate = useNavigate() + const getToOptions = useGetNavigateOptions() + const navigate = useCallback( + ({ + path, + params, + search, + ...options + }: AppNavigationTarget & NavigateOptions) => { + return _navigate(getToOptions({ path, params, search }), options) + }, + [getToOptions, _navigate] + ) + return navigate +} + +export const AppNavigationLink = forwardRef< + HTMLAnchorElement | null, + AppNavigationTarget & Omit +>(({ path, params, search, ...options }, ref) => { + const getToOptions = useGetNavigateOptions() + + return ( + + ) +}) + +AppNavigationLink.displayName = 'AppNavigationLink' diff --git a/assets/js/dashboard/pinned-header-hoc.js b/assets/js/dashboard/pinned-header-hoc.js index ded8331e0..062d4d93b 100644 --- a/assets/js/dashboard/pinned-header-hoc.js +++ b/assets/js/dashboard/pinned-header-hoc.js @@ -30,7 +30,7 @@ export const withPinnedHeader = (WrappedComponent, selector) => { } componentWillUnmount() { - this.observer && this.observer.unobserve(this.el); + this.observer?.unobserve(this.el); } render() { diff --git a/assets/js/dashboard/site-context.js b/assets/js/dashboard/site-context.js deleted file mode 100644 index e7429b2bf..000000000 --- a/assets/js/dashboard/site-context.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { createContext, useContext } from "react"; - -export function parseSiteFromDataset(dataset) { - const site = { - domain: dataset.domain, - offset: dataset.offset, - hasGoals: dataset.hasGoals === 'true', - hasProps: dataset.hasProps === 'true', - funnelsAvailable: dataset.funnelsAvailable === 'true', - propsAvailable: dataset.propsAvailable === 'true', - conversionsOptedOut: dataset.conversionsOptedOut === 'true', - funnelsOptedOut: dataset.funnelsOptedOut === 'true', - propsOptedOut: dataset.propsOptedOut === 'true', - revenueGoals: JSON.parse(dataset.revenueGoals), - funnels: JSON.parse(dataset.funnels), - statsBegin: dataset.statsBegin, - nativeStatsBegin: dataset.nativeStatsBegin, - embedded: dataset.embedded, - background: dataset.background, - isDbip: dataset.isDbip === 'true', - flags: JSON.parse(dataset.flags), - validIntervalsByPeriod: JSON.parse(dataset.validIntervalsByPeriod), - shared: !!dataset.sharedLinkAuth, - } - return site; -} - -const siteContextDefaultValue = { - domain: '', - offset: '0', - hasGoals: false, - hasProps: false, - funnelsAvailable: false, - propsAvailable: false, - conversionsOptedOut: false, - funnelsOptedOut: false, - propsOptedOut: false, - revenueGoals: [], - funnels: [], - statsBegin: '', - nativeStatsBegin: '', - embedded: '', - background: '', - isDbip: false, - flags: {}, - validIntervalsByPeriod: null, - shared: false, -} - - -const SiteContext = createContext(siteContextDefaultValue) - -export const useSiteContext = () => { return useContext(SiteContext) } - -const SiteContextProvider = ({ site, children }) => { - return {children} -}; - -export default SiteContextProvider; \ No newline at end of file diff --git a/assets/js/dashboard/site-context.tsx b/assets/js/dashboard/site-context.tsx new file mode 100644 index 000000000..fab055374 --- /dev/null +++ b/assets/js/dashboard/site-context.tsx @@ -0,0 +1,69 @@ +/* @format */ +import React, { createContext, ReactNode, useContext } from 'react' + +export function parseSiteFromDataset(dataset: Record) { + const site = { + domain: dataset.domain, + offset: dataset.offset, + hasGoals: dataset.hasGoals === 'true', + hasProps: dataset.hasProps === 'true', + funnelsAvailable: dataset.funnelsAvailable === 'true', + propsAvailable: dataset.propsAvailable === 'true', + conversionsOptedOut: dataset.conversionsOptedOut === 'true', + funnelsOptedOut: dataset.funnelsOptedOut === 'true', + propsOptedOut: dataset.propsOptedOut === 'true', + revenueGoals: JSON.parse(dataset.revenueGoals), + funnels: JSON.parse(dataset.funnels), + statsBegin: dataset.statsBegin, + nativeStatsBegin: dataset.nativeStatsBegin, + embedded: dataset.embedded, + background: dataset.background, + isDbip: dataset.isDbip === 'true', + flags: JSON.parse(dataset.flags), + validIntervalsByPeriod: JSON.parse(dataset.validIntervalsByPeriod), + shared: !!dataset.sharedLinkAuth + } + return site +} + +const siteContextDefaultValue = { + domain: '', + offset: '0', + hasGoals: false, + hasProps: false, + funnelsAvailable: false, + propsAvailable: false, + conversionsOptedOut: false, + funnelsOptedOut: false, + propsOptedOut: false, + revenueGoals: [] as { event_name: string; currency: 'USD' }[], + funnels: [] as { id: number; name: string; steps_count: number }[], + statsBegin: '', + nativeStatsBegin: '', + embedded: '', + background: '', + isDbip: false, + flags: {}, + validIntervalsByPeriod: {} as Record>, + shared: false +} + +export type PlausibleSite = typeof siteContextDefaultValue + +const SiteContext = createContext(siteContextDefaultValue) + +export const useSiteContext = () => { + return useContext(SiteContext) +} + +const SiteContextProvider = ({ + site, + children +}: { + site: PlausibleSite + children: ReactNode +}) => { + return {children} +} + +export default SiteContextProvider diff --git a/assets/js/dashboard/stats/behaviours/index.js b/assets/js/dashboard/stats/behaviours/index.js index dda314343..6f992e521 100644 --- a/assets/js/dashboard/stats/behaviours/index.js +++ b/assets/js/dashboard/stats/behaviours/index.js @@ -16,6 +16,7 @@ import { useUserContext } from '../../user-context' /*global require*/ function maybeRequire() { if (BUILD_EXTRA) { + // eslint-disable-next-line @typescript-eslint/no-require-imports return require('../../extra/funnel') } else { return { default: null } @@ -65,6 +66,7 @@ export default function Behaviours({ importedDataInView }) { setShowingPropsForGoalFilter(true) setMode(PROPS) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { @@ -73,10 +75,12 @@ export default function Behaviours({ importedDataInView }) { setShowingPropsForGoalFilter(false) setMode(CONVERSIONS) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasGoalFilter(query)]) useEffect(() => { setMode(defaultMode()) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabledModes]) useEffect(() => setLoading(true), [query, mode]) diff --git a/assets/js/dashboard/stats/behaviours/props.js b/assets/js/dashboard/stats/behaviours/props.js index 33bd766c6..2f61140ba 100644 --- a/assets/js/dashboard/stats/behaviours/props.js +++ b/assets/js/dashboard/stats/behaviours/props.js @@ -54,6 +54,7 @@ export default function Properties({ afterFetchData }) { setPropKeyLoading(false) }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [query]) function getPropKeyFromStorage() { @@ -73,6 +74,7 @@ export default function Properties({ afterFetchData }) { return (input) => { return api.get(url.apiPath(site, "/suggestions/prop_key"), query, { q: input.trim() }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [query]) function onPropKeySelect() { diff --git a/assets/js/dashboard/stats/graph/interval-picker.js b/assets/js/dashboard/stats/graph/interval-picker.js index fe65fc77a..34a3856b9 100644 --- a/assets/js/dashboard/stats/graph/interval-picker.js +++ b/assets/js/dashboard/stats/graph/interval-picker.js @@ -69,6 +69,7 @@ function subscribeKeybinding(element) { // eslint-disable-next-line react-hooks/rules-of-hooks const handleKeyPress = useCallback((event) => { if (isKeyPressed(event, "i")) element.current?.click() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/assets/js/dashboard/stats/graph/visitor-graph.js b/assets/js/dashboard/stats/graph/visitor-graph.js index 16ada53b9..7fc84be3f 100644 --- a/assets/js/dashboard/stats/graph/visitor-graph.js +++ b/assets/js/dashboard/stats/graph/visitor-graph.js @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect, useRef, useCallback } from 'react'; import * as api from '../../api'; import * as storage from '../../util/storage'; diff --git a/assets/js/dashboard/stats/locations/geolocation-notice.tsx b/assets/js/dashboard/stats/locations/geolocation-notice.tsx new file mode 100644 index 000000000..4b44bf2f0 --- /dev/null +++ b/assets/js/dashboard/stats/locations/geolocation-notice.tsx @@ -0,0 +1,18 @@ +/* @format */ +import React from 'react' + +export const GeolocationNotice = () => { + return ( +
+ IP Geolocation by{' '} + + DB-IP + +
+ ) +} diff --git a/assets/js/dashboard/stats/locations/index.js b/assets/js/dashboard/stats/locations/index.js index 4c5bf468d..b8ab6d1e7 100644 --- a/assets/js/dashboard/stats/locations/index.js +++ b/assets/js/dashboard/stats/locations/index.js @@ -200,7 +200,7 @@ export default class Locations extends React.Component { return case "map": default: - return + return } } diff --git a/assets/js/dashboard/stats/locations/map-tooltip.tsx b/assets/js/dashboard/stats/locations/map-tooltip.tsx new file mode 100644 index 000000000..b60e62829 --- /dev/null +++ b/assets/js/dashboard/stats/locations/map-tooltip.tsx @@ -0,0 +1,39 @@ +/* @format */ +import classNames from 'classnames' +import React from 'react' + +interface MapTooltipProps { + name: string + value: string + label: string + x: number + y: number +} + +export const MapTooltip = ({ name, value, label, x, y }: MapTooltipProps) => ( +
+
{name}
+ {value} {label} +
+) diff --git a/assets/js/dashboard/stats/locations/map.js b/assets/js/dashboard/stats/locations/map.js deleted file mode 100644 index 0cd0fa6d1..000000000 --- a/assets/js/dashboard/stats/locations/map.js +++ /dev/null @@ -1,180 +0,0 @@ -import React from 'react'; -import Datamap from 'datamaps' -import * as d3 from "d3" - -import numberFormatter from '../../util/number-formatter' -import FadeIn from '../../fade-in' -import LazyLoader from '../../components/lazy-loader' -import MoreLink from '../more-link' -import * as api from '../../api' -import { navigateToQuery } from '../../query' -import { cleanLabels, replaceFilterByPrefix } from '../../util/filters'; -import { countriesRoute } from '../../router'; -import { useAppNavigate } from '../../navigation/use-app-navigate'; -import { useQueryContext } from '../../query-context'; - -class Countries extends React.Component { - constructor(props) { - super(props) - this.resizeMap = this.resizeMap.bind(this) - this.drawMap = this.drawMap.bind(this) - this.getDataset = this.getDataset.bind(this) - this.state = { - loading: true, - darkTheme: document.querySelector('html').classList.contains('dark') || false - } - this.onVisible = this.onVisible.bind(this) - this.updateCountries = this.updateCountries.bind(this) - } - - componentDidUpdate(prevProps) { - if (this.props.query !== prevProps.query) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ loading: true, countries: null }) - this.fetchCountries(this.drawMap) - } - } - - componentWillUnmount() { - window.removeEventListener('resize', this.resizeMap); - document.removeEventListener('tick', this.updateCountries); - } - - onVisible() { - this.fetchCountries(this.drawMap) - window.addEventListener('resize', this.resizeMap); - if (this.props.query.period === 'realtime') { - document.addEventListener('tick', this.updateCountries) - } - } - - getDataset() { - const dataset = {}; - - var onlyValues = this.state.countries.map(function(obj) { return obj.visitors }); - var maxValue = Math.max.apply(null, onlyValues); - - // eslint-disable-next-line no-undef - const paletteScale = d3.scale.linear() - .domain([0, maxValue]) - .range([ - this.state.darkTheme ? "#2e3954" : "#f3ebff", - this.state.darkTheme ? "#6366f1" : "#a779e9" - ]) - - this.state.countries.forEach(function(item) { - dataset[item.alpha_3] = { numberOfThings: item.visitors, fillColor: paletteScale(item.visitors) }; - }); - - return dataset - } - - updateCountries() { - this.fetchCountries(() => { this.map.updateChoropleth(this.getDataset(), { reset: true }) }) - } - - fetchCountries(cb) { - return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query, { limit: 300 }) - .then((response) => { - if (this.props.afterFetchData) { - this.props.afterFetchData(response) - } - - this.setState({ loading: false, countries: response.results }, cb) - }) - } - - resizeMap() { - this.map && this.map.resize() - } - - drawMap() { - const dataset = this.getDataset(); - const label = this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors' - const defaultFill = this.state.darkTheme ? '#2d3747' : '#f8fafc' - const highlightFill = this.state.darkTheme ? '#374151' : '#F5F5F5' - const borderColor = this.state.darkTheme ? '#1f2937' : '#dae1e7' - const highlightBorderColor = this.state.darkTheme ? '#4f46e5' : '#a779e9' - - this.map = new Datamap({ - element: document.getElementById('map-container'), - responsive: true, - projection: 'mercator', - fills: { defaultFill }, - data: dataset, - geographyConfig: { - borderColor, - highlightBorderWidth: 2, - highlightFillColor: (geo) => geo.fillColor || highlightFill, - highlightBorderColor, - popupTemplate: (geo, data) => { - if (!data) { return null; } - const pluralizedLabel = data.numberOfThings === 1 ? label.slice(0, -1) : label - return ['
', - '', geo.properties.name, ' ', - '
', numberFormatter(data.numberOfThings), ' ', pluralizedLabel, - '
'].join(''); - } - }, - done: (datamap) => { - datamap.svg.selectAll('.datamaps-subunit').on('click', (geography) => { - const country = this.state.countries.find(c => c.alpha_3 === geography.id) - const filters = replaceFilterByPrefix(this.props.query, "country", ["is", "country", [country.code]]) - const labels = cleanLabels(filters, this.props.query.labels, "country", { [country.code]: country.name }) - - if (country) { - this.props.onClick() - - navigateToQuery( - this.props.navigate, - this.props.query, - { - filters, - labels - } - ) - } - - }) - } - }); - } - - geolocationDbNotice() { - if (this.props.site.isDbip) { - return ( - IP Geolocation by DB-IP - ) - } - - return null - } - - renderBody() { - if (this.state.countries) { - return ( - <> -
- search }} /> - {this.geolocationDbNotice()} - - ) - } - - return null - } - - render() { - return ( - - {this.state.loading &&
} - - {this.renderBody()} - -
- ) - } -} - -export default function CountriesWithRouter(props) {const {query} = useQueryContext(); const navigate = useAppNavigate(); -return ()} diff --git a/assets/js/dashboard/stats/locations/map.tsx b/assets/js/dashboard/stats/locations/map.tsx new file mode 100644 index 000000000..defc96ab2 --- /dev/null +++ b/assets/js/dashboard/stats/locations/map.tsx @@ -0,0 +1,329 @@ +/* @format */ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import * as d3 from 'd3' +import classNames from 'classnames' +// @ts-expect-error untyped +import * as api from '../../api' +// @ts-expect-error untyped +import { navigateToQuery } from '../../query' +// @ts-expect-error untyped +import { replaceFilterByPrefix, cleanLabels } from '../../util/filters' +import { useAppNavigate } from '../../navigation/use-app-navigate' +// @ts-expect-error untyped +import numberFormatter from '../../util/number-formatter' +import * as topojson from 'topojson-client' +import { useQuery } from '@tanstack/react-query' +import { useSiteContext } from '../../site-context' +// @ts-expect-error untyped +import { useQueryContext } from '../../query-context' +import worldJson from 'visionscarto-world-atlas/world/110m.json' +import { UIMode, useTheme } from '../../theme-context' +import { apiPath } from '../../util/url' +// @ts-expect-error untyped +import MoreLink from '../more-link' +// @ts-expect-error untyped +import { countriesRoute } from '../../router' +// @ts-expect-error untyped +import { MIN_HEIGHT } from '../reports/list' +import { MapTooltip } from './map-tooltip' +import { GeolocationNotice } from './geolocation-notice' + +const width = 475 +const height = 335 + +type CountryData = { + alpha_3: string + name: string + visitors: number + code: string +} +type WorldJsonCountryData = { properties: { name: string; a3: string } } + +const WorldMap = ({ + onCountrySelect, + afterFetchData +}: { + onCountrySelect: () => void + afterFetchData: (response: unknown) => void +}) => { + const navigate = useAppNavigate() + const { mode } = useTheme() + const site = useSiteContext() + const { query } = useQueryContext() + const svgRef = useRef(null) + const [tooltip, setTooltip] = useState<{ + x: number + y: number + hoveredCountryAlpha3Code: string | null + }>({ x: 0, y: 0, hoveredCountryAlpha3Code: null }) + + const labels = + query.period === 'realtime' + ? { singular: 'Current visitor', plural: 'Current visitors' } + : { singular: 'Visitor', plural: 'Visitors' } + + const { data, refetch, isFetching, isError } = useQuery({ + queryKey: ['countries', 'map', query], + placeholderData: (previousData) => previousData, + queryFn: async (): Promise<{ + results: CountryData[] + }> => { + return await api.get(apiPath(site, '/countries'), query, { + limit: 300 + }) + } + }) + + useEffect(() => { + const onTickRefetchData = () => { + if (query.period === 'realtime') { + refetch() + } + } + document.addEventListener('tick', onTickRefetchData) + return () => document.removeEventListener('tick', onTickRefetchData) + }, [query.period, refetch]) + + useEffect(() => { + if (data) { + afterFetchData(data) + } + }, [afterFetchData, data]) + + const { maxValue, dataByCountryCode } = useMemo(() => { + const dataByCountryCode: Map = new Map() + let maxValue = 0 + for (const { alpha_3, visitors, name, code } of data?.results || []) { + if (visitors > maxValue) { + maxValue = visitors + } + dataByCountryCode.set(alpha_3, { alpha_3, visitors, name, code }) + } + return { maxValue, dataByCountryCode } + }, [data]) + + const onCountryClick = useCallback( + (d: WorldJsonCountryData) => { + const country = dataByCountryCode.get(d.properties.a3) + const clickable = country && country.visitors + if (clickable) { + const filters = replaceFilterByPrefix(query, 'country', [ + 'is', + 'country', + [country.code] + ]) + const labels = cleanLabels(filters, query.labels, 'country', { + [country.code]: country.name + }) + onCountrySelect() + navigateToQuery(navigate, query, { filters, labels }) + } + }, + [navigate, query, dataByCountryCode, onCountrySelect] + ) + + useEffect(() => { + if (!svgRef.current) { + return + } + + const svg = drawInteractiveCountries(svgRef.current, setTooltip) + + return () => { + svg.selectAll('*').remove() + } + }, []) + + useEffect(() => { + if (svgRef.current) { + const palette = colorScales[mode] + + const getColorForValue = d3 + .scaleLinear() + .domain([0, maxValue]) + .range(palette) + + colorInCountriesWithValues( + svgRef.current, + getColorForValue, + dataByCountryCode + ).on('click', (_event, countryPath) => { + onCountryClick(countryPath as unknown as WorldJsonCountryData) + }) + } + }, [mode, maxValue, dataByCountryCode, onCountryClick]) + + const hoveredCountryData = tooltip.hoveredCountryAlpha3Code + ? dataByCountryCode.get(tooltip.hoveredCountryAlpha3Code) + : undefined + + return ( +
+
+
+ + {!!hoveredCountryData && ( + + )} + {isFetching || + (isError && ( +
+
+
+
+
+ ))} +
+ search + }} + /> + {site.isDbip && } +
+ ) +} + +const colorScales = { + [UIMode.dark]: ['#2e3954', '#6366f1'], + [UIMode.light]: ['#f3ebff', '#a779e9'] +} + +const sharedCountryClass = classNames('transition-colors') + +const countryClass = classNames( + sharedCountryClass, + 'stroke-1', + 'fill-[#f8fafc]', + 'stroke-[#dae1e7]', + 'dark:fill-[#2d3747]', + 'dark:stroke-[#1f2937]' +) + +const highlightedCountryClass = classNames( + sharedCountryClass, + 'stroke-2', + 'fill-[#f5f5f5]', + 'stroke-[#a779e9]', + 'dark:fill-[#374151]', + 'dark:stroke-[#4f46e5]' +) + +/** + * Used to color the countries + * @returns the svg elements represeting countries + */ +function colorInCountriesWithValues( + element: SVGSVGElement, + getColorForValue: d3.ScaleLinear, + dataByCountryCode: Map +) { + function getCountryByCountryPath(countryPath: unknown) { + return dataByCountryCode.get( + (countryPath as unknown as WorldJsonCountryData).properties.a3 + ) + } + + const svg = d3.select(element) + + return svg + .selectAll('path') + .style('fill', (countryPath) => { + const country = getCountryByCountryPath(countryPath) + if (!country?.visitors) { + return null + } + return getColorForValue(country.visitors) + }) + .style('cursor', (countryPath) => { + const country = getCountryByCountryPath(countryPath) + if (!country?.visitors) { + return null + } + return 'pointer' + }) +} + +/** @returns the d3 selected svg element */ +function drawInteractiveCountries( + element: SVGSVGElement, + setTooltip: React.Dispatch< + React.SetStateAction<{ + x: number + y: number + hoveredCountryAlpha3Code: string | null + }> + > +) { + const path = setupProjetionPath() + const data = parseWorldTopoJsonToGeoJsonFeatures() + const svg = d3.select(element) + + svg + .selectAll('path') + .data(data) + .enter() + .append('path') + .attr('class', countryClass) + .attr('d', path as never) + + .on('mouseover', function (event, country) { + const [x, y] = d3.pointer(event, svg.node()?.parentNode) + setTooltip({ x, y, hoveredCountryAlpha3Code: country.properties.a3 }) + // brings country to front + this.parentNode?.appendChild(this) + d3.select(this).attr('class', highlightedCountryClass) + }) + + .on('mousemove', function (event) { + const [x, y] = d3.pointer(event, svg.node()?.parentNode) + setTooltip((currentState) => ({ ...currentState, x, y })) + }) + + .on('mouseout', function () { + setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null }) + d3.select(this).attr('class', countryClass) + }) + + return svg +} + +function setupProjetionPath() { + const projection = d3 + .geoMercator() + .scale(75) + .translate([width / 2, height / 1.5]) + + const path = d3.geoPath().projection(projection) + return path +} + +function parseWorldTopoJsonToGeoJsonFeatures(): Array { + const collection = topojson.feature( + // @ts-expect-error strings in worldJson not recongizable as the enum values declared in library + worldJson, + worldJson.objects.countries + ) + // @ts-expect-error topojson.feature return type incorrectly inferred as not a collection + return collection.features +} + +export default WorldMap diff --git a/assets/js/dashboard/stats/modals/filter-modal-row.js b/assets/js/dashboard/stats/modals/filter-modal-row.js index e161f70e0..e984a13e4 100644 --- a/assets/js/dashboard/stats/modals/filter-modal-row.js +++ b/assets/js/dashboard/stats/modals/filter-modal-row.js @@ -19,6 +19,7 @@ export default function FilterModalRow({ const selectedClauses = useMemo( () => clauses.map((value) => ({ value, label: getLabel(labels, filterKey, value) })), + // eslint-disable-next-line react-hooks/exhaustive-deps [filter, labels] ) diff --git a/assets/js/dashboard/stats/more-link.js b/assets/js/dashboard/stats/more-link.js index d70048f49..9d7da72b0 100644 --- a/assets/js/dashboard/stats/more-link.js +++ b/assets/js/dashboard/stats/more-link.js @@ -14,7 +14,6 @@ function detailsIcon() { strokeLinecap="round" strokeLinejoin="round" > - {/* eslint-disable-next-line max-len */} ) @@ -26,7 +25,6 @@ export default function MoreLink({ linkProps, list, className, onClick }) {
diff --git a/assets/js/dashboard/stats/reports/list.js b/assets/js/dashboard/stats/reports/list.js index 413e4a9b5..38aacb599 100644 --- a/assets/js/dashboard/stats/reports/list.js +++ b/assets/js/dashboard/stats/reports/list.js @@ -125,6 +125,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI setState({ loading: false, list: response.results }) }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [keyLabel, query]) const onVisible = () => { setVisible(true) } @@ -136,6 +137,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI // only read the new metrics once the new list is loaded. setState({ loading: true, list: null }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [goalFilterApplied]); useEffect(() => { @@ -145,6 +147,7 @@ export default function ListReport({ keyLabel, metrics, colMinWidth = COL_MIN_WI } return () => { document.removeEventListener('tick', getData) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [keyLabel, query, visible]); // returns a filtered `metrics` list. Since currently, the backend can return different diff --git a/assets/js/dashboard/stats/reports/metrics.js b/assets/js/dashboard/stats/reports/metrics.js index be5a75e91..74fc7b0d2 100644 --- a/assets/js/dashboard/stats/reports/metrics.js +++ b/assets/js/dashboard/stats/reports/metrics.js @@ -6,6 +6,7 @@ import React from "react" /*global require*/ function maybeRequire() { if (BUILD_EXTRA) { + // eslint-disable-next-line @typescript-eslint/no-require-imports return require('../../extra/money') } else { return { default: null } diff --git a/assets/js/dashboard/theme-context.tsx b/assets/js/dashboard/theme-context.tsx new file mode 100644 index 000000000..bb1d0c96f --- /dev/null +++ b/assets/js/dashboard/theme-context.tsx @@ -0,0 +1,62 @@ +/* @format */ +import React, { + createContext, + ReactNode, + useContext, + useLayoutEffect, + useRef, + useState +} from 'react' + +export enum UIMode { + light = 'light', + dark = 'dark' +} + +const defaultValue = { mode: UIMode.light } + +const ThemeContext = createContext(defaultValue) + +function parseUIMode(element: Element | null): UIMode { + return element?.classList.contains('dark') ? UIMode.dark : UIMode.light +} + +export default function ThemeContextProvider({ + children +}: { + children: ReactNode +}) { + const observerRef = useRef(null) + const [mode, setMode] = useState( + parseUIMode(document.querySelector('html')) + ) + useLayoutEffect(() => { + const htmlElement = document.querySelector('html') + const currentObserver = observerRef.current + if (htmlElement && !currentObserver) { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if ( + mutation.type === 'attributes' && + mutation.attributeName === 'class' + ) { + return setMode(parseUIMode(mutation.target as Element)) + } + }) + }) + observerRef.current = observer + observer.observe(htmlElement, { + attributes: true, + attributeFilter: ['class'] + }) + } + return () => currentObserver?.disconnect() + }, []) + return ( + {children} + ) +} + +export function useTheme() { + return useContext(ThemeContext) +} diff --git a/assets/js/dashboard/user-context.js b/assets/js/dashboard/user-context.js deleted file mode 100644 index e1601fe19..000000000 --- a/assets/js/dashboard/user-context.js +++ /dev/null @@ -1,14 +0,0 @@ -import React, { createContext, useContext } from "react"; - -const userContextDefaultValue = { - role: '', - loggedIn: false, -} - -const UserContext = createContext(userContextDefaultValue) - -export const useUserContext = () => { return useContext(UserContext) } - -export default function UserContextProvider({ role, loggedIn, children }) { - return {children} -}; diff --git a/assets/js/dashboard/user-context.tsx b/assets/js/dashboard/user-context.tsx new file mode 100644 index 000000000..0ff62b395 --- /dev/null +++ b/assets/js/dashboard/user-context.tsx @@ -0,0 +1,35 @@ +/* @format */ +import React, { createContext, ReactNode, useContext } from 'react' + +export enum Role { + owner = 'owner', + admin = 'admin', + viewer = 'viewer' +} + +const userContextDefaultValue = { + role: Role.viewer, + loggedIn: false +} + +const UserContext = createContext(userContextDefaultValue) + +export const useUserContext = () => { + return useContext(UserContext) +} + +export default function UserContextProvider({ + role, + loggedIn, + children +}: { + role: Role + loggedIn: boolean + children: ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/assets/js/dashboard/util/storage.js b/assets/js/dashboard/util/storage.js index 642d2db33..11b137cf4 100644 --- a/assets/js/dashboard/util/storage.js +++ b/assets/js/dashboard/util/storage.js @@ -11,7 +11,7 @@ function testLocalStorageAvailability(){ localStorage.setItem(testItem, testItem); localStorage.removeItem(testItem); return true; - } catch(e) { + } catch (_e) { return false; } } diff --git a/assets/js/dashboard/util/url.js b/assets/js/dashboard/util/url.js deleted file mode 100644 index 67a193530..000000000 --- a/assets/js/dashboard/util/url.js +++ /dev/null @@ -1,129 +0,0 @@ -import JsonURL from '@jsonurl/jsonurl' - -export function apiPath(site, path = '') { - return `/api/stats/${encodeURIComponent(site.domain)}${path}/` -} - -export function externalLinkForPage(domain, page) { - const domainURL = new URL(`https://${domain}`) - return `https://${domainURL.host}${page}` -} - -export function isValidHttpUrl(string) { - let url; - - try { - url = new URL(string); - } catch (_) { - return false; - } - - return url.protocol === "http:" || url.protocol === "https:"; -} - - -export function trimURL(url, maxLength) { - if (url.length <= maxLength) { - return url; - } - - const ellipsis = "..."; - - if (isValidHttpUrl(url)) { - const [protocol, restURL] = url.split('://'); - const parts = restURL.split('/'); - - const host = parts.shift(); - if (host.length > maxLength - 5) { - return `${protocol}://${host.substr(0, maxLength - 5)}${ellipsis}${restURL.slice(-maxLength + 5)}`; - } - - let remainingLength = maxLength - host.length - 5; - let trimmedURL = `${protocol}://${host}`; - - for (const part of parts) { - if (part.length <= remainingLength) { - trimmedURL += '/' + part; - remainingLength -= part.length + 1; - } else { - const startTrim = Math.floor((remainingLength - 3) / 2); - const endTrim = Math.ceil((remainingLength - 3) / 2); - trimmedURL += `/${part.substr(0, startTrim)}...${part.slice(-endTrim)}`; - break; - } - } - - return trimmedURL; - } else { - const leftSideLength = Math.floor(maxLength / 2); - const rightSideLength = maxLength - leftSideLength; - - const leftSide = url.slice(0, leftSideLength); - const rightSide = url.slice(-rightSideLength); - - return leftSide + ellipsis + rightSide; - } -} - -/** - * @param {String} input - value to encode for URI - * @returns {String} value encoded for URI - */ -export function encodeURIComponentPermissive(input) { - return encodeURIComponent(input) - .replaceAll("%2C", ",") - .replaceAll("%3A", ":") - .replaceAll("%2F", "/") -} - -export function encodeSearchParamEntries([k, v]) { - return `${encodeURIComponentPermissive(k)}=${encodeURIComponentPermissive(v)}` -} - -export function isSearchEntryDefined([_key, value]) { - return value !== undefined -} - -export function stringifySearch(searchRecord) { - const definedSearchEntries = Object.entries(searchRecord || {}).map(stringifySearchEntry).filter(isSearchEntryDefined) - - const encodedSearchEntries = definedSearchEntries.map(encodeSearchParamEntries) - - return encodedSearchEntries.length ? `?${encodedSearchEntries.join('&')}` : '' -} - -export function stringifySearchEntry([key, value]) { - const isEmptyObjectOrArray = typeof value === 'object' && value !== null && Object.entries(value).length === 0; - if ( - value === undefined || - value === null || - isEmptyObjectOrArray - ) { - return [key, undefined] - } - - return [key, JsonURL.stringify(value)] -} - -export function parseSearchFragment(searchStringFragment) { - if (searchStringFragment === '') { - return null - } - const fragmentWithReEncodedSymbols = - searchStringFragment - .replaceAll('=',encodeURIComponent('=')) - .replaceAll('#',encodeURIComponent('#')) - try { - return JsonURL.parse(fragmentWithReEncodedSymbols) - } catch (e) { - console.error(`Failed to parse URL fragment ${fragmentWithReEncodedSymbols}`) - return null - } -} - -export function parseSearch(searchString) { - const urlSearchParams = new URLSearchParams(searchString); - const searchRecord = {}; - urlSearchParams.forEach((v, k) => searchRecord[k] = parseSearchFragment(v)) - return searchRecord; -} \ No newline at end of file diff --git a/assets/js/dashboard/util/url.ts b/assets/js/dashboard/util/url.ts new file mode 100644 index 000000000..6ed7be4dd --- /dev/null +++ b/assets/js/dashboard/util/url.ts @@ -0,0 +1,149 @@ +/* @format */ +import JsonURL from '@jsonurl/jsonurl' +import { PlausibleSite } from '../site-context' + +export function apiPath(site: PlausibleSite, path = ''): string { + return `/api/stats/${encodeURIComponent(site.domain)}${path}/` +} + +export function externalLinkForPage( + domain: PlausibleSite['domain'], + page: string +): string { + const domainURL = new URL(`https://${domain}`) + return `https://${domainURL.host}${page}` +} + +export function isValidHttpUrl(input: string): boolean { + let url + + try { + url = new URL(input) + } catch (_) { + return false + } + + return url.protocol === 'http:' || url.protocol === 'https:' +} + +export function trimURL(url: string, maxLength: number): string { + if (url.length <= maxLength) { + return url + } + + const ellipsis = '...' + + if (isValidHttpUrl(url)) { + const [protocol, restURL] = url.split('://') + const parts = restURL.split('/') + + const host = parts.shift() || '' + if (host.length > maxLength - 5) { + return `${protocol}://${host.substr(0, maxLength - 5)}${ellipsis}${restURL.slice(-maxLength + 5)}` + } + + let remainingLength = maxLength - host.length - 5 + let trimmedURL = `${protocol}://${host}` + + for (const part of parts) { + if (part.length <= remainingLength) { + trimmedURL += '/' + part + remainingLength -= part.length + 1 + } else { + const startTrim = Math.floor((remainingLength - 3) / 2) + const endTrim = Math.ceil((remainingLength - 3) / 2) + trimmedURL += `/${part.substr(0, startTrim)}...${part.slice(-endTrim)}` + break + } + } + + return trimmedURL + } else { + const leftSideLength = Math.floor(maxLength / 2) + const rightSideLength = maxLength - leftSideLength + + const leftSide = url.slice(0, leftSideLength) + const rightSide = url.slice(-rightSideLength) + + return leftSide + ellipsis + rightSide + } +} + +export function encodeURIComponentPermissive(input: string): string { + return ( + encodeURIComponent(input) + /* @ts-expect-error API supposedly not present in compilation target */ + .replaceAll('%2C', ',') + .replaceAll('%3A', ':') + .replaceAll('%2F', '/') + ) +} + +export function encodeSearchParamEntries([k, v]: [string, string]): string { + return `${encodeURIComponentPermissive(k)}=${encodeURIComponentPermissive(v)}` +} + +export function isSearchEntryDefined( + entry: [string, undefined | string] +): entry is [string, string] { + return entry[1] !== undefined +} + +export function stringifySearch( + searchRecord: Record +): '' | string { + const definedSearchEntries = Object.entries( + searchRecord || ({} as Record) + ) + .map(stringifySearchEntry) + .filter(isSearchEntryDefined) + + const encodedSearchEntries = definedSearchEntries.map( + encodeSearchParamEntries + ) + + return encodedSearchEntries.length ? `?${encodedSearchEntries.join('&')}` : '' +} + +export function stringifySearchEntry([key, value]: [string, unknown]): [ + string, + undefined | string +] { + const isEmptyObjectOrArray = + typeof value === 'object' && + value !== null && + Object.entries(value).length === 0 + if (value === undefined || value === null || isEmptyObjectOrArray) { + return [key, undefined] + } + + return [key, JsonURL.stringify(value)] +} + +export function parseSearchFragment( + searchStringFragment: string +): null | unknown { + if (searchStringFragment === '') { + return null + } + const fragmentWithReEncodedSymbols = searchStringFragment + /* @ts-expect-error API supposedly not present in compilation target */ + .replaceAll('=', encodeURIComponent('=')) + .replaceAll('#', encodeURIComponent('#')) + try { + return JsonURL.parse(fragmentWithReEncodedSymbols) + } catch (error) { + console.error( + `Failed to parse URL fragment ${fragmentWithReEncodedSymbols}`, + error + ) + return null + } +} + +export function parseSearch(searchString: string): Record { + const urlSearchParams = new URLSearchParams(searchString) + const searchRecord: Record = {} + urlSearchParams.forEach((v, k) => (searchRecord[k] = parseSearchFragment(v))) + return searchRecord +} diff --git a/assets/js/liveview/dropdown.js b/assets/js/liveview/dropdown.js index 18175bb3f..d2d662de0 100644 --- a/assets/js/liveview/dropdown.js +++ b/assets/js/liveview/dropdown.js @@ -15,6 +15,6 @@ export default () => ({ if (! this.open) return this.open = false - focusAfter && focusAfter.focus() + focusAfter?.focus() }, }) diff --git a/assets/js/polyfills/closest.js b/assets/js/polyfills/closest.js index 64f482366..7295f7ace 100644 --- a/assets/js/polyfills/closest.js +++ b/assets/js/polyfills/closest.js @@ -3,6 +3,7 @@ if (window.Element && !Element.prototype.closest) { function(s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s), i, + // eslint-disable-next-line @typescript-eslint/no-this-alias el = this; do { i = matches.length; diff --git a/assets/package-lock.json b/assets/package-lock.json index d7e079539..6a55e4082 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -23,7 +23,7 @@ "chart.js": "^3.3.2", "chartjs-plugin-datalabels": "^2.2.0", "classnames": "^2.3.1", - "datamaps": "^0.5.9", + "d3": "^7.9.0", "dayjs": "^1.11.7", "iframe-resizer": "^4.3.2", "phoenix": "^1.7.2", @@ -37,18 +37,29 @@ "react-popper": "^2.3.0", "react-router-dom": "^6.25.1", "react-transition-group": "^4.4.2", - "url-search-params-polyfill": "^8.2.5" + "topojson-client": "^3.1.0", + "url-search-params-polyfill": "^8.2.5", + "visionscarto-world-atlas": "^1.0.0" }, "devDependencies": { + "@types/classnames": "^2.3.1", + "@types/d3": "^7.4.3", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/topojson-client": "^3.1.4", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-react": "^7.35.0", "eslint-plugin-react-hooks": "^4.6.2", "prettier": "^3.3.3", "stylelint": "^16.8.1", - "stylelint-config-standard": "^36.0.1" + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.5.4" } }, "node_modules/@alloc/quick-lru": { @@ -220,18 +231,6 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", @@ -500,9 +499,274 @@ "react": "^18.0.0" } }, + "node_modules/@types/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", + "deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "classnames": "*" + } + }, "node_modules/@types/d3": { - "version": "3.5.38", - "license": "MIT" + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "dev": true + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "dev": true + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dev": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "dev": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "dev": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dev": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -510,6 +774,268 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/topojson-client": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.4.tgz", + "integrity": "sha512-Ntf3ZSetMYy7z3PrVCvcqmdRoVhgKA9UKN0ZuuZf8Ts2kcyL4qK34IXBs6qO5fem62EK4k03PtkJPVoroVu4/w==", + "dev": true, + "dependencies": { + "@types/geojson": "*", + "@types/topojson-specification": "*" + } + }, + "node_modules/@types/topojson-specification": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.5.tgz", + "integrity": "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", + "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.0.1", + "@typescript-eslint/type-utils": "8.0.1", + "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", + "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.0.1", + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/typescript-estree": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", + "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", + "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.0.1", + "@typescript-eslint/utils": "8.0.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", + "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", + "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/visitor-keys": "8.0.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", + "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.0.1", + "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/typescript-estree": "8.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", + "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -533,7 +1059,9 @@ }, "node_modules/acorn": { "version": "7.4.1", + "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -865,30 +1393,6 @@ "node": ">=8" } }, - "node_modules/brfs": { - "version": "1.6.1", - "license": "MIT", - "dependencies": { - "quote-stream": "^1.0.1", - "resolve": "^1.1.5", - "static-module": "^2.2.0", - "through2": "^2.0.0" - }, - "bin": { - "brfs": "bin/cmd.js" - } - }, - "node_modules/buffer-equal": { - "version": "0.0.1", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "license": "MIT" - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -1013,29 +1517,16 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true }, - "node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" } }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", + "node_modules/concat-map": { + "version": "0.0.1", "license": "MIT" }, "node_modules/cosmiconfig": { @@ -1114,18 +1605,385 @@ "license": "MIT" }, "node_modules/d3": { - "version": "3.5.17", - "license": "BSD-3-Clause" - }, - "node_modules/d3-geo-projection": { - "version": "0.2.16", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "dependencies": { - "brfs": "^1.3.0" + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/d3-queue": { - "version": "2.0.3", - "license": "BSD-3-Clause" + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1183,15 +2041,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/datamaps": { - "version": "0.5.9", - "license": "MIT", - "dependencies": { - "@types/d3": "3.5.38", - "d3": "^3.5.6", - "topojson": "^1.6.19" - } - }, "node_modules/dayjs": { "version": "1.11.7", "license": "MIT" @@ -1243,6 +2092,7 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "dev": true, "license": "MIT" }, "node_modules/define-data-property": { @@ -1279,6 +2129,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "license": "Apache-2.0", @@ -1320,18 +2178,24 @@ "csstype": "^3.0.2" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/emoji-regex": { "version": "9.2.2", "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -1535,86 +2399,6 @@ "node": ">=0.8.0" } }, - "node_modules/escodegen": { - "version": "1.9.1", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/esprima": { - "version": "3.1.3", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -1702,6 +2486,31 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", @@ -1897,6 +2706,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -1953,18 +2774,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/globals": { "version": "13.20.0", "dev": true, @@ -1979,23 +2788,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=8" + "node": ">=0.4.0" } }, "node_modules/espree": { @@ -2027,29 +2827,6 @@ "node": ">=0.4.0" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2084,22 +2861,12 @@ }, "node_modules/esutils": { "version": "2.0.3", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/falafel": { - "version": "2.2.5", - "license": "MIT", - "dependencies": { - "acorn": "^7.1.1", - "isarray": "^2.0.1" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -2138,6 +2905,7 @@ }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "dev": true, "license": "MIT" }, "node_modules/fast-uri": { @@ -2315,6 +3083,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "dev": true, @@ -2431,22 +3211,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "dev": true, @@ -2535,13 +3311,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/iconv-lite": { - "version": "0.2.11", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/iframe-resizer": { "version": "4.3.2", "license": "MIT", @@ -2617,6 +3386,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "dev": true, @@ -3003,6 +3780,7 @@ }, "node_modules/isarray": { "version": "2.0.5", + "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -3210,13 +3988,6 @@ "loose-envify": "cli.js" } }, - "node_modules/magic-string": { - "version": "0.22.5", - "license": "MIT", - "dependencies": { - "vlq": "^0.2.2" - } - }, "node_modules/mathml-tag-names": { "version": "2.1.3", "dev": true, @@ -3244,20 +4015,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-source-map": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "source-map": "^0.5.6" - } - }, - "node_modules/merge-source-map/node_modules/source-map": { - "version": "0.5.7", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/merge2": { "version": "1.4.1", "license": "MIT", @@ -3296,6 +4053,7 @@ }, "node_modules/minimist": { "version": "1.2.6", + "dev": true, "license": "MIT" }, "node_modules/ms": { @@ -3480,13 +4238,6 @@ "wrappy": "1" } }, - "node_modules/optimist": { - "version": "0.3.7", - "license": "MIT/X11", - "dependencies": { - "wordwrap": "~0.0.2" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3839,10 +4590,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -3879,18 +4626,6 @@ ], "license": "MIT" }, - "node_modules/quote-stream": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "buffer-equal": "0.0.1", - "minimist": "^1.1.3", - "through2": "^2.0.0" - }, - "bin": { - "quote-stream": "bin/cmd.js" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4010,23 +4745,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/readdirp": { "version": "3.6.0", "license": "MIT", @@ -4114,6 +4832,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "license": "MIT", @@ -4136,6 +4863,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "funding": [ @@ -4179,10 +4911,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "license": "MIT" - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -4200,6 +4928,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -4248,28 +4981,6 @@ "node": ">= 0.4" } }, - "node_modules/shallow-copy": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/shapefile": { - "version": "0.3.1", - "license": "BSD-3-Clause", - "dependencies": { - "d3-queue": "1", - "iconv-lite": "0.2", - "optimist": "0.3" - }, - "bin": { - "dbfcat": "bin/dbfcat", - "shp2json": "bin/shp2json", - "shpcat": "bin/shpcat" - } - }, - "node_modules/shapefile/node_modules/d3-queue": { - "version": "1.2.3", - "license": "BSD-3-Clause" - }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -4378,14 +5089,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -4394,106 +5097,6 @@ "node": ">=0.10.0" } }, - "node_modules/static-eval": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "escodegen": "^1.11.1" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "version": "1.14.3", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/estraverse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-eval/node_modules/levn": { - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/optionator": { - "version": "0.8.3", - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/prelude-ls": { - "version": "1.1.2", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/type-check": { - "version": "0.3.2", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-module": { - "version": "2.2.5", - "license": "MIT", - "dependencies": { - "concat-stream": "~1.6.0", - "convert-source-map": "^1.5.1", - "duplexer2": "~0.1.4", - "escodegen": "~1.9.0", - "falafel": "^2.1.0", - "has": "^1.0.1", - "magic-string": "^0.22.4", - "merge-source-map": "1.0.4", - "object-inspect": "~1.4.0", - "quote-stream": "~1.0.2", - "readable-stream": "~2.3.3", - "shallow-copy": "~0.0.1", - "static-eval": "^2.0.0", - "through2": "~2.0.3" - } - }, - "node_modules/static-module/node_modules/object-inspect": { - "version": "1.4.1", - "license": "MIT" - }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "dev": true, @@ -4505,13 +5108,6 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5015,6 +5611,15 @@ "node": ">=14.0.0" } }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -5039,14 +5644,6 @@ "node": ">=0.8" } }, - "node_modules/through2": { - "version": "2.0.5", - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5058,23 +5655,34 @@ "node": ">=8.0" } }, - "node_modules/topojson": { - "version": "1.6.27", - "license": "BSD-3-Clause", + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", "dependencies": { - "d3": "3", - "d3-geo-projection": "0.2", - "d3-queue": "2", - "optimist": "0.3", - "rw": "1", - "shapefile": "0.3" + "commander": "2" }, "bin": { - "topojson": "bin/topojson", - "topojson-geojson": "bin/topojson-geojson", - "topojson-group": "bin/topojson-group", - "topojson-merge": "bin/topojson-merge", - "topojson-svg": "bin/topojson-svg" + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" } }, "node_modules/ts-interface-checker": { @@ -5191,9 +5799,18 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray": { - "version": "0.0.6", - "license": "MIT" + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, "node_modules/unbox-primitive": { "version": "1.0.2", @@ -5228,9 +5845,10 @@ "version": "1.0.2", "license": "MIT" }, - "node_modules/vlq": { - "version": "0.2.3", - "license": "MIT" + "node_modules/visionscarto-world-atlas": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/visionscarto-world-atlas/-/visionscarto-world-atlas-1.0.0.tgz", + "integrity": "sha512-jHl/NQgASfw5ZML3cnbjdfr/gXK5zO8a2xKSoCVe+5+EsIaO9tMTh7SsnfhESnCpZ+Xb6XBeU91wiuyERUPshQ==" }, "node_modules/warning": { "version": "4.0.3", @@ -5335,17 +5953,11 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "0.0.3", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "license": "ISC" @@ -5363,13 +5975,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/assets/package.json b/assets/package.json index 9daff5d86..7aa67c735 100644 --- a/assets/package.json +++ b/assets/package.json @@ -3,11 +3,12 @@ "version": "1.4.0", "license": "AGPL-3.0-or-later", "scripts": { - "format": "prettier --write {css,js}/**", - "check-format": "prettier --check {css,js}/**", + "format": "prettier --write", + "check-format": "prettier --check **/*.{js,css,ts,tsx} --require-pragma", "eslint": "eslint js/**", "stylelint": "stylelint css/**", - "lint": "npm run eslint && npm run stylelint" + "lint": "npm run eslint && npm run stylelint", + "typecheck": "tsc --noEmit --pretty" }, "dependencies": { "@headlessui/react": "^1.7.10", @@ -24,7 +25,7 @@ "chart.js": "^3.3.2", "chartjs-plugin-datalabels": "^2.2.0", "classnames": "^2.3.1", - "datamaps": "^0.5.9", + "d3": "^7.9.0", "dayjs": "^1.11.7", "iframe-resizer": "^4.3.2", "phoenix": "^1.7.2", @@ -38,18 +39,29 @@ "react-popper": "^2.3.0", "react-router-dom": "^6.25.1", "react-transition-group": "^4.4.2", - "url-search-params-polyfill": "^8.2.5" + "topojson-client": "^3.1.0", + "url-search-params-polyfill": "^8.2.5", + "visionscarto-world-atlas": "^1.0.0" }, "devDependencies": { + "@types/classnames": "^2.3.1", + "@types/d3": "^7.4.3", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/topojson-client": "^3.1.4", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-react": "^7.35.0", "eslint-plugin-react-hooks": "^4.6.2", "prettier": "^3.3.3", "stylelint": "^16.8.1", - "stylelint-config-standard": "^36.0.1" + "stylelint-config-standard": "^36.0.1", + "typescript": "^5.5.4" }, "name": "assets" } diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index ec7657dab..1f8213e67 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -3,7 +3,7 @@ const plugin = require('tailwindcss/plugin') module.exports = { content: [ - "./js/**/*.js", + "./js/**/*.{js,ts,tsx}", "../lib/*_web.ex", "../lib/*_web/**/*.*ex", "../extra/**/*.*ex", diff --git a/assets/tsconfig.json b/assets/tsconfig.json new file mode 100644 index 000000000..ce902c9f2 --- /dev/null +++ b/assets/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "jsx": "react", + "target": "es2017", + "module": "commonjs", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} diff --git a/config/config.exs b/config/config.exs index 7a610cfb2..ee0db8b24 100644 --- a/config/config.exs +++ b/config/config.exs @@ -26,7 +26,7 @@ config :esbuild, ] config :tailwind, - version: "3.3.3", + version: "3.4.7", default: [ args: ~w( --config=tailwind.config.js diff --git a/config/dev.exs b/config/dev.exs index e1ede12af..997825eb2 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -8,6 +8,7 @@ config :plausible, PlausibleWeb.Endpoint, watchers: [ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}, + npm: ["--prefix", "assets", "run", "typecheck", "--", "--watch"], npm: [ "run", "deploy", diff --git a/mix.exs b/mix.exs index 423b54974..167a00ced 100644 --- a/mix.exs +++ b/mix.exs @@ -155,6 +155,7 @@ defmodule Plausible.MixProject do "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test", "clean_clickhouse"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.typecheck": ["cmd npm --prefix assets run typecheck"], "assets.build": [ "tailwind default", "esbuild default"